KunYu
h3geohashs2spatial-indexgis-basics

H3 vs Geohash vs S2:如何選擇正確的空間索引?

深入比較 H3、Geohash 與 S2 空間索引:精度對照表、緯度變形問題、邊界 Bug、JavaScript 與 Python 程式碼範例,以及決策矩陣。

KunYu TeamMarch 12, 202619 分鐘閱讀

在對數百萬個 GPS 點做鄰近搜尋時,你遲早會碰到同一道牆:暴力距離掃描太慢,而原始經緯度坐標也不適合建立索引。空間網格系統透過將坐標對齊到預先計算好的格網單元來解決這個問題——查找快速、聚合簡潔、識別碼可以直接分享。Geohash 從 2008 年起就在做這件事。H3 在 Uber 2018 年開源後讓六邊形格網走入主流。S2 則是 Google 地圖內部使用的技術。這三套系統並不能互換,在原型階段看似學術性的差異,一旦資料跨越格網邊界或橫跨多個大陸,就會變成真實的問題。

什麼是空間網格索引系統?

空間網格索引系統將地球表面切割成離散的格網單元,並為每個單元指定一個唯一識別碼。你儲存的不是原始坐標,而是單元 ID——只需比較字串前綴或整數值,就能快速執行查找、範圍查詢與資料聚合。核心取捨在於單元均勻性、實作簡易度與查詢效能之間的平衡。

這三套系統分別誕生自不同的工程文化。Geohash 由 Gustavo Niemeyer 於 2008 年發明,是一套公有領域的地理編碼系統——設計上以簡單為優先。S2 約於 2011 年在 Google 開發,作為內部地圖基礎設施的幾何函式庫。H3 則由 Uber 於 2018 年開源,用於解決城市尺度的司機與乘客配對問題。

它們的格網形狀反映了各自的起源:Geohash 產生矩形,S2 產生球面四邊形(近似球面上的正方形),H3 產生六邊形(另加 12 個五邊形以封閉球面)。

各系統的運作原理

每套系統的編碼方式既決定了它的優點,也決定了它在實際環境中的失效模式。

H3 將地球投影到一個正二十面體(20 面多面體)上,再將每個面細分為六邊形。結果是 15 個解析度等級,每個單元可細分為約 7 個子單元。單元 ID 為 64 位元整數。六邊形的形狀意味著每個單元恰好有 6 個共邊鄰居——不存在共角鄰居的歧義。

Geohash 將緯度和經度的二進位表示交錯排列,再將結果編碼為 Base32 字串。這就是 Geohash 字串可以被人類閱讀的原因:u4pruydqqvj 唯一對應巴黎市中心一塊 3 m × 3 m 的區域。精度由字串長度(1 到 12 個字元)控制。前綴特性——u4pru 永遠在 u4pr 的範圍內——讓範圍查詢非常簡單直覺。

S2 將地球表面展開到正方體的六個面上,在每個面上套用 Hilbert 空間填充曲線,再將位置映射為 64 位元整數。Hilbert 曲線保留了空間局部性:地球上相近的點會產生相近的整數。S2 有 31 個解析度等級,並透過 S2RegionCoverer 支援任意多邊形的覆蓋計算——以最少的單元數近似任意形狀。

精度與覆蓋範圍——比較表

這三套系統使用不相容的精度刻度——H3 的 15 個解析度、Geohash 的 12 個字元長度,以及 S2 的 31 個等級彼此無法直接對應。下表以三個實用尺度對齊比較:城市級聚合、街區路由,以及建築物級精度。

