euredact 0.3.0 → 0.3.1
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.
- package/dist/cjs/rules/countries/index.js +7 -1
- package/dist/cjs/rules/deDistricts.js +92 -0
- package/dist/cjs/rules/suppressors.js +40 -1
- package/dist/esm/rules/countries/index.js +7 -1
- package/dist/esm/rules/deDistricts.d.ts +16 -0
- package/dist/esm/rules/deDistricts.js +89 -0
- package/dist/esm/rules/suppressors.js +40 -1
- package/package.json +4 -3
|
@@ -217,7 +217,13 @@ const DE = {
|
|
|
217
217
|
p(types_js_1.EntityType.PHONE, String.raw `\b0[1-9]\d{1,4}[\s/\-]?\d{3,8}\b`),
|
|
218
218
|
p(types_js_1.EntityType.PHONE, String.raw `\+49\s?\d{2,5}[\s/\-]?\d{3,8}\b`),
|
|
219
219
|
p(types_js_1.EntityType.PASSPORT, String.raw `\b[CFGHJK][0-9A-Z]{8,9}\b`, null, "", ["Reisepass", "passport", "Passnummer", "Reisepassnummer", "Reisepass Nummer", "Reisepass-Nr", "Pass Nr"], true),
|
|
220
|
-
|
|
220
|
+
// The separator after the city code is mandatory. When it was optional, a
|
|
221
|
+
// contiguous letter run split across both groups and the hyphen was
|
|
222
|
+
// consumed as the *second* separator, so any "LETTERS-DIGITS" token
|
|
223
|
+
// matched: ICD-10 parsed as city "IC" + letters "D" + "-" + "10", and
|
|
224
|
+
// ISO-9001, COVID-19, RFC-2822, POL-2025 likewise. A real plate always
|
|
225
|
+
// separates the city code: "B-AB 1234", "M-XY 99".
|
|
226
|
+
p(types_js_1.EntityType.LICENSE_PLATE, String.raw `\b[A-ZÄÖÜ]{1,3}[\s\-][A-Z]{1,2}[\s\-]?\d{1,4}[EH]?\b`),
|
|
221
227
|
p(types_js_1.EntityType.POSTAL_CODE, String.raw `\b\d{5}\b`, null, "", ["PLZ", "Postleitzahl", "postal code", "postcode", "Anschrift", "Adresse", "Wohnort", "Ort", "Stadt", "wohnt", "wohnhaft", "Straße", "Str.", "Weg", "Platz", "Allee", "Ring"], true),
|
|
222
228
|
p(types_js_1.EntityType.CHAMBER_OF_COMMERCE, String.raw `\bHR[AB]\s?\d{4,6}\b`),
|
|
223
229
|
p(types_js_1.EntityType.SSN, String.raw `\b\d{2}[\s]?\d{6}[\s]?[A-Z][\s]?\d{3}\b`, null, "", ["Rentenversicherung", "RV-Nummer", "Sozialversicherung", "SV-Nummer", "Versicherungsnummer", "Rentenversicherungsnummer", "Sozialversicherungsnummer"], true),
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* German vehicle registration district codes (Unterscheidungszeichen).
|
|
4
|
+
*
|
|
5
|
+
* The set is closed: codes are fixed by the Fahrzeug-Zulassungsverordnung and
|
|
6
|
+
* maintained by the Kraftfahrt-Bundesamt, changing only when a district is
|
|
7
|
+
* created, merged, or a historic code is reinstated. That makes membership a
|
|
8
|
+
* usable precision signal — unlike the open-ended blocklist of standards
|
|
9
|
+
* prefixes it replaces, which only ever catches acronyms someone thought of.
|
|
10
|
+
*
|
|
11
|
+
* Used as a **tier**, not a filter: code on this list -> emit; code absent but
|
|
12
|
+
* a plate cue nearby -> emit; neither -> reject. So a missing code costs
|
|
13
|
+
* recall only where there is also no cue. Some entries are also standards
|
|
14
|
+
* prefixes — DIN is Dinslaken as well as the standards body — which is why the
|
|
15
|
+
* standards guard is kept alongside this one.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.DE_DISTRICT_CODES = void 0;
|
|
19
|
+
exports.DE_DISTRICT_CODES = new Set([
|
|
20
|
+
"A", "AA", "AB", "ABG", "AC", "AE", "AH", "AIB", "AIC", "AK", "AL", "ALF",
|
|
21
|
+
"ALS", "ALZ", "AM", "AN", "ANA", "ANG", "ANK", "AP", "APD", "AR", "ARN",
|
|
22
|
+
"ART", "AS", "ASD", "ASL", "ASZ", "AT", "AU", "AUR", "AW", "AZ", "AZE",
|
|
23
|
+
"AÖ", "B", "BA", "BAD", "BAR", "BB", "BBG", "BBL", "BC", "BCH", "BD",
|
|
24
|
+
"BE", "BED", "BEI", "BEL", "BER", "BF", "BG", "BGD", "BGL", "BH", "BI",
|
|
25
|
+
"BID", "BIN", "BIR", "BIT", "BIW", "BK", "BKS", "BL", "BLB", "BLK", "BM",
|
|
26
|
+
"BN", "BNA", "BO", "BOG", "BOH", "BOR", "BOT", "BP", "BR", "BRA", "BRB",
|
|
27
|
+
"BRG", "BRI", "BRK", "BRL", "BRV", "BS", "BSB", "BSK", "BT", "BTF", "BU",
|
|
28
|
+
"BUL", "BUR", "BW", "BWL", "BYL", "BZ", "BZA", "BÖ", "BÜD", "BÜS", "BÜZ",
|
|
29
|
+
"C", "CA", "CAS", "CB", "CE", "CHA", "CHL", "CLP", "CLZ", "CO", "COC",
|
|
30
|
+
"COE", "CR", "CT", "CUX", "CW", "D", "DA", "DAH", "DAN", "DAU", "DB",
|
|
31
|
+
"DBR", "DD", "DE", "DEG", "DEL", "DG", "DGF", "DH", "DI", "DIL", "DIN",
|
|
32
|
+
"DIZ", "DKB", "DL", "DLG", "DM", "DN", "DO", "DON", "DS", "DT", "DU",
|
|
33
|
+
"DUD", "DW", "DZ", "DÜW", "E", "EA", "EB", "EBE", "EBN", "EBS", "ECK",
|
|
34
|
+
"ED", "EE", "EF", "EG", "EH", "EHI", "EI", "EIC", "EIH", "EIL", "EIN",
|
|
35
|
+
"EIS", "EL", "EM", "EMD", "EMS", "EN", "ER", "ERB", "ERH", "ERK", "ES",
|
|
36
|
+
"ESA", "ESB", "ESW", "EU", "EUT", "EW", "F", "FAL", "FB", "FD", "FDB",
|
|
37
|
+
"FDS", "FEU", "FF", "FFB", "FG", "FH", "FI", "FKB", "FL", "FLÖ", "FN",
|
|
38
|
+
"FO", "FOR", "FR", "FRG", "FRI", "FRW", "FS", "FT", "FTL", "FW", "FZ",
|
|
39
|
+
"FÜ", "FÜS", "G", "GA", "GAN", "GAP", "GC", "GD", "GDB", "GE", "GEL",
|
|
40
|
+
"GEM", "GEO", "GER", "GF", "GG", "GHA", "GHC", "GI", "GK", "GL", "GLA",
|
|
41
|
+
"GM", "GMN", "GN", "GNT", "GOA", "GOH", "GP", "GR", "GRA", "GRH", "GRI",
|
|
42
|
+
"GRM", "GRS", "GRZ", "GS", "GT", "GTH", "GUB", "GUN", "GV", "GVM", "GW",
|
|
43
|
+
"GZ", "GÖ", "GÜ", "H", "HA", "HAB", "HAL", "HAM", "HAS", "HB", "HBN",
|
|
44
|
+
"HBS", "HC", "HCH", "HD", "HDH", "HDL", "HE", "HEB", "HEF", "HEI", "HEL",
|
|
45
|
+
"HER", "HET", "HF", "HG", "HGN", "HGW", "HH", "HHM", "HI", "HIG", "HIP",
|
|
46
|
+
"HK", "HL", "HM", "HMU", "HN", "HO", "HOG", "HOH", "HOL", "HOM", "HOR",
|
|
47
|
+
"HOT", "HP", "HR", "HRO", "HS", "HSK", "HST", "HU", "HUS", "HV", "HVL",
|
|
48
|
+
"HW", "HWI", "HX", "HY", "HZ", "HÖS", "HÜN", "IGB", "IK", "IL", "ILL",
|
|
49
|
+
"IN", "IS", "IZ", "J", "JB", "JE", "JEV", "JL", "JÜL", "K", "KA", "KAR",
|
|
50
|
+
"KB", "KC", "KE", "KEH", "KEL", "KEM", "KF", "KG", "KH", "KI", "KIB",
|
|
51
|
+
"KK", "KL", "KLE", "KLT", "KLZ", "KM", "KN", "KO", "KOZ", "KR", "KRU",
|
|
52
|
+
"KS", "KT", "KU", "KUS", "KW", "KY", "KYF", "KÖN", "KÖT", "KÜN", "L",
|
|
53
|
+
"LA", "LAN", "LAT", "LAU", "LB", "LBS", "LBZ", "LC", "LD", "LDK", "LDS",
|
|
54
|
+
"LE", "LEO", "LER", "LEV", "LF", "LG", "LH", "LI", "LIB", "LIF", "LIN",
|
|
55
|
+
"LIP", "LK", "LL", "LM", "LN", "LOH", "LOS", "LP", "LR", "LS", "LSA",
|
|
56
|
+
"LSN", "LSZ", "LU", "LUK", "LWL", "LÖ", "LÖB", "LÜD", "LÜN", "M", "MA",
|
|
57
|
+
"MAB", "MAI", "MAK", "MAL", "MAR", "MB", "MC", "MD", "ME", "MED", "MEG",
|
|
58
|
+
"MEI", "MEK", "MEL", "MEP", "MER", "MES", "MET", "MG", "MGH", "MGN", "MH",
|
|
59
|
+
"MHL", "MI", "MIL", "MK", "MKK", "ML", "MM", "MN", "MO", "MOD", "MOL",
|
|
60
|
+
"MON", "MOS", "MQ", "MR", "MS", "MSP", "MST", "MT", "MTK", "MTL", "MVL",
|
|
61
|
+
"MW", "MY", "MYK", "MZ", "MZG", "MÜ", "MÜB", "MÜL", "MÜN", "MÜR", "N",
|
|
62
|
+
"NAB", "NAI", "NAU", "NB", "ND", "NDH", "NE", "NEA", "NEB", "NEC", "NEN",
|
|
63
|
+
"NES", "NEU", "NEW", "NF", "NH", "NI", "NIB", "NK", "NL", "NM", "NMB",
|
|
64
|
+
"NMS", "NOH", "NOL", "NOM", "NOR", "NP", "NR", "NRW", "NRÜ", "NT", "NU",
|
|
65
|
+
"NVP", "NW", "NWM", "NY", "NZ", "NÖ", "OA", "OAL", "OB", "OBB", "OBG",
|
|
66
|
+
"OC", "OCH", "OD", "OE", "OF", "OG", "OH", "OHA", "OHV", "OHZ", "OK",
|
|
67
|
+
"OL", "OLD", "OP", "OPR", "OR", "OS", "OSL", "OTT", "OTW", "OVI", "OVL",
|
|
68
|
+
"OVP", "OZ", "P", "PA", "PAF", "PAN", "PAR", "PB", "PCH", "PE", "PEG",
|
|
69
|
+
"PER", "PF", "PI", "PIR", "PK", "PL", "PLÖ", "PM", "PN", "PR", "PRÜ",
|
|
70
|
+
"PS", "PW", "PZ", "QFT", "QLB", "R", "RA", "RC", "RD", "RDG", "RE", "REG",
|
|
71
|
+
"REH", "REI", "RG", "RH", "RI", "RID", "RIE", "RL", "RM", "RN", "RO",
|
|
72
|
+
"ROD", "ROF", "ROH", "ROK", "ROL", "ROS", "ROT", "ROW", "RPL", "RS",
|
|
73
|
+
"RSL", "RT", "RU", "RV", "RW", "RWL", "RY", "RZ", "RÜD", "RÜG", "S", "SA",
|
|
74
|
+
"SAB", "SAD", "SAL", "SAN", "SAO", "SAW", "SB", "SBG", "SBK", "SC", "SCZ",
|
|
75
|
+
"SDH", "SDL", "SDT", "SE", "SEB", "SEE", "SEF", "SEL", "SF", "SFA", "SFB",
|
|
76
|
+
"SFT", "SG", "SGH", "SH", "SHA", "SHG", "SHK", "SHL", "SHS", "SI", "SIG",
|
|
77
|
+
"SIM", "SK", "SL", "SLE", "SLF", "SLG", "SLK", "SLN", "SLS", "SLZ", "SLÜ",
|
|
78
|
+
"SM", "SMÜ", "SN", "SNH", "SO", "SOB", "SOG", "SOK", "SOL", "SON", "SP",
|
|
79
|
+
"SPB", "SPN", "SPR", "SR", "SRB", "SRO", "ST", "STA", "STB", "STD", "STE",
|
|
80
|
+
"STH", "STL", "STO", "SU", "SUL", "SW", "SWA", "SWZ", "SY", "SZ", "SZB",
|
|
81
|
+
"SÄK", "SÖM", "SÜW", "TBB", "TE", "TF", "TG", "TGO", "THL", "THW", "TIR",
|
|
82
|
+
"TO", "TP", "TR", "TS", "TT", "TUT", "TÖL", "TÖN", "TÜ", "UE", "UEM",
|
|
83
|
+
"UER", "UFF", "UH", "UL", "UM", "UN", "USI", "V", "VAI", "VB", "VEC",
|
|
84
|
+
"VER", "VIB", "VIE", "VIT", "VK", "VL", "VOF", "VOH", "VR", "VS", "W",
|
|
85
|
+
"WA", "WAF", "WAK", "WAM", "WAN", "WAR", "WAT", "WB", "WBS", "WD", "WDA",
|
|
86
|
+
"WE", "WEB", "WEG", "WEI", "WEL", "WEM", "WEN", "WER", "WES", "WF", "WG",
|
|
87
|
+
"WHV", "WI", "WIL", "WIS", "WIT", "WIZ", "WK", "WL", "WLG", "WM", "WMS",
|
|
88
|
+
"WN", "WND", "WO", "WOB", "WOH", "WOL", "WOR", "WOS", "WR", "WRN", "WS",
|
|
89
|
+
"WSF", "WST", "WSW", "WT", "WTL", "WTM", "WUG", "WUM", "WUN", "WUR", "WW",
|
|
90
|
+
"WZ", "WZL", "WÜ", "X", "Y", "Z", "ZE", "ZEL", "ZI", "ZIG", "ZP", "ZR",
|
|
91
|
+
"ZS", "ZW", "ZZ", "ÖHR", "ÜB",
|
|
92
|
+
]);
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.shouldSuppress = shouldSuppress;
|
|
4
4
|
const types_js_1 = require("../types.js");
|
|
5
5
|
const bicRegistry_js_1 = require("./bicRegistry.js");
|
|
6
|
+
const deDistricts_js_1 = require("./deDistricts.js");
|
|
6
7
|
const CONTEXT_CHARS = 150;
|
|
7
8
|
function getContext(text, start, end) {
|
|
8
9
|
const ctxStart = Math.max(0, start - CONTEXT_CHARS);
|
|
@@ -120,6 +121,12 @@ function suppressPhoneDateOverlap(_text, match) {
|
|
|
120
121
|
return false;
|
|
121
122
|
return DATE_PATTERN_FULL.test(match.text.trim());
|
|
122
123
|
}
|
|
124
|
+
// Standards and classification prefixes that are plate-shaped once a letter and
|
|
125
|
+
// digits follow: "ATC-N06", "ICD-O3". None is a German district code, but the
|
|
126
|
+
// guard is still gated on the absence of a plate cue, so a genuine plate that
|
|
127
|
+
// happens to collide is not lost.
|
|
128
|
+
const STANDARDS_PREFIX = new Set(["ICD", "ISO", "DIN", "IEC", "RFC", "DSM", "ATC", "MDR"]);
|
|
129
|
+
const PLATE_CUE_NEAR = /(?:Kennzeichen|Nummernschild|Kfz|Fahrzeug|amtliche[sn]?\s+Kennz|nummerplaat|plaque\s+d'immatriculation|license\s+plate|number\s+plate)/i;
|
|
123
130
|
const NOT_CITY_CODES = new Set([
|
|
124
131
|
"ID", "NR", "NO", "ST", "DR", "MR", "MS", "HR", "FR", "IM", "IN", "OR", "IF", "IS", "IT", "AT", "AD", "AG", "AV",
|
|
125
132
|
"BE", "DE", "EU", "NL", "LU", "WS", "SS", "IP",
|
|
@@ -138,6 +145,13 @@ function suppressPlateInCompound(text, match) {
|
|
|
138
145
|
}
|
|
139
146
|
const matched = match.text.trim();
|
|
140
147
|
const parts = matched.split(/[\s\-]+/);
|
|
148
|
+
// A standards or classification reference, not a plate — unless a plate cue
|
|
149
|
+
// nearby says otherwise.
|
|
150
|
+
if (parts.length > 0 && STANDARDS_PREFIX.has(parts[0].toUpperCase())) {
|
|
151
|
+
const [b, a] = getContext(text, match.start, match.end);
|
|
152
|
+
if (!PLATE_CUE_NEAR.test(b + a))
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
141
155
|
if (parts.length > 0 && NOT_CITY_CODES.has(parts[0])) {
|
|
142
156
|
const afterChar = match.end < text.length ? text[match.end] : "";
|
|
143
157
|
const beforeChar = match.start > 0 ? text[match.start - 1] : "";
|
|
@@ -160,6 +174,31 @@ function suppressPlateInCompound(text, match) {
|
|
|
160
174
|
}
|
|
161
175
|
return false;
|
|
162
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Reject a German plate whose district code is not a real one.
|
|
179
|
+
*
|
|
180
|
+
* The district-code set is closed, which makes it a whitelist rather than the
|
|
181
|
+
* open-ended blocklist of standards prefixes: "REF-A12", "SYS-B3", "KTO-A1"
|
|
182
|
+
* and every other document reference of that shape fail it without needing to
|
|
183
|
+
* be enumerated. Applied as a tier — an unknown code still emits when a plate
|
|
184
|
+
* cue is nearby, so a missing code costs recall only in the absence of any
|
|
185
|
+
* other evidence.
|
|
186
|
+
*/
|
|
187
|
+
function suppressDePlateUnknownDistrict(text, match) {
|
|
188
|
+
if (match.patternDef.entityType !== types_js_1.EntityType.LICENSE_PLATE)
|
|
189
|
+
return false;
|
|
190
|
+
if (match.countryCode !== "DE")
|
|
191
|
+
return false;
|
|
192
|
+
const parts = match.text.trim().split(/[\s\-]+/);
|
|
193
|
+
if (parts.length === 0 || !parts[0])
|
|
194
|
+
return false;
|
|
195
|
+
if (deDistricts_js_1.DE_DISTRICT_CODES.has(parts[0].toUpperCase()))
|
|
196
|
+
return false;
|
|
197
|
+
const [before, after] = getContext(text, match.start, match.end);
|
|
198
|
+
if (PLATE_CUE_NEAR.test(before + after))
|
|
199
|
+
return false;
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
163
202
|
function suppressNatidAsPassport(text, match) {
|
|
164
203
|
if (match.patternDef.entityType !== types_js_1.EntityType.NATIONAL_ID)
|
|
165
204
|
return false;
|
|
@@ -429,7 +468,7 @@ const TYPE_SUPPRESSORS = {
|
|
|
429
468
|
[types_js_1.EntityType.POSTAL_CODE]: [suppressCurrency, suppressUnits, suppressMath, suppressLegal, suppressYearAsPostal, suppressPostalInsideIban, suppressPostalAsHouseNumber, suppressPostalInLongerIdentifier],
|
|
430
469
|
[types_js_1.EntityType.BIC]: [suppressBicWithoutEvidence],
|
|
431
470
|
[types_js_1.EntityType.BANK_ACCOUNT]: [suppressReference],
|
|
432
|
-
[types_js_1.EntityType.LICENSE_PLATE]: [suppressPlateInCompound],
|
|
471
|
+
[types_js_1.EntityType.LICENSE_PLATE]: [suppressPlateInCompound, suppressDePlateUnknownDistrict],
|
|
433
472
|
[types_js_1.EntityType.CHAMBER_OF_COMMERCE]: [suppressReference],
|
|
434
473
|
};
|
|
435
474
|
function shouldSuppress(text, match) {
|
|
@@ -214,7 +214,13 @@ const DE = {
|
|
|
214
214
|
p(EntityType.PHONE, String.raw `\b0[1-9]\d{1,4}[\s/\-]?\d{3,8}\b`),
|
|
215
215
|
p(EntityType.PHONE, String.raw `\+49\s?\d{2,5}[\s/\-]?\d{3,8}\b`),
|
|
216
216
|
p(EntityType.PASSPORT, String.raw `\b[CFGHJK][0-9A-Z]{8,9}\b`, null, "", ["Reisepass", "passport", "Passnummer", "Reisepassnummer", "Reisepass Nummer", "Reisepass-Nr", "Pass Nr"], true),
|
|
217
|
-
|
|
217
|
+
// The separator after the city code is mandatory. When it was optional, a
|
|
218
|
+
// contiguous letter run split across both groups and the hyphen was
|
|
219
|
+
// consumed as the *second* separator, so any "LETTERS-DIGITS" token
|
|
220
|
+
// matched: ICD-10 parsed as city "IC" + letters "D" + "-" + "10", and
|
|
221
|
+
// ISO-9001, COVID-19, RFC-2822, POL-2025 likewise. A real plate always
|
|
222
|
+
// separates the city code: "B-AB 1234", "M-XY 99".
|
|
223
|
+
p(EntityType.LICENSE_PLATE, String.raw `\b[A-ZÄÖÜ]{1,3}[\s\-][A-Z]{1,2}[\s\-]?\d{1,4}[EH]?\b`),
|
|
218
224
|
p(EntityType.POSTAL_CODE, String.raw `\b\d{5}\b`, null, "", ["PLZ", "Postleitzahl", "postal code", "postcode", "Anschrift", "Adresse", "Wohnort", "Ort", "Stadt", "wohnt", "wohnhaft", "Straße", "Str.", "Weg", "Platz", "Allee", "Ring"], true),
|
|
219
225
|
p(EntityType.CHAMBER_OF_COMMERCE, String.raw `\bHR[AB]\s?\d{4,6}\b`),
|
|
220
226
|
p(EntityType.SSN, String.raw `\b\d{2}[\s]?\d{6}[\s]?[A-Z][\s]?\d{3}\b`, null, "", ["Rentenversicherung", "RV-Nummer", "Sozialversicherung", "SV-Nummer", "Versicherungsnummer", "Rentenversicherungsnummer", "Sozialversicherungsnummer"], true),
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* German vehicle registration district codes (Unterscheidungszeichen).
|
|
3
|
+
*
|
|
4
|
+
* The set is closed: codes are fixed by the Fahrzeug-Zulassungsverordnung and
|
|
5
|
+
* maintained by the Kraftfahrt-Bundesamt, changing only when a district is
|
|
6
|
+
* created, merged, or a historic code is reinstated. That makes membership a
|
|
7
|
+
* usable precision signal — unlike the open-ended blocklist of standards
|
|
8
|
+
* prefixes it replaces, which only ever catches acronyms someone thought of.
|
|
9
|
+
*
|
|
10
|
+
* Used as a **tier**, not a filter: code on this list -> emit; code absent but
|
|
11
|
+
* a plate cue nearby -> emit; neither -> reject. So a missing code costs
|
|
12
|
+
* recall only where there is also no cue. Some entries are also standards
|
|
13
|
+
* prefixes — DIN is Dinslaken as well as the standards body — which is why the
|
|
14
|
+
* standards guard is kept alongside this one.
|
|
15
|
+
*/
|
|
16
|
+
export declare const DE_DISTRICT_CODES: ReadonlySet<string>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* German vehicle registration district codes (Unterscheidungszeichen).
|
|
3
|
+
*
|
|
4
|
+
* The set is closed: codes are fixed by the Fahrzeug-Zulassungsverordnung and
|
|
5
|
+
* maintained by the Kraftfahrt-Bundesamt, changing only when a district is
|
|
6
|
+
* created, merged, or a historic code is reinstated. That makes membership a
|
|
7
|
+
* usable precision signal — unlike the open-ended blocklist of standards
|
|
8
|
+
* prefixes it replaces, which only ever catches acronyms someone thought of.
|
|
9
|
+
*
|
|
10
|
+
* Used as a **tier**, not a filter: code on this list -> emit; code absent but
|
|
11
|
+
* a plate cue nearby -> emit; neither -> reject. So a missing code costs
|
|
12
|
+
* recall only where there is also no cue. Some entries are also standards
|
|
13
|
+
* prefixes — DIN is Dinslaken as well as the standards body — which is why the
|
|
14
|
+
* standards guard is kept alongside this one.
|
|
15
|
+
*/
|
|
16
|
+
export const DE_DISTRICT_CODES = new Set([
|
|
17
|
+
"A", "AA", "AB", "ABG", "AC", "AE", "AH", "AIB", "AIC", "AK", "AL", "ALF",
|
|
18
|
+
"ALS", "ALZ", "AM", "AN", "ANA", "ANG", "ANK", "AP", "APD", "AR", "ARN",
|
|
19
|
+
"ART", "AS", "ASD", "ASL", "ASZ", "AT", "AU", "AUR", "AW", "AZ", "AZE",
|
|
20
|
+
"AÖ", "B", "BA", "BAD", "BAR", "BB", "BBG", "BBL", "BC", "BCH", "BD",
|
|
21
|
+
"BE", "BED", "BEI", "BEL", "BER", "BF", "BG", "BGD", "BGL", "BH", "BI",
|
|
22
|
+
"BID", "BIN", "BIR", "BIT", "BIW", "BK", "BKS", "BL", "BLB", "BLK", "BM",
|
|
23
|
+
"BN", "BNA", "BO", "BOG", "BOH", "BOR", "BOT", "BP", "BR", "BRA", "BRB",
|
|
24
|
+
"BRG", "BRI", "BRK", "BRL", "BRV", "BS", "BSB", "BSK", "BT", "BTF", "BU",
|
|
25
|
+
"BUL", "BUR", "BW", "BWL", "BYL", "BZ", "BZA", "BÖ", "BÜD", "BÜS", "BÜZ",
|
|
26
|
+
"C", "CA", "CAS", "CB", "CE", "CHA", "CHL", "CLP", "CLZ", "CO", "COC",
|
|
27
|
+
"COE", "CR", "CT", "CUX", "CW", "D", "DA", "DAH", "DAN", "DAU", "DB",
|
|
28
|
+
"DBR", "DD", "DE", "DEG", "DEL", "DG", "DGF", "DH", "DI", "DIL", "DIN",
|
|
29
|
+
"DIZ", "DKB", "DL", "DLG", "DM", "DN", "DO", "DON", "DS", "DT", "DU",
|
|
30
|
+
"DUD", "DW", "DZ", "DÜW", "E", "EA", "EB", "EBE", "EBN", "EBS", "ECK",
|
|
31
|
+
"ED", "EE", "EF", "EG", "EH", "EHI", "EI", "EIC", "EIH", "EIL", "EIN",
|
|
32
|
+
"EIS", "EL", "EM", "EMD", "EMS", "EN", "ER", "ERB", "ERH", "ERK", "ES",
|
|
33
|
+
"ESA", "ESB", "ESW", "EU", "EUT", "EW", "F", "FAL", "FB", "FD", "FDB",
|
|
34
|
+
"FDS", "FEU", "FF", "FFB", "FG", "FH", "FI", "FKB", "FL", "FLÖ", "FN",
|
|
35
|
+
"FO", "FOR", "FR", "FRG", "FRI", "FRW", "FS", "FT", "FTL", "FW", "FZ",
|
|
36
|
+
"FÜ", "FÜS", "G", "GA", "GAN", "GAP", "GC", "GD", "GDB", "GE", "GEL",
|
|
37
|
+
"GEM", "GEO", "GER", "GF", "GG", "GHA", "GHC", "GI", "GK", "GL", "GLA",
|
|
38
|
+
"GM", "GMN", "GN", "GNT", "GOA", "GOH", "GP", "GR", "GRA", "GRH", "GRI",
|
|
39
|
+
"GRM", "GRS", "GRZ", "GS", "GT", "GTH", "GUB", "GUN", "GV", "GVM", "GW",
|
|
40
|
+
"GZ", "GÖ", "GÜ", "H", "HA", "HAB", "HAL", "HAM", "HAS", "HB", "HBN",
|
|
41
|
+
"HBS", "HC", "HCH", "HD", "HDH", "HDL", "HE", "HEB", "HEF", "HEI", "HEL",
|
|
42
|
+
"HER", "HET", "HF", "HG", "HGN", "HGW", "HH", "HHM", "HI", "HIG", "HIP",
|
|
43
|
+
"HK", "HL", "HM", "HMU", "HN", "HO", "HOG", "HOH", "HOL", "HOM", "HOR",
|
|
44
|
+
"HOT", "HP", "HR", "HRO", "HS", "HSK", "HST", "HU", "HUS", "HV", "HVL",
|
|
45
|
+
"HW", "HWI", "HX", "HY", "HZ", "HÖS", "HÜN", "IGB", "IK", "IL", "ILL",
|
|
46
|
+
"IN", "IS", "IZ", "J", "JB", "JE", "JEV", "JL", "JÜL", "K", "KA", "KAR",
|
|
47
|
+
"KB", "KC", "KE", "KEH", "KEL", "KEM", "KF", "KG", "KH", "KI", "KIB",
|
|
48
|
+
"KK", "KL", "KLE", "KLT", "KLZ", "KM", "KN", "KO", "KOZ", "KR", "KRU",
|
|
49
|
+
"KS", "KT", "KU", "KUS", "KW", "KY", "KYF", "KÖN", "KÖT", "KÜN", "L",
|
|
50
|
+
"LA", "LAN", "LAT", "LAU", "LB", "LBS", "LBZ", "LC", "LD", "LDK", "LDS",
|
|
51
|
+
"LE", "LEO", "LER", "LEV", "LF", "LG", "LH", "LI", "LIB", "LIF", "LIN",
|
|
52
|
+
"LIP", "LK", "LL", "LM", "LN", "LOH", "LOS", "LP", "LR", "LS", "LSA",
|
|
53
|
+
"LSN", "LSZ", "LU", "LUK", "LWL", "LÖ", "LÖB", "LÜD", "LÜN", "M", "MA",
|
|
54
|
+
"MAB", "MAI", "MAK", "MAL", "MAR", "MB", "MC", "MD", "ME", "MED", "MEG",
|
|
55
|
+
"MEI", "MEK", "MEL", "MEP", "MER", "MES", "MET", "MG", "MGH", "MGN", "MH",
|
|
56
|
+
"MHL", "MI", "MIL", "MK", "MKK", "ML", "MM", "MN", "MO", "MOD", "MOL",
|
|
57
|
+
"MON", "MOS", "MQ", "MR", "MS", "MSP", "MST", "MT", "MTK", "MTL", "MVL",
|
|
58
|
+
"MW", "MY", "MYK", "MZ", "MZG", "MÜ", "MÜB", "MÜL", "MÜN", "MÜR", "N",
|
|
59
|
+
"NAB", "NAI", "NAU", "NB", "ND", "NDH", "NE", "NEA", "NEB", "NEC", "NEN",
|
|
60
|
+
"NES", "NEU", "NEW", "NF", "NH", "NI", "NIB", "NK", "NL", "NM", "NMB",
|
|
61
|
+
"NMS", "NOH", "NOL", "NOM", "NOR", "NP", "NR", "NRW", "NRÜ", "NT", "NU",
|
|
62
|
+
"NVP", "NW", "NWM", "NY", "NZ", "NÖ", "OA", "OAL", "OB", "OBB", "OBG",
|
|
63
|
+
"OC", "OCH", "OD", "OE", "OF", "OG", "OH", "OHA", "OHV", "OHZ", "OK",
|
|
64
|
+
"OL", "OLD", "OP", "OPR", "OR", "OS", "OSL", "OTT", "OTW", "OVI", "OVL",
|
|
65
|
+
"OVP", "OZ", "P", "PA", "PAF", "PAN", "PAR", "PB", "PCH", "PE", "PEG",
|
|
66
|
+
"PER", "PF", "PI", "PIR", "PK", "PL", "PLÖ", "PM", "PN", "PR", "PRÜ",
|
|
67
|
+
"PS", "PW", "PZ", "QFT", "QLB", "R", "RA", "RC", "RD", "RDG", "RE", "REG",
|
|
68
|
+
"REH", "REI", "RG", "RH", "RI", "RID", "RIE", "RL", "RM", "RN", "RO",
|
|
69
|
+
"ROD", "ROF", "ROH", "ROK", "ROL", "ROS", "ROT", "ROW", "RPL", "RS",
|
|
70
|
+
"RSL", "RT", "RU", "RV", "RW", "RWL", "RY", "RZ", "RÜD", "RÜG", "S", "SA",
|
|
71
|
+
"SAB", "SAD", "SAL", "SAN", "SAO", "SAW", "SB", "SBG", "SBK", "SC", "SCZ",
|
|
72
|
+
"SDH", "SDL", "SDT", "SE", "SEB", "SEE", "SEF", "SEL", "SF", "SFA", "SFB",
|
|
73
|
+
"SFT", "SG", "SGH", "SH", "SHA", "SHG", "SHK", "SHL", "SHS", "SI", "SIG",
|
|
74
|
+
"SIM", "SK", "SL", "SLE", "SLF", "SLG", "SLK", "SLN", "SLS", "SLZ", "SLÜ",
|
|
75
|
+
"SM", "SMÜ", "SN", "SNH", "SO", "SOB", "SOG", "SOK", "SOL", "SON", "SP",
|
|
76
|
+
"SPB", "SPN", "SPR", "SR", "SRB", "SRO", "ST", "STA", "STB", "STD", "STE",
|
|
77
|
+
"STH", "STL", "STO", "SU", "SUL", "SW", "SWA", "SWZ", "SY", "SZ", "SZB",
|
|
78
|
+
"SÄK", "SÖM", "SÜW", "TBB", "TE", "TF", "TG", "TGO", "THL", "THW", "TIR",
|
|
79
|
+
"TO", "TP", "TR", "TS", "TT", "TUT", "TÖL", "TÖN", "TÜ", "UE", "UEM",
|
|
80
|
+
"UER", "UFF", "UH", "UL", "UM", "UN", "USI", "V", "VAI", "VB", "VEC",
|
|
81
|
+
"VER", "VIB", "VIE", "VIT", "VK", "VL", "VOF", "VOH", "VR", "VS", "W",
|
|
82
|
+
"WA", "WAF", "WAK", "WAM", "WAN", "WAR", "WAT", "WB", "WBS", "WD", "WDA",
|
|
83
|
+
"WE", "WEB", "WEG", "WEI", "WEL", "WEM", "WEN", "WER", "WES", "WF", "WG",
|
|
84
|
+
"WHV", "WI", "WIL", "WIS", "WIT", "WIZ", "WK", "WL", "WLG", "WM", "WMS",
|
|
85
|
+
"WN", "WND", "WO", "WOB", "WOH", "WOL", "WOR", "WOS", "WR", "WRN", "WS",
|
|
86
|
+
"WSF", "WST", "WSW", "WT", "WTL", "WTM", "WUG", "WUM", "WUN", "WUR", "WW",
|
|
87
|
+
"WZ", "WZL", "WÜ", "X", "Y", "Z", "ZE", "ZEL", "ZI", "ZIG", "ZP", "ZR",
|
|
88
|
+
"ZS", "ZW", "ZZ", "ÖHR", "ÜB",
|
|
89
|
+
]);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EntityType } from "../types.js";
|
|
2
2
|
import { isRegisteredBic } from "./bicRegistry.js";
|
|
3
|
+
import { DE_DISTRICT_CODES } from "./deDistricts.js";
|
|
3
4
|
const CONTEXT_CHARS = 150;
|
|
4
5
|
function getContext(text, start, end) {
|
|
5
6
|
const ctxStart = Math.max(0, start - CONTEXT_CHARS);
|
|
@@ -117,6 +118,12 @@ function suppressPhoneDateOverlap(_text, match) {
|
|
|
117
118
|
return false;
|
|
118
119
|
return DATE_PATTERN_FULL.test(match.text.trim());
|
|
119
120
|
}
|
|
121
|
+
// Standards and classification prefixes that are plate-shaped once a letter and
|
|
122
|
+
// digits follow: "ATC-N06", "ICD-O3". None is a German district code, but the
|
|
123
|
+
// guard is still gated on the absence of a plate cue, so a genuine plate that
|
|
124
|
+
// happens to collide is not lost.
|
|
125
|
+
const STANDARDS_PREFIX = new Set(["ICD", "ISO", "DIN", "IEC", "RFC", "DSM", "ATC", "MDR"]);
|
|
126
|
+
const PLATE_CUE_NEAR = /(?:Kennzeichen|Nummernschild|Kfz|Fahrzeug|amtliche[sn]?\s+Kennz|nummerplaat|plaque\s+d'immatriculation|license\s+plate|number\s+plate)/i;
|
|
120
127
|
const NOT_CITY_CODES = new Set([
|
|
121
128
|
"ID", "NR", "NO", "ST", "DR", "MR", "MS", "HR", "FR", "IM", "IN", "OR", "IF", "IS", "IT", "AT", "AD", "AG", "AV",
|
|
122
129
|
"BE", "DE", "EU", "NL", "LU", "WS", "SS", "IP",
|
|
@@ -135,6 +142,13 @@ function suppressPlateInCompound(text, match) {
|
|
|
135
142
|
}
|
|
136
143
|
const matched = match.text.trim();
|
|
137
144
|
const parts = matched.split(/[\s\-]+/);
|
|
145
|
+
// A standards or classification reference, not a plate — unless a plate cue
|
|
146
|
+
// nearby says otherwise.
|
|
147
|
+
if (parts.length > 0 && STANDARDS_PREFIX.has(parts[0].toUpperCase())) {
|
|
148
|
+
const [b, a] = getContext(text, match.start, match.end);
|
|
149
|
+
if (!PLATE_CUE_NEAR.test(b + a))
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
138
152
|
if (parts.length > 0 && NOT_CITY_CODES.has(parts[0])) {
|
|
139
153
|
const afterChar = match.end < text.length ? text[match.end] : "";
|
|
140
154
|
const beforeChar = match.start > 0 ? text[match.start - 1] : "";
|
|
@@ -157,6 +171,31 @@ function suppressPlateInCompound(text, match) {
|
|
|
157
171
|
}
|
|
158
172
|
return false;
|
|
159
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Reject a German plate whose district code is not a real one.
|
|
176
|
+
*
|
|
177
|
+
* The district-code set is closed, which makes it a whitelist rather than the
|
|
178
|
+
* open-ended blocklist of standards prefixes: "REF-A12", "SYS-B3", "KTO-A1"
|
|
179
|
+
* and every other document reference of that shape fail it without needing to
|
|
180
|
+
* be enumerated. Applied as a tier — an unknown code still emits when a plate
|
|
181
|
+
* cue is nearby, so a missing code costs recall only in the absence of any
|
|
182
|
+
* other evidence.
|
|
183
|
+
*/
|
|
184
|
+
function suppressDePlateUnknownDistrict(text, match) {
|
|
185
|
+
if (match.patternDef.entityType !== EntityType.LICENSE_PLATE)
|
|
186
|
+
return false;
|
|
187
|
+
if (match.countryCode !== "DE")
|
|
188
|
+
return false;
|
|
189
|
+
const parts = match.text.trim().split(/[\s\-]+/);
|
|
190
|
+
if (parts.length === 0 || !parts[0])
|
|
191
|
+
return false;
|
|
192
|
+
if (DE_DISTRICT_CODES.has(parts[0].toUpperCase()))
|
|
193
|
+
return false;
|
|
194
|
+
const [before, after] = getContext(text, match.start, match.end);
|
|
195
|
+
if (PLATE_CUE_NEAR.test(before + after))
|
|
196
|
+
return false;
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
160
199
|
function suppressNatidAsPassport(text, match) {
|
|
161
200
|
if (match.patternDef.entityType !== EntityType.NATIONAL_ID)
|
|
162
201
|
return false;
|
|
@@ -426,7 +465,7 @@ const TYPE_SUPPRESSORS = {
|
|
|
426
465
|
[EntityType.POSTAL_CODE]: [suppressCurrency, suppressUnits, suppressMath, suppressLegal, suppressYearAsPostal, suppressPostalInsideIban, suppressPostalAsHouseNumber, suppressPostalInLongerIdentifier],
|
|
427
466
|
[EntityType.BIC]: [suppressBicWithoutEvidence],
|
|
428
467
|
[EntityType.BANK_ACCOUNT]: [suppressReference],
|
|
429
|
-
[EntityType.LICENSE_PLATE]: [suppressPlateInCompound],
|
|
468
|
+
[EntityType.LICENSE_PLATE]: [suppressPlateInCompound, suppressDePlateUnknownDistrict],
|
|
430
469
|
[EntityType.CHAMBER_OF_COMMERCE]: [suppressReference],
|
|
431
470
|
};
|
|
432
471
|
export function shouldSuppress(text, match) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "euredact",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "European PII redaction SDK -- fast, zero-dependency, rule-based detection and redaction of personal data across 31 European countries",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -53,12 +53,13 @@
|
|
|
53
53
|
"build:cjs": "tsc -p tsconfig.cjs.json",
|
|
54
54
|
"postbuild:cjs": "echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
|
|
55
55
|
"build": "rm -rf dist && npm run build:esm && npm run build:cjs",
|
|
56
|
-
"test": "npm run test:spec",
|
|
56
|
+
"test": "npm run test:spec && npm run test:conformance",
|
|
57
57
|
"benchmark": "node --import tsx src/__tests__/benchmark.ts",
|
|
58
58
|
"prepublishOnly": "npm run build",
|
|
59
59
|
"test:spec": "node --import tsx src/__tests__/spec.ts",
|
|
60
60
|
"test:eval": "node --import tsx src/__tests__/run.ts",
|
|
61
|
-
"test:eval:full": "node --max-old-space-size=8192 --import tsx src/__tests__/evalFull.ts"
|
|
61
|
+
"test:eval:full": "node --max-old-space-size=8192 --import tsx src/__tests__/evalFull.ts",
|
|
62
|
+
"test:conformance": "node --import tsx src/__tests__/conformance.ts"
|
|
62
63
|
},
|
|
63
64
|
"devDependencies": {
|
|
64
65
|
"typescript": "^5.4.0",
|