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))