Blog, COLDSURF

Geohash

What is Geohash?

The Geohash system uses the following character set for base32 encoding:

0123456789bcdefghjkmnpqrstuvwxyz

Each character in the set represents a smaller section in a grid. Adding one more character to a Geohash increases the precision by subdividing the region further.

Example of Increasing Precision (Python)

import geohash

# Initial geohash
initial_geohash = "wydm"

# Increase precision by one level
new_geohash = geohash.encode(geohash.decode(initial_geohash), precision=len(initial_geohash) + 1)
print(new_geohash)

Example of Getting a Geohash for a 4x4 Grid (Python)

# Base32 character set used in geohashes, arranged for a 4x4 grid selection
base32_grid = [
    "0123",
    "4567",
    "89bc",
    "defg",
    "hjk",
    "mnpq",
    "rst",
    "vwxyz"
]

# Initial geohash
initial_geohash = "wydm"

# Function to get a 4x4 grid subset
def get_4x4_geohashes(initial_geohash):
    more_precise_geohashes = []
    # Iterate over the rows and columns of the grid
    for row in range(4):  # Adjust the range to get the row block you want (e.g., first 4 rows)
        for col in range(4):  # Adjust the range to get the column block you want (e.g., first 4 columns)
            more_precise_geohashes.append(initial_geohash + base32_grid[row][col])
    return more_precise_geohashes

# Get the 4x4 geohashes subset
subset_geohashes = get_4x4_geohashes(initial_geohash)

# Print the narrowed-down geohashes
for geohash in subset_geohashes:
    print(geohash)

TypeScript Geohash Example

Getting a 4x4 Subset

// Base32 character set used in geohashes, arranged for a 4x4 grid selection
const base32Grid: string[] = [
  "0123",
  "4567",
  "89bc",
  "defg",
  "hjk",
  "mnpq",
  "rst",
  "vwxyz"
];

// Initial geohash
const initialGeohash: string = "wydm";

// Function to get a 4x4 grid subset
function get4x4Geohashes(initialGeohash: string): string[] {
  const morePreciseGeohashes: string[] = [];

  // Iterate over the rows and columns of the grid
  for (let row = 0; row < 4; row++) {  // Adjust the range to get the row block you want (e.g., first 4 rows)
    for (let col = 0; col < 4; col++) {  // Adjust the range to get the column block you want (e.g., first 4 columns)
      morePreciseGeohashes.push(initialGeohash + base32Grid[row][col]);
    }
  }
  return morePreciseGeohashes;
}

// Get the 4x4 geohashes subset
const subsetGeohashes: string[] = get4x4Geohashes(initialGeohash);

// Print the narrowed-down geohashes
subsetGeohashes.forEach(geohash => console.log(geohash));

Getting a 4x8 Subset

// Base32 character set used in geohashes, arranged for a grid selection
const base32Grid: string[] = [
  "0123",  // Row 1
  "4567",  // Row 2
  "89bc",  // Row 3
  "defg",  // Row 4
  "hjk",   // Row 5
  "mnpq",  // Row 6
  "rst",   // Row 7
  "vwxyz"  // Row 8
];

// Initial geohash
const initialGeohash: string = "wydm";

// Function to get a 4x8 grid subset
function get4x8Geohashes(initialGeohash: string): string[] {
  const morePreciseGeohashes: string[] = [];

  // Iterate over 4 rows and 8 columns of the grid
  for (let row = 0; row < 4; row++) {  // First 4 rows
    for (let col = 0; col < 8; col++) {  // All 8 columns
      morePreciseGeohashes.push(initialGeohash + base32Grid[row][col % 4]);  // Use modulus for column wrap
    }
  }
  return morePreciseGeohashes;
}

// Get the 4x8 geohashes subset
const subsetGeohashes: string[] = get4x8Geohashes(initialGeohash);

// Print the narrowed-down geohashes
subsetGeohashes.forEach(geohash => console.log(geohash));

Useful npm Libraries for Geohash

ngeohash

  • GitHub: ngeohash

This is a pure JavaScript code written in CommonJS format, making it easy to use in Node.js environments.

If you want type support, you can install @types/ngeohash.

latlon-geohash

This library is written in ES6 syntax, so it might not work directly in CommonJS-based projects unless special handling for ESM is applied in the package.json.

To use type support, install @types/latlon-geohash.

How Geohash Works

  1. The Earth is divided into grid cells.
  2. Latitude and longitude values are inserted into these cells.
  3. The values are then converted into binary and encoded into base32.
  4. The precision is adjusted ā€” the longer the hash string, the more precise the location.

TypeScript Geohash Algorithm

const BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz";  // Geohash base32 map
const BITS = [16, 8, 4, 2, 1];

interface LatLngBounds {
    minLat: number;
    maxLat: number;
    minLng: number;
    maxLng: number;
}

/**
 * Encodes latitude and longitude into a Geohash string
 * @param lat Latitude value
 * @param lng Longitude value
 * @param precision The length of the resulting Geohash string (default: 12)
 * @returns Geohash string
 */
function encode(lat: number, lng: number, precision: number = 12): string {
    let idx = 0;  // Index in BASE32 map
    let bit = 0;  // Current bit in BITS array
    let even = true;  // Alternating between latitude and longitude
    let geohash = '';  // Resulting geohash string

    let latRange = { min: -90.0, max: 90.0 };
    let lngRange = { min: -180.0, max: 180.0 };

    while (geohash.length < precision) {
        if (even) {
            const mid = (lngRange.min + lngRange.max) / 2;
            if (lng >= mid) {
                idx = idx | BITS[bit];
                lngRange.min = mid;
            } else {
                lngRange.max = mid;
            }
        } else {
            const mid = (latRange.min + latRange.max) / 2;
            if (lat >= mid) {
                idx = idx | BITS[bit];
                latRange.min = mid;
            } else {
                latRange.max = mid;
            }
        }

        even = !even;
        if (bit < 4) {
            bit++;
        } else {
            geohash += BASE32[idx];
            bit = 0;
            idx = 0;
        }
    }

    return geohash;
}

/**
 * Decodes a Geohash string back into latitude and longitude
 * @param geohash Geohash string
 * @returns An object containing latitude, longitude, and the bounds
 */
function decode(geohash: string): { lat: number, lng: number, bounds: LatLngBounds } {
    let even = true;
    let latRange = { min: -90.0, max: 90.0 };
    let lngRange = { min: -180.0, max: 180.0 };

    for (let i = 0; i < geohash.length; i++) {
        const char = geohash[i];
        const idx = BASE32.indexOf(char);

        for (let j = 0; j < 5; j++) {
            const bit = (idx >> (4 - j)) & 1;

            if (even) {
                const mid = (lngRange.min + lngRange.max) / 2;
                if (bit === 1) {
                    lngRange.min = mid;
                } else {
                    lngRange.max = mid;
                }
            } else {
ā† Go home