openapi: 3.1.0
info:
  title: InfraNode API
  version: 1.0.0
  description: >-
    Open-Source-Proxy-REST-API für normalisierte offene Daten deutscher
    Großstädte. Bekannt sind 84 Städte über 100.000 Einwohner, davon 28
    Kern-Städte mit voller Quellen-Abdeckung; weitere Städte werden über
    AGS- und geobasierte Quellen bedient. Jede Antwort folgt demselben
    kanonischen Envelope (data/meta/attribution) mit Lizenz-Tag und ehrlichem
    source_status (ok/no_data/disabled/not_covered). Teilabgedeckte Endpunkte
    (flood, webcams, traffic, road-events) liefern für eine nicht-abgedeckte
    Stadt 200 source_status="not_covered" + meta.covered_cities statt eines
    leeren "ok"; die Abdeckung listet die Seite /abdeckung. Pfade tragen den vollen
    /api/v1-Prefix, damit die ausgelieferte Spec driftfrei zu den live
    registrierten Routen bleibt. Paginierung ist kanal-abhängig: direktes REST
    liefert Datenart-Listen voll, GPT-Actions und MCP sind serverseitig bzw.
    client-seitig auf ein Default-Limit gebunden; limit=all (bzw. ?all=1) erzwingt
    kanalunabhängig die Vollausgabe. Details siehe meta.pagination (Meta-Schema).
    Der Stadt-Slug im Pfad wird tolerant aufgelöst: der deutsche Name mit oder
    ohne Umlaute, beliebige Groß/Kleinschreibung, gängige englische Exonyme und
    Kurzformen führen zum kanonischen Slug (München/münchen/munich/munchen ->
    muenchen, cologne -> koeln, frankfurt -> frankfurt-am-main). Ein unbekannter
    Name liefert 404 mit einem "Meintest du ...?"-Hinweis auf den nächstliegenden
    Slug; die kanonischen Slugs listet GET /api/v1/cities.
  license:
    name: Apache-2.0
    url: https://www.apache.org/licenses/LICENSE-2.0
servers:
  - url: https://infranode.dev
    description: Öffentliche gehostete API, keylos (Standard)
  - url: http://localhost
    description: Lokaler Caddy-Ingress (Docker Compose)
tags:
  - name: meta
    description: Meta- und Diagnose-Routen (Health, Ping, Source-Status, Spec)
  - name: cities
    description: Stadt-Register und normalisierte Stadt-Stammdaten
  - name: compare
    description: Multi-City-Vergleich einer Ressource über mehrere Städte
  - name: Live
    description: >-
      Getrennte Live-Kategorie unter /api/v1/live. Minutenfrische bzw.
      quasi-live Quellen mit eigenem Envelope-Kennzeichen: meta.as_of
      (Datenstand) und meta.refresh_seconds (Kadenz). Re-exponiert zusätzlich
      bestehende Live-Endpunkte (air, air-uba, water-level, traffic, webcams,
      flood); deren Altpfade unter /api/v1/cities bleiben als Deprecation-Alias
      erhalten (kein Breaking Change).
  - name: stations
    description: >-
      Per-Bahnhof-Live-Boards unter /api/v1/stations/{eva}. Abfahrten/Ankünfte
      JEDES DB-Bahnhofs über seine EVA-Nummer (aus dem Katalog
      /cities/{slug}/stations), alle Gattungen inkl. Nahverkehr, mit Echtzeit-
      Verspätung und Störungen/Meldungen. Quelle: DB Timetables (CC BY 4.0).
