geohash-kit
v1.5.0
Published
Modern TypeScript geohash toolkit — encode, decode, cover polygons, and build Nostr filters
Maintainers
Readme
geohash-kit
The modern TypeScript geohash toolkit — encode, decode, cover polygons, and discover location-based Nostr events.
Interactive Demo — try every API function on a live map.
Why geohash-kit?
- Modern TypeScript — native types, ESM-only, tree-shakeable subpath exports. Zero dependencies. A drop-in replacement for
ngeohash. - 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.
- Production-hardened — input validation on all public APIs, RangeError on invalid/infeasible parameters, 736 tests including fuzz and property-based suites.
- Nostr-native — generates multi-precision
g-tag ladders for publishing and location-based#gfilter arrays for REQ subscriptions. Perfect for building location-based Nostr applications.
Install
npm install geohash-kitQuick Start
import {
encode, decode, neighbours, distance,
polygonToGeohashes, geohashesToGeoJSON,
createGTagLadder, createGTagFilter,
} from 'geohash-kit'
// Encode a location
const hash = encode(51.5074, -0.1278) // 'gcpvj'
// Decode back to coordinates
const { lat, lon, error } = decode(hash)
// Get adjacent cells
const adj = neighbours(hash) // { n, ne, e, se, s, sw, w, nw }
// Distance between two geohashes
const d = distance('gcpvj', 'u09tu') // ~340km (London → Paris)
// Cover a polygon with geohashes
const coverage = polygonToGeohashes([
[-0.15, 51.50], [-0.10, 51.50],
[-0.10, 51.52], [-0.15, 51.52],
])
// Render coverage on a map
const geojson = geohashesToGeoJSON(coverage)
// Cover a donut polygon (outer ring with a hole)
const donut = polygonToGeohashes({
type: 'Polygon',
coordinates: [
[[-0.15, 51.49], [-0.05, 51.49], [-0.05, 51.54], [-0.15, 51.54], [-0.15, 51.49]],
[[-0.12, 51.51], [-0.08, 51.51], [-0.08, 51.53], [-0.12, 51.53], [-0.12, 51.51]],
],
})
// Generate Nostr event tags
const tags = createGTagLadder(hash)
// [['g','g'], ['g','gc'], ['g','gcp'], ['g','gcpv'], ['g','gcpvj']]
// Generate Nostr subscription filter
const filter = createGTagFilter(51.5074, -0.1278, 5000)
// { '#g': ['gcpvj', 'gcpvm', ...] }For Nostr Developers
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.
Building location-based Nostr apps? Use geohash-kit to:
- Tag events with multi-precision
g-tag ladders for geographic discoverability - Query nearby events using ring-based expansion (
expandRings) - Filter subscriptions by location using geohash proximity matching
Publishing
import { encode } from 'geohash-kit/core'
import { createGTagLadder } from 'geohash-kit/nostr'
const hash = encode(51.5074, -0.1278, 6)
const tags = createGTagLadder(hash)
// Add to your event: [['g','g'], ['g','gc'], ..., ['g','gcpvjb']]Subscribing
import { createGTagFilter, nearbyFilter } from 'geohash-kit/nostr'
// From coordinates + radius
const filter = createGTagFilter(51.5074, -0.1278, 5000)
// { '#g': ['gcpvj', ...neighbours] }
// Or with explicit precision and ring count
const filter2 = nearbyFilter(51.5074, -0.1278, { precision: 4, rings: 2 })Parsing events
import { parseGTags, bestGeohash } from 'geohash-kit/nostr'
const best = bestGeohash(event.tags) // highest-precision g tag
const all = parseGTags(event.tags) // [{ geohash, precision }, ...]API Reference
geohash-kit/core
| Function | Description |
|----------|-------------|
| encode(lat, lon, precision?) | Encode coordinates to geohash (default precision 5) |
| decode(hash) | Decode to { lat, lon, error } |
| bounds(hash) | Bounding rectangle { minLat, maxLat, minLon, maxLon } |
| children(hash) | 32 child geohashes at next precision |
| neighbour(hash, direction) | Single adjacent cell |
| neighbours(hash) | All 8 adjacent cells |
| contains(a, b) | Bidirectional prefix containment |
| matchesAny(hash, candidates) | Match against multi-precision set |
| distance(hashA, hashB) | Haversine distance in metres |
| distanceFromCoords(lat1, lon1, lat2, lon2) | Haversine distance in metres |
| radiusToPrecision(metres) | Optimal precision for search radius |
| precisionToRadius(precision) | Approximate cell radius in metres |
geohash-kit/coverage
| Function | Description |
|----------|-------------|
| polygonToGeohashes(polygon, options?) | Adaptive threshold polygon coverage; accepts [lon, lat][], GeoJSON Polygon (with holes), or MultiPolygon |
| geohashesToGeoJSON(hashes) | GeoJSON FeatureCollection for map rendering |
| geohashesToConvexHull(hashes) | Convex hull reconstruction |
| deduplicateGeohashes(hashes, options?) | Remove redundant ancestors; { lossy: true } merges ≥30/32 siblings |
| pointInPolygon(point, polygon) | Ray-casting point-in-polygon test |
| boundsOverlapsPolygon(bounds, polygon) | Bounds–polygon overlap test |
| boundsFullyInsidePolygon(bounds, polygon) | Bounds fully inside polygon test |
CoverageOptions: { minPrecision?, maxPrecision?, maxCells?, mergeThreshold? }
PolygonInput: [number, number][] | GeoJSONPolygon | GeoJSONMultiPolygon
geohash-kit/nostr
| Function | Description |
|----------|-------------|
| createGTagLadder(geohash, minPrecision?) | Multi-precision g-tag ladder |
| createGTagFilter(lat, lon, radiusMetres) | REQ filter from coordinates |
| createGTagFilterFromGeohashes(hashes) | REQ filter from hash set |
| expandRings(hash, rings?) | Concentric neighbour rings |
| nearbyFilter(lat, lon, options?) | Encode + expand + filter |
| parseGTags(tags) | Extract g tags from event |
| bestGeohash(tags) | Highest-precision g tag |
Polygon Coverage Algorithm
polygonToGeohashes uses adaptive threshold recursive subdivision:
- BFS from precision-1 cells that overlap the polygon
- For each cell: fully inside → emit (if deep enough); at max precision → emit if overlaps; partial → subdivide children
mergeThresholdcontrols interior cell granularity: 1.0 = uniform max precision, 0.0 = coarsest fully-inside cells- If result exceeds
maxCells,maxPrecisionis stepped down until the result fits - 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 - If no precision level fits within
maxCells, aRangeErroris thrown — increasemaxCellsor reduce the polygon area - Holes: GeoJSON Polygon inner rings (holes) are respected — cells fully inside a hole are excluded, cells overlapping a hole boundary subdivide to
maxPrecisionfor accuracy. Degenerate holes (< 3 vertices) are silently ignored - MultiPolygon:
maxCellsis enforced globally across all child polygons, not per-polygon. The algorithm steps down precision until the merged result fits the budget
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.
Comparison
| Feature | geohash-kit | ngeohash | geohashing | latlon-geohash | geohash-poly | shape2geohash | nostr-geotags | |---------|:-----------:|:--------:|:----------:|:--------------:|:------------:|:-------------:|:-------------:| | TypeScript native | Yes | No | Yes | No | No | No | Yes | | ESM-only | Yes | No | No | Yes | No | No | Yes | | Zero dependencies | Yes | Yes | Yes | Yes | No (10) | No (11) | No (2) | | Polygon → geohashes | Multi-precision | — | — | — | Single-precision | Single-precision | — | | Multi-precision output | Yes | — | — | — | No | No | — | | maxCells budget | Yes | — | — | — | No | No | — | | GeoJSON output | Yes | No | Yes | No | No | No | No | | Convex hull | Yes | No | No | No | No | No | No | | Deduplication | Yes | No | No | No | No | No | No | | Distance / radius | Yes | No | No | No | No | No | No | | Neighbours / rings | Yes | Yes | Yes | Yes | No | No | No | | Nostr g-tag ladders | Yes | No | No | No | No | No | Partial | | Nostr REQ filters | Yes | No | No | No | No | No | No | | Input validation | Yes | No | No | No | No | No | No | | Last published | 2026 | 2018 | 2024 | 2019 | 2019 | 2022 | 2025 | | Weekly downloads | — | ~171k | ~7k | ~19k | ~1k | ~500 | <100 |
Migrating from ngeohash
geohash-kit is a modern TypeScript replacement for ngeohash.
Import change:
// Before
const ngeohash = require('ngeohash')
// After (ESM)
import { encode, decode, bounds, neighbours } from 'geohash-kit'Function mapping:
| ngeohash | geohash-kit | Notes |
|----------|-------------|-------|
| encode(lat, lon, precision?) | encode(lat, lon, precision?) | Same signature |
| decode(hash) | decode(hash) | Returns { lat, lon, error } instead of { latitude, longitude, error } |
| decode_bbox(hash) | bounds(hash) | Returns { minLat, maxLat, minLon, maxLon } object instead of [minlat, minlon, maxlat, maxlon] array |
| neighbors(hash) | neighbours(hash) | British spelling; returns { n, ne, e, ... } object instead of array |
| neighbor(hash, [latDir, lonDir]) | neighbour(hash, direction) | Direction is a string ('n', 'sw', etc.) instead of [1, 0] array |
| bboxes(minLat, minLon, maxLat, maxLon, precision) | polygonToGeohashes(polygon) | More powerful: accepts polygons (not just rectangles), multi-precision output, maxCells budget |
| encode_int / decode_int / *_int | — | Integer geohash encoding not supported |
Key differences:
- ESM-only — no
require(), useimportsyntax - Input validation — throws
RangeErroron invalid coordinates, NaN, or Infinity (ngeohash returns garbage) - British English —
neighboursnotneighbors,neighbournotneighbor - Structured returns — named object properties instead of positional arrays
Benchmarking
geohash-kit includes comprehensive performance benchmarks for all major functions. Run them with:
npm run benchPerformance summary:
- Core functions (encode, decode, bounds, etc.): >5M ops/sec, all sub-100µs
polygonToGeohashes(the main workhorse): 282–7,230 ops/sec depending on polygon size and precision- Nostr functions (tag ladders, filters): 256k–10M ops/sec
For detailed performance analysis, device comparisons, and optimization guidance, see docs/BENCHMARKS.md.
Android Compatibility
For Kotlin/Android parity implementations, use the compatibility contract and versioned vectors:
- docs/android-compat.md
- vectors/schema.json
npm run vectors:check(run afternpm run build)
For AI Assistants
See llms.txt for a concise API summary, or llms-full.txt for the complete reference with examples.
Licence
Support
For issues and feature requests, see GitHub Issues.
If you find geohash-kit useful, consider sending a tip:
- Lightning:
[email protected] - Nostr zaps:
npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2
