A checksum is not enough: detecting Indian PII in the real world
Aadhaar numbers end in a Verhoeff check digit, so it is tempting to think detecting them is solved: match twelve digits, run the checksum, done. It is not. About one random 12-digit number in ten passes the check.
What the checksum actually tells you
A check digit exists to catch typing mistakes: a wrong digit, two digits swapped. It was never designed to tell an identity number apart from every other number that happens to have twelve digits. With ten possible check digits, roughly one number in ten will satisfy it by chance.
We measured it. Our script draws one million uniformly random 12-digit strings with a fixed seed and applies the same rules a regex-plus-checksum detector uses (they match the ones in Microsoft Presidio's Aadhaar recognizer: twelve digits, first digit 2 to 9, Verhoeff valid, not a palindrome).
A second run is worse for anyone handling customer data: 9.99% of Indian mobile numbers written as 91 plus ten digits, with no separator, also pass the Aadhaar checksum. A detector that trusts the checksum alone will flag one phone number in ten as an Aadhaar number, and will flag order IDs, UTRs and invoice numbers at the same rate. Raise the bar to cut those false alarms and you start missing real Aadhaar numbers written with spaces, in Devanagari digits or off a scanned card.
The script is in our repository; rerun it with any seed.
The other identifiers are no easier
- Aadhaar Virtual ID. Sixteen digits ending in a Verhoeff digit, the same length as a payment card number checked with Luhn. Only the surrounding words tell them apart.
- Masked Aadhaar. In UIDAI's masked Aadhaar, only the last four digits of the number are visible. There is nothing left for a checksum to validate; whether to redact it is a policy choice.
- PAN. Ten characters with a holder-type letter in fourth place (P for a person, C for a company and so on), but no publicly documented check algorithm. Presidio's PAN recognizer is three regular expressions with fixed scores.
- GSTIN. Fifteen characters with a real mod-36 check character, yet Presidio's recognizer only checks that the last character is alphanumeric and never computes it. A GSTIN with a valid check character can still be cancelled.
- IFSC and UPI. IFSC has a fixed shape (four letters, a zero, six characters) and no checksum. UPI handles have no published grammar at all.
Structure helps, but in both directions it is not enough: valid-looking numbers that are not identifiers, and real identifiers with nothing to validate.
Where real text breaks the rules
The numbers that matter rarely arrive in a clean form field. They arrive in support chats, call transcripts, scanned forms and emails, and Indian text is mixed. One peer-reviewed study of Indian tweets found 11.45% were code-mixed, and Hindi-English writers switch script inside a sentence. In practice that means:
- Hinglish labels. “aadhar”, “adhaar”, “mera aadhaar” and “आधार”. Presidio's Aadhaar context words are “aadhaar” and “uidai”, so none of the others raise its score.
- Indian digits. २३४५ ६७८९ ०१२४ is the same number in Devanagari numerals and will not match
[0-9]. - OCR errors. A scanned card turns 0 into O and 8 into B, so the checksum fails on a real number until the errors are repaired.
- Look-alikes next to the real thing. “Order ID 234567890124” passes the checksum. A detector has to read the label next to it.
Order ID 234567890124 shipped → unchanged: the nearest label says "order"
mera aadhaar 2345 6789 0124 hai → mera aadhaar [AADHAAR_1] hai
AADHAAR: 2345 67B9 O124 → [AADHAAR_1] (OCR repaired, then checked)
That is how Saaf works: normalise scripts and digits first, check every candidate with its checksum, then let the nearest label decide, so a valid number under “order ID” stays and the same digits under “aadhaar” are masked. Try it in the playground with the “Look-alikes” and “OCR scan” samples.
What the tools do today
| Tool | Indian coverage out of the box |
|---|---|
| Microsoft Presidio | Six India recognizers, all disabled in the default configuration and set up for English only |
| AWS Comprehend PII | Four India types; PII detection in English and Spanish only |
| Google Sensitive Data Protection | Four India infoTypes: Aadhaar, GST, PAN and passport |
| Saaf | 22 types with checksums, Hinglish and Indian-script context, OCR repair |
Names and addresses need a model, not a pattern, and the best open option for Indian languages degrades quickly. AI4Bharat's IndicNER scores F1 around 80 to 83 for Hindi, Bengali and Telugu, but about 66 for Assamese and 26 to 39 for Odia, on silver test sets, and it covers only people, places and organisations.
On IndicPII-Bench, every tool we tested, including Saaf, scores near zero on Indian names in Indian scripts. It is the next thing we are working on.
No Indian benchmark, so we built one
We could not find an established benchmark for Indian personal data. The closest public work is a 17,000-sentence semi-synthetic set spanning India, the UK and the US, plus a few small models with self-reported scores that cannot be compared with each other.
So we built IndicPII-Bench: 2,500 documents across structured IDs, Hinglish, four Indian scripts, real bank codes, obfuscation and look-alike numbers, plus Hindi, Marathi, Tamil and Telugu name slices drawn from the public Naamapadam and HiNER datasets. We ran Saaf against Presidio with all six India recognizers switched on and its threshold tuned, because comparing against a bare regex proves nothing. Saaf scored 0.711 strict F1 to Presidio's 0.437, with 0.35 false alarms per thousand tokens on look-alikes against 131.
Those numbers come with a warning we would want from anyone else: most of the corpus is synthetic, and Saaf's rules were tuned on the same families, so its scores are optimistic. The human-labelled, held-out set comes next, and we will publish it whether or not Saaf wins.
Sources
- Safearly, verhoeff_collisions.py (seed 2026, 1,000,000 samples).
- Microsoft Presidio, default_recognizers.yaml, in_aadhaar_recognizer.py and in_pan_recognizer.py.
- UIDAI, My Aadhaar: masked Aadhaar shows only the last four digits.
- The News Minute, how the Aadhaar Virtual ID works.
- python-stdnum, GSTIN check character.
- AWS, Detecting PII entities; Google Cloud, infoType reference.
- MMT code-mixed tweets dataset (arXiv 2304.00634).
- Mhaske et al., Naamapadam (ACL 2023), AI4Bharat.
- arXiv 2504.12308, multi-jurisdiction PII dataset.
Facts checked against the linked sources on 26 October 2026. Found an error? Email lalit@safearly.com and we will correct it and say so here.