Introduction

Geohash is a geocoding system that encodes geographic coordinates (latitude and longitude) into short strings of letters and digits. Developed by Gustavo Niemeyer in 2008, Geohash provides an efficient way to represent spatial data, enable proximity searches, and create spatial indexes. This guide explores the core principles, implementation details, and practical applications of Geohash technology.

📋 Table of Contents

Key Takeaways

  • What is Geohash? A system that converts geographic coordinates into short, indexable strings.
  • How it Works: It uses bit interleaving of latitude and longitude and Base32 encoding to create a hierarchical grid system.
  • Why Use It? It can generate database-friendly candidate keys; exact proximity still requires neighboring-cell coverage and distance or spatial predicates.
  • Precision: The length of the Geohash string determines its accuracy, with longer strings representing smaller, more precise areas.
  • Implementations: This guide provides code examples in JavaScript, Python, and Java.
  • Limitations: Nearby points can have different prefixes at cell boundaries; polar distortion, antimeridian wrapping, and query geometry require explicit handling.

How Geohash Works

The encoding process involves:

  1. Coordinate Normalization: Convert latitude (-90 to 90) and longitude (-180 to 180) to binary representations.
  2. Bit Interleaving: Alternate bits from latitude and longitude.
  3. Base32 Encoding: Convert the interleaved bits to Base32 characters.
  4. Precision Control: Determine the desired accuracy level.

The core innovation of Geohash is the interleaving of latitude and longitude bits. This process creates a one-dimensional index from two-dimensional data, which is key to its efficiency.

Geohash Precision Levels

The length of a Geohash string determines its precision. Each additional character increases the accuracy of the location.

Characters Latitude half-span Longitude half-span at equator Typical cell scale
1 ±23° ±23° Continent-scale
2 ±2.8° ±5.6° Country-scale
3 ±0.70° ±0.70° Regional
4 ±0.087° ±0.087° City-scale
5 ±0.022° ±0.022° Local-area
6 ±0.0027° ±0.0055° Neighborhood-scale
7 ±0.00068° ±0.00068° Street-scale
8 ±0.000085° ±0.00017° Building-scale
9 ±0.000021° ±0.000021° Fine-grained
10 ±0.0000027° ±0.0000054° Application-specific
11 ±0.00000067° ±0.00000067° Application-specific
12 ±0.00000008° ±0.00000017° Application-specific

Geohash Implementation

Here are examples of how to implement Geohash encoding and decoding in popular programming languages.

JavaScript Implementation

javascript
class Geohash {
  static encode(latitude, longitude, precision = 9) {
    if (!Number.isFinite(latitude) || latitude < -90 || latitude > 90 ||
        !Number.isFinite(longitude) || longitude < -180 || longitude > 180) {
      throw new Error('Latitude/longitude are outside valid WGS84 bounds');
    }
    if (precision < 1 || precision > 12) {
      throw new Error('Precision must be between 1 and 12');
    }

    let latMin = -90.0;
    let latMax = 90.0;
    let lonMin = -180.0;
    let lonMax = 180.0;
    
    let bit = 0;
    let bits = 0;
    let geohash = '';
    
    const base32 = '0123456789bcdefghjkmnpqrstuvwxyz';
    
    while (geohash.length < precision) {
      if (bit % 2 === 0) {
        // Even bit: longitude
        const lonMid = (lonMin + lonMax) / 2;
        if (longitude >= lonMid) {
          bits = (bits << 1) + 1;
          lonMin = lonMid;
        } else {
          bits = (bits << 1) + 0;
          lonMax = lonMid;
        }
      } else {
        // Odd bit: latitude
        const latMid = (latMin + latMax) / 2;
        if (latitude >= latMid) {
          bits = (bits << 1) + 1;
          latMin = latMid;
        } else {
          bits = (bits << 1) + 0;
          latMax = latMid;
        }
      }
      
      bit++;
      
      if (bit % 5 === 0) {
        geohash += base32[bits];
        bits = 0;
      }
    }
    
    return geohash;
  }

