Skip to content

Commit 01b311c

Browse files
authored
Merge pull request #59 from ondata/feat/birth-geography-province-region
feat(geography): provincia, nazione e regione di nascita su entrambe le camere
2 parents 056644e + 02e993c commit 01b311c

8 files changed

Lines changed: 376 additions & 13 deletions

File tree

LOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## 2026-07-11
44

5+
- **feat geografia di nascita normalizzata — provincia, nazione, regione su entrambe le camere** (#24/#23, branch `feat/birth-geography-province-region`). Corretto un **assunto sbagliato** (era anche nel codice e nel wiki): il Senato **non** espone "solo la città". Ri-verificato con `COUNT` su tutta la leg.19 (254 senatori): `osr:cittaNascita` 254/254, `osr:provinciaNascita` 250/254 (i 4 mancanti sono i nati all'estero), `osr:nazioneNascita` 254/254 (`Italia` o stato estero). `senators list` ora restituisce `birth_province`, `birth_country`, `birth_region`. **Regione derivata** una volta sola in `src/core/province-region.ts` (modulo TS bundle-safe) da SITUAS/ISTAT report 64 via CLI `opensituas`, date-accurate (union snapshot 2010 + 2020 + più recente per le province soppresse/rinominate, + 3 alias per le forme brevi del Senato): copertura 100% (0 province non risolte). **Camera** (`deputies list`): la geografia è solo uno slug nell'URI (`comune_provincia_regione`), non triple — ora decodificata in `birth_city`/`birth_province`/`birth_country`/`birth_region` con disambiguazione dello slug a 2 parti (Valle d'Aosta / Trentino-Alto Adige `comune_regione` vs estero `comune_stato`) e `birth_region` portata alla **stessa** forma canonica del Senato (verificato: "Veneto" identico tra camere). `birth_province`/`birth_country` restano nella forma nativa di ciascuna fonte (nessun canonicalizzatore province cross-camera). Wiki `dati-anagrafici.md`/`index.md` corretti; nota ai gestori Camera (geografia non strutturata) in `docs/note-gestori-lod/`. +10 test unitari (risorsa provincia→regione + parser slug Camera), 158/158 verdi.
56
- **follow-up residuale commenti PR #56/#57 (`bill-progress`)**: migliorato l'hint del ramo Camera su risultato vuoto (ora segnala che i filtri `keyword`/intervallo di date o la paginazione `limit`/`offset` possono escludere tutte le righe), ordinamento timeline Camera reso stabile per paginazione con `?st` come tie-breaker in `ORDER BY` (non proiettato in `SELECT DISTINCT`, che ne altererebbe il dedup), escape robusto dei literal SPARQL per `keyword` (Senato + Camera) con helper dedicato che escapa solo `\ " newline CR` per la grammatica STRING_LITERAL2 (non `JSON.stringify`, che emette `\uXXXX` per i controlli, rifiutato da alcuni parser), e descrizioni `dateFrom/dateTo` chiarite sui due significati (presentazione Senato vs data stato-iter Camera).
67
- **CI PR stabilizzata su due livelli**: il check richiesto `ci` ora esegue solo build + typecheck + test deterministici (esclude `src/tools/tools.test.ts` e `src/tools/sparql.test.ts` dipendenti da endpoint live). Aggiunto job `live_integration` su PR con `continue-on-error` per mantenere visibilità sui test reali Camera/Senato senza bloccare il merge per anti-bot/instabilità esterna.
78
- **follow-up commenti review su `bill-progress`**: nel ramo Camera (`number + branch=C`) ora vengono applicati anche i filtri input (`keyword`, `dateFrom/dateTo`, `limit`, `offset`) invece di essere ignorati nel return anticipato. Aggiunto `hint` dinamico specifico quando la timeline Camera è vuota (evita messaggi fuorvianti orientati al Senato) e rese neutrali rispetto all'entrypoint le `describe()` di `number`/`branch` (niente grafia `--flag`, testo valido sia per CLI sia MCP). Inclusi test su paginazione e hint del ramo C.

docs/lod-wiki/dati-anagrafici.md

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Dati anagrafici dei parlamentari (nascita, genere, luogo)
22

3-
Cosa espone il LOD su nascita, genere e luogo di un parlamentare, e le trappole per interrogarli in massa. Verificato il 2026-07-03 su Schlein (Camera, `308930`) e La Russa/altri (Senato, `senatore/1275`, `senatore/32`).
3+
Cosa espone il LOD su nascita, genere e luogo di un parlamentare, e le trappole per interrogarli in massa. Verificato il 2026-07-03 su Schlein (Camera, `308930`) e La Russa/altri (Senato, `senatore/1275`, `senatore/32`); copertura geografica Senato ri-verificata il 2026-07-11 con `COUNT` sull'intera leg.19.
44

55
## Camera
66

@@ -17,7 +17,8 @@ Proprietà anagrafiche (sul nodo persona):
1717
- **Nascita**: `?pers bio:Birth ?b . ?b bio:date ?d ; ocd:rif_luogo ?luogo`.
1818
- `bio:date` = stringa `YYYYMMDD` (es. `19850504`).
1919
- `ocd:rif_luogo` = URI `luogo.rdf/{comune}_{provincia}_{regione}` (es. `catania_catania_sicilia`); per l'estero `{citta}_{stato}` (es. `lugano_svizzera`).
20-
- **La gerarchia geografica È presente, dentro l'URI** (stringa composta): filtrabile per comune / provincia / regione via `STRENDS`/`CONTAINS`. NON sono entità RDF distinte (non esiste un nodo "Regione Sicilia"), ma per filtri e conteggi basta il pattern sulla stringa. Es.: donne nate in Sicilia = `FILTER(STRENDS(STR(?luogo),'_sicilia'))` → 10 deputate leg.19.
20+
- **La gerarchia geografica È presente ma NON è strutturata**: vive solo dentro la stringa dell'URI, non come triple/entità RDF (non esiste `osr:provinciaNascita`/`nazioneNascita` come alla Camera, né un nodo "Regione Sicilia"). Filtrabile per comune / provincia / regione via `STRENDS`/`CONTAINS` sull'URI. Es.: donne nate in Sicilia = `FILTER(STRENDS(STR(?luogo),'_sicilia'))` → 10 deputate leg.19.
21+
- **Trappole di parsing dell'URI** (chi decompone lo slug `comune_provincia_regione`): (1) le regioni mono/bi-provinciali (Valle d'Aosta, Trentino-Alto Adige) compaiono a **2 parti** `comune_regione`, identiche nella forma ai nati all'estero `comune_stato` → la 2ª parte va disambiguata contro l'elenco delle regioni note; (2) i nomi-provincia dello slug divergono dallo standard ISTAT (es. `reggio-emilia` vs "Reggio nell'Emilia", `pesaro-urbino` vs "Pesaro e Urbino") → la regione va letta dallo slug stesso (parte 3), non ri-derivata dalla provincia. Il fatto che la geografia non sia esposta come triple è un limite di modellazione **segnalato ai gestori Camera**.
2122
- **Bio testuale**: `dc:description` (titolo di studio + carriera, testo libero non strutturato).
2223

