{
  "openapi": "3.1.0",
  "info": {
    "title": "NetCov API",
    "version": "0.1.0",
    "description": "NetCov API — internet/broadband coverage data (Italy coverage + address\ngeocoding). Italian broadband coverage lookup (AGCOM/BUL-inspired cell\ncoverage) plus offline address geocoding over ANNCSU open data.\n\n## Endpoints\n\n- `GET /geocode` — resolve a free-form address to coordinates (civic match,\n  comune centroid fallback, or low-confidence match).\n- `GET /coverage/italy` — broadband coverage for an address or a\n  `lat`/`lon` pair: list of nearby cells with per-technology downstream\n  speed and the best available speed tier.\n- `GET /autocomplete` — street suggestions (given a city) or comune\n  suggestions (when only a city fragment is given).\n- `GET /credits` — quota snapshot of the calling account (rate-limited to\n  1 req/s per account; not charged against caps).\n\n## Authentication\n\nAll endpoints require an API key:\n\n```\nAuthorization: Bearer <api_key>\n```\n\n`401` is returned for missing, invalid or revoked tokens. The key is issued\nby the platform admin (see onboarding). Rate limits and quotas vary per key\nand endpoint; headers (`X-RateLimit-*`, `Retry-After`) report the current\nstate on every response, including error responses.\n\nEmbedded browser keys: keys with plan `browser` carry an `origins`\nallowlist and are meant to be embedded in public web pages. They are\nGET-only and are only accepted when the request `Origin` header (falling\nback to `Referer` when `Origin` is absent) host matches an allowed entry.\nKeep all other keys server-side; never expose them in browser JavaScript.\n\n## Errors\n\nEvery non-2xx response (including 400 query-deserialization failures and\n404 unknown routes) is a JSON error envelope:\n\n```json\n{ \"error\": \"human readable message\", \"code\": \"machine_code\", \"retry_after\": 6 }\n```\n\n`retry_after` (seconds) is present only on `429` responses. Known\n`code` values: `missing_token` (401), `invalid_token` (401),\n`rate_limit_exceeded` (429), `daily_quota_exceeded` (429),\n`monthly_quota_exceeded` (429), `credits_rate_limited` (429),\n`bad_request` (400), `address_not_found` (404), `not_found` (404),\n`coverage_unavailable` (503), `streets_unavailable` (503), `internal`\n(500).\n\n## Data attribution (mandatory)\n\n> Fonte: Istat – Agenzia delle Entrate, Archivio Nazionale dei Numeri Civici\n> e delle Strade Urbane (ANNCSU)\n\nData is CC BY 4.0; API/spec code is MIT. Clients must surface this\nattribution wherever the data is shown (UI footers, tool descriptions,\nexports).\n\n## CORS\n\nThe API sends `Access-Control-Allow-Origin: *`, so it can be called\ndirectly from browsers (see `docs/playground.html`).\n\n## Servers\n\nProduction: `https://api.netcov.io`. The development server runs locally\non `http://localhost:8788`.\n",
    "license": {
      "name": "MIT (code) — data CC BY 4.0 (Istat/AdE ANNCSU)",
      "url": "https://creativecommons.org/licenses/by/4.0/"
    },
    "contact": {
      "name": "NetCov team"
    }
  },
  "servers": [
    {
      "url": "https://api.netcov.io",
      "description": "Production"
    },
    {
      "url": "http://100.65.240.18:8788",
      "description": "Internal / tailnet (dev, central command)"
    },
    {
      "url": "http://localhost:8788",
      "description": "Development server (local)"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/geocode": {
      "get": {
        "operationId": "geocode",
        "summary": "Geocode an address",
        "description": "Resolve a free-form Italian address (street, civic number, city) into\ncoordinates. Civic matches return `confidence: \"high\"`; comune\ncentroid fallbacks return `\"comune\"`; any weaker match returns `\"low\"`.\n",
        "tags": [
          "Geocoding"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "fields",
            "in": "query",
            "required": false,
            "description": "Comma-separated list of top-level response keys to keep\n(e.g. `fields=summary,usage`) to trim the payload. Unknown keys\nreturn 400 `bad_request` listing the allowed fields.\n",
            "schema": {
              "type": "string",
              "example": "summary,usage"
            }
          },
          {
            "name": "address",
            "in": "query",
            "required": true,
            "description": "Free-form address, e.g. `Milano, Corso Buenos Aires 1`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "code",
            "in": "query",
            "required": false,
            "description": "Stable address code, from a previous geocode, coverage or\nautocomplete response. Canonical form is `p<id>` (permanent\nANNCSU civic id, e.g. `p4353350`); legacy composite form\n`comune/odonimo/civico` (e.g. `napoli/via toledo/21`) also\nresolves. Alternative to `address`: the developer stores one\nopaque token and never re-parses address strings or coordinates.\n",
            "schema": {
              "type": "string",
              "example": "napoli/via toledo/21"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results (1–20). Values outside the range are\nclamped server-side.\n",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 20
            }
          },
          {
            "name": "strict",
            "in": "query",
            "required": false,
            "description": "Strict mode flag. Accepts `1`, `true` or `yes`. When set, any\nnon-civic match (confidence != `high`) or no-match query returns\nHTTP 404 with code `address_not_found` instead of the lenient\ncomune-centroid fallback (see `notice`).\n",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Geocoding results.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GeocodeResponse"
                },
                "examples": {
                  "default": {
                    "summary": "Civic match in Milano",
                    "value": {
                      "query": "Milano, Corso Buenos Aires 1",
                      "attribution": "Fonte: Istat – Agenzia delle Entrate, Archivio Nazionale dei Numeri Civici e delle Strade Urbane (ANNCSU)",
                      "count": 1,
                      "notice": null,
                      "results": [
                        {
                          "query": "Milano, Corso Buenos Aires 1",
                          "match": "corso buenos aires 1 milano",
                          "latitude": 45.4751935,
                          "longitude": 9.2055038,
                          "comune": "Milano",
                          "provincia": "Milano",
                          "regione": "Lombardia",
                          "cap": "20121",
                          "civico": "1",
                          "confidence": "high",
                          "source": "anncsu",
                          "score": 39.37
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or malformed `address`.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": "address is required",
                      "code": "bad_request",
                      "request_id": "1a2b3c4d5e6f",
                      "doc_url": "https://netcov.io/errors#bad_request"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid or revoked API key.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "examples": {
                  "missing-token": {
                    "value": {
                      "error": "missing bearer token",
                      "code": "missing_token",
                      "request_id": "1a2b3c4d5e6f",
                      "doc_url": "https://netcov.io/errors#missing_token"
                    }
                  },
                  "invalid-token": {
                    "value": {
                      "error": "invalid or revoked token",
                      "code": "invalid_token",
                      "request_id": "1a2b3c4d5e6f",
                      "doc_url": "https://netcov.io/errors#invalid_token"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limit or daily/monthly quota exceeded.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "Service or database unavailable.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "The requested address or resource could not be found (envelope\ncodes `address_not_found` / `not_found`). In `strict` mode,\n`address_not_found` is also returned for non-civic matches\n(`confidence` != `high`) that would otherwise fall back to the\ncomune centroid.\n",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "examples": {
                  "strict": {
                    "value": {
                      "error": "address not found (strict mode: only civic-level matches are accepted)",
                      "code": "address_not_found",
                      "request_id": "1a2b3c4d5e6f",
                      "doc_url": "https://netcov.io/errors#address_not_found"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/coverage/italy": {
      "get": {
        "operationId": "coverageItaly",
        "summary": "Broadband coverage for an address or coordinates",
        "description": "Returns the broadband coverage cells nearest to a location, identified\neither by `address` (geocoded server-side) or by `lat` + `lon`\n(required together). At least one of the two forms must be provided.\n\nEach cell lists the available `technologies` with their maximum\ndownstream speed and the best speed tier. Fields may be `null` when the\nsource does not report a value.\n",
        "tags": [
          "Coverage"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "fields",
            "in": "query",
            "required": false,
            "description": "Comma-separated list of top-level response keys to keep\n(e.g. `fields=summary,usage`) to trim the payload. Unknown keys\nreturn 400 `bad_request` listing the allowed fields.\n",
            "schema": {
              "type": "string",
              "example": "summary,usage"
            }
          },
          {
            "name": "code",
            "in": "query",
            "required": false,
            "description": "Stable address code, from a previous geocode, coverage or\nautocomplete response. Canonical form is `p<id>` (permanent\nANNCSU civic id, e.g. `p4353350`); legacy composite form\n`comune/odonimo/civico` (e.g. `napoli/via toledo/21`) also\nresolves. The recommended way to re-query a stored address:\none opaque token, no address parsing, no coordinates. Mutually\nexclusive with `address` and `lat`/`lon`.\n",
            "schema": {
              "type": "string",
              "example": "napoli/via toledo/21"
            }
          },
          {
            "name": "address",
            "in": "query",
            "required": false,
            "description": "Address to resolve and look up. The address is geocoded\nserver-side in the same call, so no separate geocoding step is\nneeded. Mutually exclusive with `code` and `lat`/`lon`.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lat",
            "in": "query",
            "required": false,
            "description": "Latitude (WGS84), alternative to `address` for developers who\nalready have coordinates (GPS positions, existing datasets).\nMust be given together with `lon`; use `address` otherwise.\n",
            "schema": {
              "type": "number",
              "format": "double",
              "minimum": -90,
              "maximum": 90
            }
          },
          {
            "name": "lon",
            "in": "query",
            "required": false,
            "description": "Longitude (WGS84), alternative to `address`. Must be given\ntogether with `lat`; use `address` otherwise.\n",
            "schema": {
              "type": "number",
              "format": "double",
              "minimum": -180,
              "maximum": 180
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of cells to return (1–20). Values outside the range\nare clamped server-side.\n",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 20
            }
          },
          {
            "name": "strict",
            "in": "query",
            "required": false,
            "description": "Strict mode flag for address queries. Accepts `1`, `true` or\n`yes`. When set, any non-civic match (confidence != `high`) or\nno-match query returns HTTP 404 with code `address_not_found`\ninstead of the lenient comune-centroid fallback (see `notice`).\nIgnored for coordinate queries.\n",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Coverage cells for the location.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CoverageResponse"
                }
              }
            }
          },
          "400": {
            "description": "Neither `address` nor `lat`+`lon` provided, or malformed\nparameters.\n",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "examples": {
                  "missing-location": {
                    "value": {
                      "error": "provide either address or lat+lon",
                      "code": "bad_request",
                      "request_id": "1a2b3c4d5e6f",
                      "doc_url": "https://netcov.io/errors#bad_request"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid or revoked API key.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit or daily/monthly quota exceeded.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "Service or database unavailable.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "The requested address or resource could not be found (envelope\ncodes `address_not_found` / `not_found`). In `strict` mode,\n`address_not_found` is also returned for non-civic matches\n(`confidence` != `high`) that would otherwise fall back to the\ncomune centroid.\n",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "examples": {
                  "strict": {
                    "value": {
                      "error": "address not found (strict mode: only civic-level matches are accepted)",
                      "code": "address_not_found",
                      "request_id": "1a2b3c4d5e6f",
                      "doc_url": "https://netcov.io/errors#address_not_found"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/autocomplete": {
      "get": {
        "operationId": "autocomplete",
        "summary": "Suggest streets or comuni",
        "description": "Street-level suggestions when `address` is given (filtered by `city`),\ncomune suggestions when only `city` (or `address` is empty) is given.\nAt least one of `city`/`address` must be provided.\n\nQuotas: autocomplete has its own generous budget derived from your\ncore limits (rpm x15, daily x15, monthly x15) with a separate token\nbucket, so form keystroke traffic never eats the coverage budget.\nExact ratios are always readable live from `GET /credits`.\n\nConditional requests: successful responses carry an HTTP `ETag`.\nSend it back in the `If-None-Match` request header; when the\nsuggestion set is unchanged the server returns `304 Not Modified`\nwith no body.\n",
        "tags": [
          "Autocomplete"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "fields",
            "in": "query",
            "required": false,
            "description": "Comma-separated list of top-level response keys to keep\n(e.g. `fields=summary,usage`) to trim the payload. Unknown keys\nreturn 400 `bad_request` listing the allowed fields.\n",
            "schema": {
              "type": "string",
              "example": "summary,usage"
            }
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "description": "ETag from a previous autocomplete response. When the suggestion\nset is unchanged the server returns `304 Not Modified` with no\nbody, so clients can reuse the cached result.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "city",
            "in": "query",
            "required": false,
            "description": "City name or fragment. Filters street results; drives comune\nsuggestions when `address` is empty.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "address",
            "in": "query",
            "required": false,
            "description": "Street name fragment to suggest.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of suggestions (1–20). Values outside the range are\nclamped server-side.\n",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Street or comune suggestions.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutocompleteResponse"
                }
              }
            }
          },
          "304": {
            "description": "Suggestion set unchanged (`If-None-Match` matched the current\n`ETag`). No body; reuse the cached result.\n",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            }
          },
          "400": {
            "description": "Neither `city` nor `address` provided.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "examples": {
                  "missing-query": {
                    "value": {
                      "error": "provide address or city",
                      "code": "bad_request",
                      "request_id": "1a2b3c4d5e6f",
                      "doc_url": "https://netcov.io/errors#bad_request"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid or revoked API key.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit or daily/monthly quota exceeded.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "Service or database unavailable.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "The requested address or resource could not be found (envelope\ncodes `address_not_found` / `not_found`).\n",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/credits": {
      "get": {
        "operationId": "credits",
        "summary": "Check remaining API credits/quota",
        "description": "Returns the calling account's quota snapshot: per-endpoint limits\n(`core`, `autocomplete`), usage counters, remaining quota and reset\ntimes. Rate-limited to 1 request/second per account; NOT charged\nagainst the daily/monthly caps.\n",
        "tags": [
          "Account"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Quota snapshot for the calling account.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreditsResponse"
                },
                "examples": {
                  "default": {
                    "summary": "Free plan with finite caps",
                    "value": {
                      "account_id": "key_xxx",
                      "plan": "free",
                      "limits": {
                        "core": {
                          "rpm": 30,
                          "daily": 1000,
                          "monthly": 0
                        },
                        "autocomplete": {
                          "rpm": 450,
                          "daily": 15000,
                          "monthly": 0
                        }
                      },
                      "usage": {
                        "core_today": 12,
                        "core_month": 340,
                        "autocomplete_today": 5,
                        "autocomplete_month": 90
                      },
                      "remaining": {
                        "core": {
                          "rpm": 27,
                          "daily": 996,
                          "monthly": null
                        },
                        "autocomplete": {
                          "rpm": 441,
                          "daily": 14970,
                          "monthly": null
                        }
                      },
                      "resets": {
                        "daily": "2026-08-04T00:00:00+00:00",
                        "monthly": "2026-09-01T00:00:00+00:00"
                      },
                      "attribution": "Fonte: Istat – Agenzia delle Entrate, Archivio Nazionale dei Numeri Civici e delle Strade Urbane (ANNCSU)"
                    }
                  },
                  "unlimited": {
                    "summary": "Unlimited caps (daily/monthly null in remaining)",
                    "value": {
                      "account_id": "key_yyy",
                      "plan": "service",
                      "limits": {
                        "core": {
                          "rpm": 100000,
                          "daily": 0,
                          "monthly": 0
                        },
                        "autocomplete": {
                          "rpm": 100000,
                          "daily": 0,
                          "monthly": 0
                        }
                      },
                      "usage": {
                        "core_today": 0,
                        "core_month": 0,
                        "autocomplete_today": 0,
                        "autocomplete_month": 0
                      },
                      "remaining": {
                        "core": {
                          "rpm": 100000,
                          "daily": null,
                          "monthly": null
                        },
                        "autocomplete": {
                          "rpm": 100000,
                          "daily": null,
                          "monthly": null
                        }
                      },
                      "resets": {
                        "daily": "2026-08-04T00:00:00+00:00",
                        "monthly": "2026-09-01T00:00:00+00:00"
                      },
                      "attribution": "Fonte: Istat – Agenzia delle Entrate, Archivio Nazionale dei Numeri Civici e delle Strade Urbane (ANNCSU)"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid or revoked API key.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "The route does not exist (`not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "`/credits` is rate-limited to 1 request/second per account\n(`credits_rate_limited`).\n",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "examples": {
                  "rate-limited": {
                    "value": {
                      "error": "credits endpoint rate limited",
                      "code": "credits_rate_limited",
                      "retry_after": 1
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Service or database unavailable.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Remaining": {
                "$ref": "#/components/headers/RateLimitDailyRemaining"
              },
              "X-RateLimit-Monthly-Remaining": {
                "$ref": "#/components/headers/RateLimitMonthlyRemaining"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/admin/keys": {
      "post": {
        "operationId": "adminCreateKey",
        "summary": "Create an API key",
        "description": "Admin key provisioning. Keys with plan `browser` carry an `origins`\nallowlist for embedded use in public pages: they are GET-only and\nare only accepted when the request `Origin` header (falling back to\n`Referer` when `Origin` is absent) host matches an allowed entry.\n`origins` accepts host names (optionally with scheme/port); all\nother plans must stay server-side.\n",
        "tags": [
          "Admin"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Human-readable key label."
                  },
                  "plan": {
                    "type": "string",
                    "description": "Plan name (e.g. `free`, `browser`, `service`)."
                  },
                  "rpm": {
                    "type": "integer",
                    "minimum": 0
                  },
                  "daily_cap": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Requests allowed per day; 0 means unlimited."
                  },
                  "monthly_cap": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Requests allowed per month; 0 means unlimited."
                  },
                  "origins": {
                    "type": "array",
                    "description": "Allowed Origin/Referer hosts for plan `browser` keys.\nIgnored for other plans.\n",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created API key.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "token"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "token": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid or revoked admin key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Valid key without admin privileges.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": "admin privileges required",
                      "code": "forbidden",
                      "request_id": "1a2b3c4d5e6f",
                      "doc_url": "https://netcov.io/errors#forbidden"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/admin/keys/{id}/limits": {
      "post": {
        "operationId": "adminUpdateKeyLimits",
        "summary": "Update key limits and browser origins",
        "description": "Update rate/quota caps and, for plan `browser` keys, the `origins`\nallowlist. Browser keys are GET-only and are only accepted when the\nrequest `Origin` header (falling back to `Referer` when `Origin` is\nabsent) host matches an allowed entry.\n",
        "tags": [
          "Admin"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "API key id (`key_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "rpm": {
                    "type": "integer",
                    "minimum": 0
                  },
                  "daily_cap": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Requests allowed per day; 0 means unlimited."
                  },
                  "monthly_cap": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Requests allowed per month; 0 means unlimited."
                  },
                  "origins": {
                    "type": "array",
                    "description": "Allowed Origin/Referer hosts for plan `browser` keys.\nIgnored for other plans.\n",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated key limits.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid or revoked admin key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Valid key without admin privileges.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": "admin privileges required",
                      "code": "forbidden",
                      "request_id": "1a2b3c4d5e6f",
                      "doc_url": "https://netcov.io/errors#forbidden"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown key id (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key issued on onboarding, shaped like\n`gk_live_<keyId>_<secret>`. Send it as\n`Authorization: Bearer <api_key>`. Never commit keys to source;\nstore them in environment variables or a gitignored `.env` file.\nDemo keys (`demo_gigabit`, `demo_adsl`, `demo_none`, `demo_404`,\n`demo_429`) return fixed scenarios with no quota.\n\nKeys with plan `browser` carry an `origins` allowlist for embedded\nuse in public pages: they are GET-only and are only accepted when\nthe request `Origin` (fallback `Referer`) host matches an allowed\nentry. All other keys must stay server-side.\n"
      }
    },
    "headers": {
      "ETag": {
        "description": "Opaque validator for the current suggestion set. Send it back in\n`If-None-Match`; when unchanged the server returns `304`.\n",
        "schema": {
          "type": "string"
        }
      },
      "RateLimitLimit": {
        "description": "The per-minute rate (rpm) for the endpoint (e.g. `600` on the CI test\nkey). This is a rate, not a burst budget: the token bucket refills at\n`rpm/60` tokens per second.\n",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "RateLimitRemaining": {
        "description": "Tokens left in the burst bucket. The bucket holds at most\n`ceil(rpm/12)` tokens (~5 seconds of headroom), so this value starts\nnear that cap and drains with each request until it hits 0, at which\npoint requests return 429 until the next token refills.\n",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "RateLimitReset": {
        "description": "Seconds until the next token refills into the burst bucket (the\nbucket refills at `rpm/60` tokens per second). May be 0 when the\nserver does not track it.\n",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "RateLimitDailyRemaining": {
        "description": "Requests remaining today.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "RateLimitMonthlyRemaining": {
        "description": "Requests remaining this month.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying after a 429.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      }
    },
    "schemas": {
      "CreditsLimits": {
        "type": "object",
        "required": [
          "rpm",
          "daily",
          "monthly"
        ],
        "additionalProperties": false,
        "properties": {
          "rpm": {
            "type": "integer",
            "minimum": 0,
            "description": "Requests allowed per minute (burst-limited token bucket: refills\nat rpm/60 per second, capacity ceil(rpm/12)).\n"
          },
          "daily": {
            "type": "integer",
            "minimum": 0,
            "description": "Requests allowed per day; 0 means unlimited."
          },
          "monthly": {
            "type": "integer",
            "minimum": 0,
            "description": "Requests allowed per month; 0 means unlimited."
          }
        }
      },
      "CreditsRemaining": {
        "type": "object",
        "required": [
          "rpm",
          "daily",
          "monthly"
        ],
        "additionalProperties": false,
        "properties": {
          "rpm": {
            "type": "integer",
            "minimum": 0,
            "description": "Tokens left in the burst bucket (capacity ceil(rpm/12), refilled\nat rpm/60 per second).\n"
          },
          "daily": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "Requests remaining today; null when unlimited."
          },
          "monthly": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "Requests remaining this month; null when unlimited."
          }
        }
      },
      "CreditsResponse": {
        "type": "object",
        "required": [
          "account_id",
          "plan",
          "limits",
          "usage",
          "remaining",
          "resets",
          "attribution"
        ],
        "additionalProperties": false,
        "properties": {
          "account_id": {
            "type": "string",
            "description": "API key id of the calling account (`key_...`)."
          },
          "plan": {
            "type": "string",
            "description": "Plan name, e.g. `free`."
          },
          "limits": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/CreditsLimits"
            },
            "description": "Per-endpoint quota limits (`core`, `autocomplete`)."
          },
          "usage": {
            "type": "object",
            "additionalProperties": {
              "type": "integer",
              "minimum": 0
            },
            "description": "Usage counters (`core_today`, `core_month`, `autocomplete_today`, `autocomplete_month`)."
          },
          "remaining": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/CreditsRemaining"
            },
            "description": "Per-endpoint remaining quota (`core`, `autocomplete`). `daily`/\n`monthly` are null when the corresponding cap is unlimited.\n"
          },
          "resets": {
            "type": "object",
            "required": [
              "daily",
              "monthly"
            ],
            "additionalProperties": false,
            "properties": {
              "daily": {
                "type": "string",
                "format": "date-time",
                "description": "When the daily quota window resets."
              },
              "monthly": {
                "type": "string",
                "format": "date-time",
                "description": "When the monthly quota window resets."
              }
            }
          },
          "attribution": {
            "$ref": "#/components/schemas/Attribution"
          }
        }
      },
      "ErrorEnvelope": {
        "type": "object",
        "description": "Standardized error envelope returned by every non-2xx response.\n`retry_after` (seconds) is present only on 429 responses.\n",
        "required": [
          "error",
          "code",
          "request_id",
          "doc_url"
        ],
        "additionalProperties": false,
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error message."
          },
          "code": {
            "type": "string",
            "description": "Machine-readable error code. One of: `missing_token`,\n`invalid_token`, `forbidden`, `rate_limit_exceeded`,\n`daily_quota_exceeded`, `monthly_quota_exceeded`,\n`credits_rate_limited`, `bad_request`, `address_not_found`,\n`not_found`, `coverage_unavailable`, `streets_unavailable`,\n`internal`.\n",
            "enum": [
              "missing_token",
              "invalid_token",
              "forbidden",
              "rate_limit_exceeded",
              "daily_quota_exceeded",
              "monthly_quota_exceeded",
              "credits_rate_limited",
              "bad_request",
              "address_not_found",
              "not_found",
              "coverage_unavailable",
              "streets_unavailable",
              "internal"
            ]
          },
          "request_id": {
            "type": "string",
            "description": "Opaque request identifier — quote it in support tickets."
          },
          "doc_url": {
            "type": "string",
            "format": "uri",
            "description": "Documentation URL for this error code (netcov.io/errors)."
          },
          "retry_after": {
            "type": "integer",
            "minimum": 0,
            "description": "Seconds to wait before retrying; only on 429 responses."
          }
        },
        "examples": [
          {
            "error": "address is required",
            "code": "bad_request",
            "request_id": "1a2b3c4d5e6f",
            "doc_url": "https://netcov.io/errors#bad_request"
          },
          {
            "error": "rate limit exceeded",
            "code": "rate_limit_exceeded",
            "request_id": "1a2b3c4d5e70",
            "doc_url": "https://netcov.io/errors#rate_limit_exceeded",
            "retry_after": 6
          }
        ]
      },
      "Attribution": {
        "type": "string",
        "const": "Fonte: Istat – Agenzia delle Entrate, Archivio Nazionale dei Numeri Civici e delle Strade Urbane (ANNCSU)",
        "description": "Mandatory CC BY 4.0 data attribution (Istat – AdE ANNCSU)."
      },
      "Confidence": {
        "type": "string",
        "enum": [
          "high",
          "comune",
          "low"
        ],
        "description": "`high` — civic match; `comune` — comune centroid fallback;\n`low` — weak match.\n"
      },
      "GeocodeResult": {
        "type": "object",
        "required": [
          "query",
          "match",
          "latitude",
          "longitude",
          "comune",
          "provincia",
          "regione",
          "cap",
          "civico",
          "confidence",
          "source",
          "score"
        ],
        "additionalProperties": false,
        "properties": {
          "query": {
            "type": "string",
            "description": "The original query string echoed back."
          },
          "match": {
            "type": "string",
            "description": "Normalized address that was matched."
          },
          "latitude": {
            "type": "number",
            "format": "double"
          },
          "longitude": {
            "type": "number",
            "format": "double"
          },
          "comune": {
            "type": "string"
          },
          "provincia": {
            "type": "string"
          },
          "regione": {
            "type": "string"
          },
          "cap": {
            "type": "string",
            "description": "Postal code; may be empty when unknown."
          },
          "civico": {
            "type": "string",
            "description": "Civic number; may be empty when unknown."
          },
          "confidence": {
            "$ref": "#/components/schemas/Confidence"
          },
          "source": {
            "type": "string",
            "enum": [
              "anncsu"
            ],
            "description": "Data source. Always `anncsu`."
          },
          "score": {
            "type": "number",
            "format": "double",
            "description": "BM25-style relevance score. The written contract notes values <= 0,\nbut the live API returns positive scores (e.g. 39.37); no sign\nconstraint is enforced.\n"
          }
        }
      },
      "GeocodeResponse": {
        "type": "object",
        "required": [
          "query",
          "attribution",
          "count",
          "results"
        ],
        "additionalProperties": false,
        "properties": {
          "usage": {
            "type": "object",
            "description": "Compact quota state for this request (mirror of the\nX-RateLimit-* headers): remaining burst, reset_seconds and\ndaily/monthly remaining (`null` = unlimited). Lets clients\nself-meter. Absent for internal/service keys; demo keys reply\nwith a fixed sample.\n",
            "additionalProperties": true
          },
          "query": {
            "type": "string"
          },
          "attribution": {
            "$ref": "#/components/schemas/Attribution"
          },
          "count": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of results returned (may be 0 when nothing matches)."
          },
          "notice": {
            "type": [
              "string",
              "null"
            ],
            "description": "Match-quality notice, or `null` for civic-level (`confidence:\n\"high\"`) matches. Set when the address fell back to a\nlower-quality match, e.g. `\"no civic-level match; fell back to\nthe comune centroid (Bari)\"`.\n"
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GeocodeResult"
            }
          }
        }
      },
      "TechnologyEnum": {
        "type": "string",
        "description": "Access technology.",
        "enum": [
          "adsl",
          "vdsl",
          "vdsl-vectoring",
          "ftth",
          "ftb",
          "fttc",
          "fttn",
          "docsis",
          "docsis-3.0",
          "docsis-3.1",
          "2g",
          "3g",
          "lte",
          "4g",
          "5g",
          "5g-fwa",
          "fwa",
          "satellite",
          "other"
        ]
      },
      "Technology": {
        "type": "object",
        "required": [
          "technology",
          "max_downstream_mbps"
        ],
        "additionalProperties": false,
        "properties": {
          "technology": {
            "$ref": "#/components/schemas/TechnologyEnum"
          },
          "max_downstream_mbps": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "Maximum downstream speed in Mbps; `null` when not reported."
          }
        }
      },
      "BestSpeedTier": {
        "type": [
          "string",
          "null"
        ],
        "enum": [
          "no-coverage",
          "basic",
          "fast",
          "ultrafast",
          "gigabit",
          null
        ],
        "description": "Best available speed tier; `null` when no tier can be determined."
      },
      "CoverageCell": {
        "type": "object",
        "description": "One deduplicated row: a single (operator, technology) combination\nwith the best declared speed at the location.\n",
        "required": [
          "technologies",
          "best_speed_mbps",
          "best_speed_tier",
          "operator"
        ],
        "additionalProperties": false,
        "properties": {
          "technologies": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Technology"
            }
          },
          "best_speed_mbps": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "Best downstream speed across technologies in Mbps; `null` when none reported."
          },
          "best_speed_tier": {
            "$ref": "#/components/schemas/BestSpeedTier"
          },
          "operator": {
            "type": [
              "string",
              "null"
            ],
            "description": "Network operator name; `null` when unknown."
          }
        }
      },
      "CoverageResponse": {
        "type": "object",
        "required": [
          "lat",
          "lon",
          "display",
          "location",
          "cells",
          "summary",
          "attribution"
        ],
        "additionalProperties": false,
        "properties": {
          "usage": {
            "type": "object",
            "description": "Compact quota state for this request (mirror of the\nX-RateLimit-* headers): remaining burst, reset_seconds and\ndaily/monthly remaining. Lets clients self-meter. Absent\nfor internal/service keys; demo keys reply with a fixed\nsample.\n",
            "additionalProperties": true
          },
          "lat": {
            "type": "number",
            "format": "double",
            "description": "Latitude of the queried location (WGS84)."
          },
          "lon": {
            "type": "number",
            "format": "double",
            "description": "Longitude of the queried location (WGS84)."
          },
          "display": {
            "type": "string",
            "description": "Human-readable location in proper Italian case (e.g.\n`Via Crespole e Fabbriche 125 Pistoia`). For coordinate queries\nit is formatted as `\"<lat>, <lon>\"`; for address queries it is\nthe normalized address. Codes, CAP/postal codes, coordinates\nand confidence values are unchanged.\n"
          },
          "notice": {
            "type": [
              "string",
              "null"
            ],
            "description": "Match-quality notice for address queries, or `null`. Set when the\naddress fell back to a centroid/low-quality match, e.g.\n`\"no civic-level match; fell back to the comune centroid\n(Bari)\"`. Always `null` for coordinate queries.\n"
          },
          "location": {
            "$ref": "#/components/schemas/LocationInfo",
            "description": "Resolved location details (the geocode result for address queries).\nThis is why a coverage request is a single call: the address is\ngeocoded server-side and the match is returned here.\n"
          },
          "cells": {
            "type": "array",
            "description": "Coverage cells, **deduplicated to one row per (operator,\ntechnology)** with the best declared speed kept (the raw grid\nreturns many identical rows for the same address), best first.\n",
            "items": {
              "$ref": "#/components/schemas/CoverageCell"
            }
          },
          "summary": {
            "$ref": "#/components/schemas/CoverageSummary",
            "description": "Consumer-facing summary: the unique technologies and operators\navailable at the location, plus the best offer. This is the\nfield lead-generation integrations render (\"which networks are\navailable here?\").\n"
          },
          "attribution": {
            "type": "string",
            "description": "Mandatory CC BY 4.0 credit for the coverage data (AGCOM\nBroadband Map). Display it wherever the coverage is shown.\n"
          }
        }
      },
      "CoverageSummary": {
        "type": "object",
        "required": [
          "technologies",
          "operators"
        ],
        "additionalProperties": false,
        "properties": {
          "technologies": {
            "type": "array",
            "description": "Unique technologies available, best first (e.g. ftth, fwa, adsl).",
            "items": {
              "type": "string"
            },
            "example": [
              "ftth",
              "vdsl"
            ]
          },
          "by_technology": {
            "type": "array",
            "description": "Additive per-technology rollup: one row per technology with the\nbest declared downlink, its tier and the deduplicated operator\nlist. Sorted by max downlink descending (nulls last); strings\nare deduplicated. Mirrors the Python SDK `by_technology()`\nhelper.\n",
            "items": {
              "$ref": "#/components/schemas/CoverageTechnologySummary"
            }
          },
          "operators": {
            "type": "array",
            "description": "Unique operator names available (normalized, no empty entries).",
            "items": {
              "type": "string"
            },
            "example": [
              "Open Fiber",
              "TIM Wholesale",
              "Fastweb"
            ]
          },
          "best": {
            "type": [
              "object",
              "null"
            ],
            "description": "The best offer available (highest declared downlink), or null.",
            "additionalProperties": false,
            "properties": {
              "technology": {
                "type": "string",
                "description": "Technology of the best offer."
              },
              "tier": {
                "$ref": "#/components/schemas/BestSpeedTier"
              },
              "downlink_mbps": {
                "type": [
                  "number",
                  "null"
                ],
                "format": "double",
                "description": "Best declared downstream speed in Mbps."
              },
              "operator": {
                "type": "string",
                "description": "Operator of the best offer."
              }
            }
          }
        }
      },
      "CoverageTechnologySummary": {
        "type": "object",
        "description": "One row per technology of the coverage summary rollup: the best\ndeclared downlink for that technology, its tier and the\ndeduplicated operators offering it. Sorted by max downlink\ndescending (nulls last); mirrors the Python SDK `by_technology()`\nhelper.\n",
        "required": [
          "technology",
          "max_downlink_mbps",
          "best_speed_tier",
          "operators"
        ],
        "additionalProperties": false,
        "properties": {
          "technology": {
            "type": "string",
            "description": "Access technology for this row."
          },
          "max_downlink_mbps": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "Best declared downstream speed for this technology in Mbps; null when not reported."
          },
          "best_speed_tier": {
            "$ref": "#/components/schemas/BestSpeedTier"
          },
          "operators": {
            "type": "array",
            "description": "Deduplicated operators offering this technology.",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "LocationInfo": {
        "type": "object",
        "required": [
          "match",
          "latitude",
          "longitude",
          "confidence"
        ],
        "additionalProperties": false,
        "properties": {
          "match": {
            "type": "string",
            "description": "Normalized address or `\"<lat>, <lon>\"` for coordinate queries.\n"
          },
          "latitude": {
            "type": "number",
            "format": "double"
          },
          "longitude": {
            "type": "number",
            "format": "double"
          },
          "comune": {
            "type": [
              "string",
              "null"
            ]
          },
          "provincia": {
            "type": [
              "string",
              "null"
            ]
          },
          "regione": {
            "type": [
              "string",
              "null"
            ]
          },
          "cap": {
            "type": [
              "string",
              "null"
            ]
          },
          "civico": {
            "type": [
              "string",
              "null"
            ]
          },
          "confidence": {
            "type": "string",
            "enum": [
              "high",
              "comune",
              "low",
              "coordinates"
            ],
            "description": "`high` (civic match), `comune` (centroid fallback), `low`, or\n`coordinates` (the query already supplied lat/lon).\n"
          },
          "code": {
            "type": [
              "string",
              "null"
            ],
            "description": "Stable address code (`comune/odonimo/civico`) for civic-level\nmatches, `null` otherwise. Store it with the lead to re-query\ncoverage later without address strings or coordinates.\n"
          }
        }
      },
      "AutocompleteStreetCivic": {
        "type": "object",
        "description": "An existing civic number of the suggested street, with its stable\naddress code. Attached to street suggestions so forms can offer\n\"pick a civic\" chips and submit exact coverage immediately.\n",
        "required": [
          "civico",
          "code",
          "latitude",
          "longitude"
        ],
        "additionalProperties": false,
        "properties": {
          "civico": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "Stable address code; pass to /coverage/italy?code=."
          },
          "latitude": {
            "type": "number",
            "format": "double"
          },
          "longitude": {
            "type": "number",
            "format": "double"
          }
        }
      },
      "AutocompleteStreet": {
        "type": "object",
        "required": [
          "kind",
          "street",
          "comune",
          "provincia",
          "cap",
          "latitude",
          "longitude",
          "display"
        ],
        "additionalProperties": false,
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "street"
            ]
          },
          "street": {
            "type": "string",
            "description": "Suggested street name in proper Italian case (e.g. Via Toledo)."
          },
          "comune": {
            "type": "string"
          },
          "provincia": {
            "type": "string"
          },
          "cap": {
            "type": "string"
          },
          "latitude": {
            "type": "number",
            "format": "double"
          },
          "longitude": {
            "type": "number",
            "format": "double"
          },
          "civics": {
            "type": "array",
            "description": "The street's existing civic numbers (best coverage suggestion\nis a real civic); each carries its stable `code`. Empty when\nthe civici index has no entries for the street — let the user\ntype the civic number in that case.\n",
            "items": {
              "$ref": "#/components/schemas/AutocompleteStreetCivic"
            }
          },
          "display": {
            "type": "string",
            "description": "Ready-to-show label in proper Italian case, e.g. `Piazza del Tiratoio, Firenze`."
          },
          "main_text": {
            "type": "string",
            "description": "Form-native headline for dropdown rendering (e.g. the street or\nthe street + civic). Pair with `matches` for bold highlighting.\n"
          },
          "secondary_text": {
            "type": "string",
            "description": "Comune context for the dropdown line (e.g. `Napoli, 80121`)."
          },
          "matches": {
            "type": "array",
            "description": "Byte offset/length of the typed tokens inside `main_text`, for\nbold highlighting in dropdowns.\n",
            "items": {
              "type": "object",
              "required": [
                "offset",
                "length"
              ],
              "additionalProperties": false,
              "properties": {
                "offset": {
                  "type": "integer",
                  "minimum": 0
                },
                "length": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          }
        }
      },
      "AutocompleteComune": {
        "type": "object",
        "required": [
          "kind",
          "name",
          "provincia",
          "regione",
          "cap",
          "latitude",
          "longitude",
          "display"
        ],
        "additionalProperties": false,
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "comune"
            ]
          },
          "name": {
            "type": "string",
            "description": "Comune name."
          },
          "provincia": {
            "type": "string"
          },
          "regione": {
            "type": "string"
          },
          "cap": {
            "type": "string"
          },
          "latitude": {
            "type": "number",
            "format": "double"
          },
          "longitude": {
            "type": "number",
            "format": "double"
          },
          "display": {
            "type": "string",
            "description": "Ready-to-show label in proper Italian case, e.g. `Firenze (Firenze)`."
          },
          "main_text": {
            "type": "string",
            "description": "Form-native headline for dropdown rendering (e.g. the street or\nthe street + civic). Pair with `matches` for bold highlighting.\n"
          },
          "secondary_text": {
            "type": "string",
            "description": "Comune context for the dropdown line (e.g. `Napoli, 80121`)."
          },
          "matches": {
            "type": "array",
            "description": "Byte offset/length of the typed tokens inside `main_text`, for\nbold highlighting in dropdowns.\n",
            "items": {
              "type": "object",
              "required": [
                "offset",
                "length"
              ],
              "additionalProperties": false,
              "properties": {
                "offset": {
                  "type": "integer",
                  "minimum": 0
                },
                "length": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          }
        }
      },
      "AutocompleteCivico": {
        "type": "object",
        "description": "A civic-level suggestion: the user typed a street plus the leading\ncivic number (\"via tol 2\"). Carries the stable `code`, so the form\ncan submit it directly to `/coverage/italy?code=...`.\n",
        "required": [
          "kind",
          "street",
          "civico",
          "comune",
          "latitude",
          "longitude",
          "code",
          "display"
        ],
        "additionalProperties": false,
        "properties": {
          "kind": {
            "type": "string",
            "const": "civico"
          },
          "street": {
            "type": "string",
            "description": "Street (odonimo) name."
          },
          "civico": {
            "type": "string",
            "description": "Civic number."
          },
          "comune": {
            "type": "string"
          },
          "provincia": {
            "type": "string"
          },
          "regione": {
            "type": "string"
          },
          "cap": {
            "type": "string"
          },
          "latitude": {
            "type": "number",
            "format": "double"
          },
          "longitude": {
            "type": "number",
            "format": "double"
          },
          "code": {
            "type": "string",
            "description": "Stable address code; pass it to /coverage/italy?code=."
          },
          "display": {
            "type": "string",
            "description": "Ready-to-show label in proper Italian case, e.g. \"Via Crespole e Fabbriche 125 Pistoia\""
          },
          "main_text": {
            "type": "string",
            "description": "Form-native headline for dropdown rendering (e.g. the street or\nthe street + civic). Pair with `matches` for bold highlighting.\n"
          },
          "secondary_text": {
            "type": "string",
            "description": "Comune context for the dropdown line (e.g. `Napoli, 80121`)."
          },
          "matches": {
            "type": "array",
            "description": "Byte offset/length of the typed tokens inside `main_text`, for\nbold highlighting in dropdowns.\n",
            "items": {
              "type": "object",
              "required": [
                "offset",
                "length"
              ],
              "additionalProperties": false,
              "properties": {
                "offset": {
                  "type": "integer",
                  "minimum": 0
                },
                "length": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          }
        }
      },
      "AutocompleteResponse": {
        "type": "object",
        "required": [
          "city",
          "address",
          "count",
          "results",
          "attribution"
        ],
        "additionalProperties": false,
        "properties": {
          "usage": {
            "type": "object",
            "description": "Compact quota state for this request (mirror of the\nX-RateLimit-* headers): remaining burst, reset_seconds and\ndaily/monthly remaining. Lets clients self-meter. Absent\nfor internal/service keys; demo keys reply with a fixed\nsample.\n",
            "additionalProperties": true
          },
          "city": {
            "type": [
              "string",
              "null"
            ],
            "description": "Echo of the `city` parameter; `null`/absent when not given."
          },
          "address": {
            "type": [
              "string",
              "null"
            ],
            "description": "Echo of the `address` parameter; `null`/absent when not given."
          },
          "count": {
            "type": "integer",
            "minimum": 0
          },
          "results": {
            "type": "array",
            "description": "Suggestions. `kind` distinguishes them: `\"comune\"` (city field),\n`\"street\"` (address field without a civic number), or\n`\"civico\"` (address field ending in a civic number, e.g.\n\"via tol 2\" — carries the stable `code` for a direct\n`/coverage/italy?code=...` submission).\n",
            "items": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/AutocompleteStreet"
                },
                {
                  "$ref": "#/components/schemas/AutocompleteComune"
                },
                {
                  "$ref": "#/components/schemas/AutocompleteCivico"
                }
              ]
            }
          },
          "attribution": {
            "type": "string",
            "description": "Mandatory CC BY 4.0 credit for the address data (Istat – AdE\nANNCSU). Display it wherever suggestions are shown.\n"
          }
        }
      }
    }
  }
}