  static decode(geohash) {
    const base32 = '0123456789bcdefghjkmnpqrstuvwxyz';
    
    let latMin = -90.0;
    let latMax = 90.0;
    let lonMin = -180.0;
    let lonMax = 180.0;
    
    let bit = 0;
    
    for (let i = 0; i < geohash.length; i++) {
      const char = geohash[i];
      const bits = base32.indexOf(char);
      if (bits === -1) {
        throw new Error('Invalid Geohash character');
      }
      
      if (bits === -1) {
        throw new Error('Invalid Geohash character');
      }
      
      for (let j = 4; j >= 0; j--) {
        const mask = 1 << j;
        
        if (bit % 2 === 0) {
          // Longitude bit
          if (bits & mask) {
            lonMin = (lonMin + lonMax) / 2;
          } else {
            lonMax = (lonMin + lonMax) / 2;
          }
        } else {
          // Latitude bit
          if (bits & mask) {
            latMin = (latMin + latMax) / 2;
          } else {
            latMax = (latMin + latMax) / 2;
          }
        }
        
        bit++;
      }
    }
    
    const latitude = (latMin + latMax) / 2;
    const longitude = (lonMin + lonMax) / 2;
    
    return {
      latitude,
      longitude,
      latError: (latMax - latMin) / 2,
      lonError: (lonMax - lonMin) / 2
    };
  }
}

// Example usage
const geohash = Geohash.encode(39.9288, 116.3884, 8); // "wx4g0gy6"
const coords = Geohash.decode("wx4g0gy6");

Python Implementation

python
class Geohash:
    BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz"
    
    @staticmethod
    def encode(latitude, longitude, precision=9):
        if precision < 1 or precision > 12:
            raise ValueError("Precision must be between 1 and 12")
        
        lat_min, lat_max = -90.0, 90.0
        lon_min, lon_max = -180.0, 180.0
        
        bit = 0
        bits = 0
        geohash = []
        
        while len(geohash) < precision:
            if bit % 2 == 0:
                mid = (lon_min + lon_max) / 2
                if longitude >= mid:
                    bits = (bits << 1) | 1
                    lon_min = mid
                else:
                    bits = (bits << 1) | 0
                    lon_max = mid
            else:
                mid = (lat_min + lat_max) / 2
                if latitude >= mid:
                    bits = (bits << 1) | 1
                    lat_min = mid
                else:
                    bits = (bits << 1) | 0
                    lat_max = mid
            
            bit += 1
            
            if bit % 5 == 0:
                geohash.append(Geohash.BASE32[bits])
                bits = 0
        
        return ''.join(geohash)
    
    @staticmethod
    def decode(geohash):
        lat_min, lat_max = -90.0, 90.0
        lon_min, lon_max = -180.0, 180.0
        
        bit = 0
        
        for char in geohash:
            bits = Geohash.BASE32.index(char)
            
            for j in range(4, -1, -1):
                mask = 1 << j
                
                if bit % 2 == 0:
                    if bits & mask:
                        lon_min = (lon_min + lon_max) / 2
                    else:
                        lon_max = (lon_min + lon_max) / 2
                else:
                    if bits & mask:
                        lat_min = (lat_min + lat_max) / 2
                    else:
                        lat_max = (lat_min + lat_max) / 2
                
                bit += 1
        
        lat = (lat_min + lat_max) / 2
        lon = (lon_min + lon_max) / 2
        
        return {
            'latitude': lat,
            'longitude': lon,
            'lat_error': (lat_max - lat_min) / 2,
            'lon_error': (lon_max - lon_min) / 2
        }

# Example usage
geohash = Geohash.encode(39.9288, 116.3884, 8) # "wx4g0gy6"
coords = Geohash.decode("wx4g0gy6")

Java Implementation

java
public class Geohash {
    private static final String BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz";

    public static String encode(double latitude, double longitude, int precision) {
        if (precision < 1 || precision > 12) {
            throw new IllegalArgumentException("Precision must be between 1 and 12");
        }

        double latMin = -90.0, latMax = 90.0;
        double lonMin = -180.0, lonMax = 180.0;
        
        StringBuilder geohash = new StringBuilder();
        int bit = 0;
        int bits = 0;

        while (geohash.length() < precision) {
            if (bit % 2 == 0) {
                double lonMid = (lonMin + lonMax) / 2;
                if (longitude >= lonMid) {
                    bits = (bits << 1) | 1;
                    lonMin = lonMid;
                } else {
                    bits = (bits << 1);
                    lonMax = lonMid;
                }
            } else {
                double latMid = (latMin + latMax) / 2;
                if (latitude >= latMid) {
                    bits = (bits << 1) | 1;
                    latMin = latMid;
                } else {
                    bits = (bits << 1);
                    latMax = latMid;
                }
            }
            
            bit++;
            
            if (bit % 5 == 0) {
                geohash.append(BASE32.charAt(bits));
                bits = 0;
            }
        }
        
        return geohash.toString();
    }

