{
  "openapi": "3.1.0",
  "info": {
    "title": "RadarAddress API",
    "version": "1",
    "description": "Verifica indirizzi postali italiani secondo lo standard di recapito, deduplica anagrafiche, genera/valida/estrai il codice fiscale, e verifica email, telefono, sito web e nominativo, il tutto in JSON su un endpoint per servizio. Ogni chiamata restituisce un esito a parole chiave (vuoto quando non c'è niente da segnalare), un'etichetta leggibile e — per gli indirizzi — le due forme normalizzate (accentata e postale, ASCII con apostrofo).",
    "contact": {
      "name": "RadarAddress",
      "email": "info@radaraddress.it",
      "url": "https://www.radaraddress.it/api.php"
    }
  },
  "externalDocs": {
    "description": "Documentazione API con esempi completi",
    "url": "https://www.radaraddress.it/api.php"
  },
  "servers": [
    { "url": "https://www.radaraddress.it", "description": "Produzione" }
  ],
  "security": [
    { "bearerAuth": [] }
  ],
  "tags": [
    { "name": "indice", "description": "Descrizione self-service degli endpoint" },
    { "name": "credito", "description": "Elaborazioni disponibili e stato dei tre contatori, senza consumarne" },
    { "name": "contatto", "description": "Verifica contatto: indirizzo, nominativo e codice fiscale del record" },
    { "name": "deduplica", "description": "Deduplica anagrafiche (\"doblonatura\")" },
    { "name": "codicefiscale", "description": "Codice fiscale: genera, valida, estrai, confronta" },
    { "name": "arricchimento", "description": "Arricchimento nominativo: sesso, tipo soggetto, titolo, saluti" },
    { "name": "email", "description": "Verifica email" },
    { "name": "telefono", "description": "Verifica telefono" },
    { "name": "sito", "description": "Verifica sito web" },
    { "name": "suggerisci", "description": "Autocomplete indirizzo a sessione" },
    { "name": "nazioni", "description": "Catalogo ISO 3166-1 con i nomi in italiano, inglese e lingua del paese" },
    { "name": "lavori", "description": "Lettura dei lavori asincroni (elaborazioni oltre il tetto per chiamata)" }
  ],
  "paths": {
    "/api/v1/": {
      "get": {
        "tags": ["indice"],
        "operationId": "indice",
        "summary": "Descrizione degli endpoint disponibili",
        "description": "Endpoint pubblico, non richiede autenticazione: elenca gli endpoint dell'API, il modo di lavorare in asincrono e i codici di stato HTTP usati.",
        "security": [],
        "responses": {
          "200": {
            "description": "Descrizione dell'API.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IndiceRisposta" } } }
          }
        }
      }
    },
    "/api/v1/contatto": {
      "post": {
        "tags": ["contatto"],
        "operationId": "contattoNormalizza",
        "summary": "Verifica un contatto (indirizzo, nominativo, codice fiscale)",
        "description": "Sistema un contatto intero: indirizzo secondo lo standard di recapito italiano, nominativo e — se la richiesta lo porta — il codice fiscale, ripulito, completato se manca il solo carattere di controllo e confrontato con cognome, nome, sesso e data di nascita. Un contatto per volta nel corpo, oppure fino a 500 in `elementi`. Con `elementi` e `asincrono:true` il lavoro va in coda (vedi `/api/v1/contatto` GET e la sezione lavori asincroni).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ContattoRichiesta" },
              "examples": {
                "singolo": {
                  "summary": "Un contatto",
                  "value": { "indirizzo": "via leopardi 4", "cap": "20100", "localita": "milano", "provincia": "mi", "id": "RIF-001" }
                },
                "lotto": {
                  "summary": "Fino a 500 contatti",
                  "value": {
                    "elementi": [
                      { "id": "1", "indirizzo": "via leopardi 4", "localita": "milano" },
                      { "id": "2", "indirizzo": "via roma 1", "cap": "20121", "codice_fiscale": "RSSMRA85M01H501Q" }
                    ]
                  }
                },
                "asincrono": {
                  "summary": "Lotto asincrono (oltre 500, fino a 100.000)",
                  "value": { "asincrono": true, "elementi": [ { "id": "1", "indirizzo": "via leopardi 4", "localita": "milano" } ] }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Contatto elaborato (singolo, lotto sincrono, o lavoro accodato con `asincrono:true`). Sul singolo, `ok:false` con lo stesso status 200 quando l'esito è `DATI_INCOMPLETI`: la richiesta è stata processata, il limite è nell'input.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/ContattoSingoloRisposta" },
                    { "$ref": "#/components/schemas/ContattoLottoRisposta" },
                    { "$ref": "#/components/schemas/LavoroAccodatoRisposta" }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "403": { "$ref": "#/components/responses/AccountRequired" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "413": { "$ref": "#/components/responses/BatchTooLarge" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      },
      "get": {
        "tags": ["contatto", "lavori"],
        "operationId": "contattoLavoro",
        "summary": "Rilegge un lavoro asincrono di verifica contatto",
        "description": "Rilegge lo stato (e, quando completato, il risultato) di un lavoro accodato con `asincrono:true`. Sotto le 5.000 righe il risultato torna anche in `results` dentro il JSON; sopra, si scarica con `formato=csv`.",
        "parameters": [
          { "$ref": "#/components/parameters/LavoroCodice" },
          { "$ref": "#/components/parameters/LavoroFormato" }
        ],
        "responses": {
          "200": {
            "description": "Stato del lavoro (JSON o, con `formato=csv`, il file CSV in streaming).",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/LavoroStatoRisposta" } },
              "text/csv": { "schema": { "type": "string", "format": "binary" } }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/v1/deduplica": {
      "post": {
        "tags": ["deduplica"],
        "operationId": "deduplica",
        "summary": "Deduplica una lista di anagrafiche",
        "description": "Riconosce i record riferiti alla stessa persona allo stesso recapito anche quando sono scritti in modo diverso (via abbreviata o con grafie diverse, CAP corretto dal motore, cognome e nome invertiti, diminutivi ed equivalenze dei nomi). Motore di confronto delle anagrafiche maturato in quindici anni di uso, appoggiato al motore di normalizzazione degli indirizzi. Rubriche (vCard): un contatto può avere più indirizzi (`indirizzi`), più telefoni (`telefoni`) e più email (`emails`), ognuno con un tipo, senza tetto; due contatti coincidono se una qualunque coppia dei loro indirizzi coincide, o se hanno lo stesso nominativo e un recapito in comune; il record fuso è l'unione di indirizzi e recapiti col tipo, e un contatto con doppioni al suo interno esce fuso anche da solo (`DOPPIO_INTERNO`). Un contatto è una elaborazione, quanti indirizzi e recapiti abbia. Massimo 500 contatti per chiamata (`anagrafiche` + `riferimento`); per liste più grandi si usa il batch CSV asincrono dall'area personale. In alternativa alla deduplica, il corpo può portare `{lavoro, gruppo, lavorato}` per marcare un gruppo già letto come revisionato.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  { "$ref": "#/components/schemas/DeduplicaRichiesta" },
                  { "$ref": "#/components/schemas/DeduplicaAggiornaRichiesta" }
                ]
              },
              "examples": {
                "lista_singola": {
                  "summary": "Una lista: cerca i doppioni al suo interno",
                  "value": {
                    "anagrafiche": [
                      { "id": "C1", "cognome": "Rossi", "nome": "Daniela", "indirizzo": "Via Leopardi 4", "cap": "20123", "localita": "Milano", "provincia": "MI" },
                      { "id": "C2", "cognome": "Rossi", "nome": "Daniela", "indirizzo": "V. Leopardi 4", "cap": "20100", "localita": "milano", "provincia": "MI" }
                    ]
                  }
                },
                "rubrica": {
                  "summary": "Rubrica: più indirizzi e più recapiti per contatto, con il tipo",
                  "value": {
                    "anagrafiche": [
                      { "id": "C1", "cognome": "Rossi", "nome": "Daniela",
                        "indirizzi": [
                          { "tipo": "casa", "indirizzo": "Via Roma 12", "cap": "20021", "localita": "Bollate", "provincia": "MI" },
                          { "tipo": "ufficio", "indirizzo": "Via Leopardi 4", "cap": "20123", "localita": "Milano", "provincia": "MI" }
                        ],
                        "telefoni": [ { "tipo": "ufficio", "valore": "02 66710423" }, "340 7491386" ],
                        "emails": [ { "tipo": "casa", "valore": "daniela@example.com" } ] },
                      { "id": "C2", "cognome": "Rossi", "nome": "Daniela", "indirizzo": "V. Leopardi 4", "cap": "20100", "localita": "milano", "provincia": "MI", "telefono": "+39 340 7491386" }
                    ]
                  }
                },
                "due_liste": {
                  "summary": "Con riferimento: cerca ogni anagrafica nella seconda lista",
                  "value": {
                    "anagrafiche": [ { "id": "C1", "cognome": "Rossi", "nome": "Daniela", "indirizzo": "Via Leopardi 4", "cap": "20123", "localita": "Milano", "provincia": "MI" } ],
                    "riferimento": [ { "id": "R1", "cognome": "Rossi", "nome": "Dany", "indirizzo": "V. Leopardi 4", "cap": "20100", "localita": "Milano", "provincia": "MI" } ]
                  }
                },
                "segna_lavorato": {
                  "summary": "Marca un gruppo come revisionato",
                  "value": { "lavoro": "a1b2c3d4e5f6a1b2c3d4e5f6", "gruppo": 2, "lavorato": true }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Gruppi di doppioni (o abbinamenti, in modalità due liste), oppure conferma dell'aggiornamento \"lavorato\".",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/DeduplicaRisposta" },
                    { "$ref": "#/components/schemas/DeduplicaRiferimentoRisposta" },
                    { "$ref": "#/components/schemas/DeduplicaAggiornaRisposta" }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "413": { "$ref": "#/components/responses/BatchTooLarge" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      },
      "get": {
        "tags": ["deduplica", "lavori"],
        "operationId": "deduplicaLavoro",
        "summary": "Rilegge un risultato di deduplica salvato",
        "description": "Rilegge i gruppi di un lavoro di deduplica salvato (di default ogni deduplica con doppioni viene salvata, rileggibile per 30 giorni). Il codice del lavoro è quello restituito in `lavoro.codice` dalla chiamata POST.",
        "parameters": [
          { "$ref": "#/components/parameters/LavoroCodice" }
        ],
        "responses": {
          "200": {
            "description": "Lavoro e gruppi salvati.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeduplicaLavoroLetturaRisposta" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/v1/codicefiscale": {
      "post": {
        "tags": ["codicefiscale"],
        "operationId": "codiceFiscale",
        "summary": "Genera, valida, estrai o confronta un codice fiscale",
        "description": "Quattro azioni sul codice fiscale delle persone fisiche, indicate dal campo `azione`: `genera` dai dati anagrafici, `valida` un codice esistente (formato, carattere di controllo, omocodia), `estrai` le informazioni contenute (data di nascita, età, sesso, comune o stato estero di nascita), `confronta` un codice con i dati anagrafici dichiarati. Un elemento nel corpo, oppure fino a 2.000 in `elementi` (stessa `azione` per tutto il lotto). Con `elementi` e `asincrono:true` il lavoro va in coda. Le prime 1.000 operazioni al mese sono gratuite (franchigia unica su sito, API e lavori in blocco); il costo oltre la franchigia è nel listino (vedi https://www.radaraddress.it/prezzi.php).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CfRichiesta" },
              "examples": {
                "genera": {
                  "summary": "Genera dai dati anagrafici",
                  "value": { "azione": "genera", "cognome": "Rossi", "nome": "Mario", "sesso": "M", "data_nascita": "1980-01-01", "comune_nascita": "Roma" }
                },
                "valida": {
                  "summary": "Valida un codice esistente",
                  "value": { "azione": "valida", "codice_fiscale": "RSSMRA80A01H501U" }
                },
                "estrai": {
                  "summary": "Estrai le informazioni contenute",
                  "value": { "azione": "estrai", "codice_fiscale": "RSSMRA80A01H501U" }
                },
                "confronta": {
                  "summary": "Confronta con i dati anagrafici",
                  "value": { "azione": "confronta", "codice_fiscale": "RSSMRA80A01H501U", "cognome": "Rossi", "nome": "Mario", "sesso": "M", "data_nascita": "1980-01-01" }
                },
                "lotto": {
                  "summary": "Fino a 2.000 elementi, stessa azione",
                  "value": { "azione": "valida", "elementi": [ { "id": "1", "codice_fiscale": "RSSMRA80A01H501U" } ] }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Esito dell'azione (singolo o lotto).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/CfSingoloRisposta" },
                    { "$ref": "#/components/schemas/CfLottoRisposta" },
                    { "$ref": "#/components/schemas/LavoroAccodatoRisposta" }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "403": { "$ref": "#/components/responses/AccountRequired" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "413": { "$ref": "#/components/responses/BatchTooLarge" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      },
      "get": {
        "tags": ["codicefiscale", "lavori"],
        "operationId": "codiceFiscaleLavoro",
        "summary": "Rilegge un lavoro asincrono di codice fiscale",
        "parameters": [
          { "$ref": "#/components/parameters/LavoroCodice" },
          { "$ref": "#/components/parameters/LavoroFormato" }
        ],
        "responses": {
          "200": {
            "description": "Stato del lavoro (JSON o, con `formato=csv`, il file CSV in streaming).",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/LavoroStatoRisposta" } },
              "text/csv": { "schema": { "type": "string", "format": "binary" } }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/v1/arricchimento": {
      "post": {
        "tags": ["arricchimento"],
        "operationId": "arricchimento",
        "summary": "Arricchisce un nominativo",
        "description": "Sesso dedotto dal nome, tipo di soggetto (persona fisica o giuridica), titolo normalizzato e forme di saluto pronte per la corrispondenza (formale ed eventualmente informale). Riconosce anche la forma coniugale nel cognome (\"Rossi in Verdi\"). Un elemento nel corpo, oppure fino a 500 in `elementi`. Con `elementi` e `asincrono:true` il lavoro va in coda.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ArricchimentoRichiesta" },
              "examples": {
                "singolo": { "summary": "Un nominativo", "value": { "cognome": "Rossi", "nome": "Dott.ssa Maria" } },
                "lotto": { "summary": "Fino a 500 elementi", "value": { "elementi": [ { "id": "1", "cognome": "Rossi", "nome": "Dott.ssa Maria" } ] } }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Nominativo arricchito (singolo o lotto).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/ArricchimentoRisposta" },
                    { "$ref": "#/components/schemas/ArricchimentoLottoRisposta" },
                    { "$ref": "#/components/schemas/LavoroAccodatoRisposta" }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "403": { "$ref": "#/components/responses/AccountRequired" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "413": { "$ref": "#/components/responses/BatchTooLarge" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      },
      "get": {
        "tags": ["arricchimento", "lavori"],
        "operationId": "arricchimentoLavoro",
        "summary": "Rilegge un lavoro asincrono di arricchimento",
        "parameters": [
          { "$ref": "#/components/parameters/LavoroCodice" },
          { "$ref": "#/components/parameters/LavoroFormato" }
        ],
        "responses": {
          "200": {
            "description": "Stato del lavoro (JSON o, con `formato=csv`, il file CSV in streaming).",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/LavoroStatoRisposta" } },
              "text/csv": { "schema": { "type": "string", "format": "binary" } }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/v1/email": {
      "post": {
        "tags": ["email"],
        "operationId": "verificaEmail",
        "summary": "Verifica un indirizzo email",
        "description": "Sintassi, esistenza del dominio (record MX/A), refusi dei domini comuni con correzione proposta, domini usa-e-getta, indirizzi generici (info@, amministrazione@). Nessuna verifica SMTP della singola casella. Un elemento nel corpo, oppure fino a 500 in `elementi`. `dns:false` salta il lookup del dominio (più veloce). Con `elementi` e `asincrono:true` il lavoro va in coda.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/EmailRichiesta" },
              "examples": {
                "singolo": { "summary": "Un indirizzo", "value": { "email": "mario.rossi@gmial.com" } },
                "lotto": { "summary": "Fino a 500 elementi", "value": { "elementi": [ { "id": "1", "email": "mario.rossi@gmial.com" } ] } }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Esito della verifica (singolo o lotto).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/EmailRisposta" },
                    { "$ref": "#/components/schemas/EmailLottoRisposta" },
                    { "$ref": "#/components/schemas/LavoroAccodatoRisposta" }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "403": { "$ref": "#/components/responses/AccountRequired" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "413": { "$ref": "#/components/responses/BatchTooLarge" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      },
      "get": {
        "tags": ["email", "lavori"],
        "operationId": "verificaEmailLavoro",
        "summary": "Rilegge un lavoro asincrono di verifica email",
        "parameters": [
          { "$ref": "#/components/parameters/LavoroCodice" },
          { "$ref": "#/components/parameters/LavoroFormato" }
        ],
        "responses": {
          "200": {
            "description": "Stato del lavoro (JSON o, con `formato=csv`, il file CSV in streaming).",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/LavoroStatoRisposta" } },
              "text/csv": { "schema": { "type": "string", "format": "binary" } }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/v1/telefono": {
      "post": {
        "tags": ["telefono"],
        "operationId": "verificaTelefono",
        "summary": "Verifica un numero di telefono senza chiamare",
        "description": "Forma, classe (`mobile`, `fisso`, `speciale`, `estero` per il prefisso internazionale) e tipo quando riconosciuto, lunghezza secondo il piano di numerazione, distretto del fisso e operatore d'origine del blocco. Un campo può contenere più numeri (\"347… - 338…\"): si separano e tornano come `telefono`, `telefono2`, `telefono3` (fino a 3 per riga). Un elemento nel corpo, oppure fino a 5.000 in `elementi`. Con `elementi` e `asincrono:true` il lavoro va in coda. Consumo: un'operazione per NUMERO verificato, non per chiamata.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/TelefonoRichiesta" },
              "examples": {
                "singolo": { "summary": "Uno o più numeri in un campo", "value": { "telefono": "3470328959 - 011 1253265" } },
                "lotto": { "summary": "Fino a 5.000 elementi", "value": { "elementi": [ { "id": "1", "telefono": "011 1234567" } ] } }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Esito della verifica (singolo o lotto).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/TelefonoRisposta" },
                    { "$ref": "#/components/schemas/TelefonoLottoRisposta" },
                    { "$ref": "#/components/schemas/LavoroAccodatoRisposta" }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "403": { "$ref": "#/components/responses/AccountRequired" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "413": { "$ref": "#/components/responses/BatchTooLarge" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      },
      "get": {
        "tags": ["telefono", "lavori"],
        "operationId": "verificaTelefonoLavoro",
        "summary": "Rilegge un lavoro asincrono di verifica telefono",
        "parameters": [
          { "$ref": "#/components/parameters/LavoroCodice" },
          { "$ref": "#/components/parameters/LavoroFormato" }
        ],
        "responses": {
          "200": {
            "description": "Stato del lavoro (JSON o, con `formato=csv`, il file CSV in streaming).",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/LavoroStatoRisposta" } },
              "text/csv": { "schema": { "type": "string", "format": "binary" } }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/v1/sito": {
      "post": {
        "tags": ["sito"],
        "operationId": "verificaSito",
        "summary": "Verifica che un sito web risponda",
        "description": "Esistenza del dominio, richiesta HTTP coi redirect seguiti (uno alla volta), codice finale, validità del certificato. Nessun giudizio sul contenuto. Un campo può contenere più indirizzi: si separano e tornano come `sito`, `sito2`, `sito3` (fino a 3 per riga). Un elemento nel corpo, oppure fino a 100 in `elementi` (lotto più piccolo degli altri endpoint: ogni verifica apre una connessione di rete). `rete:false` controlla solo la forma, senza contattare il sito. Con `elementi` e `asincrono:true` il lavoro va in coda. Consumo: un'operazione per SITO verificato, non per chiamata.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SitoRichiesta" },
              "examples": {
                "singolo": { "summary": "Un indirizzo", "value": { "sito": "www.esempio.it" } },
                "lotto": { "summary": "Fino a 100 elementi", "value": { "elementi": [ { "id": "1", "sito": "www.esempio.it" } ] } }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Esito della verifica (singolo o lotto).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/SitoRisposta" },
                    { "$ref": "#/components/schemas/SitoLottoRisposta" },
                    { "$ref": "#/components/schemas/LavoroAccodatoRisposta" }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "403": { "$ref": "#/components/responses/AccountRequired" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "413": { "$ref": "#/components/responses/BatchTooLarge" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      },
      "get": {
        "tags": ["sito", "lavori"],
        "operationId": "verificaSitoLavoro",
        "summary": "Rilegge un lavoro asincrono di verifica sito",
        "parameters": [
          { "$ref": "#/components/parameters/LavoroCodice" },
          { "$ref": "#/components/parameters/LavoroFormato" }
        ],
        "responses": {
          "200": {
            "description": "Stato del lavoro (JSON o, con `formato=csv`, il file CSV in streaming).",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/LavoroStatoRisposta" } },
              "text/csv": { "schema": { "type": "string", "format": "binary" } }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/v1/suggerisci": {
      "post": {
        "tags": ["suggerisci"],
        "operationId": "suggerisciIndirizzo",
        "summary": "Autocomplete indirizzo mentre l'utente digita",
        "description": "Modello a sessione: il client genera un UUID per ogni indirizzo in compilazione. Le query di suggerimento (azione di default `suggerisci`) sono gratuite: il server decide da sé cosa proporre in base al testo e al contesto (cap/localita/provincia già compilati). Quando l'utente sceglie, l'azione `seleziona` chiude la sessione, addebita un'operazione leggera (una sola volta a sessione: una rifinitura del civico sulla stessa via non riaddebita) e restituisce il record normalizzato. Sessione valida 10 minuti, massimo 30 query. Il catalogo è italiano: per un paese estero esplicito la risposta resta 200 con `supportato:false` e nessun addebito.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  { "$ref": "#/components/schemas/SuggerisciRichiesta" },
                  { "$ref": "#/components/schemas/SuggerisciSelezionaRichiesta" }
                ]
              },
              "examples": {
                "digitazione": {
                  "summary": "Query mentre si digita",
                  "value": { "sessione": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "q": "via verdi", "localita": "monz" }
                },
                "selezione": {
                  "summary": "L'utente sceglie un suggerimento",
                  "value": { "sessione": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "azione": "seleziona", "via_id": 1063428, "civico": "4" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Suggerimenti (fase `nomi`/`vie`/`localita`/`comuni`/`civici`), oppure il record normalizzato dopo la selezione.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/SuggerisciRisposta" },
                    { "$ref": "#/components/schemas/SuggerisciSelezionaRisposta" }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "410": {
            "description": "Sessione scaduta (oltre 10 minuti): genera un nuovo UUID.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Errore" } } }
          },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/api/v1/credito": {
      "get": {
        "tags": ["credito"],
        "operationId": "credito",
        "summary": "Elaborazioni disponibili e stato di ogni contatore, senza consumarne",
        "description": "Per ciascun contatore (`contatto`, `deduplica`, `leggere`): `stato` con `avviso` (uno zero da solo non dice se il cliente ha esaurito, e va ricaricato, o se non usa il servizio), elaborazioni disponibili, gratuite rimaste nel mese e data in cui si azzerano, abbonamento (residuo, taglia, rinnovo), pacchetti attivi uno per uno (non scadono) e quanti ne ha comprati, auto-ricarica (attiva, taglia, tetto e speso del mese in euro), uso (mese, totali, ultimo); in cima `da_ricaricare` ed `endpoint`, la mappa di quale contatore consuma ogni endpoint. Vuole il token di un account: un token senza account risponde 404 `no_account`. La chiamata non consuma nulla.",
        "responses": {
          "200": {
            "description": "Saldi dei tre contatori.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreditoContatoriRisposta" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": {
            "description": "Il token non appartiene a un account con contatori (`error.code`: `no_account`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Errore" } } }
          },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    },
    "/api/v1/nazioni": {
      "get": {
        "tags": ["nazioni"],
        "operationId": "nazioni",
        "summary": "Catalogo ISO 3166-1 con i nomi in italiano, inglese e lingua del paese",
        "description": "Senza parametri restituisce l'intero catalogo: per ogni nazione codice_iso2, codice_iso3, nome italiano breve (`nome`) e ufficiale (`nome_esteso`), nome inglese (`nome_en`), nome nella lingua del paese (`nome_originale`, solo lingue europee; inglese per le altre) e `lingue`. Con `q` risolve una singola nazione da un codice o da un nome scritto come capita: italiano, inglese, lingua del paese, nome ufficiale o alias d'uso (`DE`, `Germania`, `Germany`, `Deutschland`, `Repubblica federale di Germania`; `UK` e `Olanda` risolvono in GB e NL). Endpoint pubblico: non richiede il token Bearer.",
        "security": [],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Codice ISO 3166-1 (alpha-2 o alpha-3) oppure nome della nazione in italiano, inglese o lingua del paese, anche ufficiale esteso o alias d'uso (UK, Olanda, Turkey).",
            "schema": { "type": "string" },
            "example": "Germania"
          }
        ],
        "responses": {
          "200": {
            "description": "Catalogo completo, oppure la nazione risolta con `q`.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/NazioniElencoRisposta" },
                    { "$ref": "#/components/schemas/NazioneRisoltaRisposta" }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Nazione non trovata nel catalogo ISO 3166-1 (con `q`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Errore" } } }
          },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ra_...",
        "description": "Header `Authorization: Bearer <token>`. Il token si genera dall'area personale (prefisso `ra_`, storicamente `ra_live_`). In alternativa è accettato l'header `X-Api-Key`. Il token NON va passato come query string `?api_key=` (rifiutato con 401: finiva nei log). Una scadenza facoltativa può essere impostata alla creazione del token: passata quella data, 401 con codice `token_expired`."
      }
    },
    "parameters": {
      "LavoroCodice": {
        "name": "lavoro",
        "in": "query",
        "required": true,
        "description": "Codice del lavoro asincrono (24 caratteri esadecimali), restituito da una chiamata con `asincrono:true` (o, per la deduplica, da una deduplica salvata).",
        "schema": { "type": "string", "pattern": "^[0-9a-f]{24}$" },
        "example": "369e46e9e7bb4c1a2b3c4d5e"
      },
      "LavoroFormato": {
        "name": "formato",
        "in": "query",
        "required": false,
        "description": "Con `csv` scarica il file di risultato invece del JSON (necessario oltre 5.000 righe).",
        "schema": { "type": "string", "enum": ["json", "csv"], "default": "json" }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Input non valido: corpo non JSON, campo obbligatorio mancante, azione sconosciuta, paese non valido, o simili. Il campo `error.code` distingue il caso (`bad_request`, `missing_field`, `invalid_json`, `bad_action`, `missing_session`, `invalid_country`, …).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Errore" } } }
      },
      "Unauthorized": {
        "description": "Token mancante, non valido, scaduto o revocato. `error.code`: `unauthorized`, `invalid_token`, `token_expired`.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Errore" } } }
      },
      "PaymentRequired": {
        "description": "Credito insufficiente per l'intero fabbisogno della chiamata e auto-ricarica assente, al tetto o con addebito fallito: la richiesta non viene elaborata. `error.code`: `payment_required`. Per i prezzi vedi https://www.radaraddress.it/prezzi.php.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Errore" } } }
      },
      "AccountRequired": {
        "description": "La modalità asincrona (`asincrono:true`) richiede un token legato a un account: il lavoro va nella coda di quell'account. `error.code`: `account_required`.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Errore" } } }
      },
      "MethodNotAllowed": {
        "description": "Metodo HTTP non supportato da questo endpoint.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Errore" } } }
      },
      "NotFound": {
        "description": "Risorsa non trovata (lavoro, suggerimento, nazione…). `error.code`: `not_found`.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Errore" } } }
      },
      "BatchTooLarge": {
        "description": "Il lotto (`elementi`, o `anagrafiche`+`riferimento` per la deduplica) supera il tetto per chiamata sincrona. `error.code`: `batch_too_large`. Oltre il tetto si usa `asincrono:true` (fino a 100.000 elementi) o il caricamento file dall'area personale.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Errore" } } }
      },
      "TooManyRequests": {
        "description": "Quota o rate limit superati. `error.code`: `rate_limited` (limite di richieste al minuto per il token, con header `Retry-After`) o `quota_exceeded` (quota giornaliera della demo per i token anonimi/legacy, con header `Retry-After` fino a mezzanotte Europe/Rome) o `session_quota` (troppe query nella sessione di `/suggerisci`).",
        "headers": {
          "Retry-After": { "description": "Secondi da attendere prima di riprovare.", "schema": { "type": "integer" } }
        },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Errore" } } }
      },
      "InternalError": {
        "description": "Errore interno.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Errore" } } }
      }
    },
    "schemas": {
      "Errore": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "enum": [false] },
          "error": {
            "type": "object",
            "properties": {
              "code": { "type": "string", "description": "Codice macchina dell'errore, es. `unauthorized`, `bad_request`, `payment_required`." },
              "message": { "type": "string", "description": "Messaggio leggibile in italiano." }
            }
          }
        },
        "required": ["ok", "error"]
      },
      "EsitoLabel": {
        "type": "object",
        "description": "Etichetta leggibile dell'esito, derivata dalla lista di parole chiave del campo `esito`.",
        "properties": {
          "kind": { "type": "string", "enum": ["ok", "modified", "warning", "error"] },
          "title": { "type": "string" },
          "message": { "type": "string" }
        }
      },
      "Nazione": {
        "type": "object",
        "properties": {
          "codice_iso2": { "type": "string", "description": "Codice ISO 3166-1 alpha-2.", "example": "IT" },
          "codice_iso3": { "type": "string", "description": "Codice ISO 3166-1 alpha-3.", "example": "ITA" },
          "nome": { "type": "string", "description": "Nome italiano.", "example": "Italia" },
          "nome_esteso": { "type": "string", "description": "Nome ufficiale in italiano.", "example": "Repubblica Italiana" },
          "nome_en": { "type": "string", "description": "Nome inglese.", "example": "Italy" },
          "nome_originale": { "type": "string", "description": "Nome nella lingua del paese (lingue europee; inglese per le altre). Più lingue ufficiali separate da « / ».", "example": "Italia" },
          "lingue": { "type": "array", "items": { "type": "string" }, "description": "Lingue del paese usate per la forma dell'indirizzo (codici ISO 639-1).", "example": ["it"] }
        }
      },
      "CreditoContatoriRisposta": {
        "type": "object",
        "description": "Risposta di GET /api/v1/credito: i saldi dei tre contatori del token.",
        "properties": {
          "ok": { "type": "boolean" },
          "contatori": {
            "type": "object",
            "properties": {
              "contatto": { "$ref": "#/components/schemas/CreditoContatore" },
              "deduplica": { "$ref": "#/components/schemas/CreditoContatore" },
              "leggere": { "$ref": "#/components/schemas/CreditoContatore" }
            }
          },
          "da_ricaricare": { "type": "array", "items": { "type": "string" }, "description": "Contatori in stato `esaurito`, `gratuite_esaurite` o `in_attesa_del_rinnovo`." },
          "tetto_globale_eur": { "type": ["number", "null"], "description": "Tetto mensile complessivo dell'auto-ricarica in euro; null = nessun tetto." },
          "speso_globale_mese_eur": { "type": "number", "description": "Quanto l'auto-ricarica ha speso questo mese su tutti i contatori, in euro." },
          "gratis_per_sempre": { "type": "boolean", "description": "Account con sconto totale: le operazioni non consumano elaborazioni." },
          "endpoint": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Quale contatore consuma ogni endpoint (`contatto`, `deduplica`, `leggere`).", "example": { "contatto": "contatto", "suggerisci": "contatto", "deduplica": "deduplica", "email": "leggere", "telefono": "leggere", "sito": "leggere", "arricchimento": "leggere", "codicefiscale": "leggere" } },
          "stati": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Legenda degli stati." },
          "nota": { "type": "string" }
        }
      },
      "CreditoContatore": {
        "type": "object",
        "properties": {
          "nome": { "type": "string", "example": "Verifica contatto" },
          "stato": { "type": "string", "enum": ["mai_usato", "gratuite", "gratuite_esaurite", "attivo", "in_attesa_del_rinnovo", "esaurito"], "description": "Uno zero da solo non dice se il cliente ha esaurito o se non usa il servizio: `mai_usato` = nessuna elaborazione e nessun pacchetto, mai; `gratuite` = mai comprato, lavora nella franchigia del mese; `gratuite_esaurite` = mai comprato e franchigia finita; `attivo` = ha elaborazioni comprate o di abbonamento; `in_attesa_del_rinnovo` = abbonamento con la dotazione finita e nessun pacchetto; `esaurito` = ha comprato in passato e non resta nulla, va ricaricato." },
          "avviso": { "type": "string", "description": "Che cosa significa lo stato e che cosa fare; vuoto per `attivo`." },
          "disponibili": { "type": "integer", "description": "Gratuite rimaste + dotazione dell'abbonamento + pacchetti." },
          "usate": { "type": "object", "properties": { "mese": { "type": "integer" }, "totali": { "type": "integer" }, "ultimo_uso": { "type": ["string", "null"], "format": "date-time" } } },
          "gratuite": { "type": "object", "properties": { "rimaste": { "type": "integer" }, "mese": { "type": "integer", "description": "Franchigia mensile del contatore." }, "si_azzerano_il": { "type": "string", "format": "date" } } },
          "abbonamento": { "type": ["object", "null"], "description": "null senza abbonamento.", "properties": { "residuo": { "type": "integer" }, "taglia": { "type": "integer" }, "si_rinnova_il": { "type": ["string", "null"], "format": "date", "description": "La dotazione non usata si perde al rinnovo." }, "nota": { "type": "string" } } },
          "pacchetti": { "type": "object", "properties": {
            "residuo": { "type": "integer" },
            "attivi": { "type": "array", "description": "I pacchetti con elaborazioni ancora da usare, dal più vecchio; non scadono.", "items": { "type": "object", "properties": { "origine": { "type": "string", "enum": ["acquisto", "auto_ricarica", "omaggio"] }, "taglia": { "type": "integer" }, "residuo": { "type": "integer" }, "attivato_il": { "type": "string", "format": "date" } } } },
            "comprati": { "type": "object", "properties": { "n": { "type": "integer", "description": "Pacchetti mai comprati o ricevuti, anche finiti." }, "ultimo_il": { "type": ["string", "null"], "format": "date-time" } } },
            "nota": { "type": "string" } } },
          "auto_ricarica": { "type": "object", "properties": { "attiva": { "type": "boolean" }, "taglia": { "type": "integer", "description": "Taglia del pacchetto comprato in automatico." }, "tetto_mese_eur": { "type": ["number", "null"] }, "speso_mese_eur": { "type": "number" } } }
        }
      },
      "Credito": {
        "type": "object",
        "description": "Che cosa è stato consumato e da dove (presente su ogni risposta di un'operazione a pagamento, quando il chiamante è un account). Le elaborazioni si comprano a pacchetti o in abbonamento, per contatore: vedi https://www.radaraddress.it/prezzi.php.",
        "properties": {
          "contatore": { "type": "string", "description": "Contatore del servizio: `contatto`, `deduplica` o `leggere`.", "example": "contatto" },
          "consumate": { "type": "integer", "description": "Elaborazioni prelevate dal contatore (gratuite escluse)." },
          "gratuite": { "type": "integer", "description": "Elaborazioni coperte dalla franchigia del mese (nessun prelievo)." },
          "abbonamento": {
            "type": "object",
            "description": "Parte prelevata dalla dotazione mensile dell'abbonamento.",
            "properties": {
              "elaborazioni": { "type": "integer" },
              "residuo": { "type": "integer", "description": "Dotazione del mese rimasta dopo il prelievo." }
            }
          },
          "pacchetti": {
            "type": "object",
            "description": "Parte prelevata dai pacchetti, dal più vecchio.",
            "properties": {
              "elaborazioni": { "type": "integer" },
              "residuo": { "type": "integer", "description": "Elaborazioni rimaste nei pacchetti dopo il prelievo." }
            }
          },
          "disponibili": { "type": "integer", "description": "Elaborazioni disponibili sul contatore dopo il prelievo (gratuite del mese comprese)." },
          "auto_ricarica": { "type": "integer", "description": "Pacchetti comprati in automatico per coprire questa chiamata." },
          "nota": { "type": "string", "description": "Spiegazione in italiano, in chiaro (senza markup)." }
        }
      },
      "Civico": {
        "type": "object",
        "properties": {
          "numero": { "type": ["string", "null"] },
          "esponente": { "type": ["string", "null"] },
          "estensione": { "type": ["string", "null"] },
          "colore": { "type": ["string", "null"], "description": "Colore del civico (es. Genova, R/N)." },
          "specificita": { "type": ["string", "null"], "description": "Qualificatore del civico (colore, snc, interno)." },
          "metrico": { "type": ["string", "null"], "description": "Valore metrico per le strade chilometriche." },
          "altro": { "type": ["string", "null"] }
        }
      },
      "Toponimo": {
        "type": "object",
        "properties": {
          "dug": { "type": ["string", "null"], "description": "Denominazione urbanistica generica (Via, Corso, Piazza…)." },
          "duf": { "type": ["string", "null"], "description": "Denominazione urbanistica specifica (il nome della via)." },
          "civico_label": { "type": ["string", "null"] }
        }
      },
      "Geo": {
        "type": ["object", "null"],
        "description": "Coordinate WGS84 del recapito. `precisione` dice a che livello siamo arrivati: `civico` (numero georeferenziato), `interpolato` (numero esatto assente, punto stimato dai civici vicini), `via` (punto della strada) o `comune` (centro del comune). Null solo se il comune non è risolto.",
        "properties": {
          "lat": { "type": "number" },
          "lon": { "type": "number" },
          "precisione": { "type": "string", "enum": ["civico", "interpolato", "via", "comune"] },
          "fonte": { "type": "string", "enum": ["anncsu", "osm"], "description": "`osm` = © OpenStreetMap contributors, licenza ODbL: attribuzione da riportare se pubblichi le coordinate." }
        }
      },
      "Territorio": {
        "type": ["object", "null"],
        "description": "Identificativi territoriali del comune risolto. Null se il comune non è stato risolto.",
        "properties": {
          "istat": { "type": ["string", "null"], "description": "Codice ISTAT del comune (6 cifre)." },
          "catastale": { "type": ["string", "null"], "description": "Codice catastale (Belfiore) del comune, es. F205." },
          "cab": { "type": ["string", "null"], "description": "Codice CAB del comune." },
          "via_id": { "type": ["string", "null"], "description": "Identificativo nazionale della via nell'archivio dei civici; null se la via non è stata agganciata." }
        }
      },
      "LocalitaTipo": {
        "type": ["object", "null"],
        "description": "Se il recapito è in un capoluogo di provincia o in un comune della provincia.",
        "properties": {
          "capoluogo": { "type": ["boolean", "null"], "description": "`null` quando non ci sono elementi per dirlo." },
          "label": { "type": "string" },
          "fonte": { "type": "string" }
        }
      },
      "Modifica": {
        "type": "object",
        "properties": {
          "campo": { "type": "string" },
          "label": { "type": "string" },
          "prima": { "type": "string" },
          "dopo": { "type": "string" },
          "kind": { "type": "string", "enum": ["valorizzato", "normalizzato", "modificato"] }
        }
      },
      "Spiegazione": {
        "type": "object",
        "description": "Il perché di una differenza significativa tra l'input e il normalizzato.",
        "properties": {
          "campo": { "type": "string" },
          "kind": { "type": "string" },
          "tag": { "type": "string" },
          "titolo": { "type": "string" },
          "breve": { "type": "string" },
          "lungo": { "type": "string" },
          "fonte": { "type": "string" }
        }
      },
      "Suggestion": {
        "type": "object",
        "description": "Una proposta alternativa quando l'esito è STRADA_NON_UNIVOCA/LOCALITA_NON_UNIVOCA e simili.",
        "properties": {
          "strada": { "type": "string" },
          "localita": { "type": "string" },
          "cap": { "type": "string" },
          "provincia": { "type": "string" },
          "tipo": { "type": "string", "enum": ["strada", "frazione", "comune"] },
          "multicap": { "type": "boolean" },
          "default_storico": { "type": "boolean" }
        }
      },
      "Dropped": {
        "type": "object",
        "description": "Campo azzerato nell'output perché non risolvibile contro l'archivio (es. località non trovata): il valore originale resta comunque in `input`.",
        "properties": {
          "field": { "type": "string" },
          "value": { "type": "string" },
          "reason": { "type": "string", "enum": ["not_in_db", "ambiguous_in_db"] }
        }
      },
      "IndirizzoCampi": {
        "type": "object",
        "description": "Il record indirizzo/nominativo in una delle tre viste (input, normalizzato, normalizzato_postale). Nella vista normalizzata i campi seguono la regola di case: localita e provincia in MAIUSCOLO, cognome/nome/titolo/presso/edificio/indirizzo in Title Case, cap intatto.",
        "properties": {
          "id": { "type": ["string", "null"] },
          "cognome": { "type": "string" },
          "nome": { "type": "string" },
          "presso": { "type": "string" },
          "edificio": { "type": "string" },
          "indirizzo": { "type": "string" },
          "cap": { "type": "string" },
          "localita": { "type": "string" },
          "provincia": { "type": "string" },
          "sesso": { "type": "string" },
          "titolo": { "type": "string" },
          "nazione": { "type": "string", "description": "Nome della nazione." },
          "nazione_iso2": { "type": "string", "description": "Codice ISO 3166-1 alpha-2." },
          "nazione_originale": { "type": "string", "description": "Solo esteri: nome del paese nella sua lingua (Deutschland, France)." }
        }
      },
      "RaNormalizeResult": {
        "type": "object",
        "description": "Struttura restituita dalla verifica di un indirizzo. Presente in `/contatto` (campo `result`) e nella selezione di `/suggerisci`. Su un elemento di lotto con `indirizzo` e `presso` entrambi vuoti, il risultato è ridotto a `{error, esito:\"DATI_INCOMPLETI\"}` senza gli altri campi.",
        "properties": {
          "error": { "type": "string", "description": "Presente solo quando i dati non bastano a normalizzare quell'elemento del lotto." },
          "trovato": { "type": "boolean" },
          "esito": {
            "type": "string",
            "description": "Lista di parole chiave separate da spazio, vuota quando non c'è niente da segnalare. Schema: `<condizioni> [MODIFICATO <tipi>]`.",
            "example": "MODIFICATO CAP LOCALITA_NORM PROVINCIA INDIRIZZO"
          },
          "esito_label": { "$ref": "#/components/schemas/EsitoLabel" },
          "motivo": { "type": "string", "description": "Il perché, in una riga." },
          "commenti": { "type": "array", "items": { "type": "string" }, "description": "Azioni consigliate." },
          "suggestions": { "type": "array", "items": { "$ref": "#/components/schemas/Suggestion" } },
          "input": { "$ref": "#/components/schemas/IndirizzoCampi" },
          "normalizzato": { "$ref": "#/components/schemas/IndirizzoCampi" },
          "normalizzato_postale": {
            "allOf": [{ "$ref": "#/components/schemas/IndirizzoCampi" }],
            "description": "Come `normalizzato`, ma in forma postale: ASCII con apostrofo al posto dell'accento (FORLI', CITTA' DEL VATICANO)."
          },
          "comune": { "type": ["string", "null"], "description": "Comune principale risolto, utile quando la località è una frazione." },
          "indirizzo_canonico": { "type": ["string", "null"], "description": "Via canonica quando l'utente ha usato un alias storico (flag `preserva_originale`)." },
          "localita_canonica": { "type": ["string", "null"], "description": "Comune italiano canonico quando l'utente ha usato la forma in lingua locale (flag `preserva_originale`)." },
          "indirizzo_confronto": { "type": "string", "description": "Solo esteri (esito ESTERO): la forma con cui la deduplica riconosce la stessa via scritta in due modi («350 Fifth Avenue» e «350 5th Ave» coincidono)." },
          "localita_confronto": { "type": "string", "description": "Solo esteri: chiave ASCII della città nella lingua del paese." },
          "dropped": { "type": "array", "items": { "$ref": "#/components/schemas/Dropped" } },
          "modifiche": { "type": "array", "items": { "$ref": "#/components/schemas/Modifica" } },
          "spiegazioni": { "type": "array", "items": { "$ref": "#/components/schemas/Spiegazione" } },
          "actions": { "type": "array", "items": { "type": "string", "enum": ["valorizzato", "normalizzato", "modificato"] } },
          "civico": { "$ref": "#/components/schemas/Civico" },
          "toponimo": { "$ref": "#/components/schemas/Toponimo" },
          "geo": { "$ref": "#/components/schemas/Geo" },
          "territorio": { "$ref": "#/components/schemas/Territorio" },
          "confidenza": { "type": ["string", "null"], "enum": ["alta", "media", null], "description": "Affidabilità della decisione di normalizzazione; null sugli esiti di errore." },
          "note": { "type": ["string", "null"], "description": "Nota di trasparenza sulla decisione (quando la confidenza non è 'alta', o su altre correzioni degne di nota)." },
          "zonato": { "type": "boolean" },
          "multicap": { "type": "boolean" },
          "localita_tipo": { "$ref": "#/components/schemas/LocalitaTipo" },
          "meta": {
            "type": "object",
            "properties": {
              "cicli_localita": { "type": ["integer", "null"] },
              "cicli_toponimo": { "type": ["integer", "null"] },
              "cicli_arcostradale": { "type": ["integer", "null"] }
            }
          },
          "nazione": { "$ref": "#/components/schemas/Nazione" },
          "codice_fiscale": { "type": "string", "description": "Solo su /contatto: forma ripulita del codice fiscale del record, quando fornito." },
          "codice_fiscale_esito": {
            "type": "object",
            "description": "Solo su /contatto, quando `codice_fiscale` è fornito nel record.",
            "properties": {
              "esito": { "type": "string" },
              "kind": { "type": "string", "enum": ["ok", "modified", "warning", "error"] },
              "motivo": { "type": "string" },
              "valido": { "type": "boolean" },
              "corrisponde": { "type": ["boolean", "null"] },
              "suggerimento": { "type": ["string", "null"] }
            }
          }
        }
      },
      "ContattoElemento": {
        "type": "object",
        "description": "Un contatto (indirizzo + nominativo + codice fiscale opzionale). Almeno uno tra `indirizzo` e `presso` è richiesto.",
        "properties": {
          "id": { "type": "string", "description": "Riferimento del chiamante, restituito tale e quale." },
          "indirizzo": { "type": "string" },
          "cap": { "type": "string" },
          "localita": { "type": "string" },
          "provincia": { "type": "string" },
          "nazione_iso2": { "type": "string", "description": "Codice ISO 3166-1 alpha-2. Se manca si presume Italia." },
          "nazione": { "type": "string", "description": "In alternativa a `nazione_iso2`: nome della nazione." },
          "presso": { "type": "string" },
          "edificio": { "type": "string" },
          "cognome": { "type": "string" },
          "nome": { "type": "string" },
          "sesso": { "type": "string" },
          "data_nascita": { "type": "string" },
          "codice_fiscale": { "type": "string" }
        }
      },
      "ContattoRichiesta": {
        "oneOf": [
          {
            "allOf": [
              { "$ref": "#/components/schemas/ContattoElemento" },
              {
                "type": "object",
                "properties": {
                  "postal_form": { "type": "boolean", "description": "Forza anche `normalizzato` nella forma postale." },
                  "preserva_originale": { "type": "boolean", "description": "Tiene il nome scritto dall'utente (via storica, comune in lingua locale) ed espone il canonico a parte." },
                  "precisione": { "type": "integer", "minimum": 1, "maximum": 5, "default": 3, "description": "1 esatto … 5 il più che trova. Oltre 3 il match è approssimato e la confidenza non sarà 'alta'." },
                  "esteri": { "type": "string", "enum": ["dichiarati", "rileva"], "default": "dichiarati", "description": "`dichiarati`: è estero solo il record che indica il paese (nazione/nazione_iso2, provincia EE). `rileva`: un record senza paese che non risulta in Italia viene riconosciuto come estero dai segnali espliciti (nome del paese nel testo, città estera nota, codice postale con una forma non italiana), mai per esclusione." }
                }
              }
            ]
          },
          {
            "type": "object",
            "required": ["elementi"],
            "properties": {
              "elementi": { "type": "array", "maxItems": 100000, "items": { "$ref": "#/components/schemas/ContattoElemento" } },
              "postal_form": { "type": "boolean" },
              "preserva_originale": { "type": "boolean" },
              "precisione": { "type": "integer", "minimum": 1, "maximum": 5 },
              "esteri": { "type": "string", "enum": ["dichiarati", "rileva"], "default": "dichiarati" },
              "asincrono": { "type": "boolean", "description": "Oltre 500 elementi (fino a 100.000): accoda il lavoro invece di rispondere subito." }
            }
          }
        ]
      },
      "ContattoSingoloRisposta": {
        "type": "object",
        "description": "A differenza del lotto, la risposta singola non porta il campo `credito` nel corpo (il consumo resta comunque negli header X-RA-Addebito/X-RA-Disponibili).",
        "properties": {
          "ok": { "type": "boolean", "description": "`false` quando l'esito è DATI_INCOMPLETI: la richiesta è comunque stata processata (status HTTP 200)." },
          "id": { "type": ["string", "null"] },
          "result": { "$ref": "#/components/schemas/RaNormalizeResult" }
        }
      },
      "ContattoLottoRisposta": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "count": { "type": "integer" },
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": ["string", "null"] },
                "result": { "$ref": "#/components/schemas/RaNormalizeResult" }
              }
            }
          },
          "credito": { "$ref": "#/components/schemas/Credito" }
        }
      },
      "LavoroAccodatoRisposta": {
        "type": "object",
        "description": "Risposta quando la chiamata porta `elementi` con `asincrono:true`.",
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "lavoro": { "type": "string", "description": "Codice del lavoro (24 caratteri esadecimali).", "example": "369e46e9e7bb4c1a2b3c4d5e" },
          "stato": { "type": "string", "example": "in_coda" },
          "righe": { "type": "integer" },
          "controlla": { "type": "string", "description": "Percorso da chiamare in GET per lo stato." },
          "credito": {
            "type": "object",
            "description": "Elaborazioni consumate dal lavoro (si prelevano all'accodamento).",
            "properties": {
              "contatore": { "type": "string" },
              "consumate": { "type": "integer" },
              "gratuite": { "type": "integer" },
              "abbonamento": { "type": "object", "properties": { "elaborazioni": { "type": "integer" } } },
              "pacchetti": { "type": "object", "properties": { "elaborazioni": { "type": "integer" } } },
              "auto_ricarica": { "type": "integer" },
              "nota": { "type": "string" }
            }
          }
        }
      },
      "LavoroStatoRisposta": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "lavoro": { "type": "string" },
          "stato": { "type": "string", "enum": ["in_coda", "in_corso", "completato", "errore"] },
          "righe": { "type": "integer" },
          "fatte": { "type": "integer" },
          "esiti": {
            "type": "object",
            "properties": {
              "ok": { "type": "integer" },
              "modificati": { "type": "integer" },
              "da_controllare": { "type": "integer" },
              "non_risolti": { "type": "integer" }
            }
          },
          "errore": { "type": "string", "description": "Presente solo con stato `errore`." },
          "avviso": { "type": "string", "description": "Es. risultato non più disponibile (ritenzione 30 giorni), o rimando al CSV oltre 5.000 righe." },
          "csv": { "type": "string", "description": "Percorso per scaricare il CSV, quando il JSON non porta `results`." },
          "results": {
            "type": ["array", "null"],
            "description": "Righe del risultato (oggetto per riga, colonne del CSV come chiavi); `null` quando si rimanda al CSV.",
            "items": { "type": "object", "additionalProperties": { "type": "string" } }
          }
        }
      },
      "DedupAnagrafica": {
        "type": "object",
        "description": "Un record anagrafico da confrontare per la deduplica.",
        "properties": {
          "id": { "type": "string", "description": "Riferimento del chiamante." },
          "cognome": { "type": "string" },
          "nome": { "type": "string" },
          "indirizzo": { "type": "string" },
          "cap": { "type": "string" },
          "localita": { "type": "string" },
          "provincia": { "type": "string" },
          "sesso": { "type": "string", "description": "Facoltativo (M/F)." },
          "codice_fiscale": { "type": "string", "description": "Facoltativo: se valido e coerente col nominativo della scheda, due schede con lo stesso codice sono la stessa persona anche a indirizzi diversi (`certo`, motivo «stesso codice fiscale»); con due codici validi e diversi mai `certo` né `probabile`. Un codice che non torna col nominativo non pesa." },
          "email": { "type": "string", "description": "Facoltativo: uguale in due schede con lo stesso nominativo → almeno `probabile` anche con indirizzo diverso se è personale; un'email generica dell'organizzazione (info@, ufficio@) è del posto e vale solo `ambiguo`." },
          "telefono": { "type": "string", "description": "Facoltativo: come l'email, secondo la forza del numero: il cellulare (3…) è della persona → `probabile`; il fisso (0…) è della casa o dell'ufficio → solo `ambiguo`. Gli esteri si confrontano in E.164 e valgono come personali; un numero senza prefisso in una scheda estera prende il prefisso del paese." },
          "nazione": { "type": "string", "description": "Facoltativo: paese della scheda (codice ISO2 o nome in italiano, inglese o lingua del paese). Le schede estere si confrontano sulla forma dell'indirizzo nella lingua del paese; due paesi diversi non sono mai la stessa scheda." },
          "indirizzi": { "type": "array", "items": { "$ref": "#/components/schemas/DedupIndirizzo" }, "description": "Rubriche: gli indirizzi del contatto, ognuno con un `tipo` (casa, ufficio…), senza tetto. I campi piatti `indirizzo`/`cap`/… sono il primo. Il contatto si confronta con gli altri su OGNI suo indirizzo; un contatto senza indirizzi si abbina per nominativo e recapiti." },
          "telefoni": { "type": "array", "items": { "$ref": "#/components/schemas/DedupRecapito" }, "description": "Rubriche: i telefoni del contatto, senza tetto. Ogni elemento è una stringa oppure `{tipo, valore}`; anche `telefono` accetta le stesse forme. Due contatti con lo stesso nominativo e un cellulare in comune, in qualunque posizione e con qualunque etichetta, sono almeno `probabile`; con un fisso in comune solo `ambiguo` (il fisso è del posto, non della persona)." },
          "emails": { "type": "array", "items": { "$ref": "#/components/schemas/DedupRecapito" }, "description": "Rubriche: le email del contatto, senza tetto; stesse forme di `telefoni` (anche `email` le accetta)." }
        }
      },
      "DedupRecapito": {
        "oneOf": [
          { "type": "string", "description": "Il dato solo, senza tipo." },
          { "type": "object", "required": ["valore"], "properties": {
              "tipo": { "type": "string", "description": "Etichetta com'è nella rubrica (casa, ufficio, cellulare, home, work, cell, iPhone…): torna com'è arrivata, più `tipo_norm` nel vocabolario vCard 4.0. Non pesa sull'abbinamento." },
              "valore": { "type": "string" } } }
        ]
      },
      "DedupIndirizzo": {
        "type": "object",
        "properties": {
          "tipo": { "type": "string", "description": "Etichetta com'è nella rubrica (casa, ufficio, altro…)." },
          "indirizzo": { "type": "string" }, "cap": { "type": "string" }, "localita": { "type": "string" },
          "provincia": { "type": "string" }, "nazione": { "type": "string" },
          "presso": { "type": "string" }, "edificio": { "type": "string" }
        }
      },
      "DedupSingolo": {
        "type": "object",
        "description": "Un contatto senza doppioni con altri. Con `esito` = `DOPPIO_INTERNO` ne ha al suo interno (lo stesso indirizzo scritto due volte, un numero o un'email ripetuti): `motivo` lo dice e `record` è il contatto fuso, con indirizzi e recapiti deduplicati.",
        "properties": {
          "id": { "type": "string" },
          "esito": { "type": "string", "enum": ["", "DOPPIO_INTERNO"] },
          "motivo": { "type": "string" },
          "record": { "$ref": "#/components/schemas/DedupRecord" }
        }
      },
      "DedupRecord": {
        "type": "object",
        "description": "Il contatto fuso: nominativo migliore e l'UNIONE di indirizzi e recapiti, ognuno con il tipo com'è arrivato, `tipo_norm` vCard e la `provenienza` (id dei contatti da cui viene). I campi piatti (`indirizzo`, `cap`, `localita`, `provincia`, `telefono`, `email`) portano il primo elemento.",
        "properties": {
          "cognome": { "type": "string" }, "nome": { "type": "string" }, "sesso": { "type": "string" },
          "indirizzo": { "type": "string" }, "cap": { "type": "string" }, "localita": { "type": "string" }, "provincia": { "type": "string" }, "nazione": { "type": "string" },
          "email": { "type": "string" }, "telefono": { "type": "string" },
          "indirizzi": { "type": "array", "items": { "type": "object", "properties": {
              "tipo": { "type": "string" }, "tipo_norm": { "type": "string" },
              "indirizzo": { "type": "string" }, "cap": { "type": "string" }, "localita": { "type": "string" }, "provincia": { "type": "string" }, "nazione": { "type": "string" },
              "presso": { "type": "string" }, "edificio": { "type": "string" },
              "tipi": { "type": "array", "items": { "type": "string" }, "description": "Tutte le etichette con cui lo stesso indirizzo era scritto." },
              "provenienza": { "type": "array", "items": { "type": "string" } },
              "schede": { "type": "integer", "description": "Quante schede (grafie) sono collassate in questo indirizzo." } } } },
          "telefoni": { "type": "array", "items": { "$ref": "#/components/schemas/DedupRecapitoFuso" } },
          "emails": { "type": "array", "items": { "$ref": "#/components/schemas/DedupRecapitoFuso" } }
        }
      },
      "DedupRecapitoFuso": {
        "type": "object",
        "properties": {
          "tipo": { "type": "string" }, "tipo_norm": { "type": "string" },
          "valore": { "type": "string", "description": "Com'è arrivato nella prima occorrenza." },
          "normalizzato": { "type": "string", "description": "La chiave di confronto: E.164 per i telefoni esteri, cifre per gli italiani, minuscolo per le email." },
          "tipi": { "type": "array", "items": { "type": "string" } },
          "provenienza": { "type": "array", "items": { "type": "string" } },
          "occorrenze": { "type": "integer", "description": "Quante volte compariva nei contatti fusi." }
        }
      },
      "DeduplicaRichiesta": {
        "type": "object",
        "required": ["anagrafiche"],
        "properties": {
          "anagrafiche": { "type": "array", "items": { "$ref": "#/components/schemas/DedupAnagrafica" }, "description": "Obbligatorio, max 500 record (insieme a `riferimento`)." },
          "riferimento": { "type": "array", "items": { "$ref": "#/components/schemas/DedupAnagrafica" }, "description": "Facoltativo: attiva la modalità due liste." },
          "livello_minimo": { "type": "string", "enum": ["certo", "probabile", "ambiguo"], "default": "probabile" },
          "esteri": { "type": "string", "enum": ["dichiarati", "rileva"], "default": "dichiarati", "description": "`rileva`: riconosce le schede estere anche senza `nazione`, dai segnali espliciti nel testo." },
          "salva": { "type": "boolean", "default": true, "description": "Salva il risultato, rileggibile 30 giorni con GET ?lavoro=." }
        }
      },
      "DeduplicaAggiornaRichiesta": {
        "type": "object",
        "required": ["lavoro", "gruppo"],
        "properties": {
          "lavoro": { "type": "string", "pattern": "^[0-9a-f]{24}$" },
          "gruppo": { "type": "integer" },
          "lavorato": { "type": "boolean" }
        }
      },
      "DedupGruppo": {
        "type": "object",
        "properties": {
          "gruppo": { "type": "integer" },
          "membri": { "type": "array", "items": { "type": "string" }, "description": "Id dei membri del gruppo." },
          "principale": { "type": ["string", "null"], "description": "Id del record da tenere." },
          "record": { "type": ["object", "null"], "description": "Record consolidato (i dati migliori uniti)." },
          "esito": { "type": "string", "enum": ["DOPPIO"] },
          "livello": { "type": ["string", "null"], "enum": ["certo", "probabile", "ambiguo", null] },
          "motivo": { "type": "string" },
          "note": { "type": "array", "items": { "type": "string" } },
          "commenti": { "type": "array", "items": { "type": "string" } }
        }
      },
      "DeduplicaRisposta": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "count": { "type": "integer" },
          "gruppi": { "type": "array", "items": { "$ref": "#/components/schemas/DedupGruppo" } },
          "singoli": { "type": "array", "items": { "$ref": "#/components/schemas/DedupSingolo" }, "description": "I contatti senza corrispondenze con altri; con `esito` DOPPIO_INTERNO hanno doppioni al loro interno e portano il record fuso." },
          "scartati": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": { "posizione": { "type": "string" }, "errore": { "type": "string" }, "id": { "type": ["string", "null"] } }
            }
          },
          "riepilogo": { "type": "object" },
          "lavoro": {
            "type": "object",
            "description": "Presente quando `salva` è vero e ci sono gruppi.",
            "properties": { "codice": { "type": "string" }, "scadenza": { "type": "string", "format": "date" } }
          },
          "credito": { "$ref": "#/components/schemas/Credito" }
        }
      },
      "DeduplicaRiferimentoRisposta": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "count": { "type": "integer" },
          "abbinamenti": {
            "type": "array",
            "items": { "type": "object", "properties": { "id": { "type": "string" }, "riferimento_id": { "type": "string" } } }
          },
          "non_trovati": { "type": "array", "items": { "type": "string" } },
          "scartati": { "type": "array", "items": { "type": "object" } },
          "riepilogo": { "type": "object" },
          "credito": { "$ref": "#/components/schemas/Credito" }
        }
      },
      "DeduplicaAggiornaRisposta": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "lavoro": { "type": "string" },
          "gruppo": { "type": "integer" },
          "lavorato": { "type": "boolean" }
        }
      },
      "DeduplicaLavoroLetturaRisposta": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "lavoro": {
            "type": "object",
            "properties": {
              "codice": { "type": "string" },
              "creato": { "type": "string" },
              "n_anagrafiche": { "type": "integer" }
            }
          },
          "gruppi": {
            "type": "array",
            "items": {
              "allOf": [
                { "$ref": "#/components/schemas/DedupGruppo" },
                {
                  "type": "object",
                  "properties": {
                    "lavorato": { "type": "boolean" },
                    "datalav": { "type": ["string", "null"] }
                  }
                }
              ]
            }
          }
        }
      },
      "CfElementoGenera": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "azione": { "type": "string", "enum": ["genera"] },
          "cognome": { "type": "string" },
          "nome": { "type": "string" },
          "sesso": { "type": "string", "enum": ["M", "F"] },
          "data_nascita": { "type": "string", "description": "AAAA-MM-GG o GG/MM/AAAA, con barre, trattini o punti; anche le otto cifre attaccate, il mese in lettere (1 gennaio 1980) e l'ora in coda. Anno con quattro cifre." },
          "comune_nascita": { "type": "string", "description": "O stato estero (es. \"Francia\")." },
          "provincia_nascita": { "type": "string", "description": "Facoltativa, disambigua i comuni omonimi." }
        }
      },
      "CfRichiesta": {
        "type": "object",
        "required": ["azione"],
        "properties": {
          "azione": { "type": "string", "enum": ["genera", "valida", "estrai", "confronta"] },
          "cognome": { "type": "string" },
          "nome": { "type": "string" },
          "sesso": { "type": "string", "enum": ["M", "F"] },
          "data_nascita": { "type": "string" },
          "comune_nascita": { "type": "string" },
          "provincia_nascita": { "type": "string" },
          "codice_fiscale": { "type": "string", "description": "Richiesto per `valida`, `estrai`, `confronta`." },
          "elementi": {
            "type": "array",
            "maxItems": 100000,
            "description": "Fino a 2.000 per chiamata sincrona (oltre, con `asincrono:true`, fino a 100.000); stessa `azione` per tutto il lotto.",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "string" },
                "cognome": { "type": "string" },
                "nome": { "type": "string" },
                "sesso": { "type": "string" },
                "data_nascita": { "type": "string" },
                "comune_nascita": { "type": "string" },
                "provincia_nascita": { "type": "string" },
                "codice_fiscale": { "type": "string" }
              }
            }
          },
          "asincrono": { "type": "boolean" }
        }
      },
      "CfSingoloRisposta": {
        "type": "object",
        "description": "Forma varia con l'azione: `genera` → codice_fiscale/luogo/provincia/estero; `valida`/`estrai` → i campi qui sotto; `confronta` → corrisponde/differenze. A differenza del lotto, la risposta singola non porta il campo `credito` nel corpo (il consumo resta comunque negli header X-RA-Addebito/X-RA-Disponibili).",
        "properties": {
          "ok": { "type": "boolean" },
          "codice_fiscale": { "type": "string" },
          "luogo": { "type": ["string", "null"] },
          "provincia": { "type": ["string", "null"] },
          "estero": { "type": "boolean" },
          "valido": { "type": "boolean" },
          "formato": { "type": "boolean" },
          "controllo": { "type": "boolean" },
          "omocodo": { "type": "boolean" },
          "errore": { "type": ["string", "null"] },
          "suggerimento": { "type": ["string", "null"], "description": "Codice corretto proposto, quando ricostruibile (es. manca solo il carattere di controllo)." },
          "data_nascita": { "type": "string" },
          "eta": { "type": "integer" },
          "sesso": { "type": "string", "enum": ["M", "F"] },
          "luogo_nascita": { "type": "string" },
          "provincia_nascita": { "type": ["string", "null"] },
          "nato_estero": { "type": "boolean" },
          "codice_belfiore": { "type": "string" },
          "nazione_nascita": { "type": ["string", "null"] },
          "anno_ambiguo": { "type": "boolean", "description": "Le due cifre dell'anno non distinguono il secolo (es. 1926 vs 2026)." },
          "corrisponde": { "type": "boolean", "description": "Solo su `confronta`." },
          "differenze": { "type": "array", "items": { "type": "string" }, "description": "Solo su `confronta`: campi che non combaciano." },
          "esito": { "type": "string" },
          "motivo": { "type": "string" },
          "note": { "type": "array", "items": { "type": "string" } },
          "commenti": { "type": "array", "items": { "type": "string" } }
        }
      },
      "CfLottoRisposta": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "count": { "type": "integer" },
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": { "id": { "type": ["string", "null"] }, "result": { "$ref": "#/components/schemas/CfSingoloRisposta" } }
            }
          },
          "credito": { "$ref": "#/components/schemas/Credito" }
        }
      },
      "ArricchimentoElemento": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "cognome": { "type": "string" },
          "nome": { "type": "string" },
          "titolo": { "type": "string" },
          "sesso": { "type": "string", "description": "M o F; accettate anche le forme scritte (maschio, donna, Sig.ra, male). X = ente, G = famiglia o coppia." },
          "qualifica": { "type": "string", "description": "La professione (anche come `professione`). Quando il titolo manca, alcune professioni lo danno: avvocato → Avv., ingegnere → Ing., architetto → Arch., geometra → Geom., ragioniere → Rag., medico → Dr., professore → Prof., generale, colonnello, onorevole, commendatore, cavaliere." },
          "titolo_studio": { "type": "string", "description": "Il titolo di studio (anche come `titstudio`): una laurea dà Dr./Dr.ssa, ingegneria Ing., architettura Arch., geometra Geom." }
        }
      },
      "ArricchimentoRichiesta": {
        "oneOf": [
          { "$ref": "#/components/schemas/ArricchimentoElemento" },
          {
            "type": "object",
            "required": ["elementi"],
            "properties": {
              "elementi": { "type": "array", "maxItems": 100000, "items": { "$ref": "#/components/schemas/ArricchimentoElemento" } },
              "asincrono": { "type": "boolean" }
            }
          }
        ]
      },
      "ArricchimentoRisposta": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "esito": { "type": "string", "enum": ["ARRICCHITO", "GIURIDICA", "PARZIALE"] },
          "motivo": { "type": "string" },
          "note": { "type": "array", "items": { "type": "string" } },
          "commenti": { "type": "array", "items": { "type": "string" } },
          "tipo_soggetto": { "type": "string", "enum": ["fisica", "giuridica"] },
          "sesso": { "type": "string", "enum": ["M", "F", ""] },
          "fonte_sesso": { "type": "string", "enum": ["esplicito", "dedotto", ""] },
          "titolo": { "type": "string" },
          "cognome": { "type": "string" },
          "nome": { "type": "string" },
          "referente": {
            "type": ["object", "null"],
            "description": "Quando il campo conteneva ente + persona di riferimento.",
            "properties": {
              "titolo": { "type": "string" }, "nome": { "type": "string" }, "cognome": { "type": "string" },
              "sesso": { "type": "string" }, "saluto": { "type": "string" }
            }
          },
          "saluto_formale": { "type": "string" },
          "saluto_informale": { "type": ["string", "null"] },
          "credito": { "$ref": "#/components/schemas/Credito" }
        }
      },
      "ArricchimentoLottoRisposta": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "count": { "type": "integer" },
          "results": {
            "type": "array",
            "items": { "type": "object", "properties": { "id": { "type": ["string", "null"] }, "result": { "$ref": "#/components/schemas/ArricchimentoRisposta" } } }
          },
          "credito": { "$ref": "#/components/schemas/Credito" }
        }
      },
      "EmailElemento": {
        "type": "object",
        "properties": { "id": { "type": "string" }, "email": { "type": "string" } }
      },
      "EmailRichiesta": {
        "oneOf": [
          {
            "type": "object",
            "required": ["email"],
            "properties": { "email": { "type": "string" }, "dns": { "type": "boolean", "default": true, "description": "false salta il lookup del dominio." } }
          },
          {
            "type": "object",
            "required": ["elementi"],
            "properties": {
              "elementi": { "type": "array", "maxItems": 100000, "items": { "$ref": "#/components/schemas/EmailElemento" } },
              "dns": { "type": "boolean", "default": true },
              "asincrono": { "type": "boolean" }
            }
          }
        ]
      },
      "EmailRisposta": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "valida": { "type": "boolean" },
          "email": { "type": "string" },
          "sintassi": { "type": "boolean" },
          "dominio_esiste": { "type": ["boolean", "null"] },
          "suggerimento": { "type": ["string", "null"], "description": "Correzione proposta per un refuso di dominio comune." },
          "usa_e_getta": { "type": "boolean" },
          "generica": { "type": "boolean" },
          "avvisi": { "type": "array", "items": { "type": "string" } },
          "esito": { "type": "string" },
          "kind": { "type": "string", "enum": ["ok", "modified", "warning", "error"] },
          "titolo": { "type": "string" },
          "motivo": { "type": "string" },
          "note": { "type": "array", "items": { "type": "string" } },
          "commenti": { "type": "array", "items": { "type": "string" } },
          "credito": { "$ref": "#/components/schemas/Credito" }
        }
      },
      "EmailLottoRisposta": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "count": { "type": "integer" },
          "results": {
            "type": "array",
            "items": { "type": "object", "properties": { "id": { "type": ["string", "null"] }, "result": { "$ref": "#/components/schemas/EmailRisposta" } } }
          },
          "credito": { "$ref": "#/components/schemas/Credito" }
        }
      },
      "TelefonoDettaglio": {
        "type": "object",
        "properties": {
          "campo": { "type": "string", "description": "telefono, telefono2, telefono3 (secondo la posizione nel campo di origine)." },
          "input": { "type": "string" },
          "normalizzato": { "type": "string" },
          "numero": { "type": "string" },
          "formato_e164": { "type": "string" },
          "classe": { "type": "string", "enum": ["0", "1", "3", "8", "speciale", "estero", ""], "description": "Prima cifra: 3 cellulare, 0 fisso, 1 e 8 numero speciale; 'estero' per il prefisso internazionale." },
          "tipo": { "type": "string", "description": "Lettura fine quando il servizio è riconosciuto per nome (numero verde, tariffa speciale, pubblica utilità, estero…)." },
          "distretto": { "type": "string" },
          "citta": { "type": "string" },
          "operatore": { "type": "string", "description": "Operatore a cui il blocco è stato assegnato in origine." },
          "valido": { "type": "boolean" },
          "modificato": { "type": "boolean" },
          "esito": { "type": "string" },
          "kind": { "type": "string", "enum": ["ok", "modified", "warning", "error"] },
          "titolo": { "type": "string" },
          "motivo": { "type": "string" },
          "note": { "type": "array", "items": { "type": "string" } },
          "commenti": { "type": "array", "items": { "type": "string" } }
        }
      },
      "TelefonoElemento": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "telefono": { "type": "string" },
          "nazione_iso2": { "type": "string", "description": "Facoltativo: paese del numero. Un numero senza prefisso è letto come nazionale di quel paese e torna in E.164; vale anche `nazione` con il nome." }
        }
      },
      "TelefonoRichiesta": {
        "oneOf": [
          {
            "type": "object",
            "required": ["telefono"],
            "properties": {
              "telefono": { "type": "string", "description": "Può contenere più numeri (fino a 3): si separano automaticamente." },
              "formato": { "type": "string", "enum": ["nazionale", "internazionale"], "default": "nazionale" },
              "nazione_iso2": { "type": "string", "description": "Facoltativo: paese dei numeri senza prefisso (default Italia)." }
            }
          },
          {
            "type": "object",
            "required": ["elementi"],
            "properties": {
              "elementi": { "type": "array", "maxItems": 100000, "items": { "$ref": "#/components/schemas/TelefonoElemento" } },
              "formato": { "type": "string", "enum": ["nazionale", "internazionale"], "default": "nazionale" },
              "nazione_iso2": { "type": "string", "description": "Facoltativo: paese di tutto il lotto per i numeri senza prefisso; l'elemento può sovrascriverlo." },
              "asincrono": { "type": "boolean" }
            }
          }
        ]
      },
      "TelefonoRisposta": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "telefono": { "type": "string", "description": "Sempre valorizzato quando c'è almeno un numero, anche se nel testo era il secondo." },
          "telefono2": { "type": "string" },
          "telefono3": { "type": "string" },
          "esito": { "type": "string" },
          "kind": { "type": "string", "enum": ["ok", "modified", "warning", "error"] },
          "motivo": { "type": "string" },
          "dettaglio": { "type": "array", "items": { "$ref": "#/components/schemas/TelefonoDettaglio" } },
          "credito": { "$ref": "#/components/schemas/Credito" }
        }
      },
      "TelefonoLottoRisposta": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "count": { "type": "integer" },
          "numeri": { "type": "integer", "description": "Totale numeri distinti trovati nel lotto (l'unità di consumo)." },
          "results": {
            "type": "array",
            "items": { "type": "object", "properties": { "id": { "type": ["string", "null"] }, "result": { "$ref": "#/components/schemas/TelefonoRisposta" } } }
          },
          "credito": { "$ref": "#/components/schemas/Credito" }
        }
      },
      "SitoDettaglio": {
        "type": "object",
        "properties": {
          "campo": { "type": "string", "description": "sito, sito2, sito3 (secondo la posizione nel campo di origine)." },
          "input": { "type": "string" },
          "normalizzato": { "type": "string" },
          "url": { "type": "string" },
          "url_normalizzato": { "type": "string" },
          "host": { "type": "string" },
          "raggiungibile": { "type": ["boolean", "null"] },
          "http_status": { "type": ["integer", "null"] },
          "url_finale": { "type": "string", "description": "URL dopo i redirect seguiti." },
          "redirect": { "type": "boolean" },
          "https": { "type": "boolean" },
          "certificato_ok": { "type": ["boolean", "null"] },
          "tempo_ms": { "type": ["integer", "null"] },
          "valido": { "type": "boolean" },
          "modificato": { "type": "boolean" },
          "esito": { "type": "string" },
          "kind": { "type": "string", "enum": ["ok", "modified", "warning", "error"] },
          "titolo": { "type": "string" },
          "motivo": { "type": "string" },
          "note": { "type": "array", "items": { "type": "string" } },
          "commenti": { "type": "array", "items": { "type": "string" } }
        }
      },
      "SitoElemento": {
        "type": "object",
        "properties": { "id": { "type": "string" }, "sito": { "type": "string" } }
      },
      "SitoRichiesta": {
        "oneOf": [
          {
            "type": "object",
            "required": ["sito"],
            "properties": {
              "sito": { "type": "string", "description": "Può contenere più indirizzi (fino a 3): si separano automaticamente." },
              "rete": { "type": "boolean", "default": true, "description": "false controlla solo la forma, senza contattare il sito." }
            }
          },
          {
            "type": "object",
            "required": ["elementi"],
            "properties": {
              "elementi": { "type": "array", "maxItems": 100000, "items": { "$ref": "#/components/schemas/SitoElemento" } },
              "rete": { "type": "boolean", "default": true },
              "asincrono": { "type": "boolean" }
            }
          }
        ]
      },
      "SitoRisposta": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "sito": { "type": "string" },
          "sito2": { "type": "string" },
          "sito3": { "type": "string" },
          "esito": { "type": "string" },
          "kind": { "type": "string", "enum": ["ok", "modified", "warning", "error"] },
          "motivo": { "type": "string" },
          "dettaglio": { "type": "array", "items": { "$ref": "#/components/schemas/SitoDettaglio" } },
          "credito": { "$ref": "#/components/schemas/Credito" }
        }
      },
      "SitoLottoRisposta": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "count": { "type": "integer" },
          "siti": { "type": "integer", "description": "Totale siti distinti trovati nel lotto (l'unità di consumo)." },
          "results": {
            "type": "array",
            "items": { "type": "object", "properties": { "id": { "type": ["string", "null"] }, "result": { "$ref": "#/components/schemas/SitoRisposta" } } }
          },
          "credito": { "$ref": "#/components/schemas/Credito" }
        }
      },
      "SuggerisciRichiesta": {
        "type": "object",
        "required": ["sessione"],
        "properties": {
          "sessione": { "type": "string", "description": "UUID generato dal client per l'indirizzo in compilazione (16-36 caratteri esadecimali/trattini).", "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" },
          "azione": { "type": "string", "enum": ["suggerisci"], "default": "suggerisci" },
          "q": { "type": "string", "description": "Testo digitato nel campo indirizzo (o, dopo la scelta di un nome, filtro sulla località)." },
          "cap": { "type": "string", "description": "Contesto opzionale, anche parziale." },
          "localita": { "type": "string", "description": "Contesto opzionale, anche parziale." },
          "provincia": { "type": "string", "description": "Contesto opzionale." },
          "nazione_iso2": { "type": "string", "description": "Facoltativo; default IT. Il catalogo copre solo l'Italia." },
          "nome_id": { "type": "integer", "description": "Id del nome scelto in fase 'nomi': `q` diventa il filtro sulla località." },
          "via_id": { "type": "integer", "description": "Id della via scelta: attiva la fase 'civici' (completamento/validazione del civico)." },
          "civico": { "type": "string", "description": "Civico parziale o completo, usato con `via_id` in fase civici." }
        }
      },
      "SuggerisciSelezionaRichiesta": {
        "type": "object",
        "required": ["sessione", "azione", "via_id"],
        "properties": {
          "sessione": { "type": "string" },
          "azione": { "type": "string", "enum": ["seleziona"] },
          "via_id": { "type": "integer", "description": "Id del suggerimento scelto." },
          "civico": { "type": "string" }
        }
      },
      "SuggerisciVia": {
        "type": "object",
        "properties": {
          "tipo": { "type": "string", "enum": ["via"] },
          "id": { "type": "integer" },
          "dug": { "type": "string" },
          "nome": { "type": "string" },
          "comune": { "type": "string" },
          "provincia": { "type": "string" },
          "frazione": { "type": "string" },
          "cap": { "type": "string" },
          "zonato": { "type": "boolean" }
        }
      },
      "SuggerisciNome": {
        "type": "object",
        "properties": {
          "tipo": { "type": "string", "enum": ["nome"] },
          "id": { "type": "integer" },
          "dug": { "type": "string" },
          "nome": { "type": "string" },
          "n_comuni": { "type": "integer" }
        }
      },
      "SuggerisciComune": {
        "type": "object",
        "properties": {
          "tipo": { "type": "string", "enum": ["comune"] },
          "comune": { "type": "string" },
          "provincia": { "type": "string" }
        }
      },
      "SuggerisciCivico": {
        "type": "object",
        "properties": {
          "tipo": { "type": "string", "enum": ["civico"] },
          "civico": { "type": "string" },
          "esponente": { "type": "string" },
          "label": { "type": "string" }
        }
      },
      "SuggerisciRisposta": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "fase": { "type": "string", "enum": ["nomi", "vie", "localita", "comuni", "civici", "attesa", "non_supportata"] },
          "troncato": { "type": "boolean", "description": "Vero se i risultati superano il limite interno e sono stati tagliati." },
          "suggerimenti": {
            "type": "array",
            "items": { "oneOf": [
              { "$ref": "#/components/schemas/SuggerisciVia" },
              { "$ref": "#/components/schemas/SuggerisciNome" },
              { "$ref": "#/components/schemas/SuggerisciComune" },
              { "$ref": "#/components/schemas/SuggerisciCivico" }
            ] }
          },
          "supportato": { "type": "boolean", "description": "false quando la nazione richiesta non è l'Italia: il catalogo copre solo indirizzi italiani." },
          "nazione": { "$ref": "#/components/schemas/Nazione" },
          "disponibile": { "type": "boolean", "description": "Fase civici: false quando per quella via non abbiamo i civici (nessuna validazione possibile)." },
          "esiste": { "type": ["boolean", "null"], "description": "Fase civici, con un civico digitato: se risulta tra i civici censiti (null = non verificabile, es. 'snc')." },
          "durata_ms": { "type": "integer" }
        }
      },
      "SuggerisciSelezionaRisposta": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "result": { "$ref": "#/components/schemas/RaNormalizeResult" }
        }
      },
      "NazioniElencoRisposta": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "count": { "type": "integer" },
          "nazioni": { "type": "array", "items": { "$ref": "#/components/schemas/Nazione" } },
          "fonte": { "type": "string", "example": "ISO 3166-1 / EU Vocabularies 20260318-0" }
        }
      },
      "NazioneRisoltaRisposta": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "nazione": { "$ref": "#/components/schemas/Nazione" },
          "fonte": { "type": "string" }
        }
      },
      "IndiceEndpoint": {
        "type": "object",
        "properties": {
          "path": { "type": "string" },
          "method": { "type": "string" },
          "description": { "type": "string" },
          "auth": { "type": "string" },
          "body": { "type": "string" },
          "query": { "type": "string" }
        }
      },
      "IndiceRisposta": {
        "type": "object",
        "description": "Risposta di GET /api/v1/ (endpoint pubblico, senza campo `ok`: qui non è un JSON di esito ma un indice statico).",
        "properties": {
          "service": { "type": "string", "example": "RadarAddress API" },
          "version": { "type": "string", "example": "v1" },
          "docs_url": { "type": "string", "example": "/api.php" },
          "endpoints": { "type": "array", "items": { "$ref": "#/components/schemas/IndiceEndpoint" } },
          "asincrono": {
            "type": "object",
            "properties": { "come": { "type": "string" }, "stato": { "type": "string" }, "note": { "type": "string" } }
          },
          "status_codes": { "type": "object", "additionalProperties": { "type": "string" } }
        }
      }
    }
  }
}
