Il nome è il campo meno prevedibile di una zona a lettura ottica. Le date sono sei cifre, il numero del documento è di nove caratteri con una cifra di controllo, e i codici vengono da un elenco fisso. Il nome è come si chiama una persona, compresso in un numero fisso di lettere maiuscole, senza spazi, senza accenti, senza punteggiatura e senza una cifra di controllo che vi dica quando un carattere è stato letto male.

Questo articolo spiega come la zona scrive un nome, come separarlo di nuovo in cognome e nomi, che cosa gli fa la traslitterazione, come capire che potrebbe essere stato accorciato e come confrontarlo con il nome stampato sulla pagina. Tutti i nomi qui sotto sono inventati oppure sono il campione dell'ICAO, Anna Maria Eriksson; non compare alcuna persona reale.

Questo è il blog di doc.cheap, un'API OCR per passaporti e documenti d'identità. Niente di ciò che segue ne ha bisogno; il codice non ha dipendenze.

Dove si trova il nome

Il Doc 9303 dell'ICAO, lo standard per i documenti di viaggio a lettura ottica, assegna al nome un campo di larghezza fissa in ciascuno dei suoi tre formati. Qui le posizioni partono da 1, come le conta lo standard:

Formato Dove si trova il nome Larghezza
TD3 (passaporti), 2 × 44 riga 1, posizioni 6–44 39 caratteri
TD2, 2 × 36 riga 1, posizioni 6–36 31 caratteri
TD1 (carte d'identità), 3 × 30 riga 3, l'intera riga 30 caratteri

I primi cinque caratteri della riga 1 di un TD3 o di un TD2 sono il codice del documento e lo Stato emittente; il nome occupa tutto ciò che segue. Il TD1 dà al nome una riga tutta sua, ed è comunque il più stretto dei tre. La pagina sui formati MRZ descrive ogni formato campo per campo.

Nel campione TD3 la riga 1 è P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<. Togliete P<UTO e il campo del nome è ERIKSSON<<ANNA<MARIA, seguito da caratteri di riempimento fino a 39 caratteri.

La grammatica: <<, < e il riempimento

L'alfabeto della zona è A–Z, 0–9 e il carattere di riempimento <. Un nome vi aggiunge tre convenzioni:

  • L'identificativo primario viene per primo. Di solito è il cognome.
  • Due caratteri di riempimento, <<, chiudono l'identificativo primario. Ciò che segue è l'identificativo secondario, di solito i nomi.
  • Un solo carattere di riempimento, <, separa le parole all'interno di ciascuna parte. ANNA<MARIA sono due nomi; VAN<DER<BERG è un cognome di tre parole.

Ciò che resta del campo dopo il nome viene riempito con <. Rileggere un nome richiede quindi tre passi: togliere i caratteri di riempimento finali, dividere al primo << e trasformare i singoli < rimasti in spazi.

Due forme sembrano strane ma non sono errori. Una persona con un solo nome ha soltanto l'identificativo primario, quindi il campo contiene il nome e poi i caratteri di riempimento, senza << prima di essi: OKONKWO<<<<…. Un campo scritto con il separatore e nulla dopo, NAKAMURA<<<<…, si legge esattamente allo stesso modo una volta tolti i caratteri di riempimento finali. In entrambi i casi l'identificativo secondario è vuoto, non mancante.

Ciò che non sopravvive è la punteggiatura, e lo spazio stesso. Un trattino, un apostrofo o uno spazio non si possono scrivere nella zona, quindi uno Smith-Jones stampato arriva come SMITH<JONES, indistinguibile da un cognome di due parole. Se vi serve la punteggiatura, si trova sulla pagina stampata, non nella zona.

Traslitterazione: da qualsiasi scrittura a 26 lettere

Ogni nome deve finire in A–Z. Per un nome stampato in lettere latine con segni diacritici, ciò significa ricondurre ogni lettera accentata a lettere semplici. Per un nome in greco, arabo, cirillico, caratteri han o qualsiasi altra scrittura, significa scrivere l'intero nome in lettere latine.

La Parte 3 del Doc 9303, quella comune a tutti i formati, contiene traslitterazioni raccomandate per entrambi i casi. La decisione spetta però allo Stato emittente, e non tutti gli Stati scelgono allo stesso modo. Per il codice contano tre conseguenze:

  1. Un nome stampato può avere più di una grafia nella zona. Un MÜLLER stampato può comparire nella zona come MUELLER o come MULLER, secondo le scelte dell'emittente. Il codice che si aspetta un'unica corrispondenza fissa dichiarerà sbagliata una zona corretta.
  2. La corrispondenza non è reversibile. MULLER in una zona può venire da MULLER, MÜLLER o MÚLLER sulla pagina. Una zona vi dice quali lettere contiene, non il nome che la persona scrive.
  3. Il nome latino sulla pagina può differire dalla zona. Un documento stampato in una scrittura nazionale spesso stampa anche una versione latina, e quella grafia latina stampata non è sempre identica, lettera per lettera, a quella della zona. Trattate una differenza come qualcosa da esaminare, non come la prova di un errore di lettura.

La norma pratica: confrontate un nome della zona con un nome stampato dopo aver ridotto entrambi a sole lettere, tenete conto delle grafie che i vostri emittenti usano davvero e conservate la grafia stampata per tutto ciò che una persona legge.

Troncamento: quando il nome non ci sta

Un nome lungo non va a capo su un'altra riga. Viene accorciato per stare nel campo, e il nome completo resta sulla pagina stampata. Lo standard indica come accorciare un nome, e un nome accorciato di norma arriva fino all'ultimo carattere del suo campo.

Questo dà a un parser l'unico segnale disponibile: se l'ultimo carattere del campo del nome è una lettera e non <, il nome potrebbe essere stato troncato. È "potrebbe", non "è stato": anche un nome lungo esattamente quanto il campo finisce con una lettera. Ma un campo che termina con caratteri di riempimento non è stato certamente tagliato.

La larghezza conta. Prendete un titolare inventato, Konstantin Friedrich Hartmann Schoenebeck. In un campo TD3 di 39 caratteri:

HARTMANN<SCHOENEBECK<<KONSTANTIN<FRIEDR

L'ultimo carattere è R, una lettera, quindi il nome potrebbe essere stato tagliato, e lo è stato: FRIEDRICH ha perso tre lettere. Lo stesso nome tagliato alla larghezza TD1 di 30 conserva solo HARTMANN<SCHOENEBECK<<KONSTANT. Un sistema che salva il nome della zona come nome del titolare salva KONSTANT come nome di battesimo.

Ne derivano due abitudini. Non usate mai il nome della zona come nome da mostrare quando il campo è pieno; usate quello stampato. E quando confrontate un nome stampato completo con un nome della zona, un nome stampato che inizia con il nome della zona corrisponde, se il campo era pieno.

Un emittente può anche accorciare un nome in modi diversi dal taglio della parte finale, per esempio riducendo un secondo nome all'iniziale. Un test sul prefisso segnala un nome del genere come non corrispondente. Affidate quel caso a una persona invece di rifiutare il documento.

Nessuna cifra di controllo copre il nome

Le cifre di controllo coprono il numero del documento, le due date, il numero personale facoltativo nel TD3 e una cifra composita su gran parte della riga inferiore. Nessuna copre il nome. Nel TD3 e nel TD2 il nome si trova sulla riga 1, che nessuna cifra tocca; nel TD1 è sulla riga 3, e la cifra composita si ferma alla riga 2.

Un errore di lettura in un nome è quindi silenzioso. Una M letta come N, o un carattere di riempimento letto come lettera, dà comunque una zona i cui conti tornano. L'unico controllo incrociato per un nome è la pagina stampata: leggete entrambi e confrontateli, ed è a questo che serve la funzione di confronto più sotto. Come funzionano le cifre di controllo della MRZ spiega il calcolo per i campi che sono coperti.

Dividere il campo in Python

Nessuna dipendenza. La funzione riceve solo il campo del nome, già estratto dalla sua riga (line1[5:44] nel TD3, line1[5:36] nel TD2, line3 nel TD1).

import re

def words(part):
    return " ".join(word for word in part.split("<") if word)

def read_mrz_name(field):
    """Split an MRZ name field into (primary, secondary, may_be_truncated)."""
    if not re.fullmatch(r"[A-Z<]+", field):
        raise ValueError("a name field holds only A-Z and the filler <")
    may_be_truncated = not field.endswith("<")
    body = field.rstrip("<")
    primary, _separator, secondary = body.partition("<<")
    return words(primary), words(secondary), may_be_truncated

partition divide solo al primo << e restituisce una parte secondaria vuota quando non ce n'è, cioè il caso del nome unico. Il pattern rifiuta le cifre di proposito: uno 0 in un campo del nome è una O letta male, e deve arrivare a una persona invece di essere stampato come nome.

La stessa divisione in JavaScript

export function readMrzName(field) {
  if (!/^[A-Z<]+$/.test(field)) throw new Error("a name field holds only A-Z and the filler <");
  const mayBeTruncated = !field.endsWith("<");
  const body = field.replace(/<+$/, "");
  const at = body.indexOf("<<");
  const words = (part) => part.split("<").filter(Boolean).join(" ");
  if (at < 0) return { primary: words(body), secondary: "", mayBeTruncated };
  return { primary: words(body.slice(0, at)), secondary: words(body.slice(at + 2)), mayBeTruncated };
}

Confrontare la zona con il nome stampato

Il nome stampato viene dalla zona visiva, la parte della pagina che legge una persona, con i suoi accenti e la sua punteggiatura. La funzione qui sotto lo riduce a sole lettere maiuscole e lo confronta con una parte del nome della zona. Restituisce una di tre parole: match, mismatch, oppure unknown quando il nome stampato contiene una lettera che da sola non si riduce ad A–Z, come Ø, Æ o Ł.

import re
import unicodedata

# The spellings to try besides plain removal of the accent. These three are
# an example; put in the ones your issuers use.
EXPANSIONS = {"Ä": "AE", "Ö": "OE", "Ü": "UE"}

def compare_name(zone_part, printed_part, truncated=False, expansions=EXPANSIONS):
    """'match', 'mismatch' or 'unknown' for one name part: zone against printed."""
    zone = zone_part.replace(" ", "")
    upper = unicodedata.normalize("NFC", printed_part).upper()
    candidates = {upper, "".join(expansions.get(c, c) for c in upper)}
    verdict = "unknown"
    for candidate in candidates:
        letters = "".join(
            c for c in unicodedata.normalize("NFKD", candidate)
            if c.isalpha() and not unicodedata.combining(c)
        )
        if not re.fullmatch(r"[A-Z]*", letters):
            continue  # a letter with no plain A-Z form: leave it to a person
        if letters == zone or (truncated and letters.startswith(zone)):
            return "match"
        verdict = "mismatch"
    return verdict

NFKD scompone una lettera accentata nella lettera semplice e nel suo accento, e l'accento viene scartato. Vengono scartati anche spazi, trattini e apostrofi, perché il confronto riguarda solo le lettere: Smith-Jones e SMITH JONES diventano entrambi SMITHJONES. Per un nome stampato solo in una scrittura non latina, confrontate la zona con la versione latina che la pagina stampa, non con quella nella scrittura nazionale; quest'ultima restituirebbe qui unknown ogni volta.

Vettori di test

Tutti i nomi sono inventati, tranne il campione dell'ICAO. Nella prima tabella ogni campo è riempito con < fino alla larghezza TD3 di 39 (in Python, name.ljust(39, "<")), tranne l'ultima riga, che è già lunga esattamente 39 caratteri.

Campo prima del riempimento Primario Secondario Forse troncato
ERIKSSON<<ANNA<MARIA ERIKSSON ANNA MARIA no
VAN<DER<BERG<<PIETER VAN DER BERG PIETER no
SMITH<JONES<<MARY SMITH JONES MARY no
OKONKWO OKONKWO (vuoto) no
NAKAMURA<< NAKAMURA (vuoto) no
HARTMANN<SCHOENEBECK<<KONSTANTIN<FRIEDR HARTMANN SCHOENEBECK KONSTANTIN FRIEDR sì

E per compare_name:

Parte della zona Parte stampata Troncato Atteso
ANNA MARIA Anna Maria no match
SMITH JONES Smith-Jones no match
MULLER Müller no match
MUELLER Müller no match (tramite l'espansione)
MILLER Müller no mismatch
KONSTANTIN FRIEDR Konstantin Friedrich sì match
KONSTANTIN FRIEDR Konstantin Friedrich no mismatch
SORENSEN Sørensen no unknown

L'ultima riga è voluta. Se Ø sia diventata O oppure OE è una scelta dell'emittente, e una funzione che tira a indovinare sbaglierà per qualcuno; aggiungete invece alle espansioni la forma che usano i vostri emittenti.

Cosa fa il nostro parser MRZ

Il parser MRZ di questo sito legge un nome allo stesso modo: toglie i caratteri di riempimento finali, divide al primo << e trasforma i singoli caratteri di riempimento in spazi; un campo senza << viene letto come solo cognome. Quando il nome arriva fino all'ultimo carattere del suo campo, mostra un avviso che il nome potrebbe essere stato troncato per starci, con le posizioni che occupa. Funziona nel browser, quindi potete incollare una zona sintetica e confrontarne la lettura con il vostro codice.

Dove si colloca in una pipeline

Un servizio di riconoscimento in hosting legge sia la zona sia la pagina stampata, quindi la domanda è quale nome vi restituisce. Nella risposta di doc.cheap, holder.surname, holder.given_names e holder.full_name contengono la lettura latina unificata. Un nome stampato in una scrittura nazionale è una voce separata di fields: ogni lettura ha la propria voce, quella latina predefinita ha un id che termina con @0, e quella nella scrittura nazionale indica la sua language, per esempio Greek. La zona stessa si trova in mrz.lines, esattamente come è stata letta, così potete estrarne il campo del nome e applicarvi le due funzioni qui sopra. La guida sulle scritture non latine mostra come raggruppare le letture e conservare entrambe le grafie. Se state ancora scegliendo un servizio, il confronto tra API OCR per passaporti mette fianco a fianco i prezzi pubblicati.

Se trovate un nome che il codice qui sopra legge in modo sbagliato, scrivete a admin@doc.cheap.

Una domanda per te: quale traslitterazione ti ha sorpreso di più confrontando una zona con il nome stampato? Raccontacelo nei commenti qui sotto.