    public static double[] decode(String geohash) {
        double latMin = -90.0, latMax = 90.0;
        double lonMin = -180.0, lonMax = 180.0;
        
        boolean isEven = true;
        
        for (int i = 0; i < geohash.length(); i++) {
            int bits = BASE32.indexOf(geohash.charAt(i));
            if (bits < 0) {
                throw new IllegalArgumentException("Invalid Geohash character");
            }
            
            for (int j = 4; j >= 0; j--) {
                int mask = 1 << j;
                
                if (isEven) {
                    if ((bits & mask) != 0) {
                        lonMin = (lonMin + lonMax) / 2;
                    } else {
                        lonMax = (lonMin + lonMax) / 2;
                    }
                } else {
                    if ((bits & mask) != 0) {
                        latMin = (latMin + latMax) / 2;
                    } else {
                        latMax = (latMin + latMax) / 2;
                    }
                }
                isEven = !isEven;
            }
        }
        
        return new double[]{(latMin + latMax) / 2, (lonMin + lonMax) / 2};
    }
}

// Example usage
String geohash = Geohash.encode(39.9288, 116.3884, 8); // "wx4g0gy6"
double[] coords = Geohash.decode("wx4g0gy6");

Practical Applications

Geohash is used in a wide variety of applications, including:

  • Location-Based Services: Finding nearby points of interest, such as restaurants, ATMs, or friends.
  • Geospatial Indexing: Efficiently querying large datasets of geographic data in databases.
  • Proximity Searches: Powering features like "find friends nearby" in social media apps.
  • Data Aggregation: Grouping and analyzing data by geographic area.

Frequently Asked Questions (FAQ)

What is Geohash used for in real-world applications?

Geohash is widely used in location-based services like ride-sharing apps (e.g., Uber, Lyft) for matching drivers and riders, in social media for "nearby friends" features, and in databases like Elasticsearch and Redis for efficient geospatial queries.

How does Geohash handle the poles and the 180-degree meridian?

Geohash has limitations at the poles and the 180-degree meridian. At the poles, the cells become tall and thin, and at the meridian, nearby points can have very different Geohash prefixes. Applications that require high precision in these areas often use specialized logic to handle these edge cases, such as querying neighboring cells.

What are the limitations of Geohash?

The main limitations are the fixed grid structure, which can lead to inaccuracies at cell boundaries (the "boundary problem"), and the distortion of cell shapes near the poles. Also, two points that are close together might be in different parent cells, making proximity searches more complex.

How do I choose the right Geohash precision?

Choose precision from the target latitude, cell dimensions, query radius, false-positive budget, and boundary behavior. Do not treat a character count such as 4, 5, 8, or 9 as a universal city or building guarantee; refine candidates with exact distance or spatial predicates.

Are there alternatives to Geohash?

Yes, other geospatial indexing systems exist, such as S2 from Google, H3 from Uber, and various forms of R-trees. Each has its own strengths and weaknesses. Geohash is popular due to its simplicity and ease of implementation.

Limitations and Alternatives

Geohash is a rectangular, longitude-wrapped grid, not a distance metric. Nearby points can fall into different prefixes, and lexicographic order is not geographic distance order. A radius query should cover the query cell and relevant neighboring cells, then calculate a geodesic distance or use the database's spatial operator. Handle longitude wrapping, polar regions, coordinate reference systems, and antimeridian behavior explicitly.

Alternatives include H3 for hierarchical hexagonal cells, S2 for spherical cell geometry, R-trees for spatial bounding boxes, and native database geospatial indexes. Choose based on query shape, update rate, distance model, polar/antimeridian requirements, and operational tooling.

Conclusion

Geohash is useful when a hierarchical rectangular key fits the storage and query workload. It does not replace exact distance calculations, polygon predicates, authorization filters, or a database's spatial index. Validate precision with the target latitude, workload, coordinate reference system, and boundary cases.

Primary Sources