Le nom est le champ le moins prévisible d'une zone de lecture automatique. Les dates font six chiffres, le numéro de document neuf caractères avec un chiffre de contrôle, et les codes viennent d'une liste fixe. Le nom, c'est la façon dont une personne s'appelle, comprimée dans un nombre fixe de lettres majuscules, sans espaces, sans accents, sans ponctuation et sans aucun chiffre de contrôle pour vous signaler qu'un caractère a été mal lu.
Cet article explique comment la zone écrit un nom, comment le redécouper en nom de famille et prénoms, ce que la translittération lui fait subir, comment savoir qu'il a pu être tronqué et comment le comparer avec le nom imprimé sur la page. Tous les noms ci-dessous sont inventés ou sont le spécimen de l'OACI elle-même, Anna Maria Eriksson ; aucune personne réelle n'apparaît.
Ceci est le blog de doc.cheap, une API OCR pour passeports et pièces d'identité. Rien de ce qui suit n'en a besoin ; le code n'a aucune dépendance.
Où se trouve le nom
Le Doc 9303 de l'OACI, la norme des documents de voyage lisibles à la machine, donne au nom un champ de largeur fixe dans chacun de ses trois formats. Les positions commencent ici à 1, comme les compte la norme :
| Format | Emplacement du nom | Largeur |
|---|---|---|
| TD3 (passeports), 2 × 44 | ligne 1, positions 6–44 | 39 caractères |
| TD2, 2 × 36 | ligne 1, positions 6–36 | 31 caractères |
| TD1 (cartes d'identité), 3 × 30 | ligne 3, toute la ligne | 30 caractères |
Les cinq premiers caractères de la ligne 1 d'un TD3 ou d'un TD2 sont le code du document et l'État émetteur ; le nom occupe tout ce qui suit. Le TD1 donne au nom une ligne à part entière, et c'est pourtant le plus étroit des trois. La page des formats MRZ détaille chaque format champ par champ.
Sur le spécimen TD3, la ligne 1 est P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<. Sans P<UTO, le champ du nom est ERIKSSON<<ANNA<MARIA, suivi de caractères de remplissage jusqu'à 39 caractères.
La grammaire : <<, < et le remplissage
L'alphabet de la zone est A–Z, 0–9 et le caractère de remplissage <. Un nom y ajoute trois conventions :
- L'identifiant primaire vient en premier. C'est en général le nom de famille.
- Deux caractères de remplissage,
<<, terminent l'identifiant primaire. Ce qui suit est l'identifiant secondaire, en général les prénoms. - Un caractère de remplissage,
<, sépare les mots à l'intérieur de chacune des deux parties.ANNA<MARIAcorrespond à deux prénoms ;VAN<DER<BERGest un nom de famille de trois mots.
Ce qui reste du champ après le nom est rempli de <. Relire un nom se fait donc en trois étapes : retirer le remplissage final, couper au premier << et transformer les < isolés restants en espaces.
Deux formes paraissent étranges sans être des erreurs. Une personne qui n'a qu'un seul nom n'a qu'un identifiant primaire : le champ contient le nom puis le remplissage, sans << devant : OKONKWO<<<<…. Un champ écrit avec le séparateur et rien derrière, NAKAMURA<<<<…, se lit exactement de la même façon une fois le remplissage final retiré. Dans les deux cas, l'identifiant secondaire est vide, pas absent.
Ce qui ne survit pas, c'est la ponctuation et l'espace lui-même. Un trait d'union, une apostrophe ou un espace ne peuvent pas s'écrire dans la zone ; un Smith-Jones imprimé arrive donc sous la forme SMITH<JONES, impossible à distinguer d'un nom de famille de deux mots. Si vous avez besoin de la ponctuation, elle se trouve sur la page imprimée, pas dans la zone.
La translittération : de n'importe quelle écriture à 26 lettres
Tout nom doit finir en A–Z. Pour un nom imprimé en lettres latines avec des diacritiques, cela veut dire convertir chaque lettre accentuée en lettres simples. Pour un nom en grec, en arabe, en cyrillique, en caractères han ou dans toute autre écriture, cela veut dire écrire le nom entier en lettres latines.
La partie 3 du Doc 9303, celle qui est commune à tous les formats, propose des translittérations recommandées pour les deux cas. Mais la décision revient à l'État émetteur, et tous les États ne font pas le même choix. Trois conséquences comptent pour le code :
- Un même nom imprimé peut avoir plusieurs graphies dans la zone. Un
MÜLLERimprimé peut apparaître dans une zone sous la formeMUELLERouMULLER, selon les règles de l'émetteur. Un code qui attend une correspondance unique et fixe jugera fausse une zone correcte. - La correspondance n'est pas réversible.
MULLERdans une zone peut provenir deMULLER,MÜLLERouMÚLLERsur la page. Une zone vous dit quelles lettres elle contient, pas comment la personne écrit son nom. - Le nom latin de la page peut différer de celui de la zone. Un document imprimé dans une écriture nationale imprime souvent aussi une version latine, et cette graphie latine imprimée ne correspond pas toujours lettre pour lettre à celle de la zone. Considérez une différence comme un point à examiner, pas comme la preuve d'une erreur de lecture.
La règle pratique : comparez un nom de la zone avec un nom imprimé après avoir réduit les deux à de simples lettres, acceptez les graphies que vos émetteurs utilisent réellement, et gardez la graphie imprimée pour tout ce qu'une personne lit.
La troncature : quand le nom ne tient pas
Un nom long ne passe pas à la ligne suivante. Il est raccourci pour tenir dans le champ, et le nom complet reste sur la page imprimée. La norme fixe des règles pour raccourcir un nom, et un nom raccourci va normalement jusqu'au tout dernier caractère de son champ.
C'est le seul indice dont dispose un parser : si le dernier caractère du champ du nom est une lettre et non <, le nom a peut-être été tronqué. C'est « peut-être », pas « a été » : un nom exactement aussi long que le champ se termine lui aussi par une lettre. En revanche, un champ qui se termine par du remplissage n'a certainement pas été coupé.
La largeur compte. Prenons un titulaire inventé, Konstantin Friedrich Hartmann Schoenebeck. Dans un champ TD3 de 39 caractères :
HARTMANN<SCHOENEBECK<<KONSTANTIN<FRIEDR
Le dernier caractère est R, une lettre : le nom a peut-être été coupé, et c'est le cas, FRIEDRICH a perdu trois lettres. Le même nom coupé à la largeur TD1 de 30 ne garde que HARTMANN<SCHOENEBECK<<KONSTANT. Un système qui enregistre le nom de la zone comme nom du titulaire enregistre KONSTANT comme prénom.
Deux habitudes en découlent. N'utilisez jamais le nom de la zone comme nom affiché quand le champ est plein ; utilisez le nom imprimé. Et quand vous rapprochez un nom imprimé complet d'un nom de la zone, un nom imprimé qui commence par le nom de la zone est une correspondance si le champ était plein.
Un émetteur peut aussi raccourcir un nom autrement qu'en coupant la fin, par exemple en réduisant un second prénom à son initiale. Un test de préfixe signale un tel nom comme non concordant. Confiez ce cas à une personne plutôt que de rejeter le document.
Aucun chiffre de contrôle ne couvre le nom
Les chiffres de contrôle couvrent le numéro de document, les deux dates, le numéro personnel facultatif du TD3 et un chiffre composite sur la majeure partie de la ligne du bas. Aucun ne couvre le nom. Sur le TD3 et le TD2, le nom est sur la ligne 1, qu'aucun chiffre ne touche ; sur le TD1, il est sur la ligne 3, et le composite s'arrête à la ligne 2.
Une erreur de lecture dans un nom passe donc inaperçue. Un M lu comme N, ou un caractère de remplissage lu comme une lettre, donne toujours une zone dont l'arithmétique tombe juste. Le seul contrôle croisé possible pour un nom est la page imprimée : lisez les deux et comparez-les, c'est à cela que sert la fonction de comparaison plus bas. Le fonctionnement des chiffres de contrôle de la MRZ détaille l'arithmétique des champs qui, eux, sont couverts.
Découper le champ en Python
Aucune dépendance. La fonction reçoit uniquement le champ du nom, déjà extrait de sa ligne (line1[5:44] en TD3, line1[5:36] en TD2, line3 en 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 ne coupe qu'au premier << et renvoie une partie secondaire vide s'il n'y en a pas, ce qui correspond au cas du nom unique. Le motif refuse les chiffres à dessein : un 0 dans un champ de nom est un O mal lu, et il doit parvenir à une personne plutôt que d'être affiché comme un nom.
Le même découpage en 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 };
}
Comparer la zone avec le nom imprimé
Le nom imprimé provient de la zone visuelle, la partie de la page qu'une personne lit, avec ses accents et sa ponctuation. La fonction ci-dessous le réduit à de simples lettres majuscules et le compare avec une partie du nom de la zone. Elle renvoie l'un de trois mots : match, mismatch, ou unknown lorsque le nom imprimé contient une lettre qui ne se réduit pas d'elle-même à A–Z, comme Ø, Æ ou Ł.
import re
import unicodedata
# Les graphies à essayer en plus de la simple suppression de l'accent. Ces
# trois-là sont un exemple ; mettez celles qu'utilisent vos émetteurs.
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 # une lettre sans forme simple en A-Z : à confier à une personne
if letters == zone or (truncated and letters.startswith(zone)):
return "match"
verdict = "mismatch"
return verdict
NFKD décompose une lettre accentuée en la lettre simple et son accent, et l'accent est supprimé. Les espaces, traits d'union et apostrophes sont supprimés eux aussi, car la comparaison ne porte que sur les lettres : Smith-Jones et SMITH JONES deviennent tous deux SMITHJONES. Pour un nom imprimé uniquement dans une écriture non latine, comparez la zone avec la version latine imprimée sur la page, pas avec celle en écriture nationale ; l'écriture nationale renverrait ici unknown à chaque fois.
Cas de test
Tous les noms sont inventés, sauf le spécimen de l'OACI. Dans le premier tableau, chaque champ est complété par des < jusqu'à la largeur TD3 de 39 (en Python, name.ljust(39, "<")), sauf la dernière ligne, qui fait déjà exactement 39 caractères.
| Champ avant remplissage | Primaire | Secondaire | Peut-être tronqué |
|---|---|---|---|
ERIKSSON<<ANNA<MARIA |
ERIKSSON | ANNA MARIA | non |
VAN<DER<BERG<<PIETER |
VAN DER BERG | PIETER | non |
SMITH<JONES<<MARY |
SMITH JONES | MARY | non |
OKONKWO |
OKONKWO | (vide) | non |
NAKAMURA<< |
NAKAMURA | (vide) | non |
HARTMANN<SCHOENEBECK<<KONSTANTIN<FRIEDR |
HARTMANN SCHOENEBECK | KONSTANTIN FRIEDR | oui |
Et pour compare_name :
| Partie de la zone | Partie imprimée | Tronqué | Attendu |
|---|---|---|---|
ANNA MARIA |
Anna Maria |
non | match |
SMITH JONES |
Smith-Jones |
non | match |
MULLER |
Müller |
non | match |
MUELLER |
Müller |
non | match (via l'expansion) |
MILLER |
Müller |
non | mismatch |
KONSTANTIN FRIEDR |
Konstantin Friedrich |
oui | match |
KONSTANTIN FRIEDR |
Konstantin Friedrich |
non | mismatch |
SORENSEN |
Sørensen |
non | unknown |
La dernière ligne est voulue. Que Ø soit devenu O ou OE est le choix de l'émetteur, et une fonction qui devine se trompera pour quelqu'un ; ajoutez plutôt aux expansions la forme qu'utilisent vos émetteurs.
Ce que fait notre parser MRZ
Le parser MRZ de ce site lit un nom de la même façon : il retire le remplissage final, coupe au premier << et transforme les caractères de remplissage isolés en espaces ; un champ sans << est lu comme un nom de famille seul. Quand le nom va jusqu'au dernier caractère de son champ, il affiche un avertissement indiquant que le nom a peut-être été tronqué pour tenir, avec les positions qu'il occupe. Il s'exécute dans le navigateur : vous pouvez donc coller une zone synthétique et comparer sa lecture avec celle de votre propre code.
La place de tout cela dans un pipeline
Un service de reconnaissance hébergé lit à la fois la zone et la page imprimée ; la question est donc de savoir quel nom il vous transmet. Dans la réponse de doc.cheap, holder.surname, holder.given_names et holder.full_name contiennent la lecture latine fusionnée. Un nom imprimé dans une écriture nationale est une entrée distincte de fields : chaque lecture a sa propre entrée, la lecture latine par défaut a un id qui se termine par @0, et celle en écriture nationale indique sa language, par exemple Greek. La zone elle-même se trouve dans mrz.lines, exactement telle qu'elle a été lue, pour que vous puissiez en extraire le champ du nom et y appliquer les deux fonctions ci-dessus. Le guide sur les écritures non latines montre comment regrouper les lectures et conserver les deux graphies. Si vous en êtes encore au choix d'un service, le comparatif des API OCR de passeports met les prix publiés côte à côte.
Si vous trouvez un nom que le code ci-dessus lit mal, écrivez à admin@doc.cheap.
Une question pour vous : quelle translittération vous a le plus surpris en comparant une zone avec le nom imprimé ? Dites-le-nous dans les commentaires ci-dessous.