KunYu
h3-jsngeohashjavascriptspatial-indexnpm

h3-js vs ngeohash:JavaScriptの空間インデックスライブラリの選び方

h3-jsとngeohashのバンドルサイズ、エンコードベンチマーク、API比較、本番環境での落とし穴を実測データ付きでまとめました。

KunYu TeamMarch 25, 202613分で読めます

H3の六角形とGeohashの長方形、どちらを使うか決まっていなければ、先にH3 vs Geohash vs S2の比較記事を読んでください。この記事はその次のステップ、どのnpmパッケージをnpm installするかを扱います。

h3-jsngeohashは、それぞれH3とGeohashで最もダウンロードされているJavaScriptライブラリです。KunYuのGeoHash & H3変換ツールでは両方を使っています。バンドルサイズは22倍、エンコード速度は4〜6倍の差があり、本番で踏む地雷もまったく違います。実測値と、リリース後に初めて気づいた問題をまとめました。

バンドルサイズ:86 KB vs 3.8 KB

node_modulesからESモジュールのエントリーポイントを直接gzipして計測しました。

指標 h3-js v4.4.0 ngeohash v0.6.3
ESモジュール(gzip後) 86 KB 3.8 KB
パッケージ総サイズ 6.5 MB 52 KB
ランタイム依存 0 0
実装方式 C → Emscripten Pure JavaScript

22倍の差は実装方式そのものです。h3-jsはUberのCライブラリをEmscriptenでコンパイルしたもので、正二十面体投影、六角形グリッドの走査、ポリゴンクリッピングが全部1つのJSファイルに焼き込まれています。tree-shakingは効きません。{ latLngToCell }だけをインポートしても464 KBのフルバンドルが出荷されます。一方、ngeohashは手書きの6ファイル構成で、ライブラリ全体がh3-jsの型定義ファイルより小さいです。

gzip後86 KBはReact DOMとほぼ同じサイズです。サーバーサイドなら問題になりませんが、フロントエンドでたった1回のエンコードのためにReact DOMをもう1つ出荷するのは割に合いません。

エンコード速度:ngeohashが4〜6倍速い

Node.js v24、1,000回のウォームアップ後に各操作100,000回のイテレーションで計測:

操作 h3-js ngeohash
エンコード(lat/lng → セル) 850K ops/sec 4.86M ops/sec ngeohash 5.7倍
デコード(セル → lat/lng) 1.73M ops/sec 7.58M ops/sec ngeohash 4.4倍
隣接セル取得 614K ops/sec 465K ops/sec h3-js 1.3倍
境界 / BBox 550K ops/sec 9.29M ops/sec ngeohash 17倍

ベンチマークスクリプト(.mjsとして保存し、nodeで実行):

import { latLngToCell, cellToLatLng, gridDisk, cellToBoundary } from "h3-js";
import ngeohash from "ngeohash";

const ITERATIONS = 100_000;

function bench(name, fn) {
  for (let i = 0; i < 1000; i++) fn(); // warmup
  const start = performance.now();
  for (let i = 0; i < ITERATIONS; i++) fn();
  const ms = performance.now() - start;
  const opsPerSec = Math.round((ITERATIONS / ms) * 1000);
  console.log(`${name}: ${opsPerSec.toLocaleString()} ops/sec`);
}

const h3Cell = latLngToCell(37.7749, -122.4194, 9);
const hash = ngeohash.encode(37.7749, -122.4194, 6);

bench("h3-js  encode",    () => latLngToCell(37.7749, -122.4194, 9));
bench("ngeohash encode",  () => ngeohash.encode(37.7749, -122.4194, 6));
bench("h3-js  decode",    () => cellToLatLng(h3Cell));
bench("ngeohash decode",  () => ngeohash.decode(hash));
bench("h3-js  neighbors", () => gridDisk(h3Cell, 1));
bench("ngeohash neighbors", () => ngeohash.neighbors(hash));
bench("h3-js  boundary",  () => cellToBoundary(h3Cell));
bench("ngeohash bbox",    () => ngeohash.decode_bbox(hash));

