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/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
|
+
[](https://www.npmjs.com/package/geohash-kit)
|
|
6
|
+
[](./LICENCE)
|
|
7
|
+

|
|
8
|
+

|
|
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
|
package/dist/core.js.map
ADDED
|
@@ -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"}
|