geobatch
Polite, resumable batch geocoding for spreadsheets.
geobatch turns a CSV of thousands of addresses into coordinates (or a CSV of
coordinates into addresses) reliably and without upsetting the services you
depend on. It runs on the Python standard library alone and puts several
providers behind one interface:
- Nominatim (public or self-hosted). The OpenStreetMap Foundation usage policy is enforced on the public instance.
- Photon (komoot public demo or self-hosted).
- Pelias (self-hosted or hosted).
- Custom HTTP template for any JSON geocoding API.
- Thin adapters for API-key services: OpenCage, Geoapify, Mapbox and Google. Keys are read only from environment variables and are never logged.
Every lookup goes through a persistent SQLite cache, per-provider token-bucket
rate limiting, retries that honour Retry-After, a fallback chain and
quality checks. A batch that stops halfway can be resumed: run the same
command again.
pip install geobatch # stdlib only
pip install "geobatch[pandas]" # + DataFrame helpers
Requires Python 3.9 or newer.
Quick start (command line)
geobatch geocode addresses.csv --address-col address \
--provider nominatim \
--user-agent "MyProject/1.0 (https://my-project.org; gis-team@my-project.org)" \
-o geocoded.csv
Illustrative output (abridged):
geobatch: Nominatim usage policy applies: https://operations.osmfoundation.org/policies/nominatim/ (1 req/s, results cached, at most 5000 new lookups per run)
geocoding [##############----------------] 1412/3000 47.1% cached=388 failed=3 elapsed=17:04 eta=19:10
...
geobatch forward summary
rows 3000
ok 2941
not found 52
errors 3
skipped (empty) 4
unique lookups 2710
cache hits 388
HTTP requests 2325 (retries 3)
by provider nominatim=2941
by precision locality=61, rooftop=2402, street=478
quality flags country_mismatch=4, duplicate_coordinates=18, region_centroid=61
The output keeps every input column and adds geobatch_status, geobatch_lat,
geobatch_lon, geobatch_display_name, geobatch_precision,
geobatch_confidence, geobatch_importance, geobatch_country_code,
geobatch_provider, geobatch_flags, geobatch_from_cache and
geobatch_error.
More examples:
# Structured columns, Germany only, fallback to Photon, GeoJSON output
geobatch geocode customers.csv --street-col street --postcode-col zip --city-col city \
--country-codes de --provider nominatim,photon \
--user-agent "MyProject/1.0 (https://my-project.org)" -o customers.geojson
# Self-hosted Nominatim: no public-policy limits, so raise the rate and parallelism
geobatch geocode big.csv --address-col addr --url nominatim=http://localhost:8080 \
--rate 50 --concurrency 8 -o big.gpkg
# Reverse geocoding
geobatch reverse points.csv --lat-col lat --lon-col lon \
--user-agent "MyProject/1.0 (https://my-project.org)" -o points_addresses.csv
# A commercial provider (key from the environment, never from the command line)
export OPENCAGE_API_KEY=...
geobatch geocode addresses.csv --address-col address --provider opencage -o out.csv
# Cache maintenance
geobatch cache stats
geobatch cache clear --provider photon
geobatch cache clear --expired
# Provider terms and key variables, quality flag meanings
geobatch providers
geobatch flags
Input files: UTF-8 CSV, including the BOM that Excel writes. The delimiter
(, ; tab |) is detected automatically, so semicolon "CSV" exports from
European Excel work as they are. --excel writes the output with a BOM so
Excel shows accented characters correctly. Output format follows the file
extension: .csv, .tsv, .geojson or .gpkg. GeoPackage is written with
the standard-library sqlite3 as an OGC GeoPackage 1.3 point layer in EPSG:4326,
so GDAL is not needed. QGIS and GDAL read it directly.
Exit codes: 0 done, 2 usage or configuration error (including policy
refusals), 3 aborted after too many consecutive failures, 130 interrupted
(a partial output is still written).
Responsible use
Free geocoders are run by volunteers and small teams on donated hardware. A careless batch job can get your IP blocked, and it also costs everyone else capacity. geobatch builds the rules in rather than leaving them to the documentation.
| Provider | Terms / policy | What geobatch enforces |
|---|---|---|
Nominatim (public nominatim.openstreetmap.org) |
OSMF Nominatim usage policy, OSM copyright & ODbL | Refuses to run without an identifying user_agent= / --user-agent. Stock library agents, geobatch's own default agent and placeholders such as example.org are rejected, and geobatch never fills in an e-mail address for you. Max 1 request/second, single thread, cache required, at most 5000 uncached lookups per run ("smaller one-time bulk tasks" only). The policy link is printed on every run, and the policy cannot be switched off for this host. |
Photon (public photon.komoot.io) |
photon.komoot.io: fair use, heavy use is throttled; source | 1 request/second, single thread, cache required (a conservative choice by geobatch). |
| Pelias | documentation; hosted services such as geocode.earth have their own terms | Optional PELIAS_API_KEY, redacted from logs. |
| OpenCage | terms | Key from OPENCAGE_API_KEY; default 1 req/s (free-trial limit). |
| Geoapify | terms | Key from GEOAPIFY_API_KEY; default 5 req/s. Attribution is required by Geoapify. |
| Mapbox | terms of service | Token from MAPBOX_ACCESS_TOKEN. Temporary geocoding results are not cached; pass permanent=True / --mapbox-permanent only if your account includes permanent geocoding. |
| Google Geocoding | Maps Platform terms, geocoding policies | Key from GOOGLE_MAPS_API_KEY. Cache entries expire after 30 days. Google requires results to be used with a Google map. |
More rules that apply to every provider:
- Keys are read from environment variables only and never accepted as
arguments. They are not part of cache keys, and they are replaced by
***in every error message. - Retries use exponential backoff with jitter, and a server's
Retry-Afteralways wins. A 429 pauses the provider's token bucket for all threads, not just the thread that received it. If a server asks you to wait more thanmax_retry_after(300 s by default; usually a used-up daily quota), geobatch gives up on that lookup instead of hammering or blocking for hours. - Circuit breaker: after 25 consecutive failed lookups the batch stops, because the service is down or blocking you.
- De-duplication: identical addresses (after normalisation) are looked up once.
- Attribution: OpenStreetMap-based results (Nominatim, Photon, most Pelias installations) are © OpenStreetMap contributors under the ODbL. Credit them wherever you publish the data.
For large or recurring jobs, run your own Nominatim, Photon or Pelias, or use a
commercial provider. Self-hosted instances have no built-in limits in
geobatch. Set --rate and --concurrency to what your server can handle.
Python API
from geobatch import Geocoder, Nominatim, Photon, Query, SearchOptions, QualityConfig
ua = "MyProject/1.0 (https://my-project.org)"
geocoder = Geocoder(
[Nominatim(user_agent=ua), Photon(user_agent=ua)], # fallback chain
cache="geocache.sqlite", # default: per-user cache dir
options=SearchOptions(country_codes=("de", "at"), language="en"),
quality=QualityConfig(min_confidence=0.5, expected_countries=("de", "at")),
)
result = geocoder.geocode_many(
[
"Pariser Platz 1, 10117 Berlin",
Query(street="Stephansplatz 3", postcode="1010", city="Wien", country="Austria"),
{"city": "Hamburg"},
],
progress=True,
)
for rec in result:
if rec.ok:
r = rec.result
print(r.lat, r.lon, r.display_name, r.precision, r.confidence, rec.flags)
else:
print(rec.status, rec.error)
print(result.summary.format())
# one-offs
rec = geocoder.geocode("Brandenburger Tor, Berlin")
rec = geocoder.reverse(52.5163, 13.3777)
GeocodeResult fields: provider, lat, lon, display_name,
components (normalised keys such as house_number, road, postcode,
city, state, country, country_code), confidence (0..1), importance,
bbox (min_lon, min_lat, max_lon, max_lat), precision (rooftop,
interpolated, street, postcode, locality, region, country,
unknown), result_type and raw (the provider's original JSON). If a
provider gives no confidence score (Nominatim, Photon), confidence is
derived from precision.
Reading and writing files:
from geobatch import read_table, build_queries, write_output
columns, rows = read_table("addresses.csv")
queries = build_queries(columns, rows, street_col="street", city_col="city", postcode_col="zip")
result = geocoder.geocode_many(queries, rows=rows)
write_output(result.records, "addresses.gpkg")
With pandas (pip install "geobatch[pandas]"):
from geobatch.pandas_ext import geocode_dataframe, reverse_dataframe
df = geocode_dataframe(df, geocoder, address_col="address", expected_country_col="iso2")
df.attrs["geobatch_summary"]
Custom HTTP template provider
Any JSON API can be described by URL templates and dotted JSON paths:
from geobatch import TemplateProvider
inhouse = TemplateProvider(
"https://geo.internal/find?address={query}&token={env:GEO_TOKEN}",
reverse_url="https://geo.internal/reverse?lat={lat}&lon={lon}&token={env:GEO_TOKEN}",
results_path="data.matches", lat_field="y", lon_field="x",
display_field="label", confidence_field="score", confidence_scale=0.01,
name="inhouse", rate=10, concurrency=4,
)
The available placeholders are {query}, {street}, {city}, {county},
{state}, {postcode}, {country}, {country_codes}, {language},
{limit}, {lat}, {lon} and {env:NAME}. A parameter that uses {env:...}
is redacted from logs and kept out of the cache key. GeoJSON
FeatureCollections are recognised automatically. On the command line, use
--provider template --template-url ....
How it works
Normalisation. Before lookup and before computing cache keys, addresses
are cleaned: Unicode NFKC (full-width digits, non-breaking spaces), control
characters, collapsed whitespace, surrounding quotes, doubled or trailing
separators, and spreadsheet null tokens (NaN, N/A, -) become empty.
Wording is never rewritten. Cache keys are case-insensitive.
Cache and resume. The SQLite cache (WAL mode, thread-safe) is keyed by
provider and endpoint, query kind, normalised query and search options. It
stores found results and "not found" answers, and each one is committed as
soon as it arrives. Network errors are never cached. If a batch is
interrupted (Ctrl+C, crash, laptop lid), run the same command again: finished
lookups come from the cache and only the rest goes to the network. In Python,
BatchInterrupted.result holds the partial result. The default location is
the per-user cache directory (%LOCALAPPDATA%\geobatch, ~/Library/Caches/geobatch
or ~/.cache/geobatch). Override it with --cache or GEOBATCH_CACHE.
Rate limiting and concurrency. Each provider has a token bucket
(rate requests per second, no initial burst) and a concurrency limit, shared
by all worker threads.
Fallback. Providers are tried in order. The next one is asked when a
provider fails, and also when it finds nothing (turn that off with
fallback_on_not_found=False / --no-fallback-on-not-found).
Biasing. country_codes, bbox and bounded are translated into each
provider's own parameters, for example Nominatim
countrycodes/viewbox/bounded, Pelias boundary.*/focus.point,
Geoapify filter/bias and Google components/bounds. Structured queries
use native structured endpoints where they exist (Nominatim, Pelias, Geoapify,
Mapbox) and are joined into one line elsewhere.
Quality flags
| Flag | Meaning |
|---|---|
low_confidence |
confidence below min_confidence (default 0.5) |
country_mismatch |
result is outside the expected countries (--country-codes or a per-row --expected-country-col) |
outside_bbox |
result is outside the expected bounding box (--bbox --bounded) |
duplicate_coordinates |
the same point was returned for different addresses, a typical sign of a fallback to a street or town centroid |
region_centroid |
a postcode, city or region centroid was returned for a query that had street-level detail, or a region or country centroid was returned for any query |
fallback_provider |
answered by a provider other than the first in the chain |
Development
pip install -e ".[dev,pandas]"
pytest --cov=geobatch
ruff check . && ruff format --check src tests && mypy src
The test suite never touches the real network. It runs a local stdlib HTTP
server (tests/mockserver.py) that imitates every provider's JSON, including
429/503 with Retry-After, malformed JSON, timeouts, rate-limit timing,
cache hits, resume and fallback.
License
MIT © 2026 Yusuf Eminoğlu. See LICENSE. To cite geobatch, see CITATION.cff.
Metadata
Release files for geobatch 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| geobatch-0.1.0.tar.gz | 78.2 kB | With provenance | Details for geobatch-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| geobatch-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for geobatch-0.1.0-py3-none-any.whl |
Total release size: 138.5 kB
Release files / geobatch-0.1.0.tar.gz
| Download URL | geobatch-0.1.0.tar.gz |
|---|---|
| Size | 78.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c8a67006b7813f861c7443768ed797ba011d573e07ef0ec118a0b8fd270418b6
|
|
BLAKE2b-256 checksum How to use checksums |
54656a25ed8bba2f0282d65ef1b85d4c7fe30f57505467104d148f54a5f6b9e5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logRelease files / geobatch-0.1.0-py3-none-any.whl
| Download URL | geobatch-0.1.0-py3-none-any.whl |
|---|---|
| Size | 60.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
99659bcabea916fa46df09a6901af625c7e0a4ad5117bdbae4c62f48d39ace71
|
|
BLAKE2b-256 checksum How to use checksums |
e7954c75f11d0675d3ab1e4c3c28b46d84352d2f6c531a9388a1cd61ee06ea64
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logSecurity for geobatch 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for geobatch-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.