經度的縮短程度遵循餘弦曲線,其惡化速度往往超過多數開發者的預期。在緯度 30°(洛杉磯、開羅、上海),一度經度涵蓋 96.5 公里,而一度緯度為 110.6 公里——東西方向短少了 14.9%。到了緯度 60°(斯德哥爾摩、安克拉治、聖彼得堡),比例達到 2:1:一度經度僅有 55.7 公里寬。一個在赤道覆蓋約 0.74 km² 的 6 字元 Geohash 單元,在北緯 60° 只覆蓋約 0.37 km²。如果你在建立具有統一鄰近閾值的全球服務(例如「500 公尺以內」),固定精度的 Geohash 在不同緯度會給出不一致的結果——你需要在低緯度用較長的 Hash、在極點附近用較短的 Hash,才能獲得相當的實際覆蓋範圍,這會讓查詢邏輯變得複雜。H3 和 S2 都針對球面變形做了補償:H3 在解析度 9 時,所有緯度的面積誤差不超過 3%。

尺度 H3 解析度 H3 單元面積 Geohash 長度 Geohash 單元面積 S2 等級 S2 單元面積
城市 5 ~253 km² 4 字元 ~780 km² 10 ~325 km²
街區 9 ~0.11 km² 6 字元 ~0.74 km² 15 ~0.32 km²
建築物 11 ~0.0022 km² 7 字元 ~0.023 km² 18 ~0.005 km²

鄰居搜尋與邊界效應

鄰居查詢是最常見的空間索引操作:「給定這個單元,找出所有相鄰單元。」三套系統處理這個問題的方式在使用體驗上差異很大。

H3 擁有最簡潔的鄰居模型。每個六邊形恰好有 6 個共邊鄰居——沒有共角鄰居,沒有歧義。gridDisk(cell, k) 函式在單次呼叫中返回所有 k 環以內的單元。k=1 的環傳回 7 個單元(含中心);k=2 傳回 19 個。由於六邊形各邊長度相等,以環數計算的距離查詢在幾何上是一致的。

Geohash 有 8 個鄰居(4 個共邊 + 4 個共角)。對大多數單元來說這沒有問題。惡名昭彰的問題出在格網邊界:地理上相鄰的兩個單元,其 Geohash 字串可能毫無共同前綴。例如,跨越本初子午線(東經 0°)或赤道的單元,可能有完全不同的 Hash 值。典型症狀:查詢傳回邊界一側的結果,卻悄悄遺漏了剛好在另一側的點。

一個有據可查的真實案例:法國西南部的 La Roche-Chalais 擁有 Geohash 前綴 u000,而距離僅 30 公里的 Pomerol 卻有前綴 ezzz。這兩個相近位置在任何長度下都沒有共同前綴,因為一條主要格網邊界恰好穿越其間。以前綴為基礎的「La Roche-Chalais 附近點」查詢,會悄悄返回來自 Pomerol 的零筆結果。同樣的問題也出現在格林威治子午線(東經 0°):兩個相距僅 10 公尺、位於本初子午線兩側的 GPS 點,其 Hash 分別以 e(西側)和 s(東側)開頭——沒有共同前綴,也就沒有共同的索引範圍。標準解法是永遠同時查詢目標單元加上其 8 個鄰居,再以實際的 Haversine 距離過濾。Redis 的 GEOSEARCH 指令在內部處理了這一點,這也是為什麼使用資料庫原生支援比自行實作 Geohash 鄰近邏輯更為可取。

S2 使用 Hilbert 曲線,提供強大的局部性保證——地球上相近的點會映射到索引中相近的整數。然而,「在 Hilbert 空間中相近」與「空間上相鄰」並不總是一樣。六個正方體面引入了接縫,跨面邊界計算鄰居需要謹慎處理。S2 的 S2CellId::EdgeNeighbors() 能正確處理這個問題,但實作工作量比 H3 的 gridDisk 更多。

如果你的資料橫跨地理邊界——海岸線、國界、換日線——H3 均勻的六邊形拓撲是更安全的預設選擇。像 Geohash 這類矩形格網系統,在特定的經緯度上存在接縫,而在正式環境中排查邊界遺漏問題相當令人頭痛。

何時使用哪套系統

