geohash(ジオハッシュ)は緯度経度でどこ? — xn76urx6 の読み方と、近傍検索で境界をまたぐ落とし穴

,

geohash(ジオハッシュ)は緯度経度でどこ? — xn76urx6 の読み方と、近傍検索で境界をまたぐ落とし穴

geohash(ジオハッシュ)は緯度経度でどこ? — xn76urx6 の読み方と、近傍検索で境界をまたぐ落とし穴

geohash は緯度経度を「交互のビット」に刻んで、32文字の記号に置き換えたものです。 xn76urx6 は東京駅付近の約19m×31mの区画を指します。最大の特徴は文字列の前方一致が、地図上の近さにほぼ対応すること——だからデータベースの索引と相性がよい。ただし「ほぼ」の部分に、近傍検索でつまずく落とし穴があります。

この記事は Plus Code(プラスコード)は緯度経度でどこ? で表の1列として触れた geohash を、データベースで使う側の視点から掘り下げたものです。「人が読み上げて伝える」Plus Code とは狙いが違うので、注意すべき点も違います。

geohash とは何か

2008年に Gustavo Niemeyer 氏が geohash.org とともに公開した位置参照コードです。仕様は単純で、外部データもサーバも要りません。

  • 緯度経度を2分探索のビット列に変換し、5ビットずつ base32 の1文字にする
  • 文字を増やすほど細かい区画になる。短いコードは長いコードの前方部分になる
  • 使う文字は 0123456789bcdefghjkmnpqrstuvwxyz の32種。a・i・l・o は読み間違いを避けるため外されている

Plus Code が「人が電話で復唱できること」を狙ったのに対し、geohash は機械が文字列比較で空間を扱えることを狙っています。

読み方:経度・緯度の順にビットを交互に取る

手順はこうです。

  1. 経度の範囲 [-180, 180] を半分に割り、対象が上半分なら 1、下半分なら 0
  2. 緯度の範囲 [-90, 90] を半分に割り、同じく 1 / 0
  3. これを経度→緯度→経度→緯度…と交互に繰り返してビット列を作る
  4. 5ビットたまるごとに、その値(0〜31)を base32 の1文字へ

文字と番号の対応は次のとおりです。

 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
 0  1  2  3  4  5  6  7  8  9  b  c  d  e  f  g
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
 h  j  k  m  n  p  q  r  s  t  u  v  w  x  y  z

東京駅で1文字目を作ってみる

緯度 35.681236・経度 139.767125(WGS84)で、最初の5ビットを取ります。

bit1(経度): [-180, 180]  中点 0      → 139.77 > 0      → 1  範囲を [0, 180] へ
bit2(緯度): [-90, 90]    中点 0      → 35.68 > 0       → 1  範囲を [0, 90] へ
bit3(経度): [0, 180]     中点 90     → 139.77 > 90     → 1  範囲を [90, 180] へ
bit4(緯度): [0, 90]      中点 45     → 35.68 < 45      → 0  範囲を [0, 45] へ
bit5(経度): [90, 180]    中点 135    → 139.77 > 135    → 1  範囲を [135, 180] へ

11101 = 29 → 'x'

同じ要領で続けると、東京駅は次のようになります。

桁数 geohash
4 xn76
6 xn76ur
8 xn76urx6
9 xn76urx66
12 xn76urx6606p

戻すときは逆をやります。各文字を5ビットに開き、偶数番目のビットを経度、奇数番目を緯度として範囲を半分ずつ絞る。返ってくるのは点ではなく矩形のセルなので、代表点にするなら中心を使います。

桁数と細かさ(日本の緯度での実寸)

よく見かける精度表は赤道基準ですが、実務で必要なのは自分の現場の緯度での値です。北緯35.68度で計算し直すと次のようになります。

桁数 セルの大きさ(度) 南北 東西(北緯35.68度)
3 1.40625 × 1.40625 約156km 約127km
4 0.17578 × 0.35156 約19.5km 約31.8km
5 0.04395 × 0.04395 約4.9km 約4.0km
6 0.00549 × 0.01099 約610m 約993m
7 0.00137 × 0.00137 約153m 約124m
8 0.00017 × 0.00034 約19.1m 約31.0m
9 0.00004 × 0.00004 約4.8m 約3.9m
10 — 約0.60m 約0.97m

奇数桁と偶数桁で縦横比が入れ替わるのが geohash の癖です。5ビットは奇数なので、1文字追加するたびに緯度側と経度側の分割回数の差が入れ替わります。「8桁で約19m」という言い方は、南北だけを見た数字です。

なぜデータベースの近傍検索に使われるのか

geohash が広く使われている理由は、前方一致がそのまま範囲検索になる点にあります。

-- 「xn76ur」で始まる = 約610m × 993m の中にある
SELECT * FROM points WHERE geohash LIKE 'xn76ur%';

緯度と経度の2つの範囲で検索すると、通常のB-treeインデックスは片方しか効きません。geohash なら1本の文字列インデックスの前方一致で済みます。PostGIS の ST_GeoHash()、Redis の GEOHASH コマンド、Elasticsearch の geohash grid 集計など、実装が広く揃っているのもこのためです。

落とし穴1:隣どうしなのに、文字列がまったく違う

ここが最大の注意点です。セルの境界をまたぐと、前方一致は一気に効かなくなります。

東京駅の少し北、北緯 35.68359375 度はちょうど区画の境界に当たります。この線を挟んで南北に11mだけ離れた2点を geohash にすると、こうなります。