2324
Trappola: **serve `DISTINCT`** — il doppio `rdf:type` genera righe duplicate.
@@ -30,11 +31,18 @@ Nodo unico `osr:Senatore`. La nascita è esposta in due forme ridondanti, entram
3031
- `bio:birth ?b . ?b bio:date ?d ; bio:place ?p . ?p rdfs:label ?citta`.
3132

3233
- **Genere**: `foaf:gender`, valori `F` / `M`.
33-
- Il tool `senators list` già restituisce `birth_date`, `birth_city`, `gender`, `election_region` per ogni senatore.
3434

35-
Trappola filtro legislatura: usare `?s osr:mandato ?m . ?m osr:legislatura ?leg . FILTER(?leg={n})`. **NON** funziona `?m osr:legislatura {n}` come triple diretto (il letterale tipizzato non matcha in quella posizione → risultato vuoto).
35+
**Geografia di nascita — a differenza di quanto si credeva, il Senato la espone strutturata** (predicati diretti, non solo la città). Ri-verificato il 2026-07-11 con `COUNT(DISTINCT ?s)` sull'intera leg.19 (254 senatori):
36+
37+
| predicato | valore | copertura leg.19 |
38+
|---|---|---|
39+
| `osr:cittaNascita` | città nuda (`Rovigo`) | 254 / 254 |
40+
| `osr:provinciaNascita` | provincia nuda (`Rovigo`, `Potenza`) | 250 / 254 |
41+
| `osr:nazioneNascita` | nazione (`Italia`, o stato estero) | 254 / 254 |
3642

