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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT Licence
2
+
3
+ Copyright (c) 2026 TheCryptoDonkey
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,250 @@
1
+ # geohash-kit
2
+
3
+ **The modern TypeScript geohash toolkit — encode, decode, cover polygons, and discover location-based Nostr events.**
4
+
5
+ [![npm](https://img.shields.io/npm/v/geohash-kit)](https://www.npmjs.com/package/geohash-kit)
6
+ [![licence](https://img.shields.io/npm/l/geohash-kit)](./LICENCE)
7
+ ![zero deps](https://img.shields.io/badge/dependencies-0-brightgreen)
8
+ ![TypeScript](https://img.shields.io/badge/TypeScript-native-blue)
9
+
10
+ ## Why geohash-kit?
11
+
12
+ - **Modern TypeScript** — native types, ESM-only, tree-shakeable subpath exports. Zero dependencies. A drop-in replacement for `ngeohash`.
13
+ - **Smart polygon coverage** — adaptive multi-precision subdivision produces compact geohash sets (coarse interior, fine edges). Other polygon libraries use single-precision brute-force, producing 10-100x more cells for the same area.
14
+ - **Production-hardened** — input validation on all public APIs, RangeError on invalid/infeasible parameters, 736 tests including fuzz and property-based suites.
15
+ - **Nostr-native** — generates multi-precision `g`-tag ladders for publishing and location-based `#g` filter arrays for REQ subscriptions. Perfect for building location-based Nostr applications.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install geohash-kit
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```typescript
26
+ import {
27
+ encode, decode, neighbours, distance,
28
+ polygonToGeohashes, geohashesToGeoJSON,
29
+ createGTagLadder, createGTagFilter,
30
+ } from 'geohash-kit'
31
+
32
+ // Encode a location
33
+ const hash = encode(51.5074, -0.1278) // 'gcpvj'
34
+
35
+ // Decode back to coordinates
36
+ const { lat, lon, error } = decode(hash)
37
+
38
+ // Get adjacent cells
39
+ const adj = neighbours(hash) // { n, ne, e, se, s, sw, w, nw }
40
+
41
+ // Distance between two geohashes
42
+ const d = distance('gcpvj', 'u09tu') // ~340km (London → Paris)
43
+
44
+ // Cover a polygon with geohashes
45
+ const coverage = polygonToGeohashes([
46
+ [-0.15, 51.50], [-0.10, 51.50],
47
+ [-0.10, 51.52], [-0.15, 51.52],
48
+ ])
49
+
50
+ // Render coverage on a map
51
+ const geojson = geohashesToGeoJSON(coverage)
52
+
53
+ // Cover a donut polygon (outer ring with a hole)
54
+ const donut = polygonToGeohashes({
55
+ type: 'Polygon',
56
+ coordinates: [
57
+ [[-0.15, 51.49], [-0.05, 51.49], [-0.05, 51.54], [-0.15, 51.54], [-0.15, 51.49]],
58
+ [[-0.12, 51.51], [-0.08, 51.51], [-0.08, 51.53], [-0.12, 51.53], [-0.12, 51.51]],
59
+ ],
60
+ })
61
+
62
+ // Generate Nostr event tags
63
+ const tags = createGTagLadder(hash)
64
+ // [['g','g'], ['g','gc'], ['g','gcp'], ['g','gcpv'], ['g','gcpvj']]
65
+
66
+ // Generate Nostr subscription filter
67
+ const filter = createGTagFilter(51.5074, -0.1278, 5000)
68
+ // { '#g': ['gcpvj', 'gcpvm', ...] }
69
+ ```
70
+
71
+ ## For Nostr Developers
72
+
73
+ Nostr relays match `#g` tags by exact equality — there's no prefix matching. An event tagged `["g", "gcpvjb"]` won't match filter `{"#g": ["gcpvj"]}`. The workaround is a **tag ladder**: publish every precision prefix, subscribe at the right precision with neighbour expansion.
74
+
75
+ **Building location-based Nostr apps?** Use geohash-kit to:
76
+ - Tag events with multi-precision `g`-tag ladders for geographic discoverability
77
+ - Query nearby events using ring-based expansion (`expandRings`)
78
+ - Filter subscriptions by location using geohash proximity matching
79
+
80
+ ### Publishing
81
+
82
+ ```typescript
83
+ import { encode } from 'geohash-kit/core'
84
+ import { createGTagLadder } from 'geohash-kit/nostr'
85
+
86
+ const hash = encode(51.5074, -0.1278, 6)
87
+ const tags = createGTagLadder(hash)
88
+ // Add to your event: [['g','g'], ['g','gc'], ..., ['g','gcpvjb']]
89
+ ```
90
+
91
+ ### Subscribing
92
+
93
+ ```typescript
94
+ import { createGTagFilter, nearbyFilter } from 'geohash-kit/nostr'
95
+
96
+ // From coordinates + radius
97
+ const filter = createGTagFilter(51.5074, -0.1278, 5000)
98
+ // { '#g': ['gcpvj', ...neighbours] }
99
+
100
+ // Or with explicit precision and ring count
101
+ const filter2 = nearbyFilter(51.5074, -0.1278, { precision: 4, rings: 2 })
102
+ ```
103
+
104
+ ### Parsing events
105
+
106
+ ```typescript
107
+ import { parseGTags, bestGeohash } from 'geohash-kit/nostr'
108
+
109
+ const best = bestGeohash(event.tags) // highest-precision g tag
110
+ const all = parseGTags(event.tags) // [{ geohash, precision }, ...]
111
+ ```
112
+
113
+ ## API Reference
114
+
115
+ ### `geohash-kit/core`
116
+
117
+ | Function | Description |
118
+ |----------|-------------|
119
+ | `encode(lat, lon, precision?)` | Encode coordinates to geohash (default precision 5) |
120
+ | `decode(hash)` | Decode to `{ lat, lon, error }` |
121
+ | `bounds(hash)` | Bounding rectangle `{ minLat, maxLat, minLon, maxLon }` |
122
+ | `children(hash)` | 32 child geohashes at next precision |
123
+ | `neighbour(hash, direction)` | Single adjacent cell |
124
+ | `neighbours(hash)` | All 8 adjacent cells |
125
+ | `contains(a, b)` | Bidirectional prefix containment |
126
+ | `matchesAny(hash, candidates)` | Match against multi-precision set |
127
+ | `distance(hashA, hashB)` | Haversine distance in metres |
128
+ | `distanceFromCoords(lat1, lon1, lat2, lon2)` | Haversine distance in metres |
129
+ | `radiusToPrecision(metres)` | Optimal precision for search radius |
130
+ | `precisionToRadius(precision)` | Approximate cell radius in metres |
131
+
132
+ ### `geohash-kit/coverage`
133
+
134
+ | Function | Description |
135
+ |----------|-------------|
136
+ | `polygonToGeohashes(polygon, options?)` | Adaptive threshold polygon coverage; accepts `[lon, lat][]`, GeoJSON `Polygon` (with holes), or `MultiPolygon` |
137
+ | `geohashesToGeoJSON(hashes)` | GeoJSON FeatureCollection for map rendering |
138
+ | `geohashesToConvexHull(hashes)` | Convex hull reconstruction |
139
+ | `deduplicateGeohashes(hashes, options?)` | Remove redundant ancestors; `{ lossy: true }` merges ≥30/32 siblings |
140
+ | `pointInPolygon(point, polygon)` | Ray-casting point-in-polygon test |
141
+ | `boundsOverlapsPolygon(bounds, polygon)` | Bounds–polygon overlap test |
142
+ | `boundsFullyInsidePolygon(bounds, polygon)` | Bounds fully inside polygon test |
143
+
144
+ **`CoverageOptions`:** `{ minPrecision?, maxPrecision?, maxCells?, mergeThreshold? }`
145
+
146
+ **`PolygonInput`:** `[number, number][] | GeoJSONPolygon | GeoJSONMultiPolygon`
147
+
148
+ ### `geohash-kit/nostr`
149
+
150
+ | Function | Description |
151
+ |----------|-------------|
152
+ | `createGTagLadder(geohash, minPrecision?)` | Multi-precision g-tag ladder |
153
+ | `createGTagFilter(lat, lon, radiusMetres)` | REQ filter from coordinates |
154
+ | `createGTagFilterFromGeohashes(hashes)` | REQ filter from hash set |
155
+ | `expandRings(hash, rings?)` | Concentric neighbour rings |
156
+ | `nearbyFilter(lat, lon, options?)` | Encode + expand + filter |
157
+ | `parseGTags(tags)` | Extract g tags from event |
158
+ | `bestGeohash(tags)` | Highest-precision g tag |
159
+
160
+ ## Polygon Coverage Algorithm
161
+
162
+ `polygonToGeohashes` uses adaptive threshold recursive subdivision:
163
+
164
+ 1. BFS from precision-1 cells that overlap the polygon
165
+ 2. For each cell: fully inside → emit (if deep enough); at max precision → emit if overlaps; partial → subdivide children
166
+ 3. `mergeThreshold` controls interior cell granularity: 1.0 = uniform max precision, 0.0 = coarsest fully-inside cells
167
+ 4. If result exceeds `maxCells`, `maxPrecision` is stepped down until the result fits
168
+ 5. Post-processing merges sibling sets based on `mergeThreshold` — at threshold 1.0 only complete sets (32/32), at 0.0 as few as 24/32. Result is sorted and deduplicated
169
+ 6. If no precision level fits within `maxCells`, a `RangeError` is thrown — increase `maxCells` or reduce the polygon area
170
+ 7. **Holes:** GeoJSON Polygon inner rings (holes) are respected — cells fully inside a hole are excluded, cells overlapping a hole boundary subdivide to `maxPrecision` for accuracy. Degenerate holes (< 3 vertices) are silently ignored
171
+ 8. **MultiPolygon:** `maxCells` is enforced globally across all child polygons, not per-polygon. The algorithm steps down precision until the merged result fits the budget
172
+
173
+ **Memory:** `polygonToGeohashes` builds the full result array in memory. At `maxCells: 100,000` with average hash length 6, this is roughly 1–2 MB — well within typical Node.js/browser limits. For extremely large polygons (millions of cells), consider splitting the polygon into smaller regions and processing each independently.
174
+
175
+ ## Comparison
176
+
177
+ | Feature | geohash-kit | ngeohash | geohashing | latlon-geohash | geohash-poly | shape2geohash | nostr-geotags |
178
+ |---------|:-----------:|:--------:|:----------:|:--------------:|:------------:|:-------------:|:-------------:|
179
+ | TypeScript native | **Yes** | No | Yes | No | No | No | Yes |
180
+ | ESM-only | **Yes** | No | No | Yes | No | No | Yes |
181
+ | Zero dependencies | **Yes** | Yes | Yes | Yes | No (10) | No (11) | No (2) |
182
+ | Polygon → geohashes | **Multi-precision** | — | — | — | Single-precision | Single-precision | — |
183
+ | Multi-precision output | **Yes** | — | — | — | No | No | — |
184
+ | maxCells budget | **Yes** | — | — | — | No | No | — |
185
+ | GeoJSON output | **Yes** | No | Yes | No | No | No | No |
186
+ | Convex hull | **Yes** | No | No | No | No | No | No |
187
+ | Deduplication | **Yes** | No | No | No | No | No | No |
188
+ | Distance / radius | **Yes** | No | No | No | No | No | No |
189
+ | Neighbours / rings | **Yes** | Yes | Yes | Yes | No | No | No |
190
+ | Nostr g-tag ladders | **Yes** | No | No | No | No | No | Partial |
191
+ | Nostr REQ filters | **Yes** | No | No | No | No | No | No |
192
+ | Input validation | **Yes** | No | No | No | No | No | No |
193
+ | Last published | 2026 | 2018 | 2024 | 2019 | 2019 | 2022 | 2025 |
194
+ | Weekly downloads | — | ~171k | ~7k | ~19k | ~1k | ~500 | <100 |
195
+
196
+ ## Migrating from ngeohash
197
+
198
+ geohash-kit is a modern TypeScript replacement for [ngeohash](https://github.com/sunng87/node-geohash).
199
+
200
+ **Import change:**
201
+
202
+ ```typescript
203
+ // Before
204
+ const ngeohash = require('ngeohash')
205
+
206
+ // After (ESM)
207
+ import { encode, decode, bounds, neighbours } from 'geohash-kit'
208
+ ```
209
+
210
+ **Function mapping:**
211
+
212
+ | ngeohash | geohash-kit | Notes |
213
+ |----------|-------------|-------|
214
+ | `encode(lat, lon, precision?)` | `encode(lat, lon, precision?)` | Same signature |
215
+ | `decode(hash)` | `decode(hash)` | Returns `{ lat, lon, error }` instead of `{ latitude, longitude, error }` |
216
+ | `decode_bbox(hash)` | `bounds(hash)` | Returns `{ minLat, maxLat, minLon, maxLon }` object instead of `[minlat, minlon, maxlat, maxlon]` array |
217
+ | `neighbors(hash)` | `neighbours(hash)` | British spelling; returns `{ n, ne, e, ... }` object instead of array |
218
+ | `neighbor(hash, [latDir, lonDir])` | `neighbour(hash, direction)` | Direction is a string (`'n'`, `'sw'`, etc.) instead of `[1, 0]` array |
219
+ | `bboxes(minLat, minLon, maxLat, maxLon, precision)` | `polygonToGeohashes(polygon)` | More powerful: accepts polygons (not just rectangles), multi-precision output, maxCells budget |
220
+ | `encode_int` / `decode_int` / `*_int` | — | Integer geohash encoding not supported |
221
+
222
+ **Key differences:**
223
+
224
+ - **ESM-only** — no `require()`, use `import` syntax
225
+ - **Input validation** — throws `RangeError` on invalid coordinates, NaN, or Infinity (ngeohash returns garbage)
226
+ - **British English** — `neighbours` not `neighbors`, `neighbour` not `neighbor`
227
+ - **Structured returns** — named object properties instead of positional arrays
228
+
229
+ ## Benchmarking
230
+
231
+ geohash-kit includes comprehensive performance benchmarks for all major functions. Run them with:
232
+
233
+ ```bash
234
+ npm run bench
235
+ ```
236
+
237
+ Performance summary:
238
+ - **Core functions** (encode, decode, bounds, etc.): >5M ops/sec, all sub-100µs
239
+ - **`polygonToGeohashes`** (the main workhorse): 282–7,230 ops/sec depending on polygon size and precision
240
+ - **Nostr functions** (tag ladders, filters): 256k–10M ops/sec
241
+
242
+ For detailed performance analysis, device comparisons, and optimization guidance, see [docs/BENCHMARKS.md](./docs/BENCHMARKS.md).
243
+
244
+ ## For AI Assistants
245
+
246
+ See [llms.txt](./llms.txt) for a concise API summary, or [llms-full.txt](./llms-full.txt) for the complete reference with examples.
247
+
248
+ ## Licence
249
+
250
+ [MIT](./LICENCE)
package/dist/core.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ export interface GeohashBounds {
2
+ minLat: number;
3
+ maxLat: number;
4
+ minLon: number;
5
+ maxLon: number;
6
+ }
7
+ export type Direction = 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'nw';
8
+ /** Encode latitude/longitude to a geohash string. Default precision 5 (~4.9km). */
9
+ export declare function encode(lat: number, lon: number, precision?: number): string;
10
+ /** Decode a geohash to its centre point with error margins. */
11
+ export declare function decode(hash: string): {
12
+ lat: number;
13
+ lon: number;
14
+ error: {
15
+ lat: number;
16
+ lon: number;
17
+ };
18
+ };
19
+ /** Get the bounding rectangle of a geohash cell. */
20
+ export declare function bounds(hash: string): GeohashBounds;
21
+ /** Get the 32 children of a geohash at the next precision level. */
22
+ export declare function children(hash: string): string[];
23
+ /** Check if two geohashes overlap (bidirectional prefix containment). */
24
+ export declare function contains(a: string, b: string): boolean;
25
+ /** Check if a geohash matches any candidate in a multi-precision set. */
26
+ export declare function matchesAny(hash: string, candidates: string[]): boolean;
27
+ /** Get a single adjacent geohash cell in the given direction. */
28
+ export declare function neighbour(hash: string, direction: Direction): string;
29
+ /** Get all 8 adjacent geohash cells. */
30
+ export declare function neighbours(hash: string): Record<Direction, string>;
31
+ /** Haversine distance in metres between two coordinate pairs. */
32
+ export declare function distanceFromCoords(lat1: number, lon1: number, lat2: number, lon2: number): number;
33
+ /** Haversine distance in metres between centres of two geohash cells. */
34
+ export declare function distance(hashA: string, hashB: string): number;
35
+ /** Optimal geohash precision for a given search radius in metres. */
36
+ export declare function radiusToPrecision(metres: number): number;
37
+ /** Approximate cell radius in metres for a given precision level. */
38
+ export declare function precisionToRadius(precision: number): number;
39
+ //# sourceMappingURL=core.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,MAAM,SAAS,GAAG,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,CAAA;AAczE,mFAAmF;AACnF,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,SAAI,GAAG,MAAM,CA2BtE;AAED,+DAA+D;AAC/D,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAWtG;AAED,oDAAoD;AACpD,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAoBlD;AAED,oEAAoE;AACpE,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAG/C;AAID,yEAAyE;AACzE,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED,yEAAyE;AACzE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAEtE;AAID,iEAAiE;AACjE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,MAAM,CA0BpE;AAED,wCAAwC;AACxC,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAWlE;AAMD,iEAAiE;AACjE,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAWjG;AAED,yEAAyE;AACzE,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAI7D;AAkBD,qEAAqE;AACrE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAMxD;AAED,qEAAqE;AACrE,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAI3D"}
package/dist/core.js ADDED
@@ -0,0 +1,213 @@
1
+ // geohash-kit/core — encode, decode, bounds, children, contains, matchesAny
2
+ const BASE32 = '0123456789bcdefghjkmnpqrstuvwxyz';
3
+ const BASE32_DECODE = {};
4
+ for (let i = 0; i < BASE32.length; i++)
5
+ BASE32_DECODE[BASE32[i]] = i;
6
+ // --- Validation ---
7
+ function validateGeohash(hash) {
8
+ for (const ch of hash) {
9
+ if (!(ch in BASE32_DECODE)) {
10
+ throw new TypeError(`Invalid geohash character: '${ch}' in "${hash}"`);
11
+ }
12
+ }
13
+ }
14
+ // --- Encoding ---
15
+ /** Encode latitude/longitude to a geohash string. Default precision 5 (~4.9km). */
16
+ export function encode(lat, lon, precision = 5) {
17
+ if (!Number.isFinite(lat) || lat < -90 || lat > 90)
18
+ throw new RangeError(`Invalid latitude: ${lat}`);
19
+ if (!Number.isFinite(lon) || lon < -180 || lon > 180)
20
+ throw new RangeError(`Invalid longitude: ${lon}`);
21
+ if (!Number.isFinite(precision))
22
+ throw new RangeError(`Invalid precision: ${precision}`);
23
+ precision = Math.round(precision);
24
+ if (precision < 1)
25
+ throw new RangeError(`Invalid precision: ${precision}`);
26
+ precision = Math.min(12, precision);
27
+ let latMin = -90, latMax = 90;
28
+ let lonMin = -180, lonMax = 180;
29
+ let hash = '';
30
+ let bit = 0;
31
+ let ch = 0;
32
+ let isLon = true;
33
+ while (hash.length < precision) {
34
+ if (isLon) {
35
+ const mid = (lonMin + lonMax) / 2;
36
+ if (lon >= mid) {
37
+ ch |= 1 << (4 - bit);
38
+ lonMin = mid;
39
+ }
40
+ else {
41
+ lonMax = mid;
42
+ }
43
+ }
44
+ else {
45
+ const mid = (latMin + latMax) / 2;
46
+ if (lat >= mid) {
47
+ ch |= 1 << (4 - bit);
48
+ latMin = mid;
49
+ }
50
+ else {
51
+ latMax = mid;
52
+ }
53
+ }
54
+ isLon = !isLon;
55
+ bit++;
56
+ if (bit === 5) {
57
+ hash += BASE32[ch];
58
+ bit = 0;
59
+ ch = 0;
60
+ }
61
+ }
62
+ return hash;
63
+ }
64
+ /** Decode a geohash to its centre point with error margins. */
65
+ export function decode(hash) {
66
+ if (hash.length === 0)
67
+ throw new TypeError('Cannot decode an empty geohash');
68
+ const b = bounds(hash);
69
+ return {
70
+ lat: (b.minLat + b.maxLat) / 2,
71
+ lon: (b.minLon + b.maxLon) / 2,
72
+ error: {
73
+ lat: (b.maxLat - b.minLat) / 2,
74
+ lon: (b.maxLon - b.minLon) / 2,
75
+ },
76
+ };
77
+ }
78
+ /** Get the bounding rectangle of a geohash cell. */
79
+ export function bounds(hash) {
80
+ validateGeohash(hash);
81
+ let minLat = -90, maxLat = 90;
82
+ let minLon = -180, maxLon = 180;
83
+ let isLon = true;
84
+ for (const ch of hash) {
85
+ const bits = BASE32_DECODE[ch];
86
+ for (let bit = 4; bit >= 0; bit--) {
87
+ if (isLon) {
88
+ const mid = (minLon + maxLon) / 2;
89
+ if ((bits >> bit) & 1)
90
+ minLon = mid;
91
+ else
92
+ maxLon = mid;
93
+ }
94
+ else {
95
+ const mid = (minLat + maxLat) / 2;
96
+ if ((bits >> bit) & 1)
97
+ minLat = mid;
98
+ else
99
+ maxLat = mid;
100
+ }
101
+ isLon = !isLon;
102
+ }
103
+ }
104
+ return { minLat, maxLat, minLon, maxLon };
105
+ }
106
+ /** Get the 32 children of a geohash at the next precision level. */
107
+ export function children(hash) {
108
+ validateGeohash(hash);
109
+ return Array.from(BASE32, (ch) => hash + ch);
110
+ }
111
+ // --- Matching ---
112
+ /** Check if two geohashes overlap (bidirectional prefix containment). */
113
+ export function contains(a, b) {
114
+ return a.startsWith(b) || b.startsWith(a);
115
+ }
116
+ /** Check if a geohash matches any candidate in a multi-precision set. */
117
+ export function matchesAny(hash, candidates) {
118
+ return candidates.some(c => contains(hash, c));
119
+ }
120
+ // --- Neighbours ---
121
+ /** Get a single adjacent geohash cell in the given direction. */
122
+ export function neighbour(hash, direction) {
123
+ const b = bounds(hash);
124
+ const latHeight = b.maxLat - b.minLat;
125
+ const lonWidth = b.maxLon - b.minLon;
126
+ const centreLat = (b.minLat + b.maxLat) / 2;
127
+ const centreLon = (b.minLon + b.maxLon) / 2;
128
+ let dLat = 0;
129
+ let dLon = 0;
130
+ if (direction.includes('n'))
131
+ dLat = latHeight;
132
+ if (direction.includes('s'))
133
+ dLat = -latHeight;
134
+ if (direction.includes('e'))
135
+ dLon = lonWidth;
136
+ if (direction.includes('w'))
137
+ dLon = -lonWidth;
138
+ let newLat = centreLat + dLat;
139
+ let newLon = centreLon + dLon;
140
+ // Wrap longitude around the antimeridian
141
+ if (newLon > 180)
142
+ newLon -= 360;
143
+ if (newLon < -180)
144
+ newLon += 360;
145
+ // Clamp latitude at poles (no wrapping)
146
+ newLat = Math.max(-89.99999, Math.min(89.99999, newLat));
147
+ return encode(newLat, newLon, hash.length);
148
+ }
149
+ /** Get all 8 adjacent geohash cells. */
150
+ export function neighbours(hash) {
151
+ return {
152
+ n: neighbour(hash, 'n'),
153
+ ne: neighbour(hash, 'ne'),
154
+ e: neighbour(hash, 'e'),
155
+ se: neighbour(hash, 'se'),
156
+ s: neighbour(hash, 's'),
157
+ sw: neighbour(hash, 'sw'),
158
+ w: neighbour(hash, 'w'),
159
+ nw: neighbour(hash, 'nw'),
160
+ };
161
+ }
162
+ // --- Distance ---
163
+ const EARTH_RADIUS_M = 6_371_000; // Earth mean radius in metres
164
+ /** Haversine distance in metres between two coordinate pairs. */
165
+ export function distanceFromCoords(lat1, lon1, lat2, lon2) {
166
+ if (!Number.isFinite(lat1) || !Number.isFinite(lon1) || !Number.isFinite(lat2) || !Number.isFinite(lon2)) {
167
+ throw new RangeError('All coordinate arguments must be finite numbers');
168
+ }
169
+ const toRad = (deg) => (deg * Math.PI) / 180;
170
+ const dLat = toRad(lat2 - lat1);
171
+ const dLon = toRad(lon2 - lon1);
172
+ const a = Math.sin(dLat / 2) ** 2 +
173
+ Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
174
+ return EARTH_RADIUS_M * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
175
+ }
176
+ /** Haversine distance in metres between centres of two geohash cells. */
177
+ export function distance(hashA, hashB) {
178
+ const a = decode(hashA);
179
+ const b = decode(hashB);
180
+ return distanceFromCoords(a.lat, a.lon, b.lat, b.lon);
181
+ }
182
+ // --- Precision ↔ Radius ---
183
+ // Approximate cell half-diagonal in metres at each precision level (equator).
184
+ const PRECISION_RADIUS_M = [
185
+ /* 0 (unused) */ 0,
186
+ /* 1 */ 2_500_000,
187
+ /* 2 */ 630_000,
188
+ /* 3 */ 78_000,
189
+ /* 4 */ 20_000,
190
+ /* 5 */ 2_400,
191
+ /* 6 */ 610,
192
+ /* 7 */ 76,
193
+ /* 8 */ 19,
194
+ /* 9 */ 2.4,
195
+ ];
196
+ /** Optimal geohash precision for a given search radius in metres. */
197
+ export function radiusToPrecision(metres) {
198
+ if (!Number.isFinite(metres) || metres < 0)
199
+ throw new RangeError(`Invalid radius: ${metres}`);
200
+ for (let p = 1; p <= 9; p++) {
201
+ if (PRECISION_RADIUS_M[p] <= metres)
202
+ return p;
203
+ }
204
+ return 9;
205
+ }
206
+ /** Approximate cell radius in metres for a given precision level. */
207
+ export function precisionToRadius(precision) {
208
+ if (!Number.isFinite(precision))
209
+ throw new RangeError(`Invalid precision: ${precision}`);
210
+ const p = Math.max(1, Math.min(9, Math.round(precision)));
211
+ return PRECISION_RADIUS_M[p];
212
+ }
213
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ import type { GeohashBounds } from './core.js';
2
+ import type { PolygonInput } from './geojson.js';
3
+ export type { GeohashBounds } from './core.js';
4
+ export type { PolygonInput, GeoJSONPolygon, GeoJSONMultiPolygon } from './geojson.js';
5
+ /**
6
+ * Ray-casting algorithm: test whether a point [x, y] lies inside a polygon.
7
+ * Polygon is an array of [x, y] vertices (closed automatically).
8
+ */
9
+ export declare function pointInPolygon(point: [number, number], polygon: [number, number][]): boolean;
10
+ /**
11
+ * Test whether all four corners of bounds lie inside the polygon.
12
+ */
13
+ export declare function boundsFullyInsidePolygon(bounds: GeohashBounds, polygon: [number, number][]): boolean;
14
+ /**
15
+ * Test whether a bounds rectangle overlaps a polygon at all.
16
+ * Checks: (1) any bounds corner inside polygon, (2) any polygon vertex inside bounds,
17
+ * (3) any edge intersection.
18
+ */
19
+ export declare function boundsOverlapsPolygon(bounds: GeohashBounds, polygon: [number, number][]): boolean;
20
+ export interface CoverageOptions {
21
+ minPrecision?: number;
22
+ maxPrecision?: number;
23
+ maxCells?: number;
24
+ mergeThreshold?: number;
25
+ }
26
+ /**
27
+ * Convert a polygon (array of [lon, lat] vertices) to an efficient set of
28
+ * multi-precision geohash strings using recursive subdivision.
29
+ *
30
+ * Edges always subdivide to maxPrecision for a tight boundary. Interior
31
+ * cells use the coarsest precision allowed by mergeThreshold. If the result
32
+ * exceeds maxCells, maxPrecision is stepped down until it fits.
33
+ *
34
+ * Throws RangeError if the polygon cannot be covered within maxCells at
35
+ * the given minPrecision.
36
+ *
37
+ * **Antimeridian:** polygons crossing ±180° longitude are not supported.
38
+ * Split at the antimeridian and cover each half separately.
39
+ */
40
+ export declare function polygonToGeohashes(input: PolygonInput, options?: CoverageOptions): string[];
41
+ /**
42
+ * Compute a convex hull polygon from an array of geohash strings.
43
+ * Collects all unique cell corners, then builds the hull using
44
+ * Andrew's monotone chain algorithm.
45
+ * Returns `[lon, lat][]`.
46
+ *
47
+ * **Antimeridian:** throws if the input hashes straddle ±180° longitude.
48
+ * Dateline-crossing hulls cannot be consumed by planar geometry functions
49
+ * (`pointInPolygon`, `polygonToGeohashes`). Split hash sets at the
50
+ * antimeridian and compute separate hulls for each side.
51
+ */
52
+ export declare function geohashesToConvexHull(hashes: string[]): [number, number][];
53
+ export interface DeduplicateOptions {
54
+ /**
55
+ * Allow near-complete sibling merges (30/32) for a smaller result array.
56
+ * Trades a tiny boundary overshoot for fewer cells. Default: `false` (exact).
57
+ */
58
+ lossy?: boolean;
59
+ }
60
+ /**
61
+ * Remove redundant geohashes and merge sibling groups.
62
+ * 1. Remove any geohash whose ancestor (shorter prefix) is already in the set.
63
+ * 2. Merge sibling sets bottom-up — exact (all 32) by default, or
64
+ * near-complete (≥30/32) when `lossy: true`.
65
+ */
66
+ export declare function deduplicateGeohashes(hashes: string[], options?: DeduplicateOptions): string[];
67
+ export interface GeohashGeoJSON {
68
+ type: 'FeatureCollection';
69
+ features: {
70
+ type: 'Feature';
71
+ geometry: {
72
+ type: 'Polygon';
73
+ coordinates: [number, number][][];
74
+ };
75
+ properties: {
76
+ geohash: string;
77
+ precision: number;
78
+ };
79
+ }[];
80
+ }
81
+ /**
82
+ * Convert an array of geohash strings to a GeoJSON FeatureCollection
83
+ * of polygon rectangles, suitable for rendering on a MapLibre map.
84
+ */
85
+ export declare function geohashesToGeoJSON(hashes: string[]): GeohashGeoJSON;
86
+ //# sourceMappingURL=coverage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../src/coverage.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAuC,MAAM,cAAc,CAAA;AAGrF,YAAY,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AAC9C,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAgBrF;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EACvB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAC1B,OAAO,CAcT;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAC1B,OAAO,CAwBT;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAC1B,OAAO,CAoCT;AAoCD,MAAM,WAAW,eAAe;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AA+CD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,YAAY,EACnB,OAAO,GAAE,eAAoB,GAC5B,MAAM,EAAE,CAmHV;AAyKD;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CA8D1E;AAID,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,kBAAuB,GAAG,MAAM,EAAE,CAgBjG;AAID,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,mBAAmB,CAAA;IACzB,QAAQ,EAAE;QACR,IAAI,EAAE,SAAS,CAAA;QACf,QAAQ,EAAE;YAAE,IAAI,EAAE,SAAS,CAAC;YAAC,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,CAAA;SAAE,CAAA;QAChE,UAAU,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAA;SAAE,CAAA;KACnD,EAAE,CAAA;CACJ;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,cAAc,CAqBnE"}