Nama adalah kolom yang paling sulit ditebak di zona yang dapat dibaca mesin. Tanggal berupa enam digit, nomor dokumen berupa sembilan karakter dengan digit pemeriksa, dan kode diambil dari daftar yang tetap. Nama adalah apa pun sebutan seseorang, dijejalkan ke sejumlah huruf kapital yang tetap, tanpa spasi, tanpa aksen, tanpa tanda baca, dan tanpa digit pemeriksa yang memberi tahu Anda ketika sebuah karakter salah terbaca.

Tulisan ini membahas cara zona menulis nama, cara memisahkannya kembali menjadi nama keluarga dan nama depan, apa yang dilakukan transliterasi terhadapnya, cara mengetahui bahwa nama itu mungkin terpotong, dan cara membandingkannya dengan nama yang tercetak di halaman. Setiap nama di bawah adalah rekaan atau spesimen milik ICAO sendiri, Anna Maria Eriksson; tidak ada orang sungguhan yang muncul.

Ini adalah blog doc.cheap, API OCR Paspor dan Kartu Identitas. Tidak ada bagian di bawah yang membutuhkannya; kodenya tanpa dependensi.

Di mana nama berada

ICAO Doc 9303, standar untuk dokumen perjalanan yang dapat dibaca mesin, memberi nama sebuah kolom dengan lebar tetap di masing-masing dari tiga tata letaknya. Posisi di sini dimulai dari 1, seperti cara standar menghitungnya:

Format Letak nama Lebar
TD3 (paspor), 2 × 44 baris 1, posisi 6–44 39 karakter
TD2, 2 × 36 baris 1, posisi 6–36 31 karakter
TD1 (kartu identitas), 3 × 30 baris 3, seluruh baris 30 karakter

Lima karakter pertama di baris 1 TD3 atau TD2 adalah kode dokumen dan negara penerbit; nama mengisi semua yang ada setelahnya. TD1 memberi nama satu baris tersendiri, dan tetap saja itu yang paling sempit dari ketiganya. Halaman format MRZ membahas setiap tata letak kolom demi kolom.

Pada spesimen TD3, baris 1 adalah P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<. Buang P<UTO, dan kolom namanya adalah ERIKSSON<<ANNA<MARIA diikuti karakter pengisi hingga 39 karakter.

Tata bahasanya: <<, <, dan pengisi

Alfabet zona adalah A–Z, 0–9, dan karakter pengisi <. Nama memakai tiga konvensi di atasnya:

  • Identifikator primer ditulis lebih dulu. Biasanya itu nama keluarga.
  • Dua pengisi, <<, mengakhiri identifikator primer. Yang menyusul adalah identifikator sekunder, biasanya nama depan.
  • Satu pengisi, <, memisahkan kata di dalam salah satu bagian. ANNA<MARIA adalah dua nama depan; VAN<DER<BERG adalah nama keluarga tiga kata.

Sisa kolom setelah nama diisi dengan <. Jadi membaca kembali sebuah nama terdiri dari tiga langkah: buang pengisi di akhir, pisahkan pada << pertama, dan ubah < tunggal yang tersisa menjadi spasi.

Dua bentuk terlihat aneh padahal bukan kesalahan. Orang yang hanya punya satu nama hanya memiliki identifikator primer, sehingga kolomnya berisi nama lalu pengisi, tanpa << di depannya: OKONKWO<<<<…. Kolom yang ditulis dengan pemisah tanpa apa pun di belakangnya, NAKAMURA<<<<…, terbaca persis sama setelah pengisi di akhir dibuang. Dalam kedua kasus, identifikator sekunder kosong, bukan hilang.

Yang tidak bertahan adalah tanda baca dan spasi itu sendiri. Tanda hubung, apostrof, atau spasi tidak bisa ditulis di zona, sehingga Smith-Jones yang tercetak tiba sebagai SMITH<JONES, tidak bisa dibedakan dari nama keluarga dua kata. Jika Anda membutuhkan tanda bacanya, itu ada di halaman cetak, bukan di zona.

Transliterasi: dari aksara apa pun ke 26 huruf

Setiap nama harus berakhir dalam A–Z. Untuk nama yang dicetak dalam huruf Latin dengan diakritik, artinya setiap huruf beraksen dipetakan ke huruf polos. Untuk nama dalam aksara Yunani, Arab, Sirilik, Han, atau aksara lain, artinya seluruh nama ditulis dalam huruf Latin.

