id-extract 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. id_extract/__init__.py +26 -0
  2. id_extract/checksums.py +472 -0
  3. id_extract/countries/__init__.py +1 -0
  4. id_extract/countries/ar.py +7 -0
  5. id_extract/countries/at.py +7 -0
  6. id_extract/countries/au.py +9 -0
  7. id_extract/countries/be.py +7 -0
  8. id_extract/countries/br.py +9 -0
  9. id_extract/countries/ca.py +15 -0
  10. id_extract/countries/ch.py +8 -0
  11. id_extract/countries/cn.py +9 -0
  12. id_extract/countries/de.py +10 -0
  13. id_extract/countries/dk.py +7 -0
  14. id_extract/countries/es.py +11 -0
  15. id_extract/countries/fi.py +7 -0
  16. id_extract/countries/fr.py +10 -0
  17. id_extract/countries/gb.py +10 -0
  18. id_extract/countries/gr.py +7 -0
  19. id_extract/countries/hk.py +7 -0
  20. id_extract/countries/id.py +7 -0
  21. id_extract/countries/ie.py +7 -0
  22. id_extract/countries/il.py +7 -0
  23. id_extract/countries/in.py +10 -0
  24. id_extract/countries/it.py +9 -0
  25. id_extract/countries/jp.py +9 -0
  26. id_extract/countries/kr.py +8 -0
  27. id_extract/countries/mx.py +13 -0
  28. id_extract/countries/my.py +7 -0
  29. id_extract/countries/nl.py +8 -0
  30. id_extract/countries/no.py +7 -0
  31. id_extract/countries/nz.py +7 -0
  32. id_extract/countries/pl.py +7 -0
  33. id_extract/countries/pt.py +7 -0
  34. id_extract/countries/ru.py +7 -0
  35. id_extract/countries/se.py +7 -0
  36. id_extract/countries/sg.py +7 -0
  37. id_extract/countries/th.py +7 -0
  38. id_extract/countries/tr.py +7 -0
  39. id_extract/countries/tw.py +7 -0
  40. id_extract/countries/universal.py +24 -0
  41. id_extract/countries/us.py +28 -0
  42. id_extract/countries/za.py +8 -0
  43. id_extract/extract.py +120 -0
  44. id_extract/opt_in.py +185 -0
  45. id_extract/plugins.py +197 -0
  46. id_extract/types.py +50 -0
  47. id_extract-0.2.0.dist-info/METADATA +36 -0
  48. id_extract-0.2.0.dist-info/RECORD +50 -0
  49. id_extract-0.2.0.dist-info/WHEEL +5 -0
  50. id_extract-0.2.0.dist-info/top_level.txt +1 -0
