geohash-kit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ /** Generate multi-precision g-tag ladder for Nostr event publishing. */
2
+ export declare function createGTagLadder(geohash: string, minPrecision?: number): string[][];
3
+ /**
4
+ * Extract and parse g tags from a Nostr event's tag array.
5
+ * Invalid geohashes (containing non-base32 characters) are silently filtered out,
6
+ * since relay data is untrusted. Empty strings are also excluded.
7
+ */
8
+ export declare function parseGTags(tags: string[][]): Array<{
9
+ geohash: string;
10
+ precision: number;
11
+ }>;
12
+ /** Return the highest-precision g tag from an event's tag array. */
13
+ export declare function bestGeohash(tags: string[][]): string | undefined;
14
+ /** Expand geohash into concentric rings of neighbours. */
15
+ export declare function expandRings(hash: string, rings?: number): string[][];
16
+ /** Generate a #g filter for Nostr REQ from coordinates and radius. */
17
+ export declare function createGTagFilter(lat: number, lon: number, radiusMetres: number): {
18
+ '#g': string[];
19
+ };
20
+ /** Generate a #g filter from an existing geohash set. */
21
+ export declare function createGTagFilterFromGeohashes(hashes: string[]): {
22
+ '#g': string[];
23
+ };
24
+ /** Convenience: encode + expand rings + flatten to filter. */
25
+ export declare function nearbyFilter(lat: number, lon: number, options?: {
26
+ precision?: number;
27
+ rings?: number;
28
+ }): {
29
+ '#g': string[];
30
+ };
31
+ //# sourceMappingURL=nostr.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nostr.d.ts","sourceRoot":"","sources":["../src/nostr.ts"],"names":[],"mappings":"AAkBA,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,SAAI,GAAG,MAAM,EAAE,EAAE,CAM9E;AAID;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAI1F;AAED,oEAAoE;AACpE,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,MAAM,GAAG,SAAS,CAIhE;AAID,0DAA0D;AAC1D,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,MAAM,EAAE,EAAE,CAqB/D;AAID,sEAAsE;AACtE,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,MAAM,GACnB;IAAE,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CASpB;AAED,yDAAyD;AACzD,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG;IAAE,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAElF;AAED,8DAA8D;AAC9D,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAOpB"}
package/dist/nostr.js ADDED
@@ -0,0 +1,87 @@
1
+ // geohash-kit/nostr — Nostr g-tag utilities
2
+ import { encode, neighbours, radiusToPrecision } from './core.js';
3
+ // --- Validation ---
4
+ const BASE32 = '0123456789bcdefghjkmnpqrstuvwxyz';
5
+ function isValidGeohash(hash) {
6
+ if (hash.length === 0)
7
+ return false;
8
+ for (const ch of hash) {
9
+ if (!BASE32.includes(ch))
10
+ return false;
11
+ }
12
+ return true;
13
+ }
14
+ // --- Publishing (event tags) ---
15
+ /** Generate multi-precision g-tag ladder for Nostr event publishing. */
16
+ export function createGTagLadder(geohash, minPrecision = 1) {
17
+ const tags = [];
18
+ for (let p = Math.max(1, minPrecision); p <= geohash.length; p++) {
19
+ tags.push(['g', geohash.slice(0, p)]);
20
+ }
21
+ return tags;
22
+ }
23
+ // --- Parsing ---
24
+ /**
25
+ * Extract and parse g tags from a Nostr event's tag array.
26
+ * Invalid geohashes (containing non-base32 characters) are silently filtered out,
27
+ * since relay data is untrusted. Empty strings are also excluded.
28
+ */
29
+ export function parseGTags(tags) {
30
+ return tags
31
+ .filter((t) => t[0] === 'g' && t[1] && isValidGeohash(t[1]))
32
+ .map((t) => ({ geohash: t[1], precision: t[1].length }));
33
+ }
34
+ /** Return the highest-precision g tag from an event's tag array. */
35
+ export function bestGeohash(tags) {
36
+ const parsed = parseGTags(tags);
37
+ if (parsed.length === 0)
38
+ return undefined;
39
+ return parsed.reduce((best, curr) => curr.precision > best.precision ? curr : best).geohash;
40
+ }
41
+ // --- Ring expansion ---
42
+ /** Expand geohash into concentric rings of neighbours. */
43
+ export function expandRings(hash, rings = 1) {
44
+ const result = [[hash]];
45
+ const seen = new Set([hash]);
46
+ for (let ring = 1; ring <= rings; ring++) {
47
+ const prevRing = result[ring - 1];
48
+ const candidates = new Set();
49
+ for (const cell of prevRing) {
50
+ const n = neighbours(cell);
51
+ for (const adj of Object.values(n)) {
52
+ if (!seen.has(adj)) {
53
+ candidates.add(adj);
54
+ }
55
+ }
56
+ }
57
+ const ringCells = Array.from(candidates);
58
+ for (const c of ringCells)
59
+ seen.add(c);
60
+ result.push(ringCells);
61
+ }
62
+ return result;
63
+ }
64
+ // --- Filter generation (subscribing) ---
65
+ /** Generate a #g filter for Nostr REQ from coordinates and radius. */
66
+ export function createGTagFilter(lat, lon, radiusMetres) {
67
+ const precision = radiusToPrecision(radiusMetres);
68
+ const hash = encode(lat, lon, precision);
69
+ // Expand one ring to cover cell boundaries
70
+ const rings = expandRings(hash, 1);
71
+ const allHashes = rings.flat();
72
+ return { '#g': [...new Set(allHashes)] };
73
+ }
74
+ /** Generate a #g filter from an existing geohash set. */
75
+ export function createGTagFilterFromGeohashes(hashes) {
76
+ return { '#g': [...new Set(hashes)] };
77
+ }
78
+ /** Convenience: encode + expand rings + flatten to filter. */
79
+ export function nearbyFilter(lat, lon, options) {
80
+ const precision = options?.precision ?? 5;
81
+ const ringCount = options?.rings ?? 1;
82
+ const hash = encode(lat, lon, precision);
83
+ const rings = expandRings(hash, ringCount);
84
+ const allHashes = rings.flat();
85
+ return { '#g': [...new Set(allHashes)] };
86
+ }
87
+ //# sourceMappingURL=nostr.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nostr.js","sourceRoot":"","sources":["../src/nostr.ts"],"names":[],"mappings":"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"}
package/llms-full.txt ADDED
@@ -0,0 +1,454 @@
1
+ # geohash-kit — Full API Reference
2
+
3
+ > Modern TypeScript geohash toolkit — encode, decode, cover polygons, and build Nostr filters.
4
+
5
+ Zero dependencies. ESM-only. TypeScript native.
6
+
7
+ Repository: https://github.com/TheCryptoDonkey/geohash-kit
8
+ Licence: MIT
9
+
10
+ ## Install
11
+
12
+ ```
13
+ npm install geohash-kit
14
+ ```
15
+
16
+ ## Imports
17
+
18
+ ```typescript
19
+ // Barrel (everything)
20
+ import { encode, polygonToGeohashes, createGTagLadder } from 'geohash-kit'
21
+
22
+ // Subpath (tree-shakeable)
23
+ import { encode, decode, bounds } from 'geohash-kit/core'
24
+ import { polygonToGeohashes } from 'geohash-kit/coverage'
25
+ import { createGTagLadder } from 'geohash-kit/nostr'
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Types
31
+
32
+ ```typescript
33
+ interface GeohashBounds {
34
+ minLat: number
35
+ maxLat: number
36
+ minLon: number
37
+ maxLon: number
38
+ }
39
+
40
+ type Direction = 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'nw'
41
+
42
+ interface CoverageOptions {
43
+ minPrecision?: number // default 1 — coarsest exploration level
44
+ maxPrecision?: number // default 9 — finest exploration level
45
+ maxCells?: number // default 500 — hard cap, triggers threshold tightening
46
+ mergeThreshold?: number // default 1.0 — fraction of children to merge parent (0–1)
47
+ }
48
+
49
+ interface GeohashGeoJSON {
50
+ type: 'FeatureCollection'
51
+ features: Array<{
52
+ type: 'Feature'
53
+ geometry: { type: 'Polygon'; coordinates: [number, number][][] }
54
+ properties: { geohash: string; precision: number }
55
+ }>
56
+ }
57
+
58
+ interface GeoJSONPolygon {
59
+ type: 'Polygon'
60
+ coordinates: number[][][]
61
+ }
62
+
63
+ interface GeoJSONMultiPolygon {
64
+ type: 'MultiPolygon'
65
+ coordinates: number[][][][]
66
+ }
67
+
68
+ type PolygonInput = [number, number][] | GeoJSONPolygon | GeoJSONMultiPolygon
69
+ ```
70
+
71
+ ---
72
+
73
+ ## geohash-kit/core
74
+
75
+ ### encode(lat, lon, precision?)
76
+
77
+ Encode latitude/longitude to a geohash string. Default precision 5 (~4.9km).
78
+
79
+ ```typescript
80
+ encode(lat: number, lon: number, precision?: number): string
81
+
82
+ encode(51.5074, -0.1278) // 'gcpvj'
83
+ encode(51.5074, -0.1278, 7) // 'gcpvjbs'
84
+ encode(0, 0, 5) // 's0000'
85
+ ```
86
+
87
+ ### decode(hash)
88
+
89
+ Decode a geohash to its centre point with error margins.
90
+
91
+ ```typescript
92
+ decode(hash: string): { lat: number; lon: number; error: { lat: number; lon: number } }
93
+
94
+ decode('gcpvj') // { lat: 51.51, lon: -0.13, error: { lat: 0.02, lon: 0.02 } }
95
+ ```
96
+
97
+ ### bounds(hash)
98
+
99
+ Get the bounding rectangle of a geohash cell.
100
+
101
+ ```typescript
102
+ bounds(hash: string): GeohashBounds
103
+
104
+ bounds('gcpvj') // { minLat: 51.50, maxLat: 51.52, minLon: -0.15, maxLon: -0.10 }
105
+ bounds('') // { minLat: -90, maxLat: 90, minLon: -180, maxLon: 180 }
106
+ ```
107
+
108
+ ### children(hash)
109
+
110
+ Get the 32 children of a geohash at the next precision level.
111
+
112
+ ```typescript
113
+ children(hash: string): string[]
114
+
115
+ children('g') // ['g0', 'g1', ..., 'gz'] (32 entries)
116
+ children('') // ['0', '1', ..., 'z'] (32 top-level cells)
117
+ ```
118
+
119
+ ### neighbour(hash, direction)
120
+
121
+ Get a single adjacent geohash cell in the given direction.
122
+
123
+ ```typescript
124
+ neighbour(hash: string, direction: Direction): string
125
+
126
+ neighbour('gcpvj', 'n') // north neighbour at same precision
127
+ neighbour('gcpvj', 'ne') // north-east neighbour
128
+ ```
129
+
130
+ ### neighbours(hash)
131
+
132
+ Get all 8 adjacent geohash cells.
133
+
134
+ ```typescript
135
+ neighbours(hash: string): Record<Direction, string>
136
+
137
+ neighbours('gcpvj') // { n: '...', ne: '...', e: '...', se: '...', s: '...', sw: '...', w: '...', nw: '...' }
138
+ ```
139
+
140
+ ### contains(a, b)
141
+
142
+ Check if two geohashes overlap (bidirectional prefix containment).
143
+
144
+ ```typescript
145
+ contains(a: string, b: string): boolean
146
+
147
+ contains('gcvd', 'gcvdn') // true (gcvd contains gcvdn)
148
+ contains('gcvdn', 'gcvd') // true (gcvd contains gcvdn)
149
+ contains('gcvdn', 'gcvdp') // false (siblings)
150
+ ```
151
+
152
+ ### matchesAny(hash, candidates)
153
+
154
+ Check if a geohash matches any candidate in a multi-precision set.
155
+
156
+ ```typescript
157
+ matchesAny(hash: string, candidates: string[]): boolean
158
+
159
+ matchesAny('gcvdn', ['gcpvj', 'gcvd', 'u10h']) // true (gcvd is prefix)
160
+ matchesAny('gcvdn', ['gcpvj', 'u10h']) // false
161
+ ```
162
+
163
+ ### distance(hashA, hashB)
164
+
165
+ Haversine distance in metres between centres of two geohash cells.
166
+
167
+ ```typescript
168
+ distance(hashA: string, hashB: string): number
169
+
170
+ distance('gcpvj', 'u09tu') // ~340000 (London to Paris)
171
+ distance('gcpvj', 'gcpvj') // 0
172
+ ```
173
+
174
+ ### distanceFromCoords(lat1, lon1, lat2, lon2)
175
+
176
+ Haversine distance in metres between two coordinate pairs.
177
+
178
+ ```typescript
179
+ distanceFromCoords(lat1: number, lon1: number, lat2: number, lon2: number): number
180
+
181
+ distanceFromCoords(51.5074, -0.1278, 48.8566, 2.3522) // ~340000
182
+ ```
183
+
184
+ ### radiusToPrecision(metres)
185
+
186
+ Optimal geohash precision for a given search radius in metres.
187
+
188
+ ```typescript
189
+ radiusToPrecision(metres: number): number
190
+
191
+ radiusToPrecision(5_000_000) // 1
192
+ radiusToPrecision(2_500) // 5
193
+ radiusToPrecision(80) // 7
194
+ radiusToPrecision(2) // 9
195
+ ```
196
+
197
+ ### precisionToRadius(precision)
198
+
199
+ Approximate cell radius in metres for a given precision level.
200
+
201
+ ```typescript
202
+ precisionToRadius(precision: number): number
203
+
204
+ precisionToRadius(1) // 2_500_000
205
+ precisionToRadius(5) // 2_400
206
+ precisionToRadius(9) // 2.4
207
+ ```
208
+
209
+ Precision table:
210
+
211
+ | Precision | Cell width | Cell height | Approx. radius |
212
+ |-----------|-----------|-------------|----------------|
213
+ | 1 | ~5,000 km | ~5,000 km | ~2,500 km |
214
+ | 2 | ~1,250 km | ~625 km | ~630 km |
215
+ | 3 | ~156 km | ~156 km | ~78 km |
216
+ | 4 | ~39.1 km | ~19.5 km | ~20 km |
217
+ | 5 | ~4.89 km | ~4.89 km | ~2.4 km |
218
+ | 6 | ~1.22 km | ~0.61 km | ~610 m |
219
+ | 7 | ~153 m | ~153 m | ~76 m |
220
+ | 8 | ~38.2 m | ~19.1 m | ~19 m |
221
+ | 9 | ~4.77 m | ~4.77 m | ~2.4 m |
222
+
223
+ ---
224
+
225
+ ## geohash-kit/coverage
226
+
227
+ ### polygonToGeohashes(polygon, options?)
228
+
229
+ Convert a polygon to a set of covering geohashes at variable precision.
230
+ Uses adaptive threshold recursive subdivision with automatic threshold
231
+ tightening to respect maxCells budget.
232
+
233
+ ```typescript
234
+ polygonToGeohashes(
235
+ polygon: [number, number][] | GeoJSONPolygon | GeoJSONMultiPolygon,
236
+ options?: CoverageOptions
237
+ ): string[]
238
+
239
+ // Cover central London (coordinate array)
240
+ polygonToGeohashes([[-0.15, 51.50], [-0.10, 51.50], [-0.10, 51.52], [-0.15, 51.52]])
241
+
242
+ // With options
243
+ polygonToGeohashes(polygon, { maxCells: 100, mergeThreshold: 0.7 })
244
+
245
+ // GeoJSON Polygon input
246
+ polygonToGeohashes({
247
+ type: 'Polygon',
248
+ coordinates: [[[-0.15, 51.50], [-0.10, 51.50], [-0.10, 51.52], [-0.15, 51.52], [-0.15, 51.50]]]
249
+ })
250
+
251
+ // GeoJSON MultiPolygon input (results merged and deduplicated)
252
+ polygonToGeohashes({
253
+ type: 'MultiPolygon',
254
+ coordinates: [
255
+ [[[-0.15, 51.50], [-0.10, 51.50], [-0.10, 51.52], [-0.15, 51.52], [-0.15, 51.50]]],
256
+ [[[-0.08, 51.50], [-0.03, 51.50], [-0.03, 51.52], [-0.08, 51.52], [-0.08, 51.50]]],
257
+ ]
258
+ })
259
+ ```
260
+
261
+ ### geohashesToGeoJSON(hashes)
262
+
263
+ Convert geohash set to GeoJSON FeatureCollection for map rendering.
264
+
265
+ ```typescript
266
+ geohashesToGeoJSON(hashes: string[]): GeohashGeoJSON
267
+
268
+ const geojson = geohashesToGeoJSON(['gcpvj', 'gcpvm'])
269
+ // { type: 'FeatureCollection', features: [{ type: 'Feature', geometry: { type: 'Polygon', ... }, properties: { geohash: 'gcpvj', precision: 5 } }, ...] }
270
+ ```
271
+
272
+ ### geohashesToConvexHull(hashes)
273
+
274
+ Reconstruct a convex polygon from a geohash set (Andrew's monotone chain).
275
+
276
+ ```typescript
277
+ geohashesToConvexHull(hashes: string[]): [number, number][]
278
+
279
+ const hull = geohashesToConvexHull(['gcpvj', 'gcpvm', 'gcpvn'])
280
+ // [[lon, lat], [lon, lat], ...] — convex hull vertices
281
+ ```
282
+
283
+ ### deduplicateGeohashes(hashes, options?)
284
+
285
+ Remove redundant ancestors from a multi-precision geohash set.
286
+ Default mode merges only complete sibling sets (32/32). Pass `{ lossy: true }` to merge near-complete sets (≥30/32) for smaller output at the cost of slight boundary overshoot.
287
+
288
+ ```typescript
289
+ deduplicateGeohashes(hashes: string[], options?: DeduplicateOptions): string[]
290
+
291
+ deduplicateGeohashes(['gcp', 'gcpvj', 'gcpvm', 'u10']) // ['gcp', 'u10']
292
+ deduplicateGeohashes(hashes, { lossy: true }) // more aggressive merging
293
+ ```
294
+
295
+ ### pointInPolygon(point, polygon)
296
+
297
+ Ray-casting point-in-polygon test.
298
+
299
+ ```typescript
300
+ pointInPolygon(point: [number, number], polygon: [number, number][]): boolean
301
+
302
+ pointInPolygon([5, 5], [[0, 0], [10, 0], [10, 10], [0, 10]]) // true
303
+ ```
304
+
305
+ ### boundsOverlapsPolygon(bounds, polygon)
306
+
307
+ Check if a geohash cell's bounds overlap a polygon.
308
+
309
+ ```typescript
310
+ boundsOverlapsPolygon(bounds: GeohashBounds, polygon: [number, number][]): boolean
311
+ ```
312
+
313
+ ### boundsFullyInsidePolygon(bounds, polygon)
314
+
315
+ Check if a geohash cell is fully inside a polygon.
316
+
317
+ ```typescript
318
+ boundsFullyInsidePolygon(bounds: GeohashBounds, polygon: [number, number][]): boolean
319
+ ```
320
+
321
+ ---
322
+
323
+ ## geohash-kit/nostr
324
+
325
+ ### createGTagLadder(geohash, minPrecision?)
326
+
327
+ Generate multi-precision g-tag ladder for Nostr event publishing.
328
+
329
+ ```typescript
330
+ createGTagLadder(geohash: string, minPrecision?: number): string[][]
331
+
332
+ createGTagLadder('gcpvj')
333
+ // [['g','g'], ['g','gc'], ['g','gcp'], ['g','gcpv'], ['g','gcpvj']]
334
+
335
+ createGTagLadder('gcpvj', 3)
336
+ // [['g','gcp'], ['g','gcpv'], ['g','gcpvj']]
337
+ ```
338
+
339
+ ### createGTagFilter(lat, lon, radiusMetres)
340
+
341
+ Generate a #g filter for Nostr REQ from coordinates and radius.
342
+ Encodes location, selects precision from radius, expands to neighbours.
343
+
344
+ ```typescript
345
+ createGTagFilter(lat: number, lon: number, radiusMetres: number): { '#g': string[] }
346
+
347
+ createGTagFilter(51.5074, -0.1278, 5000)
348
+ // { '#g': ['gcpvj', 'gcpvm', 'gcpvn', ...] }
349
+ ```
350
+
351
+ ### createGTagFilterFromGeohashes(hashes)
352
+
353
+ Generate a #g filter from an existing geohash set.
354
+
355
+ ```typescript
356
+ createGTagFilterFromGeohashes(hashes: string[]): { '#g': string[] }
357
+
358
+ createGTagFilterFromGeohashes(['gcpvj', 'gcpvm'])
359
+ // { '#g': ['gcpvj', 'gcpvm'] }
360
+ ```
361
+
362
+ ### expandRings(hash, rings?)
363
+
364
+ Expand geohash into concentric rings of neighbours.
365
+
366
+ ```typescript
367
+ expandRings(hash: string, rings?: number): string[][]
368
+
369
+ expandRings('gcpvj', 1)
370
+ // [['gcpvj'], ['neighbour1', 'neighbour2', ..., 'neighbour8']]
371
+
372
+ expandRings('gcpvj', 2)
373
+ // [['gcpvj'], [8 ring-1 cells], [16 ring-2 cells]]
374
+ ```
375
+
376
+ ### nearbyFilter(lat, lon, options?)
377
+
378
+ Convenience: encode + expand rings + flatten to filter.
379
+
380
+ ```typescript
381
+ nearbyFilter(
382
+ lat: number,
383
+ lon: number,
384
+ options?: { precision?: number; rings?: number }
385
+ ): { '#g': string[] }
386
+
387
+ nearbyFilter(51.5074, -0.1278) // precision 5, 1 ring
388
+ nearbyFilter(51.5074, -0.1278, { precision: 3 }) // precision 3, 1 ring
389
+ nearbyFilter(51.5074, -0.1278, { rings: 2 }) // precision 5, 2 rings
390
+ ```
391
+
392
+ ### parseGTags(tags)
393
+
394
+ Extract and parse g tags from a Nostr event's tag array.
395
+
396
+ ```typescript
397
+ parseGTags(tags: string[][]): Array<{ geohash: string; precision: number }>
398
+
399
+ parseGTags([['g', 'gcpvj'], ['p', 'abc'], ['g', 'gcp']])
400
+ // [{ geohash: 'gcpvj', precision: 5 }, { geohash: 'gcp', precision: 3 }]
401
+ ```
402
+
403
+ ### bestGeohash(tags)
404
+
405
+ Return the highest-precision g tag from an event's tag array.
406
+
407
+ ```typescript
408
+ bestGeohash(tags: string[][]): string | undefined
409
+
410
+ bestGeohash([['g', 'g'], ['g', 'gc'], ['g', 'gcpvj']]) // 'gcpvj'
411
+ bestGeohash([['p', 'abc']]) // undefined
412
+ ```
413
+
414
+ ---
415
+
416
+ ## Nostr Usage Patterns
417
+
418
+ ### Publishing an event with location
419
+
420
+ ```typescript
421
+ import { encode, createGTagLadder } from 'geohash-kit'
422
+
423
+ const geohash = encode(51.5074, -0.1278, 6) // 'gcpvjb'
424
+ const tags = createGTagLadder(geohash)
425
+ // Add tags to your Nostr event:
426
+ // [['g','g'], ['g','gc'], ['g','gcp'], ['g','gcpv'], ['g','gcpvj'], ['g','gcpvjb']]
427
+ ```
428
+
429
+ ### Subscribing to nearby events
430
+
431
+ ```typescript
432
+ import { createGTagFilter } from 'geohash-kit'
433
+
434
+ const filter = createGTagFilter(51.5074, -0.1278, 5000)
435
+ // Use in Nostr REQ: ['REQ', subId, { kinds: [1], ...filter }]
436
+ ```
437
+
438
+ ### Round-trip: publish → subscribe → match
439
+
440
+ ```typescript
441
+ import { encode, createGTagLadder, createGTagFilter } from 'geohash-kit'
442
+
443
+ // Publisher
444
+ const hash = encode(51.5074, -0.1278, 6)
445
+ const eventTags = createGTagLadder(hash)
446
+
447
+ // Subscriber (nearby location)
448
+ const filter = createGTagFilter(51.5080, -0.1270, 2000)
449
+
450
+ // Match: at least one event tag value appears in the filter
451
+ const tagValues = eventTags.map(t => t[1])
452
+ const filterSet = new Set(filter['#g'])
453
+ const matches = tagValues.some(v => filterSet.has(v)) // true
454
+ ```
package/llms.txt ADDED
@@ -0,0 +1,74 @@
1
+ # geohash-kit
2
+
3
+ > Modern TypeScript geohash toolkit — encode, decode, cover polygons, and build Nostr filters.
4
+
5
+ Zero dependencies. ESM-only. Three subpath exports.
6
+
7
+ ## Install
8
+
9
+ npm install geohash-kit
10
+
11
+ ## Subpath Exports
12
+
13
+ ### geohash-kit/core
14
+ Basic geohash operations: encode, decode, bounds, neighbours, distance.
15
+
16
+ - encode(lat, lon, precision?) → geohash string
17
+ - decode(hash) → { lat, lon, error }
18
+ - bounds(hash) → { minLat, maxLat, minLon, maxLon }
19
+ - neighbours(hash) → { n, ne, e, se, s, sw, w, nw }
20
+ - neighbour(hash, direction) → adjacent hash
21
+ - children(hash) → 32 child hashes
22
+ - contains(a, b) → boolean (prefix containment)
23
+ - matchesAny(hash, candidates) → boolean
24
+ - distance(hashA, hashB) → metres
25
+ - distanceFromCoords(lat1, lon1, lat2, lon2) → metres
26
+ - radiusToPrecision(metres) → precision level (1-9)
27
+ - precisionToRadius(precision) → metres
28
+
29
+ ### geohash-kit/coverage
30
+ Polygon-to-geohash coverage with adaptive threshold subdivision.
31
+
32
+ - polygonToGeohashes(polygon, options?) → multi-precision hash set (accepts [lon,lat][], GeoJSON Polygon, or MultiPolygon)
33
+ - geohashesToGeoJSON(hashes) → GeoJSON FeatureCollection
34
+ - geohashesToConvexHull(hashes) → polygon vertices
35
+ - deduplicateGeohashes(hashes, options?) → ancestor-free set ({ lossy: true } for 30/32 merges)
36
+ - pointInPolygon(point, polygon) → boolean
37
+ - boundsOverlapsPolygon(bounds, polygon) → boolean
38
+ - boundsFullyInsidePolygon(bounds, polygon) → boolean
39
+
40
+ ### geohash-kit/nostr
41
+ Nostr-specific g-tag utilities for event publishing and relay subscription.
42
+
43
+ - createGTagLadder(geohash, minPrecision?) → string[][] (event tags)
44
+ - createGTagFilter(lat, lon, radiusMetres) → { "#g": string[] }
45
+ - createGTagFilterFromGeohashes(hashes) → { "#g": string[] }
46
+ - expandRings(hash, rings?) → string[][] (concentric neighbour rings)
47
+ - nearbyFilter(lat, lon, options?) → { "#g": string[] }
48
+ - parseGTags(tags) → { geohash, precision }[]
49
+ - bestGeohash(tags) → string | undefined
50
+
51
+ ## Quick Examples
52
+
53
+ ```typescript
54
+ import { encode, neighbours, polygonToGeohashes, createGTagLadder, createGTagFilter } from 'geohash-kit'
55
+
56
+ // Encode a location
57
+ const hash = encode(51.5074, -0.1278) // 'gcpvj'
58
+
59
+ // Get adjacent cells
60
+ const adj = neighbours(hash) // { n: '...', ne: '...', ... }
61
+
62
+ // Cover a polygon with geohashes
63
+ const coverage = polygonToGeohashes([[-0.15, 51.50], [-0.10, 51.50], [-0.10, 51.52], [-0.15, 51.52]])
64
+
65
+ // Generate Nostr event tags
66
+ const tags = createGTagLadder(hash) // [['g','g'], ['g','gc'], ['g','gcp'], ['g','gcpv'], ['g','gcpvj']]
67
+
68
+ // Generate Nostr subscription filter
69
+ const filter = createGTagFilter(51.5074, -0.1278, 5000) // { '#g': ['gcpvj', 'gcpvm', ...] }
70
+ ```
71
+
72
+ ## Optional
73
+
74
+ - llms-full.txt: Full API reference with all type signatures