Doc 9303 Part 3, bagian yang berlaku untuk semua format, memuat transliterasi yang direkomendasikan untuk kedua kasus. Namun keputusannya ada di tangan negara penerbit, dan tidak semua negara memilih dengan cara yang sama. Tiga akibatnya penting bagi kode:

  1. Satu nama tercetak bisa punya lebih dari satu ejaan di zona. MÜLLER yang tercetak bisa muncul di zona sebagai MUELLER atau MULLER, tergantung aturan penerbit. Kode yang mengharapkan satu pemetaan tetap akan menganggap salah zona yang benar.
  2. Pemetaannya tidak bisa dibalik. MULLER di zona bisa berasal dari MULLER, MÜLLER, atau MÚLLER di halaman. Zona memberi tahu huruf apa yang dimuatnya, bukan nama yang ditulis orang itu.
  3. Nama Latin di halaman bisa berbeda dari zona. Dokumen yang dicetak dalam aksara nasional sering juga mencetak versi Latin, dan ejaan Latin tercetak itu tidak selalu sama huruf demi huruf dengan yang ada di zona. Anggap perbedaan sebagai sesuatu yang perlu diperiksa, bukan bukti salah baca.

Aturan praktisnya: bandingkan nama di zona dengan nama tercetak setelah keduanya direduksi menjadi huruf polos, beri ruang untuk ejaan yang benar-benar dipakai penerbit Anda, dan simpan ejaan tercetak untuk apa pun yang dibaca manusia.

Pemotongan: ketika nama tidak muat

Nama yang panjang tidak berlanjut ke baris lain. Nama itu dipendekkan agar muat di kolom, dan nama lengkapnya tetap ada di halaman cetak. Standar memberikan aturan cara memendekkan nama, dan nama yang dipendekkan biasanya terisi sampai karakter terakhir kolomnya.

Itulah satu-satunya sinyal yang dimiliki parser: jika karakter terakhir kolom nama adalah huruf, bukan <, nama itu mungkin terpotong. Ini "mungkin", bukan "pasti": nama yang panjangnya persis sama dengan kolom juga berakhir dengan huruf. Tetapi kolom yang berakhir dengan pengisi pasti tidak terpotong.

Lebar kolom berpengaruh. Ambil pemegang rekaan, Konstantin Friedrich Hartmann Schoenebeck. Dalam kolom TD3 selebar 39 karakter:

HARTMANN<SCHOENEBECK<<KONSTANTIN<FRIEDR

Karakter terakhir adalah R, sebuah huruf, jadi nama itu mungkin terpotong, dan memang terpotong: FRIEDRICH kehilangan tiga huruf. Nama yang sama yang dipotong pada lebar TD1 sebesar 30 hanya menyisakan HARTMANN<SCHOENEBECK<<KONSTANT. Sistem yang menyimpan nama zona sebagai nama pemegang akan menyimpan KONSTANT sebagai nama depan.

Dari sini lahir dua kebiasaan. Jangan pernah memakai nama zona sebagai nama tampilan ketika kolomnya penuh; pakai yang tercetak. Dan ketika Anda mencocokkan nama tercetak lengkap dengan nama zona, nama tercetak yang diawali oleh nama zona dianggap cocok jika kolomnya penuh.

Penerbit juga bisa memendekkan nama dengan cara selain memotong bagian akhir, misalnya dengan menyingkat nama depan kedua menjadi inisialnya. Uji awalan akan melaporkan nama seperti itu sebagai tidak cocok. Teruskan kasus itu ke manusia alih-alih menolak dokumennya.

Tidak ada digit pemeriksa yang mencakup nama

Digit pemeriksa mencakup nomor dokumen, kedua tanggal, nomor pribadi opsional pada TD3, dan digit gabungan yang mencakup sebagian besar baris bawah. Tidak satu pun yang mencakup nama. Pada TD3 dan TD2, nama ada di baris 1, yang sama sekali tidak tersentuh digit mana pun; pada TD1 nama ada di baris 3, dan digit gabungan berhenti di baris 2.

Jadi salah baca pada nama terjadi tanpa suara. M yang terbaca sebagai N, atau pengisi yang terbaca sebagai huruf, tetap menghasilkan zona yang aritmetikanya seimbang. Satu-satunya pemeriksaan silang untuk nama adalah halaman cetak: baca keduanya dan bandingkan, dan itulah gunanya fungsi pembanding di bawah. Cara kerja digit pemeriksa MRZ memuat aritmetika untuk kolom-kolom yang dicakup.

Memisahkan kolom di Python

Tanpa dependensi. Fungsi ini hanya menerima kolom nama, yang sudah dipotong dari barisnya (line1[5:44] pada TD3, line1[5:36] pada TD2, line3 pada 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 hanya memisahkan pada << pertama dan mengembalikan bagian sekunder kosong jika tidak ada, yaitu kasus nama tunggal. Polanya sengaja menolak digit: 0 di kolom nama adalah O yang salah terbaca, dan harus sampai ke manusia alih-alih dicetak sebagai nama.

Pemisahan yang sama di 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 };
}

Membandingkan zona dengan nama tercetak