地点 緯度 geohash(9桁)
境界の南 35.683544 xn76urzrd
境界の北 35.683644 xn77h2p26

一致しているのは先頭3文字の xn7 だけです。xn7 は南北約156kmのセルですから、11m離れただけで「156kmの箱の中でしか近いと言えない」状態になります。参考までに、東京駅(xn76urx6)と境界南の点(xn76urzr)は xn76ur まで6文字一致していて、こちらは実際に約500m以内です。

近傍検索で「該当なし」や「取りこぼし」が起きるのは、たいていこれが原因です。対策は決まっています。

  • 自分のセルだけでなく、周囲8セルも合わせて検索する(多くのライブラリに neighbors() 相当の関数がある)
  • 取り出したあとで実距離を計算して絞り込む。geohash はあくまで粗い一次フィルタ
  • 「前方一致の長さ」を距離の代わりに使わない。近いなら前方一致するが、逆は必ずしも成り立たない

落とし穴2:セルは正方形でも、面積一定でもない

上の表のとおり、セルは緯度経度を等分した矩形です。したがって高緯度ほど東西が詰まります。同じ8桁でも、稚内と石垣島ではセルの横幅が1割以上違います。

統計をメッシュ単位で集計する目的なら、この不均一はそのまま結果に効きます。面積あたりの密度を出したいときは、geohash のセルを「同じ広さの区画」として扱わないでください。日本国内で面積集計をするなら、標準地域メッシュコード のほうが目的に合っています。

落とし穴3:測地系は、どこにも書かれていない

geohash の仕様には測地系の定義がありません。「緯度経度をビットに刻む」としか決めていないので、入れた座標の測地系がそのまま結果の意味を決めます。

実務上は WGS84 / JGD2011 / JGD2024 のいずれかだと考えてほぼ問題ありませんが、旧日本測地系(Tokyo Datum)の座標をそのまま geohash 化すると、約400m離れた別のセルになります。8桁セルが約19m×31mですから、20セル前後ずれる計算です。

緯度経度を手入力して測地系を変換した結果の画面

古い台帳・図面から起こした座標を扱うときは、geohash にする前に世界測地系へそろえる。順番を逆にすると、文字列だけ見ても異常に気づけません。geohash は「どの測地系で作られたか」を自分では覚えてくれないからです。

Plus Code・地域メッシュとの使い分け

geohash Plus Code 標準地域メッシュコード
分割 32分割(ビット交互) 20分割の繰り返し 段階ごとに異なる
前方一致の意味 あり(索引向き) あり あり
人が読み上げる 向かない 向く(紛らわしい文字を排除) 数字のみ
主な用途 DBの近傍検索・一次フィルタ 場所の共有 統計集計
日本での既存資産 少ない 少ない 多い(国勢調査等)

どれも緯度経度の格子である点は共通です。選ぶ基準は「誰が読むか」——機械の索引なら geohash、人の口頭伝達なら Plus Code、統計の突合なら地域メッシュ、という切り分けになります。

まとめ

  • geohash は緯度経度を経度・緯度の順に交互のビットへ刻み、5ビットずつ base32 の1文字にしたもの
  • 使う文字は32種。a i l o は不採用
  • 北緯35.68度では 8桁で約19m(南北)×31m(東西)。奇数桁・偶数桁で縦横比が入れ替わる
  • 前方一致がそのまま範囲検索になるのが最大の利点。PostGIS・Redis・Elasticsearch に実装がある
  • 境界をまたぐと文字列が一気に変わる。11m離れただけで一致が3文字まで落ちる例がある。周囲8セルを併せて検索し、実距離で絞り込むこと
  • セルは正方形でも面積一定でもない。面積集計には向かない
  • 測地系は仕様に書かれていない。旧日本測地系のまま入れると約400mずれた別セルになる

出典

  • geohash.org: http://geohash.org/
  • ST_GeoHash — PostGIS マニュアル: https://postgis.net/docs/ST_GeoHash.html
  • GEOHASH — Redis コマンドリファレンス: https://redis.io/docs/latest/commands/geohash/
  • Geohash grid aggregation — Elasticsearch リファレンス: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geohashgrid-aggregation.html
  • 世界測地系 — 国土地理院: https://www.gsi.go.jp/sokuchikijun/datum-main.html

本記事で計算した値(いずれも仕様どおりの符号化・復号を python3 で実装して検算)

値 根拠
35.681236 / 139.767125 → xn76urx6 経度・緯度交互のビット列を base32 へ
8桁セル=0.00017166° × 0.00034332° 8文字=40ビットを緯度20・経度20に分け、180 ÷ 2²⁰ / 360 ÷ 2²⁰
同・南北約19.1m / 東西約31.0m 緯度側 × 111,132.95、経度側 × 111,319.49 × cos(35.68°)
35.683544 → xn76urzrd / 35.683644 → xn77h2p26 境界 35.68359375°(= 90 ÷ 2⁹ の整数倍)を挟む2点を符号化
2点間 約11m 0.0001° × 111,132.95 m/度

次に確認する


開発者より: アプリ・Kindle本・公開プロジェクトの一覧は GitHub: amru195704 にまとめています。


お願い
本記事の情報は参考目的で掲載しており、正確性・完全性を保証するものではありません。誤記・不正確な情報がございましたら、コメント欄よりご指摘いただければ、確認のうえ修正いたします。


コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です

トップへ戻る