geohash 的原理及应用

geohash 是 Gustavo Niemeyer发明的一种对地理位置进行编码的算法, 是一种分层的空间数据结构,核心原理是将分成网格状的区域。并将一个区域编码成一定长度的字符串。 例如 将坐标 (57.64911, 10.40744)(lat, lon) 编码成11 位的 geohash 得到了4pruydqqvj。

geohash 的特点

  • 纬度和经度是二维的数据,建索引的时候需要对纬度和经度同时建索引,geohash 能将二维的数据转化为一维的数据,可以用B树等索引
  • geohash 生成的字符串代表的不是地图上的一个点, 而是地图上一个矩形的区域,在一定程度上能保证隐私。
  • 字符串越长,表示的范围能够更加的精确。 8位的geohash 编码的经度能达到19米左右,9位的geohash经度能达到米级,一般情况下能满足我们大部分的需求

    /*
    // *******        length        lat bits     lng bits   lat error   lng error     km error
    // *******        1            2             3          ±23         ±23           ±2500
    // *******        2            5             5          ±2.8.       ±5.6.         ±630
    // *******        3            7             8          ±0.70       ±0.70         ±78
    // *******        4            10             10         ±0.087      ±0.18.        ±20
    // *******        5            12             13         ±0.022      ±0.022        ±2.4
    // *******        6            15             15         ±0.0027     ±0.0055       ±0.61
    // *******        7            17           18         ±0.00068    ±0.00068      ±0.076
    // *******        8            20             20         ±0.00008    ±0.00017      ±0.019
    // *******        9              22             23         ±0.00002    ±0.000021     ±0.00478
    // *******        10          25             25         ±0.000002.  ±0.0000054.   ±0.0006
    // *******        11          27             28         ±0.00000067 ±0.00000067.  ±0.00015
    */     
    
  • 字符串前缀相同的越多,代表的距离也就越近(有特殊的情况)

编码

原理

接下来以将(57.64911, 10.40744)编码为11位的 geohash 为例,说明geohash的编码过程

首先对纬度和经度分别划分, 将(-90~90, -180~180)划分为四个字区域(-90~0,-180~0)
(-90~0, 0~180)(0~90, -180~90)(0~90, 0~180), 四个区域分别编码为00,01, 10, 11 (57.64911, 10.40744) 所在的区域是11, 然后对geohash 所在的区域进行继续编码,最后得到一个56位的比特串(后面将会说明为什么是56位) 1101000100101011011111010111100110010110101101101110

(90, -180)        (90,180)
    | 01   |  11 |
    |______|_____|
    |      |     |
    | 00   |  10 |
(-90, -180)        (-90,180)

实现

通常的实现方式是对维度和经度分别编码,然后将经度放在偶数位,纬度放在奇数位

even := true
for i := 0; i < int(step)*2; i++ {
    bits = bits << 1
    if even {
        if m := (max_lon + min_lon) / 2; longitude > m {
            bits = bits | 0x01
            min_lon = m
        } else {
            max_lon = m
        }
    } else {
        if m := (max_lat + min_lat) / 2; latitude > m {
            bits = bits | 0x01
            min_lat = m
        } else {
            max_lat = m
        }
    }
    even = !even
}

redis (一个高性能的 key-value 数据库)的实现中,作者巧妙的利用了 IEE754 表示浮点数的原理,算法的核心代码表示如下。 (对于这个理解的不是很透测,自己用 go 语言验证了一遍,得到的结果是正确的)

double lat_offset =            //(57.64911 - -90)/(90 - -90)
       (latitude - lat_range->min) / (lat_range->max - lat_range->min);
   double long_offset =        //(10.40744 - -180)/(180 - -180)
       (longitude - long_range->min) / (long_range->max - long_range->min);

   /* convert to fixed point based on the step size */
   lat_offset *= (1 << step);
   long_offset *= (1 << step);
   hash->bits = interleave64(lat_offset, long_offset); 
   // interleave 是将lat_offset he long_offset 分别放到偶数和奇数位上去,

编码

在得到上述的二进制串之后,我们采用一种。base32 的编码方式,对字符串进行编码

Decimal    0    1    2    3    4    5    6    7    8    9    10    11    12    13    14    15
Base 32    0    1    2    3    4    5    6    7    8    9    b    c    d    e    f    g

Decimal    16    17    18    19    20    21    22    23    24    25    26    27    28    29    30    31
Base 32    h    j    k    m    n    p    q    r    s    t    u    v    w    x    y    z

如上表所示,将26个字母去掉 a、 i、 l、 o 自个字母再加上 0~9 一共 32个字符,刚好能代表 5 bit 的数据 (2^5 = 32). 对于我们得到的 56 位二进制串,我们能最高编码11 位的 hashstr 4pruydqqvj

geohash 的 go 实现

如果你觉得本文对你有帮助,欢迎打赏!