如果你已在使用 Redis 或 Elasticsearch,就用 Geohash——鄰近搜尋開箱即用,無需額外程式碼。如果需要全球一致性或六邊形視覺化,就用 H3。只有在需要索引任意多邊形,或是在現有 Google 基礎設施旁工作時,才需要考慮 S2。下表涵蓋了常見場景:

使用情境 推薦 理由
Redis GEO 指令 / Elasticsearch geo_point Geohash 原生內建支援,零設定
共乘/外送需求熱力圖 H3 一致的單元大小 + 六邊形視覺化
全球資料聚合(氣候、遙測) H3 消除極區緯度變形
任意多邊形區域索引 S2 S2RegionCoverer 近似任意形狀
遊戲格網系統(Pokémon GO 使用此方案) S2 Niantic 的原始基礎設施選擇
快速原型與簡單地理圍欄 Geohash 學習曲線最低,識別碼可讀
多解析度分析(縮放切換) H3 清晰的父子層級結構
PostGIS / 空間資料庫 Geohash 標準 GIS 生態系統支援

生態系統成熟度往往在幾何特性之前就決定了選擇。Geohash 自 2015 年起已內建於 Redis(GEOADDGEORADIUS),Elasticsearch 的 geo_point 欄位類型也在內部使用 Geohash 進行格網聚合。如果你的技術堆疊已包含其中之一,整合 Geohash 幾乎不需要額外工作。

H3 擁有完善的 Python(h3)和 JavaScript(h3-js)函式庫、日益壯大的視覺化工具生態系統(Deck.gl 的 H3HexagonLayer),以及 DuckDB 空間擴充的第一級支援。S2 的幾何操作能力最強,但 JavaScript 支援最弱——主函式庫是 C++,其他語言的移植版由社群維護。

GeoHash 轉換

在 GeoHash、H3、Plus Code 之間轉換。

Try it now

在程式碼中使用 H3、Geohash 與 S2

這三套系統都有 JavaScript 和 Python 函式庫。以下範例涵蓋你最常用到的三個操作:將坐標編碼、尋找鄰居,以及取得格網單元的邊界多邊形。關於 h3-js 與 ngeohash 兩個 npm 套件的詳細比較(套件大小、編碼效能、正式環境踩坑),請參閱 h3-js vs ngeohash 比較指南

上線前有兩個函式庫的注意事項值得了解:

ngeohash 整數精度encode_int / decode_int 函式在 JavaScript 中限制為 52 位元(標準 IEEE 754 float64 安全整數範圍)。多數情況下這沒有問題,但如果你將 bitDepth 設定超過 52,結果會悄悄失去精度。除非有特定原因需要使用整數,否則請使用字串 API(encode / decode),並在編碼與解碼呼叫中始終傳入相同的 bitDepth 值。

h3-js 單元 ID 表示:H3 單元 ID 是 64 位元整數,超過了 JavaScript 的安全整數範圍(2^53 − 1)。h3-js 函式庫預設以字串形式返回它們(例如 "8928308280fffff")。一個常見的正式環境 Bug:如果你的 API 將 H3 ID 序列化為 JSON 數字而非字串,某些 JSON 解析器會悄悄四捨五入,導致單元 ID 損壞。請在整個技術堆疊中——包括資料庫結構、API 回應和前端程式碼——始終將 H3 ID 視為字串。

JavaScript

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

// --- H3 ---
// Encode coordinate to H3 cell (resolution 9 ≈ 0.1 km²)
const h3Cell = latLngToCell(37.7749, -122.4194, 9);
// → "8928308280fffff"

// Get all cells within 1 ring (7 cells including center)
const h3Neighbors = gridDisk(h3Cell, 1);

// Get cell boundary polygon (array of [lat, lng] pairs)
const h3Boundary = cellToBoundary(h3Cell);

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

// Decode back to coordinate
const { latitude, longitude } = ngeohash.decode(hash);

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

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

Python

import h3
import geohash  # pip install python-geohash

