@stll/anonymize 0.0.3 → 0.0.5
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/index.d.ts +1 -1
- package/dist/index.js +108 -14
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -204,7 +204,7 @@ type PipelineConfig = {
|
|
|
204
204
|
* every pipeline run and never persisted to the database.
|
|
205
205
|
* Renaming a label here requires no migration.
|
|
206
206
|
*/
|
|
207
|
-
declare const DEFAULT_ENTITY_LABELS: readonly ["person", "organization", "phone number", "address", "email address", "date", "date of birth", "bank account number", "iban", "tax identification number", "identity card number", "registration number", "credit card number", "passport number", "monetary amount"];
|
|
207
|
+
declare const DEFAULT_ENTITY_LABELS: readonly ["person", "organization", "phone number", "address", "email address", "date", "date of birth", "bank account number", "iban", "tax identification number", "identity card number", "registration number", "credit card number", "passport number", "monetary amount", "land parcel"];
|
|
208
208
|
//#endregion
|
|
209
209
|
//#region src/detectors/regex.d.ts
|
|
210
210
|
type RegexMeta = {
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { at, be, bg, cy, cz, de, dk, ee, es, fi, fr, gb, gr, hr, hu, ie, it, lt, lu, lv, mt, nl, pl, pt, ro, se, si, sk } from "@stll/stdnum";
|
|
2
3
|
import { toRegex } from "@stll/stdnum/patterns";
|
|
3
4
|
import { TextSearch } from "@stll/text-search";
|
|
5
|
+
//#region \0rolldown/runtime.js
|
|
6
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
7
|
+
//#endregion
|
|
4
8
|
//#region src/types.ts
|
|
5
9
|
/**
|
|
6
10
|
* Source of a detected entity span.
|
|
@@ -59,7 +63,8 @@ const DEFAULT_ENTITY_LABELS = [
|
|
|
59
63
|
"registration number",
|
|
60
64
|
"credit card number",
|
|
61
65
|
"passport number",
|
|
62
|
-
"monetary amount"
|
|
66
|
+
"monetary amount",
|
|
67
|
+
"land parcel"
|
|
63
68
|
];
|
|
64
69
|
//#endregion
|
|
65
70
|
//#region src/context.ts
|
|
@@ -140,6 +145,7 @@ const LOADER_REGISTRIES = {
|
|
|
140
145
|
it: () => import("@stll/anonymize-data/config/triggers.it.json"),
|
|
141
146
|
pl: () => import("@stll/anonymize-data/config/triggers.pl.json"),
|
|
142
147
|
ro: () => import("@stll/anonymize-data/config/triggers.ro.json"),
|
|
148
|
+
sk: () => import("@stll/anonymize-data/config/triggers.sk.json"),
|
|
143
149
|
sv: () => import("@stll/anonymize-data/config/triggers.sv.json")
|
|
144
150
|
},
|
|
145
151
|
coreference: {
|
|
@@ -160,6 +166,7 @@ const FALLBACK_LANGUAGES = {
|
|
|
160
166
|
"it",
|
|
161
167
|
"pl",
|
|
162
168
|
"ro",
|
|
169
|
+
"sk",
|
|
163
170
|
"sv"
|
|
164
171
|
],
|
|
165
172
|
coreference: [
|
|
@@ -991,6 +998,68 @@ const POST_NOMINALS = [
|
|
|
991
998
|
"CIPP"
|
|
992
999
|
];
|
|
993
1000
|
//#endregion
|
|
1001
|
+
//#region src/util/char-groups.ts
|
|
1002
|
+
/** Chars that need escaping inside a regex char class. */
|
|
1003
|
+
const REGEX_CLASS_SPECIAL = /[\\\]^-]/;
|
|
1004
|
+
const escapeForCharClass = (ch) => REGEX_CLASS_SPECIAL.test(ch) ? `\\${ch}` : ch;
|
|
1005
|
+
let cached;
|
|
1006
|
+
const loadConfig = () => {
|
|
1007
|
+
if (cached) return cached;
|
|
1008
|
+
cached = __require("@stll/anonymize-data/config/char-groups.json");
|
|
1009
|
+
return cached;
|
|
1010
|
+
};
|
|
1011
|
+
/**
|
|
1012
|
+
* Get the raw characters for a named group.
|
|
1013
|
+
* Throws if the group does not exist.
|
|
1014
|
+
*/
|
|
1015
|
+
const charSet = (group) => {
|
|
1016
|
+
const g = loadConfig().groups[group];
|
|
1017
|
+
if (!g) throw new Error(`Unknown char group: "${group}"`);
|
|
1018
|
+
return g.chars.map((entry) => entry.char);
|
|
1019
|
+
};
|
|
1020
|
+
/**
|
|
1021
|
+
* Build a regex character class string for a named
|
|
1022
|
+
* group. E.g., charClass("dash") returns a string
|
|
1023
|
+
* like "[-\u2013\u2014\u2010\u2011\u2212\u2043]".
|
|
1024
|
+
*
|
|
1025
|
+
* The hyphen-minus is placed first so it is treated
|
|
1026
|
+
* as a literal, not a range indicator.
|
|
1027
|
+
*/
|
|
1028
|
+
const charClass = (group) => {
|
|
1029
|
+
return `[${[...charSet(group)].sort((a, b) => {
|
|
1030
|
+
if (a === "-") return -1;
|
|
1031
|
+
if (b === "-") return 1;
|
|
1032
|
+
return a.localeCompare(b);
|
|
1033
|
+
}).map(escapeForCharClass).join("")}]`;
|
|
1034
|
+
};
|
|
1035
|
+
/**
|
|
1036
|
+
* Return the inner content of a character class (without
|
|
1037
|
+
* the surrounding brackets). Useful for embedding a group
|
|
1038
|
+
* inside a larger character class, e.g.:
|
|
1039
|
+
* `[\\s&,.${charClassInner("dash")}]`
|
|
1040
|
+
*/
|
|
1041
|
+
const charClassInner = (group) => {
|
|
1042
|
+
return [...charSet(group)].sort((a, b) => {
|
|
1043
|
+
if (a === "-") return -1;
|
|
1044
|
+
if (b === "-") return 1;
|
|
1045
|
+
return a.localeCompare(b);
|
|
1046
|
+
}).map(escapeForCharClass).join("");
|
|
1047
|
+
};
|
|
1048
|
+
/** Regex char class matching all dash variants. */
|
|
1049
|
+
const DASH = charClass("dash");
|
|
1050
|
+
/**
|
|
1051
|
+
* Inner content of the dash char class (no brackets).
|
|
1052
|
+
* For embedding inside a larger character class:
|
|
1053
|
+
* `[\\s&,.${DASH_INNER}]`
|
|
1054
|
+
*/
|
|
1055
|
+
const DASH_INNER = charClassInner("dash");
|
|
1056
|
+
charClass("space");
|
|
1057
|
+
charClass("quote-double");
|
|
1058
|
+
charClass("quote-single");
|
|
1059
|
+
charClass("slash");
|
|
1060
|
+
charClass("dot");
|
|
1061
|
+
charClass("colon");
|
|
1062
|
+
//#endregion
|
|
994
1063
|
//#region src/detectors/regex.ts
|
|
995
1064
|
const MIN_PHONE_LENGTH = 7;
|
|
996
1065
|
const MIN_MONTH_NAME_LENGTH = 3;
|
|
@@ -1328,9 +1397,9 @@ const buildCurrencyPatterns = (data) => {
|
|
|
1328
1397
|
if (!symbols && !trailingAlt) return [];
|
|
1329
1398
|
const NUM = "(?:\\d{1,3}(?:[,.'[^\\S\\n\\t]]\\d{3})+|\\d{1,9})";
|
|
1330
1399
|
const patterns = [];
|
|
1331
|
-
if (symbols) patterns.push(`(?:[${symbols}])[^\\S\\n\\t]?${NUM}(?:[.,](?:\\d{1,2}
|
|
1332
|
-
if (trailingAlt) patterns.push(`\\b(?:${trailingAlt})[^\\S\\n\\t]{0,2}${NUM}(?:[.,](?:\\d{1,2}
|
|
1333
|
-
if (trailingAlt) patterns.push(`\\b${NUM}(?:[.,](?:\\d{1,2}
|
|
1400
|
+
if (symbols) patterns.push(`(?:[${symbols}])[^\\S\\n\\t]?${NUM}(?:[.,][^\\S\\n\\t]?(?:\\d{1,2}${DASH}?|${DASH}{1,2})?)?\\b`);
|
|
1401
|
+
if (trailingAlt) patterns.push(`\\b(?:${trailingAlt})[^\\S\\n\\t]{0,2}${NUM}(?:[.,][^\\S\\n\\t]?(?:\\d{1,2}${DASH}?|${DASH}{1,2})?)?\\b`);
|
|
1402
|
+
if (trailingAlt) patterns.push(`\\b${NUM}(?:[.,][^\\S\\n\\t]?(?:\\d{1,2}${DASH}?|${DASH}{1,2})?)?[^\\S\\n\\t]{0,4}(?:${trailingAlt})(?:\\b|(?=\\s|[.,;!?)]|$))`);
|
|
1334
1403
|
return patterns;
|
|
1335
1404
|
};
|
|
1336
1405
|
/** Cached promise for currency patterns. Loaded once. */
|
|
@@ -1441,7 +1510,7 @@ const isShortForm = (form) => form.replace(/[.\s]/g, "").length <= 3 && !form.in
|
|
|
1441
1510
|
const buildPatternString = (forms, requireCapBefore) => {
|
|
1442
1511
|
if (forms.length === 0) return null;
|
|
1443
1512
|
const alt = forms.toSorted((a, b) => b.length - a.length).map(escapeForRegex).join("|");
|
|
1444
|
-
return `${`(?:${CAP_WORD})(
|
|
1513
|
+
return `${`(?:${CAP_WORD})(?:${`(?:[\\s&,.${DASH_INNER}]{1,4}|\\s+(?:a|and|und|et|e|y|i)\\s+)`}(?:${ANY_WORD})){0,10}`}${requireCapBefore ? `(?:\\s+|,\\s*)` : `\\s+`}(?:${alt})(?![${LOWER}])`;
|
|
1445
1514
|
};
|
|
1446
1515
|
/**
|
|
1447
1516
|
* Build legal form regex pattern strings.
|
|
@@ -1470,7 +1539,7 @@ const buildLegalFormPatterns = async () => {
|
|
|
1470
1539
|
if (longPattern) patterns.push(longPattern);
|
|
1471
1540
|
const shortPattern = buildPatternString(allForms.filter(isShortForm), true);
|
|
1472
1541
|
if (shortPattern) patterns.push(shortPattern);
|
|
1473
|
-
const allcapPrefix = `(?:${ALLCAP_WORD})(?:[\\s
|
|
1542
|
+
const allcapPrefix = `(?:${ALLCAP_WORD})(?:[\\s&,.${DASH_INNER}]{1,4}(?:${ALLCAP_WORD})){0,2}`;
|
|
1474
1543
|
const allcapAlt = allForms.toSorted((a, b) => b.length - a.length).map(escapeForRegex).join("|");
|
|
1475
1544
|
patterns.push(`${allcapPrefix}(?:\\s+|,\\s*)(?:${allcapAlt})(?![${LOWER}])`);
|
|
1476
1545
|
return patterns;
|
|
@@ -1566,6 +1635,11 @@ const TRIGGER_SCORE = .95;
|
|
|
1566
1635
|
const WHITESPACE_RE$1 = /\s+/;
|
|
1567
1636
|
const LETTER_RE = /\p{L}/u;
|
|
1568
1637
|
/**
|
|
1638
|
+
* Decimal-comma pattern: comma followed by digit or
|
|
1639
|
+
* dash notation ("0,05%", "1.529,50 Kč", "98.000,- Kč").
|
|
1640
|
+
*/
|
|
1641
|
+
const DECIMAL_COMMA_RE = new RegExp(`^,(?:\\d|${DASH}{1,2})`);
|
|
1642
|
+
/**
|
|
1569
1643
|
* Post-nominal degree regex. When a comma-stop is
|
|
1570
1644
|
* followed by a known post-nominal (Ph.D., CSc., MBA
|
|
1571
1645
|
* etc.), skip the comma and degree, then continue.
|
|
@@ -1759,6 +1833,10 @@ const extractValue = (text, triggerEnd, strategy, label) => {
|
|
|
1759
1833
|
}
|
|
1760
1834
|
if (ch === ",") {
|
|
1761
1835
|
const afterComma = valueText.slice(end);
|
|
1836
|
+
if (DECIMAL_COMMA_RE.test(afterComma)) {
|
|
1837
|
+
end++;
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1762
1840
|
const degreeMatch = label === "person" ? POST_NOMINAL_RE.exec(afterComma) : null;
|
|
1763
1841
|
if (degreeMatch) {
|
|
1764
1842
|
end += degreeMatch[0].length;
|
|
@@ -2243,6 +2321,7 @@ const filterFalsePositives = (entities, ctx = defaultContext) => {
|
|
|
2243
2321
|
if (maxLen && trimmed.length > maxLen && entity.source !== "legal-form") continue;
|
|
2244
2322
|
if (SECTION_NUMBER_RE.test(trimmed) && entity.source !== "trigger") continue;
|
|
2245
2323
|
if (STANDALONE_YEAR_RE.test(trimmed)) continue;
|
|
2324
|
+
if (entity.label === "person" && HAS_DIGIT_RE.test(trimmed)) continue;
|
|
2246
2325
|
if ((entity.label === "person" || entity.label === "organization") && roles.has(trimmed.toLowerCase())) continue;
|
|
2247
2326
|
if (entity.label === "address" && trimmed.length > 40 && !POSTAL_CODE_RE.test(trimmed) && !HAS_DIGIT_RE.test(trimmed) && !ADDRESS_COMPONENTS_RE.test(trimmed)) continue;
|
|
2248
2327
|
if (entity.label === "address" && entity.source === "trigger" && !HAS_DIGIT_RE.test(trimmed) && !ADDRESS_COMPONENTS_RE.test(trimmed)) continue;
|
|
@@ -2647,7 +2726,11 @@ const processDenyListMatches = (allMatches, sliceStart, sliceEnd, fullText, data
|
|
|
2647
2726
|
if (!first || !last) continue;
|
|
2648
2727
|
const extended = extendPersonName(fullText, first.start, last.end, ctx);
|
|
2649
2728
|
const score = chain.length >= 2 ? .9 : .5;
|
|
2650
|
-
if (chain.length === 1 && isSentenceStart(fullText, first.start))
|
|
2729
|
+
if (chain.length === 1 && isSentenceStart(fullText, first.start)) {
|
|
2730
|
+
const afterEnd = last.end;
|
|
2731
|
+
const rest = fullText.slice(afterEnd).trimStart();
|
|
2732
|
+
if (!(rest.length > 1 && /^\p{Lu}\p{Ll}/u.test(rest))) continue;
|
|
2733
|
+
}
|
|
2651
2734
|
results.push({
|
|
2652
2735
|
start: first.start,
|
|
2653
2736
|
end: extended.end,
|
|
@@ -2661,7 +2744,7 @@ const processDenyListMatches = (allMatches, sliceStart, sliceEnd, fullText, data
|
|
|
2661
2744
|
return results;
|
|
2662
2745
|
};
|
|
2663
2746
|
const DISTRICT_SUFFIX_RE = new RegExp(`^ (\\d{1,2}(?!\\d)|(?:XXX|XXIX|XXVIII|XXVII|XXVI|XXV|XXIV|XXIII|XXII|XXI|XX|XIX|XVIII|XVII|XVI|XV|XIV|XIII|XII|XI|X|IX|VIII|VII|VI|IV|III|II))(?=[\\s,;.)"\\n]|$)`);
|
|
2664
|
-
const POSTAL_PREFIX_RE =
|
|
2747
|
+
const POSTAL_PREFIX_RE = new RegExp(`(?:\\d{5}|\\d{3}\\s\\d{2})\\s*${DASH}?\\s*$`);
|
|
2665
2748
|
const TRAILING_WORD_EXCLUSIONS = new Set([
|
|
2666
2749
|
"nájemce",
|
|
2667
2750
|
"pronajímatel",
|
|
@@ -2715,7 +2798,7 @@ const extendCityDistricts = (entities, fullText) => {
|
|
|
2715
2798
|
entity.text = fullText.slice(entity.start, entity.end);
|
|
2716
2799
|
}
|
|
2717
2800
|
const afterExt = fullText.slice(entity.end);
|
|
2718
|
-
const trailingWordM = /^[\s]
|
|
2801
|
+
const trailingWordM = /^[\s]{1,4}(\p{Lu}\p{Ll}+)/u.exec(afterExt);
|
|
2719
2802
|
if (trailingWordM && !trailingWordM[0].includes("\n")) {
|
|
2720
2803
|
const candidate = (trailingWordM[1] ?? "").toLowerCase();
|
|
2721
2804
|
if (!TRAILING_WORD_EXCLUSIONS.has(candidate)) {
|
|
@@ -4135,11 +4218,21 @@ const unmaskNerEntities = (nerEntities, maskResult, fullText) => {
|
|
|
4135
4218
|
};
|
|
4136
4219
|
//#endregion
|
|
4137
4220
|
//#region src/pipeline.ts
|
|
4221
|
+
/**
|
|
4222
|
+
* Sources backed by curated literal dictionaries.
|
|
4223
|
+
* Longer matches from these sources are more specific,
|
|
4224
|
+
* so the containment rule trusts their length.
|
|
4225
|
+
*/
|
|
4226
|
+
const LITERAL_SOURCES = new Set(["deny-list", "gazetteer"]);
|
|
4138
4227
|
const shouldReplace = (a, b) => {
|
|
4228
|
+
const aLen = a.end - a.start;
|
|
4229
|
+
const bLen = b.end - b.start;
|
|
4230
|
+
if (a.label === b.label && LITERAL_SOURCES.has(a.source) && a.start <= b.start && a.end >= b.end && aLen > bLen) return true;
|
|
4231
|
+
if (a.label === b.label && LITERAL_SOURCES.has(b.source) && b.start <= a.start && b.end >= a.end && bLen > aLen) return false;
|
|
4139
4232
|
const aPri = DETECTOR_PRIORITY[a.source] ?? 0;
|
|
4140
4233
|
const bPri = DETECTOR_PRIORITY[b.source] ?? 0;
|
|
4141
4234
|
if (aPri !== bPri) return aPri > bPri;
|
|
4142
|
-
return a.score > b.score || a.score === b.score &&
|
|
4235
|
+
return a.score > b.score || a.score === b.score && aLen > bLen;
|
|
4143
4236
|
};
|
|
4144
4237
|
/** Labels where colons are structurally significant. */
|
|
4145
4238
|
const COLON_LABELS = new Set(["ip address", "mac address"]);
|
|
@@ -4151,12 +4244,13 @@ const sanitizeEntities = (entities) => entities.flatMap((e) => {
|
|
|
4151
4244
|
const cleaned = leadTrimmed.replace(new RegExp(`${strip.source}$`, strip.flags), "");
|
|
4152
4245
|
if (cleaned.length === 0) return [];
|
|
4153
4246
|
if (!/[\p{L}\p{N}]/u.test(cleaned)) return [];
|
|
4154
|
-
|
|
4247
|
+
const collapsed = cleaned.replace(/\s*\n\s*/g, " ").replace(/\s{2,}/g, " ");
|
|
4248
|
+
if (collapsed === e.text) return [e];
|
|
4155
4249
|
return [{
|
|
4156
4250
|
...e,
|
|
4157
4251
|
start: e.start + lead,
|
|
4158
|
-
end: e.start + lead +
|
|
4159
|
-
text:
|
|
4252
|
+
end: e.start + lead + collapsed.length,
|
|
4253
|
+
text: collapsed
|
|
4160
4254
|
}];
|
|
4161
4255
|
});
|
|
4162
4256
|
const mergeAndDedup = (...layers) => {
|
|
@@ -4423,7 +4517,7 @@ const normalizeEntityText = (label, text) => {
|
|
|
4423
4517
|
if (upper === "EMAIL_ADDRESS" || upper === "EMAIL") return text.toLowerCase().trim();
|
|
4424
4518
|
if (upper === "PHONE_NUMBER" || upper === "PHONE") return text.replace(PHONE_NOISE_RE, "");
|
|
4425
4519
|
if (upper === "IBAN" || upper === "BANK_ACCOUNT_NUMBER" || upper === "TAX_IDENTIFICATION_NUMBER" || upper === "REGISTRATION_NUMBER") return text.replace(SPACE_DASH_RE, "").toUpperCase();
|
|
4426
|
-
if (upper === "PERSON" || upper === "ORGANIZATION" || upper === "ADDRESS") return text.replace(WHITESPACE_RE, " ").toLowerCase().trim();
|
|
4520
|
+
if (upper === "PERSON" || upper === "ORGANIZATION" || upper === "ADDRESS" || upper === "LAND_PARCEL") return text.replace(WHITESPACE_RE, " ").toLowerCase().trim();
|
|
4427
4521
|
return text.trim();
|
|
4428
4522
|
};
|
|
4429
4523
|
/**
|