Commit 1887398
feat: CartoCiudad (IGN España) geocoding provider (#1712)
* feat(geocoding): add CartoCiudad (IGN España) provider
Adds CartoCiudad as a geocoding provider using the public IGN/CNIG REST API:
- Forward geocoding via /geocoder/api/geocoder/find?q=
- Reverse geocoding via /geocoder/api/geocoder/reverseGeocode?lat=&lon=
- No API key required (public service)
- Parses rich Spanish address data: tip_via, portalNumber, postalCode,
refCatastral, provincia, comunidadAutónoma, etc.
- 9 unit tests covering URL building, forward/reverse parsing, edge cases
* style: auto-format (ruff + oxfmt) [pre-commit.ci]
* fix(geocoding): address code review feedback for CartoCiudad provider
- Allow cartociudad.es in Tauri desktop HTTP capability configuration
- Guard against null/empty lat/lng in parseForward/parseReverse
- Validate coordinate bounds and finiteness in geocodeReverse
- Document CartoCiudad in data-integrations.md guide
- Add tests for invalid coordinates and null/empty field parsing
* Address review feedback
Verified the open questions against the live CartoCiudad API before fixing.
- geocodeReverse now wraps longitude into [-180, 180] instead of rejecting it.
MapLibre reports lngLat.lng unwrapped for clicks on repeated world copies, so
the new bounds check was silently dropping legitimate reverse-geocode clicks
for every provider, not just CartoCiudad.
- Handle HTTP 204 / empty response bodies as "no results" in geocodeForward and
geocodeReverse. CartoCiudad answers an unmatched query with 204 and an empty
body (verified against find and reverseGeocode), which response.json() would
reject on, surfacing a nonsense query as "Search failed" rather than no match.
- Validate CartoCiudad coordinates through one shared helper used by both
parseForward and parseReverse: rejects null/blank/non-numeric values ("NaN",
"invalid") and anything outside WGS84 bounds. Replaces the duplicated blocks.
- Reject an array response explicitly in both parsers. find answers with a
single object even for an ambiguous query ("calle mayor" verified), so an
array is an unrecognized shape rather than a candidate list.
- Drop the unused state field from CartoCiudadResult and document why there is
no in-body success flag to check: state is 0 on every observed match and
no-match is signalled by 204, not by a failure code with bogus coordinates.
- Stop forwarding limit in buildForwardUrl; find has no result-count parameter,
so it was a silent no-op. Commented like the Google provider does.
- Skip a blank portalNumber instead of appending a trailing space to the street.
- Tests for each of the above, including the world-copy wrap and the 204 path.
* Address Claude review feedback
- Return an in-range longitude untouched from wrapLongitude instead of running
it through the modulo round-trip, which is not exact in floating point: every
ordinary longitude drifted (-3.7 became -3.7000000000000455, 2.35 became
2.3500000000000227). Also keeps 180 as 180 rather than flipping it to -180.
- Extend the world-copy test to assert an in-range longitude reaches the
provider unperturbed.
* Address Claude review feedback
Checked the two open questions against IGN's official service documentation
(github.com/IDEESpain/Cartociudad) rather than inference.
- Trim province before interpolating it into the display name, so a padded
value renders as "(Madrid)" like the other segments rather than "( Madrid )".
- Replace the empirical "state is 0 on every match observed" note with the
documented reason: IGN records state/stateMsg as suppressed in the current
Elasticsearch-backed geocoder, which cannot rank candidates by degree of
match, so state is permanently 0 and there is no confidence signal to map
onto score.
- Document why CartoCiudad is deliberately excluded from shouldThrottle: the
1 req/sec pace and 1000-row cap are Nominatim's published policy, IGN
documents no rate limit, and its own bulk geocoder processes 60,000 records
per run.
* Address Claude review feedback
- Pace the public CartoCiudad host at ~5 req/sec instead of leaving it
unbounded. The earlier rationale conflated two things: IGN's 60,000-record
figure describes their purpose-built bulk geocoder, not the single-lookup
find/reverseGeocode endpoints this provider calls, so unbounded bursting was
an unverified assumption rather than a sanctioned one. Nominatim's 1 req/sec
is still not borrowed: CARTOCIUDAD_MIN_INTERVAL_MS is our own courtesy
default, and CartoCiudad stays out of shouldThrottle so it keeps no row cap
(IGN documents none). Split the hostname lookup into a helper so the pacing
and policy gates no longer have to share one boolean.
- Fall back to muni when poblacion is absent in the display name. Verified
against the live API: a road kilometre point ("A-6 km 120") returns muni
"Espinosa de los Caballeros" with a null poblacion, so the municipality was
being dropped from a name the raw response carried.
- Document both in the geocoding usage-policy section of the user guide.
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Qiusheng Wu <giswqs@gmail.com>1 parent f879b67 commit 1887398
5 files changed
Lines changed: 513 additions & 24 deletions
File tree
- apps/geolibre-desktop/src-tauri/capabilities
- docs/user-guide
- packages/core/src
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
| 40 | + | |
40 | 41 | | |
41 | 42 | | |
42 | 43 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
| 71 | + | |
71 | 72 | | |
72 | 73 | | |
73 | 74 | | |
| |||
77 | 78 | | |
78 | 79 | | |
79 | 80 | | |
80 | | - | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
81 | 86 | | |
82 | 87 | | |
83 | 88 | | |
84 | 89 | | |
85 | 90 | | |
86 | 91 | | |
87 | 92 | | |
88 | | - | |
| 93 | + | |
89 | 94 | | |
90 | 95 | | |
91 | 96 | | |
| |||
0 commit comments