Skip to content

@studnicky/grid-schemes

Grid scheme contract and implementations (geohash, H3, S2, MGRS) for spatial indexing and coverage queries.

A grid-agnostic contract (GridSchemeInterface) for spatial indexing systems, plus four implementations: GeohashScheme (base-32 string cells), H3Scheme (Uber's hexagonal grid, via h3-js), S2Scheme (Google's cube-sphere cell hierarchy, via the pure-JS s2js port), and MgrsScheme (the NATO-standard Military Grid Reference System, via the mgrs package). Callers that need to encode coordinates to cell IDs, decode cells back to bounding boxes, walk cell hierarchies, or compute an adaptive coverage of a region can code against the interface without depending on any one scheme. The package also ships tiling helpers for splitting bounding boxes into API-sized chunks, and GeohashTzMap, an O(1) geohash-to-timezone/country/locale lookup backed by pre-generated binary artifacts.

Install

bash
pnpm add @studnicky/grid-schemes

Requires @studnicky:registry=https://npm.pkg.github.com in .npmrc.

Geohash encode/decode

Pure geohash encoding and decoding between a WGS-84 coordinate and its bounding box:

ts
import { Geohash } from '../src/index.js';

// json-schema-uninexpressible: example-only demonstration fixture inline in a runnable usage script, not a package data contract requiring schema derivation
type GeohashBboxType = { 'maxLat': number; 'maxLon': number; 'minLat': number; 'minLon': number };

function demonstrateGeohashEncodeDecode(): {
  'invalid': GeohashBboxType | null
  'paris': string
  'parisBbox': GeohashBboxType | null
  'parisCoarse': string
  'parisCoarseBbox': GeohashBboxType | null
} {
  // encode a WGS-84 coordinate (Paris) to a geohash string at precision 7
  const paris = Geohash.encode(48.8584, 2.2945, 7);
  console.log('Paris geohash (precision 7):', paris);

  // decode the geohash back to the bounding box it represents
  const parisBbox = Geohash.decode(paris);
  console.log('Paris bbox:', parisBbox);

  // higher precision narrows the bbox
  const parisCoarse = Geohash.encode(48.8584, 2.2945, 3);
  const parisCoarseBbox = Geohash.decode(parisCoarse);
  console.log('coarse (precision 3) geohash:', parisCoarse);
  console.log('coarse bbox:', parisCoarseBbox);

  // decode returns null for an invalid geohash string
  const invalid = Geohash.decode('!!!');
  console.log('decode of an invalid geohash:', invalid);

  return { 'invalid': invalid, 'paris': paris, 'parisBbox': parisBbox, 'parisCoarse': parisCoarse, 'parisCoarseBbox': parisCoarseBbox };
}

GeohashScheme navigation

GeohashScheme implements GridSchemeInterface — navigate the cell hierarchy via parent/children/neighbors and inspect a cell's resolution. Try it live — edit the code and press Execute:

Navigating the geohash cell hierarchy
/** geohashSchemeNavigation — demonstrates GeohashScheme parent/children/neighbors/resolution. Run: npx tsx examples/geohashSchemeNavigation.ts */

import assert from 'node:assert/strict';

// #region usage
import { GeohashScheme } from '../src/index.js';

function demonstrateGeohashSchemeNavigation(): {
  'approxEdgeMeters': number
  'children': readonly string[]
  'neighbors': readonly string[]
  'parentCell': string | null
  'schemeId': string
  'tokyoCell': string
} {
  const scheme = GeohashScheme.create();
  console.log('schemeId:', scheme.schemeId);

  // encode a coordinate (Tokyo) to a cell id
  const tokyoCell = scheme.encode(35.6762, 139.6503, 5);
  console.log('Tokyo cell (precision 5):', tokyoCell);

  // parent is the same cell with one fewer character
  const parentCell = scheme.parent(tokyoCell);
  console.log('parent cell:', parentCell);

  // children are the 32 cells one precision level finer
  const children = scheme.children(parentCell ?? '');
  console.log('parent has', children.length, 'children, including:', children[0]);

  // neighbors are the (up to) 8 cells adjacent to a cell at the same precision
  const neighbors = scheme.neighbors(tokyoCell);
  console.log('Tokyo cell has', neighbors.length, 'distinct neighbors');

  // resolution approximates a cell's real-world edge length in meters
  const { approxEdgeMeters } = scheme.resolution(tokyoCell);
  console.log('approx edge length (meters):', approxEdgeMeters);

  return { 'approxEdgeMeters': approxEdgeMeters, 'children': children, 'neighbors': neighbors, 'parentCell': parentCell, 'schemeId': scheme.schemeId, 'tokyoCell': tokyoCell };
}
// #endregion usage

const result = demonstrateGeohashSchemeNavigation();

