euredact 0.0.1 → 0.1.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.
- package/LICENSE +189 -0
- package/README.md +86 -10
- package/dist/cjs/cache.js +56 -0
- package/dist/cjs/index.js +30 -0
- package/dist/cjs/normalizer.js +45 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/rules/countries/index.js +578 -0
- package/dist/cjs/rules/engine.js +88 -0
- package/dist/cjs/rules/matchers.js +51 -0
- package/dist/cjs/rules/structural.js +104 -0
- package/dist/cjs/rules/suppressors.js +211 -0
- package/dist/cjs/rules/validators.js +669 -0
- package/dist/cjs/sdk.js +76 -0
- package/dist/cjs/types.js +41 -0
- package/dist/esm/cache.d.ts +11 -0
- package/dist/esm/cache.js +52 -0
- package/dist/esm/index.d.ts +9 -0
- package/dist/esm/index.js +20 -0
- package/dist/esm/normalizer.d.ts +2 -0
- package/dist/esm/normalizer.js +41 -0
- package/dist/esm/rules/countries/index.d.ts +2 -0
- package/dist/esm/rules/countries/index.js +575 -0
- package/dist/esm/rules/engine.d.ts +9 -0
- package/dist/esm/rules/engine.js +84 -0
- package/dist/esm/rules/matchers.d.ts +11 -0
- package/dist/esm/rules/matchers.js +47 -0
- package/dist/esm/rules/structural.d.ts +2 -0
- package/dist/esm/rules/structural.js +101 -0
- package/dist/esm/rules/suppressors.d.ts +9 -0
- package/dist/esm/rules/suppressors.js +208 -0
- package/dist/esm/rules/validators.d.ts +43 -0
- package/dist/esm/rules/validators.js +624 -0
- package/dist/esm/sdk.d.ts +15 -0
- package/dist/esm/sdk.js +72 -0
- package/dist/esm/types.d.ts +64 -0
- package/dist/esm/types.js +38 -0
- package/package.json +55 -9
- package/index.js +0 -3
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
loadCountries(countryCodes) {
|
|
13
|
+
let configs;
|
|
14
|
+
if (countryCodes === null) {
|
|
15
|
+
configs = Object.values(index_js_1.COUNTRY_CONFIGS);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
configs = [];
|
|
19
|
+
for (const c of countryCodes) {
|
|
20
|
+
const code = c.toUpperCase();
|
|
21
|
+
const config = index_js_1.COUNTRY_CONFIGS[code];
|
|
22
|
+
if (config)
|
|
23
|
+
configs.push(config);
|
|
24
|
+
// Silently skip unknown country codes (e.g. "INTL")
|
|
25
|
+
}
|
|
26
|
+
const shared = index_js_1.COUNTRY_CONFIGS["SHARED"];
|
|
27
|
+
if (shared)
|
|
28
|
+
configs.push(shared);
|
|
29
|
+
}
|
|
30
|
+
let newCountries = false;
|
|
31
|
+
for (const config of configs) {
|
|
32
|
+
if (!this.loadedCountries.has(config.code)) {
|
|
33
|
+
this.matcher.addCountry(config);
|
|
34
|
+
this.loadedCountries.add(config.code);
|
|
35
|
+
newCountries = true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (newCountries)
|
|
39
|
+
this.matcher.compile();
|
|
40
|
+
}
|
|
41
|
+
detect(text, countryCodes) {
|
|
42
|
+
this.loadCountries(countryCodes);
|
|
43
|
+
let rawMatches = this.matcher.scan(text);
|
|
44
|
+
if (countryCodes !== null) {
|
|
45
|
+
const codesUpper = new Set(countryCodes.map(c => c.toUpperCase()));
|
|
46
|
+
rawMatches = rawMatches.filter(m => codesUpper.has(m.countryCode) || m.countryCode === "SHARED");
|
|
47
|
+
}
|
|
48
|
+
const validated = rawMatches.filter(m => this.matcher.validate(m));
|
|
49
|
+
const detections = [];
|
|
50
|
+
for (const match of validated) {
|
|
51
|
+
if ((0, suppressors_js_1.shouldSuppress)(text, match))
|
|
52
|
+
continue;
|
|
53
|
+
detections.push({
|
|
54
|
+
entityType: match.patternDef.entityType,
|
|
55
|
+
start: match.start,
|
|
56
|
+
end: match.end,
|
|
57
|
+
text: match.text,
|
|
58
|
+
source: types_js_1.DetectionSource.RULES,
|
|
59
|
+
country: match.countryCode !== "SHARED" ? match.countryCode : null,
|
|
60
|
+
confidence: "high",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
detections.push(...(0, structural_js_1.detectStructuralDob)(text));
|
|
64
|
+
return this.deduplicate(detections);
|
|
65
|
+
}
|
|
66
|
+
deduplicate(detections) {
|
|
67
|
+
if (detections.length === 0)
|
|
68
|
+
return detections;
|
|
69
|
+
const sorted = [...detections].sort((a, b) => a.start - b.start || (b.end - b.start) - (a.end - a.start));
|
|
70
|
+
const result = [];
|
|
71
|
+
for (const det of sorted) {
|
|
72
|
+
let overlaps = false;
|
|
73
|
+
for (const kept of result) {
|
|
74
|
+
if (det.start < kept.end && det.end > kept.start) {
|
|
75
|
+
overlaps = true;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!overlaps)
|
|
80
|
+
result.push(det);
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
getLoadedCountries() {
|
|
85
|
+
return new Set(this.loadedCountries);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.RuleEngine = RuleEngine;
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
addCountry(config) {
|
|
9
|
+
for (const pdef of config.patterns) {
|
|
10
|
+
// Convert Python-style \b word boundaries — JS handles them natively
|
|
11
|
+
// But we need the 'u' flag for Unicode support
|
|
12
|
+
const regex = new RegExp(pdef.pattern, "gu");
|
|
13
|
+
this.patterns.push({ regex, patternDef: pdef, countryCode: config.code });
|
|
14
|
+
}
|
|
15
|
+
this.compiled = false;
|
|
16
|
+
}
|
|
17
|
+
compile() {
|
|
18
|
+
this.compiled = true;
|
|
19
|
+
}
|
|
20
|
+
scan(text) {
|
|
21
|
+
if (!this.compiled)
|
|
22
|
+
this.compile();
|
|
23
|
+
const matches = [];
|
|
24
|
+
for (const { regex, patternDef, countryCode } of this.patterns) {
|
|
25
|
+
regex.lastIndex = 0;
|
|
26
|
+
let m;
|
|
27
|
+
while ((m = regex.exec(text)) !== null) {
|
|
28
|
+
matches.push({
|
|
29
|
+
start: m.index,
|
|
30
|
+
end: m.index + m[0].length,
|
|
31
|
+
text: m[0],
|
|
32
|
+
patternDef,
|
|
33
|
+
countryCode,
|
|
34
|
+
});
|
|
35
|
+
// Prevent infinite loops on zero-length matches
|
|
36
|
+
if (m[0].length === 0)
|
|
37
|
+
regex.lastIndex++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return matches;
|
|
41
|
+
}
|
|
42
|
+
validate(match) {
|
|
43
|
+
if (match.patternDef.validator === null)
|
|
44
|
+
return true;
|
|
45
|
+
const validator = validators_js_1.VALIDATORS[match.patternDef.validator];
|
|
46
|
+
if (!validator)
|
|
47
|
+
return true;
|
|
48
|
+
return validator(match.text);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
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
|
+
}
|