Build a geocoder: a practical guide to address search and reverse lookup
A geocoder converts human location descriptions into geographic features and can also turn coordinates into nearby addresses or places. Building one reliably requires more than matching a street name: you need address parsing, normalization, structured indexing, geographic filtering, ranking, reverse-geocoding rules, data refreshes, attribution, and clear handling of uncertainty. This guide presents a provider-neutral architecture using libpostal for parsing and normalization, PostgreSQL with PostGIS for geographic computation, and an indexed place database. It also explains when an existing geocoder such as Nominatim or Pelias is a better starting point than building every component yourself.
A geocoder is the system behind queries such as “1600 Pennsylvania Avenue NW, Washington, DC,” “JFK Airport,” or “restaurants near downtown.” Forward geocoding maps text to one or more geographic results. Reverse geocoding starts with latitude and longitude and returns a nearby address, place, or administrative area. The hard part is ambiguity. A user may omit the country, misspell a street, use a local abbreviation, provide a business name instead of an address, or describe a place that has multiple entrances and operating locations. A production geocoder therefore needs a pipeline rather than a single database query: parse the input, normalize its components, retrieve plausible candidates, rank them using textual and geographic evidence, and return enough metadata for the caller to understand the result. This guide uses open-source components as concrete examples, but the design applies to commercial and proprietary datasets as well. Nominatim supports free-form and structured search over OpenStreetMap data, while Pelias exposes forward, reverse, autocomplete, structured, and place-detail workflows through a modular open-data geocoder. (nominatim.org)
How EDK fits
For: Developers and data engineers building a small-to-medium geocoding service, an internal address-resolution pipeline, a searchable place database, or a location-aware application. Readers should be comfortable with HTTP APIs, SQL, JSON, and basic geographic concepts such as latitude, longitude, coordinate reference systems, and spatial indexes.
Geocoding is a direct example of turning an unstructured description of the physical world into a computable geographic object. The output is not merely a pair of coordinates: it can include a building, road segment, neighborhood, city, administrative boundary, or named place together with identifiers, geometry, and provenance. That makes geocoding a useful boundary between human language and a world model. The connection to E2 is strongest when geocoded results become inputs to broader geographic computation. An address can be linked to nearby roads, buildings, public assets, environmental features, infrastructure, service requests, or other physical features; results can then be filtered, ranked, compared, aggregated, or measured by distance and containment. E2’s perspective is not that it replaces a specialist geocoder. Rather, a geocoder is one important way to resolve a human location description into the physical-world entities that a broader geographic system can reason about.
- •Resolve an address or place name into a candidate building, road, or point that can then be compared with nearby physical features such as residential roads, schools, parks, charging stations, fire hydrants, or construction-related records.
- •Use geocoded locations as the starting point for geographic computation: rank nearby assets, measure distances, group results by neighborhood or administrative area, test whether a point falls within a region, or connect location-based reports to the surrounding built and natural environment.
- •Preserve candidate scores, source identifiers, geometry types, and provenance so that downstream world-model queries can distinguish an exact address match from a street-level or locality-level approximation.
The EDK outcome
Before you start
Basic SQL and Python or another programming language.
PostgreSQL administration sufficient to install extensions and create indexes.
An understanding of longitude, latitude, SRIDs, geometry versus geography, and spatial indexing.
A legally usable geographic dataset with documented licensing, attribution, and update terms.
A representative evaluation set of geocoding queries and expected results.
Learning objectives
Design a geocoder data model that preserves structured address fields, geometry, provenance, and freshness.
Separate parsing and normalization from candidate retrieval and ranking.
Combine trigram text search with PostGIS spatial filtering.
Implement forward and reverse geocoding with explicit uncertainty and match levels.
Evaluate accuracy and operate a geocoder without relying on unsupported precision claims.
Verification
Confirm that every geometry has the intended SRID and that longitude and latitude are not reversed.
Test exact, partial, misspelled, multilingual, ambiguous, and empty queries.
Verify that text indexes are used for approximate searches and spatial indexes are used for distance filters.
Compare forward results against a labeled test set and report results by match level and geography.
Confirm source attribution, licensing, rate limits, and privacy requirements before production use.
Test data refreshes by importing a new snapshot into a staging database and comparing additions, removals, and changed geometries.
Troubleshooting
No results: inspect whether the query was parsed into the expected components, whether the country or locality filter is too restrictive, and whether the source dataset contains the feature at all.
Wrong city or country: check missing country hints, ambiguous locality names, postal-code parsing, and ranking weights. Do not solve every ambiguity by lowering the similarity threshold.
Wrong coordinate order: verify that APIs, GeoJSON, PostGIS constructors, and client libraries agree on longitude-first versus latitude-first conventions.
Slow fuzzy search: confirm the pg_trgm extension and index exist, inspect the query plan, reduce the candidate set with structured filters, and avoid scanning every feature for every request.
Reverse result looks unrelated: increase or decrease the search radius according to the use case, distinguish nearest feature from containing feature, and account for dense urban areas where the nearest suitable object may be on another street.
Duplicate results: deduplicate by source identity and normalized address components, but preserve separate entrances or businesses when they represent distinct physical features.
Stale results: compare source update timestamps, import job status, and record validity fields. A geocoder cannot return current information from an outdated source snapshot.
Public API blocked or throttled: review the provider’s usage policy. Do not bypass rate limits; use an approved hosted plan or operate an appropriately configured instance.
Next steps
Create the production companion guide for deployment, monitoring, refresh orchestration, and capacity planning.
Use the troubleshooting companion for parser, ranking, coordinate-system, index, and reverse-geocoding failures.
Join geocoded results to roads, buildings, public assets, service requests, or environmental layers and validate the spatial relationships before drawing conclusions.
Add user-facing disambiguation when multiple candidates are plausible instead of silently selecting a low-confidence result.
Document what your geocoder does not cover. Coverage gaps and uncertainty are part of the public contract, not implementation details.
What you need before writing the geocoder
Start by defining the result contract. A forward-geocoding response commonly includes a stable source identifier, display label, point geometry, optional bounding box or full geometry, address components, feature type, source, and a match-quality explanation. A reverse response should identify the geographic object used to answer the query and should not imply more precision than the source data supports.
Your source data must contain more than names. At minimum, store a searchable label, normalized text, geometry, feature type, locality hierarchy, country or region code, and source identifier. For address-level results, keep house number and road separately where possible. For named places, preserve categories such as airport, school, restaurant, park, or transit station. If you combine multiple datasets, record source and update timestamps per record.
- •PostgreSQL with the PostGIS extension for geometry, geography, spatial indexes, and distance predicates.
- •A text normalization and address-parsing component. libpostal is one established open-source option for parsing and normalizing international street addresses; its project documentation describes support for address components, abbreviation expansion, and multilingual training data. (github.com)
- •A source dataset such as OpenStreetMap, an official address register, a licensed commercial dataset, or a combination. Check each source’s license, attribution, redistribution, and update requirements.
- •A test corpus containing exact addresses, incomplete addresses, misspellings, local abbreviations, multilingual examples, place names, and deliberately ambiguous queries.
- •An API layer with request limits, logging, structured errors, and protection against returning private or sensitive location data without authorization.
Step 1: design the searchable place model
Create a canonical place table rather than putting all logic in application code. One practical starting point is a table with a geometry column in WGS 84, a generated geography column for meter-based distance checks, normalized name fields, address components, hierarchy identifiers, and provenance fields. Use a geometry type that reflects the source: points for address entrances or points of interest, lines for roads, and polygons for administrative areas or footprints.
A simplified schema might look like this:
- •id: internal identifier
- •source and source_id: provenance and upstream identity
- •kind: address, road, locality, building, business, boundary, or other feature type
- •name and normalized_name: display and search forms
- •house_number, street, unit, locality, region, postal_code, country_code: structured components
- •geom: source geometry with a declared SRID
- •geog: geography representation when distance in meters is needed
- •importance or source_rank: an input to ranking, not a universal truth
- •updated_at and valid_to: lifecycle and freshness metadata.
- •Keep multiple names when a place has alternate spellings, local-language names, transliterations, or historical names. Do not collapse these into one string if users are expected to search in more than one language. Also keep the source geometry separate from any display point you derive, because a polygon centroid is not necessarily an entrance or an address location.
Step 2: parse and normalize the query
Accept both free-form and structured requests. A structured request may supply house number, street, city, postal code, and country separately; a free-form request may be “12 Rue de la Paix Paris.” If you use libpostal, treat its output as a candidate interpretation rather than ground truth. Parsing identifies likely components, but it does not prove that those components refer to a real feature in your database. (github.com)
Store three versions of the input: the original query, a display-safe normalized form, and parsed components. Normalize case and punctuation for matching, but avoid destructive transformations that remove meaningful distinctions. Country and language hints should influence normalization and ranking. A postal code, city, or viewport can be especially valuable for disambiguation.
- •Normalize abbreviations and Unicode consistently, but retain the original string for display and audit logs.
- •Do not assume that every number is a house number; it may be a postal code, route number, unit, or building name.
- •Use country and language hints when available. A street token can have different meanings in different countries.
- •Apply privacy controls before sending user-entered addresses to third-party services. Nominatim’s public policy specifically asks users not to submit personal or confidential data. (operations.osmfoundation.org)
Step 3: retrieve candidates with text and geography
Candidate retrieval should be broad enough to catch misspellings but narrow enough to remain fast. PostgreSQL’s pg_trgm extension provides trigram similarity functions, operators, and GiST or GIN index support for similar-text searches. This is useful for street names, locality names, and place labels, but it should be combined with structured filters rather than used as the only ranking signal. (postgresql.org)
For example, create a normalized-name index and retrieve candidates using a similarity threshold, then filter or boost by country, postal code, locality, feature type, and parsed house number. Exact component matches should usually outrank a globally similar name. A query pattern can look like this:
- •CREATE EXTENSION IF NOT EXISTS postgis;
- •CREATE EXTENSION IF NOT EXISTS pg_trgm;
- •CREATE INDEX place_name_trgm_idx ON places USING gin (normalized_name gin_trgm_ops);
- •CREATE INDEX place_geom_gist_idx ON places USING gist (geom);
- •SELECT id, source, source_id, name, kind, geom, similarity(normalized_name, $1) AS text_score FROM places WHERE normalized_name % $1 ORDER BY text_score DESC LIMIT 100;
- •Use a second stage for geographic constraints. PostGIS documents ST_DWithin for distance filtering; with geography inputs, the distance is in meters, and spatial indexes can support the bounding comparison. (postgis.net)
- •SELECT id, name, ST_Distance(geog, ST_SetSRID(ST_Point($1, $2), 4326)::geography) AS meters FROM places WHERE ST_DWithin(geog, ST_SetSRID(ST_Point($1, $2), 4326)::geography, $3) ORDER BY meters LIMIT 50;
Step 4: rank results and implement reverse geocoding
Ranking should be explainable. A practical score can combine exact house-number agreement, street-name similarity, postal-code agreement, locality agreement, country agreement, feature-type preference, source importance, freshness, and distance from an optional viewport or bias point. Avoid presenting the score as a probability unless you have calibrated it against labeled data.
Return multiple candidates when ambiguity is material. For example, “Main Street” may match many cities, while a complete address with a country and postal code may justify a single top result. The response should expose a match level such as address, street, locality, or nearby place, along with the fields that matched and the fields that were missing.
- •Forward lookup: parse → normalize → retrieve → rank → deduplicate → return candidates.
- •Reverse lookup: validate coordinates → spatially retrieve → apply feature policy → rank by distance and suitability → return one or more results.
- •Autocomplete: use a separate low-latency endpoint and an index designed for prefixes or search-as-you-type. Do not assume a general forward-search API is suitable for autocomplete; Nominatim’s public service explicitly prohibits client-side autocomplete against its public endpoint. (operations.osmfoundation.org)
Step 5: test, operate, and improve the service
Build evaluation around real failure modes. Separate exact-address accuracy from street-level, locality-level, reverse, and named-place performance. Record whether the returned feature type was correct, whether the point was within an acceptable distance, and whether the address components were correct. Test each country or region separately if your data coverage is uneven.
Log query latency, candidate counts, no-result rates, ambiguous-result rates, source freshness, parser failures, and downstream correction events. Keep sampled queries under a privacy-preserving retention policy. When users correct a result, store the correction only when you have a lawful and appropriate basis to do so.
- •Create a labeled regression suite before changing normalization or ranking logic.
- •Pin dataset snapshots and parser versions for reproducible tests.
- •Return source attribution in the API or presentation layer when required.
- •Use separate indexes or services for high-volume autocomplete and batch address matching.
- •Document coverage gaps plainly. A no-result or low-confidence result is preferable to a precise-looking wrong coordinate.
Workflow
1. Define the response and data contract
Specify input forms, output fields, coordinate order, supported feature types, match levels, error responses, attribution, and source identifiers before importing data. Keep the original query and every normalized or parsed representation for debugging and evaluation.
{
"query": "1600 Pennsylvania Avenue NW, Washington, DC",
"results": [
{
"source": "example-source",
"source_id": "address-123",
"match_level": "address",
"geometry": {"type": "Point", "coordinates": [-77.0365, 38.8977]},
"address": {"house_number": "1600", "road": "Pennsylvania Avenue NW", "city": "Washington", "region": "DC"},
"score": 0.97
}
]
}2. Load and index geographic records
Import addresses, roads, localities, boundaries, and named places into a canonical table. Declare the SRID, create spatial indexes, and add text indexes for normalized names. Retain upstream identifiers and timestamps so records can be refreshed and traced.
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX places_geom_gist ON places USING gist (geom);
CREATE INDEX places_name_gin ON places USING gin (normalized_name gin_trgm_ops);3. Parse and normalize incoming text
Run free-form input through a parser or your own structured-field logic. Normalize only for matching; preserve the user’s original query for display, audit, and correction workflows. Apply country, language, postal-code, and viewport hints when supplied.
def prepare_query(query, country_code=None, language=None):
return {
"original": query,
"country_code": country_code,
"language": language,
"normalized": query.casefold().strip()
}4. Retrieve and rank candidates
Use exact component filters first where available, then approximate text retrieval. Rank candidates using component agreement, text similarity, feature type, source importance, locality hierarchy, and optional geographic bias. Return multiple candidates when the top result is not clearly superior.
SELECT id, name, kind,
similarity(normalized_name, $1) AS text_score
FROM places
WHERE normalized_name % $1
ORDER BY text_score DESC
LIMIT 100;5. Add reverse geocoding
Validate coordinates, search within a defined radius, and apply a documented policy for choosing address points, roads, buildings, or administrative areas. Return the distance and selected feature type. Do not turn the nearest object into an exact address unless the data supports that claim.
SELECT id, name, kind,
ST_Distance(geog, ST_SetSRID(ST_Point($1, $2), 4326)::geography) AS distance_m
FROM places
WHERE ST_DWithin(
geog,
ST_SetSRID(ST_Point($1, $2), 4326)::geography,
$3
)
ORDER BY distance_m
LIMIT 20;6. Evaluate and operate the service
Create labeled tests for complete addresses, partial addresses, misspellings, multilingual input, named places, and reverse queries. Track latency, no-result rates, ambiguity, source freshness, and corrections. Refresh data with versioned jobs and document the quality and licensing characteristics of each source.
# Example metrics to emit
metrics.increment("geocoder.requests")
metrics.observe("geocoder.latency_ms", elapsed_ms)
metrics.increment("geocoder.no_result" if not results else "geocoder.match")Questions teams ask
Should I build a geocoder from scratch or use Nominatim or Pelias?
Use an existing system when your goal is broad geographic search and you can accept its data model and operational requirements. Build a specialized layer when you need private addresses, domain-specific assets, custom ranking, a controlled update process, or integration with internal geographic features. A common approach is to start with an existing geocoder, measure its failure modes, and add a domain-specific index or reranker rather than replacing every component immediately.
Is address parsing the same as geocoding?
No. Parsing identifies likely components such as house number, road, locality, and postal code. Geocoding resolves those components against geographic data and returns candidate features. A parser can produce a plausible structure even when the address does not exist, while a geocoder can find a place-name result without having a complete street address.
How should a geocoder handle an uncertain result?
Return the candidate, its match level, source, matched components, and a score or reason code. For example, distinguish an exact address match from a street-centroid, locality match, or nearest suitable feature. If the result is below an application-specific threshold, ask the user to choose among candidates or report that no sufficiently reliable match was found.
Can I use the public Nominatim API for autocomplete or a commercial application?
Do not assume so. The OpenStreetMap Foundation’s policy for the public Nominatim service sets a maximum of one request per second, requires an identifying User-Agent or Referer and attribution, prohibits client-side autocomplete, and says applications whose primary function is geocoding must run their own service. Review the current policy before deployment; self-hosting has different operational considerations. (operations.osmfoundation.org)
What coordinate system should the API return?
WGS 84 longitude and latitude is a common interchange format, but document axis order explicitly. For geographic distance in PostGIS, use geography or a projected coordinate system appropriate to the computation. Never mix coordinate systems silently; store the SRID and test coordinate order at API boundaries.
Keep exploring
build a geocoder production guide
A follow-on guide for turning the baseline geocoder architecture into a production service with deployment, observability, refreshes, and operational controls.
build a geocoder troubleshooting guide
A troubleshooting guide for diagnosing no results, wrong locations, ambiguous matches, slow searches, reverse-geocoding surprises, and data-refresh failures.
analyze spatial data with geopandas
Useful next reading for validating geocoded outputs, joining them to geographic layers, measuring distances, and summarizing results by region.
build a routing service
A related guide for computing travel paths and network-based distances after a geocoder has resolved an origin or destination.
Sources
Nominatim Search API documentation
Documents free-form and structured forward search, query parameters, and the distinction between search and data-download workflows.
Open sourceNominatim Reverse API documentation
Explains how reverse geocoding selects a suitable nearby indexed object and why the returned address may not exactly represent the input coordinate.
Open sourceNominatim Usage Policy
Provides authoritative limits and requirements for the public Nominatim service, including rate limits, attribution, autocomplete, systematic queries, and privacy.
Open sourcePelias documentation
Describes a modular open-data geocoder with forward, reverse, autocomplete, structured search, and place-detail endpoints.
Open sourcelibpostal project documentation
Documents international address parsing, normalization, supported components, language handling, installation considerations, and limitations.
Open sourcePostgreSQL pg_trgm documentation
Documents trigram similarity, operators, thresholds, and GiST or GIN index support for approximate text search.
Open sourcePostGIS ST_DWithin documentation
Documents indexed distance filtering for geometry and geography and explains units and coordinate-system requirements.
Open source