Nama tercetak berasal dari zona visual, bagian halaman yang dibaca manusia, lengkap dengan aksen dan tanda bacanya. Fungsi di bawah mereduksinya menjadi huruf kapital polos dan membandingkannya dengan satu bagian nama zona. Fungsi ini mengembalikan salah satu dari tiga kata: match, mismatch, atau unknown ketika nama tercetak memuat huruf yang tidak bisa direduksi ke A–Z dengan sendirinya, seperti Ø, Æ, atau Ł.

import re
import unicodedata

# Ejaan yang dicoba selain sekadar membuang aksen. Ketiganya hanya
# contoh; isi dengan yang dipakai penerbit Anda.
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  # huruf tanpa bentuk polos A-Z: serahkan ke manusia
        if letters == zone or (truncated and letters.startswith(zone)):
            return "match"
        verdict = "mismatch"
    return verdict

NFKD memecah huruf beraksen menjadi huruf polos dan aksennya, lalu aksennya dibuang. Spasi, tanda hubung, dan apostrof juga dibuang, karena perbandingannya hanya pada huruf: Smith-Jones dan SMITH JONES sama-sama menjadi SMITHJONES. Untuk nama yang hanya dicetak dalam aksara non-Latin, bandingkan zona dengan versi Latin yang dicetak di halaman, bukan dengan versi aksara nasional; aksara nasional akan selalu mengembalikan unknown di sini.

Vektor uji

Setiap nama adalah rekaan kecuali spesimen ICAO. Di tabel pertama, setiap kolom diisi < hingga lebar TD3 sebesar 39 (di Python, name.ljust(39, "<")), kecuali baris terakhir, yang sudah tepat 39 karakter.

Kolom sebelum diisi Primer Sekunder Mungkin terpotong
ERIKSSON<<ANNA<MARIA ERIKSSON ANNA MARIA tidak
VAN<DER<BERG<<PIETER VAN DER BERG PIETER tidak
SMITH<JONES<<MARY SMITH JONES MARY tidak
OKONKWO OKONKWO (kosong) tidak
NAKAMURA<< NAKAMURA (kosong) tidak
HARTMANN<SCHOENEBECK<<KONSTANTIN<FRIEDR HARTMANN SCHOENEBECK KONSTANTIN FRIEDR ya

Dan untuk compare_name:

Bagian zona Bagian tercetak Terpotong Hasil yang diharapkan
ANNA MARIA Anna Maria tidak match
SMITH JONES Smith-Jones tidak match
MULLER Müller tidak match
MUELLER Müller tidak match (melalui ekspansi)
MILLER Müller tidak mismatch
KONSTANTIN FRIEDR Konstantin Friedrich ya match
KONSTANTIN FRIEDR Konstantin Friedrich tidak mismatch
SORENSEN Sørensen tidak unknown

Baris terakhir disengaja. Apakah Ø menjadi O atau OE adalah pilihan penerbit, dan fungsi yang menebak akan salah bagi seseorang; tambahkan bentuk yang dipakai penerbit Anda ke ekspansi.

Apa yang dilakukan parser MRZ kami

Parser MRZ di situs ini membaca nama dengan cara yang sama: membuang pengisi di akhir, memisahkan pada << pertama, dan mengubah pengisi tunggal menjadi spasi; kolom tanpa << dibaca sebagai nama keluarga saja. Ketika nama terisi sampai karakter terakhir kolomnya, parser menampilkan peringatan bahwa nama itu mungkin dipotong agar muat, beserta posisi yang diisinya. Parser berjalan di browser, jadi Anda bisa menempelkan zona sintetis dan membandingkan hasil bacaannya dengan kode Anda sendiri.

Posisinya dalam sebuah pipeline

Layanan pengenalan yang di-hosting membaca zona sekaligus halaman cetak, jadi pertanyaannya adalah nama mana yang diserahkannya kepada Anda. Dalam respons doc.cheap, holder.surname, holder.given_names, dan holder.full_name memuat bacaan Latin gabungan. Nama yang dicetak dalam aksara nasional adalah entri terpisah di fields: setiap bacaan punya entrinya sendiri, bacaan Latin bawaan punya id yang berakhiran @0, dan bacaan aksara nasional menyebutkan language-nya, misalnya Greek. Zonanya sendiri ada di mrz.lines, persis seperti terbaca, sehingga Anda bisa memotong kolom nama dan menjalankan kedua fungsi di atas padanya. Panduan tentang aksara non-Latin menunjukkan cara mengelompokkan bacaan dan menyimpan kedua ejaan. Jika Anda masih memilih layanan, perbandingan API OCR paspor menyandingkan harga yang dipublikasikan.

Jika Anda menemukan nama yang salah dibaca oleh kode di atas, tulis ke admin@doc.cheap.

Satu pertanyaan untuk Anda: transliterasi mana yang paling mengejutkan Anda saat membandingkan zona dengan nama tercetak? Ceritakan di kolom komentar di bawah.