From 69aab96fb1bed2e24f7913917d84ab2bb789df0c Mon Sep 17 00:00:00 2001 From: Codewriter90x Date: Thu, 30 Jul 2026 16:07:21 +0200 Subject: [PATCH 1/2] Harden data quality and publication pipeline --- .github/workflows/data-pipeline.yml | 61 +- .github/workflows/pages.yml | 29 +- .github/workflows/release.yml | 14 +- .gitignore | 1 + DATASET_PIPELINE.md | 23 +- DATA_SOURCES.md | 14 + README.md | 25 +- SCHEMA.md | 9 +- docs/TYPED_CONTRACT_MIGRATION.md | 11 + pyproject.toml | 2 +- reports/build-metadata.json | 2 +- reports/determinism.json | 2 +- reports/quality-validation.json | 2915 +++++++++++++++++++++- requirements-dev.txt | 2 + schemas/italian_locations-v4.schema.json | 228 +- scripts/build_geography.py | 26 +- scripts/build_pages.py | 251 +- scripts/check_workflow_pins.py | 61 + scripts/pages_model.py | 2 - scripts/source_data.py | 10 +- scripts/validate_dataset.py | 21 +- scripts/validate_site.py | 63 +- scripts/validate_typed_contract.py | 81 + scripts/validators/coordinates.py | 95 + site/assets/app.js | 11 +- site/assets/core.mjs | 17 +- site/assets/styles.css | 8 + site/index.html | 2 +- sources/README.md | 8 + sources/cache/Limiti01012026_g.zip | Bin 0 -> 10450609 bytes sources/manifest.json | 14 + tests/pages_core.test.mjs | 9 + tests/test_integrity.py | 31 +- tests/test_pages.py | 24 +- tests/test_quality_gate.py | 40 + tests/test_typed_contract.py | 23 + tests/test_workflow_security.py | 42 + 37 files changed, 4049 insertions(+), 128 deletions(-) create mode 100644 scripts/check_workflow_pins.py create mode 100644 scripts/validate_typed_contract.py create mode 100644 sources/cache/Limiti01012026_g.zip create mode 100644 tests/test_workflow_security.py diff --git a/.github/workflows/data-pipeline.yml b/.github/workflows/data-pipeline.yml index 905037a..2c987f9 100644 --- a/.github/workflows/data-pipeline.yml +++ b/.github/workflows/data-pipeline.yml @@ -19,6 +19,8 @@ jobs: steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 @@ -28,9 +30,14 @@ jobs: cache-dependency-path: | requirements.txt requirements-dev.txt + requirements-geography.txt - name: Install runtime and quality dependencies - run: python -m pip install -r requirements.txt -r requirements-dev.txt + run: >- + python -m pip install + -r requirements.txt + -r requirements-dev.txt + -r requirements-geography.txt - name: Lint Python sources and tests run: ruff check scripts tests @@ -38,14 +45,35 @@ jobs: - name: Type-check all maintained Python scripts run: mypy + - name: Require immutable GitHub Action references + run: python scripts/check_workflow_pins.py + + - name: Validate the schema 4 typed preview + run: python scripts/validate_typed_contract.py + + - name: Audit pinned Python dependencies + run: >- + pip-audit + -r requirements.txt + -r requirements-dev.txt + -r requirements-geography.txt + - name: Enforce maintained-script test coverage run: | coverage run -m unittest discover -s tests coverage run --append scripts/build_dataset.py + coverage run --append scripts/check_determinism.py + coverage run --append scripts/validate_dataset.py coverage run --append scripts/build_pages.py coverage run --append scripts/build_release.py coverage run --append scripts/validate_release.py - coverage run --append scripts/build_geography.py --check + coverage run --append scripts/build_geography.py --check --require-rebuild + coverage run --append scripts/validate_typed_contract.py + coverage run --append scripts/export_typed_json.py \ + --output "${RUNNER_TEMP}/italian_locations.typed.json" \ + --sqlite-output "${RUNNER_TEMP}/italian_locations.typed.sqlite" + coverage run --append scripts/reconciliation_backlog.py + coverage run --append scripts/check_workflow_pins.py coverage report coverage xml @@ -56,10 +84,12 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.11", "3.12", "3.13"] + python-version: ["3.11", "3.12", "3.13", "3.14"] steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 @@ -67,8 +97,12 @@ jobs: python-version: ${{ matrix.python-version }} cache: pip - - name: Install pipeline dependency - run: python -m pip install -r requirements.txt + - name: Install pipeline and test dependencies + run: >- + python -m pip install + -r requirements.txt + -r requirements-dev.txt + -r requirements-geography.txt - name: Validate dataset run: python scripts/validate_dataset.py @@ -83,6 +117,8 @@ jobs: steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 @@ -90,8 +126,12 @@ jobs: python-version: "3.12" cache: pip - - name: Install pipeline dependency - run: python -m pip install -r requirements.txt + - name: Install pipeline and geographic dependencies + run: >- + python -m pip install + -r requirements.txt + -r requirements-dev.txt + -r requirements-geography.txt - name: Validate committed data and quality contracts run: python scripts/validate_dataset.py @@ -115,8 +155,11 @@ jobs: - name: Test Pages search and map logic run: node --test tests/pages_core.test.mjs - - name: Verify committed geographic base - run: python scripts/build_geography.py --check + - name: Rebuild and verify committed geographic base + run: python scripts/build_geography.py --check --require-rebuild + + - name: Validate the schema 4 typed preview + run: python scripts/validate_typed_contract.py - name: Build deterministic GitHub Pages site run: python scripts/build_pages.py diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index d19121c..800226b 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -7,10 +7,15 @@ on: - "assets/social-preview.jpg" - "data/italian_locations.csv" - "project.json" + - "requirements-geography.txt" - "scripts/build_pages.py" - "scripts/build_geography.py" + - "scripts/pages_model.py" - "scripts/project_metadata.py" + - "scripts/validate_site.py" - "site/**" + - "sources/cache/Limiti01012026_g.zip" + - "sources/manifest.json" - "tests/pages_core.test.mjs" - "tests/test_pages.py" push: @@ -21,18 +26,21 @@ on: - "assets/social-preview.jpg" - "data/italian_locations.csv" - "project.json" + - "requirements-geography.txt" - "scripts/build_pages.py" - "scripts/build_geography.py" + - "scripts/pages_model.py" - "scripts/project_metadata.py" + - "scripts/validate_site.py" - "site/**" + - "sources/cache/Limiti01012026_g.zip" + - "sources/manifest.json" - "tests/pages_core.test.mjs" - "tests/test_pages.py" workflow_dispatch: permissions: contents: read - pages: write - id-token: write concurrency: group: >- @@ -48,23 +56,30 @@ jobs: name: Build and test site runs-on: ubuntu-latest timeout-minutes: 10 + permissions: + contents: read steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" + - name: Install geographic build dependencies + run: python -m pip install -r requirements-geography.txt + - name: Test search and map logic run: node --test tests/pages_core.test.mjs - name: Test deterministic Pages build run: python -m unittest discover -s tests -p "test_pages.py" -v - - name: Verify committed geographic base - run: python scripts/build_geography.py --check + - name: Rebuild and verify committed geographic base + run: python scripts/build_geography.py --check --require-rebuild - name: Build GitHub Pages artifact run: python scripts/build_pages.py --output _site @@ -72,9 +87,6 @@ jobs: - name: Validate accessibility, local links and performance budgets run: python scripts/validate_site.py _site - - name: Configure GitHub Pages - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - - name: Upload GitHub Pages artifact uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: @@ -89,6 +101,9 @@ jobs: runs-on: ubuntu-latest needs: build timeout-minutes: 10 + permissions: + id-token: write + pages: write steps: - name: Deploy GitHub Pages id: deployment diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b3f4ec1..dd1b164 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,8 +37,12 @@ jobs: python-version: "3.12" cache: pip - - name: Install pipeline dependency - run: python -m pip install -r requirements.txt + - name: Install pipeline and validation dependencies + run: >- + python -m pip install + -r requirements.txt + -r requirements-dev.txt + -r requirements-geography.txt - name: Require tag to match project metadata run: >- @@ -59,6 +63,12 @@ jobs: - name: Run tests run: python -m unittest discover -s tests -v + - name: Rebuild and verify committed geographic base + run: python scripts/build_geography.py --check --require-rebuild + + - name: Validate the schema 4 typed preview + run: python scripts/validate_typed_contract.py + - name: Build release directory run: >- python scripts/build_release.py diff --git a/.gitignore b/.gitignore index c8c9b26..d8c1785 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ sources/cache/* !sources/cache/.gitkeep +!sources/cache/Limiti01012026_g.zip .DS_Store .venv/ __pycache__/ diff --git a/DATASET_PIPELINE.md b/DATASET_PIPELINE.md index 70207e7..626c4d0 100644 --- a/DATASET_PIPELINE.md +++ b/DATASET_PIPELINE.md @@ -39,6 +39,11 @@ SHA-256 prima di aprire il workbook ISTAT o `IT.zip`. Lo snapshot GeoNames è committato come file originale; non viene modificato o estratto manualmente nel repository. +L'archivio dei confini regionali generalizzati ISTAT è dichiarato come fonte +ausiliaria non canonica. È committato con checksum e la CI ricostruisce +obbligatoriamente `site/assets/italy-regions.geojson`; un cache assente o +dipendenze geografiche mancanti fanno fallire il gate. + ## Riconciliazione `scripts/reconcile_sources.py` usa: @@ -78,6 +83,11 @@ canonica. `scripts/export_sql.py` crea lo script SQLite-compatible. - `reports/determinism.json`: firme di due build; - `reports/quality-validation.json`: gate strutturali separati dalla readiness. +Quest'ultimo include anche la distribuzione dei riusi esatti di coordinate, +alla grana del luogo univoco. I cluster grandi sono marcati +`review_required`: non rendono strutturalmente invalido il dump sorgente, ma +impediscono di descrivere `geonames_place_match` come verifica geografica. + Il report legacy può cambiare se cambia il legacy; gli output canonici no. ## Quality model @@ -96,7 +106,10 @@ certificazione postale. ## Comandi ```bash -python -m pip install -r requirements.txt -r requirements-dev.txt +python -m pip install \ + -r requirements.txt \ + -r requirements-dev.txt \ + -r requirements-geography.txt python scripts/build_dataset.py python scripts/check_determinism.py python scripts/validate_dataset.py @@ -104,6 +117,9 @@ ruff check scripts tests mypy coverage run -m unittest discover -s tests coverage report +python scripts/validate_typed_contract.py +python scripts/build_geography.py --check --require-rebuild +python scripts/check_workflow_pins.py node --test tests/pages_core.test.mjs python scripts/build_pages.py python scripts/build_release.py @@ -111,8 +127,9 @@ python scripts/validate_release.py git diff --check ``` -La CI esegue questi gate su Python 3.11, 3.12 e 3.13. Le Actions sono fissate -a commit SHA immutabili. +La CI esegue questi gate su Python 3.11, 3.12, 3.13 e 3.14. Le Actions sono +fissate a commit SHA immutabili e un test impedisce regressioni a tag o branch. +`pip-audit` controlla le dipendenze dichiarate. ## Aggiornamento fonti diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index bca2dc6..9807022 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -34,6 +34,20 @@ GeoNames dichiara che molte coordinate sono trovate o stimate algoritmicamente e fornisce i dati “as is”. Il campo `accuracy` originale è preservato. GeoNames non è Poste Italiane e non è descritto come tale. +### ISTAT — confini regionali generalizzati + +- ruolo: sola base cartografica riproducibile della GitHub Pages, non input del + dataset canonico; +- data di riferimento: 1 gennaio 2026; +- archivio: `sources/cache/Limiti01012026_g.zip`; +- SHA-256: + `b011a590656c3a3ebc297fba80726a376aa843b6f164641cf6a4a990021a81d6`; +- licenza/attribuzione: ISTAT, CC BY 4.0. + +La CI ricostruisce il GeoJSON semplificato dal file originale e confronta i +byte con l'asset committato. Il controllo non può più risultare verde saltando +la ricostruzione. + ## Legacy `legacy/2023-05-02-original/Italian Cities.csv` ha SHA-256 diff --git a/README.md b/README.md index 738fd59..ad6e941 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,14 @@ Alla grana del luogo univoco: 7.498 comuni hanno coordinate `geonames_place_match`, 10.270 località hanno coordinate `geonames_estimated` e 396 comuni sono `missing`. +`geonames_place_match` significa che nome e territorio del record GeoNames +sono stati riconciliati in modo esatto con un comune ISTAT; **non** significa +che la coordinata sia stata verificata sul territorio. Il quality report +rileva 1.305 gruppi di coordinate esattamente condivise da 3.502 luoghi; 16 +gruppi con almeno 10 luoghi richiedono revisione e il maggiore contiene 66 +luoghi. Il dettaglio riproducibile è in +[`reports/quality-validation.json`](reports/quality-validation.json). + ## Dataset | File | Grana | @@ -163,13 +171,19 @@ ORDER BY postal_code; ## Build e validazione -Richiede Python 3.11–3.13: +Richiede Python 3.11–3.14: ```bash -python -m pip install -r requirements.txt -r requirements-dev.txt +python -m pip install \ + -r requirements.txt \ + -r requirements-dev.txt \ + -r requirements-geography.txt python scripts/build_dataset.py python scripts/check_determinism.py python scripts/validate_dataset.py +python scripts/validate_typed_contract.py +python scripts/build_geography.py --check --require-rebuild +python scripts/check_workflow_pins.py python -m unittest discover -s tests -v node --test tests/pages_core.test.mjs python scripts/build_pages.py @@ -177,8 +191,11 @@ python scripts/build_release.py python scripts/validate_release.py ``` -La CI aggiunge Ruff, mypy, coverage, matrice Python 3.11–3.13, CodeQL e -controlli deterministici. Dettagli in [`DATASET_PIPELINE.md`](DATASET_PIPELINE.md). +La CI aggiunge Ruff, mypy, coverage, matrice Python 3.11–3.14, CodeQL, +dependency audit, pin SHA delle Actions e controlli deterministici. La Pages +genera inoltre schede HTML indicizzabili per province e comuni, una sitemap e +una pagina 404 accessibile. Dettagli in +[`DATASET_PIPELINE.md`](DATASET_PIPELINE.md). ## Differenze rispetto al legacy diff --git a/SCHEMA.md b/SCHEMA.md index d7d580c..09b1418 100644 --- a/SCHEMA.md +++ b/SCHEMA.md @@ -71,7 +71,7 @@ Vista unificata, una riga per relazione CAP: | `province_code`, `province_name`, `region_name` | contesto territoriale | | `country_code`, `country_name` | `IT`, `Italia` | | `latitude`, `longitude` | coppia WGS84 opzionale | -| `coordinate_verification` | origine/semantica della coordinata | +| `coordinate_verification` | origine e tipo di riconciliazione, non certificazione geografica | | `coordinate_accuracy` | campo GeoNames `accuracy`, preservato | | `reconciliation_outcome` | esito separato dal dato CAP | | `reconciliation_method` | regola riproducibile applicata | @@ -97,6 +97,13 @@ Vista unificata, una riga per relazione CAP: - `official_boundary_derived` — riservato, inutilizzato; - `missing`. +`geonames_place_match` documenta il match esatto di nome, provincia e regione +fra GeoNames e ISTAT. Non dimostra che latitudine e longitudine siano il +centroide o un punto verificato del comune. I riusi esatti sono profilati in +`reports/quality-validation.json`; i cluster grandi restano un segnale di +revisione non bloccante finché non esiste una fonte geografica autorevole con +licenza e provenienza compatibili. + `reconciliation_outcome`: - `exact_unambiguous`; diff --git a/docs/TYPED_CONTRACT_MIGRATION.md b/docs/TYPED_CONTRACT_MIGRATION.md index b4ce68d..8be53be 100644 --- a/docs/TYPED_CONTRACT_MIGRATION.md +++ b/docs/TYPED_CONTRACT_MIGRATION.md @@ -20,6 +20,17 @@ python scripts/export_typed_json.py \ --sqlite-output dist/italian_locations.typed.sqlite ``` +The generated JSON is validated against +`schemas/italian_locations-v4.schema.json` with a Draft 2020-12 validator: + +```bash +python scripts/validate_typed_contract.py +``` + +The schema fixes the exact field order, numeric/null types, enums, Italian +coordinate bounds and the consistency rules for missing CAP and coordinates. +A negative regression test proves that string coordinates are rejected. + The preview SQLite table uses `REAL`, `INTEGER` and `NULL` directly. The original v2 JSON, SQLite table and SQL script stay unchanged. diff --git a/pyproject.toml b/pyproject.toml index c977ffa..7e90d2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,6 @@ omit = [ ] [tool.coverage.report] -fail_under = 80 +fail_under = 85 show_missing = true skip_covered = true diff --git a/reports/build-metadata.json b/reports/build-metadata.json index c65f206..1aac25d 100644 --- a/reports/build-metadata.json +++ b/reports/build-metadata.json @@ -26,7 +26,7 @@ "operational_data_readiness": "experimental_non_official", "postal_use_warning": "Experimental, non-official data. GeoNames is not Poste Italiane.", "schema_version": "3.0.0", - "source_manifest_sha256": "02f4f84002abfa710bb8b20561165e8615c4bf35716c515fa2ab2fdc97f2ea25", + "source_manifest_sha256": "ec18bcb983c4fdff17c9a75a58fb995d3cfb4490f29be39a8c22a1e3a56758ef", "statistics": { "ambiguous_matches": 0, "exact_matches": 8095, diff --git a/reports/determinism.json b/reports/determinism.json index 995ed5f..cc84057 100644 --- a/reports/determinism.json +++ b/reports/determinism.json @@ -8,7 +8,7 @@ "runs": 2, "signatures": { "byte_sha256": { - "build_metadata": "f12a901848d2f65f6d3e309e76d78eb1759a53b6b4707f4f80120c46151a0c5e", + "build_metadata": "21552203af29355f948bd9f3d7bdf53d59da49878c4a6278b60435b1c7248a90", "export_manifest": "09f23108a80d10b22a79848496070b74115ec54898ad955033bca8ba153ebdf7", "italian_locations_csv": "803adc8dcb9e4cf0c65f6284e640a4e3b44b6f8b599f66c01cfaf9de231cf977", "italian_locations_json": "7a20df78d925d4ce9e8be8d3ec31b7687665ac64f3f21a6adba793f7099acf5f", diff --git a/reports/quality-validation.json b/reports/quality-validation.json index 4f96dc0..6090628 100644 --- a/reports/quality-validation.json +++ b/reports/quality-validation.json @@ -5,6 +5,2913 @@ "4": 15951, "missing": 396 }, + "coordinate_distribution": { + "exact_shared_coordinate_groups": 1305, + "grain": "unique_location", + "groups_requiring_review": 16, + "interpretation": "Exact coordinate reuse is a source-quality signal, not automatic proof of an error. geonames_place_match records a conservative name and territory match; it does not certify coordinate accuracy.", + "largest_shared_coordinate_group": 66, + "locations_in_shared_coordinate_groups": 3502, + "review_groups": [ + { + "latitude": 46.6304, + "location_count": 66, + "locations": [ + { + "location_id": "IT-LOC-77bfb27f-e2cf-53c5-9e96-d151d03b5911", + "name": "Afers", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-e862cc07-558f-515f-a523-74d02dfc0525", + "name": "Aldein", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-0d5133f7-9761-5593-8fe2-0616280ac58c", + "name": "Barbian", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-89f9e8d7-4ad4-57f5-b26e-636e4f65e722", + "name": "Entiklar", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-da7ea6b3-c638-5cfc-91ba-38ff7c65c40a", + "name": "Fennberg", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-f952cf57-c540-5d3b-9a1e-cff0c5a8e010", + "name": "Flains", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-6956261f-3cad-5901-99b5-f936b8530df1", + "name": "Flans", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-7194e87a-3934-58bc-a2b0-54ef078f81b0", + "name": "Flitt", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-6a56ef09-600b-5a69-8904-82937640f43e", + "name": "Freins", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-d23962da-9904-5200-b3f3-f0c1046414dc", + "name": "Garn", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-23107b36-0c99-50e3-ac51-53399745adb2", + "name": "Glen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-0e5d0761-5f98-556d-8167-2b071289493f", + "name": "Grasstein", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-633ff241-130a-56b4-a147-5c9b9549e34d", + "name": "Graun/Unterl.", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-b50c8a85-a8c2-5fa5-b2ba-e7956b79b237", + "name": "Gschnon", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-cafffa64-bf34-5a36-a8ac-ec842f516792", + "name": "Hohlen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-cd282b94-bcda-5e80-b109-5f19ab338a4c", + "name": "Innerpfitsch", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-c65269f5-fbf7-5a06-9dff-0558ef3b613a", + "name": "Karnol", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-be605a99-4cf6-522e-98ae-3c58a39ef0d1", + "name": "Klerant", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-d7eb3647-2c97-510a-ba0c-67283bb4997f", + "name": "Kollmann", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-6637a86b-c77a-5168-a964-411b9612ef20", + "name": "Margreid", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-b4a2a01e-386f-5a32-a7d8-815d91c8c2da", + "name": "Mazon", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-a5ea1ceb-f80f-5b7b-a69a-ce7a464e5ecb", + "name": "Mellaun", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-6fb5cd2d-7aa4-55ca-bc39-b6641b3be983", + "name": "Milland", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-2acb55e3-9b8a-56ea-83cb-a138e1d77040", + "name": "Mittewald", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-cbfdce97-d18c-5630-bd15-b0bf27ecf045", + "name": "Montan", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-c49152fd-1c29-5fb1-8548-dd00d3095e23", + "name": "Mühlen/Truden", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-33412fee-aa5c-5c8a-8e65-8a85f810df34", + "name": "Oberau", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-2e4278e7-1f9a-58d8-a1fe-ceff14609120", + "name": "Pflersch", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-473b09db-78e0-54c1-acbb-3efc89265cd0", + "name": "Pfulters", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-f9b65093-db00-51d8-9d38-46747c711f43", + "name": "Pinzagen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-6d2699f4-da14-510f-9dcd-6c864fe10951", + "name": "Pinzon", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-5ded91d3-bc1c-5869-a750-0586da9f3138", + "name": "Proveis", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-e894dd8b-dc50-5d12-871e-770efa277ab4", + "name": "Pufels", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-a319503a-8785-5f73-bb7d-31124ec19471", + "name": "Rungg", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-96200f57-42da-53a0-9ad9-e68ab68f4c25", + "name": "S. Lugano", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-9cacf6c1-cc97-5a5a-a828-e72c749c7b41", + "name": "Sarns", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-66fc26f3-d18c-528d-ad02-2fc8aebafa6b", + "name": "Schalders", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-abf1aeff-8a07-5a49-8066-f86228bd6e5a", + "name": "Schmuders", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-2c0ee67c-7f71-53cc-8208-6c7295769b84", + "name": "Schnauders", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-24b685cf-8b18-59fe-b3fe-b7c4a153a8d2", + "name": "Schrambach", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-840b4473-e81d-52c9-92d1-14bb163ef039", + "name": "Spiluck", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-7edc12e4-221f-5d09-a3dc-99dceb6cc86c", + "name": "St. Andrä_", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-a9dd99d3-6592-5591-b5b5-20ddfb1b0d3d", + "name": "St. Christina/S. Crestina -Gherdeina", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-3a682f49-5228-5a7d-90ea-648cbeede33c", + "name": "St. Jakob", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-a59e8a0c-e935-59a1-a874-60028c5b161b", + "name": "St. Leonhard", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-f021886a-2b0d-5bd9-9e80-b7fcce244f4b", + "name": "St. Michael/Kastelr.", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-3ae07338-73d3-51e7-8386-983c9a12b5de", + "name": "St. Moritz b. Villanders", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-d71c2ba3-776b-522d-bcdf-06abcf69366b", + "name": "St. Oswald", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-1729a288-dc0d-5b9c-9651-12935a794ba8", + "name": "St. Peter b. Lajen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-59429a57-7699-5610-8c89-8f25462fe378", + "name": "Steckholz", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-4562e11f-aaec-540c-98a4-c8bae9c11e80", + "name": "Söll", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-01f9fe19-2a13-503a-90f7-5b020d8a4858", + "name": "Tagusens", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-c081a126-8678-55a1-9d08-f6293b9e7da6", + "name": "Tanürz", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-77074fb2-2382-511d-a1b3-97dbc126f1f0", + "name": "Teis", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-62926fb5-465b-5aa0-a916-847ef07153c7", + "name": "Telfes", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-25dcfd6e-d1f9-5ef9-ab10-899c51437e7b", + "name": "Thuins", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-fda385db-bded-5501-b825-b6ead8bca797", + "name": "Tils", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-d228580a-1590-52e2-b928-4aec31c91e0f", + "name": "Trens", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-0df8a297-4275-584e-9fa6-6a3892aac30e", + "name": "Tschöfs", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-94f3069d-93d5-573b-98aa-12d6d4aaae27", + "name": "Tschötsch", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-60cc9768-2c72-5316-9407-def127ee81ba", + "name": "Tschövas", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-cf8d453f-49a2-5df0-9ed5-cd6b2b7acc36", + "name": "Verdings", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-743f06e1-0ab2-5f63-803e-91f53c9cfd2f", + "name": "Villnöß", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-f37d1d01-2c84-51a7-a05b-73b1f60dd4f4", + "name": "Viums", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-54640c48-6bbd-5fb5-9437-4974e3c896f4", + "name": "Weißenstein", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-298b9e1b-944f-52d3-8ba1-e315113e5475", + "name": "Zinggen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + } + ], + "longitude": 11.4693, + "municipality_count": 0, + "verification_counts": { + "geonames_estimated": 66 + } + }, + { + "latitude": 39.2071, + "location_count": 59, + "locations": [ + { + "location_id": "IT-LOC-9950d056-dd84-5d6b-b9b3-18b6581a9fe6", + "name": "Arixi", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-e90022d0-54e4-5409-9ba5-5e1b0d0d093f", + "name": "Forte Village", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-e44b1e64-5b34-5b6d-bcc1-fd7c4625364a", + "name": "Lixius", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-59c72eee-9925-59d3-b423-7fda8b622d07", + "name": "Santa Maria", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-e8146d33-0732-51df-8d5f-610f7027bbb2", + "name": "Seulo", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-67831e36-ffbd-54f7-9314-e3eebfbbb42a", + "name": "Sisini", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-8f123feb-d312-503c-b2fd-fe0149de4599", + "name": "Teulada", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-1d413b8d-ddda-50f2-a2b0-122b1399efde", + "name": "Villagreca", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118001", + "name": "Armungia", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118003", + "name": "Ballao", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118004", + "name": "Barrali", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118005", + "name": "Burcei", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118008", + "name": "Castiadas", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118010", + "name": "Decimoputzu", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118011", + "name": "Dolianova", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118012", + "name": "Domus de Maria", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118013", + "name": "Donori", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118015", + "name": "Escalaplano", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118016", + "name": "Escolca", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118017", + "name": "Esterzili", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118019", + "name": "Gergei", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118020", + "name": "Gesico", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118021", + "name": "Goni", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118022", + "name": "Guamaggiore", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118023", + "name": "Guasila", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118024", + "name": "Isili", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118025", + "name": "Mandas", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118027", + "name": "Monastir", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118029", + "name": "Muravera", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118030", + "name": "Nuragus", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118031", + "name": "Nurallao", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118032", + "name": "Nuraminis", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118033", + "name": "Nurri", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118034", + "name": "Orroli", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118035", + "name": "Ortacesus", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118036", + "name": "Pimentel", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118040", + "name": "Sadali", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118041", + "name": "Samatzai", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118043", + "name": "San Nicolò Gerrei", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118044", + "name": "San Sperate", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118045", + "name": "San Vito", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118046", + "name": "Sant'Andrea Frius", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118049", + "name": "Selegas", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118050", + "name": "Senorbì", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118051", + "name": "Serdiana", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118052", + "name": "Serri", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118056", + "name": "Siliqua", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118057", + "name": "Silius", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118059", + "name": "Siurgus Donigala", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118060", + "name": "Soleminis", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118061", + "name": "Suelli", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118063", + "name": "Ussana", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118065", + "name": "Vallermosa", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118067", + "name": "Villanova Tulo", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118068", + "name": "Villaputzu", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118069", + "name": "Villasalto", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118070", + "name": "Villasimius", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118071", + "name": "Villasor", + "province_code": "CA", + "region_name": "Sardegna" + }, + { + "location_id": "IT-COM-118072", + "name": "Villaspeciosa", + "province_code": "CA", + "region_name": "Sardegna" + } + ], + "longitude": 9.1074, + "municipality_count": 51, + "verification_counts": { + "geonames_estimated": 8, + "geonames_place_match": 51 + } + }, + { + "latitude": 46.7758, + "location_count": 49, + "locations": [ + { + "location_id": "IT-LOC-2edd4c85-82a3-53c1-a77e-7452c53d3e79", + "name": "Abtei/Badia", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-f2f17e6c-bede-5990-81e2-bf3f1f7ac2d2", + "name": "Ahornach", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-6618384c-b4eb-5fee-a05d-fb9adce4cc17", + "name": "Ahrntal", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-a4ec12e1-6582-52e4-8b56-8c5e85b493dd", + "name": "Aufhofen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-f2ac2cfe-0487-52fd-9b6f-00dafa1f8a25", + "name": "Ellen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-9a7cd86b-7145-540e-8e21-9ea553caa6bc", + "name": "Enneberg/Mareo", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-96f2db0a-318c-5528-bd60-50f90ca469a3", + "name": "Geiselsberg", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-9a780202-bce8-5917-a040-ff794b0aff1d", + "name": "Greinwalden", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-bba61379-0774-556a-b53b-b326315b8ca8", + "name": "Hofern", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-c4c8b547-8bba-54b3-8ab4-0402fde2553e", + "name": "Issing", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-efb93897-dee8-5670-b065-777a651833cb", + "name": "Kampill", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-69e902c7-6ffe-5e35-981e-ae44a6dc0ce5", + "name": "Kasern", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-05171ae3-b8b6-56ee-9342-3442e5c07f43", + "name": "Kolfuschg", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-d57da5a4-4c80-5954-bbc8-2f93c8476252", + "name": "Kurfar", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-708064ac-387e-57c3-99d1-da75e0909999", + "name": "Margen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-83282127-af45-5bf9-b2dd-2c9a7f50e1f2", + "name": "Meransen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-aa137094-dc60-5de9-bf25-9900dbdebf94", + "name": "Montal", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-81accf2d-f7f7-5811-bdd3-a5adc1dc4f84", + "name": "Mühlbach b. Gais", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-abcd435a-72ef-5973-8155-e31524068948", + "name": "Mühlen i. Taufers", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-bff8de2c-4aba-5655-9d4f-059f4c15a866", + "name": "Mühlen/Pfalzen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-5a0ec05a-db10-5136-9c58-d91f58f83a8b", + "name": "Nasen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-51377226-e4e5-5723-ae7a-6b3c340de64a", + "name": "Oberolang", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-ef531bbb-5267-57b1-9440-18a6b3accffa", + "name": "Oberwielenbach", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-33cebbcb-55d1-5114-b43f-2fb321f7f19f", + "name": "Onach", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-5219840a-c940-52a0-bea8-f86cbf784f12", + "name": "Pflaurenz", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-2bf32b95-8d3f-5167-83ea-d063d6edb3c3", + "name": "Saalen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-adfe8f64-1fb9-5746-af1a-c5e9efda0641", + "name": "Spinges", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-3bd4d4d1-53d3-522a-8a70-42302b0b9a8c", + "name": "St. Georgen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-64950150-c538-5676-9da1-29766f9c5cba", + "name": "St. Jakob in Ahrn", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-2c362be7-3b8e-59e8-ae85-fb7f23b19652", + "name": "St. Johann in Ahrn", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-e56c00fc-4a03-53eb-8a09-062287595c5f", + "name": "St. Magdalena i. G.", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-5909efca-2aba-5d42-9d53-6ede3cfed6fb", + "name": "St. Martin in Gsies", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-f9a46a65-c0f9-5313-9122-7645ca2f0b65", + "name": "St. Martin in Thurn/S. Martin de Tor", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-b6e0ff4a-9222-50a5-82d8-a409fe10696d", + "name": "St. Peter in Ahrn", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-2ef10cf0-4d4e-5b3d-bed0-b394751247c1", + "name": "St. Veit in Prags", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-e0295bec-3bf6-53fa-a9b4-8cacce4b2d81", + "name": "St. Vigil i. Enneberg", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-8dd7cdc0-c653-59cf-abcd-c639776fd585", + "name": "St. Walburg i. Antholz", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-d37b7238-8708-57d5-b048-8f9beed7c195", + "name": "Stefansdorf", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-a392f4a3-5bb2-5ee7-afd4-ac9ac74a9710", + "name": "Stegen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-2e07291a-1640-5607-9deb-feea06eee627", + "name": "Taufers/Ahrntal", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-799250a5-ba94-582a-ad43-15a7cf2b5c04", + "name": "Untergsies", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-55f56b89-9a9d-5ad9-ad9f-beac56b6daec", + "name": "Untermoi", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-03aafe64-857a-5c3b-9db5-2d6a089c6c0f", + "name": "Untervintl", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-ddcaa40b-4196-5ee1-ab7c-aa67137ecd15", + "name": "Vals", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-d389996b-2370-52c5-a2cc-c3123556037d", + "name": "Vierschach", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-2acf8b0a-7a20-582a-80a1-abd571df8ab8", + "name": "Vintl", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-8979654c-1430-57eb-951e-af1a2bafc706", + "name": "Wengen/La Val", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-a72d7489-e607-5610-ad62-ee3bb9ebf1d5", + "name": "Wielenberg", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-68016cd6-ad5b-5807-83b6-44d77d426af7", + "name": "Winnebach", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + } + ], + "longitude": 11.9383, + "municipality_count": 0, + "verification_counts": { + "geonames_estimated": 49 + } + }, + { + "latitude": 40.7465, + "location_count": 36, + "locations": [ + { + "location_id": "IT-LOC-21357776-bb2e-5b63-b3d8-338080edc16e", + "name": "Bircolovo'", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-1e7e9ecc-0631-5d57-a708-e8957bfcd6c7", + "name": "Budditogliu", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-f3136ad6-74ec-5b8b-ba02-15fb49c3b1a0", + "name": "Franculacciu", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-172b7407-d2d0-51d5-9203-0f6018fc624c", + "name": "L'Alzoni", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-e08b07dd-ffc9-5a6c-b1c0-eb87ca94cf8b", + "name": "La Runcina", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-39de97d1-b84a-5d50-80de-af18f177d6b6", + "name": "La Traversa", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-4a53b1a5-9729-5b0a-a0ea-1b6bee95f6cd", + "name": "Li Mori", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-8cc6ca6e-861b-570c-bfbe-64bab2d71c8b", + "name": "Li Teggi", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-adae7839-7c12-5957-90cd-86d0f66f8d2f", + "name": "Limpostu", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-596c76fa-2bca-553a-8191-a956a78d1826", + "name": "Lotturai", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-f8924a44-c300-5f4c-bef2-66bc36b71aa7", + "name": "Lu Cuponeddi", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-9bed6884-3a4a-52f9-be78-ac3a5e33a5ec", + "name": "Lu Impostu", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-eb332804-ffde-527f-a86f-96754255c1e2", + "name": "Lu Muvruneddu", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-9c3a5b9b-7548-51af-8359-a7cbb0451bb9", + "name": "Lu Ricciu", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-f427d2af-c00f-510a-a6bb-1c70551ccf0d", + "name": "Lu Sitagliacciu", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-96d4eee4-26d4-50c8-bcc6-96037bb6a858", + "name": "Lu Tintimbaru", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-ec2f6828-85ad-5f9a-b0f2-6a09d64a194d", + "name": "Lu Titimbaru", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-e5d74c3e-5209-5e0a-88c9-d1ed82625b98", + "name": "Lutturai", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-ef7e7222-7725-5317-a8e0-7a130d2ebc9f", + "name": "Malamori'", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-c2638eaf-45e5-5a26-b94c-5bfe55a5ca51", + "name": "Muvruneddi", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-f70225ba-072b-5223-a6ad-61c3f674552b", + "name": "Nuragheddu", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-2b2a86d7-8925-5fff-8f33-c6c79c9438dc", + "name": "Pattimedda", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-e148cad9-348b-5a26-a4a7-937aa3622c15", + "name": "Pedra E Cupa", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-636ccbd2-bcfb-529b-aaa7-f569c3214603", + "name": "Pira Maseda", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-bb5530bf-814c-5376-bc00-afc8944b2852", + "name": "Rinaggiu", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-1f7ce59b-0a2e-5545-99e7-44eacdd331a9", + "name": "Schifoni", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-cb059699-126a-5dfa-bc4e-e55789c0b1fb", + "name": "Silimini", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-b283aa75-2f8b-5ab3-9556-e53bcb20ac2a", + "name": "Sitagliacciu", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-04d4d9c8-a3b5-505f-a6cc-3e4acdccf212", + "name": "Stazzu Bruciatu", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-ecfe6292-a448-57c6-abfd-81ec637000e9", + "name": "Stazzu Mesu", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-4442a091-52b4-58a8-92de-e0a1823599c7", + "name": "Strugas", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-c194763c-ed19-5538-addc-db648a2c77bf", + "name": "Su Linalvu", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-739046ad-a03d-59eb-b610-e0e8b9f76817", + "name": "Suaredda", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-5049cc4b-5819-5d7a-b12a-6fae9c242627", + "name": "Terrapadedda", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-603e4655-ab97-582e-9019-1e81b8b0dde4", + "name": "Tiridduli", + "province_code": "SS", + "region_name": "Sardegna" + }, + { + "location_id": "IT-LOC-7559f370-536b-58d5-bc24-2b1f92115895", + "name": "Traversa", + "province_code": "SS", + "region_name": "Sardegna" + } + ], + "longitude": 8.9639, + "municipality_count": 0, + "verification_counts": { + "geonames_estimated": 36 + } + }, + { + "latitude": 46.6521, + "location_count": 33, + "locations": [ + { + "location_id": "IT-LOC-53ba3db6-8153-59ae-8315-43c740957eb0", + "name": "Aschbach", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-3558cbb1-256a-561c-8402-37fb883a03a3", + "name": "Eyrs", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-054bef83-5540-5b59-8b2d-8140a2b25f35", + "name": "Freiberg", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-9dc0f0e7-e374-5e59-a3a7-5572150dcd18", + "name": "Gand/Martell", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-4aef8107-d9bc-5eb5-924f-fd9d57de5295", + "name": "Graun/Vinschg.", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-6eb71e32-6da1-511c-85db-01b00cfee3be", + "name": "Gries", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-28a9e13b-f3bb-5d25-8256-50c769064912", + "name": "Göflan", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-507d4604-8ee9-5337-9469-3ec2e8951a5f", + "name": "Karthaus", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-2aa7b3b9-ae26-58ca-9794-ae4508220d27", + "name": "Kortsch", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-ebc70917-9507-5013-8084-84983ce10c81", + "name": "Laatsch", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-eb649019-9daf-50c0-8de4-59ca892dea1d", + "name": "Lichtenberg", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-24620d1d-35c6-5543-901a-1cbe0dbeacfb", + "name": "Marein", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-2fefacfc-97f4-59f7-ab6c-cb1c0385ff74", + "name": "Marienberg", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-4ab8d058-0221-589f-a27e-5093d5dae01b", + "name": "Matsch", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-4bee925a-215b-51b0-af07-d98968c4fff3", + "name": "Melag", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-f4d9a61d-216c-50ae-8dc9-2f54bf1fa798", + "name": "Oberplars", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-15e3b4a8-d1ac-5991-a12d-a86d008351fb", + "name": "Planeil", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-c9ac1193-eb5c-5f1a-bf13-a0cc026b970c", + "name": "Plars", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-bc734f86-bfd2-55a2-a052-9563f8dd2246", + "name": "Plawenn", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-fe44696d-b8de-569e-8a5a-fc2753f5d7a7", + "name": "Prad", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-cfc2c321-e4e5-5f8f-9c88-f6d90dd34890", + "name": "Schleis", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-5e2c29b0-283b-56ff-97bd-de7e548a4736", + "name": "St. Martin am Kofl", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-b41fe9d7-85ea-5b55-b83a-4c44ef8ed5fb", + "name": "St. Valentin a. d. H.", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-655bd91c-9cd1-5034-af33-46434220c6cf", + "name": "Staben", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-701b3361-801d-5124-89c0-00a5a9ae1589", + "name": "Tabland", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-a993459c-e234-51bb-b661-c9e1120d2673", + "name": "Talatsch", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-3b28f645-7d1c-59e4-83c1-d59cc70eb74e", + "name": "Tarnell", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-04bec956-c712-5efd-bd40-c91e39568684", + "name": "Tartsch", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-9a188535-805c-5e1b-ae3e-95bf886d2d94", + "name": "Taufers i. Münstertal", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-f85252bc-5e73-58c8-b4c2-cc8005243d66", + "name": "Töll", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-f1b3185e-fd7b-5a6e-b254-a3091a112622", + "name": "Unsere Frau i. Schnals", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-3ff00971-e81d-5342-a966-c9484b289f0e", + "name": "Vellau", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-efd26e7a-26e2-5cae-a8b5-ebd338b78379", + "name": "Vernagt", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + } + ], + "longitude": 10.7411, + "municipality_count": 0, + "verification_counts": { + "geonames_estimated": 33 + } + }, + { + "latitude": 46.5041, + "location_count": 31, + "locations": [ + { + "location_id": "IT-LOC-451a36fb-9ed8-549c-bc32-116140f0bf9b", + "name": "Altenburg", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-89833683-cfed-5641-a4ad-3b6e687ca495", + "name": "Asten", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-3105b8d8-27ab-54f0-8a4b-b3dd12aeee12", + "name": "Breien", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-6a76f0eb-2c55-5781-9a16-1ad11c6a9a74", + "name": "Bundschen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-9d69f127-e7e6-5e48-8808-caf40b20a6f9", + "name": "Gummer", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-784dccd8-a6ce-5783-8436-c59896ca22db", + "name": "Innichen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-c6b84079-4906-5ce3-9b2d-d84168deb43b", + "name": "Lengstein", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-f6ed0c3e-27db-5860-aedd-da7585e224fc", + "name": "Missian", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-04cf1b35-022e-5abf-9d33-62524bfa0cfb", + "name": "Mitterdorf", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-c975505d-5c1e-506d-83cd-4e5e8ec0a8b7", + "name": "Montiggl", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-8662bd5d-0188-5f32-bde7-3b6b4eb920c6", + "name": "Oberbozen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-7f84361e-e626-5a48-b3ec-bacfff2f598b", + "name": "Oberinn", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-c92d4380-9c1f-5628-ab74-837edead1f3d", + "name": "Oberplanitzing", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-ad569d67-5288-558b-8541-3dc5297c5790", + "name": "Perdonig", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-f96452c5-54dd-5443-be7a-df95a3499ccd", + "name": "Prösels", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-a1cd5a9b-ab3c-5cc0-86fe-97d2d056c97f", + "name": "Reinswald", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-e3f0b490-e00e-5ef9-8fbc-62a6b4d7a56a", + "name": "Riedelsberg", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-36a9b445-02a1-5952-b235-c82f698271dc", + "name": "Ritten", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-c1f602d2-40d5-5a4e-866d-4dd728e06479", + "name": "Seit", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-47d9d2cc-e35f-57e4-9a56-2bb64335b526", + "name": "St. Anton", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-d5b4229f-da83-59af-a1a7-67ed9db988ff", + "name": "St. Jakob/Bozen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-060e4482-5dd9-5480-86e1-9daf688bd49f", + "name": "St. Josef am See", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-c2fc9a27-b8cc-587f-8ad1-72ffb8a5d7e9", + "name": "St. Justina/Eppan", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-a39051d6-e0cd-5053-82ef-4d2ade9481cb", + "name": "St. Nikolaus i. Eggen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-d562fd11-5b80-52ec-a6d4-2edee5cd4dc1", + "name": "St. Nikolaus/Kaltern", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-37273125-3198-5d5d-bca8-58d810857c40", + "name": "Steinmannwald", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-7919aa0a-01d5-5d84-99c1-c6be40861622", + "name": "Ums", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-1d2afe05-a815-5282-9b41-c85d08dea2d6", + "name": "Unterplanitzing", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-6afc0026-fc9e-5f45-9138-b0f15c3a8d5b", + "name": "Unterrain/Eppan", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-39ea2f85-a91d-5427-ab42-15dbd9bac9f9", + "name": "Wangen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-5c7a4aa9-d1d4-57b5-b6d8-9ae63eb22dc9", + "name": "Wolfsgruben", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + } + ], + "longitude": 11.3975, + "municipality_count": 0, + "verification_counts": { + "geonames_estimated": 31 + } + }, + { + "latitude": 46.5907, + "location_count": 30, + "locations": [ + { + "location_id": "IT-LOC-67d9692a-d275-5550-925d-6d7afd249fc0", + "name": "Alpe Di Siusi", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-5970d3ff-e36e-51ea-b81b-4e0e3ffdb4c6", + "name": "Atzwang", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-74c1b395-4b6b-5c2c-bd60-9b924e20f280", + "name": "Elzenbaum", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-4743ec4a-a224-5f29-a153-ae5f6403361d", + "name": "Fontanefredde", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-1cd6dd7c-61ea-5d8b-ab6b-45cf8abe478e", + "name": "Gastei", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-065f5ac1-16cb-5ea0-9d53-ecd885a95d7e", + "name": "Gfrill/Unterland", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-acf3b230-cbc1-5ec1-9fc0-b1c87f24861a", + "name": "Hole", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-f9b14e4a-6a45-5de6-b281-d96ba89e2f2c", + "name": "Jaufental", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-6ef175ae-cedf-5cbb-9cbc-84a03fa2b5f8", + "name": "Kaltenbrunn", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-f46d82d1-8e82-54d6-96fe-318074518435", + "name": "Loewenviertel Elisabethsiedlun", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-38157764-41db-55bc-a83c-c3b42cc4351e", + "name": "Montagna", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-27a89a6f-7a84-5133-b35f-9b561c3a2d3c", + "name": "Natz", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-685b40d1-4d4e-5bce-a348-c2bc3db7deb4", + "name": "Naz", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-5468c217-d5d2-50c7-a556-2fa8db28b897", + "name": "Pruno Di Stilves", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-fce08b84-974c-5a07-96c0-ebc653605c86", + "name": "Raa", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-10936fd9-35ec-5f6b-8179-ddccc9586055", + "name": "Radein", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-c07730e5-0f01-520b-9b4a-99a3d3d7ff9b", + "name": "Redagno", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-1f3ee3de-4aff-57a8-bf5e-d9926051e9c6", + "name": "San Pietro Laion", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-5fbf0866-d270-5ced-9a65-bfb4561bdb04", + "name": "Seis Am Schler", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-b832318f-cf0b-55eb-a2a9-1323f8a474f2", + "name": "Seiser Al", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-9cc4402d-e31f-533b-935b-8a0d30380c92", + "name": "St.Lugan", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-cd801b70-4159-5d02-86bb-00ffd72ae9f9", + "name": "St.Pete", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-245ca055-f7c5-5d25-9e89-e2aa73de9709", + "name": "St.Peter Laje", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-38e1fdc9-7fc2-50aa-85b8-015040e81a48", + "name": "Stange", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-1a6df626-e07d-5557-a07a-b588b0a539c4", + "name": "Stilfes", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-8088c135-a431-53f8-ba31-33174cacd4c0", + "name": "Valgiovo", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-COM-021033", + "name": "Funes", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-COM-021045", + "name": "Magrè sulla strada del vino", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-COM-021057", + "name": "Naz-Sciaves", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-COM-021098", + "name": "Termeno sulla strada del vino", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + } + ], + "longitude": 11.4295, + "municipality_count": 4, + "verification_counts": { + "geonames_estimated": 26, + "geonames_place_match": 4 + } + }, + { + "latitude": 46.6461, + "location_count": 29, + "locations": [ + { + "location_id": "IT-LOC-40e9bef6-fa13-5c4d-8a22-9473bdd92455", + "name": "Gfeis", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-f2185189-2a81-5fe7-aba4-461cb8a6b1d7", + "name": "Gfrill", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-1cb01338-2145-5e14-aa56-6a99fa1927b4", + "name": "Gratsch", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-71e97105-2fc4-5f78-830c-d8e9ea16efc9", + "name": "Grissian", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-471112fc-93f3-5b57-b0eb-80569d087f3e", + "name": "Kuens", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-57166201-097c-5ea9-af58-d7b012212197", + "name": "Kuppelwies", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-96df6725-70a2-530b-98e5-402e75640ade", + "name": "Obermais", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-d4f26422-8deb-5a0f-9dea-dbad2b67579a", + "name": "Pawigl", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-b084c550-d30f-5f10-9fd2-79a4b03423c0", + "name": "Pfelders", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-f508c27a-4655-5746-b2ff-fbbac4f05ff8", + "name": "Platzers", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-6bedadc0-77b9-5513-bb51-52fb6a1b2034", + "name": "Rabenstein", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-daf1a3d1-dfbc-5e3a-9eb3-f7bcbabba3a3", + "name": "Riffian", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-5c3df4e0-3c86-5c80-9c27-9f45e457c49a", + "name": "Schlaneid", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-6bfa4e3b-5377-5806-83cf-5f945c7f333e", + "name": "Sigmundskron", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-74ba7b59-6e39-521c-8251-31c847eacff0", + "name": "Sinich", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-5db83a3a-276c-55a9-a068-22b6baf88122", + "name": "St. Felix", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-baf76025-0120-5927-9d38-882729ecbd51", + "name": "St. Gertraud", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-1a2d39b4-5728-5701-ad2e-fbdc5003691b", + "name": "St. Helena", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-f8664849-7a56-5836-8d7b-d47542ed4ca7", + "name": "St. Leonhard in Pass.", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-2bc10023-3223-534f-84bb-54e32593364e", + "name": "St. Moritz/Ulten", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-77ecbffd-a35d-5469-a66d-545082122444", + "name": "St. Nikolaus/Ulten", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-18e0c4ec-bed1-5bbb-b049-18db851900bd", + "name": "Tall", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-a07e67e6-2112-5329-8993-2c6493563b77", + "name": "Tirol", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-7accc7a9-974e-5a93-88e3-e5efca81bf28", + "name": "Unsere liebe Frau i. W.", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-954136d4-07b0-5bc3-8110-6de326690c6e", + "name": "Vernue", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-90df8d27-e3a6-5f0e-b605-85cf4c578245", + "name": "Verschneid", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-5f39b696-fa77-5e9c-8e4b-49b9dc6a1ece", + "name": "Videgg", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-b1edc7f0-13aa-5d57-9601-cda35bbe245b", + "name": "Vilpian", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-e5b0e762-26ee-5643-8843-61ce385b58bd", + "name": "Völlan", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + } + ], + "longitude": 11.17, + "municipality_count": 0, + "verification_counts": { + "geonames_estimated": 29 + } + }, + { + "latitude": 46.7953, + "location_count": 27, + "locations": [ + { + "location_id": "IT-LOC-79ef0e21-8935-5be5-8d4e-e29fd062b99d", + "name": "Campil", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-2d9571ab-cbcd-5a0f-9a66-1217123acf9b", + "name": "Longiaru'", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-404e75c1-e19f-52c2-8d41-9ddba5e746f3", + "name": "Mitterolang", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-f4f0e435-14af-5b8d-9ed3-adb00ad43625", + "name": "Moo", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-27e863c4-1200-5d85-b149-4d7a746b0f2a", + "name": "Niederolang", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-74f55924-f7cc-5153-a356-620ef075cce7", + "name": "Niederrasen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-8f75e896-460d-516d-9820-94db8b491f22", + "name": "Oberrasen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-c087b2f2-5d9c-586a-8de3-959b90ddfb5e", + "name": "Obervintl", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-fbb79a94-6c65-5844-bcda-98ed7e572ce1", + "name": "Pedero", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-d9aee659-6bc9-504b-b9b4-3b75540da638", + "name": "Pikolein", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-16c993a5-af62-5985-a382-fa9495c03076", + "name": "San Giacomo In Valle Aurina", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-74c1786c-68dd-5dcb-a20a-f7792f253fbd", + "name": "San Giovanni In Valle Aurina", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-8c9093eb-6621-5c6b-aae0-e691f74f8ec6", + "name": "St. Kassia", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-baa6d904-f961-5141-a9bc-b2b048a888dd", + "name": "St.Jakob In Ahrnta", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-fbe7be08-c229-5380-932e-dac58f2243bf", + "name": "St.Johann In Ahrnta", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-ef8703cc-8dd1-5601-88bc-240d2ae1d120", + "name": "St.Magdalen", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-0c17e5fc-2e91-5e5a-80ad-5f73032b6bc4", + "name": "St.Martin Gsie", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-5782e684-c342-544f-86fc-07dea067edd8", + "name": "St.Sigmun", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-b4c76f56-8ca3-5305-82a1-fd9478c45934", + "name": "St.Vigi", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-7f839cf0-74ad-591b-a474-da2540723616", + "name": "Steinhaus", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-323d1aea-d28d-53a9-b45b-2cc8e471432e", + "name": "Weitental", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-LOC-3c435dce-e080-5aa6-8249-fe5199348d2a", + "name": "Zwischenwasser", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-COM-021117", + "name": "La Valle", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-COM-021071", + "name": "Rasun-Anterselva", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-COM-021108", + "name": "Valle Aurina", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-COM-021109", + "name": "Valle di Casies", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + }, + { + "location_id": "IT-COM-021110", + "name": "Vandoies", + "province_code": "BZ", + "region_name": "Trentino-Alto Adige/Südtirol" + } + ], + "longitude": 11.9114, + "municipality_count": 5, + "verification_counts": { + "geonames_estimated": 22, + "geonames_place_match": 5 + } + }, + { + "latitude": 38.1321, + "location_count": 21, + "locations": [ + { + "location_id": "IT-LOC-02d6408c-4e92-562a-9591-45bc018a8de6", + "name": "Arenella Vergine Maria", + "province_code": "PA", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-89c31a7e-4fd9-515d-b8c1-ebdb7c1cdf92", + "name": "Brancaccio Ciaculli", + "province_code": "PA", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-3c290cb8-e678-5f29-950a-39107c6e974d", + "name": "Cruillas", + "province_code": "PA", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-81aa78f6-9ebd-577f-869b-2d4fc6143679", + "name": "In Via Aloi", + "province_code": "PA", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-6ee6ccd3-04e6-55ae-940e-4261eac643c1", + "name": "In Via Brancaccio", + "province_code": "PA", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-cf9de296-b9ff-5441-a3e7-350996772280", + "name": "In Via Chiavelli", + "province_code": "PA", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-e068dc69-ad7d-5931-8f7d-0633e82fb7d4", + "name": "In Via Gioiamia", + "province_code": "PA", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-cfdffe19-2a55-5192-bc87-3b42ae7d6bfd", + "name": "In Via San Nicola", + "province_code": "PA", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-4c87cdb7-dbf2-54cf-9828-da48e051d285", + "name": "In Via Santa Maria Di Gesu'", + "province_code": "PA", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-6e7a950c-8870-5dce-981c-d69add0826ce", + "name": "Mezzomonreale Boccadifalco", + "province_code": "PA", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-9f550d7f-42d6-59fa-bf4b-dd88746b6723", + "name": "Montepellegrino", + "province_code": "PA", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-e95d2f79-fda4-5005-bca3-6815ac7ecbe5", + "name": "Pallavicino", + "province_code": "PA", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-aa412eff-27bc-5e18-8e6e-5dd62d5b8306", + "name": "Resuttana San Lorenzo", + "province_code": "PA", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-b8ddcd5b-538d-58f5-96ed-ccd7dffa3253", + "name": "Rocca Monreale", + "province_code": "PA", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-4f819f6c-58ca-51ff-b472-5c3057d021ba", + "name": "Roccella Guarnaschelli", + "province_code": "PA", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-24af8890-5126-5321-a2ee-cca913a92cef", + "name": "San Lorenzo Colli", + "province_code": "PA", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-0af88414-c2f9-5f46-a76f-6c3f5300a5ff", + "name": "Settecannoli", + "province_code": "PA", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-48c597d4-08c3-532e-a1c2-91414d6dfcee", + "name": "Tommaso Natale Sferracavallo", + "province_code": "PA", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-f17d05e8-4ff8-579c-b715-1cdf48a89909", + "name": "Tribunali Castellammare", + "province_code": "PA", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-e95321ed-e10e-532a-972a-939343930ac5", + "name": "Zisa", + "province_code": "PA", + "region_name": "Sicilia" + }, + { + "location_id": "IT-COM-082053", + "name": "Palermo", + "province_code": "PA", + "region_name": "Sicilia" + } + ], + "longitude": 13.3356, + "municipality_count": 1, + "verification_counts": { + "geonames_estimated": 20, + "geonames_place_match": 1 + } + }, + { + "latitude": 41.8919, + "location_count": 15, + "locations": [ + { + "location_id": "IT-LOC-e3a2f940-5b67-581f-bed9-d3b766e6d6ac", + "name": "Acilia", + "province_code": "RM", + "region_name": "Lazio" + }, + { + "location_id": "IT-LOC-a3c6a34c-2eae-5118-979a-0751e64d77bc", + "name": "Borgata Borghesiana", + "province_code": "RM", + "region_name": "Lazio" + }, + { + "location_id": "IT-LOC-4755aaef-874c-5240-8e7a-73ebcdae2111", + "name": "Borgata Casalotti", + "province_code": "RM", + "region_name": "Lazio" + }, + { + "location_id": "IT-LOC-8f41744c-8bfb-5a9d-a99f-d6beba24a30d", + "name": "Borgata Corviale", + "province_code": "RM", + "region_name": "Lazio" + }, + { + "location_id": "IT-LOC-e2892a18-ebd0-58f4-a741-5dc1bbfbb5a6", + "name": "Borgata Finocchio", + "province_code": "RM", + "region_name": "Lazio" + }, + { + "location_id": "IT-LOC-4798c5b7-e731-5a21-8a0f-e50da85d5384", + "name": "Colle Della Valentina", + "province_code": "RM", + "region_name": "Lazio" + }, + { + "location_id": "IT-LOC-95716bc3-c72f-5f14-a4a0-a201709fc40c", + "name": "Decima", + "province_code": "RM", + "region_name": "Lazio" + }, + { + "location_id": "IT-LOC-b13d585f-2701-5229-9a96-3bd4e32b8150", + "name": "Grottarossa", + "province_code": "RM", + "region_name": "Lazio" + }, + { + "location_id": "IT-LOC-8cf35dfd-a0ab-554d-9bd7-9566c4845a55", + "name": "Magliana Trullo", + "province_code": "RM", + "region_name": "Lazio" + }, + { + "location_id": "IT-LOC-c6bd15e9-e287-5162-b0bc-b950b5ed2cda", + "name": "Ostia Antica", + "province_code": "RM", + "region_name": "Lazio" + }, + { + "location_id": "IT-LOC-903b4f47-cf4e-5bc7-b837-038f1a4986bf", + "name": "Rebibbia", + "province_code": "RM", + "region_name": "Lazio" + }, + { + "location_id": "IT-LOC-78da21ca-d35a-54da-b927-c0215af10c56", + "name": "Tomba Di Nerone", + "province_code": "RM", + "region_name": "Lazio" + }, + { + "location_id": "IT-LOC-f10f7c96-b59b-58ea-8172-622fabcc24b3", + "name": "Torre Maura", + "province_code": "RM", + "region_name": "Lazio" + }, + { + "location_id": "IT-LOC-76ba66b5-2e46-5e08-b6d3-3bbfe70b4a22", + "name": "Torre Spaccata", + "province_code": "RM", + "region_name": "Lazio" + }, + { + "location_id": "IT-COM-058091", + "name": "Roma", + "province_code": "RM", + "region_name": "Lazio" + } + ], + "longitude": 12.5113, + "municipality_count": 1, + "verification_counts": { + "geonames_estimated": 14, + "geonames_place_match": 1 + } + }, + { + "latitude": 40.8438, + "location_count": 14, + "locations": [ + { + "location_id": "IT-LOC-0b4a5ee3-81f5-525c-9834-a980abab8e06", + "name": "Buonopane", + "province_code": "NA", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-590c6270-2e1d-510b-ad2c-ff0cfa3d554f", + "name": "Capo Miseno", + "province_code": "NA", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-121a40ce-8b95-58a3-97e4-e9b8deabb42a", + "name": "Cappuccini", + "province_code": "NA", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-62c60a7f-e0b5-51e4-b042-bc836c612bc5", + "name": "Carditello", + "province_code": "NA", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-148a1f6f-0cc3-5496-957a-4fcfa52dbe8e", + "name": "Fontana", + "province_code": "NA", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-abc73a86-c704-5a13-829f-4c47f44fc16c", + "name": "Fusaro", + "province_code": "NA", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-9bb5aa91-5327-57d0-8e03-b4600a2b0ffd", + "name": "Miliscola", + "province_code": "NA", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-75785d84-095b-52f2-85cb-f7e64557dd4c", + "name": "Polvica", + "province_code": "NA", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-db808f7b-e631-5b7a-b6c7-736e63278881", + "name": "San Rocco", + "province_code": "NA", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-f497f773-19fd-5fa5-b677-05761bcba9ea", + "name": "Serrara", + "province_code": "NA", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-68c1fa3d-8464-5dad-ae90-cba6c99f2b51", + "name": "Succhivo", + "province_code": "NA", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-416e2633-d4ab-5e05-918b-9a7a35cf19fb", + "name": "Tavernanova", + "province_code": "NA", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-e37caa21-3a2d-52f4-8908-d71b41350c4c", + "name": "Testaccio D'Ischia", + "province_code": "NA", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-7acb98ab-5c2b-5a38-978d-d0cecb7eeb3a", + "name": "Torregaveta", + "province_code": "NA", + "region_name": "Campania" + } + ], + "longitude": 14.3407, + "municipality_count": 0, + "verification_counts": { + "geonames_estimated": 14 + } + }, + { + "latitude": 42.829, + "location_count": 12, + "locations": [ + { + "location_id": "IT-LOC-1e78d0bd-2a30-58a2-b37c-1a04c6553bac", + "name": "Caldarette", + "province_code": "FM", + "region_name": "Marche" + }, + { + "location_id": "IT-LOC-aa673bd4-4524-5bb7-8ae2-1dd54d7c876c", + "name": "Campiglione", + "province_code": "FM", + "region_name": "Marche" + }, + { + "location_id": "IT-LOC-60cdf62a-10d0-5dad-8710-b0a30ca52821", + "name": "Capodarco", + "province_code": "FM", + "region_name": "Marche" + }, + { + "location_id": "IT-LOC-f217f0f8-9ccd-5f67-8337-b6d566e17f55", + "name": "Lido Di Fermo", + "province_code": "FM", + "region_name": "Marche" + }, + { + "location_id": "IT-LOC-ef143008-07fc-5710-98ce-76e6359922a6", + "name": "Marina Palmense", + "province_code": "FM", + "region_name": "Marche" + }, + { + "location_id": "IT-LOC-4022024f-9172-58d6-8c64-8187e8e83cfd", + "name": "Molini Di Tenna", + "province_code": "FM", + "region_name": "Marche" + }, + { + "location_id": "IT-LOC-c6fd8b8d-4128-51f8-9564-63babc391176", + "name": "Ponte Ete", + "province_code": "FM", + "region_name": "Marche" + }, + { + "location_id": "IT-LOC-b6dffc77-c840-5c7f-b8b0-81dbd95c497f", + "name": "Salvano", + "province_code": "FM", + "region_name": "Marche" + }, + { + "location_id": "IT-LOC-9d8c340c-1b4d-52e0-9b10-564ce429d154", + "name": "San Marco", + "province_code": "FM", + "region_name": "Marche" + }, + { + "location_id": "IT-LOC-cda5ed3a-0b45-5a68-9533-0164020f4bf9", + "name": "San Tommaso Tre Archi", + "province_code": "FM", + "region_name": "Marche" + }, + { + "location_id": "IT-LOC-516dfbf0-7ee0-5c71-99fb-915b847b2d67", + "name": "Torre Di Palme", + "province_code": "FM", + "region_name": "Marche" + }, + { + "location_id": "IT-COM-109006", + "name": "Fermo", + "province_code": "FM", + "region_name": "Marche" + } + ], + "longitude": 13.58, + "municipality_count": 1, + "verification_counts": { + "geonames_estimated": 11, + "geonames_place_match": 1 + } + }, + { + "latitude": 38.5192, + "location_count": 10, + "locations": [ + { + "location_id": "IT-LOC-0db05392-927d-5d6b-ada5-7a9812a8cf3e", + "name": "Alicudi", + "province_code": "ME", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-f1745be4-9f93-5956-b024-1814009e1217", + "name": "Alicudi Porto", + "province_code": "ME", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-89bcc9fb-fdcc-579c-8d46-493f1fba3527", + "name": "Castroreale Terme", + "province_code": "ME", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-aa49630f-2d77-54da-8246-c0a482d88938", + "name": "Filicudi", + "province_code": "ME", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-ab80542f-7f9b-5cc0-81ad-5b663b5b0a73", + "name": "Filicudi Porto", + "province_code": "ME", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-8fb75657-daef-5cc7-bb0c-9893a897425e", + "name": "Rubino", + "province_code": "ME", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-408cb306-f33b-5531-bf42-7b8087fd8af6", + "name": "Vigliatore", + "province_code": "ME", + "region_name": "Sicilia" + }, + { + "location_id": "IT-LOC-cd522506-7d9b-51a0-bbc9-88be29516b9f", + "name": "Vulcano", + "province_code": "ME", + "region_name": "Sicilia" + }, + { + "location_id": "IT-COM-083023", + "name": "Fondachelli-Fantina", + "province_code": "ME", + "region_name": "Sicilia" + }, + { + "location_id": "IT-COM-083106", + "name": "Terme Vigliatore", + "province_code": "ME", + "region_name": "Sicilia" + } + ], + "longitude": 14.9976, + "municipality_count": 2, + "verification_counts": { + "geonames_estimated": 8, + "geonames_place_match": 2 + } + }, + { + "latitude": 41.1575, + "location_count": 10, + "locations": [ + { + "location_id": "IT-LOC-5c91d91a-de73-5fd1-b30a-12516b9b2bdb", + "name": "Briano", + "province_code": "CE", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-ac9303ca-0fca-5617-9e21-cf9eac5c6277", + "name": "Casolla", + "province_code": "CE", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-b008bff9-1be9-56ba-925a-548323a14421", + "name": "Centurano", + "province_code": "CE", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-5d0a0df6-9230-55b7-bb9e-636034c3cbab", + "name": "Ercole", + "province_code": "CE", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-2b0ca9ed-24ff-569a-a240-f5d1353ad1bd", + "name": "Falciano", + "province_code": "CE", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-b95bba8c-d860-5e69-b3c7-c5bbe595bd2f", + "name": "Mezzano", + "province_code": "CE", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-b8a8bfc0-6649-5a51-b3aa-accc9dfe4a37", + "name": "Puccianiello", + "province_code": "CE", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-dfc6b905-e7ed-51ca-9d55-355dd7657cf9", + "name": "Staturano", + "province_code": "CE", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-5bc9b11d-c423-5c17-8189-781bb802216b", + "name": "Tredici", + "province_code": "CE", + "region_name": "Campania" + }, + { + "location_id": "IT-LOC-6dc085b8-9e3b-5fad-ada5-698bd053cdf4", + "name": "Vaccheria", + "province_code": "CE", + "region_name": "Campania" + } + ], + "longitude": 14.2436, + "municipality_count": 0, + "verification_counts": { + "geonames_estimated": 10 + } + }, + { + "latitude": 45.7235, + "location_count": 10, + "locations": [ + { + "location_id": "IT-LOC-db29eb8c-f608-5cea-8e0b-623e01b1946f", + "name": "Grand Villa Cravon", + "province_code": "AO", + "region_name": "Valle d'Aosta/Vallée d'Aoste" + }, + { + "location_id": "IT-LOC-8a2feb22-9ce9-5a2e-810b-6b15c9f2a46b", + "name": "Peroulaz", + "province_code": "AO", + "region_name": "Valle d'Aosta/Vallée d'Aoste" + }, + { + "location_id": "IT-LOC-f03ccbb2-3b67-50d9-9b70-4f0bdf46848f", + "name": "Saint Barthelemy", + "province_code": "AO", + "region_name": "Valle d'Aosta/Vallée d'Aoste" + }, + { + "location_id": "IT-LOC-f3813cd2-af4c-5ebd-b44c-8ea102b0a591", + "name": "Ville", + "province_code": "AO", + "region_name": "Valle d'Aosta/Vallée d'Aoste" + }, + { + "location_id": "IT-COM-007002", + "name": "Antey-Saint-André", + "province_code": "AO", + "region_name": "Valle d'Aosta/Vallée d'Aoste" + }, + { + "location_id": "IT-COM-007013", + "name": "Challand-Saint-Anselme", + "province_code": "AO", + "region_name": "Valle d'Aosta/Vallée d'Aoste" + }, + { + "location_id": "IT-COM-007014", + "name": "Challand-Saint-Victor", + "province_code": "AO", + "region_name": "Valle d'Aosta/Vallée d'Aoste" + }, + { + "location_id": "IT-COM-007032", + "name": "Gressoney-La-Trinité", + "province_code": "AO", + "region_name": "Valle d'Aosta/Vallée d'Aoste" + }, + { + "location_id": "IT-COM-007050", + "name": "Pontboset", + "province_code": "AO", + "region_name": "Valle d'Aosta/Vallée d'Aoste" + }, + { + "location_id": "IT-COM-007058", + "name": "Saint-Christophe", + "province_code": "AO", + "region_name": "Valle d'Aosta/Vallée d'Aoste" + } + ], + "longitude": 7.577, + "municipality_count": 6, + "verification_counts": { + "geonames_estimated": 4, + "geonames_place_match": 6 + } + } + ], + "review_threshold": 10 + }, "coordinate_verification_counts": { "geonames_estimated": 10320, "geonames_place_match": 8095, @@ -34,6 +2941,11 @@ "status": "passed", "violations": 0 }, + "coordinate_distribution": { + "blocking": false, + "status": "review_required", + "violations": 16 + }, "coordinate_verification": { "status": "passed", "violations": 0 @@ -101,6 +3013,7 @@ "warnings": [ "GeoNames is not Poste Italiane.", "CAP and coordinates are experimental, non-official and without warranty.", - "GeoNames coordinates may be algorithmically estimated." + "GeoNames coordinates may be algorithmically estimated.", + "16 exact shared-coordinate groups contain at least 10 unique locations; review coordinate_distribution before geographic use." ] } diff --git a/requirements-dev.txt b/requirements-dev.txt index 343904b..d980bab 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,5 @@ coverage==7.15.2 +jsonschema==4.26.0 mypy==2.3.0 +pip-audit==2.10.1 ruff==0.16.0 diff --git a/schemas/italian_locations-v4.schema.json b/schemas/italian_locations-v4.schema.json index 7f18a21..659491c 100644 --- a/schemas/italian_locations-v4.schema.json +++ b/schemas/italian_locations-v4.schema.json @@ -1,74 +1,75 @@ { "$id": "https://codewriter90x.github.io/Italian_Cities/schemas/italian_locations-v4.schema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Italian Cities typed JSON preview", + "type": "object", "additionalProperties": false, + "required": ["metadata", "fields", "rows"], "properties": { - "fields": { - "items": { - "type": "string" - }, - "type": "array" - }, "metadata": { - "additionalProperties": true, + "type": "object", + "additionalProperties": false, + "required": [ + "source_dataset_version", + "schema_version", + "contract_status", + "null_policy" + ], "properties": { - "contract_status": { - "const": "next_major_preview" + "source_dataset_version": { + "type": "string", + "pattern": "^v[0-9]+\\.[0-9]+\\.[0-9]+$" }, - "schema_version": { - "const": "4.0.0" + "schema_version": { "const": "4.0.0" }, + "contract_status": { "const": "next_major_preview" }, + "null_policy": { + "const": "JSON null for missing numeric values" } - }, - "required": [ - "schema_version", - "contract_status" + } + }, + "fields": { + "type": "array", + "minItems": 25, + "maxItems": 25, + "prefixItems": [ + { "const": "location_postal_id" }, + { "const": "location_id" }, + { "const": "name" }, + { "const": "normalized_name" }, + { "const": "location_kind" }, + { "const": "municipality_istat_code" }, + { "const": "parent_municipality_id" }, + { "const": "candidate_municipality_ids" }, + { "const": "postal_code" }, + { "const": "postal_code_status" }, + { "const": "province_code" }, + { "const": "province_name" }, + { "const": "region_name" }, + { "const": "country_code" }, + { "const": "country_name" }, + { "const": "latitude" }, + { "const": "longitude" }, + { "const": "coordinate_verification" }, + { "const": "coordinate_accuracy" }, + { "const": "reconciliation_outcome" }, + { "const": "reconciliation_method" }, + { "const": "reconciliation_confidence" }, + { "const": "source_ids" }, + { "const": "source_record_ids" }, + { "const": "source_reference_dates" } ], - "type": "object" + "items": false }, "rows": { - "items": { - "$ref": "#/$defs/location" - }, - "type": "array" + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/location" } } }, - "required": [ - "metadata", - "fields", - "rows" - ], - "title": "Italian Cities typed JSON preview", - "type": "object", "$defs": { "location": { + "type": "object", "additionalProperties": false, - "properties": { - "candidate_municipality_ids": { "type": "string" }, - "coordinate_accuracy": { "type": ["integer", "null"] }, - "coordinate_verification": { "type": "string" }, - "country_code": { "type": "string" }, - "country_name": { "type": "string" }, - "latitude": { "type": ["number", "null"] }, - "location_id": { "type": "string" }, - "location_kind": { "type": "string" }, - "location_postal_id": { "type": "string" }, - "longitude": { "type": ["number", "null"] }, - "municipality_istat_code": { "type": "string" }, - "name": { "type": "string" }, - "normalized_name": { "type": "string" }, - "parent_municipality_id": { "type": "string" }, - "postal_code": { "type": "string" }, - "postal_code_status": { "type": "string" }, - "province_code": { "type": "string" }, - "province_name": { "type": "string" }, - "reconciliation_confidence": { "type": "string" }, - "reconciliation_method": { "type": "string" }, - "reconciliation_outcome": { "type": "string" }, - "region_name": { "type": "string" }, - "source_ids": { "type": "string" }, - "source_record_ids": { "type": "string" }, - "source_reference_dates": { "type": "string" } - }, "required": [ "location_postal_id", "location_id", @@ -96,7 +97,124 @@ "source_record_ids", "source_reference_dates" ], - "type": "object" + "properties": { + "location_postal_id": { "type": "string", "minLength": 1 }, + "location_id": { "type": "string", "minLength": 1 }, + "name": { "type": "string", "minLength": 1 }, + "normalized_name": { "type": "string", "minLength": 1 }, + "location_kind": { + "enum": [ + "municipality", + "geonames_unreconciled", + "geonames_ambiguous" + ] + }, + "municipality_istat_code": { + "type": "string", + "pattern": "^$|^[0-9]{6}$" + }, + "parent_municipality_id": { "type": "string" }, + "candidate_municipality_ids": { "type": "string" }, + "postal_code": { + "type": "string", + "pattern": "^$|^[0-9]{5}$" + }, + "postal_code_status": { + "enum": ["geonames_matched", "geonames_ambiguous", "missing"] + }, + "province_code": { + "type": "string", + "pattern": "^[A-Z]{2}$" + }, + "province_name": { "type": "string", "minLength": 1 }, + "region_name": { "type": "string", "minLength": 1 }, + "country_code": { "const": "IT" }, + "country_name": { "const": "Italia" }, + "latitude": { + "type": ["number", "null"], + "minimum": 35, + "maximum": 48 + }, + "longitude": { + "type": ["number", "null"], + "minimum": 6, + "maximum": 19 + }, + "coordinate_verification": { + "enum": [ + "geonames_estimated", + "geonames_place_match", + "missing" + ] + }, + "coordinate_accuracy": { + "type": ["integer", "null"], + "minimum": 1, + "maximum": 6 + }, + "reconciliation_outcome": { + "enum": [ + "exact_unambiguous", + "multiple_candidates", + "unmatched_no_parent", + "istat_without_postal_code" + ] + }, + "reconciliation_method": { "type": "string", "minLength": 1 }, + "reconciliation_confidence": { + "enum": ["high", "ambiguous", "unmatched", "not_applicable"] + }, + "source_ids": { "type": "string", "minLength": 1 }, + "source_record_ids": { "type": "string", "minLength": 1 }, + "source_reference_dates": { "type": "string", "minLength": 1 } + }, + "allOf": [ + { + "if": { + "properties": { + "coordinate_verification": { "const": "missing" } + }, + "required": ["coordinate_verification"] + }, + "then": { + "properties": { + "latitude": { "const": null }, + "longitude": { "const": null }, + "coordinate_accuracy": { "const": null } + } + }, + "else": { + "properties": { + "latitude": { "type": "number" }, + "longitude": { "type": "number" }, + "coordinate_accuracy": { + "type": "integer", + "minimum": 1, + "maximum": 6 + } + } + } + }, + { + "if": { + "properties": { + "postal_code_status": { "const": "missing" } + }, + "required": ["postal_code_status"] + }, + "then": { + "properties": { "postal_code": { "const": "" } } + }, + "else": { + "properties": { + "postal_code": { + "type": "string", + "pattern": "^[0-9]{5}$" + } + } + } + } + ] } } } diff --git a/scripts/build_geography.py b/scripts/build_geography.py index 84e0674..0a4099d 100644 --- a/scripts/build_geography.py +++ b/scripts/build_geography.py @@ -8,7 +8,7 @@ import json import tempfile import zipfile -from pathlib import Path +from pathlib import Path, PurePosixPath from typing import Any ROOT = Path(__file__).resolve().parents[1] @@ -33,6 +33,15 @@ def sha256(path: Path) -> str: return digest.hexdigest() +def display_path(path: Path) -> str: + resolved = path.resolve() + return ( + str(resolved.relative_to(ROOT)) + if resolved.is_relative_to(ROOT) + else str(resolved) + ) + + def rounded(value: Any) -> Any: if isinstance(value, float): return round(value, 5) @@ -50,7 +59,12 @@ def extract_region_layer(source: Path, destination: Path) -> Path: if missing: raise ValueError(f"ISTAT archive is missing: {sorted(missing)}") for member in sorted(required): - archive.extract(member, destination) + member_path = PurePosixPath(member) + if member_path.is_absolute() or ".." in member_path.parts: + raise ValueError(f"unsafe archive member: {member}") + target = destination.joinpath(*member_path.parts) + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(archive.read(member)) return destination / SHAPEFILE_ROOT @@ -122,7 +136,7 @@ def build_geography(source: Path, output: Path) -> dict[str, Any]: encoding="utf-8", ) return { - "output": str(output.relative_to(ROOT)), + "output": display_path(output), "features": len(features), "sha256": sha256(output), "source_sha256": actual_sha256, @@ -179,9 +193,13 @@ def check_geography( if require_rebuild: raise rebuild_status = "skipped_geography_dependencies_not_installed" + elif require_rebuild: + raise FileNotFoundError( + f"{source} is required to prove a clean geographic rebuild" + ) return { "status": "passed", - "output": str(output.resolve().relative_to(ROOT)), + "output": display_path(output), "features": len(features), "sha256": sha256(output), "source_rebuild": rebuild_status, diff --git a/scripts/build_pages.py b/scripts/build_pages.py index e943a2f..f1ce468 100644 --- a/scripts/build_pages.py +++ b/scripts/build_pages.py @@ -44,6 +44,7 @@ f"{REPOSITORY_URL}/releases/download/{RELEASE_VERSION}" ) + def sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as stream: @@ -56,6 +57,13 @@ def release_asset_url(name: str) -> str: return f"{RELEASE_DOWNLOAD_ROOT}/{name}" +def municipality_path(row: dict[str, Any]) -> str: + return ( + f"comuni/{row['municipality_istat_code']}-" + f"{slugify(row['name'])}/" + ) + + def structured_data(stats: dict[str, Any]) -> str: graph = { "@context": "https://schema.org", @@ -225,19 +233,22 @@ def page_document( extra_structured_data: dict[str, Any] | None = None, ) -> str: canonical = f"{SITE_ROOT}{canonical_path}" - graph: list[dict[str, Any]] = [ - { - "@type": page_type, - "@id": f"{canonical}#page", - "url": canonical, - "name": title, - "description": description, - "dateModified": BUILD_DATE, - "inLanguage": "it-IT", - "isPartOf": {"@id": f"{SITE_ROOT}#website"}, - } - ] + page_entry: dict[str, Any] = { + "@type": page_type, + "@id": f"{canonical}#page", + "url": canonical, + "name": title, + "description": description, + "dateModified": BUILD_DATE, + "inLanguage": "it-IT", + "isPartOf": {"@id": f"{SITE_ROOT}#website"}, + } + graph: list[dict[str, Any]] = [page_entry] if extra_structured_data: + if page_type == "ProfilePage" and extra_structured_data.get("@id"): + page_entry["mainEntity"] = { + "@id": extra_structured_data["@id"] + } graph.append(extra_structured_data) json_ld = json.dumps( {"@context": "https://schema.org", "@graph": graph}, @@ -395,7 +406,7 @@ def build_information_pages( postal_codeCAP GeoNames a cinque cifre oppure vuoto con stato missing. province_codeSigla della provincia. latitude, longitudeCoordinate WGS84 opzionali. - coordinate_verificationOrigine e livello di associazione della coordinata. + coordinate_verificationOrigine e tipo di riconciliazione; non certifica l'accuratezza geografica. source_idsFonti canoniche che contribuiscono al record. @@ -541,16 +552,16 @@ def build_information_pages( for (province_name, province_code), items in sorted(province_groups.items()): links = [] for municipality in items: - query = quote_plus(municipality["name"]) links.append( - f'
  • ' + f'
  • ' f"{html.escape(municipality['name'])}" f" ISTAT {html.escape(municipality['municipality_istat_code'])}
  • " ) province_sections.append( f"

    " - f"{html.escape(province_name)} ({html.escape(province_code)})" + f'' + f"{html.escape(province_name)} " + f"({html.escape(province_code)})" f"

    " ) region_body = f""" @@ -572,7 +583,7 @@ def build_information_pages( I CAP e le coordinate provengono da GeoNames e possono essere incompleti o non aggiornati.

    Elenco dei comuni

    -

    Seleziona un comune per aprirlo nella ricerca interattiva.

    +

    Seleziona un comune per aprire la sua scheda statica.

    {''.join(province_sections)}
    """ @@ -602,9 +613,212 @@ def build_information_pages( ), ) ) + + municipality_relations: dict[str, list[dict[str, Any]]] = {} + municipalities_by_province: dict[ + tuple[str, str, str], + list[dict[str, Any]], + ] = {} + for row in rows: + if row["location_kind"] != "municipality": + continue + municipality_relations.setdefault(row["location_id"], []).append(row) + for relations in municipality_relations.values(): + municipality = relations[0] + province_key = ( + municipality["province_code"], + municipality["province_name"], + municipality["region_name"], + ) + municipalities_by_province.setdefault(province_key, []).append( + municipality + ) + + for ( + province_code, + province_name, + region_name, + ), municipalities in sorted(municipalities_by_province.items()): + municipalities.sort( + key=lambda row: ( + row["name"], + row["municipality_istat_code"], + ) + ) + province_links = "".join( + f'
  • ' + f"{html.escape(row['name'])} " + f"ISTAT " + f"{html.escape(row['municipality_istat_code'])}
  • " + for row in municipalities + ) + province_body = f""" + +

    Provincia {html.escape(province_code)} · {version}

    +

    Comuni della provincia di {html.escape(province_name)}

    +

    Elenco di + {format_integer(len(municipalities))} comuni ISTAT della provincia + di {html.escape(province_name)}, nella regione + {html.escape(region_name)}. CAP e coordinate associati nelle + schede provengono da GeoNames e non sono dati postali ufficiali.

    +

    Elenco dei comuni

    + +
    + """ + pages.append( + write_page( + output, + f"province/{province_code.casefold()}", + page_document( + title=( + f"Comuni della provincia di {province_name} " + f"({province_code})" + ), + description=( + f"Elenco di {len(municipalities)} comuni ISTAT della " + f"provincia di {province_name}, con codici, CAP " + "GeoNames e stato delle coordinate." + ), + canonical_path=f"province/{province_code.casefold()}/", + body=province_body, + page_type="CollectionPage", + extra_structured_data={ + "@type": "AdministrativeArea", + "@id": ( + f"{SITE_ROOT}province/" + f"{province_code.casefold()}/#province" + ), + "name": province_name, + "containedInPlace": { + "@type": "AdministrativeArea", + "name": region_name, + }, + }, + ), + ) + ) + + for _, relations in sorted(municipality_relations.items()): + municipality = relations[0] + municipality_postal_codes = sorted( + { + relation["postal_code"] + for relation in relations + if relation["postal_code"] + } + ) + postal_text = ( + ", ".join( + html.escape(value) for value in municipality_postal_codes + ) + if municipality_postal_codes + else "non disponibile nello snapshot GeoNames" + ) + if municipality["latitude"] is None: + coordinate_text = "Coordinate non disponibili." + else: + coordinate_text = ( + f"Record GeoNames: {municipality['latitude']:.5f}, " + f"{municipality['longitude']:.5f}; accuracy " + f"{html.escape(municipality['coordinate_accuracy'])}. " + "Il match anagrafico non certifica l'accuratezza geografica." + ) + search_query = quote_plus(municipality["name"]) + body = f""" + +

    Comune ISTAT + {html.escape(municipality['municipality_istat_code'])}

    +

    {html.escape(municipality['name'])}

    +

    {html.escape(municipality['name'])} è un + comune della provincia di + {html.escape(municipality['province_name'])} + ({html.escape(municipality['province_code'])}), in + {html.escape(municipality['region_name'])}.

    +
    +

    Codice ISTAT

    + {html.escape(municipality['municipality_istat_code'])} +

    +

    CAP GeoNames

    + {postal_text}

    +

    Coordinate

    + {coordinate_text}

    +
    + +

    Consulta e scarica

    + +
    + """ + pages.append( + write_page( + output, + municipality_path(municipality).rstrip("/"), + page_document( + title=( + f"{municipality['name']}: codice ISTAT, CAP e " + "coordinate" + ), + description=( + f"Scheda del comune di {municipality['name']} " + f"({municipality['province_code']}): codice ISTAT " + f"{municipality['municipality_istat_code']}, CAP " + "GeoNames e stato delle coordinate." + ), + canonical_path=municipality_path(municipality), + body=body, + page_type="ProfilePage", + extra_structured_data={ + "@type": "AdministrativeArea", + "@id": ( + f"{SITE_ROOT}{municipality_path(municipality)}" + "#municipality" + ), + "name": municipality["name"], + "identifier": ( + municipality["municipality_istat_code"] + ), + "containedInPlace": { + "@type": "AdministrativeArea", + "name": municipality["province_name"], + }, + }, + ), + ) + ) return pages +def write_not_found_page(output: Path) -> None: + document = page_document( + title="Pagina non trovata", + description="La pagina richiesta non esiste nel sito Italian Cities.", + canonical_path="404.html", + body=f""" +

    Errore 404

    +

    Pagina non trovata

    +

    Il collegamento potrebbe essere cambiato. + Torna alla ricerca dei comuni oppure + consulta la copertura regionale. +

    + """, + ).replace( + '', + '', + ) + (output / "404.html").write_text(document, encoding="utf-8") + + def write_sitemap(output: Path, paths: list[str]) -> None: urls = ["", *paths] entries = "".join( @@ -690,6 +904,7 @@ def build_site(output: Path = DEFAULT_OUTPUT) -> dict[str, Any]: shutil.copy2(SOCIAL_PREVIEW, assets / "social-preview.jpg") render_homepage(output, stats) information_pages = build_information_pages(output, rows, stats) + write_not_found_page(output) (output / ".nojekyll").write_text("", encoding="utf-8") (output / "robots.txt").write_text( "User-agent: *\nAllow: /\n" diff --git a/scripts/check_workflow_pins.py b/scripts/check_workflow_pins.py new file mode 100644 index 0000000..1a039fb --- /dev/null +++ b/scripts/check_workflow_pins.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Require every third-party GitHub Action to use an immutable commit SHA.""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path + +from dataset_common import ROOT + +WORKFLOWS = ROOT / ".github/workflows" +USES_PATTERN = re.compile( + r"^\s*(?:-\s*)?uses:\s*([^#\s]+)", + re.MULTILINE, +) +PIN_PATTERN = re.compile(r"^[^@\s]+@[0-9a-fA-F]{40}$") + + +def find_unpinned_actions(workflows: Path = WORKFLOWS) -> list[str]: + errors: list[str] = [] + for path in sorted(workflows.glob("*.y*ml")): + source = path.read_text(encoding="utf-8") + for match in USES_PATTERN.finditer(source): + reference = match.group(1).strip("\"'") + if reference.startswith("./"): + continue + if not PIN_PATTERN.fullmatch(reference): + line = source.count("\n", 0, match.start()) + 1 + display_path = ( + path.relative_to(ROOT) + if path.is_relative_to(ROOT) + else path + ) + errors.append( + f"{display_path}:{line}: {reference} is not " + "pinned to a 40-character commit SHA" + ) + return errors + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--workflows", type=Path, default=WORKFLOWS) + return parser.parse_args() + + +def main() -> None: + errors = find_unpinned_actions(parse_args().workflows) + report = { + "status": "passed" if not errors else "failed", + "errors": errors, + } + print(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True)) + if errors: + raise SystemExit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/pages_model.py b/scripts/pages_model.py index 295e5cd..e6b27d4 100644 --- a/scripts/pages_model.py +++ b/scripts/pages_model.py @@ -11,7 +11,6 @@ WEB_FIELDS = ( "location_id", "name", - "normalized_name", "location_kind", "municipality_istat_code", "postal_code", @@ -24,7 +23,6 @@ "coordinate_verification", "coordinate_accuracy", "reconciliation_outcome", - "reconciliation_confidence", ) diff --git a/scripts/source_data.py b/scripts/source_data.py index df3fdc5..7721bdd 100644 --- a/scripts/source_data.py +++ b/scripts/source_data.py @@ -23,12 +23,14 @@ ROOT / "sources/snapshots/geonames/IT-2026-07-30.zip" ) DEFAULT_LEGACY = ROOT / "legacy/2023-05-02-original/Italian Cities.csv" +DEFAULT_ISTAT_REGIONS = ROOT / "sources/cache/Limiti01012026_g.zip" CANONICAL_SOURCE_IDS = ( "istat_municipalities", "geonames_postal_codes", ) HISTORICAL_SOURCE_IDS = ("legacy_csv",) +AUXILIARY_SOURCE_IDS = ("istat_region_boundaries",) ISTAT_NS = {"x": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"} GEONAMES_FIELDS = ( "country_code", @@ -60,7 +62,11 @@ def load_manifest(path: Path = SOURCE_MANIFEST) -> dict[str, Any]: source_ids = [source.get("id") for source in sources] if len(source_ids) != len(set(source_ids)): raise ValueError(f"{path}: duplicate source id") - for source_id in (*CANONICAL_SOURCE_IDS, *HISTORICAL_SOURCE_IDS): + for source_id in ( + *CANONICAL_SOURCE_IDS, + *HISTORICAL_SOURCE_IDS, + *AUXILIARY_SOURCE_IDS, + ): if source_id not in source_ids: raise ValueError(f"{path}: missing source {source_id}") return manifest @@ -101,11 +107,13 @@ def validate_declared_sources( istat_path: Path = DEFAULT_ISTAT, geonames_path: Path = DEFAULT_GEONAMES, legacy_path: Path = DEFAULT_LEGACY, + geography_path: Path = DEFAULT_ISTAT_REGIONS, ) -> dict[str, Any]: manifest = load_manifest(manifest_path) validate_source(manifest, "istat_municipalities", istat_path) validate_source(manifest, "geonames_postal_codes", geonames_path) validate_source(manifest, "legacy_csv", legacy_path) + validate_source(manifest, "istat_region_boundaries", geography_path) return manifest diff --git a/scripts/validate_dataset.py b/scripts/validate_dataset.py index c6ebdab..980d614 100644 --- a/scripts/validate_dataset.py +++ b/scripts/validate_dataset.py @@ -37,7 +37,10 @@ add_quality_error, completely_blank_record_lines, ) -from validators.coordinates import validate_coordinate_table +from validators.coordinates import ( + analyze_coordinate_distribution, + validate_coordinate_table, +) from validators.formats import validate_formats from validators.provenance import validate_verification_and_provenance from validators.schema import ( @@ -62,6 +65,7 @@ "numeric_coordinates", "coordinate_bounds", "coordinate_verification", + "coordinate_distribution", "provenance_completeness", "no_empty_rows", "no_logical_duplicates", @@ -371,6 +375,20 @@ def validate() -> dict[str, object]: accuracy_counts = Counter( row["coordinate_accuracy"] or "missing" for row in locations ) + coordinate_distribution = analyze_coordinate_distribution(locations) + distribution_check = checks["coordinate_distribution"] + review_groups = coordinate_distribution["groups_requiring_review"] + distribution_check["status"] = ( + "review_required" if review_groups else "passed" + ) + distribution_check["violations"] = review_groups + distribution_check["blocking"] = False + if review_groups: + warnings.append( + f"{review_groups} exact shared-coordinate groups contain at least " + f"{coordinate_distribution['review_threshold']} unique locations; " + "review coordinate_distribution before geographic use." + ) return { "structural_quality": "passed" if not errors else "failed", "operational_data_readiness": OPERATIONAL_DATA_READINESS, @@ -393,6 +411,7 @@ def validate() -> dict[str, object]: sorted(coordinate_counts.items()) ), "coordinate_accuracy_counts": dict(sorted(accuracy_counts.items())), + "coordinate_distribution": coordinate_distribution, "cross_format_equivalence": equivalence, } diff --git a/scripts/validate_site.py b/scripts/validate_site.py index 38c374c..a90775c 100644 --- a/scripts/validate_site.py +++ b/scripts/validate_site.py @@ -4,13 +4,15 @@ from __future__ import annotations import argparse +import gzip import json from html.parser import HTMLParser from pathlib import Path from urllib.parse import unquote, urlparse MAX_HOMEPAGE_BYTES = 30_000 -MAX_SEARCH_DATA_BYTES = 4_200_000 +MAX_SEARCH_DATA_BYTES = 4_500_000 +MAX_SEARCH_DATA_GZIP_BYTES = 900_000 MAX_FIRST_PARTY_CODE_BYTES = 80_000 @@ -55,20 +57,34 @@ def handle_endtag(self, tag: str) -> None: self._current_button = None -def resolve_local_link(site: Path, page: Path, href: str) -> Path | None: +def resolve_local_link( + site: Path, + page: Path, + href: str, +) -> tuple[Path, str] | None: parsed = urlparse(href) - if parsed.scheme or parsed.netloc or href.startswith(("mailto:", "#")): + if href.startswith("mailto:"): + return None + if (parsed.scheme or parsed.netloc) and ( + parsed.scheme not in {"http", "https"} + or parsed.netloc != "codewriter90x.github.io" + or not parsed.path.startswith("/Italian_Cities/") + ): return None path = unquote(parsed.path) - if path.startswith("/Italian_Cities/"): + if not path: + target = page + elif path.startswith("/Italian_Cities/"): target = site / path.removeprefix("/Italian_Cities/") elif path.startswith("/"): return None else: target = page.parent / path - if not path or path.endswith("/"): + if (not path or path.endswith("/")) and ( + target.is_dir() or path.endswith("/") + ): target /= "index.html" - return target.resolve() + return target.resolve(), unquote(parsed.fragment) def validate_site(site: Path) -> dict[str, object]: @@ -77,9 +93,11 @@ def validate_site(site: Path) -> dict[str, object]: html_files = sorted(site.rglob("*.html")) if not html_files: errors.append("site contains no HTML pages") + inspectors: dict[Path, PageInspector] = {} for page in html_files: inspector = PageInspector() inspector.feed(page.read_text(encoding="utf-8")) + inspectors[page.resolve()] = inspector duplicate_ids = sorted( identifier for identifier in set(inspector.ids) @@ -100,10 +118,24 @@ def validate_site(site: Path) -> dict[str, object]: ) if any(not text for text in inspector.button_texts): errors.append(f"{page}: button without accessible text") + for page, inspector in inspectors.items(): for href in inspector.links: - target = resolve_local_link(site, page, href) - if target is not None and not target.is_file(): + resolved = resolve_local_link(site, page, href) + if resolved is None: + continue + target, fragment = resolved + if not target.is_file(): errors.append(f"{page}: broken local link {href!r}") + continue + if fragment: + target_inspector = inspectors.get(target) + if ( + target_inspector is None + or fragment not in target_inspector.ids + ): + errors.append( + f"{page}: broken local fragment {href!r}" + ) homepage = site / "index.html" locations = site / "assets/locations.json" @@ -115,11 +147,19 @@ def validate_site(site: Path) -> dict[str, object]: budgets = { "homepage_bytes": homepage.stat().st_size, "search_data_bytes": locations.stat().st_size, + "search_data_gzip_bytes": len( + gzip.compress( + locations.read_bytes(), + compresslevel=9, + mtime=0, + ) + ), "first_party_code_bytes": code_bytes, } limits = { "homepage_bytes": MAX_HOMEPAGE_BYTES, "search_data_bytes": MAX_SEARCH_DATA_BYTES, + "search_data_gzip_bytes": MAX_SEARCH_DATA_GZIP_BYTES, "first_party_code_bytes": MAX_FIRST_PARTY_CODE_BYTES, } for name, value in budgets.items(): @@ -130,6 +170,13 @@ def validate_site(site: Path) -> dict[str, object]: errors.append("search data is not guarded by lazy loading") if 'id="map-fallback-body"' not in homepage.read_text(encoding="utf-8"): errors.append("canvas map lacks the accessible tabular fallback") + not_found = site / "404.html" + if not not_found.is_file(): + errors.append("custom 404.html is missing") + elif 'content="noindex, follow"' not in not_found.read_text( + encoding="utf-8" + ): + errors.append("custom 404.html must be noindex") report: dict[str, object] = { "status": "passed" if not errors else "failed", diff --git a/scripts/validate_typed_contract.py b/scripts/validate_typed_contract.py new file mode 100644 index 0000000..4fbc11b --- /dev/null +++ b/scripts/validate_typed_contract.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Validate the opt-in schema 4 typed JSON preview.""" + +from __future__ import annotations + +import argparse +import json +import tempfile +from pathlib import Path +from typing import Any + +from dataset_common import GENERATED_PATHS, ROOT +from export_typed_json import export_typed_json +from jsonschema.validators import validator_for + +DEFAULT_SCHEMA = ROOT / "schemas/italian_locations-v4.schema.json" + + +def validate_payload( + payload: dict[str, Any], + schema: dict[str, Any], +) -> list[str]: + validator_class = validator_for(schema) + validator_class.check_schema(schema) + validator = validator_class(schema) + errors = sorted( + validator.iter_errors(payload), + key=lambda error: [str(part) for part in error.absolute_path], + ) + return [ + f"{'/'.join(str(part) for part in error.absolute_path) or ''}: " + f"{error.message}" + for error in errors + ] + + +def validate_typed_contract( + *, + schema_path: Path = DEFAULT_SCHEMA, + input_path: Path | None = None, +) -> dict[str, Any]: + schema = json.loads(schema_path.read_text(encoding="utf-8")) + if input_path is None: + with tempfile.TemporaryDirectory(prefix="italian-cities-schema4-") as folder: + generated = Path(folder) / "italian_locations.typed.json" + export_typed_json( + GENERATED_PATHS["italian_locations"], + generated, + ) + payload = json.loads(generated.read_text(encoding="utf-8")) + else: + payload = json.loads(input_path.read_text(encoding="utf-8")) + errors = validate_payload(payload, schema) + return { + "status": "passed" if not errors else "failed", + "schema": str(schema_path), + "rows": len(payload.get("rows", [])), + "errors": errors, + } + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--schema", type=Path, default=DEFAULT_SCHEMA) + parser.add_argument("--input", type=Path) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + report = validate_typed_contract( + schema_path=args.schema, + input_path=args.input, + ) + print(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True)) + if report["status"] != "passed": + raise SystemExit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/validators/coordinates.py b/scripts/validators/coordinates.py index 1b55279..2169bb8 100644 --- a/scripts/validators/coordinates.py +++ b/scripts/validators/coordinates.py @@ -4,6 +4,8 @@ import math import re +from collections import Counter, defaultdict +from typing import Any from validators.common import QualityChecks, add_quality_error @@ -19,6 +21,99 @@ "official_boundary_derived", "missing", } +SHARED_COORDINATE_REVIEW_THRESHOLD = 10 + + +def analyze_coordinate_distribution( + rows: list[dict[str, str]], +) -> dict[str, Any]: + """Profile exact coordinate reuse at unique-location grain.""" + + unique_locations: dict[str, dict[str, str]] = {} + for row in rows: + location_id = row.get("location_id") or row.get("municipality_id") + if location_id: + unique_locations.setdefault(location_id, row) + + groups: dict[tuple[str, str], list[dict[str, str]]] = defaultdict(list) + for row in unique_locations.values(): + latitude = row.get("latitude", "") + longitude = row.get("longitude", "") + if latitude and longitude: + groups[(latitude, longitude)].append(row) + + shared = [ + (coordinate, members) + for coordinate, members in groups.items() + if len(members) > 1 + ] + shared.sort(key=lambda item: (-len(item[1]), item[0])) + details: list[dict[str, Any]] = [] + for (latitude, longitude), members in shared: + ordered = sorted( + members, + key=lambda row: ( + row.get("location_kind", ""), + row.get("name", ""), + row.get("location_id", row.get("municipality_id", "")), + ), + ) + details.append( + { + "latitude": float(latitude), + "longitude": float(longitude), + "location_count": len(ordered), + "municipality_count": sum( + row.get("location_kind") == "municipality" + or bool(row.get("municipality_id")) + for row in ordered + ), + "verification_counts": dict( + sorted( + Counter( + row.get("coordinate_verification", "") + for row in ordered + ).items() + ) + ), + "locations": [ + { + "location_id": row.get( + "location_id", + row.get("municipality_id", ""), + ), + "name": row.get("name", ""), + "province_code": row.get("province_code", ""), + "region_name": row.get("region_name", ""), + } + for row in ordered + ], + } + ) + + review_groups = [ + group + for group in details + if group["location_count"] >= SHARED_COORDINATE_REVIEW_THRESHOLD + ] + return { + "grain": "unique_location", + "exact_shared_coordinate_groups": len(details), + "locations_in_shared_coordinate_groups": sum( + group["location_count"] for group in details + ), + "largest_shared_coordinate_group": ( + details[0]["location_count"] if details else 0 + ), + "review_threshold": SHARED_COORDINATE_REVIEW_THRESHOLD, + "groups_requiring_review": len(review_groups), + "interpretation": ( + "Exact coordinate reuse is a source-quality signal, not automatic " + "proof of an error. geonames_place_match records a conservative " + "name and territory match; it does not certify coordinate accuracy." + ), + "review_groups": review_groups, + } def validate_coordinate_table( diff --git a/site/assets/app.js b/site/assets/app.js index d74c220..3fe8a19 100644 --- a/site/assets/app.js +++ b/site/assets/app.js @@ -3,6 +3,7 @@ import { filtersToSearchParams, formatInteger, inflateRows, + municipalityPagePath, projectCoordinates, searchLocations, } from "./core.mjs"; @@ -129,7 +130,7 @@ function renderResults() { row.latitude === null ? "coordinate mancanti" : row.coordinate_verification === "geonames_place_match" - ? `coordinate GeoNames associate al luogo · accuracy ${row.coordinate_accuracy}` + ? `record GeoNames con match anagrafico · accuracy ${row.coordinate_accuracy}; accuratezza geografica non certificata` : `coordinate GeoNames stimate · accuracy ${row.coordinate_accuracy}`; const postalText = row.postal_code_status === "missing" @@ -143,6 +144,14 @@ function renderResults() { button.append(heading, location, meta); button.addEventListener("click", () => selectLocation(row)); item.append(button); + const detailPath = municipalityPagePath(row); + if (detailPath) { + const detailLink = document.createElement("a"); + detailLink.className = "result-detail-link"; + detailLink.href = detailPath; + detailLink.textContent = "Scheda statica del comune"; + item.append(detailLink); + } fragment.append(item); } elements.results.append(fragment); diff --git a/site/assets/core.mjs b/site/assets/core.mjs index 9de02bf..41756a0 100644 --- a/site/assets/core.mjs +++ b/site/assets/core.mjs @@ -27,7 +27,6 @@ function searchableText(row) { return normalizeTerm( [ row.name, - row.normalized_name, row.postal_code, row.province_code, row.province_name, @@ -37,6 +36,22 @@ function searchableText(row) { ); } +export function slugifyTerm(value = "") { + return normalizeTerm(value) + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); +} + +export function municipalityPagePath(row) { + if ( + row.location_kind !== "municipality" || + !/^\d{6}$/.test(row.municipality_istat_code ?? "") + ) { + return ""; + } + return `./comuni/${row.municipality_istat_code}-${slugifyTerm(row.name)}/`; +} + function relevance(row, normalizedQuery) { if (!normalizedQuery) return row.location_kind === "municipality" ? 2 : 3; const normalizedName = normalizeTerm(row.name); diff --git a/site/assets/styles.css b/site/assets/styles.css index 5420c3c..b6e5b0b 100644 --- a/site/assets/styles.css +++ b/site/assets/styles.css @@ -397,6 +397,14 @@ a:focus-visible { border-color: rgba(69, 215, 232, 0.42); } +.result-detail-link { + display: inline-block; + margin: 0.45rem 0.75rem 0.7rem; + color: var(--accent); + font-size: 0.85rem; + font-weight: 700; +} + .result-heading { display: flex; gap: 0.75rem; diff --git a/site/index.html b/site/index.html index df0ac5c..bc9d329 100644 --- a/site/index.html +++ b/site/index.html @@ -204,7 +204,7 @@

    Cerca comuni, CAP e province