paths:
  /api/v1/health:
    get:
      tags: [meta]
      summary: Liveness und Readiness der App prüfen
      x-summary-en: Check app liveness and readiness
      description: >-
        Liefert den App-Status und die Redis-Erreichbarkeit. Gedacht für
        Uptime-Monitore und Load-Balancer-Probes: antwortet mit 200, sobald die
        App bereit ist, sonst mit einem Fehlerstatus.
      x-description-en: >-
        Returns the app status and Redis reachability. Designed for uptime
        monitors and load-balancer probes: responds with 200 once the app is
        ready, otherwise with an error status.
      operationId: getHealth
      responses:
        "200":
          description: App läuft
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Health"
  /api/v1/ping:
    get:
      tags: [meta]
      summary: Einfacher Ping mit Correlation-ID
      x-summary-en: Simple ping with correlation ID
      description: >-
        Gibt eine Pong-Antwort inklusive correlation_id zurück und schreibt eine
        strukturierte JSON-Log-Zeile mit request_id. Nützlich, um die
        Request-Verkettung im Logging zu prüfen.
      x-description-en: >-
        Returns a pong response including a correlation_id and writes a
        structured JSON log line with a request_id. Useful for verifying request
        tracing in the logs.
      operationId: getPing
      responses:
        "200":
          description: Pong
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ping"
          headers:
            X-Request-ID:
              description: Pro Request generierte/durchgereichte Correlation-ID
              schema:
                type: string
  /api/v1/echo:
    get:
      tags: [meta]
      summary: Integer-Query zurückgeben (Validierungs-Demo)
      x-summary-en: Echo an integer query (validation demo)
      description: >-
        Gibt den übergebenen Integer-Query-Parameter zurück. Ein nicht-ganzzahliger
        Wert löst den einheitlichen 400-Fehler-Envelope aus und zeigt so die
        Eingabevalidierung.
      x-description-en: >-
        Returns the integer query parameter you pass in. A non-integer value
        triggers the unified 400 error envelope, demonstrating input validation.
      operationId: getEcho
      parameters:
        - name: n
          in: query
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Echo
          content:
            application/json:
              schema:
                type: object
        "400":
          description: Ungültiger Query-Parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/_boom:
    get:
      tags: [meta]
      summary: Erzwungener Upstream-Fehler (Demo)
      x-summary-en: Forced upstream failure (demo)
      description: >-
        Existiert nur zum Testen des zentralen Fehler-Mappings. Liefert immer
        einen 503-Fehler-Envelope.
      x-description-en: >-
        Exists only to test the central error mapping. Always returns a 503
        error envelope.
      operationId: getBoom
      responses:
        "503":
          description: Simulierter Upstream-Ausfall
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/sources:
    get:
      tags: [meta]
      summary: Status aller Upstream-Quellen
      x-summary-en: Status of all upstream sources
      description: >-
        Listet je bekannter Quelle den enabled-Zustand (aus den Settings) und
        den Circuit-Breaker-Zustand (CLOSED/OPEN/HALF_OPEN). So ist auf einen
        Blick sichtbar, welche Quelle gerade liefert und welche pausiert ist.
      x-description-en: >-
        Lists each known source with its enabled state (from settings) and its
        circuit-breaker state (CLOSED/OPEN/HALF_OPEN). Shows at a glance which
        source is currently serving and which is paused.
      operationId: getSources
      responses:
        "200":
          description: Liste der Quellen-Status
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
            Cache-Control:
              $ref: "#/components/headers/CacheControl"
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/SourceStatus"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "304":
          $ref: "#/components/responses/NotModified"
      parameters:
        - in: header
          name: If-None-Match
          required: false
          schema: { type: string }
          description: >-
            Conditional GET. Stimmt der Wert mit dem aktuellen ETag überein,
            antwortet der Server mit 304 Not Modified ohne Body.
          x-description-en: >-
            Conditional GET. If the value matches the current ETag, the server
            responds with 304 Not Modified and no body.
        - in: query
          name: page
          schema: { type: integer, minimum: 1, default: 1 }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, default: 50 }
          description: >-
            Seitengröße. Wird auf MAX_LIMIT (200) gedeckelt: zu große Werte
            liefern eine 200er-Seite mit 200 Einträgen statt eines Fehlers.
          x-description-en: >-
            Page size. Capped at MAX_LIMIT (200): larger values return a 200 page
            of 200 items instead of an error.
        - in: query
          name: offset
          schema: { type: integer, minimum: 0, default: 0 }
          description: Ein zu großer Offset liefert eine leere Seite (200), nie 500.
          x-description-en: An offset past the end returns an empty page (200), never 500.
        - in: query
          name: sort
          schema: { type: string, enum: [source, enabled, license] }
          description: >-
            Nur die gelisteten Felder sind erlaubt; ein unbekanntes Feld wird mit
            400 (invalid_request) abgewiesen, bevor es ausgewertet wird.
          x-description-en: >-
            Only the listed fields are allowed; an unknown field is rejected with
            400 (invalid_request) before it is evaluated.
        - in: query
          name: order
          schema: { type: string, enum: [asc, desc], default: asc }
  /api/v1/compare:
    get:
      tags: [compare]
      summary: Eine Ressource über mehrere Städte vergleichen
      x-summary-en: Compare one resource across multiple cities
      description: >-
        Vergleicht EINE Ressource (resource) über mehrere Städte (cities,
        kommasepariert) in einer einzigen Antwort. Jede Stadt trägt einen eigenen
        source_status (ok/disabled/no_data/error/not_found); eine fehlende oder
        tote Stadt-Quelle führt nicht zu einem Gesamt-5xx, sondern wird pro Stadt
        ehrlich ausgewiesen. resource wird gegen eine feste Liste geprüft
        (unbekannt -> 400). Neben weather/air sind auch indicators,
        demographics, unemployment, tourism, charging-status und
        weather-warnings vergleichbar. Das Ergebnis ist paginierbar
        (page/limit/offset/sort).
      x-description-en: >-
        Compares ONE resource across multiple cities (comma-separated) in a
        single response. Each city carries its own source_status
        (ok/disabled/no_data/error/not_found); a missing or dead city source does
        not cause an overall 5xx but is reported honestly per city. resource is
        checked against a fixed list (unknown -> 400). Besides weather/air,
        indicators, demographics, unemployment, tourism, charging-status and
        weather-warnings can be compared as well. The result is paginated
        (page/limit/offset/sort).
      operationId: compareCities
      parameters:
        - in: query
          name: cities
          required: true
          schema: { type: string }
          example: berlin,koeln,hamburg
          description: Kommaseparierte Stadt-Slugs (auf MAX_CITIES begrenzt).
          x-description-en: Comma-separated city slugs (limited to MAX_CITIES).
        - in: query
          name: resource
          required: true
          schema: { type: string, enum: [weather, air, indicators, demographics, unemployment, tourism, charging-status, weather-warnings] }
          description: >-
            Zu vergleichende Ressource; unbekannt -> 400 invalid_request.
            weather/air = Live-Adapter (DWD/UBA); indicators, demographics,
            unemployment, tourism, charging-status und weather-warnings
            delegieren an die jeweiligen Stadt-Endpunkte (identisches
            Verhalten inkl. Attribution). charging-status liefert im Compare
            nur die Aggregate (points_omitted statt Einzelpunkt-Liste).
          x-description-en: >-
            Resource to compare; unknown -> 400 invalid_request. weather/air =
            live adapters (DWD/UBA); indicators, demographics, unemployment,
            tourism, charging-status and weather-warnings delegate to the
            per-city endpoints (identical behaviour incl. attribution).
            charging-status returns aggregates only in compare
            (points_omitted instead of the point list).
        - in: query
          name: page
          schema: { type: integer, minimum: 1, default: 1 }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, default: 50 }
        - in: query
          name: offset
          schema: { type: integer, minimum: 0, default: 0 }
        - in: query
          name: sort
          schema: { type: string, enum: [city, source_status] }
        - in: query
          name: order
          schema: { type: string, enum: [asc, desc], default: asc }
        - in: header
          name: If-None-Match
          required: false
          schema: { type: string }
          description: >-
            Conditional GET; If-None-Match == ETag -> 304 Not Modified ohne Body.
          x-description-en: >-
            Conditional GET; If-None-Match == ETag -> 304 Not Modified with no body.
      responses:
        "200":
          description: Vergleichsliste mit per-Stadt source_status
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
            Cache-Control:
              $ref: "#/components/headers/CacheControl"
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      required: [city, source_status]
                      properties:
                        city: { type: string }
                        data: { type: object, nullable: true }
                        source_status:
                          type: string
                          enum: [ok, disabled, no_data, error, not_found]
                  meta:
                    $ref: "#/components/schemas/Meta"
        "304":
          $ref: "#/components/responses/NotModified"
        "400":
          description: Unbekannte resource oder leeres cities (invalid_request)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/openapi.yaml:
    get:
      tags: [meta]
      summary: Diese OpenAPI-Spec als YAML
      x-summary-en: This OpenAPI spec as YAML
      description: >-
        Liefert die handgepflegte Spec als application/yaml. FastAPI serviert
        intern nur /openapi.json; diese Route stellt die stabile YAML-Variante
        bereit.
      x-description-en: >-
        Returns the hand-maintained spec as application/yaml. FastAPI internally
        serves only /openapi.json; this route provides the stable YAML variant.
      operationId: getOpenapiYaml
      responses:
        "200":
          description: Die Spec als YAML
          content:
            application/yaml:
              schema:
                type: string
  /api/v1/cities:
    get:
      tags: [cities]
      summary: Alle bekannten Städte auflisten
      x-summary-en: List all known cities
      description: >-
        Listet die Einträge des Stadt-Registers: 84 deutsche Großstädte über
        100.000 Einwohner, davon 28 Kern-Städte mit voller Quellen-Abdeckung.
        Jeder Eintrag trägt den Slug, den du als {slug}-Pfadparameter in allen
        weiteren Endpunkten verwendest.
      x-description-en: >-
        Lists the entries of the city register: 84 German cities with more than
        100,000 inhabitants, 28 of them core cities with full source coverage.
        Each entry carries the slug you use as the {slug} path parameter in all
        other endpoints.
      operationId: getCities
      responses:
        "200":
          description: Stadt-Liste
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items:
                      type: object
                  meta:
                    $ref: "#/components/schemas/Meta"
  /api/v1/cities/{slug}:
    get:
      tags: [cities]
      summary: Eine Stadt aus dem Register holen
      x-summary-en: Get a single city from the register
      description: >-
        Liefert den Register-Eintrag zur Stadt mit dem angegebenen {slug}. Ein
        unbekannter Slug löst den 404-Fehler-Envelope aus, dessen Hint auf die
        Stadt-Liste verweist.
      x-description-en: >-
        Returns the register entry for the city with the given {slug}. An unknown
        slug triggers the 404 error envelope whose hint points to the city list.
      operationId: getCity
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Stadt-Eintrag
          content:
            application/json:
              schema:
                type: object
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/base:
    get:
      tags: [cities]
      summary: Stadt-Stammdaten aus Wikidata
      x-summary-en: City master data from Wikidata
      description: >-
        Liefert Einwohnerzahl, Fläche und Geo-Koordinaten einer Stadt aus
        Wikidata im kanonischen Envelope mit Attribution und Lizenz-Tag. Ist die
        Quelle deaktiviert, antwortet die Route mit 200, data=null und
        meta.source_status="disabled" (nie 5xx). Ist der Upstream tot und liegt
        kein zwischengespeicherter Wert vor, kommt 503 mit einem Hint auf
        GET /api/v1/health.
      x-description-en: >-
        Returns a city's population, area and geo coordinates from Wikidata in
        the canonical envelope with attribution and license tag. If the source
        is disabled, the route responds with 200, data=null and
        meta.source_status="disabled" (never 5xx). If the upstream is down and no
        cached value exists, it returns 503 with a hint pointing to
        GET /api/v1/health.
      operationId: getCityBase
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: >-
            Stammdaten-Envelope. data ist der CanonicalRecord oder null, wenn die
            Quelle deaktiviert ist (meta.source_status="disabled").
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    allOf:
                      - $ref: "#/components/schemas/Meta"
                    description: >-
                      meta trägt zusätzlich source_status ("ok"|"disabled") und
                      auf dem ok-Pfad cache_status (HIT/MISS/STALE/STALE-ON-ERROR).
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/air:
    get:
      tags: [cities]
      summary: Luftqualität je Stadt (Umweltbundesamt)
      x-summary-en: Air quality per city (Federal Environment Agency)
      description: >-
        Aktuelle Luftqualitäts-Messwerte einer Stadt aus der Luftdaten-API des
        Umweltbundesamts im kanonischen Envelope. Die fünf Schadstoff-Messwerte
        (PM10, PM2.5, NO2, O3, SO2) stehen in data, Quelle und source_status in
        meta. Werte sind live (kein Zwischenbestand). Sonderfälle: deaktivierte
        Quelle liefert 200 mit data=null und source_status="disabled"; toter
        Upstream ohne Cache liefert 503 mit Hint auf GET /api/v1/health.
      x-description-en: >-
        Current air-quality readings for a city from the German Federal
        Environment Agency (UBA) air-data API in the canonical envelope. The five
        pollutant readings (PM10, PM2.5, NO2, O3, SO2) are in data, source and
        source_status in meta. Values are live (no intermediate store). Special
        cases: a disabled source returns 200 with data=null and
        source_status="disabled"; a dead upstream with no cache returns 503 with
        a hint to GET /api/v1/health.
      operationId: getCityAir
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Luftqualitäts-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/overview:
    get:
      tags: [cities]
      summary: Stadt-Überblick (Katalog aller Datenarten + Live-Highlights)
      x-summary-en: City overview (catalog of all data types + live highlights)
      description: >-
        Ein-Aufruf-Überblick einer Stadt: Basisdaten, ein Katalog ALLER
        verfügbaren Datenarten (je mit Abdeckungsstatus und dem passenden
        MCP-Tool) sowie ein kleiner Live-Highlight-Snapshot (Wetter, Luft,
        Bahn-Abfahrten), parallel und zeitgedeckelt. Ideal als Einstieg, um die ganze Breite je
        Stadt zu sehen (nicht nur Wetter). Eine noch nicht abgedeckte Datenart
        wird ehrlich, aber vorwärts gewandt dargestellt (sie nennt die bereits
        abgedeckten Städte plus einen Roadmap-Hinweis, weil InfraNode laufend
        mehr Daten und Städte bekommt). Unbekannte Stadt liefert 404.
      x-description-en: >-
        One-call overview of a city: base data, a catalog of ALL available data
        types (each with its coverage status and the matching MCP tool), and a
        small live highlights snapshot (weather + air), fetched in parallel and
        time-capped. Ideal as an entry point to see the full breadth per city,
        not just weather. A data type that is not yet covered is shown honestly
        but forward-looking (it lists the cities where it IS available plus a
        roadmap note, because InfraNode keeps adding more data types and cities).
        An unknown city returns 404.
      operationId: getCityOverview
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Überblick-Envelope (Basis + Katalog + Highlights + Summary)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: object
                    properties:
                      city:
                        type: object
                        description: Basisdaten der Stadt (slug, Lage, Einwohner).
                      data_types:
                        type: array
                        description: >-
                          Katalog aller Datenarten je mit type, label, label_en,
                          tool (MCP-Tool), path und available; nicht abgedeckte
                          tragen zusätzlich covered_cities und einen note-Hinweis.
                        items:
                          type: object
                      highlights:
                        type: object
                        description: >-
                          Live-Snapshot je Highlight-Quelle (weather, air) mit
                          eigenem source_status (ok/no_data/disabled/error).
                      summary:
                        type: object
                        description: >-
                          data_types_total, data_types_available, cities_total,
                          eine headline (macht die verfügbare Breite in einem Satz
                          greifbar) und ein Roadmap-Hinweis (note).
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/weather:
    get:
      tags: [cities]
      summary: Wetter je Stadt (DWD über Bright Sky)
      x-summary-en: Weather per city (DWD via Bright Sky)
      description: >-
        Aktuelle Wetter-Messwerte einer Stadt vom Deutschen Wetterdienst (über
        Bright Sky) im kanonischen Envelope. data trägt die Messwerte,
        attribution den GeoNutzV-Bearbeitungshinweis (modified=true).
        Sonderfälle: deaktivierte Quelle liefert 200 mit
        source_status="disabled"; toter Upstream ohne Cache liefert 503 mit Hint
        auf GET /api/v1/health.
      x-description-en: >-
        Current weather readings for a city from the German Weather Service (DWD,
        via Bright Sky) in the canonical envelope. data holds the readings,
        attribution carries the GeoNutzV modification notice (modified=true).
        Special cases: a disabled source returns 200 with
        source_status="disabled"; a dead upstream with no cache returns 503 with
        a hint to GET /api/v1/health.
      operationId: getCityWeather
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Wetter-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/pois:
    get:
      tags: [cities]
      summary: POIs je Stadt, nach Typ gefiltert (OSM/Overpass, Tier B)
      x-summary-en: POIs per city, filtered by type (OSM/Overpass, Tier B)
      description: >-
        Liefert POIs einer Stadt aus OpenStreetMap (über Overpass) im
        kanonischen Envelope, gefiltert nach dem Query-Parameter type. Der Typ
        wird gegen eine feste Liste geprüft; ein unbekannter Typ löst 422 aus
        (kein roher Input in die Overpass-Query). Tier B (copyleft).
        Deaktiviert -> 200 source_status="disabled".
      x-description-en: >-
        Returns a city's POIs from OpenStreetMap (via Overpass) in the canonical
        envelope, filtered by the type query parameter. The type is checked
        against a fixed list; an unknown type triggers 422 (no raw input reaches
        the Overpass query). Tier B (copyleft). Disabled -> 200
        source_status="disabled".
      operationId: getCityPois
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
        - name: type
          in: query
          required: true
          description: POI-Typ aus der erlaubten Liste (z.B. hospital, school, pharmacy).
          x-description-en: POI type from the allowed list (e.g. hospital, school, pharmacy).
          schema:
            type: string
      responses:
        "200":
          description: POI-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Unbekannter POI-Typ (nicht in der Whitelist)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/playgrounds:
    get:
      tags: [cities]
      summary: Spielplätze je Stadt (OSM, Tier B)
      x-summary-en: Playgrounds per city (OSM, Tier B)
      description: >-
        Öffentliche Spielplätze (OSM leisure=playground) im kanonischen Envelope. Tier B (copyleft, ODbL).
        Deaktiviert -> 200 source_status="disabled".
      x-description-en: >-
        Public playgrounds (OSM leisure=playground) in the canonical envelope. Tier B (copyleft, ODbL).
        Disabled -> 200 source_status="disabled".
      operationId: getCityPlaygrounds
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OSM-Feature-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/drinking-water:
    get:
      tags: [cities]
      summary: Trinkwasserbrunnen je Stadt (OSM, Tier B)
      x-summary-en: Public drinking water fountains per city (OSM, Tier B)
      description: >-
        Öffentliche Trinkwasserbrunnen (OSM amenity=drinking_water). Die OSM-Abdeckung ist je Stadt unterschiedlich vollständig. Tier B (ODbL).
        Deaktiviert -> 200 source_status="disabled".
      x-description-en: >-
        Public drinking water fountains (OSM amenity=drinking_water). OSM coverage varies per city. Tier B (ODbL).
        Disabled -> 200 source_status="disabled".
      operationId: getCityDrinkingWater
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OSM-Feature-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/public-toilets:
    get:
      tags: [cities]
      summary: Öffentliche Toiletten je Stadt (OSM, Tier B)
      x-summary-en: Public toilets per city (OSM, Tier B)
      description: >-
        Öffentliche Toiletten (OSM amenity=toilets) mit Barrierefreiheits-Tags
        je Element (wheelchair, changing_table) sowie fee/access/opening_hours/
        unisex. Die OSM-Abdeckung ist je Stadt unterschiedlich vollständig.
        Tier B (ODbL). Deaktiviert -> 200 source_status="disabled".
      x-description-en: >-
        Public toilets (OSM amenity=toilets) with per-item accessibility tags
        (wheelchair, changing_table) plus fee/access/opening_hours/unisex. OSM
        coverage varies per city. Tier B (ODbL). Disabled -> 200
        source_status="disabled".
      operationId: getCityPublicToilets
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OSM-Feature-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/markets:
    get:
      tags: [cities]
      summary: Wochenmärkte/Marktplätze je Stadt (OSM, Tier B)
      x-summary-en: Markets per city (OSM, Tier B)
      description: >-
        Marktplätze (OSM amenity=marketplace). Markttage/Zeiten kommen als optionales opening_hours je Element (häufig leer). Tier B (ODbL).
        Deaktiviert -> 200 source_status="disabled".
      x-description-en: >-
        Marketplaces (OSM amenity=marketplace). Market days/times come as optional opening_hours per element (often empty). Tier B (ODbL).
        Disabled -> 200 source_status="disabled".
      operationId: getCityMarkets
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OSM-Feature-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/parcel-lockers:
    get:
      tags: [cities]
      summary: Paketstationen je Stadt (OSM, Tier B)
      x-summary-en: Parcel lockers per city (OSM, Tier B)
      description: >-
        Paketstationen/Locker (OSM amenity=parcel_locker) mit optionalem operator/brand (DHL/Amazon/DPD/Hermes/GLS). Tier B (ODbL).
        Deaktiviert -> 200 source_status="disabled".
      x-description-en: >-
        Parcel lockers (OSM amenity=parcel_locker) with optional operator/brand (DHL/Amazon/DPD/Hermes/GLS). Tier B (ODbL).
        Disabled -> 200 source_status="disabled".
      operationId: getCityParcelLockers
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OSM-Feature-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/post-offices:
    get:
      tags: [cities]
      summary: Postfilialen je Stadt (OSM, Tier B)
      x-summary-en: Post offices per city (OSM, Tier B)
      description: >-
        Postfilialen (OSM amenity=post_office) mit optionalem opening_hours/operator. Tier B (ODbL).
        Deaktiviert -> 200 source_status="disabled".
      x-description-en: >-
        Post offices (OSM amenity=post_office) with optional opening_hours/operator. Tier B (ODbL).
        Disabled -> 200 source_status="disabled".
      operationId: getCityPostOffices
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OSM-Feature-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/post-boxes:
    get:
      tags: [cities]
      summary: Briefkästen je Stadt, mit Leerungszeiten (OSM, Tier B)
      x-summary-en: Post boxes per city, with collection times (OSM, Tier B)
      description: >-
        Öffentliche Briefkästen (OSM amenity=post_box). Leerungszeiten als optionales collection_times je Element (ca. drei Viertel getaggt; fehlend = Datenpunkt-Lücke). Tier B (ODbL).
        Deaktiviert -> 200 source_status="disabled".
      x-description-en: >-
        Public post boxes (OSM amenity=post_box). Collection times as optional collection_times per element (~75% tagged; missing = data gap). Tier B (ODbL).
        Disabled -> 200 source_status="disabled".
      operationId: getCityPostBoxes
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OSM-Feature-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/public-wifi:
    get:
      tags: [cities]
      summary: Öffentliche WLAN-Standorte je Stadt (OSM, Tier B)
      x-summary-en: Public Wi-Fi locations per city (OSM, Tier B)
      description: >-
        Öffentliche WLAN-Standorte (OSM internet_access=wlan) mit optionalem operator. Tier B (ODbL).
        Deaktiviert -> 200 source_status="disabled".
      x-description-en: >-
        Public Wi-Fi locations (OSM internet_access=wlan) with optional operator. Tier B (ODbL).
        Disabled -> 200 source_status="disabled".
      operationId: getCityPublicWifi
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OSM-Feature-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/recycling-centres:
    get:
      tags: [cities]
      summary: Recycling-/Wertstoffhöfe je Stadt (OSM, Tier B)
      x-summary-en: Recycling centres per city (OSM, Tier B)
      description: >-
        Recycling-/Wertstoffhöfe (OSM amenity=recycling + recycling_type=centre) mit optionalem opening_hours. Tier B (ODbL).
        Deaktiviert -> 200 source_status="disabled".
      x-description-en: >-
        Recycling centres (OSM amenity=recycling + recycling_type=centre) with optional opening_hours. Tier B (ODbL).
        Disabled -> 200 source_status="disabled".
      operationId: getCityRecyclingCentres
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OSM-Feature-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/government-offices:
    get:
      tags: [cities]
      summary: Behörden/Ämter je Stadt (OSM, Tier B)
      x-summary-en: Government offices per city (OSM, Tier B)
      description: >-
        Behörden/Ämter (OSM office=government + amenity=townhall), konsolidiert Bürgerämter, Verwaltungs- und sonstige Ämter. Subtyp je Element als optionales government-Tag. Tier B (ODbL).
        Deaktiviert -> 200 source_status="disabled".
      x-description-en: >-
        Government offices (OSM office=government + amenity=townhall), consolidating citizen, administrative and other offices. Subtype per element as optional government tag. Tier B (ODbL).
        Disabled -> 200 source_status="disabled".
      operationId: getCityGovernmentOffices
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OSM-Feature-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/education:
    get:
      tags: [cities]
      summary: Bildungseinrichtungen je Stadt (OSM, Tier B)
      x-summary-en: Education facilities per city (OSM, Tier B)
      description: >-
        Bildungseinrichtungen (OSM amenity=school/college/university/kindergarten). Tier B (ODbL).
        Deaktiviert -> 200 source_status="disabled".
      x-description-en: >-
        Education facilities (OSM amenity=school/college/university/kindergarten). Tier B (ODbL).
        Disabled -> 200 source_status="disabled".
      operationId: getCityEducation
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OSM-Feature-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/population-density:
    get:
      tags: [cities]
      summary: Einwohnerdichte je Stadt (Zensus-2022-Gitter, Tier A)
      x-summary-en: Population density per city (Census 2022 grid, Tier A)
      description: >-
        Einwohnerdichte einer Stadt aus dem amtlichen Zensus-2022-100m-Gitter,
        exakt aggregiert über die Gitterzellen mit der Stadt-AGS: Summe Einwohner,
        Zahl bewohnter 100m-Zellen, bewohnte Fläche (km²) und Dichte (Einwohner/km²
        über die bewohnte Fläche, nicht die Gesamtfläche). Flächendeckend (alle
        Städte); ohne Gitterzellen -> 200 source_status="no_data". DL-DE/BY 2.0.
        Deaktiviert -> 200 source_status="disabled".
      x-description-en: >-
        A city's population density from the official Census 2022 100m grid,
        aggregated exactly over the grid cells with the city's AGS: total
        inhabitants, populated 100m cells, populated area (km²) and density
        (inhabitants/km² over the populated area, not the total area). Full
        coverage (all cities); no grid cells -> 200 source_status="no_data".
        DL-DE/BY 2.0. Disabled -> 200 source_status="disabled".
      operationId: getCityPopulationDensity
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Einwohnerdichte-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/public-tenders:
    get:
      tags: [cities]
      summary: Öffentliche Auftragsvergabe je Stadt (oeffentlichevergabe.de, Tier A)
      x-summary-en: Public procurement notices per city (oeffentlichevergabe.de, Tier A)
      description: >-
        Öffentliche Auftragsbekanntmachungen einer Stadt aus dem
        Bekanntmachungsdienst des Bundes (oeffentlichevergabe.de, OCDS): laufende
        Vergabeverfahren und entschiedene Verfahren, je Bekanntmachung Titel,
        Status, Auftraggeber, Wert und Quell-Link; bei entschiedenen Verfahren
        zusaetzlich die Auftragnehmer (suppliers, Namensliste; leer wenn die
        Quelle nicht offenlegt) und der tatsaechlich vergebene Auftragswert
        (award_value/award_currency, getrennt vom Ausschreibungswert value).
        status wird semantisch aus dem
        notice_type abgeleitet: active = laufendes Vergabeverfahren
        (Auftragsbekanntmachung), complete = entschiedenes Verfahren inkl.
        aufgehobener Vergaben; der rohe Zuschlag-Status steht im Feld
        award_status (active/pending/unsuccessful/null). Betraege <= 0 aus der Quelle werden als nicht angegeben (null) gewertet. Die Angebotsfrist
        (deadline) fehlt im Quell-Export fast immer und steht dann nur in der
        Original-Bekanntmachung über source_url. Read-only aus dem deduplizierten
        Store (kein Live-Pull im Request-Pfad). Filter status (active/complete)
        und match (buyer_city/place_of_performance), mit Pagination
        (limit/offset). Keine Bekanntmachungen -> 200 source_status="no_data",
        data=null (ehrlich). CC0 (Tier A). Deaktiviert -> 200
        source_status="disabled".
      x-description-en: >-
        A city's public procurement notices from the German federal procurement
        publication service (oeffentlichevergabe.de, OCDS): running procedures and
        decided procedures, each with title, status, buyer, value and a source
        link; decided procedures additionally carry the contractors (suppliers,
        list of names; empty when the source does not disclose them) and the
        actually awarded value (award_value/award_currency, separate from the
        tender estimate value). status is derived semantically from the notice_type: active =
        running procedure (contract notice), complete = decided procedure incl.
        cancelled awards; the raw award status is in the award_status field
        (active/pending/unsuccessful/null). Amounts of 0 or less from the source
        are treated as not stated (null). The submission deadline is almost
        always missing in the source export and then only found in the original
        notice via source_url. Read-only from the deduplicated store (no live pull
        in the request path). Filter by status (active/complete) and match
        (buyer_city/place_of_performance), with pagination (limit/offset). No
        notices -> 200 source_status="no_data", data=null (honest). CC0 (Tier A).
        Disabled -> 200 source_status="disabled".
      operationId: getCityPublicTenders
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
        - name: status
          in: query
          required: false
          description: >-
            Verfahrens-Status (active = laufendes Vergabeverfahren, complete =
            entschiedenes Verfahren inkl. aufgehobener Vergaben; Detail im Feld
            award_status).
          schema:
            type: string
            enum: [active, complete]
        - name: match
          in: query
          required: false
          description: >-
            Stadt-Zuordnung (buyer_city = Sitz des Amts, place_of_performance =
            Erfüllungsort).
          schema:
            type: string
            enum: [buyer_city, place_of_performance]
        - name: q
          in: query
          required: false
          description: Titel-Stichwort (Teilstring, parametrisierter LIKE-Filter).
          schema:
            type: string
        - name: since
          in: query
          required: false
          description: >-
            Nur Bekanntmachungen ab diesem ISO-Datum YYYY-MM-DD; ungültiges Datum
            -> 422.
          schema:
            type: string
            format: date
        - name: limit
          in: query
          required: false
          description: Seitengröße (Default 50, Cap 200).
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: offset
          in: query
          required: false
          description: Seiten-Offset (>= 0); Overflow -> leere Seite, no_data.
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: >-
            Auftragsvergabe-Envelope (aggregierte notices-Liste + count, oder null
            bei no_data/disabled)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - type: object
                        properties:
                          notices:
                            type: array
                            items:
                              type: object
                          count:
                            type: integer
                          license_id:
                            type: string
                            description: Lizenz des Datensatzes (CC0 = Tier A).
                          license_tier:
                            type: string
                          attribution:
                            type: object
                            properties:
                              text:
                                type: string
                              license_url:
                                type: string
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Ungültiger Query-Parameter (status/match/q/since/limit/offset)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/council-papers:
    get:
      tags: [cities]
      summary: Kommunale Ratsinformationen je Stadt (OParl, Tier A)
      x-summary-en: Municipal council information per city (OParl, Tier A)
      description: >-
        Kommunale Ratsinformationen einer Stadt aus dem OParl-1.x-Standard
        (OParl "Paper" = Vorlagen, Anträge, Beschlüsse): je Paper Titel, Referenz,
        Datum, Paper-Typ und der Link zur Hauptdatei (main_file_url, das PDF wird
        NICHT gespiegelt). Abgedeckt sind die fünf lizenzgeklärten Städte Dresden,
        Köln, Düsseldorf, Münster und Leipzig (je eigene Lizenz in der
        attribution). Read-only aus dem deduplizierten Store (kein Live-Pull im
        Request-Pfad). Filter q (Titel-Stichwort), paper_type und since (ab
        ISO-Datum), mit Pagination (limit/offset). Nicht abgedeckte Stadt -> 404
        mit Hint. Keine Paper -> 200 source_status="no_data", data=null.
        Deaktiviert -> 200 source_status="disabled". Zusätzlich unter dem Alias
        /api/v1/cities/{slug}/council/papers erreichbar.
      x-description-en: >-
        A city's municipal council information from the OParl 1.x standard (OParl
        "Paper" = council papers, motions, resolutions): each paper with title,
        reference, date, paper type and a link to the main file (main_file_url; the
        PDF is NOT mirrored). Covered are the five licence-cleared cities Dresden,
        Cologne, Düsseldorf, Münster and Leipzig (each with its own licence in the
        attribution). Read-only from the deduplicated store (no live pull in the
        request path). Filter by q (title keyword), paper_type and since (from an
        ISO date), with pagination (limit/offset). Not-covered city -> 404 with
        hint. No papers -> 200 source_status="no_data", data=null. Disabled -> 200
        source_status="disabled". Also available under the alias
        /api/v1/cities/{slug}/council/papers.
      operationId: getCityCouncilPapers
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
        - name: q
          in: query
          required: false
          description: Titel-Stichwort (Teilstring, parametrisierter LIKE-Filter).
          schema:
            type: string
        - name: paper_type
          in: query
          required: false
          description: Exakter Paper-Typ (z. B. Beschlussvorlage, Antrag).
          schema:
            type: string
        - name: since
          in: query
          required: false
          description: >-
            Nur Paper ab diesem ISO-Datum YYYY-MM-DD; ungültiges Datum -> 422.
          schema:
            type: string
            format: date
        - name: limit
          in: query
          required: false
          description: Seitengröße (Default 50, Cap 100).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: offset
          in: query
          required: false
          description: Seiten-Offset (>= 0); Overflow -> leere Seite, no_data.
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: >-
            Ratsinformations-Envelope (aggregierte papers-Liste + count, oder null
            bei no_data/disabled)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - type: object
                        properties:
                          papers:
                            type: array
                            items:
                              type: object
                          count:
                            type: integer
                          license_id:
                            type: string
                            description: Lizenz des Datensatzes je Stadt (Tier A).
                          license_tier:
                            type: string
                          attribution:
                            type: object
                            properties:
                              text:
                                type: string
                              license_url:
                                type: string
                              source:
                                type: string
                              modified:
                                type: boolean
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte oder nicht abgedeckte Stadt (mit Hint)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Ungültiger Query-Parameter (q/paper_type/since/limit/offset)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/tenders:
    get:
      tags: [cities]
      summary: Öffentliche Auftragsvergabe deutschlandweit (oeffentlichevergabe.de, Tier A)
      x-summary-en: Public procurement notices nationwide (oeffentlichevergabe.de, Tier A)
      description: >-
        Öffentliche Auftragsbekanntmachungen über ALLE Städte im Store aus dem
        Bekanntmachungsdienst des Bundes (oeffentlichevergabe.de, OCDS): sucht
        deutschlandweit, ohne jede Stadt einzeln abzufragen. Je Bekanntmachung
        Titel, Status, Auftraggeber, Wert und Quell-Link. status wird semantisch
        aus dem notice_type abgeleitet: active = laufendes Vergabeverfahren,
        complete = entschiedenes Verfahren inkl. aufgehobener Vergaben; der rohe
        Zuschlag-Status steht im Feld award_status. Entschiedene Verfahren tragen
        zusaetzlich Auftragnehmer (suppliers) und vergebenen Auftragswert
        (award_value/award_currency); Betraege <= 0 aus der Quelle werden als
        nicht angegeben (null) gewertet. Die Angebotsfrist (deadline)
        fehlt im Quell-Export fast immer und steht dann nur in der
        Original-Bekanntmachung über source_url. Read-only aus dem deduplizierten
        Store (kein Live-Pull im Request-Pfad). Filter q (Titel-Stichwort), status
        (active/complete) und since (ab ISO-Datum), mit Pagination (limit/offset).
        Keine Bekanntmachungen -> 200 source_status="no_data", data=null
        (ehrlich). CC0 (Tier A). Deaktiviert -> 200 source_status="disabled".
      x-description-en: >-
        Public procurement notices across ALL cities in the store from the German
        federal procurement publication service (oeffentlichevergabe.de, OCDS):
        searches nationwide without querying each city individually. Each notice
        with title, status, buyer, value and a source link. status is derived
        semantically from the notice_type: active = running procedure, complete =
        decided procedure incl. cancelled awards; the raw award status is in the
        award_status field. Decided procedures additionally carry contractors
        (suppliers) and the awarded value (award_value/award_currency); amounts of
        0 or less from the source are treated as not stated (null). The
        submission deadline is almost always missing in
        the source export and then only found in the original notice via
        source_url. Read-only from the deduplicated store (no live pull in the
        request path). Filter by q (title keyword), status (active/complete) and
        since (from ISO date), with pagination (limit/offset). No notices -> 200
        source_status="no_data", data=null (honest). CC0 (Tier A). Disabled -> 200
        source_status="disabled".
      operationId: getAllPublicTenders
      parameters:
        - name: q
          in: query
          required: false
          description: Titel-Stichwort (Teilstring, parametrisierter LIKE-Filter).
          schema:
            type: string
        - name: status
          in: query
          required: false
          description: >-
            Verfahrens-Status (active = laufendes Vergabeverfahren, complete =
            entschiedenes Verfahren inkl. aufgehobener Vergaben; Detail im Feld
            award_status).
          schema:
            type: string
            enum: [active, complete]
        - name: since
          in: query
          required: false
          description: >-
            Nur Bekanntmachungen ab diesem ISO-Datum YYYY-MM-DD; ungültiges Datum
            -> 422.
          schema:
            type: string
            format: date
        - name: limit
          in: query
          required: false
          description: Seitengröße (Default 50, Cap 200).
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: offset
          in: query
          required: false
          description: Seiten-Offset (>= 0); Overflow -> leere Seite, no_data.
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: >-
            Auftragsvergabe-Envelope (aggregierte notices-Liste + count, oder null
            bei no_data/disabled)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - type: object
                        properties:
                          notices:
                            type: array
                            items:
                              type: object
                          count:
                            type: integer
                          license_id:
                            type: string
                            description: Lizenz des Datensatzes (CC0 = Tier A).
                          license_tier:
                            type: string
                          attribution:
                            type: object
                            properties:
                              text:
                                type: string
                              license_url:
                                type: string
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "422":
          description: Ungültiger Query-Parameter (status/q/since/limit/offset)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/tree-cadastre:
    get:
      tags: [cities]
      summary: Baumkataster je Stadt (kommunaler WFS, Tier A)
      x-summary-en: Street-tree cadastre per city (municipal WFS, Tier A)
      description: >-
        Das städtische Baumkataster (Straßenbäume) aus dem kommunalen WFS, je Baum
        Punkt plus Attribute (Art, Pflanzjahr, Höhe, Straße, Bezirk). Kataster sind
        sehr groß: die Antwort ist eine gedeckelte Stichprobe (count = ausgelieferte
        Bäume, nicht der Gesamtbestand). Teilabdeckung (kommunal, nur Städte mit
        verifiziertem offenem WFS, aktuell Berlin, DL-DE/Zero 2.0); andere Städte
        antworten mit 200 source_status="not_covered" + meta.covered_cities.
        Deaktiviert -> 200 source_status="disabled".
      x-description-en: >-
        A city's street-tree cadastre from the municipal WFS, each tree as a point
        plus attributes (species, planting year, height, street, district). Registers
        are very large: the response is a capped sample (count = returned trees, not
        the full stock). Partial coverage (municipal, only cities with a verified
        open WFS, currently Berlin, DL-DE/Zero 2.0); other cities return 200
        source_status="not_covered" + meta.covered_cities. Disabled -> 200
        source_status="disabled".
      operationId: getCityTreeCadastre
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Baumkataster-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/heritage:
    get:
      tags: [cities]
      summary: Denkmäler/Baudenkmale je Stadt (Land-WFS, Tier A)
      x-summary-en: Heritage/listed monuments per city (state WFS, Tier A)
      description: >-
        Bau- und Denkmalobjekte einer Stadt aus der Denkmalliste des jeweiligen
        Bundeslandes (WFS), je Objekt ein Repräsentativpunkt plus Typ/Link.
        Denkmalschutz ist Ländersache: Teilabdeckung, nur Städte in Ländern mit
        verifiziertem offenem WFS (Berlin DL-DE/Zero 2.0; Hamburg, Baden-Württemberg, Hessen DL-DE/BY 2.0, Flächenländer per Stadt-Umkreis eingegrenzt). Andere Städte
        antworten ehrlich mit 200 source_status="not_covered", data=null und
        meta.covered_cities. Deaktiviert -> 200 source_status="disabled".
      x-description-en: >-
        A city's listed/heritage objects from the respective federal state's
        heritage register (WFS), each as a representative point plus type/link.
        Heritage protection is a state matter: partial coverage, only cities in
        states with a verified open WFS (Berlin DL-DE/Zero 2.0; Hamburg, Baden-Württemberg, Hesse DL-DE/BY 2.0, area states narrowed to a city radius). Other
        cities honestly return 200 source_status="not_covered", data=null and
        meta.covered_cities. Disabled -> 200 source_status="disabled".
      operationId: getCityHeritage
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Denkmal-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/traffic:
    get:
      tags: [cities]
      summary: Baustellen und Verkehrsmeldungen je Stadt (Autobahn-API)
      x-summary-en: Roadworks and traffic warnings per city (Autobahn API)
      description: >-
        Baustellen (roadworks) und Verkehrswarnungen (warnings) im Umfeld einer
        Stadt aus der Autobahn-API im kanonischen Envelope. Die Meldungen werden
        per Bounding-Box um die Stadt-Koordinaten gefiltert, data enthält die
        passenden Einträge. Flächendeckend: jeder der 84 Städte sind die nahen
        Autobahnen kuratiert zugeordnet (datengetrieben aus der Autobahn-API),
        eine Stadt ohne aktuelle Meldung antwortet ehrlich mit no_data.
        Stau/Verkehrslage:
        jede Verkehrswarnung trägt zusätzlich ein congestion-Feld (level
        stau/stockend/dicht/unspezifisch aus dem DATEX-abnormalTrafficType,
        delay_minutes = Reisezeitverlust, blocked), und payload.congestion_summary
        verdichtet die Stau-Lage je Stadt (count/stau/stockend/blocked/
        max_delay_minutes; null wenn keine Stau-Meldung). Geometrie: die Antwort
        ist per Default schlank (die Roh-Polyline je Meldung ist weggelassen); mit
        include=geometry (oder ?full=1) bleibt das geometry-Feld in roadworks und
        warnings erhalten. Weitere Sonderfälle:
        deaktivierte Quelle liefert 200 mit source_status="disabled"; toter
        Upstream ohne Cache liefert 503 mit Hint auf GET /api/v1/health.
      x-description-en: >-
        Roadworks and traffic warnings around a city from the Autobahn API in the
        canonical envelope. Items are filtered by a bounding box around the
        city's coordinates; data contains the matching entries. Full coverage:
        each of the 84 cities has its nearby motorways curated (data-driven from
        the Autobahn API); a city with no current message honestly responds with
        no_data. Congestion: each traffic warning additionally carries a
        congestion field (level stau/stockend/dicht/unspezifisch derived from the
        DATEX abnormalTrafficType, delay_minutes = travel-time loss, blocked), and
        payload.congestion_summary aggregates the congestion situation per city
        (count/stau/stockend/blocked/max_delay_minutes; null if no congestion
        message). Other special cases: a disabled source returns 200 with
        source_status="disabled"; a dead upstream with no cache returns 503 with a
        hint to GET /api/v1/health.
      operationId: getCityTraffic
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
        - name: include
          in: query
          required: false
          description: >-
            Feld-Opt-in: include=geometry behält die Roh-Polyline (geometry) je
            roadwork/warning; Default ist schlank (geometry weggelassen).
          x-description-en: >-
            Field opt-in: include=geometry keeps the raw polyline (geometry) per
            roadwork/warning; default is slim (geometry omitted).
          schema:
            type: string
            enum: [geometry]
        - name: full
          in: query
          required: false
          description: >-
            Äquivalentes Flag zu include=geometry (?full=1 behält die Polyline).
          x-description-en: >-
            Equivalent flag to include=geometry (?full=1 keeps the polyline).
          schema:
            type: boolean
      responses:
        "200":
          description: Verkehrs-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/transit:
    get:
      tags: [cities]
      summary: ÖPNV-Haltestellen je Stadt (DELFI und HVV GTFS)
      x-summary-en: Public-transit stops per city (DELFI and HVV GTFS)
      description: >-
        Normalisierte ÖPNV-Haltestellen einer Stadt aus dem bundesweiten
        DELFI-GTFS-Feed bzw. dem HVV-Feed im kanonischen Envelope. Die Daten
        werden offline aufbereitet und als Snapshot je Stadt gehalten, sodass der
        Abruf schnell antwortet, ohne den Feed pro Request zu parsen.
        Sonderfälle: sind beide Quellen deaktiviert, kommt 200 mit
        source_status="disabled"; liegt noch kein Snapshot vor, kommt 200 mit
        source_status="not_ingested" und leeren data, nie ein 5xx.
        Mit q (Namenssuche) oder near (Umkreissuche) lassen sich gezielt
        einzelne Haltestellen finden, statt die Vollliste abzurufen.
      x-description-en: >-
        Normalized public-transit stops for a city from the nationwide DELFI GTFS
        feed or the HVV feed in the canonical envelope. The data is prepared
        offline and kept as a per-city snapshot, so the request responds quickly
        without parsing the feed per request. Special cases: if both sources are
        disabled it returns 200 with source_status="disabled"; if no snapshot
        exists yet it returns 200 with source_status="not_ingested" and empty
        data, never a 5xx. Use q (name search) or near (proximity search) to
        find specific stops instead of fetching the full list.
      operationId: getCityTransit
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
        - name: q
          in: query
          required: false
          description: >-
            Case-insensitiver Substring-Filter auf den Haltestellennamen
            (stop_name), z.B. alsterdorf.
          x-description-en: >-
            Case-insensitive substring filter on the stop name (stop_name),
            e.g. alsterdorf.
          schema:
            type: string
        - name: near
          in: query
          required: false
          description: >-
            "lat,lon"; liefert nur Haltestellen im Umkreis, aufsteigend nach
            Distanz sortiert. Ungültige Werte -> 400.
          x-description-en: >-
            "lat,lon"; returns only stops within the radius, sorted by distance
            ascending. Invalid values -> 400.
          schema:
            type: string
        - name: radius_m
          in: query
          required: false
          description: Suchradius für near in Metern (Default 1000).
          x-description-en: Search radius for near in metres (default 1000).
          schema:
            type: integer
            default: 1000
        - name: page
          in: query
          required: false
          description: >-
            Seitennummer der Default-Paginierung ohne q/near (Default 50 Einträge,
            Cap 200); meta.pagination weist total/returned/limit/offset/truncated aus.
          x-description-en: >-
            Page number of the default pagination without q/near (default 50
            entries, cap 200); meta.pagination reports
            total/returned/limit/offset/truncated.
          schema:
            type: integer
        - name: limit
          in: query
          required: false
          description: >-
            Einträge pro Seite der Default-Paginierung ohne q/near (Default 50,
            Cap 200).
          x-description-en: >-
            Entries per page of the default pagination without q/near (default
            50, cap 200).
          schema:
            type: integer
      responses:
        "200":
          description: >-
            Transit-Envelope. data ist eine Liste von CanonicalRecord
            (TransitStopPayload) oder null/leer, je nach source_status
            ("ok"|"disabled"|"not_ingested").
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - type: array
                        items:
                          $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Ungültiger near-Parameter (kein "lat,lon")
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/charging:
    get:
      tags: [cities]
      summary: E-Ladesäulen-Standorte je Stadt (BNetzA, Tier A)
      x-summary-en: EV charging-station locations per city (BNetzA, Tier A)
      description: >-
        Liefert die E-Ladesäulen-Standorte einer Stadt (nur Stammdaten, KEINE
        Belegung) aus dem BNetzA-Ladesäulenregister im kanonischen Envelope. Die
        Daten stammen aus dem CSV-Bulk-Download der BNetzA
        (data.bundesnetzagentur.de) und werden periodisch aktualisiert; die Route
        liefert den jüngsten aufbereiteten Stand, keinen Live-Abruf.
        Deaktiviert -> 200 source_status="disabled"; kein Snapshot -> 200
        source_status="not_ingested".
      x-description-en: >-
        Returns a city's EV charging-station locations (master data only, NO
        occupancy) from the BNetzA charging-station register in the canonical
        envelope. The data comes from the BNetzA CSV bulk download
        (data.bundesnetzagentur.de) and is refreshed periodically; the route
        serves the most recent prepared state, not a live fetch.
        Disabled -> 200 source_status="disabled"; no snapshot -> 200
        source_status="not_ingested".
      operationId: getCityCharging
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Ladesäulen-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/charging-status:
    get:
      tags: [cities]
      summary: Live-Ladesäulen-Belegung je Stadt (eRound, CC0/Tier A)
      x-summary-en: Live EV charging occupancy per city (eRound, CC0/Tier A)
      description: >-
        Liefert die aggregierte Live-Ladesäulen-Belegung einer Stadt (eRound
        AFIR-Recharging, DATEX-II V3, CC0) im kanonischen Envelope. Join aus
        zwei Zuständen, kein Upstream-Abruf im Request: der statische
        eRound-Standort-Vollbestand liefert die Zuordnung Ladepunkt -> Stadt
        + Koordinaten (täglicher Ingest, alle 84 Städte abgedeckt), der
        dynamische Belegungs-Feed wird als Delta-Strom akkumuliert (Status je
        Ladepunkt, Staleness-Fenster 24 h). Payload: total_points (bekannte
        Ladepunkte im Stadtumkreis), reported_points (davon mit frischem
        Live-Status), status_counts (available/occupied/charging/unavailable
        u. a.) sowie die Einzelpunkte (refill_point_id, lat/lon, status,
        observed_at; Liste auf 500 gekappt, truncated markiert ehrlich).
        Quelle deaktiviert -> 200 source_status="disabled"; noch kein
        akkumulierter Live-Status -> 200 source_status="no_data".
      x-description-en: >-
        Returns a city's aggregated live EV charging occupancy (eRound AFIR
        recharging, DATEX-II V3, CC0) in the canonical envelope. A join of
        two states with no upstream call at request time: the static eRound
        location inventory maps each charging point to a city + coordinates
        (daily ingest, all 84 cities covered), while the dynamic occupancy
        feed is accumulated as a delta stream (status per charging point,
        24 h staleness window). Payload: total_points (known charging points
        within the city radius), reported_points (of which with a fresh live
        status), status_counts (available/occupied/charging/unavailable
        etc.) plus the individual points (refill_point_id, lat/lon, status,
        observed_at; list capped at 500, truncated flags honestly).
        Source disabled -> 200 source_status="disabled"; no accumulated
        live status yet -> 200 source_status="no_data".
      operationId: getCityChargingStatus
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Ladebelegungs-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/air-uba:
    get:
      tags: [cities]
      summary: Luftqualität je Stadt (Umweltbundesamt)
      x-summary-en: Air quality per city (Federal Environment Agency)
      description: >-
        Luftqualitäts-Messwerte einer Stadt aus der Luftdaten-API des
        Umweltbundesamts im kanonischen Envelope. Amtlicher Tier-A-Pfad für
        behördlich validierte Werte. Liefert dieselbe UBA-Quelle wie der ältere,
        nicht persistierte Pfad (/air). Sonderfälle: deaktivierte Quelle liefert
        200 mit source_status="disabled"; toter Upstream ohne Cache liefert 503
        mit Hint auf GET /api/v1/health.
      x-description-en: >-
        Air-quality readings for a city from the German Federal Environment
        Agency (UBA) air-data API in the canonical envelope. The official Tier-A
        path for authority-validated values. Serves the same UBA source as the
        older, non-persisted path (/air). Special cases: a disabled source
        returns 200 with source_status="disabled"; a dead upstream with no cache
        returns 503 with a hint to GET /api/v1/health.
      operationId: getCityAirUba
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Luftqualitäts-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/water-level:
    get:
      tags: [cities]
      summary: Pegelstand je Stadt (PEGELONLINE, Tier A, Teilabdeckung)
      x-summary-en: Water level per city (PEGELONLINE, Tier A, partial coverage)
      description: >-
        Liefert den aktuellen Pegelstand der nächstgelegenen Station einer Stadt
        aus PEGELONLINE im kanonischen Envelope. Teilabdeckung: nur Städte an
        Bundeswasserstraßen haben eine Station, sonst kommt 200 mit
        source_status="no_data" (kein 5xx). Tier A. Deaktiviert -> 200
        source_status="disabled".
      x-description-en: >-
        Returns the current water level of the nearest station to a city from
        PEGELONLINE in the canonical envelope. Partial coverage: only cities on
        federal waterways have a station, otherwise it returns 200 with
        source_status="no_data" (no 5xx). Tier A. Disabled -> 200
        source_status="disabled".
      operationId: getCityWaterLevel
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Pegelstand-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/flood:
    get:
      tags: [cities]
      summary: Hochwasser-Warnstufen je Stadt (LHP, Tier A)
      x-summary-en: Flood warning levels per city (LHP, Tier A)
      description: >-
        Liefert die aktuellen Hochwasser-Warnstufen der kuratierten Pegel einer
        Stadt aus dem Länderübergreifenden Hochwasserportal (LHP) im kanonischen
        Envelope. Die Attribution trägt den "Stand:"-Zeitstempel (Pflicht).
        Tier A. Teilabdeckung (kuratierte Pegel je Stadt): nur berlin, bonn,
        dresden, duesseldorf, duisburg, erfurt, essen, frankfurt-am-main, hamburg,
        koeln, leipzig, mainz, muenchen, nuernberg liefern Daten. Andere
        registrierte Städte antworten ehrlich mit 200 source_status="not_covered",
        data=null und meta.covered_cities (Liste der abgedeckten Städte). Weitere
        Sonderfälle: deaktiviert -> 200 source_status="disabled"; toter Upstream
        ohne Cache -> 503 mit Hint.
      x-description-en: >-
        Returns the current flood warning levels of a city's curated gauges from
        the German cross-state flood portal (LHP) in the canonical envelope. The
        attribution carries the mandatory "Stand:" timestamp. Tier A. Partial
        coverage (curated gauges per city): only berlin, bonn, dresden,
        duesseldorf, duisburg, erfurt, essen, frankfurt-am-main, hamburg, koeln,
        leipzig, mainz, muenchen, nuernberg return data. Other registered cities
        honestly respond with 200 source_status="not_covered", data=null and
        meta.covered_cities (the list of covered cities). Other special cases:
        disabled -> 200 source_status="disabled"; dead upstream with no cache ->
        503 with a hint.
      operationId: getCityFlood
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Hochwasser-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/pollen-uv:
    get:
      tags: [cities]
      summary: Pollenflug und UV-Index je Stadt (DWD opendata, Tier A)
      x-summary-en: Pollen and UV index per city (DWD opendata, Tier A)
      description: >-
        Liefert Pollenflug-Gefahrenindex und UV-Index für die Großregion einer
        Stadt aus DWD opendata im kanonischen Envelope. Die Daten sind nach
        DWD-Großregionen gegliedert, NICHT stadtgenau (region_name im Payload).
        Attribution mit modified=true (GeoNutzV). Tier A. Deaktiviert -> 200
        source_status="disabled".
      x-description-en: >-
        Returns the pollen hazard index and UV index for a city's macro-region
        from DWD opendata in the canonical envelope. The data is organized by
        DWD macro-regions, NOT city-precise (region_name in the payload).
        Attribution with modified=true (GeoNutzV). Tier A. Disabled -> 200
        source_status="disabled".
      operationId: getCityPollenUv
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Pollen-/UV-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/fire-danger:
    get:
      tags: [cities]
      summary: Waldbrand- und Graslandfeuerindex je Stadt (DWD, Tier A)
      x-summary-en: Forest-fire and grassland-fire danger index per city (DWD, Tier A)
      description: >-
        Liefert den Waldbrandgefahrenindex (Stufe 1 sehr gering bis 5 sehr hoch)
        und den Graslandfeuerindex der nächsten DWD-Station zu einer Stadt im
        kanonischen Envelope. Der Index ist STATIONS-genau, NICHT stadtgenau
        (station_name und distance_km im Payload weisen die Station aus).
        Attribution mit modified=true (GeoNutzV). Tier A. Deaktiviert -> 200
        source_status="disabled".
      x-description-en: >-
        Returns the forest-fire danger index (level 1 very low to 5 very high)
        and the grassland-fire index of the nearest DWD station to a city in the
        canonical envelope. The index is STATION-based, NOT city-precise
        (station_name and distance_km in the payload identify the station).
        Attribution with modified=true (GeoNutzV). Tier A. Disabled -> 200
        source_status="disabled".
      operationId: getCityFireDanger
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Waldbrand-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/bathing-water:
    get:
      tags: [cities]
      summary: Badegewässerqualität im Umkreis einer Stadt (EEA, Tier A)
      x-summary-en: Bathing water quality near a city (EEA, Tier A)
      description: >-
        Liefert die Badegewässerqualität (EU-Badegewässerrichtlinie 2006/7/EG,
        EEA DiscoMap) im Umkreis einer Stadt im kanonischen Envelope. Je Badestelle
        Klasse (Excellent/Good/Sufficient/Poor), Gewässertyp, Koordinaten, Distanz
        und Profil-Link. ORTSNAH (Umland), NICHT stadtgenau (distance_km je Stelle);
        Inland-Städte ohne Badegewässer liefern count=0. CC-BY 4.0, Tier A.
        Deaktiviert -> 200 source_status="disabled".
      x-description-en: >-
        Returns bathing water quality (EU Bathing Water Directive 2006/7/EC, EEA
        DiscoMap) near a city in the canonical envelope. Per site: class
        (Excellent/Good/Sufficient/Poor), water category, coordinates, distance and
        profile link. NEARBY (surroundings), NOT city-precise (distance_km per
        site); inland cities without bathing waters return count=0. CC-BY 4.0,
        Tier A. Disabled -> 200 source_status="disabled".
      operationId: getCityBathingWater
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Badegewässer-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/hospitals-atlas:
    get:
      tags: [cities]
      summary: Krankenhausstandorte je Stadt (Bundes-Klinik-Atlas, fail-closed)
      x-summary-en: Hospital locations per city (Bundes-Klinik-Atlas, fail-closed)
      description: >-
        Liefert standortgenaue Krankenhausdaten (Name, Adresse, Betten, Kontakt,
        Koordinaten) im Umkreis einer Stadt aus dem Bundes-Klinik-Atlas (BMG/IQTIG)
        im kanonischen Envelope. FAIL-CLOSED: keine explizite offene Lizenz
        ausgewiesen -> license_id UNKNOWN/Tier C und per Default DEAKTIVIERT
        (-> 200 source_status="disabled"), bis BMG/IQTIG die Lizenz bestätigt.
        ORTSNAH gefiltert (distance_km je Standort).
      x-description-en: >-
        Returns per-location hospital data (name, address, beds, contact,
        coordinates) near a city from the Bundes-Klinik-Atlas (BMG/IQTIG) in the
        canonical envelope. FAIL-CLOSED: no explicit open licence stated ->
        license_id UNKNOWN/Tier C and disabled by default (-> 200
        source_status="disabled") until BMG/IQTIG confirm the licence. Filtered
        by proximity (distance_km per location).
      operationId: getCityHospitalsAtlas
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Krankenhausatlas-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/station-facilities:
    get:
      tags: [cities]
      summary: Aufzug-/Rolltreppen-Status an Bahnhöfen je Stadt (DB FaSta, Tier A)
      x-summary-en: Elevator/escalator status at stations per city (DB FaSta, Tier A)
      description: >-
        Liefert den Echtzeit-Betriebsstatus von Aufzügen und Rolltreppen an
        Bahnhöfen einer Stadt (DB FaSta) im kanonischen Envelope. Je Anlage Typ
        (ELEVATOR/ESCALATOR), Status (ACTIVE/INACTIVE/UNKNOWN) + Begründung,
        Bahnhofsnummer, Koordinaten und Distanz. KEY-GATED: ohne DB-API-Schlüssel
        -> 200 source_status="disabled". CC-BY 4.0, Tier A. ORTSNAH (Bahnhöfe im
        Stadtgebiet), distance_km je Anlage.
      x-description-en: >-
        Returns the real-time operational state of elevators and escalators at a
        city's railway stations (DB FaSta) in the canonical envelope. Per facility:
        type (ELEVATOR/ESCALATOR), state (ACTIVE/INACTIVE/UNKNOWN) + reason, station
        number, coordinates and distance. KEY-GATED: without a DB API key -> 200
        source_status="disabled". CC-BY 4.0, Tier A.
      operationId: getCityStationFacilities
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Aufzug-/Rolltreppen-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/demographics:
    get:
      tags: [cities]
      summary: Demografie-Zeitreihen je Stadt (GENESIS/Regionalstatistik, Tier A)
      x-summary-en: Demographic time series per city (GENESIS/Regionalstatistik, Tier A)
      description: >-
        Liefert Demografie-Stammwerte und Zeitreihen einer Stadt aus der
        POST-API der GENESIS-/Regionalstatistik im kanonischen Envelope. Der
        Zugang ist account-gebunden: ohne hinterlegte Zugangsdaten oder bei
        deaktivierter Quelle antwortet der Endpunkt mit 200
        source_status="disabled". Tier A. Toter Upstream ohne Cache -> 503 mit
        Hint.
      x-description-en: >-
        Returns a city's demographic base values and time series from the
        GENESIS/Regionalstatistik POST API in the canonical envelope. Access is
        account-bound: without stored credentials or with the source disabled the
        endpoint responds with 200 source_status="disabled". Tier A. Dead
        upstream with no cache -> 503 with a hint.
      operationId: getCityDemographics
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Demografie-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/energy:
    get:
      tags: [cities]
      summary: Energie-Anlagen je Stadt (MaStR, Tier A)
      x-summary-en: Energy installations per city (MaStR, Tier A)
      description: >-
        Liefert die Energie-Anlagen (PV/Wind/Speicher/Biogas) einer Stadt aus dem
        offline aufbereiteten MaStR-SQLite-Store im kanonischen Envelope. Kein
        Upstream im Request-Pfad, der Abruf liest nur den vorbereiteten Bestand.
        Deaktiviert -> 200 source_status="disabled". Batch noch nicht gelaufen ->
        200 source_status="not_ingested" (nie 5xx).
      x-description-en: >-
        Returns a city's energy installations (PV/wind/storage/biogas) from the
        offline-prepared MaStR SQLite store in the canonical envelope. No
        upstream in the request path; the request only reads the prepared store.
        Disabled -> 200 source_status="disabled". Batch not yet run -> 200
        source_status="not_ingested" (never 5xx).
      operationId: getCityEnergy
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: >-
            Energie-Envelope. data ist eine Liste von CanonicalRecord
            (EnergyAssetPayload) oder null/leer, je nach source_status.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - type: array
                        items:
                          $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/vehicle-registrations:
    get:
      tags: [cities]
      summary: Pkw-Bestand und Elektro-Anteil je Stadt (KBA, Tier A)
      x-summary-en: Car stock and electric share per city (KBA, Tier A)
      description: >-
        Liefert den Pkw-Bestand und den Elektro-Anteil einer Stadt aus dem offline
        aufbereiteten KBA-SQLite-Store im kanonischen Envelope. Die regionale
        Auflösung ist der Zulassungsbezirk (= Kreis bzw. kreisfreie Stadt) und
        wird über district/district_key ehrlich ausgewiesen. Kein Upstream im
        Request-Pfad, der Abruf liest nur den vorbereiteten Bestand. Deaktiviert ->
        200 source_status="disabled". Batch noch nicht gelaufen -> 200
        source_status="not_ingested" (nie 5xx).
      x-description-en: >-
        Returns a city's car stock and electric share from the offline-prepared
        KBA SQLite store in the canonical envelope. The spatial resolution is the
        registration district (= county or county-free city) and is honestly
        reported via district/district_key. No upstream in the request path; the
        request only reads the prepared store. Disabled -> 200
        source_status="disabled". Batch not yet run -> 200
        source_status="not_ingested" (never 5xx).
      operationId: getCityVehicleRegistrations
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: >-
            Fahrzeug-Envelope. data ist ein CanonicalRecord
            (VehicleRegistrationPayload) oder null, je nach source_status.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/accidents:
    get:
      tags: [cities]
      summary: Straßenverkehrsunfälle je Stadt (Unfallatlas, Tier A)
      x-summary-en: Road traffic accidents per city (accident atlas, Tier A)
      description: >-
        Liefert das jährliche Unfall-Aggregat einer Stadt aus dem offline
        aufbereiteten Unfallatlas-SQLite-Store im kanonischen Envelope: Gesamtzahl
        sowie Aufschlüsselung nach Unfallkategorie (Getötete/Schwer-/
        Leichtverletzte) und beteiligter Verkehrsart (Rad/Fuß/Pkw/Krad).
        Regionale Auflösung Kreis/kreisfreie Stadt. Kein Upstream im Request-Pfad.
        Deaktiviert -> 200 source_status="disabled". Batch noch nicht gelaufen ->
        200 source_status="not_ingested" (nie 5xx).
      x-description-en: >-
        Returns a city's annual accident aggregate from the offline-prepared
        accident-atlas SQLite store in the canonical envelope: total plus a
        breakdown by accident category (fatal/serious/slight) and by participant
        type (bicycle/pedestrian/car/motorcycle). Spatial resolution county/
        county-free city. No upstream in the request path. Disabled -> 200
        source_status="disabled". Batch not yet run -> 200
        source_status="not_ingested" (never 5xx).
      operationId: getCityAccidents
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Unfall-Envelope (CanonicalRecord mit AccidentPayload oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/crime-stats:
    get:
      tags: [cities]
      summary: Polizeiliche Kriminalstatistik je Stadt (BKA PKS, Tier A)
      x-summary-en: Police crime statistics per city (BKA PKS, Tier A)
      description: >-
        Liefert die polizeiliche Kriminalstatistik je Kreis aus dem offline
        aufbereiteten BKA-PKS-SQLite-Store im kanonischen Envelope: je
        Hauptstraftatengruppe (z.B. Straftaten insgesamt, Gewaltkriminalität,
        Wohnungseinbruchdiebstahl) die erfassten Fälle, die Häufigkeitszahl je
        100.000 Einwohner und die Aufklärungsquote in Prozent, dazu Berichtsjahr
        (reference_year) und Version. Lizenz DL-DE/BY 2.0 (Tier A). Regionale
        Auflösung Kreis/kreisfreie Stadt. Kein Upstream im Request-Pfad.
        Sperr-/Leerwerte -> null. Deaktiviert -> 200 source_status="disabled".
        Batch noch nicht gelaufen -> 200 source_status="not_ingested" (nie 5xx).
      x-description-en: >-
        Returns a city's police crime statistics per county from the offline-
        prepared BKA PKS SQLite store in the canonical envelope: per main offence
        group (e.g. total offences, violent crime, residential burglary) the
        recorded cases, the frequency per 100,000 inhabitants and the clearance
        rate in percent, plus the reporting year (reference_year) and version.
        Licence DL-DE/BY 2.0 (Tier A). Spatial resolution county/county-free
        city. No upstream in the request path. Blocked/empty values -> null.
        Disabled -> 200 source_status="disabled". Batch not yet run -> 200
        source_status="not_ingested" (never 5xx).
      operationId: getCityCrimeStats
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Kriminalstatistik-Envelope (CanonicalRecord mit CrimeStatsPayload oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/unemployment:
    get:
      tags: [cities]
      summary: Arbeitslose und Arbeitslosenquote je Stadt (GENESIS, Tier A)
      x-summary-en: Unemployment count and rate per city (GENESIS, Tier A)
      description: >-
        Liefert Arbeitslose (Anzahl) und Arbeitslosenquote (Prozent) einer Stadt
        aus der Regionalstatistik (Arbeitsmarktstatistik der Bundesagentur für
        Arbeit, Jahresdurchschnitt) im kanonischen Envelope. Regionale Auflösung
        Kreis/kreisfreie Stadt (region_name). Quelle account-gated; deaktiviert ->
        200 source_status="disabled". Kein Wert -> 200 source_status="no_data".
      x-description-en: >-
        Returns a city's unemployment count and rate from the regional statistics
        (Federal Employment Agency, annual average) in the canonical envelope.
        Spatial resolution county/county-free city. Account-gated; disabled -> 200
        source_status="disabled". No value -> 200 source_status="no_data".
      operationId: getCityUnemployment
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Regionalstatistik-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/tourism:
    get:
      tags: [cities]
      summary: Gästeübernachtungen und Ankünfte je Stadt (GENESIS, Tier A)
      x-summary-en: Overnight stays and arrivals per city (GENESIS, Tier A)
      description: >-
        Liefert Gästeübernachtungen und Gästeankünfte einer Stadt aus der
        Regionalstatistik (Monatserhebung im Tourismus, Jahressumme) im
        kanonischen Envelope. Regionale Auflösung Kreis/kreisfreie Stadt. Quelle
        account-gated; deaktiviert -> 200 source_status="disabled". Kein Wert ->
        200 source_status="no_data".
      x-description-en: >-
        Returns a city's overnight stays and guest arrivals from the regional
        statistics (tourism survey, annual sum) in the canonical envelope. Spatial
        resolution county/county-free city. Account-gated; disabled -> 200
        source_status="disabled". No value -> 200 source_status="no_data".
      operationId: getCityTourism
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Regionalstatistik-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/construction:
    get:
      tags: [cities]
      summary: Baugenehmigungen je Stadt (GENESIS, Tier A)
      x-summary-en: Building permits per city (GENESIS, Tier A)
      description: >-
        Liefert genehmigte Wohngebäude und Wohnungen einer Stadt aus der
        Regionalstatistik (Statistik der Baugenehmigungen, Jahressumme) im
        kanonischen Envelope. Regionale Auflösung Kreis/kreisfreie Stadt. Quelle
        account-gated; deaktiviert -> 200 source_status="disabled". Kein Wert ->
        200 source_status="no_data".
      x-description-en: >-
        Returns a city's permitted residential buildings and dwellings from the
        regional statistics (building permit statistics, annual sum) in the
        canonical envelope. Spatial resolution county/county-free city.
        Account-gated; disabled -> 200 source_status="disabled". No value -> 200
        source_status="no_data".
      operationId: getCityConstruction
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Regionalstatistik-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/geo:
    get:
      tags: [cities]
      summary: Verwaltungsgrenze je Stadt (BKG VG250, Tier A)
      x-summary-en: Administrative boundary per city (BKG VG250, Tier A)
      description: >-
        Liefert die Attributdaten der Verwaltungsgrenze (AGS, Gemeindename,
        Fläche) einer Stadt aus dem offline aufbereiteten BKG-VG250-Snapshot im
        kanonischen Envelope. Attribution wortgenau "(c) GeoBasis-DE / BKG".
        Deaktiviert -> 200 source_status="disabled". Snapshot noch nicht gebaut ->
        200 source_status="not_ingested".
      x-description-en: >-
        Returns the attribute data of a city's administrative boundary (AGS,
        municipality name, area) from the offline-prepared BKG VG250 snapshot in
        the canonical envelope. Attribution verbatim "(c) GeoBasis-DE / BKG".
        Disabled -> 200 source_status="disabled". Snapshot not yet built -> 200
        source_status="not_ingested".
      operationId: getCityGeo
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Geo-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/election:
    get:
      tags: [cities]
      summary: Wahlergebnis je Stadt (Bundeswahlleiterin, Tier A)
      x-summary-en: Election result per city (Federal Returning Officer, Tier A)
      description: >-
        Liefert das Wahlergebnis je Wahlkreis/Kreis einer Stadt aus dem offline
        aufbereiteten Bundeswahl-Snapshot im kanonischen Envelope. Die
        Granularität wird ehrlich als "teilweise" ausgewiesen. Deaktiviert -> 200
        source_status="disabled". Batch noch nicht gelaufen -> 200
        source_status="not_ingested".
      x-description-en: >-
        Returns a city's election result per constituency/district from the
        offline-prepared federal-election snapshot in the canonical envelope. The
        granularity is honestly reported as "partial". Disabled -> 200
        source_status="disabled". Batch not yet run -> 200
        source_status="not_ingested".
      operationId: getCityElection
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Wahl-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/holidays:
    get:
      tags: [cities]
      summary: Feiertage und Schulferien je Stadt (Seed, gemeinfrei)
      x-summary-en: Public holidays and school vacations per city (seed, public domain)
      description: >-
        Liefert Feiertage und Schulferien je Bundesland (über entry.state
        aufgelöst) aus den eingebetteten Seed-Daten (data/seeds/) im kanonischen
        Envelope. Kein Upstream, gemeinfrei. Deaktiviert -> 200
        source_status="disabled". Kein Seed für das Bundesland -> 200
        source_status="no_data".
      x-description-en: >-
        Returns public holidays and school vacations per federal state (resolved
        via entry.state) from the embedded seed data (data/seeds/) in the
        canonical envelope. No upstream, public domain. Disabled -> 200
        source_status="disabled". No seed for the state -> 200
        source_status="no_data".
      operationId: getCityHolidays
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Feiertags-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/power-load:
    get:
      tags: [cities]
      summary: Stromverbrauch (Netzlast) der Regelzone je Stadt (SMARD, Tier A)
      x-summary-en: Electricity consumption (grid load) per city's control zone (SMARD, Tier A)
      description: >-
        Realisierter Stromverbrauch (Netzlast) der Regelzone, in der die Stadt
        liegt (50Hertz/Amprion/TenneT/TransnetBW), als Tageswert in MWh. Regionale
        Kennzahl, nicht stadtgenau. Quelle: Bundesnetzagentur | SMARD.de (CC BY
        4.0). Deaktiviert -> 200 source_status="disabled".
      x-description-en: >-
        Realised electricity consumption (grid load) of the control zone the city
        belongs to (50Hertz/Amprion/TenneT/TransnetBW) as a daily value in MWh.
        Regional metric, not city-exact. Source: Bundesnetzagentur | SMARD.de.
      operationId: getCityPowerLoad
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Power-Envelope (CanonicalRecord mit PowerPayload) oder null.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/power-price:
    get:
      tags: [cities]
      summary: Day-ahead-Börsenstrompreis bundesweit (SMARD, Tier A)
      x-summary-en: Day-ahead wholesale electricity price, nationwide (SMARD, Tier A)
      description: >-
        Day-ahead-Großhandelspreis der Gebotszone DE/LU als Tageswert in EUR/MWh
        (bundesweit, für alle Städte identisch). Quelle: Bundesnetzagentur |
        SMARD.de (CC BY 4.0). Deaktiviert -> 200 source_status="disabled".
      x-description-en: >-
        Day-ahead wholesale electricity price of the DE/LU bidding zone as a daily
        value in EUR/MWh (nationwide, identical for all cities). Source:
        Bundesnetzagentur | SMARD.de.
      operationId: getCityPowerPrice
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Power-Envelope (CanonicalRecord mit PowerPayload) oder null.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/weather-warnings:
    get:
      tags: [cities]
      summary: Amtliche DWD-Wetterwarnungen je Stadt (Tier A)
      x-summary-en: Official DWD weather warnings per city (Tier A)
      description: >-
        Aktive amtliche Wetterwarnungen des Deutschen Wetterdienstes für die
        Stadt (Filterung über die Gemeinde-Warncell = "1"+AGS). Liefert
        max_level (0 = keine reguläre Warnung, 1-4 = DWD-Warnstufe), count und die
        Einzelwarnungen. Hitze-/Sonderwarnungen (DWD-Code >= 50) zählen NICHT in
        max_level, sondern stehen separat in special_warnings. Quelle: Deutscher
        Wetterdienst (GeoNutzV). Deaktiviert -> 200 source_status="disabled".
      x-description-en: >-
        Active official weather warnings from the German Weather Service (DWD)
        for the city (filtered via the municipality warncell = "1"+AGS). Returns
        max_level (0 = none, 1-4 = DWD level), count and the individual warnings.
        Heat/special warnings (DWD code >= 50) do NOT count towards max_level and
        are listed separately in special_warnings. Source: Deutscher Wetterdienst
        (GeoNutzV).
      operationId: getCityWeatherWarnings
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Warnungen-Envelope (CanonicalRecord mit WeatherWarningPayload) oder null.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/civil-protection-warnings:
    get:
      tags: [cities]
      summary: Amtliche BBK-NINA-Bevölkerungsschutz-Warnungen je Stadt (Tier A)
      x-summary-en: Official BBK NINA civil protection warnings per city (Tier A)
      description: >-
        Aktive amtliche Bevölkerungsschutz-Warnungen des Bundesamts für
        Bevölkerungsschutz und Katastrophenhilfe (BBK, NINA-API) für die Stadt.
        Füllt die Zivilschutz-Lücke (Gefahrstoff, Großbrand, Bombenentschärfung)
        neben weather-warnings und flood. Der Regionsbezug erfolgt über den
        12-stelligen Kreis-ARS (aus dem AGS abgeleitet); coverage_granularity
        (city|district) weist die ARS-Kreisschärfe aus. Der amtliche Warntext
        (headline) wird UNVERÄNDERT, verbatim durchgereicht (§ 5 Abs. 2 UrhG,
        keine Umformulierung/Kürzung/Übersetzung); je Warnung stehen provider
        (MOWAS/KATWARN/BIWAPP/POLICE/DWD/LHP), severity, sent, onset, expires,
        detail_url und duplicate_of (DWD -> weather-warnings, LHP -> flood).
        Quelle: BBK NINA, keylos. Deaktiviert -> 200 source_status="disabled";
        keine aktive Warnung -> 200 source_status="ok" (count 0).
      x-description-en: >-
        Active official civil protection warnings from the German Federal Office
        of Civil Protection and Disaster Assistance (BBK, NINA API) for the city.
        Fills the civil-protection gap (hazmat, major fire, bomb disposal)
        alongside weather-warnings and flood. Region mapping uses the 12-digit
        district ARS (derived from the AGS); coverage_granularity (city|district)
        states the ARS granularity. The official warning text (headline) is passed
        through VERBATIM, unchanged (§ 5 (2) UrhG, no rewording/shortening/
        translation); each warning carries provider (MOWAS/KATWARN/BIWAPP/POLICE/
        DWD/LHP), severity, sent, onset, expires, detail_url and duplicate_of
        (DWD -> weather-warnings, LHP -> flood). Source: BBK NINA, keyless.
      operationId: getCityCivilProtectionWarnings
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: >-
            Warnungen-Envelope (CanonicalRecord mit CivilProtectionWarningPayload:
            ars, coverage_granularity, count, warnings) oder null bei
            source_status disabled. source_status ok/disabled.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Upstream (BBK NINA) nicht erreichbar, kein Cache
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/fuel-prices:
    get:
      tags: [cities]
      summary: Aktuelle Spritpreise je Stadt, aggregiert (Tankerkönig/MTS-K, Tier A)
      x-summary-en: Current fuel prices per city, aggregated (Tankerkönig/MTS-K, Tier A)
      description: >-
        Aktuelle Spritpreise im Umkreis der Stadtkoordinate, aggregiert zu einer
        Stadt-Kennzahl: Durchschnitts- und Minimal-Preis je Sorte (e5/e10/diesel,
        EUR/Liter) über die geöffneten Tankstellen, plus station_count/open_count
        und die Einzel-Tankstellen. Quelle: Markttransparenzstelle für Kraftstoffe
        (MTS-K) via Tankerkönig (CC BY 4.0). Ohne API-Key ODER Toggle aus -> 200
        source_status="disabled"; keine Tankstelle im Radius -> 200
        source_status="no_data".
      x-description-en: >-
        Current fuel prices around the city coordinate, aggregated into a
        city-level metric: average and minimum price per fuel grade (e5/e10/diesel,
        EUR/litre) across the open stations, plus station_count/open_count and the
        individual stations. Source: German fuel price transparency unit (MTS-K)
        via Tankerkönig (CC BY 4.0). Without an API key OR toggle off -> 200
        source_status="disabled"; no station in radius -> 200 source_status="no_data".
      operationId: getCityFuelPrices
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Spritpreis-Envelope (CanonicalRecord mit FuelPricePayload) oder null.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/stations:
    get:
      tags: [cities]
      summary: Bahnhofs-Katalog einer Stadt, alle DB-Bahnhöfe (StaDa, Tier A)
      x-summary-en: City station catalog, all DB stations (StaDa, Tier A)
      description: >-
        Liste ALLER DB-Bahnhöfe im Stadtgebiet (Zuordnung über den amtlichen
        Gemeindeschlüssel: StaDa municipalityCode == Stadt-ags), nicht nur der
        Fernverkehrs-Hbf. Je Bahnhof eva (EVA-Nummer, füttert
        GET /stations/{eva}/departures), name, category (1-7), lat, lon, zip.
        Quelle: Deutsche Bahn AG / StaDa (CC BY 4.0). Ohne Zugangsdaten ODER Toggle
        aus -> 200 source_status="disabled"; kein DB-Bahnhof im Stadtgebiet -> 200
        source_status="no_data". Volle Abdeckung (alle Städte).
      x-description-en: >-
        List of ALL DB stations within the city (mapped via the official
        municipality key: StaDa municipalityCode == city ags), not just the
        long-distance main station. Each carries eva (EVA number, feeds
        GET /stations/{eva}/departures), name, category (1-7), lat, lon, zip.
        Source: Deutsche Bahn AG / StaDa (CC BY 4.0). Without credentials OR toggle
        off -> 200 source_status="disabled"; no DB station in the city -> 200
        source_status="no_data". Full coverage (all cities).
      operationId: getCityStations
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
        - name: q
          in: query
          required: false
          description: >-
            Optionaler Namensfilter: umlaut-toleranter Teilstring-Match auf dem
            Bahnhofsnamen (munchen UND muenchen matchen "München Hbf"). Kein
            Treffer -> 200 source_status="no_data".
          x-description-en: >-
            Optional name filter: umlaut-tolerant substring match on the station
            name (both munchen and muenchen match "München Hbf"). No match ->
            200 source_status="no_data".
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: >-
            Kappt die sortierte Liste (Default: alle Bahnhöfe). Werte über 100
            werden auf 100 geklemmt; nicht-numerisch oder kleiner 1 -> 422.
          x-description-en: >-
            Caps the sorted list (default: all stations). Values above 100 are
            clamped to 100; non-numeric or below 1 -> 422.
          schema:
            type: integer
            minimum: 1
            maximum: 100
      responses:
        "200":
          description: Katalog-Envelope (CanonicalRecord mit StationCatalogPayload) oder null.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/station-departures:
    get:
      tags: [cities]
      summary: Live-Abfahrtstafel Haupt-Bahnhof der Stadt, alle Gattungen (DB Timetables, Tier A)
      x-summary-en: Live departure board for a city's main station, all categories (DB Timetables, Tier A)
      description: >-
        Nächste Zugabfahrten am Haupt-Bahnhof der Stadt mit Echtzeit-Verspätung:
        Sollfahrplan (/plan) und aktuelle Abweichungen (/fchg) der
        DB-Timetables-API, je Bahnhof zu einer nach Abfahrt sortierten Liste
        zusammengeführt (line, category, destination, planned_time, platform,
        delay_minutes, cancelled, long_distance). Volle Abdeckung über alle 84
        Städte: der Haupt-Bahnhof wird automatisch aus dem amtlichen StaDa-Katalog
        abgeleitet (für die großen Knoten eine verifizierte Auswahl). Quelle:
        Deutsche Bahn AG (CC BY 4.0). Ohne Zugangsdaten ODER Toggle aus -> 200
        source_status="disabled"; keine Abfahrt im Zeitfenster -> 200
        source_status="no_data". Für einen bestimmten Bahnhof statt der
        Stadt-Auswahl dient /api/v1/stations/{eva}/departures.
      x-description-en: >-
        Upcoming train departures at a city's main station with real-time delays:
        scheduled timetable (/plan) and current changes (/fchg) of the DB
        Timetables API, merged per station into a list sorted by departure (line,
        category, destination, planned_time, platform, delay_minutes, cancelled,
        long_distance). Full coverage across all 84 cities: the main station is
        derived automatically from the official StaDa catalog (a verified pick for
        the major hubs). Source: Deutsche Bahn AG (CC BY 4.0). Without credentials
        OR toggle off -> 200 source_status="disabled"; no departure in the window ->
        200 source_status="no_data". To target a specific station instead of the
        city's pick, use /api/v1/stations/{eva}/departures.
      operationId: getCityStationDepartures
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Abfahrts-Envelope (CanonicalRecord mit StationDeparturesPayload) oder null.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/station-arrivals:
    get:
      tags: [cities]
      summary: Live-Ankunftstafel Haupt-Bahnhof der Stadt, alle Gattungen (DB Timetables, Tier A)
      x-summary-en: Live arrival board for a city's main station, all categories (DB Timetables, Tier A)
      description: >-
        Spiegelbild zu station-departures: ankommende Züge am Haupt-Bahnhof der
        Stadt mit Echtzeit-Verspätung (Soll /plan + Änderungen /fchg gemerged),
        je Eintrag mit origin (Startbahnhof), line, category, planned_time,
        platform, delay_minutes, cancelled, long_distance. Volle Abdeckung über
        alle 84 Städte: der Haupt-Bahnhof wird automatisch aus dem amtlichen
        StaDa-Katalog abgeleitet. Quelle: Deutsche Bahn AG (CC BY 4.0). Ohne
        Zugangsdaten ODER Toggle aus -> 200 source_status="disabled"; keine Ankunft
        im Zeitfenster -> 200 source_status="no_data". Für einen bestimmten Bahnhof
        statt der Stadt-Auswahl dient /api/v1/stations/{eva}/arrivals.
      x-description-en: >-
        Mirror of station-departures: incoming trains at a city's main station with
        real-time delays (scheduled /plan + changes /fchg merged), each with origin
        (start station), line, category, planned_time, platform, delay_minutes,
        cancelled, long_distance. Full coverage across all 84 cities: the main
        station is derived automatically from the official StaDa catalog. Source:
        Deutsche Bahn AG (CC BY 4.0). Without credentials OR toggle off -> 200
        source_status="disabled"; no arrival in the window -> 200
        source_status="no_data". To target a specific station instead of the city's
        pick, use /api/v1/stations/{eva}/arrivals.
      operationId: getCityStationArrivals
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Ankunfts-Envelope (CanonicalRecord mit StationArrivalsPayload) oder null.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/stations/{eva}/departures:
    get:
      tags: [stations]
      summary: Live-Abfahrten eines beliebigen Bahnhofs per EVA, alle Gattungen (DB Timetables, Tier A)
      x-summary-en: Live departures for any station by EVA, all categories (DB Timetables, Tier A)
      description: >-
        Live-Abfahrtstafel JEDES DB-Bahnhofs über seine EVA-Nummer (aus
        GET /cities/{slug}/stations), inkl. Nahverkehr (alle Gattungen
        ICE/IC/RE/RB/S), Echtzeit-Verspätung und Störungen/Meldungen (messages je
        Eintrag). Quelle: Deutsche Bahn AG (CC BY 4.0). Ohne Zugangsdaten ODER
        Toggle aus -> 200 source_status="disabled"; keine Abfahrt im Zeitfenster ->
        200 source_status="no_data"; ungültige EVA -> 422.
      x-description-en: >-
        Live departure board for ANY DB station via its EVA number (from
        GET /cities/{slug}/stations), including local/regional trains (all
        categories ICE/IC/RE/RB/S), real-time delays and disruption messages
        (messages per entry). Source: Deutsche Bahn AG (CC BY 4.0). Without
        credentials OR toggle off -> 200 source_status="disabled"; no departure in
        the window -> 200 source_status="no_data"; invalid EVA -> 422.
      operationId: getStationDepartures
      parameters:
        - name: eva
          in: path
          required: true
          description: EVA-Nummer des Bahnhofs (6-8-stellige Zahl).
          schema:
            type: string
            pattern: "^[0-9]{6,8}$"
      responses:
        "200":
          description: Abfahrts-Envelope (CanonicalRecord mit StationDeparturesPayload) oder null.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "422":
          description: Ungueltige EVA-Nummer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/stations/{eva}/arrivals:
    get:
      tags: [stations]
      summary: Live-Ankünfte eines beliebigen Bahnhofs per EVA, alle Gattungen (DB Timetables, Tier A)
      x-summary-en: Live arrivals for any station by EVA, all categories (DB Timetables, Tier A)
      description: >-
        Spiegelbild zu /stations/{eva}/departures: ankommende Züge jedes DB-
        Bahnhofs über seine EVA-Nummer (inkl. Nahverkehr, origin = Startbahnhof,
        Echtzeit-Verspätung, Störungen/Meldungen). Quelle: Deutsche Bahn AG (CC
        BY 4.0). Ohne Zugangsdaten ODER Toggle aus -> 200 source_status="disabled";
        keine Ankunft im Zeitfenster -> 200 source_status="no_data"; ungültige EVA
        -> 422.
      x-description-en: >-
        Mirror of /stations/{eva}/departures: incoming trains for any DB station
        via its EVA number (incl. local/regional, origin = start station, real-time
        delays, disruption messages). Source: Deutsche Bahn AG (CC BY 4.0). Without
        credentials OR toggle off -> 200 source_status="disabled"; no arrival in the
        window -> 200 source_status="no_data"; invalid EVA -> 422.
      operationId: getStationArrivals
      parameters:
        - name: eva
          in: path
          required: true
          description: EVA-Nummer des Bahnhofs (6-8-stellige Zahl).
          schema:
            type: string
            pattern: "^[0-9]{6,8}$"
      responses:
        "200":
          description: Ankunfts-Envelope (CanonicalRecord mit StationArrivalsPayload) oder null.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "422":
          description: Ungueltige EVA-Nummer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/indicators:
    get:
      tags: [cities]
      summary: Sozialökonomische Indikatoren je Stadt (INKAR/BBSR, Tier A)
      x-summary-en: Socioeconomic indicators per city (INKAR/BBSR, Tier A)
      description: >-
        Kuratiertes, breites Set sozialökonomischer Indikatoren je Kreis/
        kreisfreie Stadt (Arbeitsmarkt, Wirtschaft, Einkommen, Demografie, Wohnen,
        Erreichbarkeit, Verkehr, Bildung, Gesundheit, Fläche), aus der offenen
        INKAR-Wizard-API des BBSR. Je Indikator der jüngste Jahreswert mit
        Bezeichnung (inkl. Einheit), Wert, Jahr und Kategorie. Quelle: BBSR / INKAR
        (DL-DE/BY 2.0). Toggle aus -> 200 source_status="disabled"; kein Snapshot
        für den Kreis -> 200 source_status="not_ingested".
      x-description-en: >-
        A curated, broad set of socioeconomic indicators per district/independent
        city (labour market, economy, income, demographics, housing,
        accessibility, transport, education, health, land use), from the open
        INKAR wizard API of the BBSR. Each indicator carries its latest annual
        value with label (incl. unit), value, year and category. Source: BBSR /
        INKAR (DL-DE/BY 2.0). Toggle off -> 200 source_status="disabled"; no
        snapshot for the district -> 200 source_status="not_ingested".
      operationId: getCityIndicators
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Indikatoren-Envelope (CanonicalRecord mit IndicatorsPayload) oder null.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/land-values:
    get:
      tags: [cities]
      summary: Amtliche Bodenrichtwerte je Stadt, aggregiert (BORIS, Tier A)
      x-summary-en: Official land values per city, aggregated (BORIS, Tier A)
      description: >-
        Amtliche Bodenrichtwerte (BORIS, Bodenrichtwert-Informationssystem der
        Gutachterausschüsse) je Stadt zu einer Bauland-Kennzahl aggregiert
        (Bauland = Wohnen/Misch/Gewerbe, ohne Wald/Wasser/Landwirtschaft):
        brw_median_eur_m2, brw_min_eur_m2, brw_max_eur_m2, zone_count, der
        Bewertungsstichtag und der bbox_radius_deg (Umkreis um das Stadtzentrum).
        BORIS ist pro Bundesland föderiert (je Land ein WFS). Toggle aus -> 200
        source_status="disabled"; Bundesland ohne BORIS-WFS -> 200
        source_status="not_covered" (mit covered_cities); abgedeckt, aber kein
        Snapshot -> 200 source_status="not_ingested".
      x-description-en: >-
        Official land values (BORIS, the surveyor committees' land-value
        information system) per city, aggregated into a building-land metric
        (building land = residential/mixed/commercial, excluding
        forest/water/farmland): brw_median_eur_m2, brw_min_eur_m2,
        brw_max_eur_m2, zone_count, the valuation reference date and
        bbox_radius_deg (radius around the city centre). BORIS is federated per
        federal state (one WFS per state). Toggle off -> 200
        source_status="disabled"; state without a BORIS WFS -> 200
        source_status="not_covered" (with covered_cities); covered but no
        snapshot -> 200 source_status="not_ingested".
      operationId: getCityLandValues
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Bodenrichtwerte-Envelope (CanonicalRecord mit LandValuesPayload) oder null.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/tax-rates:
    get:
      tags: [cities]
      summary: Realsteuer-Hebesätze je Gemeinde (Regionalstatistik, Tier A)
      x-summary-en: Local business/property tax rates per municipality (Regionalstatistik, Tier A)
      description: >-
        Amtliche Realsteuer-Hebesätze GEMEINDE-genau (Regionalstatistik der
        Statistischen Ämter, Tabelle 71231): gewerbesteuer_hebesatz, grundsteuer_a,
        grundsteuer_b, grundsteuer_c (alle in %, ein nicht festgesetzter Satz ist
        null) sowie der stichtag (Stand 31.12., neuester Jahrgang). Standort-/
        immobilienrelevant. Toggle aus oder keine GENESIS-Credentials -> 200
        source_status="disabled"; Batch nicht gelaufen -> 200
        source_status="not_ingested".
      x-description-en: >-
        Official local real-property tax multipliers per MUNICIPALITY
        (Regionalstatistik of the German statistical offices, table 71231):
        gewerbesteuer_hebesatz (trade tax), grundsteuer_a/b/c (property tax, all in
        %, an unset rate is null) plus the stichtag (as of Dec 31, latest year).
        Toggle off or no GENESIS credentials -> 200 source_status="disabled"; batch
        not run -> 200 source_status="not_ingested".
      operationId: getCityTaxRates
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Hebesatz-Envelope (CanonicalRecord mit TaxRatesPayload) oder null.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/business-registrations:
    get:
      tags: [cities]
      summary: Gewerbean-/-abmeldungen je Kreis (Regionalstatistik, Tier A)
      x-summary-en: Business registrations/deregistrations per district (Regionalstatistik, Tier A)
      description: >-
        Gewerbean-/-abmeldungen KREIS-genau (Gewerbeanzeigenstatistik der
        Statistischen Ämter, Tabelle 52311, Jahressumme, ohne Automatenaufsteller):
        anmeldungen, abmeldungen, saldo (anmeldungen - abmeldungen; positiv =
        Netto-Gründungsplus) und das Berichtsjahr (jahr). Gründungsdynamik je
        Stadt. Toggle aus oder keine GENESIS-Credentials -> 200
        source_status="disabled"; Batch nicht gelaufen -> 200
        source_status="not_ingested".
      x-description-en: >-
        Business registrations and deregistrations per DISTRICT (German business
        notification statistics, table 52311, annual total, excluding vending
        machines): anmeldungen (registrations), abmeldungen (deregistrations),
        saldo (net, positive = founding surplus) and the reporting year (jahr).
        Toggle off or no GENESIS credentials -> 200 source_status="disabled"; batch
        not run -> 200 source_status="not_ingested".
      operationId: getCityBusinessRegistrations
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Gewerbeanzeigen-Envelope (CanonicalRecord mit BusinessRegistrationsPayload) oder null.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/insolvencies:
    get:
      tags: [cities]
      summary: Beantragte Insolvenzen je Kreis (Regionalstatistik, Tier A)
      x-summary-en: Filed insolvencies per district (Regionalstatistik, Tier A)
      description: >-
        Beantragte Insolvenzen KREIS-genau (Insolvenzstatistik der Statistischen
        Ämter, Tabelle 52411, Jahressumme): unternehmensinsolvenzen (beantragte
        Unternehmensinsolvenzen, Tabelle 52411-02) und
        uebrige_schuldner_insolvenzen (beantragte Insolvenzen übriger Schuldner,
        Tabelle 52411-03; umfasst Verbraucher, ehemalige Selbständige und sonstige
        natürliche Personen, NICHT nur Verbraucher) sowie das Berichtsjahr (jahr,
        neuester Jahrgang mit beiden Werten). Toggle aus oder keine
        GENESIS-Credentials -> 200 source_status="disabled"; Batch nicht gelaufen
        -> 200 source_status="not_ingested".
      x-description-en: >-
        Filed insolvencies per DISTRICT (German insolvency statistics, table
        52411, annual total): unternehmensinsolvenzen (filed corporate
        insolvencies, table 52411-02) and uebrige_schuldner_insolvenzen (filed
        insolvencies of other debtors, table 52411-03; covers consumers, former
        self-employed and other natural persons, NOT only consumers) plus the
        reporting year (jahr, latest year with both values). Toggle off or no
        GENESIS credentials -> 200 source_status="disabled"; batch not run -> 200
        source_status="not_ingested".
      operationId: getCityInsolvencies
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Insolvenzen-Envelope (CanonicalRecord mit InsolvenciesPayload) oder null.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/solar:
    get:
      tags: [cities]
      summary: Solar-Einstrahlung + PV-Ertrag je Stadt (PVGIS, Tier A)
      x-summary-en: Solar irradiation + PV yield per city (PVGIS, Tier A)
      description: >-
        Solar-Potenzial je Stadt aus der keylosen PVGIS-Rechen-API (EU JRC,
        PVcalc). PVGIS rechnet jede EU-Koordinate, daher sind alle Städte
        abgedeckt. Klimatologisches Mehrjahresmittel am Stadtzentrum, normiert auf
        1 kWp bei optimalem Neigungswinkel: annual_yield_kwh_kwp (Jahresertrag
        kWh/kWp), annual_irradiation_kwh_m2 (Globalstrahlung kWh/m²),
        optimal_slope_deg/optimal_azimuth_deg und 12 Monatswerte (monthly).
        system_loss_pct ist der konfigurierte (gewollte) Systemverlust aus
        Verkabelung/Wechselrichter/Verschmutzung (an PVGIS übergebener
        loss-Parameter, i.d.R. 14 %), NICHT die Gesamt-Performance-Differenz;
        diese steht separat als total_performance_delta_pct (PVGIS l_total, inkl.
        Temperatur-/Einstrahlungs-/Winkel-Effekten, negativ = Gesamtminderung).
        Kein Messzeitpunkt, daher observed_at=null; der Bezugszeitraum steht als
        period_start/period_end. Tier A (EU-Reuse-Policy). Toggle aus -> 200
        source_status="disabled"; toter Upstream ohne Cache -> 503 mit Hint.
      x-description-en: >-
        Solar potential per city from the keyless PVGIS computation API (EU JRC,
        PVcalc). PVGIS computes any EU coordinate, so all cities are covered. A
        multi-year climatological average at the city centre, normalized to a
        1 kWp system at the optimal tilt: annual_yield_kwh_kwp (annual yield
        kWh/kWp), annual_irradiation_kwh_m2 (global irradiation kWh/m²),
        optimal_slope_deg/optimal_azimuth_deg and 12 monthly values (monthly).
        system_loss_pct is the configured (intended) system loss from
        cabling/inverter/soiling (the loss parameter passed to PVGIS, typically
        14 %), NOT the overall performance delta; that is carried separately as
        total_performance_delta_pct (PVGIS l_total, incl. temperature/irradiance/
        angle effects, negative = overall reduction). No observation timestamp, so
        observed_at=null; the reference period is carried as
        period_start/period_end. Tier A (EU reuse policy). Toggle off -> 200
        source_status="disabled"; dead upstream with no cache -> 503 with a hint.
      operationId: getCitySolar
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Solar-Envelope (CanonicalRecord mit SolarPayload) oder null.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/parking:
    get:
      tags: [cities]
      summary: Parkhaus-Belegung/-Katalog je Stadt (EIN Endpunkt mit Quellen-Fallback)
      x-summary-en: Parking occupancy/catalogue per city (one endpoint, source fallback)
      description: >-
        Parkhaus-Daten je Stadt aus EINEM Endpunkt mit Quellen-Fallback (Dedup):
        bevorzugt ParkenDD-Live-Belegung (frei/gesamt je Parkhaus, 13 Städte mit
        verifiziert offener Lizenz, keylos), für München der statische
        Open-Data-Standortkatalog (DL-DE/BY 2.0, ohne Live-Belegung), für
        Frankfurt am Main, Wuppertal und Magdeburg die Mobilithek-Quellen der
        Städte (DATEX II, Live-Belegung + Stammdaten, DL-DE/BY bzw. DL-DE/Zero). Lizenz/Tier
        kommen pro Datensatz aus der am Ursprung verifizierten Quelle (alle Tier A:
        z.B. Köln/Dortmund DL-DE/Zero 2.0, Hamburg DL-DE/BY 2.0, Aachen/Ulm CC0,
        Karlsruhe/Heidelberg CC BY 4.0). Löst den deprecateten
        Pfad /api/v1/live/dortmund/parking ab. Toggle aus -> 200
        source_status="disabled"; nicht-abgedeckte Stadt -> 200
        source_status="not_covered" (mit covered_cities); abgedeckte Stadt ohne
        Parkhaus -> 200 source_status="no_data". Staleness-Guard: ist der
        ParkenDD-Datenstand (last_updated) aelter als 48 Stunden (eingefrorener
        Upstream, z.B. Koeln seit 2021), antwortet die Stadt ehrlich mit
        source_status="no_data" statt einer veralteten "Live"-Belegung;
        liefert der Ursprung wieder, kippt sie automatisch auf ok zurueck.
      x-description-en: >-
        Per-city parking data from ONE endpoint with a source fallback (dedup):
        prefers ParkenDD live occupancy (free/total per facility, 13 cities with a
        verified open license, key-less), and for Munich the static open-data
        location catalogue (DL-DE/BY 2.0, no live occupancy), and for Frankfurt,
        Wuppertal and Magdeburg the cities own Mobilithek sources (DATEX II,
        live occupancy + master data, DL-DE/BY or DL-DE/Zero). License/tier are per
        record from the source verified at origin (all Tier A: e.g. Cologne/Dortmund
        DL-DE/Zero 2.0, Hamburg DL-DE/BY 2.0, Aachen/Ulm CC0, Karlsruhe/Heidelberg
        CC BY 4.0). Supersedes the
        deprecated path /api/v1/live/dortmund/parking. Toggle off -> 200
        source_status="disabled"; city not covered -> 200
        source_status="not_covered" (with covered_cities); covered city without a
        parking facility -> 200 source_status="no_data". Staleness guard: if the
        ParkenDD data timestamp (last_updated) is older than 48 hours (frozen
        upstream, e.g. Cologne since 2021), the city honestly answers with
        source_status="no_data" instead of an outdated "live" occupancy; once
        the origin delivers again, it automatically flips back to ok.
      operationId: getCityParking
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Parking-Envelope (CanonicalRecord mit ParkingPayload) oder null.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/cities/{slug}/bike-counts:
    get:
      tags: [cities]
      summary: Radzählstellen je Stadt (kommunale Open Data, Tier A)
      x-summary-en: Bicycle counting stations per city (municipal open data, Tier A)
      description: >-
        Radzählstellen (Dauerzählstellen) je Stadt aus kommunalen Open-Data-Quellen,
        je Ursprung lizenzverifiziert. Liefert je Zählstelle (counts) Name,
        Koordinaten, Zählwert (value), Granularität (granularity, z.B. "day") und
        ggf. die Werte je Richtung (directions). Eco-Counter/Eco-Visio ist bewusst
        NICHT eingebunden (Lizenz ungeklärt). Aktuell: München (Mobilitätsreferat,
        DL-DE/BY 2.0, Tagessummen der 6 Dauerzählstellen). Teilabgedeckt: Toggle aus
        -> 200 source_status="disabled"; nicht-abgedeckte Stadt -> 200
        source_status="not_covered" (mit covered_cities); abgedeckte Stadt ohne
        Zählstelle -> 200 source_status="no_data". Granularität je Quelle (siehe
        counts[].granularity): München Tag, Leipzig/Hamburg/Berlin Stunde,
        Stuttgart/Köln/Essen/Düsseldorf Jahr. Lizenzen je Ursprung (München/Leipzig/Hamburg/Essen/Düsseldorf
        DL-DE/BY 2.0, Berlin/Köln DL-DE/Zero 2.0, Stuttgart CC BY 4.0).
      x-description-en: >-
        Bicycle counting stations (permanent counters) per city from municipal
        open-data sources, license-verified at origin. Each station (counts)
        carries name, coordinates, value, granularity (e.g. "day") and optionally
        per-direction values (directions). Eco-Counter/Eco-Visio is deliberately
        NOT included (license unclear). Currently: Munich (Mobilitätsreferat,
        DL-DE/BY 2.0, daily totals of the 6 permanent counters). Partial coverage:
        toggle off -> 200 source_status="disabled"; city not covered -> 200
        source_status="not_covered" (with covered_cities); covered city without a
        station -> 200 source_status="no_data".
      operationId: getCityBikeCounts
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Bike-Counts-Envelope (CanonicalRecord mit CountStationPayload) oder null.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/cities/{slug}/office-wait-times:
    get:
      tags: [cities]
      summary: Behörden-Wartezeiten je Stadt (live, keylos, Tier A)
      x-summary-en: Government office wait times per city (live, keyless, Tier A)
      description: >-
        Live-Wartezeiten der Bürgerämter/Kundenzentren + Kfz-Zulassungsstelle je
        Stadt aus dem offenen Feed der Stadt. Liefert je Standort (offices) name,
        wait_minutes (Wartezeit in Minuten, null bei fehlendem/nicht-numerischem
        Wert), is_open (bool), status_text (Klartext), detail_url (Detailseite) und
        observed_at (ISO-UTC). Reine Live-Daten (keine Speicherung). Aktuell: Köln
        (waiting-od.php, DL-DE/Zero 2.0, minütlich während der Öffnungszeiten).
        Teilabgedeckt: Toggle aus -> 200 source_status="disabled"; nicht-abgedeckte
        Stadt -> 200 source_status="not_covered" (mit covered_cities); abgedeckte
        Stadt ohne Standort -> 200 source_status="no_data"; toter Upstream ohne
        Cache -> 503 mit Hint. Kaputte Upstream-Felder führen nie zu 500 (Zero-Trust).
      x-description-en: >-
        Live wait times of citizen offices/service centres and the vehicle
        registration office per city from the city's open feed. Each office
        carries name, wait_minutes (null when missing/non-numeric), is_open,
        status_text, detail_url and observed_at (ISO-UTC). Live-only (not persisted).
        Currently: Cologne (waiting-od.php, DL-DE/Zero 2.0). Partial coverage:
        toggle off -> 200 source_status="disabled"; city not covered -> 200
        source_status="not_covered" (with covered_cities); covered city without an
        office -> 200 source_status="no_data". Broken upstream fields never cause 500.
      operationId: getCityOfficeWaitTimes
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Office-Wait-Times-Envelope (CanonicalRecord mit OfficeWaitTimesPayload) oder null.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/cities/{slug}/solar-roofs:
    get:
      tags: [cities]
      summary: Dach-Solarkataster je Stadt, föderiert (Tier A)
      x-summary-en: Rooftop solar cadastre per city, federated (Tier A)
      description: >-
        Dach-Solarkataster je Stadt aus den amtlichen Aggregaten der Länder
        (NRW: Solarkataster NRW, MaStR/LANUK/Geobasis NRW, DL-DE/Zero 2.0; Bayern:
        Energie-Atlas Bayern, CC BY 4.0; Berlin: Umweltatlas/SenMVKU, DL-DE/Zero 2.0;
        Hamburg: LGV-Solarpotenzialanalyse, DL-DE/BY 2.0). Liefert das gesamte
        installierbare Dach-PV-Potenzial (potential_kwp, potential_yield_mwh), den
        bereits installierten Bestand (installed_kwp, installed_yield_mwh), den
        Ausschöpfungsgrad (exploitation_pct) und die Aufschlüsselung des Potenzials
        je Gebäudekategorie (potential_by_category). Der Umfang variiert je Quelle
        (z.B. Hamburg nur Potenzial, keine Kategorien). Anders als /solar
        (PVGIS-Einstrahlung je kWp) trägt diese Route die Mengen je Stadt.
        Teilabgedeckt, föderiert je Bundesland: Toggle aus -> 200
        source_status="disabled"; Stadt außerhalb der abgedeckten Länder -> 200
        source_status="not_covered" (mit covered_cities); abgedeckte Stadt ohne
        Seed-Eintrag -> 200 source_status="no_data".
      x-description-en: >-
        Rooftop solar cadastre per city from the official state aggregates (NRW:
        Solarkataster NRW, MaStR/LANUK/Geobasis NRW, DL-DE/Zero 2.0; Bavaria:
        Energie-Atlas Bayern, CC BY 4.0; Berlin: Umweltatlas/SenMVKU, DL-DE/Zero 2.0;
        Hamburg: LGV solar potential analysis, DL-DE/BY 2.0). Returns the total
        installable rooftop PV potential (potential_kwp, potential_yield_mwh), the
        already installed stock (installed_kwp, installed_yield_mwh), the
        exploitation ratio (exploitation_pct) and the potential broken down per
        building category (potential_by_category). Coverage varies by source (e.g.
        Hamburg potential only, no categories). Unlike /solar (PVGIS irradiation per
        kWp) this route carries the per-city quantities. Partial coverage, federated
        per state: toggle off -> 200 source_status="disabled"; city outside covered
        states -> 200 source_status="not_covered" (with covered_cities); covered city
        without a seed entry -> 200 source_status="no_data".
      operationId: getCitySolarRoofs
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Solar-Roofs-Envelope (CanonicalRecord mit SolarRoofsPayload) oder null.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/district-heating:
    get:
      tags: [cities]
      summary: Fernwärme- & Wärmenetz-Versorgung je Stadt, föderiert (Tier A)
      x-summary-en: District-heating / heat-network supply per city, federated (Tier A)
      description: >-
        Fernwärme-/Wärmenetz-Versorgung je Stadt aus den amtlichen Geodaten der
        kommunalen Wärmeplanung, föderiert je Stadt-WFS (Berlin: Energienetze,
        Netzgebiet Fernwärme einschl. 250 m Puffer, DL-DE/Zero 2.0; Hamburg:
        Gebiete mit Wärmenetz der kommunalen Wärmeplanung, DL-DE/BY 2.0). Liefert
        die Netzbetreiber (operators, operator_count), die Zahl der Versorgungs-/
        Netzflächen (network_area_count) und je nach Quelle die versorgte Fläche
        (supplied_area_km2, Berlin) bzw. die Hausanschlüsse und Trassenlänge
        (house_connections, network_length_km, Hamburg) sowie je Betreiber ein
        Detail-Aggregat (networks). Read-only aus dem Batch-Store (kein WFS im
        Request-Pfad). Teilabgedeckt, föderiert je Stadt: Toggle aus -> 200
        source_status="disabled"; Stadt außerhalb -> 200
        source_status="not_covered" (mit covered_cities); abgedeckte Stadt ohne
        Snapshot -> 200 source_status="not_ingested".
      x-description-en: >-
        District-heating / heat-network supply per city from the official
        municipal heat-planning geodata, federated per-city WFS (Berlin:
        Energienetze, heat-network supply area incl. 250 m buffer, DL-DE/Zero 2.0;
        Hamburg: areas with a heat network from municipal heat planning,
        DL-DE/BY 2.0). Returns the network operators (operators, operator_count),
        the number of supply/network areas (network_area_count) and, depending on
        the source, the supplied area (supplied_area_km2, Berlin) or the house
        connections and trench length (house_connections, network_length_km,
        Hamburg), plus a per-operator detail aggregate (networks). Read-only from
        the batch store (no WFS in the request path). Partial coverage, federated
        per city: toggle off -> 200 source_status="disabled"; city outside ->
        200 source_status="not_covered" (with covered_cities); covered city without
        a snapshot -> 200 source_status="not_ingested".
      operationId: getCityDistrictHeating
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: >-
            District-Heating-Envelope (CanonicalRecord mit DistrictHeatingPayload)
            oder null.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/sharing:
    get:
      tags: [cities]
      summary: Bike-/Scooter-Sharing je Stadt, aggregiert (GBFS, Tier A)
      x-summary-en: Bike/scooter sharing per city, aggregated (GBFS, Tier A)
      description: >-
        Live-Bike-/Scooter-Sharing je Stadt, aus offenen GBFS-Feeds der
        kuratierten Tier-A-Anbieter (Nextbike, CC0; für Frankfurt am Main
        DB Call a Bike über MobiData BW, DL-DE/BY-2.0) im Stadtgebiet zu
        einer Kennzahl aggregiert: vehicles_available (frei abgestellt + an
        Stationen), station_count und je Anbieter eine fail-closed verifizierte
        Tier-A-license_id. Toggle aus -> 200 source_status="disabled"; Stadt ohne
        kuratiertes GBFS-System -> 200 source_status="not_covered" (mit
        covered_cities); kein akzeptierter Tier-A-Anbieter -> 200
        source_status="no_data".
      x-description-en: >-
        Live bike/scooter sharing per city, aggregated from open GBFS feeds of the
        curated Tier-A providers (Nextbike, CC0; for Frankfurt am Main
        DB Call a Bike via MobiData BW, DL-DE/BY-2.0) within the city
        area: vehicles_available (free-floating + docked), station_count and a
        fail-closed verified Tier-A license_id per provider. Toggle off -> 200
        source_status="disabled"; city without a curated GBFS system -> 200
        source_status="not_covered" (with covered_cities); no accepted Tier-A
        provider -> 200 source_status="no_data".
      operationId: getCitySharing
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Sharing-Envelope (CanonicalRecord mit SharingPayload) oder null.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/health:
    get:
      tags: [cities]
      summary: Krankenhaus-Stammdaten je Stadt (Destatis-Verzeichnis, Tier A)
      x-summary-en: Hospital master data per city (Destatis directory, Tier A)
      description: >-
        Liefert die Krankenhaus-Stammdaten einer Stadt aus dem
        Destatis-Krankenhausverzeichnis (über GENESIS) im kanonischen Envelope.
        Die Attribution trägt den exakten Destatis-Wortlaut (nicht pauschal
        DL-DE/BY). Tier A. Deaktiviert -> 200 source_status="disabled". Toter
        Upstream ohne Cache -> 503 mit Hint.
      x-description-en: >-
        Returns a city's hospital master data from the Destatis hospital
        directory (via GENESIS) in the canonical envelope. The attribution
        carries the exact Destatis wording (not a generic DL-DE/BY). Tier A.
        Disabled -> 200 source_status="disabled". Dead upstream with no cache ->
        503 with a hint.
      operationId: getCityHealth
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Krankenhaus-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/icu-live:
    get:
      tags: [cities]
      summary: Intensivbetten-Belegung live je Stadt (DIVI, Tier C)
      x-summary-en: Live ICU bed occupancy per city (DIVI, Tier C)
      description: >-
        Liefert die klinikscharfe Intensivbetten-Belegung im Kreis einer Stadt
        aus dem DIVI-Intensivregister (RKI) im kanonischen Envelope. Tier C, nur
        live (Datenbank-Schutzrecht, kein Zwischenbestand). Deaktiviert -> 200
        source_status="disabled". Toter Upstream ohne Cache -> 503 mit Hint.
      x-description-en: >-
        Returns the per-clinic ICU bed occupancy in a city's district from the
        DIVI intensive-care register (RKI) in the canonical envelope. Tier C,
        live only (database protection right, no intermediate store).
        Disabled -> 200 source_status="disabled". Dead upstream with no cache ->
        503 with a hint.
      operationId: getCityIcuLive
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: ICU-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/road-events:
    get:
      tags: [cities]
      summary: Innerstädtische Baustellen und Sperrungen je Stadt (Tier A)
      x-summary-en: Inner-city roadworks and closures per city (Tier A)
      description: >-
        Liefert innerstädtische Baustellen und Sperrungen einer Stadt aus den
        schlüssellosen Pro-Stadt-Quellen (Berlin VIZ, Hamburg, Köln, München,
        MobiData BW, Dortmund, SPERRINFOSYS Sachsen) im kanonischen Envelope.
        Teilabdeckung (kuratierte Connectoren): berlin, hamburg, koeln, muenchen,
        stuttgart, dortmund, bremen, dresden, leipzig liefern Daten. Eine Stadt ohne Connector antwortet ehrlich mit 200
        source_status="not_covered", data=null und meta.covered_cities (Liste der
        abgedeckten Städte), klar unterscheidbar von no_data (Connector vorhanden,
        aber gerade keine Ereignisse). Tier A. Deaktiviert -> 200
        source_status="disabled". Toter Upstream ohne Cache -> 503 mit Hint.
      x-description-en: >-
        Returns a city's inner-city roadworks and closures from the keyless
        per-city sources (Berlin VIZ, Hamburg, Cologne, Munich, MobiData BW,
        Dortmund, SPERRINFOSYS Saxony) in the canonical envelope. Partial
        coverage (curated connectors): only berlin, hamburg, koeln, muenchen,
        stuttgart, dortmund, bremen, dresden, leipzig return data. A city without a
        connector honestly responds with 200 source_status="not_covered",
        data=null and meta.covered_cities (the list of covered cities), clearly
        distinct from no_data (connector present, but currently no events).
        Tier A. Disabled -> 200 source_status="disabled". Dead upstream with no
        cache -> 503 with a hint.
      operationId: getCityRoadEvents
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Road-Event-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/events:
    get:
      tags: [cities]
      summary: Veranstaltungen je Stadt (destination.one)
      x-summary-en: Events per city (destination.one)
      description: >-
        Aktuelle und kommende Veranstaltungen einer Stadt aus der
        account-gebundenen Quelle destination.one/eT4.META im kanonischen
        Envelope. Die Lizenz wird je Datensatz aus dem Lizenzfeld abgeleitet
        (CC0/CC-BY, CC-BY-SA, sonst unbekannt) und in attribution gespiegelt.
        Vergangene Termine werden herausgefiltert. Sonderfälle: ist die Quelle
        aus oder fehlt der licensekey, kommt 200 mit source_status="disabled";
        liefert die Quelle nichts Aktuelles, kommt 200 mit
        source_status="no_data"; toter Upstream ohne Cache liefert 503 mit Hint.
      x-description-en: >-
        Current and upcoming events for a city from the account-bound source
        destination.one/eT4.META in the canonical envelope. The license is
        derived per record from the license field (CC0/CC-BY, CC-BY-SA, otherwise
        unknown) and mirrored in attribution. Past dates are filtered out.
        Special cases: if the source is off or the licensekey is missing it
        returns 200 with source_status="disabled"; if the source has nothing
        current it returns 200 with source_status="no_data"; a dead upstream with
        no cache returns 503 with a hint.
      operationId: getCityEvents
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Event-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/cities/{slug}/webcams:
    get:
      tags: [cities]
      summary: Autobahn-Webcams je Stadt (Autobahn-API, Tier A)
      x-summary-en: Autobahn webcams per city (Autobahn API, Tier A)
      description: >-
        Liefert Autobahn-Webcams (Koordinaten und Bild-URLs) im Umfeld einer
        Stadt aus dem schlüssellosen Autobahn-Webcam-Service. Die Webcams werden
        per Bounding-Box um die Stadt gefiltert. Tier A. Flächendeckend: jeder der
        84 Städte sind die nahen Autobahnen kuratiert zugeordnet; eine Stadt ohne
        Webcam antwortet ehrlich mit no_data. Weitere Sonderfälle: deaktiviert -> 200
        source_status="disabled"; toter Upstream ohne Cache -> 503 mit Hint.
        Hinweis: Die Autobahn-Webcam-Quelle liefert aktuell bundesweit keine
        Webcams, daher antwortet der Endpunkt derzeit durchgehend mit no_data.
      x-description-en: >-
        Returns Autobahn webcams (coordinates and image URLs) around a city from
        the keyless Autobahn webcam service. The webcams are filtered by a
        bounding box around the city. Tier A. Full coverage: each of the 84 cities
        has its nearby motorways curated; a city with no webcam honestly responds
        with no_data. Other special
        cases: disabled -> 200 source_status="disabled"; dead upstream with no
        cache -> 503 with a hint. Note: the Autobahn webcam source currently
        returns no webcams nationwide, so this endpoint currently always responds
        with no_data.
      operationId: getCityWebcams
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Webcam-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/{city}/traffic-flow:
    get:
      tags: [Live]
      summary: Live-Verkehrslage je Stadt (Mobilithek DATEX-II)
      x-summary-en: Live traffic flow per city (Mobilithek DATEX-II)
      description: >-
        Liefert die minutenfrische Verkehrslage einer Stadt aus der Mobilithek
        (DATEX-II) im Live-Envelope. Der meta-Block trägt zusätzlich as_of
        (Datenstand) und refresh_seconds (Aktualisierungstakt). Solange
        Zertifikat und Abo noch nicht gesetzt sind -> 200
        source_status="disabled" (nie 5xx); kein Datenpaket -> 200
        source_status="no_data".
      x-description-en: >-
        Returns a city's minute-fresh traffic flow from the Mobilithek (DATEX-II)
        in the live envelope. The meta block additionally carries as_of (data
        timestamp) and refresh_seconds (update cadence). As long as the
        certificate and subscription are not set up -> 200
        source_status="disabled" (never 5xx); no data packet -> 200
        source_status="no_data".
      operationId: getLiveTrafficFlow
      parameters:
        - name: city
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Live-Verkehrslage-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/{city}/baustellen:
    get:
      tags: [Live]
      summary: Live-Baustellen je Stadt (Mobilithek DATEX-II)
      x-summary-en: Live roadworks per city (Mobilithek DATEX-II)
      description: >-
        Liefert die Baustellen einer Stadt aus der Mobilithek (DATEX-II
        SituationPublication) im Live-Envelope. Der meta-Block trägt zusätzlich
        as_of (Datenstand) und refresh_seconds (Aktualisierungstakt). Solange
        Zertifikat und Abo nicht gesetzt sind -> 200 source_status="disabled"
        (nie 5xx); kein Datenpaket -> 200 source_status="no_data". Reine
        Live-Daten, nur im Redis-Cache gehalten.
      x-description-en: >-
        Returns a city's roadworks from the Mobilithek (DATEX-II
        SituationPublication) in the live envelope. The meta block additionally
        carries as_of (data timestamp) and refresh_seconds (update cadence). As
        long as the certificate and subscription are not set up -> 200
        source_status="disabled" (never 5xx); no data packet -> 200
        source_status="no_data". Live data only, kept in the Redis cache.
      operationId: getLiveBaustellen
      parameters:
        - name: city
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Live-Baustellen-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/{city}/ereignisse:
    get:
      tags: [Live]
      summary: Live-Verkehrsereignisse je Stadt (Mobilithek DATEX-II)
      x-summary-en: Live traffic events per city (Mobilithek DATEX-II)
      description: >-
        Liefert die Verkehrsereignisse einer Stadt aus der Mobilithek (DATEX-II
        SituationPublication) im Live-Envelope. Der meta-Block trägt zusätzlich
        as_of (Datenstand) und refresh_seconds (Aktualisierungstakt). Solange
        Zertifikat und Abo nicht gesetzt sind -> 200 source_status="disabled"
        (nie 5xx); kein Datenpaket -> 200 source_status="no_data". Reine
        Live-Daten, nur im Redis-Cache gehalten.
      x-description-en: >-
        Returns a city's traffic events from the Mobilithek (DATEX-II
        SituationPublication) in the live envelope. The meta block additionally
        carries as_of (data timestamp) and refresh_seconds (update cadence). As
        long as the certificate and subscription are not set up -> 200
        source_status="disabled" (never 5xx); no data packet -> 200
        source_status="no_data". Live data only, kept in the Redis cache.
      operationId: getLiveEreignisse
      parameters:
        - name: city
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Live-Ereignisse-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/berlin/verkehrsmeldungen:
    get:
      tags: [Live]
      summary: Live-Verkehrsmeldungen Berlin (Mobilithek DATEX-II)
      x-summary-en: Live traffic messages for Berlin (Mobilithek DATEX-II)
      description: >-
        Liefert die Berlin-Verkehrsmeldungen (SenMVKU, DATEX-II
        SituationPublication) im Live-Envelope. Der meta-Block trägt zusätzlich
        as_of (Datenstand) und refresh_seconds (Aktualisierungstakt). Solange
        Zertifikat und Abo nicht gesetzt sind -> 200 source_status="disabled"
        (nie 5xx); kein Datenpaket -> 200 source_status="no_data". Reine
        Live-Daten, nur im Redis-Cache gehalten.
      x-description-en: >-
        Returns Berlin traffic messages (SenMVKU, DATEX-II SituationPublication)
        in the live envelope. The meta block additionally carries as_of (data
        timestamp) and refresh_seconds (update cadence). As long as the
        certificate and subscription are not set up -> 200
        source_status="disabled" (never 5xx); no data packet -> 200
        source_status="no_data". Live data only, kept in the Redis cache.
      operationId: getLiveBerlinVerkehrsmeldungen
      responses:
        "200":
          description: Live-Verkehrsmeldungen-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/koeln/umweltzone:
    get:
      tags: [Live]
      summary: Live-Umweltzone Köln (Mobilithek DATEX-II)
      x-summary-en: Live low-emission zone for Cologne (Mobilithek DATEX-II)
      description: >-
        Liefert die Köln-Umweltzone (LowEmissionZone, MoCKiii, DATEX-II
        SituationPublication) im Live-Envelope. Der meta-Block trägt zusätzlich
        as_of (Datenstand) und refresh_seconds (Aktualisierungstakt). Solange
        Zertifikat und Abo nicht gesetzt sind -> 200 source_status="disabled"
        (nie 5xx); kein Datenpaket -> 200 source_status="no_data". Reine
        Live-Daten, nur im Redis-Cache gehalten.
      x-description-en: >-
        Returns the Cologne low-emission zone (MoCKiii, DATEX-II
        SituationPublication) in the live envelope. The meta block additionally
        carries as_of (data timestamp) and refresh_seconds (update cadence). As
        long as the certificate and subscription are not set up -> 200
        source_status="disabled" (never 5xx); no data packet -> 200
        source_status="no_data". Live data only, kept in the Redis cache.
      operationId: getLiveKoelnUmweltzone
      responses:
        "200":
          description: Live-Umweltzone-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/hannover/verkehrsmeldungen:
    get:
      tags: [Live]
      summary: Live-Verkehrsmeldungen Hannover (Mobilithek DATEX-II)
      x-summary-en: Live traffic messages for Hanover (Mobilithek DATEX-II)
      description: >-
        Liefert die Hannover-Verkehrsmeldungen (Landeshauptstadt Hannover,
        Fachbereich Tiefbau: Baustellen, verkehrsrelevante Veranstaltungen,
        Verkehrsstörungen, DATEX-II SituationPublication) im Live-Envelope. Der
        meta-Block trägt zusätzlich as_of (Datenstand) und refresh_seconds
        (Aktualisierungstakt). Solange Zertifikat und Abo nicht gesetzt sind ->
        200 source_status="disabled" (nie 5xx); kein Datenpaket -> 200
        source_status="no_data". Reine Live-Daten, nur im Redis-Cache gehalten.
      x-description-en: >-
        Returns Hanover traffic messages (City of Hanover, road construction
        department: roadworks, traffic-relevant events, traffic disruptions,
        DATEX-II SituationPublication) in the live envelope. The meta block
        additionally carries as_of (data timestamp) and refresh_seconds (update
        cadence). As long as the certificate and subscription are not set up ->
        200 source_status="disabled" (never 5xx); no data packet -> 200
        source_status="no_data". Live data only, kept in the Redis cache.
      operationId: getLiveHannoverVerkehrsmeldungen
      responses:
        "200":
          description: Live-Verkehrsmeldungen-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/dortmund/parking:
    get:
      tags: [Live]
      deprecated: true
      summary: "[DEPRECATED] Live-Parkbelegung Dortmund (Nachfolger: /cities/dortmund/parking)"
      x-summary-en: "[DEPRECATED] Live parking occupancy Dortmund (successor: /cities/dortmund/parking)"
      description: >-
        DEPRECATED (DATA-40): abgelöst vom vereinheitlichten Endpunkt
        GET /api/v1/cities/dortmund/parking (EIN Parking-Endpunkt für alle Städte
        mit Quellen-Fallback). Dieser Pfad bleibt rückwärtskompatibel und trägt
        Deprecation-/Link-Header auf den Nachfolger.
        Liefert die Dortmund-Parkbelegung (dynamisches Parkleitsystem, Parkhäuser
        und Park&Ride) im Live-Envelope. Quelle ist der direkte, keylose offene
        Datensatz der Stadt Dortmund (open-data.dortmund.de), nicht die
        Mobilithek. Der meta-Block trägt zusätzlich as_of (Datenstand) und
        refresh_seconds (Aktualisierungstakt). Quelle deaktiviert -> 200
        source_status="disabled" (nie 5xx); keine Daten -> 200
        source_status="no_data". Lizenz Datenlizenz Deutschland Zero 2.0.
        Reine Live-Daten, nur im Redis-Cache gehalten.
      x-description-en: >-
        DEPRECATED (DATA-40): superseded by the unified endpoint
        GET /api/v1/cities/dortmund/parking (one parking endpoint for all cities
        with a source fallback). This path stays backward-compatible and carries
        Deprecation/Link headers pointing to the successor.
        Returns Dortmund parking occupancy (dynamic parking guidance system,
        car parks and park&ride) in the live envelope. Source is the City of
        Dortmund's direct, key-less open dataset (open-data.dortmund.de), not the
        Mobilithek. The meta block additionally carries as_of (data timestamp)
        and refresh_seconds (update cadence). Source disabled -> 200
        source_status="disabled" (never 5xx); no data -> 200
        source_status="no_data". Licence Data licence Germany Zero 2.0. Live data
        only, kept in the Redis cache.
      operationId: getLiveDortmundParking
      responses:
        "200":
          description: Live-Parkbelegung-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/frankfurt-am-main/parking:
    get:
      tags: [Live]
      summary: Live-Parkbelegung Frankfurt am Main (Stadt Frankfurt, Mobilithek DATEX II V3)
      x-summary-en: Live parking occupancy for Frankfurt am Main (City of Frankfurt, Mobilithek DATEX II V3)
      description: >-
        Liefert die Parkbelegung der Stadt Frankfurt am Main im Live-Envelope:
        je Parkplatz freie Plätze, Auslastung in Prozent und gestufte Auslastung
        (facilities), angereichert um Name, Geo-Koordinate und Kapazität aus den
        statischen Stammdaten. Quelle sind zwei Mobilithek-Abos (DATEX II V3,
        dynamisch + statisch), die der Adapter über die parkingRecord-ID joint.
        Der meta-Block trägt zusätzlich as_of (Datenstand) und refresh_seconds
        (Aktualisierungstakt). Quelle deaktiviert -> 200 source_status="disabled"
        (nie 5xx); keine Daten -> 200 source_status="no_data". Lizenz Datenlizenz
        Deutschland Namensnennung 2.0. Reine Live-Daten, nur im Redis-Cache.
      x-description-en: >-
        Returns the City of Frankfurt am Main parking occupancy in the live
        envelope: per car park the vacant spaces, occupancy percentage and graded
        occupancy (facilities), enriched with name, geo coordinate and capacity
        from the static master data. Source is two Mobilithek subscriptions
        (DATEX II V3, dynamic + static) joined by the adapter via the
        parkingRecord id. The meta block additionally carries as_of (data
        timestamp) and refresh_seconds (update cadence). Source disabled -> 200
        source_status="disabled" (never 5xx); no data -> 200
        source_status="no_data". Licence Data licence Germany Attribution 2.0.
        Live data only, kept in the Redis cache.
      operationId: getLiveFrankfurtParking
      responses:
        "200":
          description: Live-Parkbelegung-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/magdeburg/parking:
    get:
      tags: [Live]
      summary: Live-Parkbelegung Magdeburg (Landeshauptstadt Magdeburg, Mobilithek DATEX II V2)
      x-summary-en: Live parking occupancy for Magdeburg (City of Magdeburg, Mobilithek DATEX II V2)
      description: >-
        Liefert die Parkbelegung der Landeshauptstadt Magdeburg im Live-Envelope:
        je Parkplatz freie Plätze, Auslastung in Prozent, Belegungstrend und Status
        (facilities), angereichert um Name, Geo-Koordinate und Kapazität aus den
        statischen Stammdaten. Quelle sind zwei Mobilithek-Abos (DATEX II V2
        ParkingFacility, dynamisch + statisch, Anbieter ifak e.V.), die der Adapter
        über die parkingFacility-ID joint. Der meta-Block trägt zusätzlich as_of
        (Datenstand) und refresh_seconds (Aktualisierungstakt). Quelle deaktiviert
        -> 200 source_status="disabled" (nie 5xx); keine Daten -> 200
        source_status="no_data". Lizenz freie Nutzung/Open Data (keine benannte
        Standardlizenz). Reine Live-Daten, nur im Redis-Cache gehalten.
      x-description-en: >-
        Returns the City of Magdeburg parking occupancy in the live envelope: per
        car park the vacant spaces, occupancy percentage, occupancy trend and
        status (facilities), enriched with name, geo coordinate and capacity from
        the static master data. Source is two Mobilithek subscriptions (DATEX II
        V2 ParkingFacility, dynamic + static, provider ifak e.V.) joined by the
        adapter via the parkingFacility id. The meta block additionally carries
        as_of (data timestamp) and refresh_seconds (update cadence). Source
        disabled -> 200 source_status="disabled" (never 5xx); no data -> 200
        source_status="no_data". Licence free use/Open Data (no named standard
        licence). Live data only, kept in the Redis cache.
      operationId: getLiveMagdeburgParking
      responses:
        "200":
          description: Live-Parkbelegung-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/wuppertal/parking:
    get:
      tags: [Live]
      summary: Live-Parkbelegung Wuppertal (Stadt Wuppertal, Mobilithek DATEX II V2)
      x-summary-en: Live parking occupancy for Wuppertal (City of Wuppertal, Mobilithek DATEX II V2)
      description: >-
        Liefert die Parkbelegung der Stadt Wuppertal im Live-Envelope: je
        Parkplatz freie Plätze, Auslastung in Prozent, Belegungstrend und Status
        (facilities), angereichert um Name, Geo-Koordinate und Kapazität aus den
        statischen Stammdaten. Quelle sind zwei Mobilithek-Abos (DATEX II V2
        ParkingFacility, dynamisch + statisch), die der Adapter über die
        parkingFacility-ID joint. Der meta-Block trägt zusätzlich as_of
        (Datenstand) und refresh_seconds (Aktualisierungstakt). Quelle deaktiviert
        -> 200 source_status="disabled" (nie 5xx); keine Daten -> 200
        source_status="no_data". Lizenz Datenlizenz Deutschland Zero 2.0. Reine
        Live-Daten, nur im Redis-Cache gehalten.
      x-description-en: >-
        Returns the City of Wuppertal parking occupancy in the live envelope: per
        car park the vacant spaces, occupancy percentage, occupancy trend and
        status (facilities), enriched with name, geo coordinate and capacity from
        the static master data. Source is two Mobilithek subscriptions (DATEX II
        V2 ParkingFacility, dynamic + static) joined by the adapter via the
        parkingFacility id. The meta block additionally carries as_of (data
        timestamp) and refresh_seconds (update cadence). Source disabled -> 200
        source_status="disabled" (never 5xx); no data -> 200
        source_status="no_data". Licence Data licence Germany Zero 2.0. Live data
        only, kept in the Redis cache.
      operationId: getLiveWuppertalParking
      responses:
        "200":
          description: Live-Parkbelegung-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/hamburg/verkehrslage:
    get:
      tags: [Live]
      summary: Live-Verkehrslage Hamburg (Freie und Hansestadt Hamburg, keylos)
      x-summary-en: Live traffic situation for Hamburg (City of Hamburg open data, key-less)
      description: >-
        Liefert die Hamburger Echtzeit-Verkehrslage im Live-Envelope: eine
        Netz-Zusammenfassung (summary.total und by_state-Zählung je Zustandsklasse
        fließend/dicht/zäh/gestaut) sowie die priorisierte, gedeckelte Liste der
        nicht-fließenden Straßenabschnitte (measurements mit Zustand und
        Mittelpunkt-Koordinate). Quelle ist der direkte, keylose offene Datensatz
        der Freien und Hansestadt Hamburg (api.hamburg.de, OGC API Features), nicht
        die Mobilithek. Der meta-Block trägt zusätzlich as_of (Datenstand) und
        refresh_seconds (Aktualisierungstakt). Quelle deaktiviert -> 200
        source_status="disabled" (nie 5xx); keine Daten -> 200
        source_status="no_data". Lizenz Datenlizenz Deutschland Namensnennung 2.0.
        Reine Live-Daten, nur im Redis-Cache gehalten.
      x-description-en: >-
        Returns Hamburg's real-time traffic situation in the live envelope: a
        network summary (summary.total and by_state counts per state
        free/dense/sluggish/jammed) plus the prioritised, capped list of
        non-free-flowing road segments (measurements with state and midpoint
        coordinate). Source is the City of Hamburg's direct, key-less open dataset
        (api.hamburg.de, OGC API Features), not the Mobilithek. The meta block
        additionally carries as_of (data timestamp) and refresh_seconds (update
        cadence). Source disabled -> 200 source_status="disabled" (never 5xx); no
        data -> 200 source_status="no_data". Licence Data licence Germany
        Attribution 2.0. Live data only, kept in the Redis cache.
      operationId: getLiveHamburgVerkehrslage
      responses:
        "200":
          description: Live-Verkehrslage-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/kiel/zaehlstellen:
    get:
      tags: [Live]
      summary: Live-Zähldaten Kiel, Kfz- und Radzählstellen (Mobilithek DATEX-II)
      x-summary-en: Live count data for Kiel, vehicle and bicycle counters (Mobilithek DATEX-II)
      description: >-
        Liefert die Kiel-Zähldaten (Kfz-Dauerzählstellen und Radzähler, DATEX-II
        MeasuredDataPublication) im Live-Envelope. Der meta-Block trägt
        zusätzlich as_of (Datenstand) und refresh_seconds (Aktualisierungstakt).
        Solange Zertifikat und Abo nicht gesetzt sind -> 200
        source_status="disabled" (nie 5xx); kein Datenpaket -> 200
        source_status="no_data". Reine Live-Daten, nur im Redis-Cache gehalten.
      x-description-en: >-
        Returns Kiel count data (permanent motor-vehicle counters and bicycle
        counters, DATEX-II MeasuredDataPublication) in the live envelope. The
        meta block additionally carries as_of (data timestamp) and
        refresh_seconds (update cadence). As long as the certificate and
        subscription are not set up -> 200 source_status="disabled" (never 5xx);
        no data packet -> 200 source_status="no_data". Live data only, kept in
        the Redis cache.
      operationId: getLiveKielZaehlstellen
      responses:
        "200":
          description: Live-Zähldaten-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/eround/charging:
    get:
      tags: [Live]
      summary: Live-Ladesäulen-Belegung eRound (Mobilithek DATEX-II V3)
      x-summary-en: Live EV charging-station occupancy via eRound (Mobilithek DATEX-II V3)
      description: >-
        Liefert das jüngste eRound-Belegungs-Delta (AFIR-Recharging, DATEX-II
        V3 EnergyInfrastructureStatusPublication, JSON-Syntax) im Live-Envelope.
        Der Feed wird ausschließlich von einem Hintergrund-Poller gepullt
        (alle 5 Minuten); dieser Endpunkt liest nur dessen Redis-Snapshot,
        es findet kein Upstream-Abruf im Request statt. Der meta-Block trägt
        zusätzlich as_of (Datenstand) und refresh_seconds (Poll-Takt). Lizenz
        Creative Commons CC Zero (Tier A). Ohne Zertifikat/Abo -> 200
        source_status="disabled" (nie 5xx); kein/abgelaufener Snapshot oder
        leeres Delta -> 200 source_status="no_data". Reine Live-Daten, nur in
        Redis gehalten. Je-Stadt-Aggregation: /cities/{slug}/charging-status.
      x-description-en: >-
        Returns the most recent eRound occupancy delta (AFIR recharging,
        DATEX-II V3 EnergyInfrastructureStatusPublication, JSON syntax) in the
        live envelope. The feed is pulled exclusively by a background poller
        (every 5 minutes); this endpoint only reads its Redis snapshot, no
        upstream call happens at request time. The meta block additionally
        carries as_of (data timestamp) and refresh_seconds (poll cadence).
        License Creative Commons CC Zero (Tier A). Without certificate/
        subscription -> 200 source_status="disabled" (never 5xx); missing/
        expired snapshot or an empty delta -> 200 source_status="no_data".
        Live data only, kept in Redis. Per-city aggregation:
        /cities/{slug}/charging-status.
      operationId: getLiveEroundCharging
      responses:
        "200":
          description: Live-Ladesäulen-Belegung-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/hamburg/departures:
    get:
      tags: [Live]
      summary: Live-ÖPNV-Abfahrten Hamburg je Station (HVV-Geofox-GTI)
      x-summary-en: Live public-transit departures for Hamburg per station (HVV Geofox GTI)
      description: >-
        Liefert die Echtzeit-Abfahrtstafel einer HVV-Station aus der
        HVV-Geofox-GTI-API: je Abfahrt Linie, Richtung, Soll-Offset in Minuten,
        Verspätung in Sekunden und Linien-Störungshinweise. Stadt fix hamburg
        (Geofox deckt nur den HVV-Raum ab). Der Query-Parameter station ist ein
        Stationsname (Default Hauptbahnhof), der intern via checkName auf die
        Geofox-Station-ID aufgelöst wird. Der meta-Block trägt zusätzlich as_of
        und refresh_seconds (60). Tier C live-only: die Geofox-Lizenz ist nicht
        offen (source_status der Quelle = unknown), reine Live-Daten, nur
        kurzlebig im Redis-Cache gehalten. Toggle aus oder fehlende Credentials -> 200
        source_status="disabled"; unbekannte Station oder keine Abfahrten -> 200
        source_status="no_data"; toter Upstream ohne Cache -> 503 mit Hint.
      x-description-en: >-
        Returns the real-time departure board of an HVV station from the HVV
        Geofox GTI API: per departure the line, direction, scheduled offset in
        minutes, delay in seconds and per-line service alerts. City fixed to
        hamburg (Geofox only covers the HVV area). The station query parameter is
        a station name (default Hauptbahnhof), resolved internally via checkName
        to the Geofox station id. The meta block additionally carries as_of and
        refresh_seconds (60). Tier C live-only: the Geofox license is not open
        (source license = unknown), live data only, kept only briefly in the
        Redis cache. Disabled or
        missing credentials -> 200 source_status="disabled"; unknown station or no
        departures -> 200 source_status="no_data"; dead upstream with no cache ->
        503 with a hint.
      operationId: getLiveHamburgDepartures
      parameters:
        - name: station
          in: query
          required: false
          description: HVV-Stationsname (Default "Hamburg Hauptbahnhof").
          schema: { type: string, default: Hamburg Hauptbahnhof }
      responses:
        "200":
          description: Live-Abfahrten-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Ungültiger oder fehlender station-Parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/frankfurt-am-main/departures:
    get:
      tags: [Live]
      summary: Live-ÖPNV-Abfahrten Frankfurt/Rhein-Main je Station (RMV-HAPI)
      x-summary-en: Live public-transit departures for Frankfurt/Rhine-Main per station (RMV HAPI)
      description: >-
        Liefert die Echtzeit-Abfahrtstafel einer Rhein-Main-Station aus der
        RMV-HAPI (HAFAS-ReST, www.rmv.de/hapi): je Abfahrt Linie, Richtung,
        Minuten bis zur Abfahrt und Verspätung in Sekunden. Stadt fix
        frankfurt-am-main (RMV deckt den Rhein-Main-Raum ab). Der Query-Parameter
        station ist ein Stationsname (Default "Frankfurt (Main) Hauptbahnhof"),
        der intern via location.name auf die HAFAS-Station-ID aufgelöst wird. Der
        meta-Block trägt zusätzlich as_of und refresh_seconds (60). Tier C
        live-only: die RMV-HAPI ist registrierungspflichtig, die Lizenz nicht
        offen (source license = unknown), reine Live-Daten, nur kurzlebig im
        Redis-Cache gehalten. Toggle aus oder fehlender accessId -> 200
        source_status="disabled"; unbekannte Station oder keine Abfahrten -> 200
        source_status="no_data"; toter Upstream ohne Cache -> 503 mit Hint.
      x-description-en: >-
        Returns the real-time departure board of a Rhine-Main station from the
        RMV HAPI (HAFAS ReST, www.rmv.de/hapi): per departure the line,
        direction, minutes until departure and delay in seconds. City fixed to
        frankfurt-am-main (RMV covers the Rhine-Main area). The station query
        parameter is a station name (default "Frankfurt (Main) Hauptbahnhof"),
        resolved internally via location.name to the HAFAS station id. The meta
        block additionally carries as_of and refresh_seconds (60). Tier C
        live-only: the RMV HAPI requires registration, the license is not open
        (source license = unknown), live data only, kept only briefly in the
        Redis cache. Disabled or missing accessId -> 200 source_status="disabled";
        unknown station or no departures -> 200 source_status="no_data"; dead
        upstream with no cache -> 503 with a hint.
      operationId: getLiveFrankfurtDepartures
      parameters:
        - name: station
          in: query
          required: false
          description: RMV-Stationsname (Default "Frankfurt (Main) Hauptbahnhof").
          schema: { type: string, default: Frankfurt (Main) Hauptbahnhof }
      responses:
        "200":
          description: Live-Abfahrten-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Ungültiger oder fehlender station-Parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/nuernberg/departures:
    get:
      tags: [Live]
      summary: Live-ÖPNV-Abfahrten Nürnberg je Halt (VGN/VAG, keylos, Tier A)
      x-summary-en: Live public-transit departures for Nuremberg per stop (VGN/VAG, key-less, Tier A)
      description: >-
        Liefert die Echtzeit-Abfahrtstafel eines VGN-Halts aus der offenen,
        keylosen VAG-Puls-API (start.vag.de): je Abfahrt Linie, Richtung,
        Soll-Offset in Minuten, Verspätung in Sekunden und Produkt. Stadt fix
        nuernberg (VGN-Raum). Der Query-Parameter stop_id ist die numerische
        VGN-Halt-ID (VGNKennung, Default 510 = Nürnberg Hbf). Der meta-Block
        trägt zusätzlich as_of und refresh_seconds (60). Tier A: offene Lizenz
        Creative Commons Attribution 4.0 (opendata.vag.de), sauber verwertbar.
        Reine Live-Daten, nur kurzlebig im Redis-Cache gehalten. Toggle aus -> 200
        source_status="disabled"; ungültige stop_id -> 400; keine Abfahrten -> 200
        source_status="no_data"; toter Upstream ohne Cache -> 503 mit Hint.
      x-description-en: >-
        Returns the real-time departure board of a VGN stop from the open,
        key-less VAG Puls API (start.vag.de): per departure the line, direction,
        scheduled offset in minutes, delay in seconds and product. City fixed to
        nuernberg (VGN area). The stop_id query parameter is the numeric VGN stop
        id (VGNKennung, default 510 = Nuremberg Hbf). The meta block additionally
        carries as_of and refresh_seconds (60). Tier A: open licence Creative
        Commons Attribution 4.0 (opendata.vag.de), freely reusable. Live data
        only, kept only briefly in the Redis cache. Disabled -> 200
        source_status="disabled"; invalid stop_id -> 400; no departures -> 200
        source_status="no_data"; dead upstream with no cache -> 503 with a hint.
      operationId: getLiveNuernbergDepartures
      parameters:
        - name: stop_id
          in: query
          required: false
          description: Numerische VGN-Halt-ID (VGNKennung), Default "510" (Nürnberg Hbf).
          schema: { type: string, default: "510" }
      responses:
        "200":
          description: Live-Abfahrten-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Ungültige oder fehlende stop_id
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/{slug}/departures:
    get:
      tags: [Live]
      summary: Live-ÖPNV-Abfahrten der VRR-Kernstädte + VVS Stuttgart je Station (Mentz-EFA, Tier C)
      x-summary-en: Live public-transit departures for the VRR core cities + VVS Stuttgart per station (Mentz EFA, Tier C)
      description: >-
        Liefert die Echtzeit-Abfahrtstafel einer Station in einer der sechs
        VRR-Kernstädte (duesseldorf, dortmund, essen, duisburg, bochum,
        wuppertal, keylose Mentz-EFA efa.vrr.de) oder in Stuttgart (VVS,
        keylose Mentz-EFA www3.vvs.de): je Abfahrt Linie, Richtung, Minuten bis
        zur Abfahrt und Verspätung in Sekunden. Der Slug wählt die Stadt
        (Stadt-Scoping über das AGS5-Präfix); ein Nicht-EFA-Slug liefert 404. Der
        Query-Parameter station ist ein Stationsname (Default Hauptbahnhof der
        Stadt), intern via StopFinder auf die EFA-Global-ID aufgelöst. Der
        meta-Block trägt zusätzlich as_of und refresh_seconds (60). Tier C
        live-only: die EFA-Lizenz ist nicht klar offen (source license =
        unknown), reine Live-Daten, nur kurzlebig im Redis-Cache gehalten.
        Attribution je Verbund "Verkehrsverbund Rhein-Ruhr (VRR)" bzw.
        "Verkehrs- und Tarifverbund Stuttgart (VVS)". Toggle aus -> 200
        source_status="disabled"; unbekannte Station oder keine Abfahrten -> 200
        source_status="no_data"; toter Upstream ohne Cache -> 503 mit Hint.
      x-description-en: >-
        Returns the real-time departure board of a station in one of the six VRR
        core cities (duesseldorf, dortmund, essen, duisburg, bochum, wuppertal,
        key-less Mentz EFA efa.vrr.de) or in Stuttgart (VVS, key-less Mentz EFA
        www3.vvs.de): per departure the line, direction, minutes until departure
        and delay in seconds. The slug selects the city (city-scoping via the
        AGS5 prefix); a non-EFA slug returns 404. The station query parameter is
        a station name (default the city's main station), resolved internally via
        StopFinder to the EFA global id. The meta block additionally carries
        as_of and refresh_seconds (60). Tier C live-only: the EFA licence is not
        clearly open (source license = unknown), live data only, kept only
        briefly in the Redis cache. Attribution per network "Verkehrsverbund
        Rhein-Ruhr (VRR)" or "Verkehrs- und Tarifverbund Stuttgart (VVS)".
        Disabled -> 200 source_status="disabled"; unknown station or no
        departures -> 200 source_status="no_data"; dead upstream with no cache ->
        503 with a hint.
      operationId: getLiveVrrDepartures
      parameters:
        - name: slug
          in: path
          required: true
          description: >-
            EFA-Verbund-Stadt-Slug: VRR-Kernstädte (duesseldorf, dortmund,
            essen, duisburg, bochum, wuppertal) oder VVS Stuttgart (stuttgart).
            Andere Städte liefern 404.
          schema:
            type: string
            enum: [duesseldorf, dortmund, essen, duisburg, bochum, wuppertal, stuttgart]
        - name: station
          in: query
          required: false
          description: Stationsname (Default Hauptbahnhof der Stadt).
          schema: { type: string }
      responses:
        "200":
          description: Live-Abfahrten-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte (Nicht-EFA-)Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/{city}/transit/departures:
    get:
      tags: [Live]
      summary: Live-ÖPNV-Abfahrten je Halt mit Verspätung (GTFS-RT, Tier B)
      x-summary-en: Live public-transit departures per stop with delay (GTFS-RT, Tier B)
      description: >-
        Liefert die Live-Abfahrten eines Halts mit aktueller Verspätung aus dem
        GTFS-RT-Feed (gtfs.de bzw. Mobilithek-DELFI, CC-BY-SA = Tier B) im
        Live-Envelope (meta mit as_of und refresh_seconds=45, dem Poller-Takt).
        Der Request-Pfad liest NUR aus Redis: ein Hintergrund-Poller parst den
        Feed einmal je Takt, sodass nicht bei jedem Request ein 68-MB-Feed
        geparst wird. RT-only-Semantik: ausgeliefert werden ausschliesslich
        Trips mit Echtzeit-Update UND absoluter Abfahrtszeit; Fahrplan-Sollzeiten
        sind NICHT enthalten. Vergangene Abfahrten (aelter als eine 90-s-Grace)
        und Eintraege ohne verwertbare Zeit werden verworfen; die verbleibenden
        Abfahrten sind aufsteigend nach Zeit sortiert (naechste zuerst). meta
        weist zusaetzlich raw_updates_total und filtered_out aus (ehrliche
        Rohzahlen, keine stille Kappung). Toggle aus -> 200
        source_status="disabled"; kein Update in Redis oder alles gefiltert ->
        200 source_status="no_data"; ungültige stop_id -> 400. Reine Live-Daten,
        nur im Redis-Cache gehalten (Tier B).
      x-description-en: >-
        Returns a stop's live departures with current delay from the GTFS-RT feed
        (gtfs.de or Mobilithek-DELFI, CC-BY-SA = Tier B) in the live envelope
        (meta with as_of and refresh_seconds=45, the poller cadence). The request
        path reads ONLY from Redis: a background poller parses the feed once per
        cadence, so no 68 MB feed is parsed per request. RT-only semantics: only
        trips with a realtime update AND an absolute departure time are served;
        scheduled timetable times are NOT included. Past departures (older than a
        90 s grace) and entries without a usable time are dropped; the remaining
        departures are sorted ascending by time (soonest first). meta
        additionally exposes raw_updates_total and filtered_out (honest raw
        counts, no silent truncation). Toggle off -> 200
        source_status="disabled"; no update in Redis or everything filtered out
        -> 200 source_status="no_data"; invalid stop_id -> 400. Live data only,
        kept in the Redis cache (Tier B).
      operationId: getLiveTransitDepartures
      parameters:
        - name: city
          in: path
          required: true
          schema:
            type: string
        - name: stop_id
          in: query
          required: true
          description: DELFI-Halt-ID im Muster de:<AGS>:<id> (gegen eine Allowlist geprüft).
          x-description-en: DELFI stop ID in the pattern de:<AGS>:<id> (checked against an allowlist).
          schema:
            type: string
      responses:
        "200":
          description: Live-Abfahrten-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Ungültige oder fehlende stop_id
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/{city}/transit/trips/{trip_id}:
    get:
      tags: [Live]
      summary: Live-ÖPNV-Fahrt-Detail inkl. geschätzter Position (GTFS-RT, Tier B)
      x-summary-en: Live public-transit trip detail incl. estimated position (GTFS-RT, Tier B)
      description: >-
        Liefert das Fahrt-Detail (Verspätung, Halt-Updates) inklusive linear
        interpolierter Position (als estimated=true gekennzeichnet) aus dem
        GTFS-RT-Feed (Tier B) im Live-Envelope (refresh_seconds=45). Der
        Request-Pfad liest NUR aus Redis. Lässt sich die Fahrt nicht gegen das
        statische GTFS auflösen, trägt der Payload unresolved=true (ehrlich statt
        500). Toggle aus -> disabled; kein Update -> no_data. Reine Live-Daten,
        nur im Redis-Cache gehalten (Tier B).
      x-description-en: >-
        Returns the trip detail (delay, stop updates) including a
        linearly-interpolated position (flagged as estimated=true) from the
        GTFS-RT feed (Tier B) in the live envelope (refresh_seconds=45). The
        request path reads ONLY from Redis. If the trip cannot be resolved
        against the static GTFS, the payload carries unresolved=true (honest
        instead of 500). Toggle off -> disabled; no update -> no_data. Live data
        only, kept in the Redis cache (Tier B).
      operationId: getLiveTransitTrip
      parameters:
        - name: city
          in: path
          required: true
          schema:
            type: string
        - name: trip_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Live-Fahrt-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Ungültige trip_id
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/{city}/transit/routes/{route_id}/status:
    get:
      tags: [Live]
      summary: Live-ÖPNV-Verspätungslage einer Linie (GTFS-RT, Tier B)
      x-summary-en: Live public-transit delay overview for a route (GTFS-RT, Tier B)
      description: >-
        Aggregiert die aktiven Fahrten einer Linie aus dem GTFS-RT-Feed (Tier B):
        active_trips (Anzahl), avg_delay_s und max_delay_s, im Live-Envelope
        (refresh_seconds=45). Der Request-Pfad liest NUR aus Redis. Toggle aus ->
        disabled; keine aktiven Fahrten -> no_data. Reine Live-Daten, nur im
        Redis-Cache gehalten (Tier B).
      x-description-en: >-
        Aggregates a route's active trips from the GTFS-RT feed (Tier B):
        active_trips (count), avg_delay_s and max_delay_s, in the live envelope
        (refresh_seconds=45). The request path reads ONLY from Redis. Toggle
        off -> disabled; no active trips -> no_data. Live data only, kept in the
        Redis cache (Tier B).
      operationId: getLiveTransitRouteStatus
      parameters:
        - name: city
          in: path
          required: true
          schema:
            type: string
        - name: route_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Live-Linienstatus-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Ungültige route_id
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/{slug}/air:
    get:
      tags: [Live]
      summary: Live-Alias für Luftqualität (Umweltbundesamt)
      x-summary-en: Live alias for air quality (Federal Environment Agency)
      description: >-
        Spiegelt den bestehenden UBA-Luft-Endpunkt unter der Live-Kategorie.
        Gleicher Envelope-Kontrakt wie /api/v1/cities/{slug}/air, derselbe
        Handler. Der alte Pfad bleibt als Alias erhalten und ist als veraltet
        markiert (Deprecation-Header und deprecated=true).
      x-description-en: >-
        Mirrors the existing UBA air endpoint under the live category. Same
        envelope contract as /api/v1/cities/{slug}/air, same handler. The old
        path remains as an alias and is marked deprecated (Deprecation header and
        deprecated=true).
      operationId: getLiveAir
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Luft-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/{slug}/air-uba:
    get:
      tags: [Live]
      summary: Live-Alias für Luftqualität UBA
      x-summary-en: Live alias for UBA air quality
      description: >-
        Spiegelt den bestehenden UBA-Luft-Endpunkt unter der Live-Kategorie.
        Gleicher Envelope-Kontrakt wie /api/v1/cities/{slug}/air-uba, derselbe
        Handler. Der alte Pfad bleibt als Alias erhalten und ist als veraltet
        markiert.
      x-description-en: >-
        Mirrors the existing UBA air endpoint under the live category. Same
        envelope contract as /api/v1/cities/{slug}/air-uba, same handler. The old
        path remains as an alias and is marked deprecated.
      operationId: getLiveAirUba
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Luft-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/{slug}/water-level:
    get:
      tags: [Live]
      summary: Live-Alias für Pegelstand PEGELONLINE
      x-summary-en: Live alias for PEGELONLINE water level
      description: >-
        Spiegelt den bestehenden PEGELONLINE-Endpunkt unter der Live-Kategorie.
        Gleicher Envelope-Kontrakt wie /api/v1/cities/{slug}/water-level,
        derselbe Handler. Der alte Pfad bleibt als Alias erhalten und ist als
        veraltet markiert.
      x-description-en: >-
        Mirrors the existing PEGELONLINE endpoint under the live category. Same
        envelope contract as /api/v1/cities/{slug}/water-level, same handler. The
        old path remains as an alias and is marked deprecated.
      operationId: getLiveWaterLevel
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Pegelstand-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/{slug}/traffic:
    get:
      tags: [Live]
      summary: Live-Alias für Verkehr und Baustellen Autobahn
      x-summary-en: Live alias for Autobahn traffic and roadworks
      description: >-
        Spiegelt den bestehenden Autobahn-Verkehrs-Endpunkt unter der
        Live-Kategorie. Gleicher Envelope-Kontrakt wie
        /api/v1/cities/{slug}/traffic, derselbe Handler. Der alte Pfad bleibt als
        Alias erhalten und ist als veraltet markiert.
      x-description-en: >-
        Mirrors the existing Autobahn traffic endpoint under the live category.
        Same envelope contract as /api/v1/cities/{slug}/traffic, same handler.
        The old path remains as an alias and is marked deprecated.
      operationId: getLiveTraffic
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Verkehrs-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/{slug}/webcams:
    get:
      tags: [Live]
      summary: Live-Alias für Autobahn-Webcams
      x-summary-en: Live alias for Autobahn webcams
      description: >-
        Spiegelt den bestehenden Autobahn-Webcam-Endpunkt unter der
        Live-Kategorie. Gleicher Envelope-Kontrakt wie
        /api/v1/cities/{slug}/webcams, derselbe Handler. Der alte Pfad bleibt als
        Alias erhalten und ist als veraltet markiert.
      x-description-en: >-
        Mirrors the existing Autobahn webcam endpoint under the live category.
        Same envelope contract as /api/v1/cities/{slug}/webcams, same handler.
        The old path remains as an alias and is marked deprecated.
      operationId: getLiveWebcams
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Webcam-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/live/{slug}/flood:
    get:
      tags: [Live]
      summary: Live-Alias für Hochwasser-Warnstufen LHP
      x-summary-en: Live alias for LHP flood warning levels
      description: >-
        Spiegelt den bestehenden LHP-Hochwasser-Endpunkt unter der
        Live-Kategorie. Gleicher Envelope-Kontrakt wie
        /api/v1/cities/{slug}/flood, derselbe Handler. Der alte Pfad bleibt als
        Alias erhalten und ist als veraltet markiert.
      x-description-en: >-
        Mirrors the existing LHP flood endpoint under the live category. Same
        envelope contract as /api/v1/cities/{slug}/flood, same handler. The old
        path remains as an alias and is marked deprecated.
      operationId: getLiveFlood
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Hochwasser-Envelope (CanonicalRecord oder null)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    oneOf:
                      - $ref: "#/components/schemas/CanonicalRecord"
                      - type: "null"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Unbekannte Stadt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Quelle vorübergehend nicht erreichbar (kein Cache)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
components:
  headers:
    ETag:
      description: >-
        Stabiler Entity-Tag des Response-Bodys (sha256-basiert). Nur auf
        erfolgreichen GET-Reads (200) gesetzt, nie auf Fehler-Envelopes/503.
      schema: { type: string }
    CacheControl:
      description: >-
        Cache-Control je Ressource ("public, max-age=<ttl>,
        stale-while-revalidate=<ttl>, stale-if-error=<ttl>"). TTL aus der
        serverseitigen CACHE_TTL-Map (z.B. wikidata 86400 s, dwd 1800 s, uba
        600 s, default 300 s). stale-while-revalidate/stale-if-error erlauben
        einem Shared Cache (Cloudflare), bei Ablauf bzw. Origin-Fehler kurz die
        letzte gute Antwort weiterzuliefern. Echtzeit-Endpunkte (/api/v1/live/*)
        liefern stattdessen "no-store".
      schema: { type: string }
    RateLimitLimit:
      description: Erlaubte Requests im aktuellen Fenster (IETF RateLimit-Header).
      schema: { type: integer }
    RateLimitRemaining:
      description: Verbleibende Requests im aktuellen Fenster.
      schema: { type: integer }
    RateLimitReset:
      description: Sekunden bis zum Reset des Fensters.
      schema: { type: integer }
  responses:
    NotModified:
      description: >-
        Not Modified. If-None-Match stimmte mit dem aktuellen ETag überein;
        es wird kein Body geliefert (ETag + Cache-Control bleiben erhalten).
      headers:
        ETag:
          $ref: "#/components/headers/ETag"
        Cache-Control:
          $ref: "#/components/headers/CacheControl"
  schemas:
    Meta:
      type: object
      properties:
        correlation_id:
          type: string
          nullable: true
        source_status:
          type: string
          description: >-
            Ehrlicher Quellen-Status der Antwort. "ok" (Daten vorhanden),
            "no_data" (Quelle erreichbar/abgedeckt, aber gerade keine Daten),
            "disabled" (Quelle per Toggle aus), "not_ingested" (kein Snapshot),
            "not_covered" (Stadt ist für diesen teilabgedeckten Endpunkt
            strukturell nicht abgedeckt; data=null, siehe covered_cities). Klar
            unterscheidbar vom 404 (Stadt unbekannt).
          enum: [ok, no_data, disabled, not_ingested, not_covered]
        cache_status:
          type: string
          nullable: true
          description: Cache-Herkunft (hit/miss/stale), sofern die Route cacht.
        covered_cities:
          type: array
          items:
            type: string
          description: >-
            Nur bei source_status="not_covered": die Stadt-Slugs, die dieser
            teilabgedeckte Endpunkt tatsächlich bedient (z. B. flood, webcams,
            traffic, road-events).
        pagination:
          type: object
          description: >-
            Bei paginierbaren Datenart-Listen (charging, energy, events, transit,
            OSM-Feature-Endpunkte): der EHRLICH ausgewiesene Ausschnitt (keine
            stille Kappung). KANAL-ABHÄNGIGER Default für gleiche URL:
            direktes REST liefert die volle Liste (limit=null, returned==total,
            truncated=false); GPT-Actions (erkannt am OpenAI-Header) bekommen ein
            serverseitig gebundenes Default-Limit; MCP-Aufrufe sind gebunden, weil
            der MCP-Client selbst ein Default-Limit setzt. Kanalunabhängige
            Overrides: limit=all (oder ?all=1) erzwingt die volle Liste, limit +
            offset blättern gezielt. meta.pagination ist bei ALLEN Kanälen gesetzt.
          x-description-en: >-
            For paginable data-type lists (charging, energy, events, transit, OSM
            feature endpoints): the honestly reported slice (no silent capping).
            CHANNEL-DEPENDENT default for the same URL: direct REST returns the
            full list (limit=null, returned==total, truncated=false); GPT Actions
            (detected via the OpenAI header) get a server-side bound default limit;
            MCP calls are bound because the MCP client sets a default limit itself.
            Channel-independent overrides: limit=all (or ?all=1) forces the full
            list, limit + offset page explicitly. meta.pagination is set on ALL
            channels.
          properties:
            total:
              type: integer
              description: Gesamtzahl der Einträge (voller Bestand).
            returned:
              type: integer
              description: Zahl der auf dieser Seite ausgelieferten Einträge.
            limit:
              type: integer
              nullable: true
              description: >-
                Angewandtes Seiten-Limit; null bei Vollausgabe (REST-Default oder
                limit=all).
            offset:
              type: integer
              description: Start-Offset der Seite.
            truncated:
              type: boolean
              description: >-
                true, wenn hinter dieser Seite noch Einträge liegen (offset +
                returned < total); bei Vollausgabe false.
    Health:
      type: object
      required: [status, version, redis]
      properties:
        status:
          type: string
          example: ok
        version:
          type: string
          example: "1.0.0"
        redis:
          type: boolean
          description: true wenn Redis erreichbar (Ping erfolgreich)
    Ping:
      type: object
      required: [data, meta]
      properties:
        data:
          type: object
          required: [pong]
          properties:
            pong:
              type: boolean
              example: true
        meta:
          $ref: "#/components/schemas/Meta"
    SourceStatus:
      type: object
      required: [source, enabled, breaker_state]
      properties:
        source:
          type: string
          example: wikidata
        enabled:
          type: boolean
        breaker_state:
          type: string
          enum: [CLOSED, OPEN, HALF_OPEN]
    Attribution:
      type: object
      required: [text]
      properties:
        text:
          type: string
          example: Wikidata
        license_url:
          type: string
          nullable: true
        modified:
          type: boolean
          default: false
    CityBaseDataPayload:
      type: object
      required: [kind]
      properties:
        kind:
          type: string
          enum: [city_base]
          default: city_base
        population:
          type: integer
          nullable: true
        area_km2:
          type: number
          nullable: true
    AirQualityPayload:
      type: object
      required: [kind]
      properties:
        kind:
          type: string
          enum: [air_quality]
          default: air_quality
        station_id:
          type: string
          nullable: true
        pm10:
          type: number
          nullable: true
        no2:
          type: number
          nullable: true
        pm25:
          type: number
          nullable: true
        o3:
          type: number
          nullable: true
        so2:
          type: number
          nullable: true
    WeatherPayload:
      type: object
      required: [kind]
      properties:
        kind:
          type: string
          enum: [weather]
          default: weather
        station_id:
          type: string
          nullable: true
        temperature_c:
          type: number
          nullable: true
        humidity:
          type: number
          nullable: true
        wind_speed:
          type: number
          nullable: true
        condition:
          type: string
          nullable: true
    PoiPayload:
      type: object
      required: [kind, poi_type, count]
      properties:
        kind:
          type: string
          enum: [poi]
          default: poi
        poi_type:
          type: string
        count:
          type: integer
        total_available:
          type: integer
          nullable: true
        truncated:
          type: boolean
          default: false
        items:
          type: array
          items:
            type: object
    TrafficEventPayload:
      type: object
      required: [kind]
      properties:
        kind:
          type: string
          enum: [traffic_event]
          default: traffic_event
        station_id:
          type: string
          nullable: true
        roadworks:
          type: array
          items:
            type: object
        warnings:
          type: array
          items:
            type: object
    RoadEventPayload:
      type: object
      required: [kind]
      properties:
        kind:
          type: string
          enum: [road_event]
          default: road_event
        city_source:
          type: string
          nullable: true
        events:
          type: array
          items:
            type: object
    WebcamPayload:
      type: object
      required: [kind, count]
      properties:
        kind:
          type: string
          enum: [webcam]
          default: webcam
        count:
          type: integer
        webcams:
          type: array
          items:
            type: object
    TransitStopPayload:
      type: object
      required: [kind, stop_id, stop_name]
      properties:
        kind:
          type: string
          enum: [transit_stop]
          default: transit_stop
        stop_id:
          type: string
        stop_name:
          type: string
        location_type:
          type: integer
          nullable: true
        parent_station:
          type: string
          nullable: true
        platform_code:
          type: string
          nullable: true
        wheelchair_boarding:
          type: integer
          nullable: true
    ChargingStationPayload:
      type: object
      required: [kind, count]
      properties:
        kind:
          type: string
          enum: [charging_station]
          default: charging_station
        count:
          type: integer
        stations:
          type: array
          items:
            type: object
    WaterLevelPayload:
      type: object
      required: [kind]
      properties:
        kind:
          type: string
          enum: [water_level]
          default: water_level
        station:
          type: string
          nullable: true
        water:
          type: string
          nullable: true
        value:
          type: number
          nullable: true
        unit:
          type: string
          nullable: true
    FloodWarningPayload:
      type: object
      required: [kind]
      properties:
        kind:
          type: string
          enum: [flood_warning]
          default: flood_warning
        warnings:
          type: array
          items:
            type: object
        stand:
          type: string
          nullable: true
    PollenUvPayload:
      type: object
      required: [kind]
      properties:
        kind:
          type: string
          enum: [pollen_uv]
          default: pollen_uv
        region_id:
          type: integer
          nullable: true
        region_name:
          type: string
          nullable: true
        pollen:
          type: object
        uv_index:
          type: number
          nullable: true
    DemographicsPayload:
      type: object
      required: [kind]
      properties:
        kind:
          type: string
          enum: [demographics]
          default: demographics
        population:
          type: integer
          nullable: true
        households:
          type: integer
          nullable: true
        buildings:
          type: integer
          nullable: true
        rent_avg:
          type: number
          nullable: true
        reference_year:
          type: integer
          nullable: true
        series:
          type: array
          items:
            type: object
    EnergyAssetPayload:
      type: object
      required: [kind, count]
      properties:
        kind:
          type: string
          enum: [energy_asset]
          default: energy_asset
        count:
          type: integer
        by_type:
          type: object
        assets:
          type: array
          items:
            type: object
    AdminBoundaryPayload:
      type: object
      required: [kind]
      properties:
        kind:
          type: string
          enum: [admin_boundary]
          default: admin_boundary
        ags:
          type: string
          nullable: true
        gen_name:
          type: string
          nullable: true
        area_km2:
          type: number
          nullable: true
        reference_year:
          type: integer
          nullable: true
    ElectionResultPayload:
      type: object
      required: [kind]
      properties:
        kind:
          type: string
          enum: [election_result]
          default: election_result
        election:
          type: string
          nullable: true
        granularity:
          type: string
          default: teilweise
        area_name:
          type: string
          nullable: true
        turnout:
          type: string
          nullable: true
          description: >-
            Wahlbeteiligung in Prozent als String mit Dezimal-Komma (kerg2,
            Zeile Gruppenname "Wählende", Spalte Prozent), z.B. "82,5122".
        results:
          type: array
          items:
            type: object
    HolidayPayload:
      type: object
      required: [kind]
      properties:
        kind:
          type: string
          enum: [holiday]
          default: holiday
        state:
          type: string
          nullable: true
        year:
          type: integer
          nullable: true
        holidays:
          type: array
          items:
            type: object
        school_holidays:
          type: array
          items:
            type: object
    HospitalPayload:
      type: object
      required: [kind, count]
      properties:
        kind:
          type: string
          enum: [hospital]
          default: hospital
        count:
          type: integer
        hospitals:
          type: array
          items:
            type: object
        reference_date:
          type: string
          nullable: true
    IcuCapacityPayload:
      type: object
      required: [kind]
      properties:
        kind:
          type: string
          enum: [icu_capacity]
          default: icu_capacity
        kreis_id:
          type: string
          nullable: true
        kreis_name:
          type: string
          nullable: true
        beds_free:
          type: integer
          nullable: true
        beds_occupied:
          type: integer
          nullable: true
        hospitals:
          type: array
          description: >-
            Klinikscharfe Live-Status-Einschätzungen (DIVI, Tier C) je
            Krankenhaus-Standort (bezeichnung, ort, letzte_meldung,
            status_high_care, status_ecmo). Die Live-API liefert keine
            numerische Belegung mehr (beds_free/beds_occupied bleiben null
            und tragen nur das Tier-A-Kreis-Aggregat).
          items:
            type: object
        datum:
          type: string
          nullable: true
    CanonicalRecord:
      type: object
      required: [city_slug, retrieved_at, source, license_id, license_tier, schema_version, record_id, content_hash, attribution, payload]
      properties:
        city_slug:
          type: string
        schema_version:
          type: integer
          default: 1
        record_id:
          type: string
          description: Deterministischer Primärschlüssel (computed, immer im Dump).
        content_hash:
          type: string
          description: Deterministischer Dedup-Hash der Nutzdaten (computed, immer im Dump).
        ags:
          type: string
          nullable: true
          description: Amtlicher Gemeindeschlüssel als Join-Key (kein Pfadsegment).
        wikidata_qid:
          type: string
          nullable: true
          description: Wikidata-QID als Join-Key.
        geo:
          nullable: true
          type: object
          properties:
            lat:
              type: number
            lon:
              type: number
        observed_at:
          type: string
          format: date-time
          nullable: true
        retrieved_at:
          type: string
          format: date-time
        source:
          type: string
          enum: [wikidata, dwd, osm, autobahn, bnetza, uba, delfi, hvv, pegelonline, hochwasser, dwd_pollen, genesis, zensus, mastr, bkg, bundeswahl, divi, feiertage, berlin_viz, hamburg_baustellen, koeln_verkehr, muenchen_baustellen, dortmund_baustellen, sperrinfosys, mobidata_bw]
        license_id:
          type: string
          enum: [cc0, cc_by_4_0, dl_de_by_2_0, dl_de_zero_2_0, geonutzv, odbl, cc_by_sa_4_0, unknown]
        license_tier:
          type: string
          enum: [A, B, C]
        attribution:
          $ref: "#/components/schemas/Attribution"
        payload:
          oneOf:
            - $ref: "#/components/schemas/CityBaseDataPayload"
            - $ref: "#/components/schemas/AirQualityPayload"
            - $ref: "#/components/schemas/WeatherPayload"
            - $ref: "#/components/schemas/PoiPayload"
            - $ref: "#/components/schemas/TrafficEventPayload"
            - $ref: "#/components/schemas/TransitStopPayload"
            - $ref: "#/components/schemas/ChargingStationPayload"
            - $ref: "#/components/schemas/WaterLevelPayload"
            - $ref: "#/components/schemas/FloodWarningPayload"
            - $ref: "#/components/schemas/PollenUvPayload"
            - $ref: "#/components/schemas/DemographicsPayload"
            - $ref: "#/components/schemas/EnergyAssetPayload"
            - $ref: "#/components/schemas/AdminBoundaryPayload"
            - $ref: "#/components/schemas/ElectionResultPayload"
            - $ref: "#/components/schemas/HolidayPayload"
            - $ref: "#/components/schemas/HospitalPayload"
            - $ref: "#/components/schemas/IcuCapacityPayload"
            - $ref: "#/components/schemas/RoadEventPayload"
            - $ref: "#/components/schemas/WebcamPayload"
          discriminator:
            propertyName: kind
    ErrorDetail:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          example: upstream_unavailable
        message:
          type: string
          example: Simulated upstream failure
        hint:
          type: string
          nullable: true
    ErrorEnvelope:
      type: object
      required: [error, meta]
      properties:
        error:
          $ref: "#/components/schemas/ErrorDetail"
        meta:
          type: object
          properties:
            correlation_id:
              type: string
              nullable: true
            generated_at:
              type: string
              format: date-time
