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.
- package/LICENCE +21 -0
- package/README.md +250 -0
- package/dist/core.d.ts +39 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +213 -0
- package/dist/core.js.map +1 -0
- package/dist/coverage.d.ts +86 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/coverage.js +555 -0
- package/dist/coverage.js.map +1 -0
- package/dist/geojson.d.ts +13 -0
- package/dist/geojson.d.ts.map +1 -0
- package/dist/geojson.js +3 -0
- package/dist/geojson.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/nostr.d.ts +31 -0
- package/dist/nostr.d.ts.map +1 -0
- package/dist/nostr.js +87 -0
- package/dist/nostr.js.map +1 -0
- package/llms-full.txt +454 -0
- package/llms.txt +74 -0
- package/package.json +66 -0
package/dist/nostr.d.ts
ADDED
|
@@ -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
|