GeoHash 指南
本页目录
如果要查询附近的地点,逐个计算所有地点与查询点的距离显然很浪费。GeoHash 提供了一种候选筛选方式:把经纬度编码为字符串,用网格把大部分无关地点先排除,再计算精确距离。
本文整理自 2016 年的笔记,以文字推导和可运行代码替换缺失的旧截图,并修正编码、精度和相邻区域的说明。
把二维坐标交错编码
经度范围是 ,纬度范围是 。每次把一个区间平分:坐标在较低的一半记 0,在较高的一半记 1,再对选中的那一半继续二分。
标准 GeoHash 从经度开始,经纬度轮流贡献一个二进制位。每五个位组成一个数,再用下面的专用字符表转换为字符:
0123456789bcdefghjkmnpqrstuvwxyz
例如广州的一组坐标是北纬 23.1291、东经 113.2644。前五位交错编码为 11100,十进制为 28,对应字符 w。继续二分,前四个字符是 ws0e。
GeoHash 使用的是编码,不是加密。知道字符串就可以恢复对应的经纬度范围,不能靠它保护精确位置隐私。
一个可以验证的编码器
下面只实现编码,用来理解二分过程;输入采用纬度、经度、字符串长度的顺序。
def geohash_encode(latitude, longitude, precision=6):
if not (-90 <= latitude <= 90 and -180 <= longitude < 180):
raise ValueError('坐标超出范围')
if not isinstance(precision, int) or not 1 <= precision <= 12:
raise ValueError('编码长度应为 1 到 12 的整数')
alphabet = '0123456789bcdefghjkmnpqrstuvwxyz'
bounds = [[-180.0, 180.0], [-90.0, 90.0]]
coordinates = [longitude, latitude]
result = []
value = 0
for index in range(precision * 5):
axis = index % 2
low, high = bounds[axis]
middle = (low + high) / 2
bit = int(coordinates[axis] >= middle)
bounds[axis][1 - bit] = middle
value = value * 2 + bit
if index % 5 == 4:
result.append(alphabet[value])
value = 0
return ''.join(result)
assert geohash_encode(23.1291, 113.2644, 4) == 'ws0e'
一个长度为 的编码包含 个二进制位。经度分得 位,纬度分得 位。因此,网格的角度宽度和高度可以直接计算:
角度换算为地面距离还与纬度有关,不能把某个编码长度写成全球固定的“误差几米”。Elasticsearch 的 GeoHash 网格文档也按网格与精度来描述它的用途。
前缀与附近不是同一个概念
相同前缀表示落在同一个更大的网格里,但相邻的点可能被网格边界分开,拥有很不相同的前缀。只搜索查询点所在格,会漏掉边界另一侧的邻居。
查询时应先根据搜索半径确定需要覆盖的网格,再收集候选点,最后按原始经纬度计算距离。当前格加周围八格是一种常见局部覆盖方式,但是否足够取决于半径和网格大小,不能对任意半径都只取九格。
保存原始坐标,编码只作辅助
在关系数据库中,可以用前缀条件表达候选范围,例如 WHERE geohash LIKE 'ws0e%'。是否走索引要看数据库、排序规则、索引类型与执行计划;写了前缀条件不等于已经高效。
使用空间数据库时,也可以让空间索引和距离查询处理这项工作。手动保存 GeoHash 的价值主要在于明确分区、聚合或检索策略,不应为了采用某种编码而重复建设已有能力。
应用时要写清楚的约定
接口要约定坐标顺序和坐标系,避免把经纬度颠倒。编码长度决定候选区域大小,搜索半径决定覆盖范围,二者应分别传递。跨日期变更线和极区还需要专门检查边界处理。
原文参考的实现包括 mmcloughlin/geohash。无论使用哪个库,先验证已知坐标、边界两侧和目标半径内外的点,再把结果接到业务查询中。
相关文章
- SuperMemo 与 Anki:间隔重复算法笔记2014-06-04
- 重新认识软件测试:从二度人脉设计测试用例2013-08-31
- Fast Unfolding:用模块度发现社群2018-09-16
- 用 ApacheBench 做简单的 HTTP 压测2026-11-06