npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

geohash-kit

v1.5.0

Published

Modern TypeScript geohash toolkit — encode, decode, cover polygons, and build Nostr filters

Readme

geohash-kit

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

npm licence zero deps TypeScript Nostr

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

  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

| 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(), 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.

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.

Licence

MIT

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