O nome é o campo menos previsível de uma zona de leitura mecânica. As datas têm seis dígitos, o número do documento tem nove caracteres com um dígito verificador e os códigos vêm de uma lista fechada. O nome é como cada pessoa se chama, espremido em um número fixo de letras maiúsculas, sem espaços, sem acentos, sem pontuação e sem nenhum dígito verificador que avise quando um caractere foi lido errado.
Este artigo mostra como a zona escreve um nome, como separá-lo de volta em sobrenome e prenomes, o que a transliteração faz com ele, como saber que ele pode ter sido cortado e como compará-lo com o nome impresso na página. Todos os nomes abaixo são inventados ou são o espécime da própria OACI, Anna Maria Eriksson; nenhuma pessoa real aparece.
Este é o blog do doc.cheap, uma API de OCR para passaportes e documentos de identidade. Nada do que vem a seguir depende dela; o código não tem dependências.
Onde fica o nome
O Doc 9303 da OACI, a norma para documentos de viagem de leitura mecânica, dá ao nome um campo de largura fixa em cada um dos seus três formatos. Aqui as posições começam em 1, do jeito que a norma conta:
| Formato | Onde fica o nome | Largura |
|---|---|---|
| TD3 (passaportes), 2 × 44 | linha 1, posições 6–44 | 39 caracteres |
| TD2, 2 × 36 | linha 1, posições 6–36 | 31 caracteres |
| TD1 (documentos de identidade), 3 × 30 | linha 3, a linha inteira | 30 caracteres |
Os cinco primeiros caracteres da linha 1 de um TD3 ou TD2 são o código do documento e o Estado emissor; o nome ocupa tudo o que vem depois. O TD1 dá ao nome uma linha própria e, mesmo assim, é o mais estreito dos três. A página de formatos de MRZ percorre cada formato campo por campo.
No espécime TD3, a linha 1 é P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<. Tirando P<UTO, o campo de nome é ERIKSSON<<ANNA<MARIA seguido de caracteres de preenchimento até 39 caracteres.
A gramática: <<, < e o preenchimento
O alfabeto da zona é A–Z, 0–9 e o caractere de preenchimento <. Um nome acrescenta três convenções:
- O identificador primário vem primeiro. Normalmente é o sobrenome.
- Dois preenchimentos,
<<, encerram o identificador primário. O que vem depois é o identificador secundário, normalmente os prenomes. - Um preenchimento,
<, separa as palavras dentro de qualquer uma das partes.ANNA<MARIAsão dois prenomes;VAN<DER<BERGé um sobrenome de três palavras.
O que sobra do campo depois do nome é preenchido com <. Então ler um nome são três passos: remover o preenchimento final, separar no primeiro << e transformar os < isolados restantes em espaços.
Duas formas parecem estranhas e não são erros. Uma pessoa com um único nome só tem identificador primário, então o campo é o nome e depois preenchimento, sem << antes: OKONKWO<<<<…. Um campo escrito com o separador e nada depois, NAKAMURA<<<<…, é lido exatamente da mesma forma depois de removido o preenchimento final. Nos dois casos o identificador secundário está vazio, não ausente.
O que não sobrevive é a pontuação e o próprio espaço. Um hífen, um apóstrofo ou um espaço não podem ser escritos na zona, então um Smith-Jones impresso chega como SMITH<JONES, indistinguível de um sobrenome de duas palavras. Se você precisa da pontuação, ela está na página impressa, não na zona.
Transliteração: de qualquer escrita para 26 letras
Todo nome precisa terminar em A–Z. Para um nome impresso em letras latinas com diacríticos, isso significa converter cada letra acentuada em letras simples. Para um nome em grego, árabe, cirílico, caracteres han ou qualquer outra escrita, significa escrever o nome inteiro em letras latinas.
A Parte 3 do Doc 9303, a parte comum a todos os formatos, traz transliterações recomendadas para os dois casos. Mas a decisão é do Estado emissor, e nem todos os Estados escolhem da mesma forma. Três consequências importam para o código:
- Um mesmo nome impresso pode ter mais de uma grafia na zona. Um
MÜLLERimpresso pode aparecer na zona comoMUELLERou comoMULLER, conforme as regras do emissor. O código que espera um único mapeamento fixo vai considerar errada uma zona correta. - O mapeamento não tem volta.
MULLERem uma zona pode vir deMULLER,MÜLLERouMÚLLERna página. Uma zona diz quais letras ela contém, não o nome que a pessoa escreve. - O nome latino da página pode ser diferente do da zona. Um documento impresso em uma escrita nacional costuma imprimir também uma versão latina, e essa grafia latina impressa nem sempre coincide letra por letra com a da zona. Trate uma diferença como algo a conferir, não como prova de leitura errada.
A regra prática: compare um nome da zona com um nome impresso depois de reduzir os dois a letras puras, aceite as grafias que seus emissores realmente usam e mantenha a grafia impressa para tudo o que uma pessoa vai ler.
Truncamento: quando o nome não cabe
Um nome longo não passa para outra linha. Ele é encurtado para caber no campo, e o nome completo continua na página impressa. A norma traz regras para encurtar um nome, e um nome encurtado normalmente vai até o último caractere do seu campo.
Isso dá o único sinal que um parser tem: se o último caractere do campo de nome é uma letra e não <, o nome pode ter sido truncado. É "pode", não "foi": um nome exatamente do tamanho do campo também termina em letra. Mas um campo que termina em preenchimento com certeza não foi cortado.
A largura importa. Pegue um titular inventado, Konstantin Friedrich Hartmann Schoenebeck. Em um campo TD3 de 39 caracteres:
HARTMANN<SCHOENEBECK<<KONSTANTIN<FRIEDR
O último caractere é R, uma letra, então o nome pode estar cortado, e está: FRIEDRICH perdeu três letras. O mesmo nome cortado na largura TD1 de 30 fica só com HARTMANN<SCHOENEBECK<<KONSTANT. Um sistema que grava o nome da zona como nome do titular grava KONSTANT como prenome.
Daí saem dois hábitos. Nunca use o nome da zona como nome de exibição quando o campo está cheio; use o impresso. E, ao comparar um nome impresso completo com um nome da zona, um nome impresso que começa com o nome da zona é uma correspondência se o campo estava cheio.
Um emissor também pode encurtar um nome de outras formas além de cortar o final, por exemplo reduzindo um segundo prenome à inicial. Um teste de prefixo aponta esse nome como divergente. Encaminhe esse caso a uma pessoa em vez de rejeitar o documento.
Nenhum dígito verificador cobre o nome
Os dígitos verificadores cobrem o número do documento, as duas datas, o número pessoal opcional do TD3 e um dígito composto sobre a maior parte da linha de baixo. Nenhum deles cobre o nome. No TD3 e no TD2 o nome está na linha 1, que nenhum dígito toca; no TD1 ele está na linha 3, e o dígito composto termina na linha 2.
Então um erro de leitura em um nome passa em silêncio. Um M lido como N, ou um preenchimento lido como letra, ainda resulta em uma zona cuja aritmética fecha. A única verificação cruzada para um nome é a página impressa: leia os dois e compare, que é para isso que serve a função de comparação mais abaixo. Como funcionam os dígitos verificadores da MRZ traz a aritmética dos campos que são cobertos.
Separando o campo em Python
Sem dependências. A função recebe só o campo de nome, já recortado da sua linha (line1[5:44] no TD3, line1[5:36] no TD2, line3 no 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 separa apenas no primeiro << e devolve uma parte secundária vazia quando não há nenhum, que é o caso do nome único. O padrão recusa dígitos de propósito: um 0 em um campo de nome é um O lido errado, e deve chegar a uma pessoa em vez de ser impresso como nome.
A mesma separação em 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 };
}
Comparando a zona com o nome impresso
O nome impresso vem da zona visual, a parte da página que uma pessoa lê, com seus acentos e sua pontuação. A função abaixo o reduz a letras maiúsculas puras e o compara com uma parte do nome da zona. Ela devolve uma de três palavras: match, mismatch, ou unknown quando o nome impresso contém uma letra que não se reduz sozinha a A–Z, como Ø, Æ ou Ł.
import re
import unicodedata
# As grafias a testar além da simples remoção do acento. Estas três são
# um exemplo; coloque as que seus emissores usam.
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 # uma letra sem forma simples em A-Z: deixe para uma pessoa
if letters == zone or (truncated and letters.startswith(zone)):
return "match"
verdict = "mismatch"
return verdict
NFKD separa uma letra acentuada na letra simples e no seu acento, e o acento é descartado. Espaços, hifens e apóstrofos também são descartados, porque a comparação é só sobre letras: Smith-Jones e SMITH JONES viram ambos SMITHJONES. Para um nome impresso apenas em uma escrita não latina, compare a zona com a versão latina que a página imprime, não com a da escrita nacional; a escrita nacional devolveria unknown aqui todas as vezes.
Casos de teste
Todos os nomes são inventados, exceto o espécime da OACI. Na primeira tabela, cada campo é completado com < até a largura TD3 de 39 (em Python, name.ljust(39, "<")), exceto a última linha, que já tem exatamente 39 caracteres.
| Campo antes do preenchimento | Primário | Secundário | Pode estar truncado |
|---|---|---|---|
ERIKSSON<<ANNA<MARIA |
ERIKSSON | ANNA MARIA | não |
VAN<DER<BERG<<PIETER |
VAN DER BERG | PIETER | não |
SMITH<JONES<<MARY |
SMITH JONES | MARY | não |
OKONKWO |
OKONKWO | (vazio) | não |
NAKAMURA<< |
NAKAMURA | (vazio) | não |
HARTMANN<SCHOENEBECK<<KONSTANTIN<FRIEDR |
HARTMANN SCHOENEBECK | KONSTANTIN FRIEDR | sim |
E para compare_name:
| Parte da zona | Parte impressa | Truncado | Esperado |
|---|---|---|---|
ANNA MARIA |
Anna Maria |
não | match |
SMITH JONES |
Smith-Jones |
não | match |
MULLER |
Müller |
não | match |
MUELLER |
Müller |
não | match (pela expansão) |
MILLER |
Müller |
não | mismatch |
KONSTANTIN FRIEDR |
Konstantin Friedrich |
sim | match |
KONSTANTIN FRIEDR |
Konstantin Friedrich |
não | mismatch |
SORENSEN |
Sørensen |
não | unknown |
A última linha é proposital. Se Ø virou O ou OE é escolha do emissor, e uma função que tenta adivinhar vai errar para alguém; acrescente às expansões a forma que seus emissores usam.
O que o nosso parser de MRZ faz
O parser de MRZ deste site lê um nome do mesmo jeito: remove o preenchimento final, separa no primeiro << e transforma os preenchimentos isolados em espaços; um campo sem << é lido como um sobrenome sozinho. Quando o nome vai até o último caractere do seu campo, ele mostra um aviso de que o nome pode ter sido truncado para caber, com as posições que ocupa. Ele roda no navegador, então você pode colar uma zona sintética e comparar a leitura dele com o seu próprio código.
Onde isso entra em um pipeline
Um serviço de reconhecimento hospedado lê tanto a zona quanto a página impressa, então a pergunta é qual nome ele entrega a você. Na resposta do doc.cheap, holder.surname, holder.given_names e holder.full_name trazem a leitura latina combinada. Um nome impresso em uma escrita nacional é uma entrada separada de fields: cada leitura tem a sua própria entrada, a latina padrão tem um id que termina em @0 e a da escrita nacional informa o seu language, por exemplo Greek. A zona em si fica em mrz.lines, exatamente como foi lida, para que você possa recortar o campo de nome e aplicar as duas funções acima. O guia sobre escritas não latinas mostra como agrupar as leituras e guardar as duas grafias. Se você ainda está escolhendo um serviço, a comparação de APIs de OCR de passaportes coloca os preços publicados lado a lado.
Se você encontrar um nome que o código acima lê errado, escreva para admin@doc.cheap.
Uma pergunta para você: qual transliteração mais surpreendeu você ao comparar uma zona com o nome impresso? Conte para nós nos comentários abaixo.