Overview
Beyond vector tiles, geolith can build a geocoding index from the same data with
--output-format geocoding. It reads Overture places and addresses, and with
--osm-pbf it adds named OpenStreetMap settlements, community-mapped venues, and
house-number addresses. Four output formats are supported:
| Format | Flag | Output | Consumer |
|---|---|---|---|
| glx | --geocoding-format glx (default) | one .glx file | geolith search — serves the file directly |
| PostgreSQL COPY | --geocoding-format pg-copy | .tsv + .sql | your own Postgres — COPY |
| Photon | --geocoding-format photon | .ndjson | Photon geocoder import |
| Pelias | --geocoding-format pelias | .ndjson | Elasticsearch _bulk API |
The output path defaults to the --output stem with the format's extension —
.glx for glx, .tsv for pg-copy, .ndjson for photon and pelias. Override it
with --geocoding-output.
geolith \
--data ./data/overture \
--osm-pbf ./data/planet-latest.osm.pbf \
--natural-earth ./data/natural_earth.sqlite \
--land-polygons ./data/land-polygons \
--water-polygons ./data/water-polygons \
--output ./data/planet.pmtiles \
--output-format geocoding \
--geocoding-output ./data/planet.glx
--bbox works the same as in tile builds — only features inside the box are
indexed. Add --osm-pbf to also index OpenStreetMap data (see
OpenStreetMap Data).
What Gets Indexed
| Overture source | Geocoding layer |
|---|---|
places/place | venue — requires a primary name; carries categories, confidence, and multilingual names |
addresses/address | address — requires a street or house number; carries number/street/unit/postcode |
buildings/building | Pelias only — separate flat-schema buildings index (see below) |
OpenStreetMap Data
With --osm-pbf, the pipeline reads the PBF after the Overture files and adds
what Overture lacks for geocoding: named settlements, house-number addresses,
and venues mapped by the OSM community.
| OSM feature | Geocoding layer | Default importance |
|---|---|---|
place=city / town / village, suburb / hamlet, quarter / neighbourhood with a name | locality | 0.187 / 0.160 / 0.147 / 0.133 / 0.080 |
place=state, province / region / county, district / country with a name | region / region / county / country | 0.293 / 0.160 / 0.240 / 0.347 |
named amenity, shop, tourism, leisure, historic, office, craft, healthcare, aeroway=aerodrome, railway=station/halt | venue (keeps any addr:* fields) | 0.00001 |
any other node or way with addr:housenumber or addr:street | address | 0.00001 |
A place with a Wikipedia article takes the importance of the article instead (see Importance).
- Ways are placed at their centroid. Relations are not read.
name:<lang>tags become multilingual names.- A record that the admin divisions leave without a locality gets one: the
OSM
addr:citytag when the record is from OSM, else the nearest namedplace=cityorplace=townnode within 30 km. The name is added tosearch_text, soCanara Bank, Hubballifinds the branch in Hubballi. Photon and Nominatim fill the city the same way. - An OSM venue or address is dropped when an Overture record of the same
layer lies within 50 m under a similar name (Jaro-Winkler similarity of the
lowercased names of at least 0.9), unless it has a Wikipedia article.
Settlements are always kept, because Overture
placeshas none. - OSM node, way and relation ids overlap, so the record id carries the element
type in its top two bits. Pelias ids use the usual
node/123andway/123source ids, and Photon'sosm_idis the plain element id. --node-storeand--tmpdirapply as in tile builds. With--bbox, the PBF is cut withosmiumwhen it is installed; otherwise records outside the box are filtered after reading.
Importance
Search ranks places by text relevance plus importance, so "London" finds the city before a shop named after it. Importance follows Nominatim:
- With
--wikimedia-importance <file>, an OSM place with awikidatatag takes the importance of its Wikipedia article, from 0 to 1. The value grows with the links to the article: London 0.892, the Eiffel Tower 0.621. Use Nominatim's file (322 MB):wget https://nominatim.org/data/wikimedia-importance.csv.gz geolith … --output-format geocoding --wikimedia-importance wikimedia-importance.csv.gz - Any other place takes the default of its type,
0.40001 - rank / 75, with Nominatim's search ranks: the values in the table above. - An Overture place takes the venue default. Its
confidencesays whether the place exists, not how well known it is, so it stays in the addendum and does not set importance. - An OSM venue or address with a Wikipedia article is kept even where an Overture record repeats it, because only the OSM record carries the article.
- A place's English name (
name:en, or Overturenames.common.en) is added tosearch_textwhen it differs from the primary name, so "Eiffel Tower" finds "Tour Eiffel".
glx Format
The default format writes one .glx file: the geocoding equivalent of a
PMTiles archive. geolith search opens it and
answers every Pelias endpoint from it — no database, no import step.
geolith … --output-format geocoding --geocoding-output ./data/india.glx
GLX_FILE=./data/india.glx geolith-search
The file holds:
- A term dictionary and posting lists with BM25 statistics, so search ranks places by text relevance plus importance — every word first, then about three quarters of the words when that finds too few.
- Stored word prefixes for autocomplete: posting lists for prefixes of 4 to 6 characters, and a short list of the most important places for each prefix of 2 or 3 characters.
- Per-place layer, country and position in the posting lists, so the
layers,boundary.countryand focus-point filters are applied before any place record is read. - A spatial index: place positions in Hilbert order, grouped into blocks
with bounding boxes, for
/v1/reverseand focus search. - Place records in zstd-compressed blocks of 1,024.
The writer sorts and merges on disk, so memory stays bounded at any size. The format and its reader are documented in the Search docs.
System Requirements
A geocoding build keeps its large data on disk, not in memory: the OSM node coordinates, the Overture records used to drop OSM duplicates, the posting lists, every per-place table and the admin boundary polygons. Memory holds bounded buffers and the bounding boxes of the admin boundaries. So a laptop can build a country, and the planet build stays near 5 GB of memory.
| Area | Memory (peak) | Free disk (peak) | Time |
|---|---|---|---|
| Country (India: 6.2 million places) | 4.9 GB | 32.5 GB | 16 min |
| Planet (652 million places) | 5.2 GB | about 231 GB | 7 h 35 min, with a saved node store |
Both were measured on an Apple M-series machine with 12 cores and an NVMe SSD.
The planet run reused a node store from an earlier run (--node-store), so
its time leaves out the node pass, about 1.6 hours more. Its disk is the
102 GB node store, 121 GB of temporary files and output, and the 7.5 GB
file of admin boundary polygons, all at the peak.
The planet .glx file is 84 GB.
- Disk is the main cost. The OSM node store holds a slot for every node id up to the highest one in the file: 114 GB of slots even for one country, because OSM numbers nodes across the whole planet. It is a sparse file, so only the slots of real nodes take disk: 31 GB for India (measured on APFS). Use a filesystem with sparse files, such as APFS or ext4, and an SSD; on a hard disk the node pass is many times slower.
- The node store is removed after the OSM pass when it lives in the
temporary directory. With
--node-store <path>it stays, with a.meta.jsonbeside it, so a later run on the same PBF skips the node pass (hours on a planet file). Delete it when you no longer need it. - Memory. Each stage has a fixed budget: the node page cache (80% of the free memory, at most 4 GB), one 1 GB sort buffer and one 1 GB run of posting lists. The admin boundary index is the one part that grows with the area: about 9 GB for the whole planet.
- Time is mostly reading the OSM file.
- Open files. The build raises its own open-file limit when it can (up to 1,088). If the system does not allow it, it uses fewer dedup buckets.
- Temporary files go to
--tmpdir(default: the system temporary directory). Point it at the disk with the most free space.
geolith --data ./overture --osm-pbf ./planet.osm.pbf \
--output-format geocoding --geocoding-output ./planet.glx \
--tmpdir /fast-ssd/tmp \
--node-store /fast-ssd/planet-nodes.bin --node-cache
PostgreSQL COPY Format
pg-copy writes two files: <output>.tsv holding the rows, and
<output>.sql holding the schema, for your own Postgres. Load the schema first, then the data. \COPY
needs the table to exist. The table is named after the output file stem.
psql -d gazetteer -f ./data/geocoding.sql
psql -d gazetteer -c "\COPY geocoding FROM './data/geocoding.tsv' WITH (FORMAT text, HEADER true)"
The .tsv is PostgreSQL COPY text format, not CSV: columns are
tab-separated, \N is NULL, and backslash escapes (\t, \n, \\) appear in
text fields. Load it with FORMAT text. HEADER with FORMAT text needs
Postgres 15 or later.
The schema creates the postgis extension, because the location index uses
ST_MakePoint. The database must have PostGIS available.
Columns
| Column | Type | Notes |
|---|---|---|
id | BIGINT | geolith's 64-bit feature hash, stored as signed |
name | TEXT | primary name, or the formatted address |
search_text | TEXT | name plus street, number and postcode; the column the text index covers |
lon, lat | DOUBLE PRECISION | WGS84, 7 decimals |
layer | TEXT | venue, address, and with --osm-pbf also locality, county, region, country |
country_code | TEXT | ISO alpha-2 of the containing country (see below) |
country, region, county, locality | TEXT | admin hierarchy from divisions/division_area (see below) |
street, postcode | TEXT | addresses only |
importance | REAL | 0.0 to 1.0 |
names | JSONB | language code to name |
Choosing the text index
--pg-index-flavor selects the index built over search_text. The table, the
PostGIS location index, and the layer index are identical either way.
| Flavor | Index | Needs an extension | Where it runs |
|---|---|---|---|
tin (default) | USING tin (search_text) | tin | PlanetScale Postgres and Neki |
gin | USING GIN (to_tsvector('simple', search_text)) | none | any Postgres |
TIN is an inverted text index with BM25 ranking, the ==> operator, and the
TINQL query language, which supports phrases, proximity, fuzzy terms, and
boosts. It indexes the text column directly rather than a tsvector
expression.
TIN is not self-hostable. If you run your own Postgres, pass
--pg-index-flavor gin. For local development and CI against TIN-compatible
SQL, PlanetScale publishes Lead, an
AGPL-licensed extension that accepts the same queries — it is deliberately slow
and unsuitable for production.
# self-hosted Postgres
geolith … --output-format geocoding --pg-index-flavor gin
Photon Format
The Photon output is GeoJSON Feature NDJSON — one feature per line, with no array wrapper:
{"type":"Feature","geometry":{"type":"Point","coordinates":[73.8567437,18.5204303]},"properties":{"osm_type":"N","osm_id":123456,"type":"city","name":"Pune","country_code":"IN","country":"India","state":"Maharashtra","city":"Pune","name:mr":"पुणे"}}
Key properties:
osm_typeisN,W, orRfor OSM nodes, ways, and relations. Overture records default toN.osm_idis the plain OSM element id for OSM records, and geolith's feature hash otherwise.typeis the layer mapped to a Photon type:house(address),amenity(venue),street,city(locality),county,state, orcountry.nameis the primary name, with aname:<lang>property for every multilingual name.- Admin fields (
country,state,county,city,district) and address fields (street,housenumber,postcode) are emitted only when present. Coordinates carry 7 decimals.
Feed the file to Photon's importer to build the Photon index; see the Photon documentation for the import command for your deployment.
Pelias Format
The Pelias output is Elasticsearch bulk NDJSON — pairs of action + document lines compatible with the pelias-model document shape:
{"index":{"_index":"pelias","_id":"overture:venue:08f2a100d388cfff0200724f76a3f4e8"}}
{"source":"overture","layer":"venue","source_id":"08f2a100d388cfff0200724f76a3f4e8","name":{"default":"Blue Bottle Coffee","es":"Cafe Blue Bottle"},"phrase":{"default":"Blue Bottle Coffee","es":"Cafe Blue Bottle"},"center_point":{"lon":-122.4194,"lat":37.7749},"category":["cafe","coffee_shop"],"addendum":{"overture":"{\"confidence\":0.97,...}"}}
Key properties of the document shape:
_idis the Pelias GID:{source}:{layer}:{source_id}with the original lowercased Overture GERS UUID.phraseis a literal copy ofname; multilingual names live insidenameas per-language keys.parent.*admin hierarchy values are arrays (country,country_a,region,county,locality,neighbourhood), with alpha-3 country codes incountry_a.categoryis a lowercased array built frombasic_category→ taxonomy → categories in priority order.addendum.overtureis a JSON-stringified string (thepelias-modelcodec convention) carrying confidence, category provenance, and contact fields.center_pointcoordinates are rounded to 6 decimals.- Empty objects are omitted entirely — never emitted as
{}.
Load the result into Elasticsearch with the _bulk API; Pelias then serves the index:
curl -s -H "Content-Type: application/x-ndjson" \
-XPOST "http://localhost:9200/_bulk" \
--data-binary @geocoding.ndjson
Buildings Index (Pelias only)
When Overture buildings data is present, the Pelias format additionally writes <stem>-buildings.ndjson targeting a separate overture-buildings index. Buildings use a flat Overture-schema document, not the Pelias document model:
{"index":{"_index":"overture-buildings","_id":"08b2a100d388cfff"}}
{"id":"08b2a100d388cfff","source":"overture","theme":"buildings","type":"building","centroid":{"lat":37.775,"lon":-122.415},"geometry":{"type":"Polygon","coordinates":[...]},"bbox_shape":{"type":"envelope","coordinates":[...]},"class":"residential","height":12.5,"num_floors":4}
_idis the raw lowercased building ID (nosource:layer:prefix).centroidis a{lat, lon}object (Elasticsearchgeo_point).geometryis GeoJSON, stored asgeo_shape.bbox_shapeis an Elasticsearch envelope for fast bounding-box queries.- Physical attributes (
height,num_floors,class,subtype, roof/facade fields) are included when present.
Admin Hierarchy
Every record is enriched with its admin hierarchy by point-in-polygon lookup
against Overture divisions/division_area. Put the theme=divisions/type=division_area
Parquet files in --data; only areas that intersect --bbox are loaded.
Overture subtype | Record field |
|---|---|
country | country, country_code |
dependency | country, country_code, only when no country contains the point |
region | region |
county | county |
locality | locality |
neighborhood | neighbourhood (Pelias and Photon only; pg-copy has no column) |
When no country or dependency contains the point (a coastal place can lie
outside its country's land polygon but inside its region's), country_code
comes from the region, county, locality or neighbourhood that contains it, and
country is the name of the country with that code.
- Only
class = landareas are used. Themaritimevariants extend into territorial waters and would place coastal points in the sea area. - Polygons are used exactly, with every part and every hole, so enclaves resolve to the enclave, not the country around it.
- When two areas of the same level contain a point, the smaller one wins.
- Names are the Overture
names.primaryvalue, so they are in the local language (for exampleDeutschland). - The names are appended to
search_text, so a query such ascafe Parismatches a venue in Paris. - Addresses that no polygon covers fall back to the Overture
countryandpostal_cityfields.
For the whole planet the index holds about 1.26 million polygon parts and
needs about 9 GB of memory. Without division_area in --data the pipeline
still runs, and the admin fields are empty.
Measured Results (India)
A geolith search instance built from India data was benchmarked against Pelias, Photon, and Nominatim. Method and full tables: docs.search.geolith.app/benchmarks/engines.
| Metric | Result |
|---|---|
| Top-1 within 1 km — OSM cities and towns | 96% |
| Top-1 within 1 km — Overture places | 88.5% |
| Autocomplete (top-1 within 1 km) | 74% |
| Search near a point | 91% |
| OSM addresses | 74.5% |
| Load — search / reverse | 562 req/s / 3,392 req/s (100 connections) |
The India index holds 5,466,755 rows: Overture places plus 301,576 OSM localities and 151,416 OSM addresses after dedupe.
A single-file .glx geocoding index — the PMTiles equivalent for geocoding —
is coming next; see the Search docs.