37-
Limite geografico: **il luogo di nascita è solo la città, senza provincia né regione.** Nessuna gerarchia. `osr:regioneElezione` esiste ma è la regione di **elezione**, non di nascita.
43+
I 4 senza `provinciaNascita` sono i **nati all'estero**: hanno `nazioneNascita``Italia` (es. Argentina, Libia, Canada, Regno Unito) e nessuna provincia. `nazioneNascita` è valorizzata anche per i nati in Italia (`Italia`), quindi è il campo giusto per distinguere italiani da nati all'estero. Il tool `senators list` restituisce `birth_city`, `birth_province`, `birth_country` e `birth_region` (quest'ultima derivata, vedi sotto). `osr:regioneElezione` esiste ma è la regione di **elezione**, non di nascita.
44+
45+
Trappola filtro legislatura: usare `?s osr:mandato ?m . ?m osr:legislatura ?leg . FILTER(?leg={n})`. **NON** funziona `?m osr:legislatura {n}` come triple diretto (il letterale tipizzato non matcha in quella posizione → risultato vuoto).
3846

3947
## Conseguenze per i filtri demografici
4048

@@ -43,9 +51,25 @@ Limite geografico: **il luogo di nascita è solo la città, senza provincia né
4351
| genere |`foaf:gender` (`female`/`male`) |`foaf:gender` (`F`/`M`) |
4452
| intervallo data di nascita |`bio:date` `YYYYMMDD` |`osr:dataNascita` `YYYY-MM-DD` |
4553
| luogo nascita — città | ✅ (dall'URI luogo) |`osr:cittaNascita` |
46-
| luogo nascita — provincia/regione | ✅ (nell'URI luogo) | ❌ solo città |
54+
| luogo nascita — provincia | ✅ (nell'URI luogo, non strutturata) |`osr:provinciaNascita` |
55+
| luogo nascita — nazione | ✅ (nell'URI luogo, per l'estero) |`osr:nazioneNascita` |
56+
| luogo nascita — regione | ✅ (nell'URI luogo) | ⚙️ derivata da provincia (vedi sotto) |
4757

4858
Note operative:
4959
- **Formati data diversi** (Camera `YYYYMMDD`, Senato `YYYY-MM-DD`): i filtri per intervallo vanno costruiti per-endpoint.
5060
- **Valori genere diversi** (`female`/`male` vs `F`/`M`): normalizzare l'input utente prima del filtro.
51-
- **"Nato/a in regione X"**: nativo su Camera; sul Senato serve un lookup esterno comune→provincia→regione (ISTAT) su `osr:cittaNascita` — motivo per cui conviene materializzare gli elenchi distinti in CSV e normalizzare lì la geografia.
61+
- **"Nato/a in regione X"**: entrambe le camere lo permettono, ma per vie diverse. Camera: la regione è già nello slug dell'URI luogo (parte 3). Senato: la regione **non** è esposta, si deriva da `osr:provinciaNascita` con una tabella provincia→regione (vedi sotto). Le due camere convergono sulla **stessa** stringa-regione canonica (`birth_region`), così un dataset che unisce Camera+Senato può raggruppare per regione di nascita senza riconciliazioni.
62+
- **Solo `birth_region` è unificata cross-camera.** `birth_city`, `birth_province` e `birth_country` restano nella forma **nativa** di ciascuna fonte e possono differire per grafia/maiuscole tra le due camere (es. nato all'estero: Senato `Argentina`, Camera `argentina`; provincia: Senato `Torino`, Camera `torino`). Per aggregazioni cross-camera usare `birth_region`; per città/provincia/nazione normalizzare a valle se serve confrontarle.
63+
64+
## Da provincia a regione: risorsa derivata da SITUAS/ISTAT
65+
66+
La regione di nascita non è un dato di fonte (il Senato dà la provincia, la Camera la annega nell'URI). La materializziamo una volta sola in `src/core/province-region.ts` (modulo TS, bundle-safe per il Worker), con questa provenienza:
67+
68+
- **Fonte**: SITUAS/ISTAT, report 64 *"Elenco Province/Uts"*, via la CLI `opensituas` (`opensituas get 64 -o json`). Dà `DEN_UTS` (nome provincia/UTS) → `DEN_REG` (nome regione).
69+
- **Snapshot a più date**: SITUAS è **date-accurate**`opensituas get 64 --date DD/MM/YYYY` dà la divisione in vigore a quella data (senza `--date` restituisce il *dato più recente / fine validità*, quindi una data anche futura). Uniamo tre snapshot per coprire ogni grafia che compare nei dati anagrafici (parlamentari nati sotto province poi soppresse o rinominate): **2010** (province sarde storiche Olbia-Tempio, Carbonia-Iglesias), **2020** (era *Sud Sardegna*, le 5 province ISTAT sarde nazionali attuali), **snapshot più recente** (in Sardegna riflette la riorganizzazione **regionale**: Sulcis Iglesiente, Gallura Nord-Est Sardegna, Ogliastra, Medio Campidano). Tutte le varianti sarde ricadono comunque in `Sardegna`.
70+
- **Normalizzazione**: chiavi confrontate senza accenti, minuscole, con apostrofi/trattini/slash resi spazio (`normProvince`), così `Forlì-Cesena`, `forli cesena`, `FORLI'-CESENA` collassano sulla stessa chiave.
71+
- **Alias** — solo le forme brevi che il Senato usa e che SITUAS non ha a **nessuna** data (SITUAS usa i nomi estesi/bilingui): `Aosta`→Valle d'Aosta, `Bolzano`→Trentino-Alto Adige, `Monza e Brianza`→Lombardia. (`Sud Sardegna` **non** è più un alias: è nativa nello snapshot 2020.)
72+
- **Copertura verificata**: 100% delle province di nascita distinte presenti nei dati del Senato risolvono a una regione (0 non risolte).
73+
- **Le label Senato NON sono aggiornate/allineate all'ISTAT corrente** — non è SITUAS a sbagliare (è date-accurate), è il Senato che espone nomi non normalizzati. Su 110 province distinte, 104 combaciano esattamente con la nomenclatura UTS ISTAT; le 6 differenze sono gestite da `normProvince`+alias e sono di due tipi: **refusi ortografici** (`Forli'-Cesena` con apostrofo invece dell'accento ì, `Verbano Cusio Ossola` senza trattini) e **forme non ufficiali/datate** (`Monza e Brianza` per "Monza e della Brianza"; forme brevi non bilingui `Aosta`/`Bolzano`). `Sud Sardegna` è invece **corretta** (è il nome ISTAT nazionale in vigore fino alla riorganizzazione regionale sarda). Segnalato ai gestori Senato come miglioramento: esporre i **codici** ISTAT dei luoghi (non solo le label) renderebbe i join robusti a grafia e cambi amministrativi. Nota: il Senato espone solo le **label**, non i codici ISTAT — segnalato ai gestori come miglioramento (i codici renderebbero i join robusti alla grafia).
74+
75+
Quando cambiano le circoscrizioni provinciali, **non** si edita la mappa a mano: si rigenera da SITUAS con `opensituas`.

docs/lod-wiki/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ Il differenziale rispetto a una semplice descrizione dell'ontologia sono le **tr
1616

1717
* [Trappole Virtuoso — funzioni stringa e confronti](trappole-virtuoso-funzioni-stringa.md) - due comportamenti non standard del motore Virtuoso (entrambi gli endpoint): `SUBSTR` fuori range aborta la query senza short-circuit; `>=`/`<=` sul risultato di funzioni stringa fa un confronto numerico che dà 0 righe se non avvolto in `STR()`. Causa frequente di SPARQL "corretto" ma non funzionante.
1818
* [Freschezza del dato e cosa fa fede sull'approvazione](freschezza-e-autorevolezza.md) - il LOD non espone un "as-of" affidabile; per l'esito (approvato/respinto/promulgato) la fonte di verità è resoconto/scheda iter/GU. Distinzione "approvato da un ramo" ≠ "legge".
19-
* [Dati anagrafici dei parlamentari](dati-anagrafici.md) - nascita, genere, luogo: due nodi alla Camera (persona vs deputato), doppia forma al Senato; gerarchia geografica presente nell'URI luogo Camera ma assente al Senato (solo città); trappole su formati data e filtro legislatura.
19+
* [Dati anagrafici dei parlamentari](dati-anagrafici.md) - nascita, genere, luogo: due nodi alla Camera (persona vs deputato), doppia forma al Senato; geografia di nascita strutturata al Senato (`cittaNascita`/`provinciaNascita`/`nazioneNascita`) ma solo dentro l'URI luogo alla Camera; regione derivata da provincia con tabella SITUAS/ISTAT; trappole su formati data, filtro legislatura e parsing dello slug Camera.

src/core/province-region.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, it, expect } from "vitest";
2+
import { normProvince, regionFromProvince, canonicalRegion } from "./province-region.js";
3+
4+
describe("normProvince", () => {
5+
it("toglie accenti, minuscolizza e normalizza i separatori", () => {
6+
expect(normProvince("Forlì-Cesena")).toBe("forli cesena");
7+
expect(normProvince("Valle d'Aosta")).toBe("valle d aosta");
8+
expect(normProvince(" Reggio Emilia ")).toBe("reggio emilia");
9+
});
10+
});
11+
12+
describe("regionFromProvince", () => {
13+
it("risolve province italiane in qualsiasi grafia ragionevole", () => {
14+
expect(regionFromProvince("Rovigo")).toBe("Veneto");
15+
expect(regionFromProvince("cremona")).toBe("Lombardia");
16+
expect(regionFromProvince("POTENZA")).toBe("Basilicata");
17+
});
18+
it("copre gli alias delle forme brevi del Senato", () => {
19+
expect(regionFromProvince("Aosta")).toBe("Valle d'Aosta/Vallée d'Aoste");
20+
expect(regionFromProvince("Bolzano")).toBe("Trentino-Alto Adige/Südtirol");
21+
expect(regionFromProvince("Sud Sardegna")).toBe("Sardegna");
22+
});
23+
it("ritorna stringa vuota per input assente o non risolvibile", () => {
24+
expect(regionFromProvince("")).toBe("");
25+
expect(regionFromProvince(undefined)).toBe("");
26+
expect(regionFromProvince("Parigi")).toBe("");
27+
});
28+
});
29+
30+
describe("canonicalRegion", () => {
31+
it("porta gli slug regione della Camera alla forma canonica del Senato", () => {
32+
expect(canonicalRegion("veneto")).toBe("Veneto");
33+
expect(canonicalRegion("emilia-romagna")).toBe("Emilia-Romagna");
34+
expect(canonicalRegion("friuli-venezia-giulia")).toBe("Friuli-Venezia Giulia");
35+
expect(canonicalRegion("trentino-alto-adige")).toBe("Trentino-Alto Adige/Südtirol");
36+
expect(canonicalRegion("valle-d-aosta")).toBe("Valle d'Aosta/Vallée d'Aoste");
37+
});
38+
it("ritorna vuoto per gli stati esteri (disambiguazione slug a 2 parti)", () => {
39+
expect(canonicalRegion("svizzera")).toBe("");
40+
expect(canonicalRegion("argentina")).toBe("");
41+
expect(canonicalRegion("")).toBe("");
42+
});
43+
});

0 commit comments

Comments
 (0)