Here's a simple Geohash encode/decode implementation. The decoding function pretty closely follows the technique layed out on the Geohash Wikipedia entry.
from fractions import Fraction
"""
First decode the special geohash variant of base32 encoding.
Each encoded digit (0-9-b..z) (not continuous abc) is a 5 bit val 0,1,2...,30,31.
In the resulting bitstream, every second bit is now for latitude and longtitude.
Initially the latitutde range is -90,+90.
When a latitude bit is 1, then it now starts at the mid of these.
Else if 0 it now ends at the mid of these.
Same for longtitude but with range -180,+180.
"""
def decode_geohash(s):
alphabet_32ghs = "0123456789bcdefghjkmnpqrstuvwxyz"
dec_from_32ghs = dict()
for i, c in enumerate(alphabet_32ghs):
dec_from_32ghs[c] = i
bits = 0 # Integer representation of hash.
bit_cnt = 0
for c in s:
bits = (bits << 5) | dec_from_32ghs[c]
bit_cnt += 5
# Every second bit is longtitude and latitude. Digits in even positions are latitude.
lat_bits, lon_bits = 0, 0
lat_bit_cnt = bit_cnt // 2
lon_bit_cnt = lat_bit_cnt
if bit_cnt % 2 == 1:
lon_bit_cnt += 1
for i in range(bit_cnt):
cur_bit_pos = bit_cnt - i
cur_bit = (bits & (1 << cur_bit_pos)) >> cur_bit_pos
if i % 2 == 0:
lat_bits |= cur_bit << (cur_bit_pos//2)
else:
lon_bits |= cur_bit << (cur_bit_pos//2)
lat_start, lat_end = Fraction(-90), Fraction(90)
for cur_bit_pos in range(lat_bit_cnt-1, -1, -1):
mid = (lat_start + lat_end) / 2
if lat_bits & (1 << cur_bit_pos):
lat_start = mid
else:
lat_end = mid
lon_start, lon_end = Fraction(-180), Fraction(180)
for cur_bit_pos in range(lon_bit_cnt-1, -1, -1):
mid = (lon_start + lon_end) / 2
if lon_bits & (1 << cur_bit_pos):
lon_start = mid
else:
lon_end = mid
return float(lat_start), float(lat_end), float(lon_start), float(lon_end)
# Inspired by https://www.factual.com/blog/how-geohashes-work/
def encode_geohash(lat, lon, bit_cnt):
if bit_cnt % 5 != 0:
raise ValueError("bit_cnt must be divisible by 5")
bits = 0
lat_start, lat_end = Fraction(-90), Fraction(90)
lon_start, lon_end = Fraction(-180), Fraction(180)
for i in range(bit_cnt):
if i % 2 == 0:
mid = (lon_start + lon_end) / 2
if lon < mid:
bits = (bits << 1) | 0
lon_end = mid
else:
bits = (bits << 1) | 1
lon_start = mid
else:
mid = (lat_start + lat_end) / 2
if lat < mid:
bits = (bits << 1) | 0
lat_end = mid
else:
bits = (bits << 1) | 1
lat_start = mid
print("bits: {:>b}".format(bits))
# Do the special geohash base32 encoding.
s = ""
alphabet_32ghs = "0123456789bcdefghjkmnpqrstuvwxyz"
for i in range(bit_cnt // 5):
idx = (bits >> i*5) & (1 | 2 | 4 | 8 | 16)
s += alphabet_32ghs[idx]
return s[::-1]
print(decode_geohash("ezs42"))
print(decode_geohash("9q8y"))
print(encode_geohash(37.7, -122.5, 20))