geobatch 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. geobatch-0.1.0/CHANGELOG.md +34 -0
  2. geobatch-0.1.0/CITATION.cff +20 -0
  3. geobatch-0.1.0/LICENSE +21 -0
  4. geobatch-0.1.0/MANIFEST.in +9 -0
  5. geobatch-0.1.0/PKG-INFO +338 -0
  6. geobatch-0.1.0/README.md +301 -0
  7. geobatch-0.1.0/examples/landmarks.csv +6 -0
  8. geobatch-0.1.0/examples/quickstart.py +61 -0
  9. geobatch-0.1.0/pyproject.toml +116 -0
  10. geobatch-0.1.0/setup.cfg +4 -0
  11. geobatch-0.1.0/src/geobatch/__init__.py +97 -0
  12. geobatch-0.1.0/src/geobatch/__main__.py +7 -0
  13. geobatch-0.1.0/src/geobatch/_version.py +1 -0
  14. geobatch-0.1.0/src/geobatch/batch.py +526 -0
  15. geobatch-0.1.0/src/geobatch/cache.py +185 -0
  16. geobatch-0.1.0/src/geobatch/cli.py +367 -0
  17. geobatch-0.1.0/src/geobatch/errors.py +52 -0
  18. geobatch-0.1.0/src/geobatch/http.py +201 -0
  19. geobatch-0.1.0/src/geobatch/io.py +320 -0
  20. geobatch-0.1.0/src/geobatch/models.py +182 -0
  21. geobatch-0.1.0/src/geobatch/normalize.py +73 -0
  22. geobatch-0.1.0/src/geobatch/pandas_ext.py +95 -0
  23. geobatch-0.1.0/src/geobatch/progress.py +95 -0
  24. geobatch-0.1.0/src/geobatch/providers/__init__.py +61 -0
  25. geobatch-0.1.0/src/geobatch/providers/base.py +291 -0
  26. geobatch-0.1.0/src/geobatch/providers/commercial.py +514 -0
  27. geobatch-0.1.0/src/geobatch/providers/nominatim.py +270 -0
  28. geobatch-0.1.0/src/geobatch/providers/pelias.py +156 -0
  29. geobatch-0.1.0/src/geobatch/providers/photon.py +148 -0
  30. geobatch-0.1.0/src/geobatch/providers/template.py +228 -0
  31. geobatch-0.1.0/src/geobatch/py.typed +0 -0
  32. geobatch-0.1.0/src/geobatch/quality.py +120 -0
  33. geobatch-0.1.0/src/geobatch/ratelimit.py +90 -0
  34. geobatch-0.1.0/src/geobatch.egg-info/PKG-INFO +338 -0
  35. geobatch-0.1.0/src/geobatch.egg-info/SOURCES.txt +47 -0
  36. geobatch-0.1.0/src/geobatch.egg-info/dependency_links.txt +1 -0
  37. geobatch-0.1.0/src/geobatch.egg-info/entry_points.txt +2 -0
  38. geobatch-0.1.0/src/geobatch.egg-info/requires.txt +9 -0
  39. geobatch-0.1.0/src/geobatch.egg-info/top_level.txt +1 -0
  40. geobatch-0.1.0/tests/conftest.py +60 -0
  41. geobatch-0.1.0/tests/mockserver.py +448 -0
  42. geobatch-0.1.0/tests/test_batch.py +332 -0
  43. geobatch-0.1.0/tests/test_cli.py +337 -0
  44. geobatch-0.1.0/tests/test_core.py +227 -0
  45. geobatch-0.1.0/tests/test_http.py +128 -0
  46. geobatch-0.1.0/tests/test_io.py +151 -0
  47. geobatch-0.1.0/tests/test_pandas.py +37 -0
  48. geobatch-0.1.0/tests/test_providers.py +445 -0
  49. geobatch-0.1.0/tests/test_ratelimit.py +103 -0
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.0] - 2026-11-09
8
+ ### Added
9
+ - `Geocoder` batch orchestrator: de-duplication of normalised queries, thread pool with
10
+ per-provider token-bucket rate limits and concurrency limits, provider fallback chain
11
+ (on errors and, optionally, on "not found"), circuit breaker after consecutive failures,
12
+ `BatchInterrupted` with the partial result on Ctrl+C.
13
+ - Persistent SQLite cache keyed by provider endpoint, query kind, normalised query and
14
+ options; caches found and not-found answers, never errors; resume by re-running;
15
+ per-provider TTL and "do not store" rules.
16
+ - HTTP transport on `urllib` with timeouts, exponential backoff with jitter, `Retry-After`
17
+ (seconds or HTTP date) shared across threads, `max_retry_after` guard, and redaction of
18
+ API keys from every message.
19
+ - Providers: Nominatim (OSMF usage policy enforced on the public instance: identifying
20
+ User-Agent required, 1 req/s, single thread, cache required, 5000 uncached lookups per
21
+ run, policy link printed), Photon (fair-use limits on the public server), Pelias,
22
+ generic URL-template provider, and API-key adapters for OpenCage, Geoapify, Mapbox
23
+ (temporary results not cached) and Google (30-day cache TTL).
24
+ - Structured queries (street/city/county/state/postcode/country), country and bbox
25
+ biasing, language, address normalisation.
26
+ - Structured results (lat, lon, display name, normalised components, confidence,
27
+ importance, bbox, precision, raw JSON) and quality flags: `low_confidence`,
28
+ `country_mismatch`, `outside_bbox`, `duplicate_coordinates`, `region_centroid`,
29
+ `fallback_provider`.
30
+ - I/O: CSV and Excel CSV (BOM, delimiter sniffing) in; CSV, GeoJSON and a
31
+ standard-library GeoPackage writer out; stdlib progress bar; run summary (text/JSON).
32
+ - CLI: `geobatch geocode`, `geobatch reverse`, `geobatch cache stats|clear`,
33
+ `geobatch providers`, `geobatch flags`.
34
+ - Optional pandas helpers: `geocode_dataframe`, `reverse_dataframe`.
@@ -0,0 +1,20 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it as below."
3
+ authors:
4
+ - family-names: "Eminoğlu"
5
+ given-names: "Yusuf"
6
+ email: "yusufeminoglu@gmail.com"
7
+ orcid: "https://orcid.org/0009-0005-6000-2934"
8
+ title: "geobatch"
9
+ version: "0.1.0"
10
+ date-released: "2026-11-09"
11
+ license: "MIT"
12
+ repository-code: "https://github.com/YusufEminoglu/geobatch_sdk"
13
+ url: "https://pypi.org/project/geobatch/"
14
+ abstract: "Polite, resumable batch geocoding and reverse geocoding of spreadsheets over Nominatim, Photon, Pelias and API-key providers, with a persistent SQLite cache, token-bucket rate limiting, Retry-After aware retries, provider fallback and quality flags."
15
+ keywords:
16
+ - geocoding
17
+ - reverse geocoding
18
+ - nominatim
19
+ - openstreetmap
20
+ - gis
geobatch-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yusuf Eminoğlu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,9 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include CITATION.cff
5
+ include pyproject.toml
6
+ include src/geobatch/py.typed
7
+ recursive-include src/geobatch *.py
8
+ recursive-include tests *.py
9
+ recursive-include examples *.py *.csv
@@ -0,0 +1,338 @@
1
+ Metadata-Version: 2.4
2
+ Name: geobatch
3
+ Version: 0.1.0
4
+ Summary: Polite, resumable batch geocoding and reverse geocoding of spreadsheets over Nominatim, Photon, Pelias and API-key providers, with caching, rate limiting, fallback and quality flags.
5
+ Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/YusufEminoglu/geobatch_sdk
8
+ Project-URL: Repository, https://github.com/YusufEminoglu/geobatch_sdk
9
+ Project-URL: Issues, https://github.com/YusufEminoglu/geobatch_sdk/issues
10
+ Project-URL: Changelog, https://github.com/YusufEminoglu/geobatch_sdk/blob/main/CHANGELOG.md
11
+ Keywords: geocoding,reverse-geocoding,batch-geocoding,nominatim,photon,pelias,openstreetmap,address,csv,geojson,geopackage,gis,rate-limiting,cache
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Environment :: Console
21
+ Classifier: Topic :: Scientific/Engineering :: GIS
22
+ Classifier: Topic :: Utilities
23
+ Classifier: Intended Audience :: Science/Research
24
+ Classifier: Intended Audience :: Developers
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Provides-Extra: pandas
30
+ Requires-Dist: pandas>=1.3.0; extra == "pandas"
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
33
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
34
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
35
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
36
+ Dynamic: license-file
37
+
38
+ # geobatch
39
+
40
+ **Polite, resumable batch geocoding for spreadsheets.**
41
+
42
+ `geobatch` turns a CSV of thousands of addresses into coordinates (or a CSV of
43
+ coordinates into addresses) reliably and without upsetting the services you
44
+ depend on. It runs on the Python standard library alone and puts several
45
+ providers behind one interface:
46
+
47
+ - **Nominatim** (public or self-hosted). The OpenStreetMap Foundation usage policy is enforced on the public instance.
48
+ - **Photon** (komoot public demo or self-hosted).
49
+ - **Pelias** (self-hosted or hosted).
50
+ - **Custom HTTP template** for any JSON geocoding API.
51
+ - Thin adapters for API-key services: **OpenCage**, **Geoapify**, **Mapbox** and **Google**. Keys are read only from environment variables and are never logged.
52
+
53
+ Every lookup goes through a persistent SQLite cache, per-provider token-bucket
54
+ rate limiting, retries that honour `Retry-After`, a fallback chain and
55
+ quality checks. A batch that stops halfway can be resumed: run the same
56
+ command again.
57
+
58
+ ```bash
59
+ pip install geobatch # stdlib only
60
+ pip install "geobatch[pandas]" # + DataFrame helpers
61
+ ```
62
+
63
+ Requires Python 3.9 or newer.
64
+
65
+ ---
66
+
67
+ ## Quick start (command line)
68
+
69
+ ```bash
70
+ geobatch geocode addresses.csv --address-col address \
71
+ --provider nominatim \
72
+ --user-agent "MyProject/1.0 (https://my-project.org; gis-team@my-project.org)" \
73
+ -o geocoded.csv
74
+ ```
75
+
76
+ Illustrative output (abridged):
77
+
78
+ ```
79
+ geobatch: Nominatim usage policy applies: https://operations.osmfoundation.org/policies/nominatim/ (1 req/s, results cached, at most 5000 new lookups per run)
80
+ geocoding [##############----------------] 1412/3000 47.1% cached=388 failed=3 elapsed=17:04 eta=19:10
81
+ ...
82
+ geobatch forward summary
83
+ rows 3000
84
+ ok 2941
85
+ not found 52
86
+ errors 3
87
+ skipped (empty) 4
88
+ unique lookups 2710
89
+ cache hits 388
90
+ HTTP requests 2325 (retries 3)
91
+ by provider nominatim=2941
92
+ by precision locality=61, rooftop=2402, street=478
93
+ quality flags country_mismatch=4, duplicate_coordinates=18, region_centroid=61
94
+ ```
95
+
96
+ The output keeps every input column and adds `geobatch_status`, `geobatch_lat`,
97
+ `geobatch_lon`, `geobatch_display_name`, `geobatch_precision`,
98
+ `geobatch_confidence`, `geobatch_importance`, `geobatch_country_code`,
99
+ `geobatch_provider`, `geobatch_flags`, `geobatch_from_cache` and
100
+ `geobatch_error`.
101
+
102
+ More examples:
103
+
104
+ ```bash
105
+ # Structured columns, Germany only, fallback to Photon, GeoJSON output
106
+ geobatch geocode customers.csv --street-col street --postcode-col zip --city-col city \
107
+ --country-codes de --provider nominatim,photon \
108
+ --user-agent "MyProject/1.0 (https://my-project.org)" -o customers.geojson
109
+
110
+ # Self-hosted Nominatim: no public-policy limits, so raise the rate and parallelism
111
+ geobatch geocode big.csv --address-col addr --url nominatim=http://localhost:8080 \
112
+ --rate 50 --concurrency 8 -o big.gpkg
113
+
114
+ # Reverse geocoding
115
+ geobatch reverse points.csv --lat-col lat --lon-col lon \
116
+ --user-agent "MyProject/1.0 (https://my-project.org)" -o points_addresses.csv
117
+
118
+ # A commercial provider (key from the environment, never from the command line)
119
+ export OPENCAGE_API_KEY=...
120
+ geobatch geocode addresses.csv --address-col address --provider opencage -o out.csv
121
+
122
+ # Cache maintenance
123
+ geobatch cache stats
124
+ geobatch cache clear --provider photon
125
+ geobatch cache clear --expired
126
+
127
+ # Provider terms and key variables, quality flag meanings
128
+ geobatch providers
129
+ geobatch flags
130
+ ```
131
+
132
+ Input files: UTF-8 CSV, including the BOM that Excel writes. The delimiter
133
+ (`,` `;` tab `|`) is detected automatically, so semicolon "CSV" exports from
134
+ European Excel work as they are. `--excel` writes the output with a BOM so
135
+ Excel shows accented characters correctly. Output format follows the file
136
+ extension: `.csv`, `.tsv`, `.geojson` or `.gpkg`. GeoPackage is written with
137
+ the standard-library `sqlite3` as an OGC GeoPackage 1.3 point layer in EPSG:4326,
138
+ so GDAL is not needed. QGIS and GDAL read it directly.
139
+
140
+ Exit codes: `0` done, `2` usage or configuration error (including policy
141
+ refusals), `3` aborted after too many consecutive failures, `130` interrupted
142
+ (a partial output is still written).
143
+
144
+ ---
145
+
146
+ ## Responsible use
147
+
148
+ Free geocoders are run by volunteers and small teams on donated hardware. A
149
+ careless batch job can get your IP blocked, and it also costs everyone else
150
+ capacity. geobatch builds the rules in rather than leaving them to the
151
+ documentation.
152
+
153
+ | Provider | Terms / policy | What geobatch enforces |
154
+ |---|---|---|
155
+ | Nominatim (public `nominatim.openstreetmap.org`) | [OSMF Nominatim usage policy](https://operations.osmfoundation.org/policies/nominatim/), [OSM copyright & ODbL](https://www.openstreetmap.org/copyright) | 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. |
156
+ | Photon (public `photon.komoot.io`) | [photon.komoot.io](https://photon.komoot.io/): fair use, heavy use is throttled; [source](https://github.com/komoot/photon) | 1 request/second, single thread, cache required (a conservative choice by geobatch). |
157
+ | Pelias | [documentation](https://github.com/pelias/documentation); hosted services such as [geocode.earth](https://geocode.earth) have their own terms | Optional `PELIAS_API_KEY`, redacted from logs. |
158
+ | OpenCage | [terms](https://opencagedata.com/terms) | Key from `OPENCAGE_API_KEY`; default 1 req/s (free-trial limit). |
159
+ | Geoapify | [terms](https://www.geoapify.com/terms-and-conditions/) | Key from `GEOAPIFY_API_KEY`; default 5 req/s. Attribution is required by Geoapify. |
160
+ | Mapbox | [terms of service](https://www.mapbox.com/legal/tos) | Token from `MAPBOX_ACCESS_TOKEN`. Temporary geocoding results are **not cached**; pass `permanent=True` / `--mapbox-permanent` only if your account includes permanent geocoding. |
161
+ | Google Geocoding | [Maps Platform terms](https://cloud.google.com/maps-platform/terms), [geocoding policies](https://developers.google.com/maps/documentation/geocoding/policies) | Key from `GOOGLE_MAPS_API_KEY`. Cache entries **expire after 30 days**. Google requires results to be used with a Google map. |
162
+
163
+ More rules that apply to every provider:
164
+
165
+ - **Keys** are read from environment variables only and never accepted as
166
+ arguments. They are not part of cache keys, and they are replaced by `***` in
167
+ every error message.
168
+ - **Retries** use exponential backoff with jitter, and a server's
169
+ `Retry-After` always wins. A 429 pauses the provider's token bucket for all
170
+ threads, not just the thread that received it. If a server asks you to wait
171
+ more than `max_retry_after` (300 s by default; usually a used-up daily
172
+ quota), geobatch gives up on that lookup instead of hammering or blocking
173
+ for hours.
174
+ - **Circuit breaker:** after 25 consecutive failed lookups the batch stops,
175
+ because the service is down or blocking you.
176
+ - **De-duplication:** identical addresses (after normalisation) are looked up
177
+ once.
178
+ - **Attribution:** OpenStreetMap-based results (Nominatim, Photon, most Pelias
179
+ installations) are © OpenStreetMap contributors under the ODbL. Credit them
180
+ wherever you publish the data.
181
+
182
+ For large or recurring jobs, run your own Nominatim, Photon or Pelias, or use a
183
+ commercial provider. Self-hosted instances have no built-in limits in
184
+ geobatch. Set `--rate` and `--concurrency` to what your server can handle.
185
+
186
+ ---
187
+
188
+ ## Python API
189
+
190
+ ```python
191
+ from geobatch import Geocoder, Nominatim, Photon, Query, SearchOptions, QualityConfig
192
+
193
+ ua = "MyProject/1.0 (https://my-project.org)"
194
+ geocoder = Geocoder(
195
+ [Nominatim(user_agent=ua), Photon(user_agent=ua)], # fallback chain
196
+ cache="geocache.sqlite", # default: per-user cache dir
197
+ options=SearchOptions(country_codes=("de", "at"), language="en"),
198
+ quality=QualityConfig(min_confidence=0.5, expected_countries=("de", "at")),
199
+ )
200
+
201
+ result = geocoder.geocode_many(
202
+ [
203
+ "Pariser Platz 1, 10117 Berlin",
204
+ Query(street="Stephansplatz 3", postcode="1010", city="Wien", country="Austria"),
205
+ {"city": "Hamburg"},
206
+ ],
207
+ progress=True,
208
+ )
209
+ for rec in result:
210
+ if rec.ok:
211
+ r = rec.result
212
+ print(r.lat, r.lon, r.display_name, r.precision, r.confidence, rec.flags)
213
+ else:
214
+ print(rec.status, rec.error)
215
+ print(result.summary.format())
216
+
217
+ # one-offs
218
+ rec = geocoder.geocode("Brandenburger Tor, Berlin")
219
+ rec = geocoder.reverse(52.5163, 13.3777)
220
+ ```
221
+
222
+ `GeocodeResult` fields: `provider`, `lat`, `lon`, `display_name`,
223
+ `components` (normalised keys such as `house_number`, `road`, `postcode`,
224
+ `city`, `state`, `country`, `country_code`), `confidence` (0..1), `importance`,
225
+ `bbox` `(min_lon, min_lat, max_lon, max_lat)`, `precision` (`rooftop`,
226
+ `interpolated`, `street`, `postcode`, `locality`, `region`, `country`,
227
+ `unknown`), `result_type` and `raw` (the provider's original JSON). If a
228
+ provider gives no confidence score (Nominatim, Photon), `confidence` is
229
+ derived from `precision`.
230
+
231
+ Reading and writing files:
232
+
233
+ ```python
234
+ from geobatch import read_table, build_queries, write_output
235
+
236
+ columns, rows = read_table("addresses.csv")
237
+ queries = build_queries(columns, rows, street_col="street", city_col="city", postcode_col="zip")
238
+ result = geocoder.geocode_many(queries, rows=rows)
239
+ write_output(result.records, "addresses.gpkg")
240
+ ```
241
+
242
+ With pandas (`pip install "geobatch[pandas]"`):
243
+
244
+ ```python
245
+ from geobatch.pandas_ext import geocode_dataframe, reverse_dataframe
246
+
247
+ df = geocode_dataframe(df, geocoder, address_col="address", expected_country_col="iso2")
248
+ df.attrs["geobatch_summary"]
249
+ ```
250
+
251
+ ### Custom HTTP template provider
252
+
253
+ Any JSON API can be described by URL templates and dotted JSON paths:
254
+
255
+ ```python
256
+ from geobatch import TemplateProvider
257
+
258
+ inhouse = TemplateProvider(
259
+ "https://geo.internal/find?address={query}&token={env:GEO_TOKEN}",
260
+ reverse_url="https://geo.internal/reverse?lat={lat}&lon={lon}&token={env:GEO_TOKEN}",
261
+ results_path="data.matches", lat_field="y", lon_field="x",
262
+ display_field="label", confidence_field="score", confidence_scale=0.01,
263
+ name="inhouse", rate=10, concurrency=4,
264
+ )
265
+ ```
266
+
267
+ The available placeholders are `{query}`, `{street}`, `{city}`, `{county}`,
268
+ `{state}`, `{postcode}`, `{country}`, `{country_codes}`, `{language}`,
269
+ `{limit}`, `{lat}`, `{lon}` and `{env:NAME}`. A parameter that uses `{env:...}`
270
+ is redacted from logs and kept out of the cache key. GeoJSON
271
+ FeatureCollections are recognised automatically. On the command line, use
272
+ `--provider template --template-url ...`.
273
+
274
+ ---
275
+
276
+ ## How it works
277
+
278
+ **Normalisation.** Before lookup and before computing cache keys, addresses
279
+ are cleaned: Unicode NFKC (full-width digits, non-breaking spaces), control
280
+ characters, collapsed whitespace, surrounding quotes, doubled or trailing
281
+ separators, and spreadsheet null tokens (`NaN`, `N/A`, `-`) become empty.
282
+ Wording is never rewritten. Cache keys are case-insensitive.
283
+
284
+ **Cache and resume.** The SQLite cache (WAL mode, thread-safe) is keyed by
285
+ provider and endpoint, query kind, normalised query and search options. It
286
+ stores found results and "not found" answers, and each one is committed as
287
+ soon as it arrives. Network errors are never cached. If a batch is
288
+ interrupted (Ctrl+C, crash, laptop lid), run the same command again: finished
289
+ lookups come from the cache and only the rest goes to the network. In Python,
290
+ `BatchInterrupted.result` holds the partial result. The default location is
291
+ the per-user cache directory (`%LOCALAPPDATA%\geobatch`, `~/Library/Caches/geobatch`
292
+ or `~/.cache/geobatch`). Override it with `--cache` or `GEOBATCH_CACHE`.
293
+
294
+ **Rate limiting and concurrency.** Each provider has a token bucket
295
+ (`rate` requests per second, no initial burst) and a concurrency limit, shared
296
+ by all worker threads.
297
+
298
+ **Fallback.** Providers are tried in order. The next one is asked when a
299
+ provider fails, and also when it finds nothing (turn that off with
300
+ `fallback_on_not_found=False` / `--no-fallback-on-not-found`).
301
+
302
+ **Biasing.** `country_codes`, `bbox` and `bounded` are translated into each
303
+ provider's own parameters, for example Nominatim
304
+ `countrycodes`/`viewbox`/`bounded`, Pelias `boundary.*`/`focus.point`,
305
+ Geoapify `filter`/`bias` and Google `components`/`bounds`. Structured queries
306
+ use native structured endpoints where they exist (Nominatim, Pelias, Geoapify,
307
+ Mapbox) and are joined into one line elsewhere.
308
+
309
+ ### Quality flags
310
+
311
+ | Flag | Meaning |
312
+ |---|---|
313
+ | `low_confidence` | confidence below `min_confidence` (default 0.5) |
314
+ | `country_mismatch` | result is outside the expected countries (`--country-codes` or a per-row `--expected-country-col`) |
315
+ | `outside_bbox` | result is outside the expected bounding box (`--bbox --bounded`) |
316
+ | `duplicate_coordinates` | the same point was returned for *different* addresses, a typical sign of a fallback to a street or town centroid |
317
+ | `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 |
318
+ | `fallback_provider` | answered by a provider other than the first in the chain |
319
+
320
+ ---
321
+
322
+ ## Development
323
+
324
+ ```bash
325
+ pip install -e ".[dev,pandas]"
326
+ pytest --cov=geobatch
327
+ ruff check . && ruff format --check src tests && mypy src
328
+ ```
329
+
330
+ The test suite never touches the real network. It runs a local stdlib HTTP
331
+ server (`tests/mockserver.py`) that imitates every provider's JSON, including
332
+ 429/503 with `Retry-After`, malformed JSON, timeouts, rate-limit timing,
333
+ cache hits, resume and fallback.
334
+
335
+ ## License
336
+
337
+ MIT © 2026 Yusuf Eminoğlu. See [LICENSE](LICENSE). To cite geobatch, see
338
+ [CITATION.cff](CITATION.cff).