Geohash

Geohash란?

Geohash 시스템은 다음과 같은 캐릭터셋으로 base32 인코딩을 사용한다

0123456789bcdefghjkmnpqrstuvwxyz

각각의 문자들은 그리드 격자에서 더 작은 섹션을 나타낸다. 문자를 하나 더 추가하는 것은 해당 영역에 precision을 증가시켜서 영역을 나누는 것을 의미한다.

preicision을 증가시키는 예제 (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)

4x4 그리드 격자를 나타내는 geohash를 구하는 예제(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 예제

4x4 서브셋 구하기

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

4x8 서브셋 구하기

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

geohash 관련 유용한 npm library

ngeohash

https://github.com/sunng87/node-geohash

commonjs 로 작성된 pure js 코드라서, 노드에서 사용하기 편리하다

만약 타입을 지원하고 싶다면, @types/ngeohash 를 인스톨 하면 될 것이다

latlon-geohash

https://github.com/chrisveness/latlon-geohash

ES6 문법으로 작성되어 있어서, commonjs 로 이루어진 프로젝트라면 바로 사용이 불가능 하다

package.json에서 따로 ESM 처리가 되어있지 않다

마찬가지로 타입 지원을 원한다면 @types/latlon-geohash 를 인스톨 하면 될 것이다

geohash 가 동작하는 방식

지구를 grid cell로 나눈다

위도와 경도 (latitude, longitude) 값을 끼워넣는다

binary를 base32 로 컨버팅한다

preicision을 조정한다. 더 긴 hash의 문자열 길이 값일 수록 더 정확함을 의미한다

typescript geohash 알고리즘

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 {
                const mid = (latRange.min + latRange.max) / 2;
                if (bit === 1) {
                    latRange.min = mid;
                } else {
                    latRange.max = mid;
                }
            }

            even = !even;
        }
    }

    const lat = (latRange.min + latRange.max) / 2;
    const lng = (lngRange.min + lngRange.max) / 2;

    return {
        lat: lat,
        lng: lng,
        bounds: {
            minLat: latRange.min,
            maxLat: latRange.max,
            minLng: lngRange.min,
            maxLng: lngRange.max
        }
    };
}


// Example usage
const geohash = encode(37.7749, -122.4194);  // Encode San Francisco coordinates
console.log(`Geohash: ${geohash}`);

const location = decode(geohash);  // Decode the geohash back into coordinates
console.log(`Decoded Location: Lat ${location.lat}, Lng ${location.lng}`);

참고) BITS 변수는 base32 와 연관 되어있다

유용한 웹사이트

  1. geohash explorer: https://geohash.softeng.co/wydm6
      1. geohash visualizer
  2. geohash converter: http://geohash.co/
      1. convert geohash by lat, lng, geohash and precision

출처

  • ChatGPT

← Go home