id_extract/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ """Extract structured identifiers with RE2 and checksums.
2
+
3
+ This package finds placements. It does not replace text, build a mapping,
4
+ or open files. Documentation lives in the anonymizer monorepo
5
+ (https://leo-gan.github.io/anonymizer/).
6
+ """
7
+
8
+ from id_extract.extract import extract
9
+ from id_extract.opt_in import available_opt_in
10
+ from id_extract.plugins import (
11
+ available_countries,
12
+ filter_regex_patterns,
13
+ pattern_country,
14
+ patterns,
15
+ )
16
+ from id_extract.types import Entity
17
+
18
+ __all__ = [
19
+ "Entity",
20
+ "available_countries",
21
+ "available_opt_in",
22
+ "extract",
23
+ "filter_regex_patterns",
24
+ "pattern_country",
25
+ "patterns",
26
+ ]
@@ -0,0 +1,472 @@
1
+ """Cheap, unambiguous checksums for structured regex hits.
2
+
3
+ The regex stage is structural on purpose (RE2 cannot do Luhn, IBAN mod-97, etc.).
4
+ After a match, this module checks the extra digit when one exists.
5
+
6
+ - Check passes: keep the real type (``IBAN``).
7
+ - Check fails: keep the text, but relabel as ``IBAN_LIKE`` so a mistyped
8
+ number is still hidden. Never drop the hit.
9
+ - No registered check: accept the type unchanged.
10
+
11
+ Only attach a check when it is cheap and unambiguous. Do not invent rules for
12
+ identifiers that have none (most SSNs, many passports, most VAT numbers).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Callable, Dict
18
+
19
+ # Official IBAN character lengths by country code (ISO 13616).
20
+ # Unknown countries are rejected so random "ABxx..." tokens do not survive.
21
+ _IBAN_LENGTHS: Dict[str, int] = {
22
+ "AD": 24,
23
+ "AE": 23,
24
+ "AL": 28,
25
+ "AT": 20,
26
+ "AZ": 28,
27
+ "BA": 20,
28
+ "BE": 16,
29
+ "BG": 22,
30
+ "BH": 22,
31
+ "BR": 29,
32
+ "BY": 28,
33
+ "CH": 21,
34
+ "CR": 22,
35
+ "CY": 28,
36
+ "CZ": 24,
37
+ "DE": 22,
38
+ "DK": 18,
39
+ "DO": 28,
40
+ "EE": 20,
41
+ "EG": 29,
42
+ "ES": 24,
43
+ "FI": 18,
44
+ "FO": 18,
45
+ "FR": 27,
46
+ "GB": 22,
47
+ "GE": 22,
48
+ "GI": 23,
49
+ "GL": 18,
50
+ "GR": 27,
51
+ "GT": 28,
52
+ "HR": 21,
53
+ "HU": 28,
54
+ "IE": 22,
55
+ "IL": 23,
56
+ "IQ": 23,
57
+ "IS": 26,
58
+ "IT": 27,
59
+ "JO": 30,
60
+ "KW": 30,
61
+ "KZ": 20,
62
+ "LB": 28,
63
+ "LC": 32,
64
+ "LI": 21,
65
+ "LT": 20,
66
+ "LU": 20,
67
+ "LV": 21,
68
+ "LY": 25,
69
+ "MC": 27,
70
+ "MD": 24,
71
+ "ME": 22,
72
+ "MK": 19,
73
+ "MR": 27,
74
+ "MT": 31,
75
+ "MU": 30,
76
+ "NL": 18,
77
+ "NO": 15,
78
+ "PK": 24,
79
+ "PL": 28,
80
+ "PS": 29,
81
+ "PT": 25,
82
+ "QA": 29,
83
+ "RO": 24,
84
+ "RS": 22,
85
+ "SA": 24,
86
+ "SE": 24,
87
+ "SI": 19,
88
+ "SK": 24,
89
+ "SM": 27,
90
+ "TN": 24,
91
+ "TR": 26,
92
+ "UA": 29,
93
+ "VA": 22,
94
+ "VG": 24,
95
+ "XK": 20,
96
+ }
97
+
98
+ # ISO 3779 VIN transliteration (I, O, Q are not used).
99
+ _VIN_TRANSLIT = {
100
+ "A": 1,
101
+ "B": 2,
102
+ "C": 3,
103
+ "D": 4,
104
+ "E": 5,
105
+ "F": 6,
106
+ "G": 7,
107
+ "H": 8,
108
+ "J": 1,
109
+ "K": 2,
110
+ "L": 3,
111
+ "M": 4,
112
+ "N": 5,
113
+ "P": 7,
114
+ "R": 9,
115
+ "S": 2,
116
+ "T": 3,
117
+ "U": 4,
118
+ "V": 5,
119
+ "W": 6,
120
+ "X": 7,
121
+ "Y": 8,
122
+ "Z": 9,
123
+ }
124
+ _VIN_WEIGHTS = (8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2)
125
+
126
+ # Spanish DNI / NIE remainder -> letter.
127
+ _DNI_LETTERS = "TRWAGMYFPDXBNJZSQVHLCKE"
128
+ _NIE_PREFIX = {"X": "0", "Y": "1", "Z": "2"}
129
+
130
+ # Chinese Resident Identity Card (GB 11643-1999).
131
+ _CN_WEIGHTS = (7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2)
132
+ _CN_CHECK = "10X98765432"
133
+
134
+ # Verhoeff tables (Aadhaar).
135
+ _VERHOEFF_D = (
136
+ (0, 1, 2, 3, 4, 5, 6, 7, 8, 9),
137
+ (1, 2, 3, 4, 0, 6, 7, 8, 9, 5),
138
+ (2, 3, 4, 0, 1, 7, 8, 9, 5, 6),
139
+ (3, 4, 0, 1, 2, 8, 9, 5, 6, 7),
140
+ (4, 0, 1, 2, 3, 9, 5, 6, 7, 8),
141
+ (5, 9, 8, 7, 6, 0, 4, 3, 2, 1),
142
+ (6, 5, 9, 8, 7, 1, 0, 4, 3, 2),
143
+ (7, 6, 5, 9, 8, 2, 1, 0, 4, 3),
144
+ (8, 7, 6, 5, 9, 3, 2, 1, 0, 4),
145
+ (9, 8, 7, 6, 5, 4, 3, 2, 1, 0),
146
+ )
147
+ _VERHOEFF_P = (
148
+ (0, 1, 2, 3, 4, 5, 6, 7, 8, 9),
149
+ (1, 5, 7, 6, 2, 8, 3, 0, 9, 4),
150
+ (5, 8, 0, 3, 7, 9, 6, 1, 4, 2),
151
+ (8, 9, 1, 6, 0, 4, 3, 5, 2, 7),
152
+ (9, 4, 5, 3, 1, 2, 6, 8, 7, 0),
153
+ (4, 2, 8, 6, 5, 7, 3, 9, 0, 1),
154
+ (2, 7, 9, 3, 8, 0, 6, 4, 1, 5),
155
+ (7, 0, 4, 6, 9, 1, 3, 2, 5, 8),
156
+ )
157
+
158
+ # Italian codice fiscale: odd positions (1-based) and even positions.
159
+ _CF_ODD = {
160
+ "0": 1,
161
+ "1": 0,
162
+ "2": 5,
163
+ "3": 7,
164
+ "4": 9,
165
+ "5": 13,
166
+ "6": 15,
167
+ "7": 17,
168
+ "8": 19,
169
+ "9": 21,
170
+ "A": 1,
171
+ "B": 0,
172
+ "C": 5,
173
+ "D": 7,
174
+ "E": 9,
175
+ "F": 13,
176
+ "G": 15,
177
+ "H": 17,
178
+ "I": 19,
179
+ "J": 21,
180
+ "K": 2,
181
+ "L": 4,
182
+ "M": 18,
183
+ "N": 20,
184
+ "O": 11,
185
+ "P": 3,
186
+ "Q": 6,
187
+ "R": 8,
188
+ "S": 12,
189
+ "T": 14,
190
+ "U": 16,
191
+ "V": 10,
192
+ "W": 22,
193
+ "X": 25,
194
+ "Y": 24,
195
+ "Z": 23,
196
+ }
197
+ _CF_EVEN = {
198
+ **{str(i): i for i in range(10)},
199
+ **{chr(ord("A") + i): i for i in range(26)},
200
+ }
201
+
202
+
203
+ def _digits_only(text: str) -> str:
204
+ return "".join(ch for ch in text if ch.isdigit())
205
+
206
+
207
+ def _alnum_upper(text: str) -> str:
208
+ return "".join(ch for ch in text.upper() if ch.isalnum())
209
+
210
+
211
+ def luhn_ok(digits: str) -> bool:
212
+ """Return True if ``digits`` (0-9 only) passes the Luhn check."""
213
+ if not digits or not digits.isdigit():
214
+ return False
215
+ total = 0
216
+ # Double every second digit from the right.
217
+ reverse = digits[::-1]
218
+ for i, ch in enumerate(reverse):
219
+ n = ord(ch) - 48
220
+ if i % 2 == 1:
221
+ n *= 2
222
+ if n > 9:
223
+ n -= 9
224
+ total += n
225
+ return total % 10 == 0
226
+
227
+
228
+ def verhoeff_ok(digits: str) -> bool:
229
+ """Return True if ``digits`` (0-9 only) passes the Verhoeff check."""
230
+ if not digits or not digits.isdigit():
231
+ return False
232
+ checksum = 0
233
+ for i, ch in enumerate(reversed(digits)):
234
+ checksum = _VERHOEFF_D[checksum][_VERHOEFF_P[i % 8][ord(ch) - 48]]
235
+ return checksum == 0
236
+
237
+
238
+ def validate_credit_card(text: str) -> bool:
239
+ digits = _digits_only(text)
240
+ if not 13 <= len(digits) <= 19:
241
+ return False
242
+ return luhn_ok(digits)
243
+
244
+
245
+ def validate_npi(text: str) -> bool:
246
+ # CMS: Luhn over the prefix 80840 + the 10-digit NPI.
247
+ digits = _digits_only(text)
248
+ if len(digits) != 10:
249
+ return False
250
+ return luhn_ok("80840" + digits)
251
+
252
+
253
+ def validate_sin_ca(text: str) -> bool:
254
+ digits = _digits_only(text)
255
+ if len(digits) != 9:
256
+ return False
257
+ return luhn_ok(digits)
258
+
259
+
260
+ def validate_iban(text: str) -> bool:
261
+ compact = _alnum_upper(text)
262
+ if len(compact) < 5 or not compact[:2].isalpha() or not compact[2:4].isdigit():
263
+ return False
264
+ expected = _IBAN_LENGTHS.get(compact[:2])
265
+ if expected is None or len(compact) != expected:
266
+ return False
267
+ rearranged = compact[4:] + compact[:4]
268
+ numeric = []
269
+ for ch in rearranged:
270
+ if ch.isdigit():
271
+ numeric.append(ch)
272
+ else:
273
+ numeric.append(str(ord(ch) - 55)) # A=10 ... Z=35
274
+ return int("".join(numeric)) % 97 == 1
275
+
276
+
277
+ def validate_vin(text: str) -> bool:
278
+ vin = _alnum_upper(text)
279
+ if len(vin) != 17:
280
+ return False
281
+ total = 0
282
+ for i, ch in enumerate(vin):
283
+ if ch.isdigit():
284
+ value = ord(ch) - 48
285
+ else:
286
+ value = _VIN_TRANSLIT.get(ch)
287
+ if value is None:
288
+ return False
289
+ total += value * _VIN_WEIGHTS[i]
290
+ remainder = total % 11
291
+ expected = "X" if remainder == 10 else str(remainder)
292
+ return vin[8] == expected
293
+
294
+
295
+ def validate_dni_es(text: str) -> bool:
296
+ compact = _alnum_upper(text)
297
+ if len(compact) != 9 or not compact[:8].isdigit() or not compact[8].isalpha():
298
+ return False
299
+ return compact[8] == _DNI_LETTERS[int(compact[:8]) % 23]
300
+
301
+
302
+ def validate_nie_es(text: str) -> bool:
303
+ compact = _alnum_upper(text)
304
+ if len(compact) != 9 or compact[0] not in _NIE_PREFIX:
305
+ return False
306
+ return validate_dni_es(_NIE_PREFIX[compact[0]] + compact[1:])
307
+
308
+
309
+ def validate_resident_id_cn(text: str) -> bool:
310
+ compact = _alnum_upper(text)
311
+ if len(compact) != 18 or not compact[:17].isdigit():
312
+ return False
313
+ total = sum((ord(compact[i]) - 48) * _CN_WEIGHTS[i] for i in range(17))
314
+ return compact[17] == _CN_CHECK[total % 11]
315
+
316
+
317
+ def validate_aadhaar_in(text: str) -> bool:
318
+ digits = _digits_only(text)
319
+ if len(digits) != 12 or digits[0] in "01":
320
+ return False
321
+ return verhoeff_ok(digits)
322
+
323
+
324
+ def validate_cpf_br(text: str) -> bool:
325
+ digits = _digits_only(text)
326
+ if len(digits) != 11 or len(set(digits)) == 1:
327
+ return False
328
+
329
+ def _cpf_digit(body: str, start_weight: int) -> str:
330
+ total = sum((ord(ch) - 48) * (start_weight - i) for i, ch in enumerate(body))
331
+ remainder = total % 11
332
+ return "0" if remainder < 2 else str(11 - remainder)
333
+
334
+ if digits[9] != _cpf_digit(digits[:9], 10):
335
+ return False
336
+ return digits[10] == _cpf_digit(digits[:10], 11)
337
+
338
+
339
+ def validate_codice_fiscale_it(text: str) -> bool:
340
+ compact = _alnum_upper(text)
341
+ if len(compact) != 16:
342
+ return False
343
+ total = 0
344
+ for i, ch in enumerate(compact[:15]):
345
+ table = _CF_ODD if i % 2 == 0 else _CF_EVEN
346
+ value = table.get(ch)
347
+ if value is None:
348
+ return False
349
+ total += value
350
+ return compact[15] == chr(ord("A") + (total % 26))
351
+
352
+
353
+ def validate_pesel_pl(text: str) -> bool:
354
+ digits = _digits_only(text)
355
+ if len(digits) != 11:
356
+ return False
357
+ weights = (1, 3, 7, 9, 1, 3, 7, 9, 1, 3)
358
+ total = sum((ord(digits[i]) - 48) * weights[i] for i in range(10))
359
+ check = (10 - (total % 10)) % 10
360
+ return digits[10] == str(check)
361
+
362
+
363
+ def validate_rtn_us(text: str) -> bool:
364
+ """ABA routing number: weights 3, 7, 1 and a zero mod-10 total."""
365
+ digits = _digits_only(text)
366
+ if len(digits) != 9:
367
+ return False
368
+ prefix = int(digits[:2])
369
+ if not (prefix <= 12 or 21 <= prefix <= 32 or 61 <= prefix <= 72 or prefix == 80):
370
+ return False
371
+ weights = (3, 7, 1, 3, 7, 1, 3, 7, 1)
372
+ total = sum((ord(digits[i]) - 48) * weights[i] for i in range(9))
373
+ return total % 10 == 0
374
+
375
+
376
+ def validate_cusip(text: str) -> bool:
377
+ """CUSIP Modulus 10 double-add-double check digit (CGS / ANSI X9.6)."""
378
+ compact = _alnum_upper(text)
379
+ if len(compact) != 9 or not compact[8].isdigit():
380
+ return False
381
+ total = 0
382
+ for i, ch in enumerate(compact[:8]):
383
+ if ch.isdigit():
384
+ value = ord(ch) - 48
385
+ elif "A" <= ch <= "Z":
386
+ value = ord(ch) - ord("A") + 10
387
+ else:
388
+ return False
389
+ if i % 2 == 1:
390
+ value *= 2
391
+ total += value // 10 + value % 10
392
+ check = (10 - (total % 10)) % 10
393
+ return compact[8] == str(check)
394
+
395
+
396
+ def validate_phn_bc(text: str) -> bool:
397
+ """BC Personal Health Number: leading 9 and the Teleplan MOD-11 digit."""
398
+ digits = _digits_only(text)
399
+ if len(digits) != 10 or digits[0] != "9":
400
+ return False
401
+ weights = (2, 4, 8, 5, 10, 9, 7, 3)
402
+ total = sum((ord(digits[i + 1]) - 48) * weights[i] for i in range(8))
403
+ remainder = total % 11
404
+ expected = 0 if remainder == 0 else 11 - remainder
405
+ if expected == 10:
406
+ return False
407
+ return digits[9] == str(expected)
408
+
409
+
410
+ def validate_clabe_mx(text: str) -> bool:
411
+ """CLABE control digit: cyclic weights 3, 7, 1 on the first 17 digits."""
412
+ digits = _digits_only(text)
413
+ if len(digits) != 18:
414
+ return False
415
+ weights = (3, 7, 1)
416
+ total = 0
417
+ for i, ch in enumerate(digits[:17]):
418
+ total += ((ord(ch) - 48) * weights[i % 3]) % 10
419
+ check = (10 - (total % 10)) % 10
420
+ return digits[17] == str(check)
421
+
422
+
423
+ ValidatorFn = Callable[[str], bool]
424
+
425
+ # Keys must match entity TYPEs emitted by the regex stage (upper-case).
426
+ CHECKSUM_VALIDATORS: Dict[str, ValidatorFn] = {
427
+ "CREDIT_CARD": validate_credit_card,
428
+ "MEDICAL_NPI_US": validate_npi,
429
+ "SIN_CA": validate_sin_ca,
430
+ "IBAN": validate_iban,
431
+ "VIN": validate_vin,
432
+ "DNI_ES": validate_dni_es,
433
+ "NIE_ES": validate_nie_es,
434
+ "RESIDENT_ID_CN": validate_resident_id_cn,
435
+ "AADHAAR_IN": validate_aadhaar_in,
436
+ "CPF_BR": validate_cpf_br,
437
+ "CODICE_FISCALE_IT": validate_codice_fiscale_it,
438
+ "PESEL_PL": validate_pesel_pl,
439
+ "RTN_US": validate_rtn_us,
440
+ "CUSIP_NNA": validate_cusip,
441
+ "PHN_BC_CA": validate_phn_bc,
442
+ "CLABE_MX": validate_clabe_mx,
443
+ }
444
+
445
+ # These shapes are common digit runs. A failed check is dropped instead of
446
+ # kept as TYPE_LIKE, which is what the other validators do.
447
+ STRICT_CHECKSUMS = frozenset(
448
+ {
449
+ "RTN_US",
450
+ "CUSIP_NNA",
451
+ "PHN_BC_CA",
452
+ "CLABE_MX",
453
+ }
454
+ )
455
+
456
+
457
+ def has_checksum(entity_type: str) -> bool:
458
+ """Return True if this type has a registered extra-digit check."""
459
+ return entity_type.upper() in CHECKSUM_VALIDATORS
460
+
461
+
462
+ def strict_checksum(entity_type: str) -> bool:
463
+ """Return True when a failed check should drop the hit."""
464
+ return entity_type.upper() in STRICT_CHECKSUMS
465
+
466
+
467
+ def passes_checksum(entity_type: str, text: str) -> bool:
468
+ """Return True if ``text`` has no check, or if its check succeeds."""
469
+ validator = CHECKSUM_VALIDATORS.get(entity_type.upper())
470
+ if validator is None:
471
+ return True
472
+ return validator(text)
@@ -0,0 +1 @@
1
+ """Bundled country plugins. Universal patterns always load."""
@@ -0,0 +1,7 @@
1
+ """National-ID patterns for AR."""
2
+
3
+ CODE = "AR"
4
+
5
+ PATTERNS = {
6
+ "DNI_AR": "\\b\\d{8}\\b",
7
+ }
@@ -0,0 +1,7 @@
1
+ """National-ID patterns for AT."""
2
+
3
+ CODE = "AT"
4
+
5
+ PATTERNS = {
6
+ "SVNR_AT": "\\b\\d{10}\\b",
7
+ }
@@ -0,0 +1,9 @@
1
+ """National-ID patterns for AU."""
2
+
3
+ CODE = "AU"
4
+
5
+ PATTERNS = {
6
+ "TFN_AU": "\\b\\d{3}\\s?\\d{3}\\s?\\d{3}\\b",
7
+ "ABN_AU": "\\b\\d{2}\\s?\\d{3}\\s?\\d{3}\\s?\\d{3}\\b",
8
+ "DRIVERS_LICENSE_AU": "\\b[A-Z0-9]{8,10}\\b",
9
+ }
@@ -0,0 +1,7 @@
1
+ """National-ID patterns for BE."""
2
+
3
+ CODE = "BE"
4
+
5
+ PATTERNS = {
6
+ "NISS_BE": "\\b\\d{2}\\.\\d{2}\\.\\d{2}-\\d{3}\\.\\d{2}\\b|\\b\\d{11}\\b",
7
+ }
@@ -0,0 +1,9 @@
1
+ """National-ID patterns for BR."""
2
+
3
+ CODE = "BR"
4
+
5
+ PATTERNS = {
6
+ "CPF_BR": "\\b\\d{3}\\.?\\d{3}\\.?\\d{3}-?\\d{2}\\b",
7
+ "CNPJ_BR": "\\b\\d{2}\\.?\\d{3}\\.?\\d{3}/?\\d{4}-?\\d{2}\\b",
8
+ "RG_BR": "\\b\\d{2}\\.?\\d{3}\\.?\\d{3}-?[0-9X]\\b",
9
+ }
@@ -0,0 +1,15 @@
1
+ """National-ID patterns for CA."""
2
+
3
+ CODE = "CA"
4
+
5
+ PATTERNS = {
6
+ "SIN_CA": "\\b\\d{3}-\\d{3}-\\d{3}\\b",
7
+ "DRIVERS_LICENSE_CA": "\\b[A-Z]\\d{4,5}-\\d{5,6}-\\d{5}\\b|\\b[A-Z0-9]{5,15}\\b",
8
+ # CRA program account: 9-digit BN + program letters + 4-digit reference.
9
+ "PROGRAM_ACCOUNT_CA": "\\b\\d{9}\\s?(?:RT|RP|RC|RM|RZ|RR|RG)\\s?\\d{4}\\b",
10
+ "DIN_CA": "\\bDIN\\s?\\d{8}\\b",
11
+ "NPN_CA": "\\bNPN\\s?\\d{8}\\b",
12
+ "DIN_HM_CA": "\\bDIN-HM\\s?\\d{8}\\b",
13
+ # IRCC prints the 10-digit UCI as NN-NNNN-NNNN.
14
+ "UCI_CA": "\\b\\d{2}-\\d{4}-\\d{4}\\b",
15
+ }
@@ -0,0 +1,8 @@
1
+ """National-ID patterns for CH."""
2
+
3
+ CODE = "CH"
4
+
5
+ PATTERNS = {
6
+ "AHV_CH": "\\b756\\.\\d{4}\\.\\d{4}\\.\\d{2}\\b|\\b756\\d{10}\\b",
7
+ "VAT_CH": "\\bCHE\\d{9}(?:MWST|TVA|IVA)?\\b",
8
+ }
@@ -0,0 +1,9 @@
1
+ """National-ID patterns for CN."""
2
+
3
+ CODE = "CN"
4
+
5
+ PATTERNS = {
6
+ "RESIDENT_ID_CN": "\\b\\d{17}[\\dXx]\\b",
7
+ "UNIFIED_SOCIAL_CREDIT_CODE_CN": "\\b[A-Z0-9]{18}\\b",
8
+ "PASSPORT_CN": "\\bE\\d{8}\\b|\\bG\\d{8}\\b|\\bS\\d{8}\\b",
9
+ }
@@ -0,0 +1,10 @@
1
+ """National-ID patterns for DE."""
2
+
3
+ CODE = "DE"
4
+
5
+ PATTERNS = {
6
+ "STEUER_ID_DE": "\\b\\d{11}\\b",
7
+ "VAT_DE": "\\bDE\\d{9}\\b",
8
+ "PERSONALAUSWEIS_DE": "\\b[A-Z0-9]{9,10}\\b",
9
+ "DRIVERS_LICENSE_DE": "\\b[A-Z0-9]{11,12}\\b",
10
+ }
@@ -0,0 +1,7 @@
1
+ """National-ID patterns for DK."""
2
+
3
+ CODE = "DK"
4
+
5
+ PATTERNS = {
6
+ "CPR_DK": "\\b\\d{6}-\\d{4}\\b",
7
+ }
@@ -0,0 +1,11 @@
1
+ """National-ID patterns for ES."""
2
+
3
+ CODE = "ES"
4
+
5
+ PATTERNS = {
6
+ "DNI_ES": "\\b\\d{8}[A-HJ-NP-TV-Z]\\b",
7
+ "NIE_ES": "\\b[XYZ]\\d{7}[A-HJ-NP-TV-Z]\\b",
8
+ "CIF_ES": "\\b[A-HJ-NP-S]\\d{7}[A-J0-9]\\b",
9
+ "VAT_ES": "\\bES[A-Z0-9]\\d{7}[A-Z0-9]\\b",
10
+ "DRIVERS_LICENSE_ES": "\\b[A-Z0-9]{9,10}\\b",
11
+ }
@@ -0,0 +1,7 @@
1
+ """National-ID patterns for FI."""
2
+
3
+ CODE = "FI"
4
+
5
+ PATTERNS = {
6
+ "HETU_FI": "\\b\\d{6}[+\\-A]\\d{3}[0-9A-Z]\\b",
7
+ }
@@ -0,0 +1,10 @@
1
+ """National-ID patterns for FR."""
2
+
3
+ CODE = "FR"
4
+
5
+ PATTERNS = {
6
+ "INSEE_FR": "\\b[12]\\d{12,14}\\b",
7
+ "VAT_FR": "\\bFR[A-HJ-NP-Z0-9]{2}\\d{9}\\b",
8
+ "DRIVERS_LICENSE_FR": "\\b[A-Z0-9]{12}\\b",
9
+ "PASSPORT_FR": "\\b\\d{2}[A-Z]{2}\\d{5}\\b",
10
+ }
@@ -0,0 +1,10 @@
1
+ """National-ID patterns for GB."""
2
+
3
+ CODE = "GB"
4
+
5
+ PATTERNS = {
6
+ "NINO_GB": "\\b[A-CEGHJ-PR-TW-Z]{1}[A-CEGHJ-NPR-TW-Z]{1}\\d{6}[A-DFM]?\\b",
7
+ "DRIVERS_LICENSE_GB": "\\b[A-Z9]{5}\\d{6}[A-Z9]{2}\\d[A-Z]{2}\\b",
8
+ "VAT_GB": "\\bGB\\d{9}\\b|\\bGB\\d{12}\\b|\\bGBGD\\d{3}\\b|\\bGBHA\\d{3}\\b",
9
+ "COMPANIES_HOUSE_GB": "\\b(?:SC|NI|OC|SO)?\\d{6,8}\\b",
10
+ }
@@ -0,0 +1,7 @@
1
+ """National-ID patterns for GR."""
2
+
3
+ CODE = "GR"
4
+
5
+ PATTERNS = {
6
+ "AMKA_GR": "\\b\\d{11}\\b",
7
+ }
@@ -0,0 +1,7 @@
1
+ """National-ID patterns for HK."""
2
+
3
+ CODE = "HK"
4
+
5
+ PATTERNS = {
6
+ "HKID_HK": "\\b[A-Z]{1,2}\\d{6}[0-9A]\\b",
7
+ }
@@ -0,0 +1,7 @@
1
+ """National-ID patterns for ID."""
2
+
3
+ CODE = "ID"
4
+
5
+ PATTERNS = {
6
+ "NIK_ID": "\\b\\d{16}\\b",
7
+ }
@@ -0,0 +1,7 @@
1
+ """National-ID patterns for IE."""
2
+
3
+ CODE = "IE"
4
+
5
+ PATTERNS = {
6
+ "PPS_IE": "\\b\\d{7}[A-W]\\b",
7
+ }
@@ -0,0 +1,7 @@
1
+ """National-ID patterns for IL."""
2
+
3
+ CODE = "IL"
4
+
5
+ PATTERNS = {
6
+ "ID_IL": "\\b\\d{9}\\b",
7
+ }