単純なエンコード/デコードではngeohashが圧倒します。ピュアJSのビットインターリーブ操作はV8 JITの得意分野で、FFIオーバーヘッドもゼロ。h3-jsは呼び出しのたびに引数をEmscriptenヒープにコピーし、C関数を実行し、結果をJavaScript側にコピーバックするマーシャリングコストがかかります。

隣接セルの検索ではh3-jsが逆転します。gridDiskは正二十面体グリッドをタイトなCループで走査するため、コンパイル済みアルゴリズムの効率がシリアライゼーションコストを上回ります。

境界の行は厳密には公平な比較ではありません。cellToBoundaryは六角形の6頂点座標を計算しますが、decode_bboxは4つの数値を返すだけです。とはいえ、本番で実際に呼ぶのはこれらの関数です。

エンコード速度はライブラリ選定の決め手にならない。 数百万ポイントをリクエストごとに処理するのでなければ、h3-jsの1.2マイクロ秒/エンコードで十分です。

API比較

エンコードとデコード

import { latLngToCell, cellToLatLng } from "h3-js";
import ngeohash from "ngeohash";

// H3: resolution 9 ≈ 0.11 km²
const h3Cell = latLngToCell(37.7749, -122.4194, 9);
// → "8928308280fffff"

// Geohash: precision 6 ≈ 0.74 km²
const hash = ngeohash.encode(37.7749, -122.4194, 6);
// → "9q8yyk"

// Decode
const [lat, lng] = cellToLatLng(h3Cell);
const { latitude, longitude } = ngeohash.decode(hash);

隣接セル

import { gridDisk } from "h3-js";

// H3: center + 6 hexagonal neighbors
const h3Neighbors = gridDisk(h3Cell, 1);

// Geohash: 8 neighbors (N, NE, E, SE, S, SW, W, NW)
const ghNeighbors = ngeohash.neighbors(hash);

セル境界

import { cellToBoundary } from "h3-js";

// H3: 6 [lat, lng] vertices
const h3Boundary = cellToBoundary(h3Cell);

// Geohash: bounding box
const bbox = ngeohash.decode_bbox(hash);
// → [minLat, minLon, maxLat, maxLon]

decode_bboxの戻り値は[minLat, minLon, maxLat, maxLon]、つまり南・西・北・東の順です。GeoJSONの[west, south, east, north]とも、LeafletのLatLngBoundsが期待する順序とも違います。間違えると地球の反対側にバウンディングボックスが出現し、エラーは一切出ません。必ず明示的に分割代入してください:

const [minLat, minLon, maxLat, maxLon] = ngeohash.decode_bbox(hash);

h3-jsはTypeScript型定義を同梱しています。ngeohashは@types/ngeohashを別途インストールする必要があります。

本番環境での落とし穴

H3セルIDがJSONシリアライゼーションで静かに切り捨てられる

一番痛い目に遭った問題がこれです。H3のセルIDは64ビット整数で、JavaScriptの安全整数範囲(2^53 − 1)を超えるため、h3-jsは16進数文字列で返します:"8928308280fffff"

罠はAPI層にあります。バックエンドのどこかでH3 IDを文字列ではなく数値としてJSONに書き出すと、JSON.parseが値を静かに切り捨てます。有効だが全く別の数値になり、エラーは出ず、クエリは空の結果を返す。KunYuの開発中にサービス間でデータが合わない原因を丸一日追って、最終的にシリアライゼーションだったと判明しました。

修正は単純ですが徹底が必要です。データベースではstring型カラム、APIスキーマではstringフィールド、TypeScriptインターフェースではstring型。H3 IDは全レイヤーでstringとして扱う。例外なし。

ngeohashの整数エンコードは52ビットで上限

encode_intdecode_intはBase32文字列の代わりに整数を返しますが、上限は52ビット(IEEE 754 float64の安全整数範囲)です。bitDepthを52より大きく渡しても精度が静かに失われるだけで、エラーは出ません。

Redisも内部的に52ビットのGeohash整数を使うため、Redis連携では問題になりません。ただしPythonやJavaライブラリから64ビットGeohash整数を使うロジックを移植すると値が合いません。末尾の数ビットだけが違うため結果が「近い値」に見え、デバッグが厄介です。

ngeohashと他のGeohashライブラリが一致しないことがある

