geohash-kit

July 4, 2026 · View on GitHub

Nostr: npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2

The modern TypeScript geohash toolkit — encode, decode, cover polygons, and discover location-based Nostr events.

npm licence zero deps TypeScript Nostr GitHub Sponsors

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 #g filter arrays for REQ subscriptions. Perfect for building location-based Nostr applications.

Install

npm install geohash-kit

Quick 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
// Note: polygon coordinates are [longitude, latitude] (GeoJSON standard)
// This is the opposite of encode(latitude, longitude)
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

FunctionDescription
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

FunctionDescription
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

FunctionDescription
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:

  1. BFS from precision-1 cells that overlap the polygon
  2. For each cell: fully inside → emit (if deep enough); at max precision → emit if overlaps; partial → subdivide children
  3. mergeThreshold controls interior cell granularity: 1.0 = uniform max precision, 0.0 = coarsest fully-inside cells
  4. If result exceeds maxCells, maxPrecision is stepped down until the result fits
  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
  6. If no precision level fits within maxCells, a RangeError is thrown — increase maxCells or reduce the polygon area
  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
  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

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

Featuregeohash-kitngeohashgeohashinglatlon-geohashgeohash-polyshape2geohashnostr-geotags
TypeScript nativeYesNoYesNoNoNoYes
ESM-onlyYesNoNoYesNoNoYes
Zero dependenciesYesYesYesYesNo (10)No (11)No (2)
Polygon → geohashesMulti-precisionSingle-precisionSingle-precision
Multi-precision outputYesNoNo
maxCells budgetYesNoNo
GeoJSON outputYesNoYesNoNoNoNo
Convex hullYesNoNoNoNoNoNo
DeduplicationYesNoNoNoNoNoNo
Distance / radiusYesNoNoNoNoNoNo
Neighbours / ringsYesYesYesYesNoNoNo
Nostr g-tag laddersYesNoNoNoNoNoPartial
Nostr REQ filtersYesNoNoNoNoNoNo
Input validationYesNoNoNoNoNoNo
Last published2026201820242019201920222025
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:

ngeohashgeohash-kitNotes
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 / *_intInteger geohash encoding not supported

Key differences:

  • ESM-only — no require(), use import syntax
  • Input validation — throws RangeError on invalid coordinates, NaN, or Infinity (ngeohash returns garbage)
  • British Englishneighbours not neighbors, neighbour not neighbor
  • 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 bench

Performance 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.

Runtime Compatibility

geohash-kit is pure ESM with zero dependencies and no Node.js-specific APIs. It works in:

  • Node.js 18+ (supported, tested in CI)
  • Deno — import from npm: import { encode } from 'npm:geohash-kit'
  • Bun — works out of the box: import { encode } from 'geohash-kit'
  • Browsers — bundler-compatible (Vite, esbuild, Rollup, webpack 5)
  • Cloudflare Workers — ESM-compatible, no filesystem or network calls

Android Compatibility

For Kotlin/Android parity implementations, use the compatibility contract and versioned vectors:

For AI Assistants

See llms.txt for a concise API summary, or llms-full.txt for the complete reference with examples.

Part of the ForgeSworn Toolkit

ForgeSworn builds open-source cryptographic identity, payments, and coordination tools for Nostr.

LibraryWhat it does
nsec-treeDeterministic sub-identity derivation
ring-sigSAG/LSAG ring signatures on secp256k1
range-proofPedersen commitment range proofs
canary-kitCoercion-resistant spoken verification
spoken-tokenHuman-speakable verification tokens
toll-boothL402 payment middleware
geohash-kitGeohash toolkit with polygon coverage
nostr-attestationsNIP-VA verifiable attestations
dominionEpoch-based encrypted access control
rendezvous-kitFair meeting point discovery using isochrones
nostr-veilPrivacy-preserving Web of Trust

Licence

MIT

Support

For issues and feature requests, see GitHub Issues.

If you find geohash-kit useful, consider sending a tip:

  • Lightning: profusemeat89@walletofsatoshi.com
  • Nostr zaps: npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2