--- name: geospatial-open-data description: Estonian public map services - what a register holds at a given place, from cadastral, planning, heritage, forest and nature layers. module: kaardikihid.mjs execution: code --- # Geospatial Open Data These services draw and describe geometry; they do not look an address up. Go from the place a citizen named to coordinates with `address-search` first — `findAddress()` returns `x` (easting) and `y` (northing) in L-EST97, which is what everything here takes. What this answers that no other recipe does is the **spatial** question: what a point falls inside, and what lies within N metres of it. Cadastral unit and land use, heritage protection zones, planning restrictions, forest and nature register polygons, administrative borders. ## Access One GeoServer publishes **1,091 feature layers across 43 workspaces**, all public, no authentication: - `https://gsavalik.envir.ee/geoserver/ows` — WFS 2.0.0 for every workspace at once. The per-workspace paths (`/geoserver/etak/ows`) answer only their own. - Workspaces worth knowing: `kataster` (cadastre), `planeeringud` (54 planning layers), `muinsuskaitse` (heritage), `metsaregister`, `eelis` (nature, 104 layers), `ehak` (administrative borders), `etak` (topography, 39 layers), `kmanahtused`/`kponahtused` (restriction zones). - `https://kaart.maaamet.ee/wms/{aadressid,alus,fotokaart}` — Maa-amet's rendered base maps, WMS 1.1.1. Imagery; `GetFeatureInfo` is the only route to attributes, and it answers `text/plain` or GML. - Service index: `https://geoportaal.maaamet.ee/eng/services/public-wms-wfs-p346.html`. Bulk downloads: `https://geoportaal.maaamet.ee/eng/spatial-data-p58.html`. ## Retrieve 1. `mapLayers(term, {workspace})` — find the layer. Names are `:` and are **never guessed**: an invented one is an HTTP 400 naming it, but a plausible wrong one is a 200 with no features, which reads like an answer. 2. `featuresAt(x, y, {layer, radius})` — what that layer holds at the point. `radius` is the half-width in metres of the box searched (25 m by default); a bigger radius answers "what is near here", which is the question for protection zones. 3. `mapFeatures(layer, {bbox, cql})` for a box or an attribute filter; `pointInfo(x, y)` for the Maa-amet WMS layers no WFS publishes. ```probe-limit claim an invented one is an HTTP 400 naming it # `kataster:ei_ole_sellist_kihti` is not a layer, and GeoServer says so with # the name in the ExceptionText. That is what makes a guessed layer name a # loud failure rather than the silent empty answer a plausible-but-wrong one # produces, which is the whole reason this guide forbids guessing. GET https://gsavalik.envir.ee/geoserver/ows?service=WFS&version=2.0.0&request=GetFeature&typeNames=kataster:ei_ole_sellist_kihti&count=1&outputFormat=application/json&bbox=541412.4,6589688.12,541462.4,6589738.12,EPSG:3301 expect-status 400 expect /Feature type kataster:ei_ole_sellist_kihti unknown/ # The control is the real layer at the same box: HTTP 200 with features. A # GeoServer that had gone down would answer every request with an error and # so confirm this limitation while telling us nothing. control GET https://gsavalik.envir.ee/geoserver/ows?service=WFS&version=2.0.0&request=GetFeature&typeNames=kataster:ky_kehtiv&count=1&outputFormat=application/json&bbox=541412.4,6589688.12,541462.4,6589738.12,EPSG:3301 ``` **The axis-order trap, which is why this is a module and not a worked script.** The WFS reads a `bbox` as **northing,easting** when the CRS is omitted or written as `urn:ogc:def:crs:EPSG::3301`, and as **easting,northing** when it is written `EPSG:3301`. Measured 2026-08-02 at Vabriku tn 12: easting-first with no CRS returned `numberMatched: 0`; the identical box with `,EPSG:3301` appended returned 40 features. The WMS is the other way round again — 1.1.1 `BBOX` is easting-first. The module sends the right form for each and takes `x` before `y` throughout. ## Return The layer's authoritative name **and its title** (`kataster:ky_kehtiv` means nothing to a reader; "KÜ piirid" does), the register's own attribute values, `totals.matched` as the count, the coordinates queried, and the service URL. Geometry comes back as its type, its bounding box and a point count — a cadastral polygon's full ring is not something an answer needs. ## Limits - **Zero features means zero features in that layer at that point.** It is not evidence about the place. Confirm the layer before reporting an absence. - Several of these registers have their own recipe — planning, forest, heritage, land register — which answer in words and are better for anything not spatial. - WMS is rendered imagery. `GetFeatureInfo` reports what the service drew at a pixel, not a register query. - CRS is L-EST97 (EPSG:3301) throughout; a few layers advertise EPSG:4326 or 3035 instead, which `mapLayers()` reports per layer. - The all-workspace capabilities document is ~1.3 MB. Fetch it to disk once per question and read it from there; it is far too large to pull into a model's context. - That document is also what puts this recipe out of reach of plain requests: every data request has to name a layer confirmed against it, so the list must be fetched and filtered somewhere other than the answer. ## Verify - Capabilities must parse and list feature types before any data request. A layer name not in that list is not a layer. - A feature response carries `numberMatched`; use it rather than counting what was printed. - An unknown layer or a malformed filter answers with an OGC `ExceptionReport` (HTTP 400) — a loud failure, and never to be reported as "nothing found". ```probe-limit claim An unknown layer or a malformed filter answers with an OGC `ExceptionReport` (HTTP 400) # The malformed-filter half of that sentence (the unknown-layer half is probed # under Retrieve). `ei_ole_sellist_valja` is not an attribute of ky_kehtiv, and # the service names it back instead of returning zero features. GET https://gsavalik.envir.ee/geoserver/ows?service=WFS&version=2.0.0&request=GetFeature&typeNames=kataster:ky_kehtiv&count=1&outputFormat=application/json&cql_filter=ei_ole_sellist_valja%3D%27x%27 expect-status 400 expect / {source, page, term, totals, layers, note} featuresAt(x, y, {layer, radius, limit}) -> {source, page, layer, point, radius, totals, features, note}