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
- Core Principles
- Encoding Algorithm
- Decoding Process
- Code Examples
- Real-World Applications
- Limitations & Solutions
- FAQ
- Conclusion
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:
- Coordinate Normalization: Convert latitude (-90 to 90) and longitude (-180 to 180) to binary representations.
- Bit Interleaving: Alternate bits from latitude and longitude.
- Base32 Encoding: Convert the interleaved bits to Base32 characters.
- 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
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
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
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.