同じ座標でも、Geohash実装によって結果が異なることがあります。原因は浮動小数点の丸め処理。座標がセル境界のちょうど上にある場合、ライブラリによって隣のセルに丸められます。

KunYuのGeohashツールを作っているときにこの問題に当たりました。フロントエンドのngeohash出力とバックエンドのPython geohashが時々一致しない。頻度は低いですが、実際に起きます。GPS座標(精度約3メートル)を数百メートルのセルにエンコードする用途なら実害はほぼありません。ただしNode.jsとPython/Redisが完全に一致する必要がある場合は、既知のセル境界上の座標で統合テストを書いてください。RedisのGEOHASHコマンドがクロスシステム一貫性の最も信頼できるリファレンス実装です。

h3-js v3 → v4:全関数がリネームされた

まだv3を使っているプロジェクトでは、v4でほぼ全関数名が変わります:

v3 v4
geoToH3 latLngToCell
h3ToGeo cellToLatLng
kRing gridDisk
hexRing gridRingUnsafe
compact compactCells

v4ではエラーハンドリングも変わりました。無効な入力でnullを返す代わりにスローするようになっています。h3-js/legacyに互換レイヤーがありますが、エラー動作はv4のセマンティクスです。個人的には、レガシーラッパーに頼るよりも一括で検索・置換してしまう方がクリーンだと思います。完全なリネーム対応表はv3→v4マイグレーションガイドを参照してください。

GeoHash変換

GeoHash、H3、Plus Code間の変換。

Try it now

シナリオ別の選び方

シナリオ 選択 理由(一行)
Redis GEO / Elasticsearch ngeohash Redisは内部でGeohashを使用——動作が一致する
フロントエンドの近接検索 ngeohash 3.8 KB vs 86 KB
六角形ヒートマップ / Deck.gl h3-js Deck.glがH3HexagonLayerをネイティブサポート
グローバル集計(均一なセル面積) h3-js H3のセル面積は緯度間で3%未満の差
ログで読みやすいID ngeohash "9q8yyk""8928308280fffff"より読みやすい
ポリゴンカバレッジ / マルチ解像度 h3-js polygonToCellscompactCells、親子階層
サーバーサイドバッチ処理 どちらでも 両方とも十分に高速——ライブラリではなくインデックス体系で選ぶ

JavaScriptでのS2は正直厳しいです。s2-geometryは8年間更新されておらず、nodes2tsは完成度は高いもののコミュニティが小さい。S2が必要ならPython/Goの公式ライブラリでマイクロサービスを立てるのが現実的です。

ブラウザでGeohashとH3のエンコードを試すなら、KunYuのGeoHash & H3変換ツールで座標を貼り付けてセルIDを確認し、境界を地図上で可視化できます。

GeoHash変換

GeoHash、H3、Plus Code間の変換。

Try it now

FAQ

h3-jsはWebAssemblyを使っていますか?

いいえ。Cからのコンパイルですが、Emscriptenが出力するのはプレーンなJavaScriptで、.wasmファイルは含みません。wasmファイルの別途ロードが不要で全モダンブラウザで動く反面、バンドルサイズ(gzip後86 KB)と呼び出しオーバーヘッドはネイティブwasmより大きくなります。コミュニティでwasmビルドの議論はありますが、公式な計画は今のところありません。

ngeohashにはESMエクスポートがありますか?

ありません。パッケージはCommonJSのみです。Vite、webpackなどのバンドラーがCJS → ESM変換を自動で処理するため、バンドル済みプロジェクトならimport ngeohash from "ngeohash"で動きます。"type": "module"を指定したピュアESMのNode.jsプロジェクトではcreateRequireかコミュニティフォークが必要です。週間10万以上のnpmダウンロードがありますが、最終リリースは2020年です。

同じプロジェクトで両方のライブラリを使えますか?

はい、競合しません。バックエンドでH3を使って空間集計し、フロントエンドではngeohashで軽量なジオフェンスチェックを行い、それぞれのセルIDをAPIでやり取りするのはよくあるパターンです。2つのインデックスシステムは独立して動くため、相互変換は不要です。KunYuでもまさにこの構成で運用しています。