@stll/anonymize-wasm 1.4.11 → 1.5.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/dist/address-stopwords.mjs +2 -0
- package/dist/allow-list.mjs +1 -0
- package/dist/constants.mjs +1 -0
- package/dist/constants.mjs.map +1 -1
- package/dist/constants2.d.mts +1 -1
- package/dist/wasm.d.mts +43 -9
- package/dist/wasm.mjs +223 -35
- package/dist/wasm.mjs.map +1 -1
- package/package.json +2 -2
package/dist/wasm.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_ENTITY_LABELS, DETECTION_SOURCES, DETECTOR_PRIORITY, OPERATOR_TYPES } from "./constants.mjs";
|
|
2
2
|
import { TextSearch } from "@stll/text-search-wasm";
|
|
3
|
-
import { at, au, be, bg, br, ch, cn, cy, cz, de, dk, ee, es, fi, fr, gb, gr, hr, hu, ie, it, lt, lu, lv, mt, nl, no, pl, pt, ro, se, si, sk, us } from "@stll/stdnum";
|
|
3
|
+
import { at, au, be, bg, br, ch, cn, crypto, cy, cz, de, dk, ee, es, fi, fr, gb, gr, hr, hu, ie, it, lt, lu, lv, mt, nl, no, pl, pt, ro, se, si, sk, us } from "@stll/stdnum";
|
|
4
4
|
import { toRegex } from "@stll/stdnum/patterns";
|
|
5
5
|
//#region \0rolldown/runtime.js
|
|
6
6
|
var __defProp = Object.defineProperty;
|
|
@@ -7959,6 +7959,15 @@ const processCountryMatches = (allMatches, sliceStart, sliceEnd, fullText, data)
|
|
|
7959
7959
|
return results;
|
|
7960
7960
|
};
|
|
7961
7961
|
//#endregion
|
|
7962
|
+
//#region src/util/entity-source.ts
|
|
7963
|
+
/**
|
|
7964
|
+
* True when the entity came from a caller-supplied detector — a
|
|
7965
|
+
* custom deny-list entry or a custom regex. These spans carry
|
|
7966
|
+
* caller-locked boundaries: the pipeline does not sanitise, trim,
|
|
7967
|
+
* or boundary-adjust them, since the caller chose the exact text.
|
|
7968
|
+
*/
|
|
7969
|
+
const isCallerOwnedEntity = (entity) => entity.sourceDetail === "custom-deny-list" || entity.sourceDetail === "custom-regex";
|
|
7970
|
+
//#endregion
|
|
7962
7971
|
//#region src/config/titles.ts
|
|
7963
7972
|
/**
|
|
7964
7973
|
* Academic and professional title prefixes.
|
|
@@ -8405,6 +8414,7 @@ const initNameCorpus = (ctx = defaultContext, dictionaries, languages) => {
|
|
|
8405
8414
|
titleTokens: Object.freeze(new Set(dedupTitles)),
|
|
8406
8415
|
titleAbbreviations: Object.freeze(titleAbbrSet),
|
|
8407
8416
|
excludedWords: Object.freeze(new Set(exclusions)),
|
|
8417
|
+
commonWords: Object.freeze(commonWords),
|
|
8408
8418
|
nonWesternNames: Object.freeze(new Set(dedupNonWestern)),
|
|
8409
8419
|
excludedAllCaps: Object.freeze(new Set(dedupExcludedAllCaps)),
|
|
8410
8420
|
firstNamesList: Object.freeze(dedupFirst),
|
|
@@ -9119,13 +9129,14 @@ const detectNameCorpus = (fullText, ctx = defaultContext, options = {}) => {
|
|
|
9119
9129
|
const corpusCount = chain.filter((t) => isCorpusMatch(t.type)).length;
|
|
9120
9130
|
const capitalizedCount = chain.filter((t) => t.type === TOKEN_TYPE.CAPITALIZED).length;
|
|
9121
9131
|
const nonWesternCount = chain.filter((t) => t.nonWestern).length;
|
|
9132
|
+
const chainAllCommonWords = chain.every((t) => corpus.commonWords.has(t.text.toLowerCase()));
|
|
9122
9133
|
let score;
|
|
9123
9134
|
if (hasNonWestern) {
|
|
9124
9135
|
if (hasTitle && (nonWesternCount > 0 || capitalizedCount > 0)) score = .95;
|
|
9125
9136
|
else if (hasJaSuffix && (capitalizedCount > 0 || nonWesternCount > 0)) score = .9;
|
|
9126
9137
|
else if (hasArabicConnector && nonWesternCount > 0) score = .9;
|
|
9127
9138
|
else if (nonWesternCount >= 2) score = .9;
|
|
9128
|
-
else if (nonWesternCount > 0 && (capitalizedCount > 0 || hasAbbreviation)) score = .9;
|
|
9139
|
+
else if (nonWesternCount > 0 && (capitalizedCount > 0 || hasAbbreviation) && !chainAllCommonWords) score = .9;
|
|
9129
9140
|
else if (nonWesternCount === 1 && chain.length === 1 && !isSentenceStart(fullText, token.start)) score = .5;
|
|
9130
9141
|
else continue;
|
|
9131
9142
|
if (supplementalMode && score < .9) continue;
|
|
@@ -9754,6 +9765,7 @@ var amount_words_default = {
|
|
|
9754
9765
|
//#region src/detectors/regex.ts
|
|
9755
9766
|
const MIN_PHONE_LENGTH = 7;
|
|
9756
9767
|
const MIN_MONTH_NAME_LENGTH = 3;
|
|
9768
|
+
const US_STATE_CODE = "(?:(?i:A[KLZR]|C[AOT]|D[CE]|FL|GA|HI|I[ADL]|K[SY]|LA|M[ADEHINOPST]|N[CDEHJMVY]|O[HK]|PA|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])|I[Nn]|iN|O[Rr]|oR)";
|
|
9757
9769
|
const escapeTitle = (title) => title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "\\s*");
|
|
9758
9770
|
/** Escape for use inside a regex alternation. */
|
|
9759
9771
|
const escapeRegex$2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -9991,6 +10003,58 @@ const ES_CIF = {
|
|
|
9991
10003
|
score: .95,
|
|
9992
10004
|
validator: es.cif
|
|
9993
10005
|
};
|
|
10006
|
+
const NHS_NUMBER_CONTEXT = {
|
|
10007
|
+
pattern: "\\b(?:(?i:NHS)(?:[^\\S\\n]+(?:(?i:number)|(?i:no\\.?)|#))?|(?i:National)[^\\S\\n]+(?i:Health)[^\\S\\n]+(?i:Service)[^\\S\\n]+(?:(?i:number)|(?i:no\\.?)|#))[^\\S\\n]{0,4}:?[^\\S\\n]{0,4}\\d{3}[^\\S\\n]?\\d{3}[^\\S\\n]?\\d{4}\\b",
|
|
10008
|
+
label: "national identification number",
|
|
10009
|
+
score: .95,
|
|
10010
|
+
validator: gb.nhs,
|
|
10011
|
+
validatorInput: (text) => text.replace(/\D/g, "")
|
|
10012
|
+
};
|
|
10013
|
+
const PASSPORT_CONTEXT = {
|
|
10014
|
+
pattern: "\\b(?:(?:(?i:U\\.?S\\.?)|(?i:USA)|(?i:United)[^\\S\\n]+(?i:States)|(?i:UK)|(?i:U\\.?K\\.?)|(?i:GBR)|(?i:British)|(?i:EU)|(?i:European)|(?i:France)|(?i:French))[^\\S\\n]{1,4})?(?i:passports?)(?:[^\\S\\n]+(?:(?i:number)|(?i:no\\.?)|#))?[^\\S\\n]{0,4}:?[^\\S\\n]{0,4}(?:[A-Za-z]{1,2}\\d{6,8}|\\d{2}[A-Za-z]{2}\\d{5}|\\d{7,9})\\b",
|
|
10015
|
+
label: "passport number",
|
|
10016
|
+
score: .96
|
|
10017
|
+
};
|
|
10018
|
+
const FR_CNI_CONTEXT = {
|
|
10019
|
+
pattern: "\\b(?:(?i:CNI)|(?i:carte)[^\\S\\n]+(?i:nationale)[^\\S\\n]+(?i:d)[’'](?i:identit[ée])|(?i:French)[^\\S\\n]+(?i:national)[^\\S\\n]+(?i:identity)[^\\S\\n]+(?i:card))(?:[^\\S\\n]+(?:(?i:number)|(?i:no\\.?)|(?i:n[°ºo]\\.?))[^\\S\\n]{0,4}:?[^\\S\\n]{0,4}[A-Za-z0-9]{9,12}|[^\\S\\n]{0,4}:?[^\\S\\n]{0,4}(?=[A-Za-z0-9]{0,11}\\d)[A-Za-z0-9]{9,12})\\b",
|
|
10020
|
+
label: "identity card number",
|
|
10021
|
+
score: .96
|
|
10022
|
+
};
|
|
10023
|
+
const CY_TIC_CONTEXT = {
|
|
10024
|
+
pattern: "\\b(?:(?:(?i:Cyprus)|(?i:Cypriot))[^\\S\\n]{1,4})?(?:(?i:TIC)|(?i:tax)[^\\S\\n]+(?i:identification)[^\\S\\n]+(?i:code))(?:[^\\S\\n]+(?:(?i:number)|(?i:no\\.?)|#))?[^\\S\\n]{0,4}:?[^\\S\\n]{0,4}\\d{8}[A-Za-z]\\b",
|
|
10025
|
+
label: "tax identification number",
|
|
10026
|
+
score: .96
|
|
10027
|
+
};
|
|
10028
|
+
const CY_ID_CARD_CONTEXT = {
|
|
10029
|
+
pattern: "\\b(?:(?i:Cyprus)|(?i:Cypriot))[^\\S\\n]+(?:(?i:identity)[^\\S\\n]+(?i:card)|(?i:ID)[^\\S\\n]+(?i:card))(?:[^\\S\\n]+(?:(?i:number)|(?i:no\\.?)|#))?[^\\S\\n]{0,4}:?[^\\S\\n]{0,4}\\d{6,8}\\b",
|
|
10030
|
+
label: "identity card number",
|
|
10031
|
+
score: .96
|
|
10032
|
+
};
|
|
10033
|
+
const UK_DRIVING_LICENCE_CONTEXT = {
|
|
10034
|
+
pattern: "\\b(?:(?:(?i:UK)|(?i:U\\.?K\\.?)|(?i:British))[^\\S\\n]{1,4})?(?i:driving)[^\\S\\n]+(?i:licen[cs]e)(?:[^\\S\\n]+(?:(?i:number)|(?i:no\\.?)|#))?[^\\S\\n]{0,4}:?[^\\S\\n]{0,4}[A-Za-z9]{5}\\d{6}[A-Za-z0-9]{2}\\d[A-Za-z]{2}\\b",
|
|
10035
|
+
label: "identity card number",
|
|
10036
|
+
score: .96
|
|
10037
|
+
};
|
|
10038
|
+
const US_DRIVER_LICENSE_CONTEXT = {
|
|
10039
|
+
pattern: `\\b(?:(?:(?i:U\\.?S\\.?)|(?i:USA)|(?i:United)[^\\S\\n]+(?i:States)|${US_STATE_CODE})[^\\S\\n]{1,4})?(?:(?i:driver['’]?s?)|(?i:driving))[^\\S\\n]+(?i:licen[cs]e)(?:[^\\S\\n]+(?:(?i:number)|(?i:no\\.?)|#)[^\\S\\n]{0,4}:?[^\\S\\n]{0,4}[A-Za-z0-9]{5,15}|[^\\S\\n]{0,4}:?[^\\S\\n]{0,4}(?=[A-Za-z0-9]{0,14}\\d)[A-Za-z0-9]{5,15})\\b`,
|
|
10040
|
+
label: "identity card number",
|
|
10041
|
+
score: .8
|
|
10042
|
+
};
|
|
10043
|
+
const MEDICAL_LICENSE_CONTEXT = {
|
|
10044
|
+
pattern: "\\b(?:(?:(?i:GMC)|(?i:NMC))(?:[^\\S\\n]+(?:(?i:licen[cs]e)|(?i:registration)|(?i:reg\\.?)|(?i:pin)|(?i:number)|(?i:no\\.?)))*|(?:(?i:medical)|(?i:physician)|(?i:doctor)|(?i:surgeon)|(?i:nursing)|(?i:nurse))(?:[^\\S\\n]+(?:(?i:licen[cs]e)|(?i:registration)|(?i:reg\\.?)|(?i:pin)|(?i:number)|(?i:no\\.?)))+)[^\\S\\n]{0,4}:?[^\\S\\n]{0,4}(?:[A-Za-z]{0,3}\\d{5,8}|\\d{2}[A-Za-z]\\d{4}[A-Za-z])\\b",
|
|
10045
|
+
label: "registration number",
|
|
10046
|
+
score: .85
|
|
10047
|
+
};
|
|
10048
|
+
const CRYPTO_WALLET_CANDIDATE = crypto.wallet.candidatePattern ?? "(?!)";
|
|
10049
|
+
const CRYPTO_WALLET_CANDIDATE_REGEX = new RegExp(CRYPTO_WALLET_CANDIDATE);
|
|
10050
|
+
const getCryptoWalletCandidate = (text) => CRYPTO_WALLET_CANDIDATE_REGEX.exec(text)?.[0] ?? text;
|
|
10051
|
+
const CRYPTO_WALLET_ADDRESS = {
|
|
10052
|
+
pattern: "\\b(?:0x[0-9A-Fa-f]{40}|bc1[ac-hj-np-z02-9]{11,71}|BC1[AC-HJ-NP-Z02-9]{11,71})\\b|\\b(?:(?i:BTC|Bitcoin|crypto|wallet|address)[^\\S\\n]{0,4}:?[^\\S\\n]{1,8}){1,4}[13][a-km-zA-HJ-NP-Z1-9]{25,34}\\b",
|
|
10053
|
+
label: "crypto",
|
|
10054
|
+
score: .85,
|
|
10055
|
+
validator: crypto.wallet,
|
|
10056
|
+
validatorInput: getCryptoWalletCandidate
|
|
10057
|
+
};
|
|
9994
10058
|
const AU_ABN_FORMATTED = {
|
|
9995
10059
|
pattern: `\\b\\d{2}[^\\S\\n]\\d{3}[^\\S\\n]\\d{3}[^\\S\\n]\\d{3}\\b`,
|
|
9996
10060
|
label: "tax identification number",
|
|
@@ -10137,6 +10201,15 @@ const ALL_REGEX_DEFS = [
|
|
|
10137
10201
|
ES_DNI,
|
|
10138
10202
|
ES_NIE,
|
|
10139
10203
|
ES_CIF,
|
|
10204
|
+
NHS_NUMBER_CONTEXT,
|
|
10205
|
+
PASSPORT_CONTEXT,
|
|
10206
|
+
FR_CNI_CONTEXT,
|
|
10207
|
+
CY_TIC_CONTEXT,
|
|
10208
|
+
CY_ID_CARD_CONTEXT,
|
|
10209
|
+
UK_DRIVING_LICENCE_CONTEXT,
|
|
10210
|
+
US_DRIVER_LICENSE_CONTEXT,
|
|
10211
|
+
MEDICAL_LICENSE_CONTEXT,
|
|
10212
|
+
CRYPTO_WALLET_ADDRESS,
|
|
10140
10213
|
AU_ABN_FORMATTED,
|
|
10141
10214
|
NO_ORGNR_FORMATTED,
|
|
10142
10215
|
NO_MVA_FORMATTED,
|
|
@@ -10168,6 +10241,7 @@ const REGEX_META = ALL_REGEX_DEFS.map((d) => {
|
|
|
10168
10241
|
score: d.score
|
|
10169
10242
|
};
|
|
10170
10243
|
if (d.validator) meta.validator = d.validator;
|
|
10244
|
+
if (d.validatorInput) meta.validatorInput = d.validatorInput;
|
|
10171
10245
|
return meta;
|
|
10172
10246
|
});
|
|
10173
10247
|
/**
|
|
@@ -10439,7 +10513,8 @@ const processRegexMatches = (allMatches, sliceStart, sliceEnd, meta_) => {
|
|
|
10439
10513
|
if (!meta) continue;
|
|
10440
10514
|
if (meta.sourceDetail !== "custom-regex" && meta.label === "phone number" && match.text.length < MIN_PHONE_LENGTH) continue;
|
|
10441
10515
|
if (meta.validator) {
|
|
10442
|
-
const
|
|
10516
|
+
const validatorText = meta.validatorInput ? meta.validatorInput(match.text) : match.text;
|
|
10517
|
+
const compacted = meta.validator.compact(validatorText);
|
|
10443
10518
|
if (!meta.validator.validate(compacted).valid) continue;
|
|
10444
10519
|
}
|
|
10445
10520
|
const entity = {
|
|
@@ -11895,7 +11970,40 @@ const trimTrailingAddressProse = (text) => {
|
|
|
11895
11970
|
}
|
|
11896
11971
|
return text;
|
|
11897
11972
|
};
|
|
11898
|
-
const
|
|
11973
|
+
const collapseEntityWhitespace = (text) => text.replace(/\s*\n\s*/g, " ").replace(/\s{2,}/g, " ");
|
|
11974
|
+
const normalizeEntityFromRaw = (entity, fullText) => {
|
|
11975
|
+
let start = entity.start;
|
|
11976
|
+
let end = entity.end;
|
|
11977
|
+
let text = fullText.slice(start, end);
|
|
11978
|
+
const trimLeading = (re) => {
|
|
11979
|
+
const match = re.exec(text);
|
|
11980
|
+
if (!match) return;
|
|
11981
|
+
start += match[0].length;
|
|
11982
|
+
text = text.slice(match[0].length);
|
|
11983
|
+
};
|
|
11984
|
+
trimLeading(LEADING_ARTIFACT_RE);
|
|
11985
|
+
trimLeading(/^\s+/u);
|
|
11986
|
+
if (entity.label === "address") {
|
|
11987
|
+
trimLeading(ADDRESS_ROLE_PREFIX_RE);
|
|
11988
|
+
const beforeTrim = text;
|
|
11989
|
+
text = trimTrailingAddressProse(text);
|
|
11990
|
+
end -= beforeTrim.length - text.length;
|
|
11991
|
+
}
|
|
11992
|
+
const trailingMatch = /[,\s]+$/u.exec(text);
|
|
11993
|
+
if (trailingMatch) {
|
|
11994
|
+
end -= trailingMatch[0].length;
|
|
11995
|
+
text = text.slice(0, text.length - trailingMatch[0].length);
|
|
11996
|
+
}
|
|
11997
|
+
if (text.length === 0) return null;
|
|
11998
|
+
return {
|
|
11999
|
+
...entity,
|
|
12000
|
+
start,
|
|
12001
|
+
end,
|
|
12002
|
+
text: collapseEntityWhitespace(text)
|
|
12003
|
+
};
|
|
12004
|
+
};
|
|
12005
|
+
const normalizeEntity = (entity, fullText) => {
|
|
12006
|
+
if (fullText !== void 0) return normalizeEntityFromRaw(entity, fullText);
|
|
11899
12007
|
let start = entity.start;
|
|
11900
12008
|
let text = entity.text;
|
|
11901
12009
|
const trimLeading = (re) => {
|
|
@@ -11913,10 +12021,13 @@ const normalizeEntity = (entity) => {
|
|
|
11913
12021
|
const trailingMatch = /[,\s]+$/u.exec(text);
|
|
11914
12022
|
if (trailingMatch) text = text.slice(0, text.length - trailingMatch[0].length);
|
|
11915
12023
|
if (text.length === 0) return null;
|
|
12024
|
+
const removedFromFront = start - entity.start;
|
|
12025
|
+
const removedFromBack = entity.text.length - removedFromFront - text.length;
|
|
12026
|
+
const end = entity.end - removedFromBack;
|
|
11916
12027
|
return {
|
|
11917
12028
|
...entity,
|
|
11918
12029
|
start,
|
|
11919
|
-
end
|
|
12030
|
+
end,
|
|
11920
12031
|
text
|
|
11921
12032
|
};
|
|
11922
12033
|
};
|
|
@@ -12021,7 +12132,6 @@ const initAddressComponents = () => {
|
|
|
12021
12132
|
* Czech house number form.
|
|
12022
12133
|
*/
|
|
12023
12134
|
const hasAddressComponent = (text) => _streetTypesRe.test(text) || ADDRESS_COMPONENT_EXTRA_RE.test(text);
|
|
12024
|
-
const isCallerOwnedEntity$3 = (entity) => entity.sourceDetail === "custom-deny-list" || entity.sourceDetail === "custom-regex";
|
|
12025
12135
|
/**
|
|
12026
12136
|
* Filter out entities that are likely false positives:
|
|
12027
12137
|
* template placeholders, clause/section numbers,
|
|
@@ -12034,14 +12144,14 @@ const filterFalsePositives = (entities, ctx = defaultContext, fullText) => {
|
|
|
12034
12144
|
const filtered = [];
|
|
12035
12145
|
const roles = getGenericRoles(ctx);
|
|
12036
12146
|
for (const entity of entities) {
|
|
12037
|
-
if (isCallerOwnedEntity
|
|
12147
|
+
if (isCallerOwnedEntity(entity)) {
|
|
12038
12148
|
filtered.push(entity);
|
|
12039
12149
|
continue;
|
|
12040
12150
|
}
|
|
12041
|
-
const normalized = normalizeEntity(entity);
|
|
12151
|
+
const normalized = normalizeEntity(entity, fullText);
|
|
12042
12152
|
if (!normalized) continue;
|
|
12043
12153
|
const trimmed = normalized.text;
|
|
12044
|
-
if (isCallerOwnedEntity
|
|
12154
|
+
if (isCallerOwnedEntity(normalized)) {
|
|
12045
12155
|
filtered.push(normalized);
|
|
12046
12156
|
continue;
|
|
12047
12157
|
}
|
|
@@ -12120,6 +12230,32 @@ const loadCommonWords = () => {
|
|
|
12120
12230
|
})();
|
|
12121
12231
|
return commonWordsPromise;
|
|
12122
12232
|
};
|
|
12233
|
+
let monthNamesPromise = null;
|
|
12234
|
+
let monthNamesCache = null;
|
|
12235
|
+
/**
|
|
12236
|
+
* English month names, lowercased. City gazetteers carry a
|
|
12237
|
+
* handful of entries that collide with month words (e.g. a
|
|
12238
|
+
* place named "August"); in prose these surface as address
|
|
12239
|
+
* false positives where the token is plainly a date, so they
|
|
12240
|
+
* are dropped from address patterns at build time.
|
|
12241
|
+
*/
|
|
12242
|
+
const loadMonthNames = () => {
|
|
12243
|
+
if (monthNamesCache) return Promise.resolve(monthNamesCache);
|
|
12244
|
+
if (monthNamesPromise) return monthNamesPromise;
|
|
12245
|
+
monthNamesPromise = (async () => {
|
|
12246
|
+
try {
|
|
12247
|
+
const mod = await import("./date-months.mjs");
|
|
12248
|
+
const set = new Set((mod.default?.en ?? []).map((month) => month.toLowerCase()));
|
|
12249
|
+
monthNamesCache = set;
|
|
12250
|
+
return set;
|
|
12251
|
+
} catch {
|
|
12252
|
+
const empty = /* @__PURE__ */ new Set();
|
|
12253
|
+
monthNamesCache = empty;
|
|
12254
|
+
return empty;
|
|
12255
|
+
}
|
|
12256
|
+
})();
|
|
12257
|
+
return monthNamesPromise;
|
|
12258
|
+
};
|
|
12123
12259
|
/**
|
|
12124
12260
|
* Curated dictionary entries that are pure dotted
|
|
12125
12261
|
* single-letter acronyms (e.g. `S.C.`, `D.N.J.`, `C.E.C.`)
|
|
@@ -12433,10 +12569,12 @@ const buildDenyList = async (config, ctx = defaultContext) => {
|
|
|
12433
12569
|
loadPersonStopwords(ctx),
|
|
12434
12570
|
loadAddressStopwords(ctx),
|
|
12435
12571
|
loadCommonWords(),
|
|
12572
|
+
loadMonthNames(),
|
|
12436
12573
|
loadStreetTypeRe(),
|
|
12437
12574
|
loadGenericRoles(ctx)
|
|
12438
12575
|
]);
|
|
12439
12576
|
const commonWords = await loadCommonWords();
|
|
12577
|
+
const monthNames = await loadMonthNames();
|
|
12440
12578
|
const dictionaries = config.dictionaries;
|
|
12441
12579
|
const hasDenyList = dictionaries?.denyList && dictionaries?.denyListMeta;
|
|
12442
12580
|
const hasCustomDenyList = config.customDenyList !== void 0 && config.customDenyList.length > 0;
|
|
@@ -12455,9 +12593,11 @@ const buildDenyList = async (config, ctx = defaultContext) => {
|
|
|
12455
12593
|
const normalized = source === "custom-deny-list" ? normalizeForSearch(entry) : stripCuratedPatternSyntax(normalizeForSearch(entry));
|
|
12456
12594
|
if (normalized.length === 0) return;
|
|
12457
12595
|
const lower = normalized.toLowerCase();
|
|
12458
|
-
if (source !== "custom-deny-list"
|
|
12459
|
-
if (
|
|
12460
|
-
|
|
12596
|
+
if (source !== "custom-deny-list") {
|
|
12597
|
+
if (label !== "address") {
|
|
12598
|
+
if (SINGLE_WORD_RE.test(normalized) && commonWords.has(lower)) return;
|
|
12599
|
+
if (isShortCuratedNoiseAcronym(normalized)) return;
|
|
12600
|
+
} else if (monthNames.has(lower)) return;
|
|
12461
12601
|
}
|
|
12462
12602
|
const existing = patternIndex.get(lower);
|
|
12463
12603
|
if (existing !== void 0) {
|
|
@@ -13336,7 +13476,6 @@ const TRAILING_SEP = /[,\s]+$/;
|
|
|
13336
13476
|
const WORD_CHAR_RE = /[\p{L}\p{N}]/u;
|
|
13337
13477
|
const ORG_PROPAGATION_SCORE = .9;
|
|
13338
13478
|
const ORG_DETERMINER_RE = /(?<![\p{L}\p{N}])(společnost(?:i|í|em|u)?|spolecnost(?:i|em|u)?|the\s+(?:company|corporation|firm)|die\s+(?:gesellschaft|firma)|la\s+(?:société|empresa|sociedad)|el\s+(?:empresa|sociedad))\s+$/iu;
|
|
13339
|
-
const isCallerOwnedEntity$2 = (entity) => entity.sourceDetail === "custom-deny-list" || entity.sourceDetail === "custom-regex";
|
|
13340
13479
|
/**
|
|
13341
13480
|
* After the main detection pass, collect organization
|
|
13342
13481
|
* entities with a legal form suffix, strip the suffix
|
|
@@ -13354,7 +13493,7 @@ const propagateOrgNames = (entities, fullText) => {
|
|
|
13354
13493
|
const seedByBase = /* @__PURE__ */ new Map();
|
|
13355
13494
|
for (const e of entities) {
|
|
13356
13495
|
if (e.label !== "organization") continue;
|
|
13357
|
-
if (isCallerOwnedEntity
|
|
13496
|
+
if (isCallerOwnedEntity(e)) continue;
|
|
13358
13497
|
for (const suffix of LEGAL_SUFFIXES) if (e.text.endsWith(suffix)) {
|
|
13359
13498
|
const base = e.text.slice(0, -suffix.length).replace(TRAILING_SEP, "").trim();
|
|
13360
13499
|
if (base.length >= 3) {
|
|
@@ -13724,7 +13863,6 @@ const NEAR_MISS_BAND = .15;
|
|
|
13724
13863
|
const BOOST_PER_NEIGHBOUR = .05;
|
|
13725
13864
|
const CONTEXT_WINDOW_CHARS = 150;
|
|
13726
13865
|
const HIGH_CONFIDENCE_FLOOR = .9;
|
|
13727
|
-
const isCallerOwnedEntity$1 = (entity) => entity.sourceDetail === "custom-deny-list" || entity.sourceDetail === "custom-regex";
|
|
13728
13866
|
/**
|
|
13729
13867
|
* Boost confidence of near-miss NER entities that appear
|
|
13730
13868
|
* near high-confidence detections (regex, trigger phrase).
|
|
@@ -13756,7 +13894,7 @@ const boostNearMissEntities = (entities, threshold) => {
|
|
|
13756
13894
|
const boostedScore = entity.score + neighbourCount * BOOST_PER_NEIGHBOUR;
|
|
13757
13895
|
if (boostedScore >= threshold) boosted.push({
|
|
13758
13896
|
...entity,
|
|
13759
|
-
score: boostedScore
|
|
13897
|
+
score: Math.min(1, boostedScore)
|
|
13760
13898
|
});
|
|
13761
13899
|
}
|
|
13762
13900
|
return boosted;
|
|
@@ -13835,7 +13973,7 @@ const getStreetAbbrevs = () => _streetAbbrevs ?? /* @__PURE__ */ new Set();
|
|
|
13835
13973
|
*/
|
|
13836
13974
|
const detectStreetPatternsNearAddresses = (fullText, existingEntities) => {
|
|
13837
13975
|
const results = [];
|
|
13838
|
-
const addressEntities = existingEntities.filter((entity) => !(entity.label === "address" && isCallerOwnedEntity
|
|
13976
|
+
const addressEntities = existingEntities.filter((entity) => !(entity.label === "address" && isCallerOwnedEntity(entity))).filter((e) => e.label === "address");
|
|
13839
13977
|
const headerEnd = Math.floor(fullText.length * HEADER_ZONE_FRACTION);
|
|
13840
13978
|
const houseNumRe = /\b(?:\d{1,4}\/\d+[a-zA-Z]\b|\d{3,4}\/\d+\b|(?:1[3-9]|[2-9]\d)\/\d{3,}\b)/g;
|
|
13841
13979
|
houseNumRe.lastIndex = 0;
|
|
@@ -13934,7 +14072,7 @@ const ORPHAN_STREET_RE = /^[^\S\n]*(\p{Lu}[\p{Ll}\p{Lu}]+(?:[^\S\n]+[\p{Lu}\p{Ll
|
|
|
13934
14072
|
*/
|
|
13935
14073
|
const detectOrphanStreetLines = (fullText, existingEntities) => {
|
|
13936
14074
|
const headerEnd = Math.floor(fullText.length * HEADER_ZONE_FRACTION);
|
|
13937
|
-
const contextEntities = existingEntities.filter((entity) => !(entity.label === "address" && isCallerOwnedEntity
|
|
14075
|
+
const contextEntities = existingEntities.filter((entity) => !(entity.label === "address" && isCallerOwnedEntity(entity)));
|
|
13938
14076
|
const results = [];
|
|
13939
14077
|
ORPHAN_STREET_RE.lastIndex = 0;
|
|
13940
14078
|
for (let m = ORPHAN_STREET_RE.exec(fullText); m !== null; m = ORPHAN_STREET_RE.exec(fullText)) {
|
|
@@ -14292,7 +14430,7 @@ const applyHotwordRules = (entities, fullText) => {
|
|
|
14292
14430
|
//#region src/filters/boundary-consistency.ts
|
|
14293
14431
|
/** Max gap (in chars) between entities to merge. */
|
|
14294
14432
|
const MAX_GAP = 3;
|
|
14295
|
-
const hasLockedBoundary$1 = (entity) => entity
|
|
14433
|
+
const hasLockedBoundary$1 = (entity) => isCallerOwnedEntity(entity);
|
|
14296
14434
|
const hasDetectorLockedBoundary = (entity) => entity.label === "phone number" && entity.source === DETECTION_SOURCES.TRIGGER;
|
|
14297
14435
|
/**
|
|
14298
14436
|
* Characters allowed in the gap between two adjacent
|
|
@@ -14875,7 +15013,6 @@ const unmaskNerEntities = (nerEntities, maskResult, fullText) => {
|
|
|
14875
15013
|
* so the containment rule trusts their length.
|
|
14876
15014
|
*/
|
|
14877
15015
|
const LITERAL_SOURCES = new Set(["deny-list", "gazetteer"]);
|
|
14878
|
-
const isCallerOwnedEntity = (entity) => entity.sourceDetail === "custom-deny-list" || entity.sourceDetail === "custom-regex";
|
|
14879
15016
|
const hasLockedBoundary = (entity) => isCallerOwnedEntity(entity);
|
|
14880
15017
|
const isCoveredByDenyListEntity = (entity, denyListEntity) => entity.label === denyListEntity.label && denyListEntity.start <= entity.start && denyListEntity.end >= entity.end;
|
|
14881
15018
|
const filterNameCorpusCoveredByDenyList = (nameCorpusEntities, denyListEntities) => nameCorpusEntities.filter((entity) => !denyListEntities.some((denyListEntity) => isCoveredByDenyListEntity(entity, denyListEntity)));
|
|
@@ -15099,7 +15236,7 @@ const sanitizeEntities = (entities) => entities.flatMap((e) => {
|
|
|
15099
15236
|
return [{
|
|
15100
15237
|
...e,
|
|
15101
15238
|
start: e.start + lead,
|
|
15102
|
-
end: e.start + lead +
|
|
15239
|
+
end: e.start + lead + cleaned.length,
|
|
15103
15240
|
text: collapsed
|
|
15104
15241
|
}];
|
|
15105
15242
|
});
|
|
@@ -15215,7 +15352,7 @@ const filterAllowedLabels = (entities, allowedLabels) => {
|
|
|
15215
15352
|
return entities.filter((e) => allowedLabels.has(e.label));
|
|
15216
15353
|
};
|
|
15217
15354
|
const labelIsAllowed = (label, allowedLabels) => !allowedLabels || allowedLabels.has(label);
|
|
15218
|
-
const NON_NER_LABELS = new Set(["misc"]);
|
|
15355
|
+
const NON_NER_LABELS = new Set(["crypto", "misc"]);
|
|
15219
15356
|
const getRequestedNerLabels = (config, expandForHotwords = false) => {
|
|
15220
15357
|
const labels = config.labels.length > 0 ? config.labels : DEFAULT_ENTITY_LABELS;
|
|
15221
15358
|
return (expandForHotwords ? expandLabelsForHotwordRules(labels) : labels).filter((label) => !NON_NER_LABELS.has(label));
|
|
@@ -15545,7 +15682,23 @@ const resolveOperator = (config, label) => config.operators[label] ?? "replace";
|
|
|
15545
15682
|
//#region src/redact.ts
|
|
15546
15683
|
const WHITESPACE_RE = /\s+/g;
|
|
15547
15684
|
const PHONE_NOISE_RE = /[()\s-]/g;
|
|
15685
|
+
const ETHEREUM_ADDRESS_RE = /0x[0-9A-Fa-f]{40}/;
|
|
15686
|
+
const BECH32_ADDRESS_RE = /\bbc1[ac-hj-np-z02-9]{11,71}\b/i;
|
|
15687
|
+
const BASE58_ADDRESS_RE = /\b[13][a-km-zA-HJ-NP-Z1-9]{25,34}\b/;
|
|
15688
|
+
const NHS_NUMBER_CUE_RE = /\b(?:NHS|National\s+Health\s+Service)\b/i;
|
|
15689
|
+
const PASSPORT_IDENTIFIER_RE = /\b(?:[A-Za-z]{1,2}\d{6,8}|\d{2}[A-Za-z]{2}\d{5}|\d{7,9})\b/;
|
|
15548
15690
|
const ID_SEPARATOR_RE = /[\s\-/.]/g;
|
|
15691
|
+
const normalizeCryptoText = (text) => {
|
|
15692
|
+
const trimmed = text.trim();
|
|
15693
|
+
const ethereumAddress = ETHEREUM_ADDRESS_RE.exec(trimmed)?.[0];
|
|
15694
|
+
if (ethereumAddress) return ethereumAddress.toLowerCase();
|
|
15695
|
+
const bech32Address = BECH32_ADDRESS_RE.exec(trimmed)?.[0];
|
|
15696
|
+
if (bech32Address) return bech32Address.toLowerCase();
|
|
15697
|
+
return BASE58_ADDRESS_RE.exec(trimmed)?.[0] ?? trimmed;
|
|
15698
|
+
};
|
|
15699
|
+
const normalizePassportText = (text) => {
|
|
15700
|
+
return (PASSPORT_IDENTIFIER_RE.exec(text)?.[0] ?? text).replace(ID_SEPARATOR_RE, "").toUpperCase();
|
|
15701
|
+
};
|
|
15549
15702
|
/**
|
|
15550
15703
|
* Normalize entity text so that surface-form variations
|
|
15551
15704
|
* of the same real-world value map to a single canonical
|
|
@@ -15555,7 +15708,10 @@ const normalizeEntityText = (label, text) => {
|
|
|
15555
15708
|
const upper = label.toUpperCase().replace(WHITESPACE_RE, "_");
|
|
15556
15709
|
if (upper === "EMAIL_ADDRESS" || upper === "EMAIL") return text.toLowerCase().trim();
|
|
15557
15710
|
if (upper === "PHONE_NUMBER" || upper === "PHONE") return text.replace(PHONE_NOISE_RE, "");
|
|
15558
|
-
if (upper === "
|
|
15711
|
+
if (upper === "CRYPTO") return normalizeCryptoText(text);
|
|
15712
|
+
if (upper === "NATIONAL_IDENTIFICATION_NUMBER" && NHS_NUMBER_CUE_RE.test(text)) return text.replace(/\D/g, "");
|
|
15713
|
+
if (upper === "IBAN" || upper === "BANK_ACCOUNT_NUMBER" || upper === "TAX_IDENTIFICATION_NUMBER" || upper === "REGISTRATION_NUMBER" || upper === "NATIONAL_IDENTIFICATION_NUMBER" || upper === "SOCIAL_SECURITY_NUMBER" || upper === "BIRTH_NUMBER" || upper === "IDENTITY_CARD_NUMBER" || upper === "CREDIT_CARD_NUMBER") return text.replace(ID_SEPARATOR_RE, "").toUpperCase();
|
|
15714
|
+
if (upper === "PASSPORT_NUMBER") return normalizePassportText(text);
|
|
15559
15715
|
if (upper === "PERSON" || upper === "ORGANIZATION" || upper === "ADDRESS" || upper === "LAND_PARCEL" || upper === "MISC") return text.replace(WHITESPACE_RE, " ").toLowerCase().trim();
|
|
15560
15716
|
return text.trim();
|
|
15561
15717
|
};
|
|
@@ -15688,7 +15844,7 @@ const hasOverlapping = (a, b, multiLabel) => {
|
|
|
15688
15844
|
const b0 = b[0] ?? 0;
|
|
15689
15845
|
const b1 = b[1] ?? 0;
|
|
15690
15846
|
if (a0 === b0 && a1 === b1) return !multiLabel;
|
|
15691
|
-
return
|
|
15847
|
+
return a0 < b1 && b0 < a1;
|
|
15692
15848
|
};
|
|
15693
15849
|
/**
|
|
15694
15850
|
* Greedy non-overlapping span selection.
|
|
@@ -15900,7 +16056,12 @@ const encodeInputs = (tokenizer, texts, promptLengths) => {
|
|
|
15900
16056
|
allWordsMasks
|
|
15901
16057
|
];
|
|
15902
16058
|
};
|
|
15903
|
-
/**
|
|
16059
|
+
/**
|
|
16060
|
+
* Build span index pairs and masks for the model.
|
|
16061
|
+
*
|
|
16062
|
+
* Exported for unit testing the span-mask invariant; not part
|
|
16063
|
+
* of the public package barrel.
|
|
16064
|
+
*/
|
|
15904
16065
|
const prepareSpans = (batchTokens, maxWidth) => {
|
|
15905
16066
|
const spanIdxs = [];
|
|
15906
16067
|
const spanMasks = [];
|
|
@@ -15911,7 +16072,7 @@ const prepareSpans = (batchTokens, maxWidth) => {
|
|
|
15911
16072
|
for (let i = 0; i < len; i++) for (let j = 0; j < maxWidth; j++) {
|
|
15912
16073
|
const endIdx = Math.min(i + j, len - 1);
|
|
15913
16074
|
idx.push([i, endIdx]);
|
|
15914
|
-
mask.push(
|
|
16075
|
+
mask.push(i + j < len);
|
|
15915
16076
|
}
|
|
15916
16077
|
spanIdxs.push(idx);
|
|
15917
16078
|
spanMasks.push(mask);
|
|
@@ -15977,13 +16138,20 @@ const MAX_CHUNK_CHARS = 1500;
|
|
|
15977
16138
|
const OVERLAP_CHARS = 50;
|
|
15978
16139
|
const MIN_CHUNK_LENGTH = 10;
|
|
15979
16140
|
/**
|
|
15980
|
-
* Split text into overlapping chunks
|
|
15981
|
-
*
|
|
15982
|
-
*
|
|
16141
|
+
* Split text into overlapping chunks, each paired with its
|
|
16142
|
+
* exact start offset in the source text.
|
|
16143
|
+
*
|
|
16144
|
+
* Carrying the offset out of the splitter is the robust way to
|
|
16145
|
+
* map chunk-local entity offsets back to document offsets:
|
|
16146
|
+
* downstream code never has to re-locate a chunk by content
|
|
16147
|
+
* search (which mis-locates when boilerplate repeats; see
|
|
16148
|
+
* computeChunkOffsets).
|
|
15983
16149
|
*
|
|
15984
|
-
*
|
|
16150
|
+
* Character-based splitting (rough token approximation for
|
|
16151
|
+
* GLiNER's ~512 token window); breaks at sentence boundaries
|
|
16152
|
+
* when possible.
|
|
15985
16153
|
*/
|
|
15986
|
-
const
|
|
16154
|
+
const chunkTextWithOffsets = (text) => {
|
|
15987
16155
|
const chunks = [];
|
|
15988
16156
|
let offset = 0;
|
|
15989
16157
|
while (offset < text.length) {
|
|
@@ -15993,14 +16161,34 @@ const chunkText = (text) => {
|
|
|
15993
16161
|
if (lastPeriod > MAX_CHUNK_CHARS * .5) end = offset + lastPeriod + 2;
|
|
15994
16162
|
}
|
|
15995
16163
|
const chunk = text.slice(offset, end);
|
|
15996
|
-
if (chunk.trim().length > MIN_CHUNK_LENGTH) chunks.push(
|
|
16164
|
+
if (chunk.trim().length > MIN_CHUNK_LENGTH) chunks.push({
|
|
16165
|
+
text: chunk,
|
|
16166
|
+
offset
|
|
16167
|
+
});
|
|
16168
|
+
if (end === text.length) break;
|
|
15997
16169
|
offset = Math.max(offset + 1, end - OVERLAP_CHARS);
|
|
15998
16170
|
}
|
|
15999
16171
|
return chunks;
|
|
16000
16172
|
};
|
|
16001
16173
|
/**
|
|
16002
|
-
*
|
|
16003
|
-
*
|
|
16174
|
+
* Split text into overlapping chunks for GLiNER's ~512 token
|
|
16175
|
+
* context window. Character-based splitting (rough token
|
|
16176
|
+
* approximation); breaks at sentence boundaries when possible.
|
|
16177
|
+
*
|
|
16178
|
+
* Prefer chunkTextWithOffsets when you also need each chunk's
|
|
16179
|
+
* document offset.
|
|
16180
|
+
*/
|
|
16181
|
+
const chunkText = (text) => chunkTextWithOffsets(text).map((chunk) => chunk.text);
|
|
16182
|
+
/**
|
|
16183
|
+
* Compute the start offset of each chunk within the original
|
|
16184
|
+
* document text by content search.
|
|
16185
|
+
*
|
|
16186
|
+
* @deprecated Re-locates each chunk with `indexOf`, which can
|
|
16187
|
+
* match the wrong position when identical content repeats in
|
|
16188
|
+
* the document (common in boilerplate-heavy legal text) and
|
|
16189
|
+
* then desyncs every subsequent offset. Use
|
|
16190
|
+
* `chunkTextWithOffsets`, which carries exact offsets out of
|
|
16191
|
+
* the splitter.
|
|
16004
16192
|
*/
|
|
16005
16193
|
const computeChunkOffsets = (fullText, chunks) => {
|
|
16006
16194
|
const offsets = [];
|
|
@@ -16101,6 +16289,6 @@ const levenshtein = (rawA, rawB) => {
|
|
|
16101
16289
|
//#region src/wasm.ts
|
|
16102
16290
|
initTextSearch(TextSearch);
|
|
16103
16291
|
//#endregion
|
|
16104
|
-
export { CURRENCY_PATTERN_META, DATE_PATTERN_META, DEFAULT_ENTITY_LABELS, DEFAULT_OPERATOR_CONFIG, DETECTION_SOURCES, DETECTOR_PRIORITY, OPERATOR_REGISTRY, OPERATOR_TYPES, REGEX_META, REGEX_PATTERNS, REGIONS, ZONE_SCORE_ADJUSTMENTS, applyHotwordRules, applyZoneAdjustments, boostNearMissEntities, buildDenyList, buildGazetteerPatterns, buildLegalFormPatterns, buildPlaceholderMap, buildStreetTypePatterns, buildTriggerPatterns, buildUnifiedSearch, chunkText, classifyZones, computeChunkOffsets, corefKey, createPipelineContext, deanonymise, decodeSpans, decodeTokenSpans, detectNameCorpus, ensureDenyListData, exportRedactionKey, extractDefinedTerms, filterFalsePositives, findCoreferenceSpans, getCurrencyPatterns, getDatePatterns, getNameCorpusNonWesternNames, initAddressComponents, initHotwordRules, initNameCorpus, initZoneClassifier, levenshtein, mergeAndDedup, mergeChunkEntities, normalizeForSearch, prepareBatch, preparePipelineSearch, processAddressSeeds, processDenyListMatches, processGazetteerMatches, processLegalFormMatches, processRegexMatches, processTriggerMatches, propagateOrgNames, redactText, resolveCountries, resolveOperator, runPipeline, runUnifiedSearch, sanitizeEntities, tokenizeText, warmLegalRoleHeads };
|
|
16292
|
+
export { CURRENCY_PATTERN_META, DATE_PATTERN_META, DEFAULT_ENTITY_LABELS, DEFAULT_OPERATOR_CONFIG, DETECTION_SOURCES, DETECTOR_PRIORITY, OPERATOR_REGISTRY, OPERATOR_TYPES, REGEX_META, REGEX_PATTERNS, REGIONS, ZONE_SCORE_ADJUSTMENTS, applyHotwordRules, applyZoneAdjustments, boostNearMissEntities, buildDenyList, buildGazetteerPatterns, buildLegalFormPatterns, buildPlaceholderMap, buildStreetTypePatterns, buildTriggerPatterns, buildUnifiedSearch, chunkText, chunkTextWithOffsets, classifyZones, computeChunkOffsets, corefKey, createPipelineContext, deanonymise, decodeSpans, decodeTokenSpans, detectNameCorpus, ensureDenyListData, exportRedactionKey, extractDefinedTerms, filterFalsePositives, findCoreferenceSpans, getCurrencyPatterns, getDatePatterns, getNameCorpusNonWesternNames, initAddressComponents, initHotwordRules, initNameCorpus, initZoneClassifier, levenshtein, mergeAndDedup, mergeChunkEntities, normalizeForSearch, prepareBatch, preparePipelineSearch, processAddressSeeds, processDenyListMatches, processGazetteerMatches, processLegalFormMatches, processRegexMatches, processTriggerMatches, propagateOrgNames, redactText, resolveCountries, resolveOperator, runPipeline, runUnifiedSearch, sanitizeEntities, tokenizeText, warmLegalRoleHeads };
|
|
16105
16293
|
|
|
16106
16294
|
//# sourceMappingURL=wasm.mjs.map
|