Metadata-Version: 2.5
Name: catastrogps
Version: 1.3.2
Summary: Official Python client for the Catastro GPS API: parcels from 29 European countries (including the Basque Country and Navarre foral cadastres) by reference, coordinates or address; building units, terrain, ground motion, solar and more
Project-URL: Homepage, https://www.parcelgps.com/developers
Project-URL: Documentation, https://www.parcelgps.com/developers
Project-URL: Support, https://www.parcelgps.com/developers
Author-email: The Hidden Panda <soporte@catastrogps.es>
License-Expression: MIT
License-File: LICENSE
Keywords: cadastral,cadastre,cadastre-api,catastro,europe,france,geojson,germany,gis,italy,land-registry,parcel,portugal,real-estate,spain
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx<1,>=0.25
Requires-Dist: typing-extensions>=4.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.4.0; extra == 'dev'
Description-Content-Type: text/markdown

# catastrogps

Official Python client for the [Catastro GPS API](https://www.parcelgps.com/developers): cadastral parcels from **29 European countries (including the Basque Country and Navarre foral cadastres)**, with one API key: by cadastral reference in 27, by coordinates in all 29.

- Look up a parcel by its **official cadastral reference** or by **coordinates**, with the country detected for you.
- Turn a **postal address into parcels** in Spain and 26 more countries, ranked by confidence.
- Import **every unit of a Spanish building** (dwellings, shops, garages) with paging and free `304` refreshes.
- Get the **parcel outline** (GeoJSON or a `[lat, lng]` ring), and **KML / GPX / DXF** exports.
- **Relief, Natura 2000 and climate** (Copernicus DEM, EEA, ERA5-Land) and **ground motion** (Copernicus EGMS, mm/year) for any covered country.
- **Solar**, **agriculture**, **market**, **score**, **value history** and **comparison** for Spain, Portugal, France, Italy and Germany.
- Quota, prepaid balance and per-minute limit of every response, typed exceptions for every API code.
- Two small dependencies (`httpx`, `typing_extensions`), typed, Python 3.9+.

**Free tier: 250 calls a month, forever. Failed lookups are not charged.** Get a key at [parcelgps.com/developers](https://www.parcelgps.com/developers).

## Install

```bash
pip install catastrogps
```

## Quick start

```python
from catastrogps import CatastroGPS

client = CatastroGPS("pk_live_your_key_here")

parcel = client.parcels.get("9872023VH5797S0001WX")
print(parcel["municipio"], parcel.get("superficieParcela"), parcel["latitud"], parcel["longitud"])
```

`CatastroGPS()` with no arguments reads `CATASTROGPS_API_KEY` from the environment. Use it as a context manager to close the connection pool:

```python
with CatastroGPS() as client:
    ...
```

## Methods

| Method | Endpoint | Countries |
|--------|----------|-----------|
| `parcels.get(ref, country=)` | `GET /api/catastro/{ref}` | All references (see coverage) |
| `parcels.at_point(lat, lng, country=)` | `GET /api/search/coordinates` | All, including HR, SE and UK (Scotland) |
| `search_address_candidates(q, street=, number=, municipality=, postcode=, country=, limit=)` | `GET /api/search/address/candidates` | Spain (with Basque Country and Navarre) and 26 more; not SE or HR |
| `parcels.find_by_address(text)` | `POST /api/search/address/parse` | Spain, central Catastro, live (prefer the candidates search) |
| `get_units(ref14, country=, previous=)`, `iter_unit_pages` | `GET /api/catastro/{ref14}/units` | `ES`, `PV`, `NA` |
| `parcels.geometry(ref, country=)` | `GET /api/catastro/{ref}/polygon` | All but UK |
| `parcels.terrain(ref, country=, lat=, lng=)` | `GET /api/catastro/{ref}/terrain` | All; `lat`/`lng` required outside ES, PV, NA, PT, FR, IT, DE |
| `parcels.ground_motion(ref, country=, lat=, lng=)` | `GET /api/catastro/{ref}/ground-motion` | Same as terrain, inside EGMS coverage |
| `parcels.solar`, `parcels.agriculture`, `parcels.score` | `/solar`, `/agro`, `/score` | ES, PV, NA, PT, FR, IT, DE |
| `parcels.market(ref, country=)` | `/market` | Figures in FR, IT and DE (NRW); a note without figures elsewhere |
| `parcels.value_history(ref, country=)` | `/value-history` | ES, PV, NA, PT, FR, IT, DE, AT |
| `parcels.compare([(ref, country), ...])` | `POST /api/catastro/compare` | 2 or 3 parcels in ES, PT, FR, IT, DE |
| `export.file(ref, "kml" \| "gpx" \| "dxf", country=)` | `GET /api/export/{format}` | Where a geometry exists |
| `resolve(text, hint=)` | `GET /api/resolve` | Free, no quota: which country and kind a text is |

## Examples

```python
in_warsaw = client.parcels.at_point(52.2297, 21.0122)
in_warsaw["referenciaCatastral"], in_warsaw.get("pais")

found = client.search_address_candidates("Damrak 1, 1012 LG Amsterdam", country="NL")
found["candidatos"][0]["refCatastral"], found["candidatos"][0]["confianza"]

terrain = client.parcels.terrain("10194A00110004")
terrain["relief"]["slope"]["class"], terrain["protected_areas"]["natura2000"], terrain.get("climate")

brussels = client.parcels.terrain("21004C0123/00", country="BE", lat=50.848139, lng=4.353613)

motion = client.parcels.ground_motion("0745901TG4304N0002KH")
motion["status"], motion.get("ground_motion", {}).get("vertical", {}).get("mean_mm_year")

comparison = client.parcels.compare([("9872023VH5797S0001WX", "ES"), ("750560000AB0001", "FR")])

kml_bytes = client.export.file("9872023VH5797S0001WX", "kml")

guess = client.resolve("05102200100005")
guess["candidates"]
```

`terrain` and `ground_motion` carry a `source.attribution` string that must be shown next to the data. A ground motion answer with `status: "no_data"` and `reason: "no_reflectors"` means the satellite had nothing stable to measure (fields, forest, water): there is no figure, not a zero.

Responses are the API's `data` object as a `dict`, with the field names the API uses (`refCatastral`, `municipio`, `superficieParcela`…). See the [API reference](https://www.parcelgps.com/developers) and the OpenAPI spec at `https://api.parcelgps.com/api/openapi.json`.

## Import a building (comunidad de propietarios, Spain)

Address, then finca (14-character reference), then every unit with use, area, participation coefficient, stair, floor and door.

```python
found = client.search_address_candidates("Avenida José Rodríguez de la Borbolla Camoyán 10, Dos Hermanas")
finca = found["candidatos"][0]

comunidad = client.get_units(finca["refCatastral"], country=finca["pais"])
comunidad["totalUnidadesFinca"]
for u in comunidad["unidades"]:
    print(u["refCatastral"], u["uso"], u["superficie"], u.get("participacion"), u["escalera"], u["planta"], u["puerta"])
comunidad["dataSource"], comunidad["attribution"]

refreshed = client.get_units(finca["refCatastral"], previous=comunidad)
refreshed["changed"]
```

- `get_units` follows `nextCursor` for you (up to 200 units per page). `iter_unit_pages(ref)` yields page by page.
- **Cost: one quota unit per unit served** in each page, minimum one per page; a page is cut to the quota you have left. Pass the previous result as `previous=` to re-import: unchanged pages come back as `304 Not Modified`, cost nothing and are reused; `changed` tells you if anything moved.
- Basque Country and Navarre (`country="PV"` or `"NA"`) are served from our copy of each foral cadastre: pass the foral reference of any unit or the finca key in `refCatastral`. The foral open data carry no participation coefficient.
- Organisations with a per-finca cap agreed by contract (Startup and Growth) pay at most that cap per finca and period: see `client.last_quota["finca_cap"]` (`{"cap", "used"}`), also in each page's `quota`.

## Quota, prepaid balance and limits

`client.last_quota` is read from the headers of the last response; `conditional_request` also returns the `quota` of that exact response.

| Key | Header | Meaning |
|-----|--------|---------|
| `plan`, `limit`, `remaining`, `resets_at` | `X-Quota-Tier`, `X-Quota-Limit`, `X-Quota-Remaining`, `X-Quota-Reset` | Monthly quota of your plan, after this response |
| `overage`: `balance`, `unit_price`, `remaining_units` | `X-Quota-Overage`, `X-Quota-Balance`, `X-Quota-Overage-Unit-Price`, `X-Quota-Overage-Remaining` | Prepaid pay-as-you-go balance in euros, used once the monthly quota is spent |
| `rate_limit`: `limit`, `remaining`, `reset_seconds` | `X-RateLimit-*` | Per-minute burst limit of your key (Free 10, Developer 60, Startup 120, Growth 300) |
| `finca_cap`: `cap`, `used` | `X-Quota-Finca-Cap`, `X-Quota-Finca-Cap-Used` | Only on `/units`, only with a contract cap |

When the monthly quota is spent and there is no prepaid balance left, the API answers `429 KEY_AUTH_004` (never `402`): the client raises `QuotaExceededError` with the `quota` of that response, and `prepaid_balance_empty` is true when your organisation has the prepaid overage on (top up at `https://parcelgps.com/app/developer`). Only 2xx responses spend quota; `300`, `304`, `4xx` and `5xx` are free, and so are 2xx answers without data.

## Errors

Every error is a `CatastroGPSError` with `status`, `code` and `details`:

```python
from catastrogps import (
    AmbiguousReferenceError,
    CoverageError,
    NotFoundError,
    PlaceNameError,
    QuotaExceededError,
)

try:
    client.parcels.get("05102200100005")
except AmbiguousReferenceError as error:
    client.parcels.get("05102200100005", country=error.candidates[0]["country"])
except PlaceNameError as error:
    print("A place name, not a reference", error.location)
except CoverageError as error:
    print(error.country, error.feature, error.supported_countries)
except NotFoundError as error:
    print(error.message)
except QuotaExceededError as error:
    print("Quota spent", error.quota, error.prepaid_balance_empty)
```

| Exception | API code / status |
|-----------|-------------------|
| `AmbiguousReferenceError` | `300 CNV_AMBIGUOUS`: bare digits of several countries; `candidates` lists them |
| `CoverageError` | `422 CNV_COVERAGE`: country not covered, or an analysis outside its countries |
| `PlaceNameError` (a `ValidationError`) | `422 CNV_PLACE_NAME`: a place name, with the geocoded `location` when found |
| `ValidationError` | `400 VALIDATION_ERROR`, other `422` |
| `AuthenticationError` / `ForbiddenError` | `401 KEY_AUTH_001..003`, `403` |
| `NotFoundError` | `404 NOT_FOUND` |
| `QuotaExceededError` | `429 KEY_AUTH_004` |
| `RateLimitError` | `429 KEY_RATE_002`, `RATE_LIMIT_EXCEEDED`; `retry_after` |
| `ServiceUnavailableError` | `503 SERVICE_UNAVAILABLE`; `retry_after` |
| `ServerError`, `TimeoutError`, `NetworkError` | `5xx`, client timeout, network failure |

Timeouts, network errors, per-minute 429s and 502/503/504 are retried up to `max_retries` times (default 2) with exponential backoff, waiting `Retry-After` when the API sends it (up to 60 s). An exhausted monthly quota is never retried.

## Options

| Argument | Default | |
|----------|---------|---|
| `api_key` | `CATASTROGPS_API_KEY` env var | Required |
| `base_url` | `https://api.catastrogps.es` | `https://api.parcelgps.com` is the same API |
| `timeout` | `30.0` seconds | Official cadastres can be slow |
| `max_retries` | `2` | |
| `http_client` | a new `httpx.Client` | Inject your own for proxies or tests (`httpx.MockTransport`) |

## Coverage

Measured against production on 1 October 2026. 29 European countries, including the Basque Country and Navarre foral cadastres: by cadastral reference in 27, by coordinates in all 29; in Croatia, Sweden and the United Kingdom the source is partial, as the table shows.

| Code | Country / region | Reference | Coordinates | Address | Notes |
|------|------------------|:---:|:---:|:---:|-------|
| `ES` | Spain | Yes | Yes | Yes | Units per building |
| `PV` · `NA` | Basque Country · Navarre | Yes | Yes | With `country="ES"` | Foral cadastres; units per building |
| `FR` · `IT` | France · Italy | Yes | Yes | Yes | |
| `DE` | Germany | Partial | Partial | Partial | 15 of 16 Länder: **no Bavaria** |
| `PT` | Portugal | Yes | Partial | Partial | The cadastre does not cover Lisbon, Porto or Coimbra |
| `AT` `NL` `BE` `PL` `CZ` `DK` `NO` `FI` `EE` | Austria, Netherlands, Belgium, Poland, Czechia, Denmark, Norway, Finland, Estonia | Yes | Yes | Yes | Belgium is slow (3-15 s) |
| `CH` | Switzerland | Yes | Partial | Yes | Not in cantons that publish no parcels (e.g. Vaud) |
| `LT` `SI` `SK` `BG` `LU` `LI` `IS` `CY` | Lithuania, Slovenia, Slovakia, Bulgaria, Luxembourg, Liechtenstein, Iceland, Cyprus | Yes | Yes | Yes | |
| `GR` · `LV` | Greece · Latvia | With the country set | Yes | Yes | |
| `IE` | Ireland | With `country="IE"` | Partial | Partial | Numeric SP_ID |
| `SE` | Sweden | Agricultural blocks | Agricultural blocks | No | Not property units |
| `HR` | Croatia | No | ARKOD only | No | Agricultural parcels of the Paying Agency, not the cadastre |
| `UK` | United Kingdom | No | Scotland | Scotland | |

Not covered: Hungary, Romania and the rest (`CoverageError`). Terrain and ground motion work wherever a parcel outline exists, with `lat`/`lng` outside the seven analysis countries. Data comes from each official source, so availability follows theirs.

## Pricing

| Plan | Price | Calls / month | Prepaid overage per 1,000 |
|------|-------|---------------|---------------------------|
| Free | €0, forever | 250 | €10 |
| Developer | €19 / month | 5,000 | €5 |
| Startup | €49 / month | 15,000 | €4 |
| Growth | €99 / month | 50,000 | €2.50 |

Overage prices are net (VAT apart) and are charged from a prepaid balance you top up in the portal; with the balance at zero the API answers `429`, never debt.

The same key works with the JavaScript SDK (`npm install catastrogps`), the Go SDK and the MCP server for AI agents ([`catastro-gps-mcp`](https://www.npmjs.com/package/catastro-gps-mcp)).

## License

MIT