assert.equal(result.schemeId, 'geohash');
assert.equal(result.parentCell, result.tokyoCell.slice(0, -1));
assert.equal(result.children.length, 32);
assert.ok(result.children.includes(result.tokyoCell));
assert.ok(result.neighbors.length > 0 && result.neighbors.length <= 8);
assert.ok(!result.neighbors.includes(result.tokyoCell));
assert.ok(result.approxEdgeMeters > 0);

console.log('geohashSchemeNavigation: all assertions passed');
Output
Press Execute to run this example against the real library.

H3 navigation

H3Scheme wraps Uber's h3-js, exposing H3's hexagonal cell hierarchy (resolutions 0-15) behind the same GridSchemeInterface:

Navigating the H3 cell hierarchy
/** h3SchemeNavigation — demonstrates H3Scheme parent/children/neighbors/resolution. Run: npx tsx examples/h3SchemeNavigation.ts */

import assert from 'node:assert/strict';

// #region usage
import { H3Scheme } from '../src/index.js';

function demonstrateH3SchemeNavigation(): {
  'approxEdgeMeters': number
  'children': readonly string[]
  'neighbors': readonly string[]
  'parentCell': string | null
  'schemeId': string
  'tokyoCell': string
} {
  const scheme = H3Scheme.create();
  console.log('schemeId:', scheme.schemeId);

  // encode a coordinate (Tokyo) to a cell id at H3 resolution 9
  const tokyoCell = scheme.encode(35.6762, 139.6503, 9);
  console.log('Tokyo cell (resolution 9):', tokyoCell);

  // parent is the same cell one resolution coarser
  const parentCell = scheme.parent(tokyoCell);
  console.log('parent cell:', parentCell);

  // children are the cells one resolution finer than the parent
  const children = scheme.children(parentCell ?? '');
  console.log('parent has', children.length, 'children, including:', children[0]);

  // neighbors are the (up to 6) cells adjacent to a cell at the same resolution
  const neighbors = scheme.neighbors(tokyoCell);
  console.log('Tokyo cell has', neighbors.length, 'distinct neighbors');

  // resolution approximates a cell's real-world edge length in meters
  const { approxEdgeMeters } = scheme.resolution(tokyoCell);
  console.log('approx edge length (meters):', approxEdgeMeters);

  return { 'approxEdgeMeters': approxEdgeMeters, 'children': children, 'neighbors': neighbors, 'parentCell': parentCell, 'schemeId': scheme.schemeId, 'tokyoCell': tokyoCell };
}
// #endregion usage

const result = demonstrateH3SchemeNavigation();

assert.equal(result.schemeId, 'h3');
assert.notEqual(result.parentCell, null);
assert.ok(result.children.length > 0);
assert.ok(result.children.includes(result.tokyoCell));
assert.ok(result.neighbors.length >= 5 && result.neighbors.length <= 6);
assert.ok(!result.neighbors.includes(result.tokyoCell));
assert.ok(result.approxEdgeMeters > 0);

console.log('h3SchemeNavigation: all assertions passed');
Output
Press Execute to run this example against the real library.

S2 navigation

S2Scheme wraps s2js, a pure-JS port of Google's S2 geometry library, exposing S2's cube-sphere cell hierarchy (levels 0-30) with native bbox covering via RegionCoverer:

Navigating the S2 cell hierarchy
/** s2SchemeNavigation — demonstrates S2Scheme parent/children/neighbors/resolution. Run: npx tsx examples/s2SchemeNavigation.ts */

import assert from 'node:assert/strict';

// #region usage
import { S2Scheme } from '../src/index.js';

function demonstrateS2SchemeNavigation(): {
  'approxEdgeMeters': number
  'children': readonly string[]
  'neighbors': readonly string[]
  'parentCell': string | null
  'schemeId': string
  'tokyoCell': string
} {
  const scheme = S2Scheme.create();
  console.log('schemeId:', scheme.schemeId);

  // encode a coordinate (Tokyo) to a cell id (an S2 cell token) at level 12
  const tokyoCell = scheme.encode(35.6762, 139.6503, 12);
  console.log('Tokyo cell (level 12):', tokyoCell);

  // parent is the cell one level coarser
  const parentCell = scheme.parent(tokyoCell);
  console.log('parent cell:', parentCell);

  // children are the 4 cells one level finer
  const children = scheme.children(parentCell ?? '');
  console.log('parent has', children.length, 'children, including:', children[0]);

  // neighbors are the 4 cells edge-adjacent to a cell at the same level
  const neighbors = scheme.neighbors(tokyoCell);
  console.log('Tokyo cell has', neighbors.length, 'distinct edge-adjacent neighbors');

  // resolution approximates a cell's real-world edge length in meters
  const { approxEdgeMeters } = scheme.resolution(tokyoCell);
  console.log('approx edge length (meters):', approxEdgeMeters);

  return { 'approxEdgeMeters': approxEdgeMeters, 'children': children, 'neighbors': neighbors, 'parentCell': parentCell, 'schemeId': scheme.schemeId, 'tokyoCell': tokyoCell };
}
// #endregion usage

