{
  "openapi": "3.1.0",
  "info": {
    "title": "Mantal API",
    "version": "0.1.0",
    "description": "Unified API for Swedish business & civil-registry data. One bearer, one bill, one URL across persons, real estate, companies and geography.\n\n## Two surfaces per dataset\n\nMost datasets are exposed both as a **curated/unified** namespace (English-leaning keys, WGS84, cross-dataset joins) AND a **native** namespace (original field names, SWEREF coords, no envelope). Pick what fits your code — they share the same database and billing labels.\n\n## Coverage\n\n- **Persons** — `/v1/people/*` (the consolidated civil-registry surface: PNR lookup + name search, curated `{ok}` envelope, full folkbokföring + address + name history). Legacy aliases `/v1/persons/*` (same curated surface) and `/v1/spar/*` (raw-Person native shape) are deprecated.\n- **Real estate** — `/v1/properties/*`, `/v1/buildings/*`, `/v1/addresses/*` (curated) + `/v1/lantmateriet/*` (native shape: register designation, property, building, location address).\n- **Companies** — `/v1/companies/*` (1.4M offices / 759k distinct orgnos).\n- **Cross-dataset join** — `/v1/addresses/{id}/registered` returns the people + companies registered at a location address. The killer feature of the unified model.\n- **Geography** — `/v1/geo/*`. Pelias-style hierarchy (countries → regions → localities → boroughs + places + postal codes).\n- **Tiles** — `/v1/tiles/styles*` (MapLibre) + vector tiles at `tiles.mantal.{eu,se}`.\n- **Account** — `/v1/signup*`, `/v1/login*`, `/v1/me` for self-serve key issuance, magic-link / 6-digit-code key rotation, and bearer introspection.\n\n## Auth\n\nAll authed endpoints accept `Authorization: Bearer pk_live_…` (billed against `credit_ore`) or `pk_test_…` (test environment, unlimited credit). Get a key at [mantal.eu/signup](https://mantal.eu/signup) — currently restricted to a closed-beta domain allowlist.\n\n## The `?kommun=` polymorphic param\n\nEvery endpoint that scopes by kommun accepts five forms, all canonicalised to the same row: name (`Stockholm` / `Göteborg`), slug (`stockholm` / `goteborg`), SCB code (`0180`), Pelias id (`se:locality:stockholm`), or accent-stripped name. 290 distinct Swedish kommuner, no ambiguity.\n\n## Billing\n\nTwo pricing models live side by side.\n\n**Persons — tiered.** `persons_lookup` 40→0.10 SEK per call as daily volume grows; `persons_search` flat 1.00 SEK. Cache hits free. The deprecated `/v1/spar/*` native alias writes the same labels — no double-charging.\n\n**Real estate + Companies — flat tiers.** LOOKUP **0.10 SEK** · SEARCH **0.05 SEK** · HEAVY (bbox / reverse / cross-dataset join) **0.20 SEK**. Same price on curated and native surfaces; surface is presentation, not product tier.\n\nInsufficient credit returns 402 `credit_exhausted` with a top-up link. Internal keys (`credit_ore IS NULL`) are unlimited.",
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://mantal.eu",
      "description": "Production (canonical)"
    },
    {
      "url": "https://mantal.se",
      "description": "Production (mirror)"
    },
    {
      "url": "https://api.mantal.eu",
      "description": "Production (direct API host)"
    },
    {
      "url": "https://api.mantal.se",
      "description": "Production (direct API host, mirror)"
    }
  ],
  "security": [
    {
      "BearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "account",
      "description": "Self-serve signup, magic-link sign-in (which rotates your keys), and bearer introspection (`GET /v1/me`). Start here to mint a `pk_live_…` / `pk_test_…` pair."
    },
    {
      "name": "billing",
      "description": "Stripe-backed credit top-ups. POST a SEK amount to `/v1/billing/checkout`, redirect the user to the returned `url`, and the webhook credits `credit_ore` on completion. List past top-ups via `/v1/billing/transactions`. Test keys (`plan='test'`) are rejected — they're unlimited by design."
    },
    {
      "name": "service",
      "description": "Liveness probe, service descriptor, and the OpenAPI spec itself. No auth required. Useful for curl-driven discovery and uptime checks."
    },
    {
      "name": "people",
      "description": "**The consolidated person surface.** ⚠️ **SPAR is currently disabled** — PNR lookup resolves from the register (`public.people`, exact PNR) + a free mrkoll scrape only; SPAR name search and the native `/v1/spar/*` alias return 503 `spar_disabled`. Returns the Mantal `{ ok, ... }` envelope with English-leaning keys. `POST /v1/people/lookup` takes `{ pnr }` (register→mrkoll), `{ name, city? }` or `{ address, city? }` (Hitta register), or `{ pnr, name }` (register birthday+name match — folkbokförings-address + history, never SPAR); `?source=register|mrkoll|auto` controls the source for PNR (default `auto` = register→mrkoll; `spar`/`all` return `person: null` + `spar_disabled` while SPAR is off). `POST /v1/people/search` (SPAR name search) returns 503 `spar_disabled`; `GET /v1/people/search/{searchId}` replays a cached prior search (free). `shape=rich|flat` selects nested-Person vs wide-attribute projection. Tiered pricing: `persons_lookup` 40→0.10 SEK; `persons_search` flat 1.00 SEK; cache hits + register lookups free. (Legacy aliases: `/v1/persons/*` for this same curated surface, and `/v1/spar/*` for the raw-Person native shape — both deprecated.)"
    },
    {
      "name": "properties",
      "description": "Swedish real-estate parcels (curated surface): UUID + beteckning lookup (with `?kommun=` disambiguation), free-text search, reverse-by-coords, and buildings-on-property. PostGIS-backed reads. Native-shape equivalent at `/v1/lantmateriet/fastighet/*`."
    },
    {
      "name": "buildings",
      "description": "Swedish buildings (curated surface): UUID lookup and WGS84 bounding-box query (capped at 10 sq.deg, `limit` ≤ 500). Native-shape equivalent at `/v1/lantmateriet/byggnad/*` returning Byggnad GeoJSON Features in SWEREF 99 TM."
    },
    {
      "name": "addresses",
      "description": "Swedish location addresses. Typeahead-friendly trigram search with the polymorphic `?kommun=` filter (accepts name `Stockholm`, slug `stockholm`, SCB code `0180`, Pelias id `se:locality:stockholm`, or accent-stripped form). Lookup by id includes the cross-dataset `/v1/addresses/{id}/registered` join → registered people + companies."
    },
    {
      "name": "companies",
      "description": "**Swedish company directory** — id lookup, by-orgno, name search across 1.4M offices (759k distinct orgnos). Same `?kommun=` polymorphic resolver as the property surface (accepts the name `Stockholm`, slug `stockholm`, SCB code `0180`, or Pelias id `se:locality:stockholm`)."
    },
    {
      "name": "listings",
      "description": "Swedish rental listings aggregated from ~150 sources (municipal housing companies, Boplats, Homii, Bofrid, Kvalster). 88% of rows are `rented` or `removed` — the archive carries rent history and time-on-market, so the list endpoint does not filter to active by default. Pass `?status=active` for live inventory. Each row is one dwelling offered at a price over a period."
    },
    {
      "name": "knowledge",
      "description": "**Knowledge base** — Mantal-owned free-text notes/facts attached to a company (by `orgno`) or a person (by `pnr`), layered on top of the read-only registry snapshots. A pure value-add with no upstream equivalent (curated-only). Adds/edits/deletes are **free**; list = SEARCH (0.05 SEK), single fetch = LOOKUP (0.10 SEK). Keyed on the 10-digit orgno core / 12-digit personnummer."
    },
    {
      "name": "lm-registerbeteckning",
      "description": "**Native shape** — register-designation search. Beteckning stubs (`{ id, typ, headertext, subtext, position }`) in autocomplete format. Coordinates are SWEREF 99 TM (EPSG:3006)."
    },
    {
      "name": "lm-fastighet",
      "description": "**Native shape** — full property object with all markområden inline (`enhetsutbredning[]`). Swedish field names, SWEREF 99 TM coordinates, no envelope."
    },
    {
      "name": "lm-byggnad",
      "description": "**Native shape** — building objects as GeoJSON Features (with `byggnadsattribut`, `byggnadsdel[].byggnadsyta`, `liggerPa.registerenhetsreferens`, etc.). Collections come back as a GeoJSON FeatureCollection. SWEREF 99 TM coordinates."
    },
    {
      "name": "lm-belagenhetsadress",
      "description": "**Native shape** — location address. Currently stub-only (autocomplete shape). The full location-address object (kvarter, lägenhetsbeteckning, fastighetsreferens) is not yet enabled."
    },
    {
      "name": "countries",
      "description": "**Country** — the root of the hierarchy. Endpoints: `GET /v1/geo/countries`, `GET /v1/geo/countries/{code}`. 19 countries on file; only Sweden has launched data so far. Country code is ISO 3166-1 alpha-2 (`se`, `de`, `fr`, …)."
    },
    {
      "name": "regions",
      "description": "**Region** — country-specific name varies: *län* (SE), *Bundesland* (DE), *Région* (FR), *fylke* (NO). Endpoints: `GET /v1/geo/regions`, `GET /v1/geo/regions/{slug}`. 280 regions on file across all loaded countries; Sweden has 21 län."
    },
    {
      "name": "counties",
      "description": "**County** — intermediate level used in some countries but **not in Sweden** (län collapses straight into kommun). *Landkreis* (DE), *Département* (FR). Endpoints: `GET /v1/geo/counties`, `GET /v1/geo/counties/{slug}`."
    },
    {
      "name": "localities",
      "description": "**Locality / municipality** — country-specific name varies: *kommun* (SE), *Gemeinde* (DE), *Commune* (FR), *kommune* (NO). Endpoints: `GET /v1/geo/localities`, `GET /v1/geo/localities/{slug}`. 77 657 localities on file across all 19 loaded countries; only Sweden is currently enabled (290 kommuner)."
    },
    {
      "name": "boroughs",
      "description": "**Borough / city district** — subdivisions inside a locality. *Stadsdelsområde* (SE), *Stadtbezirk* (DE), *Quartier* (FR). Endpoints: `GET /v1/geo/boroughs`, `GET /v1/geo/boroughs/{slug}`. 107 boroughs on file (mostly Stockholm, Göteborg, Malmö)."
    },
    {
      "name": "neighbourhoods",
      "description": "**Neighbourhood** — smallest unit in the admin hierarchy. Rarely populated for SE — statistical small areas live under `places` (RegSO) instead. Endpoints: `GET /v1/geo/neighbourhoods`, `GET /v1/geo/neighbourhoods/{slug}`."
    },
    {
      "name": "universal-geo",
      "description": "Universal hierarchy walker — same data as the per-level tags but addressed by Pelias-formatted id (`se` / `se:region:stockholms-lan` / `se:locality:stockholm` / `se:borough:stockholm:sodermalm`). Useful when you don't know the layer up-front or when you want children/parents at any depth. Endpoints: `GET /v1/geo/admin/{id}`, `…/children`, `…/parents`, and the filtered list `GET /v1/geo/admin`."
    },
    {
      "name": "places",
      "description": "**Statistical / populated places** — outside the admin hierarchy. *Tätort* and *RegSO* in SE; `city` / `town` / `village` / `suburb` elsewhere. Attached to an admin parent via `parent_admin_id` but not part of the parent/child tree. 4 800 places on file."
    },
    {
      "name": "postal-codes",
      "description": "**Postnummer** — separate axis from the admin hierarchy because postcodes don't nest cleanly (one code can straddle multiple kommuner). Each row links to the primary locality. 93 603 postcodes on file."
    },
    {
      "name": "geo-search",
      "description": "Free-text search (`/v1/geo/search`), typeahead (`/v1/geo/autocomplete`), reverse geocoding (`/v1/geo/reverse`), and dataset counts (`/v1/geo/stats`). Returns full Pelias-style address context (country → region → … → borough). Trigram similarity under the hood; tolerant of typos and diacritics."
    },
    {
      "name": "tiles",
      "description": "Self-hosted MapLibre styles (served by this API) and vector tiles (served at `tiles.mantal.{eu,se}`). Vector-tile schema documented inline on the `/mantal/{z}/{x}/{y}.mvt` endpoint."
    },
    {
      "name": "admin",
      "description": "Operator-only endpoints. Real-time cost reconciliation against the monthly invoice. Requires `X-Admin-Key`, not a bearer token."
    }
  ],
  "x-tagGroups": [
    {
      "name": "Get started",
      "tags": [
        "account",
        "service"
      ]
    },
    {
      "name": "Billing",
      "tags": [
        "billing"
      ]
    },
    {
      "name": "Persons",
      "tags": [
        "people"
      ]
    },
    {
      "name": "Real estate",
      "tags": [
        "properties",
        "buildings",
        "addresses"
      ]
    },
    {
      "name": "Companies",
      "tags": [
        "companies"
      ]
    },
    {
      "name": "Rental listings",
      "tags": [
        "listings"
      ]
    },
    {
      "name": "Knowledge",
      "tags": [
        "knowledge"
      ]
    },
    {
      "name": "Native shape",
      "tags": [
        "lm-registerbeteckning",
        "lm-fastighet",
        "lm-byggnad",
        "lm-belagenhetsadress"
      ]
    },
    {
      "name": "Geo — hierarchy",
      "tags": [
        "countries",
        "regions",
        "counties",
        "localities",
        "boroughs",
        "neighbourhoods",
        "universal-geo"
      ]
    },
    {
      "name": "Geo — lookup",
      "tags": [
        "geo-search",
        "places",
        "postal-codes"
      ]
    },
    {
      "name": "Maps",
      "tags": [
        "tiles"
      ]
    },
    {
      "name": "Operator",
      "tags": [
        "admin"
      ]
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "tags": [
          "service"
        ],
        "summary": "Liveness probe",
        "description": "Returns the current environment and today's call counts. No auth.",
        "security": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "tags": [
          "service"
        ],
        "summary": "This spec",
        "security": [],
        "responses": {
          "200": {
            "description": "OpenAPI 3.1 spec"
          }
        }
      }
    },
    "/v1/billing/checkout": {
      "post": {
        "tags": [
          "billing"
        ],
        "summary": "Start a Stripe Checkout session for a credit top-up",
        "description": "Creates a Stripe Checkout Session for the chosen SEK amount, pre-records a `pending` row in `mantal.credit_transactions`, and returns the `url` to redirect the user to. Amount clamped to 50…50 000 SEK. Refuses `plan='test'` keys (test keys are unlimited by design).\n\nWhen the user completes payment, Stripe POSTs `checkout.session.completed` to `/v1/billing/webhook`, which is idempotent (anchored on the unique partial index on `stripe_session_id`) and credits `credit_ore` on the live key tied to the same `customer_id`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "amount_sek"
                ],
                "properties": {
                  "amount_sek": {
                    "type": "integer",
                    "minimum": 50,
                    "maximum": 50000,
                    "example": 200
                  },
                  "return_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Where to send the user after checkout. Defaults to `WEB_BASE_URL/dashboard/billing?topup=success&session_id={CHECKOUT_SESSION_ID}`."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Checkout session created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri",
                      "description": "Redirect the browser here to complete payment."
                    },
                    "session_id": {
                      "type": "string"
                    },
                    "amount_sek": {
                      "type": "integer"
                    },
                    "amount_ore": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "description": "test_key_not_eligible — test keys are unlimited; top-ups are for live keys only."
          },
          "404": {
            "description": "no_active_live_key — customer has no live key to credit"
          },
          "503": {
            "description": "stripe_unconfigured — `STRIPE_SECRET_KEY` not set on this deploy"
          }
        }
      }
    },
    "/v1/billing/transactions": {
      "get": {
        "tags": [
          "billing"
        ],
        "summary": "List your top-up history",
        "description": "Returns the holder's `credit_transactions` rows (Stripe Checkout sessions plus any manual grants / refunds), most recent first. Includes both `pending` and `completed` rows so the dashboard can render in-flight sessions.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Transactions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "transactions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "source": {
                            "type": "string",
                            "enum": [
                              "stripe_checkout",
                              "manual_grant",
                              "refund"
                            ]
                          },
                          "amount_ore": {
                            "type": "integer"
                          },
                          "amount_sek": {
                            "type": "number"
                          },
                          "currency": {
                            "type": "string",
                            "example": "SEK"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "pending",
                              "completed",
                              "failed",
                              "refunded"
                            ]
                          },
                          "stripe_session_id": {
                            "type": "string",
                            "nullable": true
                          },
                          "created_at": {
                            "type": "integer",
                            "description": "Unix ms"
                          },
                          "completed_at": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Unix ms"
                          },
                          "notes": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/admin/billing": {
      "get": {
        "tags": [
          "admin"
        ],
        "summary": "Real-time cost",
        "description": "Computes today / month / year cost from the local `usage_log`, applying the registry's tiered pricing. There is no programmatic billing API — this is our ground truth. Reconcile against the monthly invoice; expect <1% drift.",
        "security": [
          {
            "AdminKey": []
          }
        ],
        "parameters": [
          {
            "name": "env",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "test",
                "prod"
              ]
            },
            "description": "Override the configured environment"
          }
        ],
        "responses": {
          "200": {
            "description": "Cost summary",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Billing"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "503": {
            "description": "ADMIN_KEY not configured"
          }
        }
      }
    },
    "/v1/tiles/styles": {
      "get": {
        "tags": [
          "tiles"
        ],
        "summary": "List available MapLibre styles",
        "description": "Returns the public MapLibre/Mapbox-GL JS styles bundled with the API. Each style references the vector-tile endpoint at `tiles.mantal.{se,eu}`.",
        "security": [],
        "responses": {
          "200": {
            "description": "Style index",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "styles": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string",
                            "example": "mantal-light"
                          },
                          "url": {
                            "type": "string",
                            "format": "uri"
                          },
                          "theme": {
                            "type": "string",
                            "enum": [
                              "light",
                              "dark"
                            ]
                          }
                        }
                      }
                    },
                    "tiles_endpoint": {
                      "type": "string",
                      "example": "https://tiles.mantal.se/mantal/{z}/{x}/{y}.mvt"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/tiles/styles/{name}.json": {
      "get": {
        "tags": [
          "tiles"
        ],
        "summary": "Fetch a MapLibre style JSON",
        "description": "Returns a [Mapbox GL JS style spec](https://maplibre.org/maplibre-style-spec/) document. Pass it directly to `new maplibregl.Map({ style: 'https://mantal.se/v1/tiles/styles/mantal-light.json' })`.",
        "security": [],
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "mantal-light",
                "mantal-dark"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Style JSON",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "MapLibre style spec"
                }
              }
            }
          },
          "404": {
            "description": "Unknown style"
          }
        }
      }
    },
    "/mantal/{z}/{x}/{y}.mvt": {
      "get": {
        "tags": [
          "tiles"
        ],
        "summary": "Fetch a vector tile",
        "description": "Served by the tile container at `tiles.mantal.se` / `tiles.mantal.eu` (NOT mantal.se). Returns a [Mapbox Vector Tile](https://github.com/mapbox/vector-tile-spec/tree/master/2.1) (protobuf). The available `source-layer` names are exactly: `aerodrome_label`, `aeroway`, `boundary`, `building`, `housenumber`, `landcover`, `landuse`, `mountain_peak`, `park`, `place`, `poi`, `transportation`, `transportation_name`, `water`, `water_name`, `waterway`. Road classes are on `transportation` (`motorway` / `trunk` / `primary` / `secondary` / `tertiary` / `minor` / `service` / `track` / `path`); place classes on `place` (`country` / `state` / `city` / `town` / `village` / `suburb` / `neighbourhood`).",
        "security": [],
        "servers": [
          {
            "url": "https://tiles.mantal.eu",
            "description": "Tile server (canonical)"
          },
          {
            "url": "https://tiles.mantal.se",
            "description": "Tile server (mirror)"
          }
        ],
        "parameters": [
          {
            "name": "z",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 14
            },
            "description": "Zoom level (0=world, 14=building detail). MapLibre overzoom renders past z=14 by stretching z=14 tiles; direct requests above z=14 return 404."
          },
          {
            "name": "x",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Tile column"
          },
          {
            "name": "y",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Tile row"
          }
        ],
        "responses": {
          "200": {
            "description": "Vector tile",
            "content": {
              "application/x-protobuf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "204": {
            "description": "Empty tile (valid coordinates, no features)"
          },
          "404": {
            "description": "Tile outside dataset bounds or invalid coordinates"
          }
        }
      }
    },
    "/": {
      "get": {
        "tags": [
          "service"
        ],
        "summary": "Service descriptor",
        "description": "Tiny JSON pointer to the human site, docs, OpenAPI, and health probe. Useful for curl-driven discovery — the marketing landing page lives on `mantal.eu`, not this container.",
        "security": [],
        "responses": {
          "200": {
            "description": "Descriptor",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "service": {
                      "type": "string",
                      "example": "mantal-api"
                    },
                    "description": {
                      "type": "string"
                    },
                    "site": {
                      "type": "string",
                      "format": "uri"
                    },
                    "docs": {
                      "type": "string",
                      "format": "uri"
                    },
                    "openapi": {
                      "type": "string",
                      "format": "uri"
                    },
                    "health": {
                      "type": "string",
                      "format": "uri"
                    },
                    "version": {
                      "type": "string",
                      "example": "0.1.0"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/signup": {
      "post": {
        "tags": [
          "account"
        ],
        "summary": "Request a verification email",
        "description": "Sends an email with a 24-hour verification link to `email`. The link redirects to `/signup/verify?token=…` on the mantal-web site, which then calls `POST /v1/signup/verify`.\n\nClosed beta: only approved domains are accepted right now.\n\nAlways returns 202 on accepted domains — does **not** reveal whether the address is new or repeat (idempotent at verify-time).",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "example": "you@example.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted — check email",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string",
                      "example": "check_email"
                    },
                    "email": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "description": "Domain not on the closed-beta allowlist"
          }
        }
      }
    },
    "/v1/signup/verify": {
      "post": {
        "tags": [
          "account"
        ],
        "summary": "Redeem signup token, mint key pair",
        "description": "Exchanges the emailed token for a **pair** of API keys, returned ONCE.\n\n- `apiKey` — `pk_live_…`, plan=`free`, prepaid `credit_ore=1000` (10 SEK starting balance). Hits whatever environment the server is configured for.\n- `testApiKey` — `pk_test_…`, plan=`test`, `credit_ore=null` (unlimited).\n\nBoth keys must be persisted on the client — only their sha256 hashes are stored server-side. Re-clicking the same link returns 409 (cannot recover; use `/v1/login` to rotate instead).",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "example": "vt_…"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK — keys returned once",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerifyResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "description": "Invalid token"
          },
          "409": {
            "description": "Token already redeemed — keys cannot be re-shown. Use /v1/login to rotate."
          },
          "410": {
            "description": "Token expired (24h TTL)"
          }
        }
      }
    },
    "/v1/login": {
      "post": {
        "tags": [
          "account"
        ],
        "summary": "Request a sign-in (key-rotation) email",
        "description": "Sends a 30-minute magic link to the email tied to an active key. Always returns 202 so callers cannot probe account existence. Redeeming the link via `POST /v1/login/verify` revokes the holder's existing keys and mints a new pair while preserving `customer_id`, `plan`, `scopes`, and `credit_ore`.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted (returned regardless of whether the email has an account)"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/v1/login/verify": {
      "post": {
        "tags": [
          "account"
        ],
        "summary": "Redeem login token, rotate key pair",
        "description": "Atomically:\n1. Marks the signup row verified (gate against concurrent claims).\n2. Sets all currently-active keys for the email to `status='revoked'`.\n3. Mints a new `pk_live_…` (carrying the previous plan + remaining credit) and a new `pk_test_…` (always unlimited).\n\nReturned once; both keys must be saved client-side.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "example": "lt_…"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rotated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerifyResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "description": "Invalid token or no active key for this email"
          },
          "409": {
            "description": "Already used"
          },
          "410": {
            "description": "Token expired (30 min TTL)"
          }
        }
      }
    },
    "/v1/me": {
      "get": {
        "tags": [
          "account"
        ],
        "summary": "Bearer introspection",
        "description": "Holder-side view of the current key: `customer_id`, `plan`, `status`, `email`, current prepaid credit (`ore`/`sek`/`unlimited`), today's request counts grouped by endpoint, and the most recent N requests (default 20, max 100 via `?limit=`). No admin key needed.\n\nReads from local Postgres only — no external traffic, no cost.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            },
            "description": "How many recent requests to return"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MeResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Key vanished server-side (race after revoke)"
          }
        }
      }
    },
    "/v1/people/lookup": {
      "post": {
        "tags": [
          "people"
        ],
        "summary": "Look up a person (PNR, name, address, or PNR+name)",
        "description": "Polymorphic person lookup.\n\n- `{ pnr }` → **register + mrkoll only** (⚠️ SPAR is disabled). Resolves from `public.people` (exact PNR) then a free mrkoll scrape; returns the unified person or `person: null`. `?source=register|mrkoll` pins one step. **SPAR is never called** (`?source=spar|all` return `person: null` + `spar_disabled`).\n- `{ name }` / `{ address }` → Hitta register search (free).\n- `{ pnr, name }` → **register-only** match on birthday + name, returning the registered (folkbokförings-)address. **Never calls SPAR.** `pnr` may be a bare birthdate (e.g. `990513`).\n\n**`{pnr, name}` returns history.** Each returned person carries the full register **`address_history`** (folkbokförings-/registration history: an array of past addresses, newest first, each with street/number/zipcode/postort, a GeoJSON `loc` Point, and `date` = registration date in epoch ms), the structured current `addresses`, and `pnr`/`full_name`/`ssn_valid` for identity confirmation — all from the register snapshot, never SPAR.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PersonLookupRequest"
              },
              "examples": {
                "pnr-and-name-register": {
                  "summary": "PNR/birthdate + name → register folkbokföring (never SPAR)",
                  "value": {
                    "pnr": "990513",
                    "name": "philip rönnmark"
                  }
                },
                "rich": {
                  "summary": "PNR lookup, default rich shape",
                  "value": {
                    "pnr": "195704133106"
                  }
                },
                "flat-filtered": {
                  "summary": "PNR lookup, flat shape with field filter",
                  "value": {
                    "pnr": "195704133106",
                    "shape": "flat",
                    "fields": [
                      "name1",
                      "name3",
                      "street",
                      "postalcode",
                      "city"
                    ]
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "nocache",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            },
            "description": "Bypass the person cache for this lookup"
          }
        ],
        "responses": {
          "200": {
            "description": "OK. Shape depends on the lookup variant: a single registry person (`{pnr}`) or a register match list (`{name}` / `{address}` / `{pnr,name}`).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/PersonLookupResponse"
                    },
                    {
                      "$ref": "#/components/schemas/PersonRegisterMatchResponse"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "credit_exhausted (use POST /v1/billing/checkout to top up)"
          },
          "429": {
            "description": "Daily cap reached"
          },
          "502": {
            "description": "Registry error"
          }
        }
      }
    },
    "/v1/people/search": {
      "post": {
        "tags": [
          "people"
        ],
        "summary": "Search persons by name + address",
        "description": "**Always calls the registry.** Returns a `searchId` plus result count; the actual rows are stored locally with a 24h TTL. Use `GET /v1/people/search/{searchId}` to retrieve them (no extra call, free). Each hit is also written to the person cache → subsequent `/v1/people/lookup` calls on the same PNRs are free for 24h.\n\nBilled flat at the name-search price regardless of hit count. Pure-address searches are rejected — at least one of `namn`, `fornamn`, `mellanEfternamn` is required (wildcard `A*` allowed).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PersonSearchRequest"
              },
              "examples": {
                "name-and-city": {
                  "value": {
                    "fornamn": "Anna",
                    "mellanEfternamn": "Andersson",
                    "postort": "Stockholm"
                  }
                },
                "wildcard": {
                  "value": {
                    "namn": "A*",
                    "kommunKod": "0180"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PersonSearchResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "402": {
            "description": "credit_exhausted (use POST /v1/billing/checkout to top up)"
          }
        }
      }
    },
    "/v1/people/search/{searchId}": {
      "get": {
        "tags": [
          "people"
        ],
        "summary": "Retrieve cached search results",
        "description": "**Served from the local cache.** Replays the hits stored by the originating `POST /v1/people/search`. Free; no marginal cost. 24h TTL on the cache row.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "searchId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "shape",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "rich",
                "flat"
              ],
              "default": "rich"
            }
          },
          {
            "name": "fields",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated subset of fields (flat shape only)"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "404": {
            "description": "searchId unknown or expired"
          }
        }
      }
    },
    "/v1/properties/{id}": {
      "get": {
        "tags": [
          "properties"
        ],
        "summary": "Property by UUID",
        "description": "Local read from the property dataset.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Property + parcels (with WGS84 GeoJSON polygons)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyWithParcels"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "402": {
            "description": "credit_exhausted"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/properties/by-beteckning/{beteckning}": {
      "get": {
        "tags": [
          "properties"
        ],
        "summary": "Property by fastighetsbeteckning",
        "description": "URL-encode the beteckning (e.g. `Burl%C3%B6v%2010%3A1` for `Burlöv 10:1`). Local read. Same response shape as `/v1/properties/{id}` — includes parcel polygons inline.\n\nPass `?kommun=se:locality:…` to disambiguate when the same beteckning exists in multiple kommuner — without it, ambiguous beteckningar return 409 with a candidate list.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "beteckning",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "Burl%C3%B6v%2010%3A1"
          },
          {
            "name": "kommun",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Disambiguate when the same beteckning exists in multiple kommuner. Polymorphic — accepts name (`Stockholm`, `Göteborg`), slug (`stockholm`, `goteborg`), SCB code (`0180`), Pelias id (`se:locality:stockholm`), or accent-stripped form. Without this, ambiguous beteckningar return 409 with a candidate list."
          }
        ],
        "responses": {
          "200": {
            "description": "Property + parcels",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyWithParcels"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "402": {
            "description": "credit_exhausted (use POST /v1/billing/checkout to top up)"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Ambiguous beteckning — the same name exists in multiple kommuner. Retry with `?kommun=`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "ambiguous_beteckning"
                    },
                    "beteckning": {
                      "type": "string"
                    },
                    "candidates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "beteckning": {
                            "type": "string"
                          },
                          "kommun_namn": {
                            "type": "string",
                            "nullable": true
                          },
                          "kommun_kod": {
                            "type": "string",
                            "nullable": true
                          },
                          "municipality_admin_id": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/properties/search": {
      "get": {
        "tags": [
          "properties"
        ],
        "summary": "Search properties by free text",
        "description": "Trigram similarity search across fastighetsbeteckning. Returns up to `limit` hits ordered by score. At least one of `q` (free-text) or `kommun` (scope) must be provided — passing `kommun` alone lists fastigheter alphabetically inside that municipality, which powers the drill-down navigator on `/maps`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 2
            },
            "example": "Burlöv 10",
            "description": "Free-text query. Required unless `kommun` is given."
          },
          {
            "name": "kommun",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "example": "Stockholm",
            "description": "Narrows results to one kommun. Polymorphic: accepts name (`Stockholm`), slug (`stockholm`), SCB code (`0180`), Pelias id (`se:locality:stockholm`), or accent-stripped form. Required unless `q` is given."
          },
          {
            "name": "prefix",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "example": true,
            "description": "Autocomplete mode. When `true`, `q` is matched as an anchored, case-insensitive prefix of the beteckning (fast index-backed type-ahead) instead of the default trigram fuzzy match. Requires `q`. The response `mode` field reports which path served the result."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Hits",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "query": {
                      "type": "string"
                    },
                    "mode": {
                      "type": "string",
                      "enum": [
                        "prefix",
                        "fuzzy",
                        "browse"
                      ],
                      "description": "Which matcher served the result: `prefix` (anchored type-ahead), `fuzzy` (trigram), or `browse` (kommun listing, no `q`)."
                    },
                    "count": {
                      "type": "integer"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "402": {
            "description": "credit_exhausted (use POST /v1/billing/checkout to top up)"
          }
        }
      }
    },
    "/v1/properties/reverse": {
      "get": {
        "tags": [
          "properties"
        ],
        "summary": "Property containing a coordinate",
        "description": "PostGIS `ST_Covers` against parcel geometries (GEOGRAPHY 4326; WGS84 input). Returns the smallest property covering the point, plus its parcel polygons. `property` is `null` when the point falls outside any registered fastighet.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "format": "float"
            },
            "example": 59.3326
          },
          {
            "name": "lng",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "format": "float"
            },
            "example": 18.0649
          }
        ],
        "responses": {
          "200": {
            "description": "Property + parcels, or `{property: null}`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyWithParcels"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or out-of-range coordinates"
          },
          "402": {
            "description": "credit_exhausted (use POST /v1/billing/checkout to top up)"
          }
        }
      }
    },
    "/v1/properties/{id}/buildings": {
      "get": {
        "tags": [
          "properties"
        ],
        "summary": "Buildings on a property",
        "description": "Each building carries an `addresses[]` array (folded in via JSON aggregation), so callers don't need to follow up with `GET /v1/buildings/{id}` just to read street text. Sheds/garages typically have `has_address: false` and an empty array.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Buildings list with inline addresses"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "402": {
            "description": "credit_exhausted (use POST /v1/billing/checkout to top up)"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/properties/{id}/registered": {
      "get": {
        "tags": [
          "properties"
        ],
        "summary": "People and companies registered across the whole fastighet",
        "description": "One-call fan-out: property → every building on it → every linked belägenhetsadress → curated person + company snapshots, with deduplication across the entire property. The fastighet-level equivalent of `/v1/buildings/{id}/registered`. People rows in the snapshot carry no entrance letter, so multi-entrance buildings collapse their stairs to a single per-person row by id; companies are still narrowed to the entrance via `address_entrance` where present. Use this instead of looping `/v1/buildings/{id}/registered` per building yourself — one credit-debit, one network round-trip, one dedup pass.\n\nFor authoritative, real-time civil-registry residents use `/v1/people/search` (registry-backed, billed). The registry has no \"list everyone on a fastighet\" mode by design — sekretess constraints — so this endpoint is the closest available approximation.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "include",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^(people|companies)(,(people|companies))*$"
            },
            "description": "Comma-separated subset; defaults to both.",
            "example": "people,companies"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 200,
              "minimum": 1,
              "maximum": 500
            },
            "description": "Cap applied AFTER deduplication across all buildings + entrances on the fastighet."
          }
        ],
        "responses": {
          "200": {
            "description": "People + companies registered on this fastighet, with the building/address fan-out reported in `match`"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "402": {
            "description": "credit_exhausted (use POST /v1/billing/checkout to top up)"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/buildings/{id}/registered": {
      "get": {
        "tags": [
          "buildings"
        ],
        "summary": "People and companies registered in this building",
        "description": "Fans out the building's linked addresses (via `building_addresses`) and joins each to the curated person + company snapshots — the byggnad-level equivalent of `/v1/addresses/{id}/registered`. No external call. People rows in the snapshot carry no entrance letter, so a building with multiple staircases (`13A` + `13B`) returns the same resident only once — rows are deduplicated by id across all linked entrances. Companies are still narrowed to the entrance via their `address_entrance` field where present.\n\nResponse includes the linked `addresses[]` and a `match.address_count` so the caller knows the fan-out width. For authoritative, real-time civil-registry residents use `/v1/people/search` (registry-backed, billed) — the registry has no \"list everyone in a building\" mode by design, so this endpoint is the closest available approximation.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "include",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^(people|companies)(,(people|companies))*$"
            },
            "description": "Comma-separated subset; defaults to both.",
            "example": "people,companies"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 200,
              "minimum": 1,
              "maximum": 500
            },
            "description": "Cap applied AFTER deduplication across all linked addresses."
          }
        ],
        "responses": {
          "200": {
            "description": "People + companies registered in this building"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "402": {
            "description": "credit_exhausted (use POST /v1/billing/checkout to top up)"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/buildings/{id}": {
      "get": {
        "tags": [
          "buildings"
        ],
        "summary": "Building by UUID",
        "description": "Returns `{ ok, building }`. Location addresses are embedded on `building.addresses[]` — there is no sibling `addresses` field on the response.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Building with embedded addresses",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "building": {
                      "$ref": "#/components/schemas/Building"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "402": {
            "description": "credit_exhausted (use POST /v1/billing/checkout to top up)"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/buildings": {
      "get": {
        "tags": [
          "buildings"
        ],
        "summary": "Buildings inside a bounding box",
        "description": "Bbox area capped at 10 sq.deg to refuse country-scale extraction. WGS84 (`west,south,east,north`).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "bbox",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "18.05,59.32,18.10,59.35"
          },
          {
            "name": "purpose",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Purpose code filter"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 100,
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Buildings list"
          },
          "400": {
            "description": "Bad bbox (malformed, inverted, or > 10 sq.deg)"
          },
          "402": {
            "description": "credit_exhausted (use POST /v1/billing/checkout to top up)"
          }
        }
      }
    },
    "/v1/addresses/search": {
      "get": {
        "tags": [
          "addresses"
        ],
        "summary": "Search location addresses by free text",
        "description": "Typeahead-friendly trigram search. Optional kommun_kod filter for narrowing.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 2
            },
            "example": "Sveavägen"
          },
          {
            "name": "kommun",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "example": "Stockholm",
            "description": "Polymorphic kommun scope. Accepts name, slug, SCB code, Pelias id, or accent-stripped form."
          },
          {
            "name": "kommun_kod",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "example": "0180",
            "description": "**Deprecated alias** for `?kommun=`. Accepted as a back-compat for one release; prefer `?kommun=` going forward."
          },
          {
            "name": "prefix",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "example": true,
            "description": "Autocomplete mode. When `true`, `q` is matched as an anchored, case-insensitive prefix of the street address (fast index-backed type-ahead) instead of the default trigram fuzzy match. The response `mode` field reports `prefix` or `fuzzy`."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Hits"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "402": {
            "description": "credit_exhausted (use POST /v1/billing/checkout to top up)"
          }
        }
      }
    },
    "/v1/addresses/{id}": {
      "get": {
        "tags": [
          "addresses"
        ],
        "summary": "Address by UUID",
        "description": "Returns the address plus every building that lists it as an entry (`entre[]` in the source data). Most addresses point at a single building; urban blocks can share several.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Address + buildings"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "402": {
            "description": "credit_exhausted (use POST /v1/billing/checkout to top up)"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/companies/search": {
      "get": {
        "tags": [
          "companies"
        ],
        "summary": "Search companies by name",
        "description": "Trigram similarity match on `display_name`. Optional `?kommun=` filter accepts any of the five polymorphic forms (name, slug, SCB code, Pelias id, accent-stripped). Results carry `review_count` + `review_score` alongside the registry fields.\n\nResponse shape: `{ ok, query, kommun?, count, results: [...] }`. Each result includes the company's address tuple, contact fields (email/website), and extras.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 2
            },
            "example": "volvo"
          },
          {
            "name": "kommun",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "example": "Göteborg"
          },
          {
            "name": "prefix",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "example": true,
            "description": "Autocomplete mode. When `true`, `q` is matched as an anchored, case-insensitive prefix of the company name (fast index-backed type-ahead) instead of the default trigram fuzzy match. The response `mode` field reports `prefix` or `fuzzy`."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Hits"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "402": {
            "description": "credit_exhausted"
          }
        }
      }
    },
    "/v1/companies/by-orgno/{orgno}": {
      "get": {
        "tags": [
          "companies"
        ],
        "summary": "Company by Swedish org number",
        "description": "Canonical B2B lookup. Accepts both 10-digit (`5566778899`) and 12-digit (`165568596174`) orgnos, with or without hyphens — stripped before validation.\n\nEach office of a company is a separate row, so the response surfaces the entity at top level plus every known office in the `offices[]` array:\n```\n{\n  \"ok\": true,\n  \"orgno\": \"5566778899\",\n  \"display_name\": \"…\",\n  \"legal_name\": \"…\",\n  \"legal_form_name\": \"Aktiebolag\",\n  \"count\": 12,\n  \"offices\": [{ id, address_*, phones, review_count, … }, ... up to 50]\n}\n```",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "orgno",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9-]{10,14}$"
            },
            "example": "5566778899"
          }
        ],
        "responses": {
          "200": {
            "description": "Company with offices"
          },
          "400": {
            "description": "Invalid orgno format (must be 10 or 12 digits after stripping)"
          },
          "402": {
            "description": "credit_exhausted"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/companies/by-orgno/{orgno}/listings": {
      "get": {
        "tags": [
          "companies"
        ],
        "summary": "The company's rental listings",
        "description": "Every rental listing Mantal can attribute to this company as **landlord**. There is no orgno on the listings themselves — the landlord is inferred from the listing's platform host (`minasidor.obo.se` → domain `obo.se` → the company whose registered domain is `obo.se`). Resolves the municipal-housing landlords (momentum/*); marketplace aggregators (Homii, HomeQ, Boplats) don't attribute to a single company by design.\n\nAccepts the same filters as `/v1/listings` (`status`, `rent_min`, `rent_max`, `rooms_min`, `sqm_min`, `property_type`, `furnished`, `firsthand`, `student_home`) plus `limit`/`offset`. 88% of rows are `rented`/`removed` (the archive is the product); pass `?status=active` for live inventory.\n\nResponse: `{ ok, orgno, landlord: { orgno, company_name, domains[], platforms[], total, active, rented }, count, total, limit, offset, listings[] }`. A company with no attributable platform returns 200 with an empty `listings[]` and `landlord.platforms: []`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "orgno",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9-]{10,14}$"
            },
            "example": "5563348449"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "rented",
                "removed"
              ]
            }
          },
          {
            "name": "rent_min",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "rent_max",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "rooms_min",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "sqm_min",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "property_type",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "furnished",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "firsthand",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "student_home",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Landlord summary + a page of the company's listings"
          },
          "400": {
            "description": "Invalid orgno or query"
          },
          "402": {
            "description": "credit_exhausted"
          }
        }
      }
    },
    "/v1/companies/{id}": {
      "get": {
        "tags": [
          "companies"
        ],
        "summary": "Company (office row) by listing id",
        "description": "Returns a single directory listing — one office of a company. To get the legal entity with all its offices, use `/v1/companies/by-orgno/{orgno}` instead. Customer code typically reaches an id via `/v1/addresses/{id}/registered` (cross-dataset join) or `/v1/companies/search` (name search).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "notfccmt"
          }
        ],
        "responses": {
          "200": {
            "description": "Company office"
          },
          "400": {
            "description": "Invalid id (use /v1/companies/by-orgno for orgno lookup)"
          },
          "402": {
            "description": "credit_exhausted"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/lantmateriet/registerbeteckning/sok": {
      "get": {
        "tags": [
          "lm-registerbeteckning"
        ],
        "summary": "Sök registerbeteckning",
        "description": "Trigram search over fastighetsbeteckning. Returns an **array** of autocomplete stubs (no `{ ok, results }` envelope). At least one of `q` or `kommun` is required.\n\nResponse shape per row:\n```\n{ \"id\": \"<uuid>\", \"typ\": \"REGISTERENHET\", \"subtext\": \"<kommun>\", \"position\": { \"east\": <SWEREF>, \"north\": <SWEREF> }, \"headertext\": \"<beteckning>\" }\n```",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string",
              "minLength": 2
            },
            "description": "Free-text query. Required unless `kommun` is given."
          },
          {
            "name": "kommun",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "example": "Stockholm",
            "description": "Polymorphic kommun scope. Accepts name (`Stockholm`), slug (`stockholm`), SCB code (`0180`), Pelias id (`se:locality:stockholm`), or accent-stripped form. Required unless `q` is given."
          },
          {
            "name": "prefix",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "example": true,
            "description": "Autocomplete mode. When `true`, `q` is matched as an anchored, case-insensitive prefix of the beteckning (fast index-backed type-ahead) instead of the default trigram fuzzy match. Requires `q`."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Stubs array",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/v1/lantmateriet/registerbeteckning/{id}": {
      "get": {
        "tags": [
          "lm-registerbeteckning"
        ],
        "summary": "Registerbeteckning by id",
        "description": "Returns the minimal beteckning stub for a single UUID. For full property data use `/v1/lantmateriet/fastighet/{id}`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Stub"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/lantmateriet/fastighet/{id}": {
      "get": {
        "tags": [
          "lm-fastighet"
        ],
        "summary": "Fastighet by id (native shape)",
        "description": "Full native property object with all markområden inline. **No `{ ok }` envelope** — the response body is the source JSON verbatim:\n```\n{\n  \"typ\": \"fastighet\",\n  \"objektstatus\": \"levande\",\n  \"objektidentitet\": \"<uuid>\",\n  \"enhetsutbredning\": [\n    { \"typ\": \"markområde\", \"omradesnummer\": 1, \"yta\": { \"type\": \"Polygon\", \"coordinates\": [...] }, \"centralpunkt\": { \"type\": \"Point\", \"coordinates\": [..., ...] }, \"objektidentitet\": \"<uuid>\" }\n  ]\n}\n```\nCoordinates: **SWEREF 99 TM (EPSG:3006)**. For WGS84 + opinionated envelope use `/v1/properties/{id}`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Fastighet (native shape)"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/lantmateriet/fastighet/beteckning/{beteckning}": {
      "get": {
        "tags": [
          "lm-fastighet"
        ],
        "summary": "Fastighet by beteckning (native shape)",
        "description": "URL-encode the beteckning (e.g. `Burl%C3%B6v%2010%3A1` for `Burlöv 10:1`). Same body shape as `/v1/lantmateriet/fastighet/{id}`. Use `?kommun=` to disambiguate when the same beteckning exists in multiple kommuner — without it, ambiguous beteckningar return 409 with a candidate list.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "beteckning",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "Burl%C3%B6v%2010%3A1"
          },
          {
            "name": "kommun",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "example": "Stockholm",
            "description": "Polymorphic kommun disambiguation. Accepts name, slug, SCB code, Pelias id, or accent-stripped form. Without this, ambiguous beteckningar return 409 with a candidate list."
          }
        ],
        "responses": {
          "200": {
            "description": "Fastighet"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Ambiguous beteckning across multiple kommuner — retry with `?kommun=`."
          }
        }
      }
    },
    "/v1/lantmateriet/fastighet/reverse": {
      "get": {
        "tags": [
          "lm-fastighet"
        ],
        "summary": "Reverse-geocode lat/lng → fastighet (native shape)",
        "description": "PostGIS `ST_Covers` against parcel polygons. Returns the native Fastighet object if the point falls inside a registered fastighet; `null` otherwise.\n\nNote: `lat`/`lng` input are **WGS84** (the rest of the world's convention) — the response is **SWEREF 99 TM**.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "format": "float"
            }
          },
          {
            "name": "lng",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "format": "float"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Fastighet object, or `null` when no match"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/v1/lantmateriet/fastighet/{id}/byggnad": {
      "get": {
        "tags": [
          "lm-fastighet"
        ],
        "summary": "Buildings on a fastighet (GeoJSON FeatureCollection)",
        "description": "Returns a GeoJSON `FeatureCollection` of every Byggnad attached to the fastighet. Each feature follows the native building shape — see `/v1/lantmateriet/byggnad/{id}`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "FeatureCollection of Byggnad Features"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/lantmateriet/byggnad/{id}": {
      "get": {
        "tags": [
          "lm-byggnad"
        ],
        "summary": "Byggnad by id (GeoJSON Feature, native shape)",
        "description": "Returns the native Byggnad GeoJSON Feature verbatim. Top-level `geometry` is intentionally `null` — the actual building footprint sits under `properties.byggnadsdel[].byggnadsyta.yta` . Coordinates are SWEREF 99 TM.\n\nResponse shape (abbreviated):\n```\n{\n \"type\": \"Feature\",\n \"id\": \"<uuid>\",\n \"bbox\": [..., ..., ..., ...],\n \"geometry\": null,\n \"properties\": {\n  \"objektidentitet\": \"<uuid>\",\n  \"objektstatus\": \"Gällande\",\n  \"byggnadsattribut\": { \"husnummer\": …, \"huvudbyggnad\": true, \"versionGiltigFran\": \"…\" },\n  \"byggnadsandamal\": [{ \"typ\": \"Bostad\", \"andamal\": \"Småhus med en lägenhet\", \"huvudandamal\": true }],\n  \"byggnadsdel\": [{ \"byggnadsyta\": { \"koordinatsystemPlan\": \"SWEREF 99 TM\", \"yta\": { \"type\": \"MultiPolygon\", \"coordinates\": [...] } } }],\n  \"liggerPa\": { \"registerenhetsreferens\": { \"beteckning\": \"…\", \"objektidentitet\": \"<uuid>\", \"typ\": \"Fastighet\" } }\n }\n}\n```",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Byggnad GeoJSON Feature"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/lantmateriet/byggnad": {
      "get": {
        "tags": [
          "lm-byggnad"
        ],
        "summary": "Buildings inside a bounding box (FeatureCollection)",
        "description": "Bbox is **WGS84** (`west,south,east,north`) — convention of every map library. Response Features carry SWEREF 99 TM coordinates inside. Bbox area capped at 10 sq.deg to refuse country-scale extraction.\n\nFilter params use the source's naming: `andamal` (purpose code).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "bbox",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "18.05,59.32,18.10,59.35"
          },
          {
            "name": "bbox_crs",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "WGS84",
                "SWEREF99TM"
              ],
              "default": "WGS84"
            },
            "description": "Coordinate system the `bbox` is expressed in. `WGS84` (default) matches every map library. `SWEREF99TM` (EPSG:3006) — pass this if you already have SWEREF coords. Response Features always carry SWEREF coords inside."
          },
          {
            "name": "andamal",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Purpose code filter"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 100,
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "GeoJSON FeatureCollection"
          },
          "400": {
            "description": "Bad bbox (malformed, inverted, or too large for the chosen CRS — WGS84 ≤ 10 sq.deg., SWEREF99TM ≤ 100 km²)"
          },
          "402": {
            "description": "credit_exhausted"
          }
        }
      }
    },
    "/v1/lantmateriet/belagenhetsadress/sok": {
      "get": {
        "tags": [
          "lm-belagenhetsadress"
        ],
        "summary": "Sök belägenhetsadress",
        "description": "Trigram search over location addresses. Returns an array of autocomplete stubs.\n\nNote: this is **not** the full location-address object (kvarter, lägenhetsbeteckning, fastighetsreferens). That is not yet enabled.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 2
            }
          },
          {
            "name": "kommun",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "example": "Stockholm",
            "description": "Polymorphic kommun scope. Accepts name, slug, SCB code, Pelias id, or accent-stripped form."
          },
          {
            "name": "kommun_kod",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "example": "0180",
            "description": "SCB code (the source's native param name). Accepted in parallel with `?kommun=` for drop-in compatibility."
          },
          {
            "name": "prefix",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "example": true,
            "description": "Autocomplete mode. When `true`, `q` is matched as an anchored, case-insensitive prefix of the street address (fast index-backed type-ahead) instead of the default trigram fuzzy match."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Stubs array"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/v1/lantmateriet/belagenhetsadress/{id}": {
      "get": {
        "tags": [
          "lm-belagenhetsadress"
        ],
        "summary": "Belägenhetsadress by id (stub — `_partial: true`)",
        "description": "Returns the autocomplete-stub shape plus three honesty fields so customer code can branch on them:\n```\n{\n  \"id\": \"<uuid>\",\n  \"typ\": \"ADRESS\",\n  \"subtext\": \"<kommun>\",\n  \"position\": { \"east\": <SWEREF>, \"north\": <SWEREF> },\n  \"headertext\": \"<address>\",\n  \"_partial\": true,\n  \"_missing\": [\"kvarter\", \"lagenhetsbeteckning\", \"fastighetsreferens\", ...],\n  \"_note\": \"Full location-address object not yet enabled — use /v1/addresses/{id} for the curated equivalent.\"\n}\n```\nThe full location-address response (kvarter, lägenhetsbeteckning, fastighetsreferens, etc.) is not yet enabled. When that lands, `_partial` flips to `false` and `_missing` empties.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Stub with `_partial: true`"
          },
          "402": {
            "description": "credit_exhausted"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/addresses/{id}/registered": {
      "get": {
        "tags": [
          "addresses"
        ],
        "summary": "People and companies registered at this address",
        "description": "Local lookup against the curated person and company snapshots (no external call, free). The `street_text` is split into street + number (+ optional entrance letter) and matched against the snapshot tables in the same kommun.\n\nReturns `{ people, companies }` — pass `?include=people` or `?include=companies` to fetch only one.\n\n**Match precision** is reported on every response:\n- `exact` — the address has no entrance letter (e.g. `Storgatan 12`), so each row's `address_street + address_number` is an exact hit.\n- `building` — the address carries an entrance letter (e.g. `Odengatan 13C`). The person snapshot doesn't record entrance letters, so all residents on building `13` come back together; companies are still narrowed to the entrance via their `address_entrance` field.\n\nFor authoritative, real-time civil-registry residents see `/v1/people/search` (registry-backed, billed).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "include",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^(people|companies)(,(people|companies))*$"
            },
            "description": "Comma-separated subset; defaults to both.",
            "example": "people,companies"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 200,
              "minimum": 1,
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "People + companies registered at this address"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "402": {
            "description": "credit_exhausted (use POST /v1/billing/checkout to top up)"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/geo/admin/{id}": {
      "get": {
        "tags": [
          "universal-geo"
        ],
        "summary": "Get any admin entity by id",
        "description": "Universal lookup — same `id` works for countries / län / kommun / stadsdel / neighbourhood. Returns the entity plus its full Pelias context (country → region → county → locality → borough → neighbourhood). For typed/per-level access see the corresponding hierarchy tag.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "examples": {
              "country": {
                "value": "se"
              },
              "region": {
                "value": "se:region:stockholms-lan"
              },
              "locality": {
                "value": "se:locality:stockholm"
              },
              "borough": {
                "value": "se:borough:stockholm:sodermalm"
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Admin area with context",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdminAreaWithContext"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/geo/admin/{id}/polygon": {
      "get": {
        "tags": [
          "universal-geo"
        ],
        "summary": "Admin area polygon (GeoJSON, any level)",
        "description": "GeoJSON `Feature` — geometry generated live from PostGIS `geom` (authoritative, not the cached `polygon_json` text). `properties` carries `id, slug, name, admin_level, level_label, center, boundingbox`. Pass `?simplify=<degrees>` (Douglas–Peucker, ~0.001 ≈ 100 m, clamped ≤ 0.1) to shrink geometry for a wide-area map without a second call.\n\nA row with no polygon returns the Feature with `geometry: null` (use `center`/`boundingbox`).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "se:locality:stockholm"
          },
          {
            "name": "simplify",
            "in": "query",
            "schema": {
              "type": "number",
              "maximum": 0.1
            },
            "example": 0.001,
            "description": "Douglas–Peucker tolerance in degrees. Omit for full resolution."
          }
        ],
        "responses": {
          "200": {
            "description": "GeoJSON Feature"
          },
          "402": {
            "description": "credit_exhausted"
          },
          "404": {
            "description": "not_found"
          }
        }
      }
    },
    "/v1/geo/polygons": {
      "get": {
        "tags": [
          "universal-geo"
        ],
        "summary": "Admin layer as GeoJSON FeatureCollection",
        "description": "A whole administrative layer in ONE call — draw every kommun (`country=SE&layer=locality`) or every kommun in a län (`parent=se:region:…`) without N polygon requests. Returns `{ type:'FeatureCollection', count, features:[Feature] }`; each Feature's properties carry `id, slug, name, admin_level, level_label, center`. Requires `layer` or `parent` (never dumps the whole table). `?simplify=<degrees>` is strongly recommended here (all 290 kommuner ≈ 275 KB at 0.01). Free. Capped at 400 features.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "example": "SE"
          },
          {
            "name": "layer",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "country",
                "region",
                "county",
                "locality",
                "borough",
                "neighbourhood"
              ]
            },
            "example": "locality",
            "description": "Pass this OR parent."
          },
          {
            "name": "parent",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "example": "se:region:stockholms-lan",
            "description": "Admin id; returns its direct children. Pass this OR layer."
          },
          {
            "name": "simplify",
            "in": "query",
            "schema": {
              "type": "number",
              "maximum": 0.1
            },
            "example": 0.01
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "maximum": 400
            }
          }
        ],
        "responses": {
          "200": {
            "description": "GeoJSON FeatureCollection"
          },
          "400": {
            "description": "filter_required (no layer/parent) or invalid_layer"
          },
          "402": {
            "description": "credit_exhausted"
          }
        }
      }
    },
    "/v1/geo/admin/{id}/children": {
      "get": {
        "tags": [
          "universal-geo"
        ],
        "summary": "Direct children one layer below",
        "description": "Walks one step down the hierarchy: country → regions, region → localities (SE skips county), locality → boroughs, etc.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Children list"
          }
        }
      }
    },
    "/v1/geo/admin/{id}/parents": {
      "get": {
        "tags": [
          "universal-geo"
        ],
        "summary": "Ancestor chain (root → self exclusive)",
        "description": "Returns every parent up to and excluding the entity itself, root first. E.g. for `se:borough:stockholm:sodermalm` → `[country, region, locality]`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ancestors ordered root → leaf"
          }
        }
      }
    },
    "/v1/geo/admin": {
      "get": {
        "tags": [
          "universal-geo"
        ],
        "summary": "List admin entities with filters",
        "description": "Filtered list across the entire hierarchy. Pick a `layer` (country / region / county / locality / borough / neighbourhood) to scope to one level, or filter by `parent` id to walk arbitrarily.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 2
            }
          },
          {
            "name": "layer",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "country",
                "region",
                "county",
                "locality",
                "borough",
                "neighbourhood"
              ]
            }
          },
          {
            "name": "parent",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Filtered admin areas"
          }
        }
      }
    },
    "/v1/geo/countries": {
      "get": {
        "tags": [
          "countries"
        ],
        "summary": "List countries served",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "enabled",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Country list"
          }
        }
      }
    },
    "/v1/geo/countries/{code}": {
      "get": {
        "tags": [
          "countries"
        ],
        "summary": "Country metadata",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 2
            },
            "example": "SE"
          }
        ],
        "responses": {
          "200": {
            "description": "Country"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/geo/regions": {
      "get": {
        "tags": [
          "regions"
        ],
        "summary": "List regions (län / Bundesland / Région)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Country"
          }
        ],
        "responses": {
          "200": {
            "description": "Region list"
          }
        }
      }
    },
    "/v1/geo/regions/{slug}": {
      "get": {
        "tags": [
          "regions"
        ],
        "summary": "Region by slug",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "stockholms-lan"
          },
          {
            "$ref": "#/components/parameters/Country"
          }
        ],
        "responses": {
          "200": {
            "description": "Region + context"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/geo/regions/{slug}/polygon": {
      "get": {
        "tags": [
          "regions"
        ],
        "summary": "Region (län) polygon (GeoJSON)",
        "description": "GeoJSON `Feature` — geometry generated live from PostGIS `geom` (authoritative, not the cached `polygon_json` text). `properties` carries `id, slug, name, admin_level, level_label, center, boundingbox`. Pass `?simplify=<degrees>` (Douglas–Peucker, ~0.001 ≈ 100 m, clamped ≤ 0.1) to shrink geometry for a wide-area map without a second call.\n\nA row with no polygon returns the Feature with `geometry: null` (use `center`/`boundingbox`).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "stockholms-lan"
          },
          {
            "name": "simplify",
            "in": "query",
            "schema": {
              "type": "number",
              "maximum": 0.1
            },
            "example": 0.001,
            "description": "Douglas–Peucker tolerance in degrees. Omit for full resolution."
          }
        ],
        "responses": {
          "200": {
            "description": "GeoJSON Feature"
          },
          "402": {
            "description": "credit_exhausted"
          },
          "404": {
            "description": "not_found"
          }
        }
      }
    },
    "/v1/geo/counties": {
      "get": {
        "tags": [
          "counties"
        ],
        "summary": "List counties (Landkreis / Département) — none in SE",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Country"
          }
        ],
        "responses": {
          "200": {
            "description": "County list"
          }
        }
      }
    },
    "/v1/geo/counties/{slug}": {
      "get": {
        "tags": [
          "counties"
        ],
        "summary": "County by slug",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Country"
          }
        ],
        "responses": {
          "200": {
            "description": "County"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/geo/localities": {
      "get": {
        "tags": [
          "localities"
        ],
        "summary": "List localities (kommun / Gemeinde / Commune)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Country"
          }
        ],
        "responses": {
          "200": {
            "description": "Locality list"
          }
        }
      }
    },
    "/v1/geo/localities/{slug}": {
      "get": {
        "tags": [
          "localities"
        ],
        "summary": "Locality by slug",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "stockholm"
          },
          {
            "$ref": "#/components/parameters/Country"
          }
        ],
        "responses": {
          "200": {
            "description": "Locality"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/geo/localities/{slug}/polygon": {
      "get": {
        "tags": [
          "localities"
        ],
        "summary": "Locality (kommun) polygon (GeoJSON)",
        "description": "GeoJSON `Feature` — geometry generated live from PostGIS `geom` (authoritative, not the cached `polygon_json` text). `properties` carries `id, slug, name, admin_level, level_label, center, boundingbox`. Pass `?simplify=<degrees>` (Douglas–Peucker, ~0.001 ≈ 100 m, clamped ≤ 0.1) to shrink geometry for a wide-area map without a second call.\n\nA row with no polygon returns the Feature with `geometry: null` (use `center`/`boundingbox`).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "stockholm"
          },
          {
            "name": "simplify",
            "in": "query",
            "schema": {
              "type": "number",
              "maximum": 0.1
            },
            "example": 0.001,
            "description": "Douglas–Peucker tolerance in degrees. Omit for full resolution."
          }
        ],
        "responses": {
          "200": {
            "description": "GeoJSON Feature"
          },
          "402": {
            "description": "credit_exhausted"
          },
          "404": {
            "description": "not_found"
          }
        }
      }
    },
    "/v1/geo/boroughs": {
      "get": {
        "tags": [
          "boroughs"
        ],
        "summary": "List boroughs (stadsdelsområde / Stadtbezirk)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Country"
          }
        ],
        "responses": {
          "200": {
            "description": "Borough list"
          }
        }
      }
    },
    "/v1/geo/boroughs/{slug}": {
      "get": {
        "tags": [
          "boroughs"
        ],
        "summary": "Borough by slug",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Country"
          }
        ],
        "responses": {
          "200": {
            "description": "Borough"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/geo/boroughs/{slug}/polygon": {
      "get": {
        "tags": [
          "boroughs"
        ],
        "summary": "Borough (stadsdel) polygon (GeoJSON)",
        "description": "GeoJSON `Feature` — geometry generated live from PostGIS `geom` (authoritative, not the cached `polygon_json` text). `properties` carries `id, slug, name, admin_level, level_label, center, boundingbox`. Pass `?simplify=<degrees>` (Douglas–Peucker, ~0.001 ≈ 100 m, clamped ≤ 0.1) to shrink geometry for a wide-area map without a second call.\n\nA row with no polygon returns the Feature with `geometry: null` (use `center`/`boundingbox`).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "stockholm:norra-innerstaden",
            "description": "Borough slugs are compound `kommunSlug:areaSlug` (the id is se:borough:kommun:area)."
          },
          {
            "name": "simplify",
            "in": "query",
            "schema": {
              "type": "number",
              "maximum": 0.1
            },
            "example": 0.001,
            "description": "Douglas–Peucker tolerance in degrees. Omit for full resolution."
          }
        ],
        "responses": {
          "200": {
            "description": "GeoJSON Feature"
          },
          "402": {
            "description": "credit_exhausted"
          },
          "404": {
            "description": "not_found"
          }
        }
      }
    },
    "/v1/geo/neighbourhoods": {
      "get": {
        "tags": [
          "neighbourhoods"
        ],
        "summary": "List neighbourhoods",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Neighbourhood list"
          }
        }
      }
    },
    "/v1/geo/neighbourhoods/{slug}": {
      "get": {
        "tags": [
          "neighbourhoods"
        ],
        "summary": "Neighbourhood by slug",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Neighbourhood"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/geo/neighbourhoods/{slug}/polygon": {
      "get": {
        "tags": [
          "neighbourhoods"
        ],
        "summary": "Neighbourhood polygon (GeoJSON)",
        "description": "GeoJSON `Feature` — geometry generated live from PostGIS `geom` (authoritative, not the cached `polygon_json` text). `properties` carries `id, slug, name, admin_level, level_label, center, boundingbox`. Pass `?simplify=<degrees>` (Douglas–Peucker, ~0.001 ≈ 100 m, clamped ≤ 0.1) to shrink geometry for a wide-area map without a second call.\n\nA row with no polygon returns the Feature with `geometry: null` (use `center`/`boundingbox`).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "stockholm:gamla-stan",
            "description": "Neighbourhood slugs are compound `kommunSlug:areaSlug`."
          },
          {
            "name": "simplify",
            "in": "query",
            "schema": {
              "type": "number",
              "maximum": 0.1
            },
            "example": 0.001,
            "description": "Douglas–Peucker tolerance in degrees. Omit for full resolution."
          }
        ],
        "responses": {
          "200": {
            "description": "GeoJSON Feature"
          },
          "402": {
            "description": "credit_exhausted"
          },
          "404": {
            "description": "not_found"
          }
        }
      }
    },
    "/v1/geo/kommun/{slug}": {
      "get": {
        "tags": [
          "localities"
        ],
        "summary": "Kommun skeleton + editorial content + live listing counts",
        "description": "A kommun's geo skeleton (name, population, center, polygon, parent/child context) PLUS its editorial layer mirrored from geo.bofrid.eu — `content` (housing/overview/lifestyle/neighborhoods/transportation prose), `landlordContent`, `universities`, `facebookGroups`, `wikipediaData` — and two live counts: `listingCount` (whole corpus incl. the ~88% rented/removed archive) and `activeListingCount` (rentable now). The funnel drop-in for `geo.bofrid.eu/api/geo/kommun/{slug}`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "stockholm",
            "description": "Kommun slug / kommun_kod / Pelias locality id — resolved via resolveKommun()."
          }
        ],
        "responses": {
          "200": {
            "description": "Kommun with editorial content and live listing counts"
          },
          "404": {
            "description": "invalid_kommun or not_found"
          }
        }
      }
    },
    "/v1/geo/places": {
      "get": {
        "tags": [
          "places"
        ],
        "summary": "List statistical / populated places",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Country"
          },
          {
            "name": "type",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "examples": {
              "tatort": {
                "value": "tatort"
              },
              "regso": {
                "value": "regso"
              }
            }
          },
          {
            "name": "parent",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "parent_admin_id, e.g. se:locality:stockholm"
          }
        ],
        "responses": {
          "200": {
            "description": "Places"
          }
        }
      }
    },
    "/v1/geo/places/{id}": {
      "get": {
        "tags": [
          "places"
        ],
        "summary": "Place by id",
        "description": "ID format is `{country_lc}:{place_type}:{municipality_slug}:{slug}`. Stockholm city is not stored as a single tätort — find real ids via `/v1/geo/places?country=SE&type=tatort` or `/v1/geo/search?q=…&layer=place`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "se:tatort:skurup:abbekas"
          }
        ],
        "responses": {
          "200": {
            "description": "Place + context"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/geo/places/{id}/polygon": {
      "get": {
        "tags": [
          "places"
        ],
        "summary": "Place polygon (GeoJSON — RegSO / tätort)",
        "description": "GeoJSON `Feature` for a statistical place (RegSO, tätort, suburb), geometry generated live from PostGIS `geom` via `ST_AsGeoJSON`. `properties` carries `id, slug, name, level_label (place_type), center, boundingbox`. `?simplify=<degrees>` supported (clamped ≤ 0.1). Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "se:regso:stockholm:klara-jacob"
          },
          {
            "name": "simplify",
            "in": "query",
            "schema": {
              "type": "number",
              "maximum": 0.1
            },
            "example": 0.001
          }
        ],
        "responses": {
          "200": {
            "description": "GeoJSON Feature"
          },
          "402": {
            "description": "credit_exhausted"
          },
          "404": {
            "description": "not_found"
          }
        }
      }
    },
    "/v1/geo/postal-codes": {
      "get": {
        "tags": [
          "postal-codes"
        ],
        "summary": "List postal codes",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Country"
          },
          {
            "name": "admin",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by primary_admin_area_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Postal codes"
          }
        }
      }
    },
    "/v1/geo/postal-codes/{code}": {
      "get": {
        "tags": [
          "postal-codes"
        ],
        "summary": "Postal code with streets + context",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "11146"
          },
          {
            "$ref": "#/components/parameters/Country"
          }
        ],
        "responses": {
          "200": {
            "description": "Postal code",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "postal_code": {
                      "$ref": "#/components/schemas/PostalCode"
                    },
                    "context": {
                      "$ref": "#/components/schemas/PeliasContext"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/geo/search": {
      "get": {
        "tags": [
          "geo-search"
        ],
        "summary": "Typo-tolerant search across admin areas + places",
        "description": "Uses pg_trgm trigram similarity. Returns hits ordered by similarity score.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 2
            },
            "example": "stockh"
          },
          {
            "name": "layer",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "country",
                "region",
                "county",
                "locality",
                "borough",
                "neighbourhood",
                "place"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/Country"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Search hits",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchHits"
                }
              }
            }
          }
        }
      }
    },
    "/v1/geo/autocomplete": {
      "get": {
        "tags": [
          "geo-search"
        ],
        "summary": "Typeahead predictions",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 2
            }
          },
          {
            "name": "layers",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated layers (default: country,region,locality,borough,place)"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 8,
              "maximum": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Predictions"
          }
        }
      }
    },
    "/v1/geo/reverse": {
      "get": {
        "tags": [
          "geo-search"
        ],
        "summary": "Reverse geocoding (coords → Pelias context)",
        "description": "PostGIS ST_Contains across admin polygons + nearest postal code via ST_Distance. Returns full Pelias address-hierarchy context (country → region → county → locality → borough → neighbourhood) for the smallest containing admin area.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "format": "float"
            },
            "example": 59.3326
          },
          {
            "name": "lng",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "format": "float"
            },
            "example": 18.0649
          }
        ],
        "responses": {
          "200": {
            "description": "Reverse geocode result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReverseResult"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or out-of-bounds coordinates"
          },
          "503": {
            "description": "PostGIS unavailable (geometry columns not populated for the queried point)"
          }
        }
      }
    },
    "/v1/geo/stats": {
      "get": {
        "tags": [
          "geo-search"
        ],
        "summary": "Dataset counts per Pelias layer",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Counts",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stats"
                }
              }
            }
          }
        }
      }
    },
    "/v1/knowledge": {
      "post": {
        "tags": [
          "knowledge"
        ],
        "summary": "Add a knowledge entry",
        "description": "Attach a free-text note/fact to a company or person. **Free** (`knowledge_add`). Keyed on the normalized `entity_key` (10-digit orgno / 12-digit pnr).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "entity_type",
                  "entity_key",
                  "body"
                ],
                "properties": {
                  "entity_type": {
                    "type": "string",
                    "enum": [
                      "company",
                      "person"
                    ]
                  },
                  "entity_key": {
                    "type": "string",
                    "description": "Company: 10- or 12-digit orgno. Person: 10- or 12-digit personnummer."
                  },
                  "body": {
                    "type": "string",
                    "maxLength": 20000,
                    "description": "The note/fact text."
                  },
                  "title": {
                    "type": "string",
                    "maxLength": 300
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Free-text tags for later filtering."
                  },
                  "author": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "metadata": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Structured extras."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created — `{ ok, entry }`"
          },
          "400": {
            "description": "invalid_body / invalid_entity_key"
          },
          "402": {
            "description": "credit_exhausted"
          }
        }
      },
      "get": {
        "tags": [
          "knowledge"
        ],
        "summary": "List an entity’s knowledge entries",
        "description": "List entries for one entity, newest first (`knowledge_list`, SEARCH). Filter with `?tag=`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "entity_type",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "company",
                "person"
              ]
            }
          },
          {
            "name": "entity_key",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Company: 10- or 12-digit orgno. Person: 10- or 12-digit personnummer."
            },
            "example": "5566778899"
          },
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "`{ ok, entity_type, entity_key, count, total, entries[] }`"
          },
          "400": {
            "description": "invalid_query / invalid_entity_key"
          },
          "402": {
            "description": "credit_exhausted"
          }
        }
      }
    },
    "/v1/knowledge/{id}": {
      "get": {
        "tags": [
          "knowledge"
        ],
        "summary": "Fetch one knowledge entry",
        "description": "Fetch a single entry by id (`knowledge_get`, LOOKUP).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "`{ ok, entry }`"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "knowledge"
        ],
        "summary": "Edit a knowledge entry",
        "description": "Partial update (`knowledge_update`, **free**). Any subset of the fields below.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [],
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 300
                  },
                  "body": {
                    "type": "string",
                    "maxLength": 20000,
                    "description": "The note/fact text."
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Free-text tags for later filtering."
                  },
                  "author": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "metadata": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Structured extras."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "`{ ok, entry }`"
          },
          "400": {
            "description": "invalid_body"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "knowledge"
        ],
        "summary": "Delete a knowledge entry",
        "description": "Remove an entry (`knowledge_delete`, **free**).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "`{ ok, deleted }`"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/companies/by-orgno/{orgno}/knowledge": {
      "get": {
        "tags": [
          "knowledge"
        ],
        "summary": "List a company’s knowledge entries",
        "description": "Company-scoped convenience over `/v1/knowledge` (entity_type=company). Keyed on the 10-digit orgno core.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "orgno",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9-]{10,14}$"
            },
            "example": "5566778899"
          },
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "`{ ok, entity_type, entity_key, count, total, entries[] }`"
          },
          "400": {
            "description": "invalid_orgno"
          },
          "402": {
            "description": "credit_exhausted"
          }
        }
      },
      "post": {
        "tags": [
          "knowledge"
        ],
        "summary": "Add a knowledge entry to a company",
        "description": "Company-scoped convenience over `POST /v1/knowledge`. **Free** (`knowledge_add`).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "orgno",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9-]{10,14}$"
            },
            "example": "5566778899"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "maxLength": 20000,
                    "description": "The note/fact text."
                  },
                  "title": {
                    "type": "string",
                    "maxLength": 300
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Free-text tags for later filtering."
                  },
                  "author": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "metadata": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Structured extras."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created — `{ ok, entry }`"
          },
          "400": {
            "description": "invalid_orgno / invalid_body"
          },
          "402": {
            "description": "credit_exhausted"
          }
        }
      }
    },
    "/v1/knowledge/attachments": {
      "get": {
        "tags": [
          "knowledge"
        ],
        "summary": "List file attachments for an entity",
        "description": "File metadata (no bytes) for a company/person, newest first (`knowledge_attachment_list`, SEARCH). Files are stored on the NAS file store; download via `/v1/knowledge/attachments/{id}`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "entity_type",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "company",
                "person"
              ]
            }
          },
          {
            "name": "entity_key",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "5566778899"
          }
        ],
        "responses": {
          "200": {
            "description": "`{ ok, entity_type, entity_key, count, attachments[] }`"
          },
          "400": {
            "description": "invalid_entity_type / invalid_entity_key"
          },
          "503": {
            "description": "attachments_unconfigured"
          }
        }
      },
      "post": {
        "tags": [
          "knowledge"
        ],
        "summary": "Upload a file attachment",
        "description": "Attach a file to a company/person (`knowledge_attach`, **free**, max 10 MB). Two content types accepted:\n- `multipart/form-data` — fields `file`, `entity_type`, `entity_key`, optional `knowledge_id`.\n- `application/json` — `{ entity_type, entity_key, filename, content_type, data_base64 }`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file",
                  "entity_type",
                  "entity_key"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  },
                  "entity_type": {
                    "type": "string",
                    "enum": [
                      "company",
                      "person"
                    ]
                  },
                  "entity_key": {
                    "type": "string"
                  },
                  "knowledge_id": {
                    "type": "string"
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "entity_type",
                  "entity_key",
                  "filename",
                  "data_base64"
                ],
                "properties": {
                  "entity_type": {
                    "type": "string",
                    "enum": [
                      "company",
                      "person"
                    ]
                  },
                  "entity_key": {
                    "type": "string"
                  },
                  "filename": {
                    "type": "string"
                  },
                  "content_type": {
                    "type": "string"
                  },
                  "data_base64": {
                    "type": "string",
                    "description": "base64-encoded file bytes"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created — `{ ok, attachment }`"
          },
          "400": {
            "description": "invalid upload / entity"
          },
          "413": {
            "description": "file_too_large"
          },
          "503": {
            "description": "attachments_unconfigured"
          }
        }
      }
    },
    "/v1/knowledge/attachments/{id}": {
      "get": {
        "tags": [
          "knowledge"
        ],
        "summary": "Download a file attachment",
        "description": "Streams the raw file bytes with its stored `Content-Type` (`knowledge_attachment_get`, LOOKUP).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The file bytes",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "503": {
            "description": "attachments_unconfigured"
          }
        }
      },
      "delete": {
        "tags": [
          "knowledge"
        ],
        "summary": "Delete a file attachment",
        "description": "Remove a file from the store (`knowledge_attachment_delete`, **free**).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "`{ ok, deleted }`"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "503": {
            "description": "attachments_unconfigured"
          }
        }
      }
    },
    "/v1/listings": {
      "get": {
        "tags": [
          "listings"
        ],
        "summary": "List rental listings",
        "description": "Filtered, paginated list ordered by `updated_at DESC, id DESC`. Returns the unpaged `total` alongside the page, so a client can fetch every listing by walking `offset` to `total`.\n\n**Not filtered to active by default** — 88% of rows are `rented`/`removed` and that archive is the point. Pass `?status=active` for live inventory only.\n\nEach listing carries: identity (`id`, `source`, `source_id`, `source_platform`, `source_url`, `external_url`), media (`image_url`, `image_urls`), dwelling (`property_type`, `rent_sek`, `rooms`, `sqm`, `floor`, `traits`, `furnished`, `shared`, `firsthand`, `student_home`, `senior_home`, `corporate_home`, `tenure_type`, `lease_type`, `premium`), location (`city`, `region`, `postal_code`, `address`, `latitude`, `longitude`, `kommun_slug`, `lan_slug`, `area_id`), and lifecycle (`status`, `is_rented`, `rented_at`, `published_at`, `available_from`, `available_to`, `created_at`, `updated_at`).\n\nResponse shape: `{ ok, count, total, limit, offset, listings: [...] }`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "rented",
                "removed"
              ]
            },
            "example": "active"
          },
          {
            "name": "source",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "example": "homii",
            "description": "Source system name (`homii`, `boplats`, `momentum`, `bofrid`, `kvalster`) OR a `source_platform` host (`bofrid.se`) — matches either."
          },
          {
            "name": "area",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "example": "se:regso:orebro:tybble",
            "description": "RegSO/area id — exact `area_id` match."
          },
          {
            "name": "kommun",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "example": "goteborg",
            "description": "Kommun slug, joins `geo.admin_areas.slug` (admin_level 8)."
          },
          {
            "name": "lan",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "example": "vastra-gotaland",
            "description": "Län slug."
          },
          {
            "name": "rent_min",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "example": 5000
          },
          {
            "name": "rent_max",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "example": 15000
          },
          {
            "name": "rooms_min",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "example": 2
          },
          {
            "name": "sqm_min",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "example": 40
          },
          {
            "name": "property_type",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "example": "apartment"
          },
          {
            "name": "furnished",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "example": true
          },
          {
            "name": "firsthand",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "example": true
          },
          {
            "name": "student_home",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "example": false
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 1000
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of listings plus unpaged total"
          },
          "400": {
            "description": "invalid_query, or invalid_range (rent_min > rent_max)"
          },
          "402": {
            "description": "credit_exhausted"
          }
        }
      }
    },
    "/v1/listings/search": {
      "get": {
        "tags": [
          "listings"
        ],
        "summary": "Search rental listings by free text",
        "description": "Case-insensitive substring match over `title`, `address`, and `city`. Active listings rank before dead ones, then newest-updated first.\n\nResponse shape: `{ ok, query, count, results: [...] }`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string",
              "minLength": 2
            },
            "required": true,
            "example": "Storgatan"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching listings"
          },
          "400": {
            "description": "invalid_query (q shorter than 2 chars)"
          },
          "402": {
            "description": "credit_exhausted"
          }
        }
      }
    },
    "/v1/listings/markers": {
      "get": {
        "tags": [
          "listings"
        ],
        "summary": "Map markers (lightweight pins)",
        "description": "Lightweight points for a map viewport — `id`, `latitude`, `longitude`, `rent_sek`, `rooms`, `sqm`, `property_type`, `status` only. Same filters as `/v1/listings` but defaults to `status=active` and allows a higher cap (2000). Backed by a PostGIS GIST index on the listing geometry.\n\nResponse shape: `{ ok, count, markers: [...] }`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "active",
              "enum": [
                "active",
                "rented",
                "removed"
              ]
            },
            "description": "Defaults to active — a map shows what you can rent, not the archive."
          },
          {
            "name": "source",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "example": "homii"
          },
          {
            "name": "area",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "example": "se:regso:orebro:tybble"
          },
          {
            "name": "kommun",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "example": "goteborg"
          },
          {
            "name": "lan",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "example": "vastra-gotaland"
          },
          {
            "name": "rent_min",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "example": 5000
          },
          {
            "name": "rent_max",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "example": 15000
          },
          {
            "name": "rooms_min",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "example": 2
          },
          {
            "name": "sqm_min",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "example": 40
          },
          {
            "name": "property_type",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "example": "apartment"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 500,
              "maximum": 2000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Map markers"
          },
          "400": {
            "description": "invalid_query"
          },
          "402": {
            "description": "credit_exhausted"
          }
        }
      }
    },
    "/v1/listings/batch": {
      "get": {
        "tags": [
          "listings"
        ],
        "summary": "Fetch many listings by id",
        "description": "Hydrate up to 100 listings in one call — e.g. after a map-marker click or to refresh a saved set. Full listing objects, same shape as `/v1/listings/{id}`. Missing/unknown ids are silently omitted (`requested` vs `count` reveals the gap).\n\nResponse shape: `{ ok, requested, count, listings: [...] }`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "ids",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "required": true,
            "example": "00000000-0000-0000-0000-000000000000,11111111-1111-1111-1111-111111111111",
            "description": "Comma-separated uuids, max 100."
          }
        ],
        "responses": {
          "200": {
            "description": "Listings for the given ids"
          },
          "400": {
            "description": "invalid_query, too_many_ids (>100), or invalid_id"
          },
          "402": {
            "description": "credit_exhausted"
          }
        }
      }
    },
    "/v1/listings/sources": {
      "get": {
        "tags": [
          "listings"
        ],
        "summary": "Source platforms + live counts",
        "description": "Enumerate the source systems feeding the data, each with `total`, `active`, and `rented` counts — a discovery endpoint for `?source=` filtering. Free.\n\nResponse shape: `{ ok, count, sources: [{ source, total, active, rented }] }`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "One row per source system"
          },
          "402": {
            "description": "credit_exhausted"
          }
        }
      }
    },
    "/v1/listings/landlords": {
      "get": {
        "tags": [
          "listings"
        ],
        "summary": "Resolved landlords (company + counts)",
        "description": "One row per company Mantal can attribute as a landlord (platform host → registered domain → orgno), newest inventory first. The company-rolled-up mirror of `/v1/listings/sources`. Free.\n\nResponse: `{ ok, count, landlords: [{ orgno, company_name, domains[], platforms[], total, active, rented }] }`. Feed an `orgno` into `/v1/companies/by-orgno/{orgno}/listings` to page that landlord's listings.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "One row per attributable landlord company"
          },
          "402": {
            "description": "credit_exhausted"
          }
        }
      }
    },
    "/v1/listings/{id}": {
      "get": {
        "tags": [
          "listings"
        ],
        "summary": "Rental listing by id",
        "description": "A single listing by uuid. Each listing carries: identity (`id`, `source`, `source_id`, `source_platform`, `source_url`, `external_url`), media (`image_url`, `image_urls`), dwelling (`property_type`, `rent_sek`, `rooms`, `sqm`, `floor`, `traits`, `furnished`, `shared`, `firsthand`, `student_home`, `senior_home`, `corporate_home`, `tenure_type`, `lease_type`, `premium`), location (`city`, `region`, `postal_code`, `address`, `latitude`, `longitude`, `kommun_slug`, `lan_slug`, `area_id`), and lifecycle (`status`, `is_rented`, `rented_at`, `published_at`, `available_from`, `available_to`, `created_at`, `updated_at`).\n\nResponse shape: `{ ok, listing }`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "00000000-0000-0000-0000-000000000000"
          }
        ],
        "responses": {
          "200": {
            "description": "Listing"
          },
          "400": {
            "description": "invalid_id (must be a uuid)"
          },
          "402": {
            "description": "credit_exhausted"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "pk_live_… / pk_test_…",
        "description": "Self-serve a key pair at [mantal.eu/signup](https://mantal.eu/signup) (closed-beta domain allowlist). Each signup mints a `pk_live_…` (billed; 10 SEK starting credit) and a `pk_test_…` (unlimited credit). Operators can also mint keys via `pnpm create-key <customer_id> [--plan pro] [--test]` on the server."
      },
      "AdminKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Admin-Key",
        "description": "Server-side admin key for billing endpoint"
      }
    },
    "parameters": {
      "Limit": {
        "name": "limit",
        "in": "query",
        "schema": {
          "type": "integer",
          "default": 100,
          "maximum": 1000
        }
      },
      "Offset": {
        "name": "offset",
        "in": "query",
        "schema": {
          "type": "integer",
          "default": 0
        }
      },
      "Country": {
        "name": "country",
        "in": "query",
        "schema": {
          "type": "string",
          "minLength": 2,
          "maxLength": 2
        },
        "description": "ISO 3166-1 alpha-2 (defaults to SE on slug lookups)"
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Validation error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing / invalid Bearer token",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "VerifyResponse": {
        "type": "object",
        "description": "Issued once by `/v1/signup/verify` and `/v1/login/verify`. Both keys must be stored client-side — only their sha256 hashes are kept server-side.",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "apiKey": {
            "type": "string",
            "example": "pk_live_…",
            "description": "Billed key (plan='free' on signup; whatever the prior plan was on rotation). Hits the configured environment."
          },
          "testApiKey": {
            "type": "string",
            "example": "pk_test_…",
            "description": "Unlimited-credit key."
          },
          "customerId": {
            "type": "string",
            "example": "signup_42"
          },
          "plan": {
            "type": "string",
            "example": "free"
          },
          "creditOre": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Prepaid credit balance in öre on the live key (NULL = unlimited)"
          },
          "creditSek": {
            "type": [
              "number",
              "null"
            ]
          },
          "rotated": {
            "type": "boolean",
            "description": "Present and `true` on login/verify; absent on signup/verify"
          },
          "keepThis": {
            "type": "string"
          }
        }
      },
      "MeResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "customer_id": {
            "type": "string"
          },
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "pro",
              "enterprise",
              "internal",
              "test"
            ],
            "description": "Plan tier on the API key. `free`=self-signup with 10 SEK prepaid credit; `pro`/`enterprise`/`internal`=CLI-minted, unmetered; `test`=`pk_test_` key, hits the test environment regardless of server env, unmetered."
          },
          "status": {
            "type": "string"
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "credit": {
            "type": "object",
            "properties": {
              "ore": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "sek": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "unlimited": {
                "type": "boolean"
              }
            }
          },
          "today": {
            "type": "object",
            "properties": {
              "since": {
                "type": "string",
                "format": "date-time"
              },
              "by_endpoint": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              },
              "total": {
                "type": "integer"
              }
            }
          },
          "recent": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "enum": [
                    "persons_lookup",
                    "persons_search",
                    "persons_retrieve",
                    "geo_admin_get",
                    "geo_admin_children",
                    "geo_admin_parents",
                    "geo_admin_list",
                    "geo_places_list",
                    "geo_place_get",
                    "geo_countries_list",
                    "geo_country_get",
                    "geo_postal_codes_list",
                    "geo_postal_code_get",
                    "geo_search",
                    "geo_autocomplete",
                    "geo_reverse",
                    "geo_stats",
                    "properties_lookup",
                    "properties_search",
                    "properties_reverse",
                    "properties_buildings",
                    "buildings_lookup",
                    "buildings_bbox",
                    "addresses_search",
                    "addresses_registered",
                    "addresses_lookup",
                    "me_get"
                  ],
                  "description": "Internal endpoint label written to `usage_log.endpoint`. Not derivable from the URL path; this is the canonical list."
                },
                "source": {
                  "type": "string",
                  "enum": [
                    "spar",
                    "lantmateriet",
                    "geo",
                    "tiles",
                    "api"
                  ],
                  "description": "Internal subsystem tag for the request. Note: `source: 'spar'` does NOT imply a billed call — `persons_retrieve` and cache hits are also tagged this way; check `cache_hit` and the endpoint label for the actual cost. (These enum values are legacy internal tags and may be renamed in a future version.)"
                },
                "success": {
                  "type": "boolean"
                },
                "cache_hit": {
                  "type": "boolean"
                },
                "latency_ms": {
                  "type": "integer"
                },
                "status_code": {
                  "type": "integer"
                },
                "ts": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          }
        }
      },
      "Health": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "upstreamEnv": {
            "type": "string",
            "enum": [
              "test",
              "prod"
            ]
          },
          "callsTodayProd": {
            "type": "integer"
          },
          "callsTodayTest": {
            "type": "integer"
          },
          "dailyCap": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "Billing": {
        "type": "object",
        "properties": {
          "upstreamEnv": {
            "type": "string"
          },
          "today": {
            "$ref": "#/components/schemas/BillingPeriod"
          },
          "this_month": {
            "$ref": "#/components/schemas/BillingPeriod"
          },
          "this_year": {
            "$ref": "#/components/schemas/BillingPeriod"
          }
        }
      },
      "BillingPeriod": {
        "type": "object",
        "properties": {
          "persnr_calls": {
            "type": "integer"
          },
          "namn_calls": {
            "type": "integer"
          },
          "persnr_cost_sek": {
            "type": "number"
          },
          "namn_cost_sek": {
            "type": "number"
          },
          "total_cost_sek": {
            "type": "number"
          },
          "next_persnr_call_price_sek": {
            "type": "number",
            "description": "Marginal price for the very next billed PNR call today (tier-based)"
          }
        }
      },
      "PersonLookupRequest": {
        "type": "object",
        "description": "Polymorphic lookup body. Provide ONE of:\n- `{ pnr }` — civil-registry (SPAR) PNR lookup (`?source=spar|register|all`, default `spar`).\n- `{ name, city? }` — Hitta register name search (free).\n- `{ address, city? }` — Hitta register address search (free).\n- `{ pnr, name, city? }` — **register-only** match on birthday (derived from `pnr`) AND name. Returns the registered (folkbokförings-)address from the Hitta register. **Never calls SPAR**, regardless of `source`. `pnr` here may be a bare birthdate fragment such as `990513` (YYMMDD), `19990513` (YYYYMMDD), or a full 10/12-digit personnummer.\n\n_For the `{pnr,name}` match:_ Each returned person carries the full register **`address_history`** (folkbokförings-/registration history: an array of past addresses, newest first, each with street/number/zipcode/postort, a GeoJSON `loc` Point, and `date` = registration date in epoch ms), the structured current `addresses`, and `pnr`/`full_name`/`ssn_valid` for identity confirmation — all from the register snapshot, never SPAR.",
        "properties": {
          "pnr": {
            "type": "string",
            "description": "A personnummer (10 or 12 digits, dashes optional) or — when combined with `name` — a bare birthdate fragment (`990513` / `19990513`).",
            "example": "990513"
          },
          "name": {
            "type": "string",
            "description": "Full or partial name; the last token is treated as the surname (e.g. \"philip rönnmark\")."
          },
          "address": {
            "type": "string",
            "description": "Street address for a register address search."
          },
          "city": {
            "type": "string",
            "description": "Optional city narrowing for register searches."
          },
          "limit": {
            "type": "integer",
            "description": "Max register hits (default 10, max 50)."
          },
          "source": {
            "type": "string",
            "enum": [
              "spar",
              "register",
              "all"
            ],
            "description": "PNR-only lookups: source selection (default `spar`). Ignored for the register-only `{pnr,name}` match."
          },
          "shape": {
            "type": "string",
            "enum": [
              "rich",
              "flat"
            ],
            "default": "rich"
          },
          "fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Flat-shape projection filter (e.g. ['name1','street','postalcode','city'])."
          }
        }
      },
      "PersonLookupResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "cached": {
            "type": "boolean",
            "description": "True when this response came from the local 24h cache instead of a billed call. Cached results do not count toward billing. Identical PNRs within the TTL will return `cached: true` on the second call onward — expected behavior."
          },
          "uuid": {
            "type": [
              "string",
              "null"
            ],
            "description": "UUID for this call (audit trail)"
          },
          "person": {
            "description": "Either a rich Person object or a flat key/value map depending on `shape`"
          }
        }
      },
      "PersonSearchRequest": {
        "type": "object",
        "description": "Mix-and-match — at least one of (`namn`, `fornamn`, `mellanEfternamn`) is required (pure-address searches are rejected).",
        "properties": {
          "namn": {
            "type": "string",
            "description": "Full-name search (wildcard '*' allowed)"
          },
          "fornamn": {
            "type": "string"
          },
          "mellanEfternamn": {
            "type": "string",
            "description": "Surname OR middle name"
          },
          "utdelningsadress": {
            "type": "string"
          },
          "postort": {
            "type": "string"
          },
          "postnummer": {
            "type": "string"
          },
          "yearFrom": {
            "type": "string"
          },
          "yearTo": {
            "type": "string"
          },
          "kon": {
            "type": "string",
            "enum": [
              "K",
              "M"
            ]
          },
          "lanKod": {
            "type": "string"
          },
          "kommunKod": {
            "type": "string"
          },
          "distriktKod": {
            "type": "string"
          },
          "fonetiskSokning": {
            "type": "boolean"
          }
        }
      },
      "PersonSearchResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "searchId": {
            "type": "string",
            "format": "uuid"
          },
          "numberOfResults": {
            "type": "integer"
          },
          "truncatedAt100": {
            "type": "boolean"
          }
        }
      },
      "PeliasLayer": {
        "type": "string",
        "enum": [
          "country",
          "region",
          "county",
          "locality",
          "borough",
          "neighbourhood"
        ]
      },
      "AdminArea": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "se:locality:stockholm"
          },
          "countryCode": {
            "type": "string",
            "example": "SE"
          },
          "adminLevel": {
            "type": "integer",
            "description": "OSM admin_level (2-11)"
          },
          "peliasLayer": {
            "$ref": "#/components/schemas/PeliasLayer"
          },
          "parentId": {
            "type": [
              "string",
              "null"
            ]
          },
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "nameEn": {
            "type": [
              "string",
              "null"
            ]
          },
          "levelLabel": {
            "type": [
              "string",
              "null"
            ],
            "description": "Country-specific label, e.g. 'Län' / 'Bundesland' / 'Région'"
          },
          "code": {
            "type": [
              "string",
              "null"
            ]
          },
          "iso31662": {
            "type": [
              "string",
              "null"
            ]
          },
          "nutsCode": {
            "type": [
              "string",
              "null"
            ]
          },
          "center": {
            "type": [
              "object",
              "null"
            ]
          },
          "boundingbox": {
            "type": [
              "array",
              "null"
            ]
          },
          "population": {
            "type": [
              "integer",
              "null"
            ]
          },
          "areaKm2": {
            "type": [
              "number",
              "null"
            ]
          }
        }
      },
      "PeliasContext": {
        "type": "object",
        "description": "Pelias address hierarchy — every layer is present (null if not applicable in the country).",
        "properties": {
          "country": {
            "type": [
              "object",
              "null"
            ]
          },
          "region": {
            "type": [
              "object",
              "null"
            ]
          },
          "county": {
            "type": [
              "object",
              "null"
            ]
          },
          "locality": {
            "type": [
              "object",
              "null"
            ]
          },
          "borough": {
            "type": [
              "object",
              "null"
            ]
          },
          "neighbourhood": {
            "type": [
              "object",
              "null"
            ]
          }
        }
      },
      "AdminAreaWithContext": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "admin": {
            "$ref": "#/components/schemas/AdminArea"
          },
          "context": {
            "$ref": "#/components/schemas/PeliasContext"
          }
        }
      },
      "SearchHits": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "query": {
            "type": "string"
          },
          "count": {
            "type": "integer"
          },
          "results": {
            "type": "array",
            "items": {
              "type": "object"
            }
          }
        }
      },
      "ReverseResult": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "context": {
            "$ref": "#/components/schemas/PeliasContext"
          },
          "matchedAdminId": {
            "type": [
              "string",
              "null"
            ]
          },
          "places": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "nearestPostalCode": {
            "type": [
              "object",
              "null"
            ]
          }
        }
      },
      "Parcel": {
        "type": "object",
        "description": "A single markområde (parcel polygon) belonging to a fastighet. The `geometry` is a GeoJSON Polygon in WGS84 — paste it directly into MapLibre, Mapbox GL, or Leaflet.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "property_id": {
            "type": "string",
            "format": "uuid"
          },
          "omradesnummer": {
            "type": "integer",
            "description": "Parcel index within the fastighet (1..n)"
          },
          "area_m2": {
            "type": "number"
          },
          "geometry": {
            "type": "object",
            "description": "GeoJSON Polygon (WGS84)"
          }
        }
      },
      "PropertyWithParcels": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "property": {
            "type": [
              "object",
              "null"
            ],
            "description": "Property metadata (id, beteckning, kommun, centroid, bbox, area, counts). `null` only on the reverse endpoint when the point misses every fastighet."
          },
          "parcels": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Parcel"
            },
            "description": "All markområden for this fastighet, with full polygon geometry."
          }
        }
      },
      "Stats": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "stats": {
            "type": "object",
            "properties": {
              "countries": {
                "type": "integer",
                "example": 19
              },
              "regions": {
                "type": "integer",
                "example": 280
              },
              "counties": {
                "type": "integer",
                "example": 0
              },
              "localities": {
                "type": "integer",
                "example": 77657
              },
              "boroughs": {
                "type": "integer",
                "example": 107
              },
              "places": {
                "type": "integer",
                "example": 4800
              },
              "postalCodes": {
                "type": "integer",
                "example": 93603
              }
            }
          }
        }
      },
      "Address": {
        "type": "object",
        "description": "Swedish location address.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "street_text": {
            "type": [
              "string",
              "null"
            ],
            "description": "Rendered street + number (+ optional entrance letter), e.g. `Odengatan 13C`"
          },
          "kommun_namn": {
            "type": [
              "string",
              "null"
            ]
          },
          "kommun_kod": {
            "type": [
              "string",
              "null"
            ]
          },
          "municipality_admin_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "FK to `geo.admin_areas` for the kommun, e.g. `se:locality:stockholm`"
          },
          "lat": {
            "type": [
              "number",
              "null"
            ]
          },
          "lng": {
            "type": [
              "number",
              "null"
            ]
          },
          "is_main_entrance": {
            "type": [
              "boolean",
              "null"
            ]
          }
        }
      },
      "Building": {
        "type": "object",
        "description": "Swedish building footprint with embedded addresses (no sibling `addresses` field on the response).",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "property_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "has_address": {
            "type": "boolean"
          },
          "byggnadsar": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Year built"
          },
          "area_m2": {
            "type": [
              "number",
              "null"
            ]
          },
          "centroid": {
            "type": [
              "object",
              "null"
            ],
            "description": "GeoJSON Point (WGS84)"
          },
          "footprint": {
            "type": [
              "object",
              "null"
            ],
            "description": "GeoJSON Polygon (WGS84)"
          },
          "all_purposes": {
            "type": "array",
            "description": "All recorded purpose codes for this building. Each entry is an object — never a bare string.",
            "items": {
              "type": "object",
              "properties": {
                "typ": {
                  "type": "string",
                  "description": "Purpose code, e.g. `Bostad; Småhus med en lägenhet`"
                },
                "huvudandamal": {
                  "type": "boolean",
                  "description": "True if this is the primary purpose for the building"
                }
              },
              "additionalProperties": true
            }
          },
          "addresses": {
            "type": "array",
            "description": "Location addresses embedded on the building. Empty array when `has_address=false`.",
            "items": {
              "$ref": "#/components/schemas/Address"
            }
          }
        }
      },
      "PostalCode": {
        "type": "object",
        "description": "Swedish postnummer (or country-equivalent) with the streets that fall under it.",
        "properties": {
          "id": {
            "type": "string"
          },
          "country_code": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "example": "11146"
          },
          "primary_admin_area_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "FK to `geo.admin_areas` for the primary locality"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "streets": {
            "type": "array",
            "description": "Streets falling under this postal code.",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "description": "Street name"
                },
                "numbers": {
                  "type": "string",
                  "description": "Number range (e.g. `1-99`, `2-12`, or `*` for any)"
                }
              },
              "additionalProperties": true
            }
          }
        }
      },
      "PersonRegisterMatchResponse": {
        "type": "object",
        "description": "Register (Hitta) result for `{name}`, `{address}`, or `{pnr,name}` lookups. A LIST — zero, one, or many people can match. Never involves SPAR.",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "source": {
            "type": "string",
            "enum": [
              "register"
            ]
          },
          "match": {
            "type": "string",
            "enum": [
              "pnr+name"
            ],
            "description": "Present on the `{pnr,name}` variant."
          },
          "searched_birthday": {
            "type": "string",
            "description": "ISO date parsed from the supplied `pnr` (e.g. `1999-05-13`). `{pnr,name}` only."
          },
          "dob_verified": {
            "type": "boolean",
            "description": "`{pnr,name}` only. `true` → every returned person is confirmed against `searched_birthday` (same-name people with a different known DOB are excluded). `false` → no exact-DOB match existed, so these are name-only candidates whose register DOB is unknown (register DOB coverage is partial) — narrow further with `city`."
          },
          "count": {
            "type": "integer",
            "description": "Number of people returned."
          },
          "truncated": {
            "type": "boolean",
            "description": "True when `count` hit `limit` — more rows may exist; refine with `city` or a higher `limit` (max 50)."
          },
          "persons": {
            "type": "array",
            "description": "Matched register records. For `{pnr,name}` each item additionally carries `addresses`, `pnr`, `full_name`, `ssn_valid`, and `address_history`.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "first_name": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "last_name": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "birthday": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Register DOB; may be null (partial coverage)."
                },
                "address_street": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "address_number": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "address_zipcode": {
                  "type": [
                    "integer",
                    "null"
                  ]
                },
                "address_city": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "address_history": {
                  "type": "array",
                  "description": "Registration/folkbokförings history, newest first (`{pnr,name}` only).",
                  "items": {
                    "type": "object",
                    "properties": {
                      "streetname": {
                        "type": "string"
                      },
                      "number": {},
                      "zipcode-name": {
                        "type": "string"
                      },
                      "postort": {
                        "type": "string"
                      },
                      "loc": {
                        "type": "object",
                        "description": "GeoJSON Point."
                      },
                      "date": {
                        "type": "integer",
                        "description": "Registration date, epoch milliseconds."
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}