Geohash: When Coordinates Need Neighbourhoods
Benchmark coordinates are precise—51.0447°N, 114.0719°W gets you to the exact brass cap—but they’re terrible at answering “what else is nearby?” Numeric comparisons don’t help: 51.0440°N is closer than 51.0500°N, but what about 51.0448°N, 114.0800°W? You’d need to calculate distance for every pair.
Geohash solves this by interleaving latitude and longitude bits into a single string, then encoding it as base32. The elegant part: strings that share a prefix are geographically close. c3nfkhrk and c3nfkhrm are metres apart. c3nf and c3ng are kilometres apart. Proximity becomes a string operation.
Here’s Swift encoding a benchmark location:
func geohash(_ lat: Double, _ lon: Double, precision: Int = 6) -> String {
let base32 = Array("0123456789bcdefghjkmnpqrstuvwxyz")
var (latRange, lonRange) = ((-90.0, 90.0), (-180.0, 180.0))
var hash = "", bits = 0, bit = 0
var even = true
while hash.count < precision {
let (mid, range) = even ? ((lonRange.0 + lonRange.1) / 2, &lonRange) : ((latRange.0 + latRange.1) / 2, &latRange)
if (even ? lon : lat) > mid { bit |= (1 << (4 - bits)); range.0 = mid } else { range.1 = mid }
bits += 1; even.toggle()
if bits == 5 { hash.append(base32[bit]); bits = 0; bit = 0 }
}
return hash
}
print(geohash(51.0447, -114.0719, precision: 8)) // c3nfkhrk
Perl does the same with bit manipulation:
sub geohash {
my ($lat, $lon, $prec) = (@_, 6);
my @base32 = split //, '0123456789bcdefghjkmnpqrstuvwxyz';
my ($lat_r, $lon_r) = ([-90, 90], [-180, 180]);
my ($hash, $bits, $bit, $even) = ('', 0, 0, 1);
while (length($hash) < $prec) {
my $r = $even ? $lon_r : $lat_r;
my $mid = ($r->[0] + $r->[1]) / 2;
if (($even ? $lon : $lat) > $mid) { $bit |= 1 << (4 - $bits); $r->[0] = $mid; } else { $r->[1] = $mid; }
if (++$bits == 5) { $hash .= $base32[$bit]; ($bits, $bit) = (0, 0); }
$even = !$even;
}
return $hash;
}
print geohash(51.0447, -114.0719, 8); # c3nfkhrk
The algorithm recursively bisects the world, setting bits based on which half the coordinate falls into. Five bits make one base32 character. Each added character increases precision by roughly 4-5×. Database queries for nearby benchmarks become prefix matches: WHERE hash LIKE 'c3nfkh%'. No trigonometry required.