const result = demonstrateS2SchemeNavigation();

assert.equal(result.schemeId, 's2');
assert.notEqual(result.parentCell, null);
assert.equal(result.children.length, 4);
assert.ok(result.children.includes(result.tokyoCell));
assert.equal(result.neighbors.length, 4);
assert.ok(!result.neighbors.includes(result.tokyoCell));
assert.ok(result.approxEdgeMeters > 0);

console.log('s2SchemeNavigation: all assertions passed');
Output
Press Execute to run this example against the real library.

MGRS navigation

MgrsScheme wraps the mgrs package, exposing the NATO-standard Military Grid Reference System (accuracy digits 0-5, 100km down to 1m). MGRS has no native prefix hierarchy, so children()/neighbors()/covering() are point-sampling reconstructions rather than trie-walks:

Navigating the MGRS cell hierarchy
/** mgrsSchemeNavigation — demonstrates MgrsScheme parent/children/neighbors/resolution. MGRS (Military Grid Reference System) is the NATO-standard UTM-based geocoordinate system. Run: npx tsx examples/mgrsSchemeNavigation.ts */

import assert from 'node:assert/strict';

// #region usage
import { MgrsScheme } from '../src/index.js';

function demonstrateMgrsSchemeNavigation(): {
  'approxEdgeMeters': number
  'children': readonly string[]
  'neighbors': readonly string[]
  'parentCell': string | null
  'schemeId': string
  'tokyoCell': string
} {
  const scheme = MgrsScheme.create();
  console.log('schemeId:', scheme.schemeId);

  // encode a coordinate (Tokyo) to a cell id at accuracy 5 (1 m resolution)
  const tokyoCell = scheme.encode(35.6762, 139.6503, 5);
  console.log('Tokyo cell (accuracy 5):', tokyoCell);

  // parent is the same cell one accuracy digit coarser
  const parentCell = scheme.parent(tokyoCell);
  console.log('parent cell:', parentCell);

  // children are cells one accuracy digit finer, reconstructed by sampling
  // the parent's bbox (MGRS has no native prefix-based children operation)
  const children = scheme.children(parentCell ?? '');
  console.log('parent has', children.length, 'sampled children, including:', children[0]);

  // neighbors are the (up to) 8 cells adjacent to a cell at the same accuracy
  const neighbors = scheme.neighbors(tokyoCell);
  console.log('Tokyo cell has', neighbors.length, 'distinct neighbors');

  // resolution approximates a cell's real-world edge length in meters
  const { approxEdgeMeters } = scheme.resolution(tokyoCell);
  console.log('approx edge length (meters):', approxEdgeMeters);

  return { 'approxEdgeMeters': approxEdgeMeters, 'children': children, 'neighbors': neighbors, 'parentCell': parentCell, 'schemeId': scheme.schemeId, 'tokyoCell': tokyoCell };
}
// #endregion usage

const result = demonstrateMgrsSchemeNavigation();

assert.equal(result.schemeId, 'mgrs');
assert.notEqual(result.parentCell, null);
assert.ok(result.children.length > 0);
assert.ok(result.neighbors.length > 0 && result.neighbors.length <= 8);
assert.ok(!result.neighbors.includes(result.tokyoCell));
assert.equal(result.approxEdgeMeters, 1);

console.log('mgrsSchemeNavigation: all assertions passed');
Output
Press Execute to run this example against the real library.

Coverage and tiling

covering() computes a minimal adaptive-precision cell set for a bounding box; cellsToPresets and tilePresets turn that into named, API-sized tiles:

ts
import { cellsToPresets, GeohashScheme, tilePresets } from '../src/index.js';

function demonstrateCoverageAndTiling(): { 'coveringCellCount': number; 'presets': readonly TilePresetType[]; 'tiles': readonly TilePresetType[] } {
  const scheme = GeohashScheme.create();

  // a bbox around Manhattan: [minLon, minLat, maxLon, maxLat]
  const manhattanBbox = [-74.02, 40.70, -73.93, 40.88] as const;

  // covering() returns a minimal adaptive-precision set of cells covering the bbox
  const coveringCells = scheme.covering([...manhattanBbox], 50);
  console.log('covering cell count:', coveringCells.length);
  console.log('first covering cell:', coveringCells[0]);

  // cellsToPresets turns cell ids into named { bbox, label } presets
  const presets = cellsToPresets(scheme, coveringCells);
  console.log('first preset:', presets[0]);

  // tilePresets splits each preset into smaller tiles no larger than maximumTileDegrees,
  // useful for paginating a downstream API that caps request bbox size
  const tiles = tilePresets(presets, 0.02);
  console.log('preset count:', presets.length, '-> tile count:', tiles.length);

  return { 'coveringCellCount': coveringCells.length, 'presets': presets, 'tiles': tiles };
}

Source on GitHub