euredact 0.0.1 → 0.2.0

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.
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RuleEngine = void 0;
4
+ const types_js_1 = require("../types.js");
5
+ const matchers_js_1 = require("./matchers.js");
6
+ const suppressors_js_1 = require("./suppressors.js");
7
+ const structural_js_1 = require("./structural.js");
8
+ const index_js_1 = require("./countries/index.js");
9
+ class RuleEngine {
10
+ matcher = new matchers_js_1.MultiPatternMatcher();
11
+ loadedCountries = new Set();
12
+ addCustomPattern(name, pattern) {
13
+ const pdef = {
14
+ entityType: name, pattern, validator: null,
15
+ description: "", contextKeywords: [], requiresContext: false,
16
+ };
17
+ this.matcher.addPattern(pdef, "CUSTOM");
18
+ this.matcher.compile();
19
+ }
20
+ loadCountries(countryCodes) {
21
+ let configs;
22
+ if (countryCodes === null) {
23
+ configs = Object.values(index_js_1.COUNTRY_CONFIGS);
24
+ }
25
+ else {
26
+ configs = [];
27
+ for (const c of countryCodes) {
28
+ const code = c.toUpperCase();
29
+ const config = index_js_1.COUNTRY_CONFIGS[code];
30
+ if (config)
31
+ configs.push(config);
32
+ }
33
+ const shared = index_js_1.COUNTRY_CONFIGS["SHARED"];
34
+ if (shared)
35
+ configs.push(shared);
36
+ }
37
+ let newCountries = false;
38
+ for (const config of configs) {
39
+ if (!this.loadedCountries.has(config.code)) {
40
+ this.matcher.addCountry(config);
41
+ this.loadedCountries.add(config.code);
42
+ newCountries = true;
43
+ }
44
+ }
45
+ if (newCountries)
46
+ this.matcher.compile();
47
+ }
48
+ detect(text, countryCodes) {
49
+ this.loadCountries(countryCodes);
50
+ let rawMatches = this.matcher.scan(text);
51
+ if (countryCodes !== null) {
52
+ const codesUpper = new Set(countryCodes.map(c => c.toUpperCase()));
53
+ rawMatches = rawMatches.filter(m => codesUpper.has(m.countryCode) || m.countryCode === "SHARED" || m.countryCode === "CUSTOM");
54
+ }
55
+ // Matches that have a validator but fail it create suppression zones:
56
+ // the span is recognisably a specific entity (e.g. IBAN-shaped) so
57
+ // overlapping regex-only matches (license plate, phone) are false
58
+ // positives and must be suppressed.
59
+ const validated = [];
60
+ const suppressionZones = [];
61
+ for (const m of rawMatches) {
62
+ if (this.matcher.validate(m)) {
63
+ validated.push(m);
64
+ }
65
+ else if (m.patternDef.validator !== null && !m.patternDef.requiresContext) {
66
+ suppressionZones.push([m.start, m.end]);
67
+ }
68
+ }
69
+ // Build candidates with priority: validated (3) > custom (2) > regex-only (1)
70
+ const candidates = [];
71
+ for (const match of validated) {
72
+ if ((0, suppressors_js_1.shouldSuppress)(text, match))
73
+ continue;
74
+ if (match.patternDef.validator === null && suppressionZones.length > 0) {
75
+ if (suppressionZones.some(([zStart, zEnd]) => match.start >= zStart && match.end <= zEnd)) {
76
+ continue;
77
+ }
78
+ }
79
+ const hasValidator = match.patternDef.validator !== null;
80
+ const isValid = this.matcher.validate(match);
81
+ const priority = (hasValidator && isValid) ? 3 : match.countryCode === "CUSTOM" ? 2 : 1;
82
+ const spanLength = match.end - match.start;
83
+ const score = priority * 1_000_000 + spanLength;
84
+ candidates.push({
85
+ det: {
86
+ entityType: match.patternDef.entityType,
87
+ start: match.start,
88
+ end: match.end,
89
+ text: match.text,
90
+ source: types_js_1.DetectionSource.RULES,
91
+ country: (match.countryCode !== "SHARED" && match.countryCode !== "CUSTOM") ? match.countryCode : null,
92
+ confidence: "high",
93
+ },
94
+ score,
95
+ });
96
+ }
97
+ for (const d of (0, structural_js_1.detectStructuralDob)(text)) {
98
+ candidates.push({ det: d, score: 1_000_000 + (d.end - d.start) });
99
+ }
100
+ return this.deduplicate(candidates);
101
+ }
102
+ /**
103
+ * Remove overlapping detections with priority-aware resolution.
104
+ * Priority: validated (3) > custom (2) > regex-only (1).
105
+ * Within the same tier, longer span wins.
106
+ */
107
+ deduplicate(candidates) {
108
+ if (candidates.length === 0)
109
+ return [];
110
+ const sorted = [...candidates].sort((a, b) => b.score - a.score);
111
+ const result = [];
112
+ for (const { det } of sorted) {
113
+ let overlaps = false;
114
+ for (const kept of result) {
115
+ if (det.start < kept.end && det.end > kept.start) {
116
+ overlaps = true;
117
+ break;
118
+ }
119
+ }
120
+ if (!overlaps)
121
+ result.push(det);
122
+ }
123
+ return result;
124
+ }
125
+ getLoadedCountries() {
126
+ return new Set(this.loadedCountries);
127
+ }
128
+ }
129
+ exports.RuleEngine = RuleEngine;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MultiPatternMatcher = void 0;
4
+ const validators_js_1 = require("./validators.js");
5
+ class MultiPatternMatcher {
6
+ patterns = [];
7
+ compiled = false;
8
+ addPattern(patternDef, countryCode) {
9
+ const regex = new RegExp(patternDef.pattern, "gu");
10
+ this.patterns.push({ regex, patternDef, countryCode });
11
+ this.compiled = false;
12
+ }
13
+ addCountry(config) {
14
+ for (const pdef of config.patterns) {
15
+ // Convert Python-style \b word boundaries — JS handles them natively
16
+ // But we need the 'u' flag for Unicode support
17
+ const regex = new RegExp(pdef.pattern, "gu");
18
+ this.patterns.push({ regex, patternDef: pdef, countryCode: config.code });
19
+ }
20
+ this.compiled = false;
21
+ }
22
+ compile() {
23
+ this.compiled = true;
24
+ }
25
+ scan(text) {
26
+ if (!this.compiled)
27
+ this.compile();
28
+ const matches = [];
29
+ for (const { regex, patternDef, countryCode } of this.patterns) {
30
+ regex.lastIndex = 0;
31
+ let m;
32
+ while ((m = regex.exec(text)) !== null) {
33
+ matches.push({
34
+ start: m.index,
35
+ end: m.index + m[0].length,
36
+ text: m[0],
37
+ patternDef,
38
+ countryCode,
39
+ });
40
+ // Prevent infinite loops on zero-length matches
41
+ if (m[0].length === 0)
42
+ regex.lastIndex++;
43
+ }
44
+ }
45
+ return matches;
46
+ }
47
+ validate(match) {
48
+ if (match.patternDef.validator === null)
49
+ return true;
50
+ const validator = validators_js_1.VALIDATORS[match.patternDef.validator];
51
+ if (!validator)
52
+ return true;
53
+ return validator(match.text);
54
+ }
55
+ }
56
+ exports.MultiPatternMatcher = MultiPatternMatcher;
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectStructuralDob = detectStructuralDob;
4
+ const types_js_1 = require("../types.js");
5
+ const DATE_RE = /(?:0[1-9]|[12]\d|3[01])[/.\-](?:0[1-9]|1[0-2])[/.\-](?:19|20)\d{2}|(?:19|20)\d{2}[/.\-](?:0[1-9]|1[0-2])[/.\-](?:0[1-9]|[12]\d|3[01])/g;
6
+ const DOB_FIELD_NAMES = [
7
+ "geboortedatum", "date_naissance", "date de naissance", "geburtsdatum",
8
+ "dob", "birth_date", "birthdate", "date_of_birth",
9
+ "fecha_nacimiento", "data_nascita", "born",
10
+ "naissance", "geboren",
11
+ ];
12
+ const JSON_KV = /"([^"]+?)"\s*:\s*"([^"]*?)"/g;
13
+ function detectJsonDob(text) {
14
+ const detections = [];
15
+ JSON_KV.lastIndex = 0;
16
+ let m;
17
+ while ((m = JSON_KV.exec(text)) !== null) {
18
+ const key = m[1].toLowerCase().trim();
19
+ const value = m[2];
20
+ if (!DOB_FIELD_NAMES.some(kw => key.includes(kw)))
21
+ continue;
22
+ DATE_RE.lastIndex = 0;
23
+ const dm = DATE_RE.exec(value);
24
+ if (dm) {
25
+ const valStart = m.index + m[0].indexOf(m[2]);
26
+ const absStart = valStart + dm.index;
27
+ const absEnd = absStart + dm[0].length;
28
+ detections.push({
29
+ entityType: types_js_1.EntityType.DOB,
30
+ start: absStart,
31
+ end: absEnd,
32
+ text: text.slice(absStart, absEnd),
33
+ source: types_js_1.DetectionSource.RULES,
34
+ country: null,
35
+ confidence: "high",
36
+ });
37
+ }
38
+ }
39
+ return detections;
40
+ }
41
+ function detectDelimiter(header) {
42
+ for (const delim of ["|", ";", "\t", ","]) {
43
+ if ((header.match(new RegExp(delim.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g")) || []).length >= 2)
44
+ return delim;
45
+ }
46
+ return null;
47
+ }
48
+ function detectCsvDob(text) {
49
+ const detections = [];
50
+ const lines = text.split("\n");
51
+ if (lines.length < 2)
52
+ return detections;
53
+ const headerLine = lines[0];
54
+ const delimiter = detectDelimiter(headerLine);
55
+ if (!delimiter)
56
+ return detections;
57
+ const headers = headerLine.split(delimiter).map(h => h.trim().toLowerCase());
58
+ if (headers.length < 2)
59
+ return detections;
60
+ const dobColumns = [];
61
+ for (let i = 0; i < headers.length; i++) {
62
+ if (DOB_FIELD_NAMES.some(kw => headers[i].includes(kw)))
63
+ dobColumns.push(i);
64
+ }
65
+ if (dobColumns.length === 0)
66
+ return detections;
67
+ let offset = lines[0].length + 1;
68
+ for (let li = 1; li < lines.length; li++) {
69
+ const line = lines[li];
70
+ if (!line.trim()) {
71
+ offset += line.length + 1;
72
+ continue;
73
+ }
74
+ const fields = line.split(delimiter);
75
+ for (const colIdx of dobColumns) {
76
+ if (colIdx >= fields.length)
77
+ continue;
78
+ const field = fields[colIdx];
79
+ DATE_RE.lastIndex = 0;
80
+ const dm = DATE_RE.exec(field);
81
+ if (dm) {
82
+ let fieldOffset = 0;
83
+ for (let fi = 0; fi < colIdx; fi++)
84
+ fieldOffset += fields[fi].length + delimiter.length;
85
+ const absStart = offset + fieldOffset + dm.index;
86
+ const absEnd = absStart + dm[0].length;
87
+ detections.push({
88
+ entityType: types_js_1.EntityType.DOB,
89
+ start: absStart,
90
+ end: absEnd,
91
+ text: text.slice(absStart, absEnd),
92
+ source: types_js_1.DetectionSource.RULES,
93
+ country: null,
94
+ confidence: "high",
95
+ });
96
+ }
97
+ }
98
+ offset += line.length + 1;
99
+ }
100
+ return detections;
101
+ }
102
+ function detectStructuralDob(text) {
103
+ return [...detectJsonDob(text), ...detectCsvDob(text)];
104
+ }
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.shouldSuppress = shouldSuppress;
4
+ const types_js_1 = require("../types.js");
5
+ const CONTEXT_CHARS = 150;
6
+ function getContext(text, start, end) {
7
+ const ctxStart = Math.max(0, start - CONTEXT_CHARS);
8
+ const ctxEnd = Math.min(text.length, end + CONTEXT_CHARS);
9
+ return [text.slice(ctxStart, start), text.slice(end, ctxEnd)];
10
+ }
11
+ // Unicode-safe word boundary: negative lookahead for any letter (including accented)
12
+ const _UWB = "(?![a-zA-Z\\u00C0-\\u024F\\u0400-\\u04FF])";
13
+ const CURRENCY_AFTER = new RegExp(`^\\s*(?:EUR|€|\\$|USD|GBP|£|CHF|ISK|SEK|NOK|DKK|euro|euros|dollar|dollars|pond|kronor|kroner|kr)${_UWB}`, "i");
14
+ const CURRENCY_COMMA_AFTER = new RegExp(`^[.,]\\d{1,2}\\s*(?:EUR|€|\\$|USD|GBP|£|CHF|ISK|SEK|NOK|DKK|euro|euros|kr(?:onor|oner)?|pond)${_UWB}`, "i");
15
+ const CURRENCY_BEFORE = /(?:EUR|€|\$|USD|GBP|£|CHF|ISK|SEK|NOK|DKK)\s*$/;
16
+ const AMOUNT_LABEL_BEFORE = /(?:Montant|Beløb|Summa|Summe|Bedrag|Amount|Total|TTC|inkl|Upphæð)\s*:?\s*$/i;
17
+ // Use Unicode-aware word boundary via \p{L} negative lookahead instead of \b
18
+ // because JS \b is ASCII-only and fails on Unicode letters (e.g. "München" → \bm\b matches M before ü)
19
+ const UNIT_AFTER = /^\s*(?:kg|km|cm|mm|m[²³]|ml|mg|GB|MB|KB|TB|%|jaar|maanden|weken|dagen|uur|minuten|seconden|stuks|pcs|pieces|ans|mois|semaines|jours|heures|Jahre|Monate|Wochen|Tage|Stunden)(?![a-zA-Z\u00C0-\u024F\u0400-\u04FF])/i;
20
+ const REFERENCE_BEFORE = /(?:dossier|ref\.?|referentie|reference|référence|factuurnummer|invoice\s*(?:nr|number|no)?|bestelnummer|order\s*(?:nr|number|no)?|kenmerk|ordernummer|Aktenzeichen|numéro\s*de\s*(?:dossier|facture|commande)|bestellnummer|Rechnungsnummer|artikelnr|article\s*no|contract\s*(?:nr|number|no)?|pagina|page|Seite|blz\.?|Facture\s*n[°o]?|Faktura\s*n[°or]\.?|Lasku\s*n[°or]o?\.?|Rechnung\s*(?:Nr|n[°o])?|faktura\s*(?:nr|n[°o])?|bestilling\s*(?:nr|n[°o])?|bestelling\s*n[°or]\.?|Reikningur\s*nr)\s*[:.]?\s*$/i;
21
+ const LEGAL_BEFORE = /(?:Art(?:ikel|icle|\.)|§|Artikel|Section|Sectie|Afdeling|paragraaf|Absatz|alinéa|punt|point|Punkt|lid)\s*$/i;
22
+ const MATH_BEFORE = /[=+\-×÷*/]\s*$/;
23
+ const MATH_AFTER = /^\s*[=+\-×÷*/]/;
24
+ const SEQUENTIAL_PATTERNS = /^(?:0{6,}|1234567890?|0123456789|9876543210?|1111111111?|000000000|123456789)$/;
25
+ const YEAR_PATTERN = /^(?:19[4-9]\d|20[0-3]\d)$/;
26
+ const DATE_KEYWORD_NEAR = /(?:jaar|year|année|Jahr|datum|date|Datum|in\s+\d{4}|since|sinds|depuis|seit|født|fødselsdato|fødsel|Fødselsdato|född|födelsedatum|födelsedag|syntynyt|syntymäaika|fæddur|fæðingardagur|\d{2}\.\d{2}\.|(?:januar|februar|marts|april|maj|juni|juli|august|september|oktober|november|december|januari|februari|mars|april|mei|juin|juillet|août|Tiltr[æa]delsesdato|Tiltredelsesdato))/i;
27
+ const ID_LABEL_BEFORE = /(?:BSN|RR|NN|NIR|INSZ|NISS|NIS|Steuer-?ID|TIN|NIF|NIE|SSN|rijksregisternummer|numéro\s*national|national\s*number|matricule|Ausweisnummer|Personalausweis|Versichertennummer|KVNR|KV-Nr|Steuernummer|St\.\-Nr|StNr|Finanzamt\s+ist|Ondernemingen\s+onder\s+nummer|ondernemingsnummer|numéro\s*d'entreprise|enterprise\s*number|Kruispuntbank)\s*[:.]?\s*$/i;
28
+ const SERVICE_NUMBER = /^0800[\-\s]/;
29
+ const DATE_PATTERN_FULL = /^\d{2}[-/.]\d{2}[-/.]\d{4}$/;
30
+ const PASSPORT_CONTEXT_BEFORE = /(?:Reisepass|passport|passeport|paspoort|Bisheriger\s+Reisepass)\s*(?:Nr\.?|Nummer|nummer|number|n[°o])?\s*[:.]?\s*$/i;
31
+ const SE_ORG_CONTEXT_BEFORE = /(?:org\.?\s*nr\.?|organisationsnummer|organisationsnr|Bolagsverket|företag)\s*[:.]?\s*$/i;
32
+ const NUMERIC_TYPES = new Set([types_js_1.EntityType.PHONE, types_js_1.EntityType.NATIONAL_ID, types_js_1.EntityType.SSN, types_js_1.EntityType.TAX_ID, types_js_1.EntityType.POSTAL_CODE]);
33
+ function suppressCurrency(text, match) {
34
+ if (!NUMERIC_TYPES.has(match.patternDef.entityType))
35
+ return false;
36
+ const [before, after] = getContext(text, match.start, match.end);
37
+ if (CURRENCY_AFTER.test(after) || CURRENCY_BEFORE.test(before))
38
+ return true;
39
+ if (CURRENCY_COMMA_AFTER.test(after))
40
+ return true;
41
+ if (AMOUNT_LABEL_BEFORE.test(before))
42
+ return true;
43
+ return false;
44
+ }
45
+ function suppressUnits(text, match) {
46
+ if (!NUMERIC_TYPES.has(match.patternDef.entityType))
47
+ return false;
48
+ const [, after] = getContext(text, match.start, match.end);
49
+ return UNIT_AFTER.test(after);
50
+ }
51
+ function suppressReference(text, match) {
52
+ const applicable = new Set([types_js_1.EntityType.PHONE, types_js_1.EntityType.NATIONAL_ID, types_js_1.EntityType.SSN, types_js_1.EntityType.TAX_ID, types_js_1.EntityType.IBAN, types_js_1.EntityType.CHAMBER_OF_COMMERCE]);
53
+ if (!applicable.has(match.patternDef.entityType))
54
+ return false;
55
+ const [before] = getContext(text, match.start, match.end);
56
+ return REFERENCE_BEFORE.test(before);
57
+ }
58
+ function suppressLegal(text, match) {
59
+ const applicable = new Set([types_js_1.EntityType.PHONE, types_js_1.EntityType.NATIONAL_ID, types_js_1.EntityType.POSTAL_CODE]);
60
+ if (!applicable.has(match.patternDef.entityType))
61
+ return false;
62
+ const [before] = getContext(text, match.start, match.end);
63
+ return LEGAL_BEFORE.test(before);
64
+ }
65
+ function suppressMath(text, match) {
66
+ if (!NUMERIC_TYPES.has(match.patternDef.entityType))
67
+ return false;
68
+ const [before, after] = getContext(text, match.start, match.end);
69
+ return MATH_BEFORE.test(before) || MATH_AFTER.test(after);
70
+ }
71
+ function suppressSequential(_text, match) {
72
+ const clean = match.text.replace(/[\s.\-]/g, "");
73
+ return SEQUENTIAL_PATTERNS.test(clean);
74
+ }
75
+ function suppressYearAsPostal(text, match) {
76
+ if (match.patternDef.entityType !== types_js_1.EntityType.POSTAL_CODE)
77
+ return false;
78
+ const clean = match.text.trim();
79
+ if (!YEAR_PATTERN.test(clean))
80
+ return false;
81
+ const immediateBefore = text.slice(Math.max(0, match.start - 3), match.start);
82
+ if (immediateBefore.endsWith(", ") || immediateBefore.endsWith(",\n"))
83
+ return false;
84
+ const [before, after] = getContext(text, match.start, match.end);
85
+ return DATE_KEYWORD_NEAR.test(before + after);
86
+ }
87
+ function suppressPhoneAfterIdLabel(text, match) {
88
+ if (match.patternDef.entityType !== types_js_1.EntityType.PHONE)
89
+ return false;
90
+ const [before] = getContext(text, match.start, match.end);
91
+ return ID_LABEL_BEFORE.test(before);
92
+ }
93
+ function suppressPhoneServiceNumber(_text, match) {
94
+ if (match.patternDef.entityType !== types_js_1.EntityType.PHONE)
95
+ return false;
96
+ return SERVICE_NUMBER.test(match.text);
97
+ }
98
+ function suppressPhoneDateOverlap(_text, match) {
99
+ if (match.patternDef.entityType !== types_js_1.EntityType.PHONE)
100
+ return false;
101
+ return DATE_PATTERN_FULL.test(match.text.trim());
102
+ }
103
+ const NOT_CITY_CODES = new Set([
104
+ "ID", "NR", "NO", "ST", "DR", "MR", "MS", "HR", "FR", "IM", "IN", "OR", "IF", "IS", "IT", "AT", "AD", "AG", "AV",
105
+ "BE", "DE", "EU", "NL", "LU", "WS", "SS", "IP",
106
+ ]);
107
+ function suppressPlateInCompound(text, match) {
108
+ if (match.patternDef.entityType !== types_js_1.EntityType.LICENSE_PLATE)
109
+ return false;
110
+ if (match.start >= 3) {
111
+ const before = text.slice(Math.max(0, match.start - 10), match.start);
112
+ if (/[A-Za-zÄÖÜäöüß]{2,}-$/.test(before))
113
+ return true;
114
+ }
115
+ const matched = match.text.trim();
116
+ const parts = matched.split(/[\s\-]+/);
117
+ if (parts.length > 0 && NOT_CITY_CODES.has(parts[0])) {
118
+ const afterChar = match.end < text.length ? text[match.end] : "";
119
+ const beforeChar = match.start > 0 ? text[match.start - 1] : "";
120
+ if (/\d/.test(afterChar) || beforeChar === "-")
121
+ return true;
122
+ if (parts[0] === "WS" || parts[0] === "SS")
123
+ return true;
124
+ if (parts[0] === "IP") {
125
+ const afterTwo = text.slice(match.end, match.end + 2);
126
+ if (afterTwo.length >= 2 && afterTwo[0] === "." && /\d/.test(afterTwo[1]))
127
+ return true;
128
+ }
129
+ }
130
+ if (matched.startsWith("HRA") || matched.startsWith("HRB"))
131
+ return true;
132
+ const [before, after] = getContext(text, match.start, match.end);
133
+ if (/(?:Semester|Hochschule|Uni\b)/i.test(before + after)) {
134
+ if (parts.length > 0 && (parts[0] === "WS" || parts[0] === "SS"))
135
+ return true;
136
+ }
137
+ return false;
138
+ }
139
+ function suppressNatidAsPassport(text, match) {
140
+ if (match.patternDef.entityType !== types_js_1.EntityType.NATIONAL_ID)
141
+ return false;
142
+ const [before] = getContext(text, match.start, match.end);
143
+ return PASSPORT_CONTEXT_BEFORE.test(before);
144
+ }
145
+ function suppressSeNatidAsOrg(text, match) {
146
+ if (match.patternDef.entityType !== types_js_1.EntityType.NATIONAL_ID)
147
+ return false;
148
+ if (match.countryCode !== "SE")
149
+ return false;
150
+ const [before] = getContext(text, match.start, match.end);
151
+ return SE_ORG_CONTEXT_BEFORE.test(before);
152
+ }
153
+ function suppressPostalInsideIban(text, match) {
154
+ if (match.patternDef.entityType !== types_js_1.EntityType.POSTAL_CODE)
155
+ return false;
156
+ const beforeChar = match.start > 0 ? text[match.start - 1] : " ";
157
+ const afterChar = match.end < text.length ? text[match.end] : " ";
158
+ if (/[A-Za-z0-9]/.test(beforeChar) && /[A-Za-z0-9]/.test(afterChar))
159
+ return true;
160
+ if (match.start >= 5) {
161
+ const prefix = text.slice(match.start - 5, match.start);
162
+ if (/[A-Z]{2}\d{2}\s$/.test(prefix))
163
+ return true;
164
+ }
165
+ return false;
166
+ }
167
+ function suppressPostalAsHouseNumber(text, match) {
168
+ if (match.patternDef.entityType !== types_js_1.EntityType.POSTAL_CODE)
169
+ return false;
170
+ const clean = match.text.replace(/\s/g, "");
171
+ if (clean.length > 3)
172
+ return false;
173
+ const before = text.slice(Math.max(0, match.start - 30), match.start);
174
+ if (/[a-záéíóúýþæöðA-ZÁÉÍÓÚÝÞÆÖÐ]{3,}\s+$/.test(before)) {
175
+ const after = text.slice(match.end, match.end + 5);
176
+ if (!/^,?\s+[A-ZÁÉÍÓÚÝÞÆÖÐ]/.test(after))
177
+ return true;
178
+ }
179
+ return false;
180
+ }
181
+ function suppressRequiresContext(text, match) {
182
+ if (!match.patternDef.requiresContext || match.patternDef.contextKeywords.length === 0)
183
+ return false;
184
+ const [before, after] = getContext(text, match.start, match.end);
185
+ const context = (before + " " + after).toLowerCase();
186
+ return !match.patternDef.contextKeywords.some(kw => context.includes(kw.toLowerCase()));
187
+ }
188
+ const TYPE_SUPPRESSORS = {
189
+ [types_js_1.EntityType.PHONE]: [suppressCurrency, suppressUnits, suppressReference, suppressMath, suppressPhoneAfterIdLabel, suppressPhoneServiceNumber, suppressPhoneDateOverlap],
190
+ [types_js_1.EntityType.NATIONAL_ID]: [suppressCurrency, suppressUnits, suppressReference, suppressLegal, suppressMath, suppressNatidAsPassport, suppressSeNatidAsOrg],
191
+ [types_js_1.EntityType.SSN]: [suppressCurrency, suppressUnits, suppressReference, suppressMath],
192
+ [types_js_1.EntityType.TAX_ID]: [suppressCurrency, suppressUnits, suppressReference, suppressMath],
193
+ [types_js_1.EntityType.POSTAL_CODE]: [suppressCurrency, suppressUnits, suppressMath, suppressLegal, suppressYearAsPostal, suppressPostalInsideIban, suppressPostalAsHouseNumber],
194
+ [types_js_1.EntityType.IBAN]: [suppressReference],
195
+ [types_js_1.EntityType.LICENSE_PLATE]: [suppressPlateInCompound],
196
+ [types_js_1.EntityType.CHAMBER_OF_COMMERCE]: [suppressReference],
197
+ };
198
+ function shouldSuppress(text, match) {
199
+ if (suppressSequential(text, match))
200
+ return true;
201
+ const typeSups = TYPE_SUPPRESSORS[match.patternDef.entityType];
202
+ if (typeSups) {
203
+ for (const s of typeSups) {
204
+ if (s(text, match))
205
+ return true;
206
+ }
207
+ }
208
+ if (match.patternDef.requiresContext)
209
+ return suppressRequiresContext(text, match);
210
+ return false;
211
+ }