# --- H3 ---
cell = h3.latlng_to_cell(37.7749, -122.4194, 9)
neighbors = h3.grid_disk(cell, 1)
boundary = h3.cell_to_boundary(cell)  # list of (lat, lng) tuples

# --- Geohash ---
hash_str = geohash.encode(37.7749, -122.4194, precision=6)
decoded_lat, decoded_lng = geohash.decode(hash_str)
neighbors = geohash.neighbors(hash_str)  # dict with N/NE/E/SE/S/SW/W/NW keys

S2 在 JavaScript 中需要使用社群移植版(s2-geometry npm 套件),其更新進度落後於 C++ 函式庫。在 JS 環境中需要正式使用 S2 時,最實際的做法是呼叫 Python/Java 服務,或在 Cloudflare Workers 中使用 WASM。在 Python 中,s2sphere 函式庫提供了簡潔的介面:

import s2sphere

# Encode coordinate to S2 cell (level 15 ≈ 0.32 km²)
latlng = s2sphere.LatLng.from_degrees(37.7749, -122.4194)
cell_id = s2sphere.CellId.from_lat_lng(latlng).parent(15)

# Get 4 edge neighbors
neighbors = cell_id.edge_neighbors()

# Cover an arbitrary polygon with S2 cells
region_coverer = s2sphere.RegionCoverer()
region_coverer.min_level = 10
region_coverer.max_level = 15
covering = region_coverer.get_covering(some_s2_region)

你可以直接在瀏覽器中使用坤舆的 GeoHash 編碼工具 試驗 Geohash 與 H3 的編碼——無需安裝任何函式庫。

GeoHash 轉換

在 GeoHash、H3、Plus Code 之間轉換。

Try it now

常見問題

H3 比 Geohash 更好嗎?

對於沒有既有基礎設施限制的新專案,H3 是更好的預設選擇:單元大小一致、鄰居查詢更簡潔,且不存在邊界 Bug。當你需要 Redis GEO 指令或 Elasticsearch 聚合開箱即用,或是識別碼的可讀性有重要意義時,Geohash 勝出——Geohash 字串在日誌中容易閱讀;H3 的 ID 如 8928308280fffff 則不然。

Redis 支援 H3 嗎?

Redis 沒有原生的 H3 支援。內建的 GEO 指令(GEOADDGEORADIUSGEOSEARCH)在內部使用 Geohash。若要在 Redis 中使用 H3,需在應用程式層將坐標編碼為 H3 單元 ID,並以一般字串鍵或有序集合儲存。截至 Redis 7.x,核心伺服器並無計劃加入 H3 原語。

S2 代表什麼?

S2 是 Sphere²(球的平方)的縮寫,指的是將球面映射到二維平面的數學概念。該函式庫由 Google 開發,並在 Google 地圖、Google Earth 及其他 Google 地理產品中內部使用。

在解析度 5 下,H3 格網涵蓋地球需要多少個單元?

在解析度 5 下,共有 2,016,842 個 H3 單元覆蓋地球表面(包含封閉正二十面體所需的 12 個五邊形)。每個單元涵蓋約 252.9 km²,此解析度適合國家級或大型都會區的分析。

我可以在 H3 和 Geohash 之間相互轉換嗎?

沒有直接的數學轉換方式。兩套格網是獨立的——解析度 9 的 H3 六邊形與 6 字元的 Geohash 單元覆蓋不同區域,只能近似對應。實際做法是將任一格式解碼為經緯度坐標,再重新編碼為目標格式。請注意,由於大小和形狀的差異,轉換後的單元可能無法完全包含原始單元。

Geohash 的最大精度是多少?

Geohash 規格最多支援 12 個字元,對應約 3.7 cm × 1.9 cm 的單元大小。實際上,大多數應用使用 6 到 9 個字元(從 1 km² 到約 7 m²)。超過 9 個字元後,精度已超出多數 GPS 硬體所能提供的範圍,而過長的字串鍵會造成不必要的儲存和索引負擔。