@stll/anonymize-wasm 1.4.3 → 1.4.4
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/common-words-en.mjs +0 -1
- package/dist/legal-form-leading-clauses.mjs +6 -1
- package/dist/person-stopwords.mjs +6 -0
- package/dist/wasm.mjs +250 -17
- package/dist/wasm.mjs.map +1 -1
- package/package.json +1 -1
package/dist/common-words-en.mjs
CHANGED
|
@@ -17,6 +17,7 @@ var person_stopwords_default = {
|
|
|
17
17
|
"basic",
|
|
18
18
|
"belgian",
|
|
19
19
|
"borrower",
|
|
20
|
+
"branch",
|
|
20
21
|
"british",
|
|
21
22
|
"bulgarian",
|
|
22
23
|
"buyer",
|
|
@@ -35,6 +36,8 @@ var person_stopwords_default = {
|
|
|
35
36
|
"default",
|
|
36
37
|
"delaware",
|
|
37
38
|
"delivery",
|
|
39
|
+
"department",
|
|
40
|
+
"division",
|
|
38
41
|
"dry",
|
|
39
42
|
"dutch",
|
|
40
43
|
"english",
|
|
@@ -50,6 +53,7 @@ var person_stopwords_default = {
|
|
|
50
53
|
"germany",
|
|
51
54
|
"greek",
|
|
52
55
|
"guarantor",
|
|
56
|
+
"headquarters",
|
|
53
57
|
"hungarian",
|
|
54
58
|
"indiana",
|
|
55
59
|
"interest",
|
|
@@ -80,6 +84,7 @@ var person_stopwords_default = {
|
|
|
80
84
|
"notice",
|
|
81
85
|
"november",
|
|
82
86
|
"october",
|
|
87
|
+
"office",
|
|
83
88
|
"ohio",
|
|
84
89
|
"opportunity",
|
|
85
90
|
"owner",
|
|
@@ -102,6 +107,7 @@ var person_stopwords_default = {
|
|
|
102
107
|
"section",
|
|
103
108
|
"seller",
|
|
104
109
|
"september",
|
|
110
|
+
"subsidiary",
|
|
105
111
|
"slovak",
|
|
106
112
|
"slovenian",
|
|
107
113
|
"spanish",
|
package/dist/wasm.mjs
CHANGED
|
@@ -231,12 +231,22 @@ const LEGAL_SUFFIXES = [...[
|
|
|
231
231
|
"OHG",
|
|
232
232
|
"Ltd.",
|
|
233
233
|
"Ltd",
|
|
234
|
+
"LTD.",
|
|
235
|
+
"LTD",
|
|
234
236
|
"LLC",
|
|
235
237
|
"LLP",
|
|
236
238
|
"Inc.",
|
|
239
|
+
"INC.",
|
|
240
|
+
"Inc",
|
|
241
|
+
"INC",
|
|
237
242
|
"Corp.",
|
|
243
|
+
"CORP.",
|
|
244
|
+
"Corp",
|
|
245
|
+
"CORP",
|
|
238
246
|
"Corporation",
|
|
247
|
+
"CORPORATION",
|
|
239
248
|
"Co.",
|
|
249
|
+
"CO.",
|
|
240
250
|
"LP",
|
|
241
251
|
"L.P.",
|
|
242
252
|
"PLC",
|
|
@@ -246,6 +256,8 @@ const LEGAL_SUFFIXES = [...[
|
|
|
246
256
|
"B.V.",
|
|
247
257
|
"Pty Ltd.",
|
|
248
258
|
"Pty Ltd",
|
|
259
|
+
"PTY LTD.",
|
|
260
|
+
"PTY LTD",
|
|
249
261
|
"S.A.",
|
|
250
262
|
"SA",
|
|
251
263
|
"SAS",
|
|
@@ -1242,8 +1254,6 @@ const splitEmbeddedLegalFormList = (entityStart, entityText) => {
|
|
|
1242
1254
|
const boundary = LEGAL_LIST_BOUNDARY_RE.exec(afterSuffix);
|
|
1243
1255
|
if (boundary === null) continue;
|
|
1244
1256
|
const nextStart = suffixEnd + boundary[0].length;
|
|
1245
|
-
const remainder = entityText.slice(nextStart);
|
|
1246
|
-
if (!getAllLegalSuffixesSync().some((form) => remainder.endsWith(form))) continue;
|
|
1247
1257
|
cuts.push(nextStart);
|
|
1248
1258
|
}
|
|
1249
1259
|
}
|
|
@@ -1259,6 +1269,7 @@ const splitEmbeddedLegalFormList = (entityStart, entityText) => {
|
|
|
1259
1269
|
if (start === void 0) continue;
|
|
1260
1270
|
const segmentText = entityText.slice(start, end).replace(/[,\s;]+$/u, "");
|
|
1261
1271
|
if (segmentText.length === 0) continue;
|
|
1272
|
+
if (!getAllLegalSuffixesSync().some((form) => segmentText.endsWith(form))) continue;
|
|
1262
1273
|
segments.push({
|
|
1263
1274
|
entityStart: entityStart + start,
|
|
1264
1275
|
entityText: segmentText
|
|
@@ -1442,10 +1453,18 @@ const trimLeadingClause = (text) => {
|
|
|
1442
1453
|
for (const match of text.matchAll(phraseRe)) cut = Math.max(cut, match.index + match[0].length);
|
|
1443
1454
|
}
|
|
1444
1455
|
const directPrefixAlternation = trims.directPrefixes.map(escapeForRegex).join("|");
|
|
1456
|
+
const COMMA_GATED_DIRECT_PREFIXES = new Set([
|
|
1457
|
+
"among",
|
|
1458
|
+
"amongst",
|
|
1459
|
+
"between"
|
|
1460
|
+
]);
|
|
1445
1461
|
if (directPrefixAlternation.length > 0) {
|
|
1446
1462
|
const directPrefixRe = new RegExp(`\\b(?:${directPrefixAlternation})${HSPACE}+(?=\\p{Lu})`, "giu");
|
|
1447
1463
|
for (const match of text.matchAll(directPrefixRe)) {
|
|
1448
|
-
const
|
|
1464
|
+
const matchedPrefix = match[0].trim().toLowerCase();
|
|
1465
|
+
const before = text.slice(0, match.index);
|
|
1466
|
+
if (COMMA_GATED_DIRECT_PREFIXES.has(matchedPrefix) && !/,\s*$/u.test(before)) continue;
|
|
1467
|
+
const words = before.match(/\p{L}[\p{L}\p{M}]*/gu) ?? [];
|
|
1449
1468
|
if (words.length >= 3 && words.some((word) => /\p{Ll}/u.test(word))) cut = Math.max(cut, match.index + match[0].length);
|
|
1450
1469
|
}
|
|
1451
1470
|
}
|
|
@@ -1572,11 +1591,6 @@ const processLegalFormMatches = (allMatches, sliceStart, sliceEnd, fullText) =>
|
|
|
1572
1591
|
let { prefixEnd, prefixPart } = getPrefixInfo(entityText);
|
|
1573
1592
|
let isAllCapsMatch = prefixPart.length > 2 && prefixPart === prefixPart.toUpperCase();
|
|
1574
1593
|
if (isAllCapsMatch && fullText) {
|
|
1575
|
-
const lineStart = fullText.lastIndexOf("\n", entityStart);
|
|
1576
|
-
const lineEnd = fullText.indexOf("\n", entityStart + entityText.length);
|
|
1577
|
-
const lineLetters = fullText.slice(lineStart + 1, lineEnd === -1 ? fullText.length : lineEnd).replace(/[^a-zA-ZÀ-ž]/g, "");
|
|
1578
|
-
const upperCount = [...lineLetters].filter((c) => c === c.toUpperCase()).length;
|
|
1579
|
-
if (lineLetters.length > 5 && upperCount / lineLetters.length >= .95) continue;
|
|
1580
1594
|
if ((prefixPart.length > 0 ? entityText.slice(0, prefixEnd > 0 ? prefixEnd : entityText.length).trim().split(/\s+/).length : 0) > 3) {
|
|
1581
1595
|
entityStart = match.start;
|
|
1582
1596
|
entityText = text;
|
|
@@ -2145,7 +2159,31 @@ const segmentWords = (fullText) => {
|
|
|
2145
2159
|
};
|
|
2146
2160
|
/** NAME or SURNAME — both represent corpus-matched tokens */
|
|
2147
2161
|
const isCorpusMatch = (type) => type === TOKEN_TYPE.NAME || type === TOKEN_TYPE.SURNAME;
|
|
2148
|
-
const
|
|
2162
|
+
const ALL_CAPS_NAME_LINE_RATIO = .9;
|
|
2163
|
+
const ALL_CAPS_NAME_LINE_MIN_LETTERS = 3;
|
|
2164
|
+
const ALL_CAPS_NAME_LINE_MAX_TOKENS = 6;
|
|
2165
|
+
const isAllCapsLineNameShaped = (fullText, start) => {
|
|
2166
|
+
const lineStart = fullText.lastIndexOf("\n", start - 1) + 1;
|
|
2167
|
+
const lineEndIdx = fullText.indexOf("\n", start);
|
|
2168
|
+
const line = fullText.slice(lineStart, lineEndIdx === -1 ? fullText.length : lineEndIdx);
|
|
2169
|
+
if (/\d/.test(line)) return false;
|
|
2170
|
+
const tokens = line.match(/\p{L}[\p{L}\p{M}'\-]*/gu) ?? [];
|
|
2171
|
+
return tokens.length > 0 && tokens.length <= ALL_CAPS_NAME_LINE_MAX_TOKENS;
|
|
2172
|
+
};
|
|
2173
|
+
const isAllCapsContextLine = (fullText, start) => {
|
|
2174
|
+
const lineStart = fullText.lastIndexOf("\n", start - 1) + 1;
|
|
2175
|
+
const lineEndIdx = fullText.indexOf("\n", start);
|
|
2176
|
+
const line = fullText.slice(lineStart, lineEndIdx === -1 ? fullText.length : lineEndIdx);
|
|
2177
|
+
let letters = 0;
|
|
2178
|
+
let upper = 0;
|
|
2179
|
+
for (const ch of line) if (/\p{L}/u.test(ch)) {
|
|
2180
|
+
letters += 1;
|
|
2181
|
+
if (ch === ch.toUpperCase() && ch !== ch.toLowerCase()) upper += 1;
|
|
2182
|
+
}
|
|
2183
|
+
if (letters < ALL_CAPS_NAME_LINE_MIN_LETTERS) return false;
|
|
2184
|
+
return upper / letters >= ALL_CAPS_NAME_LINE_RATIO;
|
|
2185
|
+
};
|
|
2186
|
+
const classifyToken = (word, corpus, fullText) => {
|
|
2149
2187
|
const { text, start, end } = word;
|
|
2150
2188
|
const lower = text.toLowerCase();
|
|
2151
2189
|
const stripped = text.endsWith(".") ? text.slice(0, -1).toLowerCase() : lower;
|
|
@@ -2161,6 +2199,26 @@ const classifyToken = (word, corpus) => {
|
|
|
2161
2199
|
start,
|
|
2162
2200
|
end
|
|
2163
2201
|
};
|
|
2202
|
+
if (text.length === 1 && UPPER_START_RE.test(text) && fullText[end] === ".") {
|
|
2203
|
+
const lineStart = fullText.lastIndexOf("\n", start - 1) + 1;
|
|
2204
|
+
const before = fullText.slice(lineStart, start).trimEnd();
|
|
2205
|
+
const lastWord = /\p{L}[\p{L}\p{M}'\-]*$/u.exec(before)?.[0];
|
|
2206
|
+
if (lastWord) {
|
|
2207
|
+
const lookup = (token) => isFirstNameToken(token, corpus) || isFirstNameToken((token[0] ?? "") + token.slice(1).toLowerCase(), corpus);
|
|
2208
|
+
if (lookup(lastWord)) return {
|
|
2209
|
+
text,
|
|
2210
|
+
type: TOKEN_TYPE.ABBREVIATION,
|
|
2211
|
+
start,
|
|
2212
|
+
end
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
return {
|
|
2216
|
+
text,
|
|
2217
|
+
type: TOKEN_TYPE.OTHER,
|
|
2218
|
+
start,
|
|
2219
|
+
end
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2164
2222
|
if (corpus.excludedWords.has(lower)) return {
|
|
2165
2223
|
text,
|
|
2166
2224
|
type: TOKEN_TYPE.OTHER,
|
|
@@ -2173,12 +2231,29 @@ const classifyToken = (word, corpus) => {
|
|
|
2173
2231
|
start,
|
|
2174
2232
|
end
|
|
2175
2233
|
};
|
|
2176
|
-
if (text.length
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2234
|
+
if (text.length >= 3 && ALL_UPPER_RE.test(text)) {
|
|
2235
|
+
if (isAllCapsContextLine(fullText, start) && isAllCapsLineNameShaped(fullText, start)) {
|
|
2236
|
+
const titleCased = (text[0] ?? "") + text.slice(1).toLowerCase();
|
|
2237
|
+
if (isFirstNameToken(titleCased, corpus)) return {
|
|
2238
|
+
text,
|
|
2239
|
+
type: TOKEN_TYPE.NAME,
|
|
2240
|
+
start,
|
|
2241
|
+
end
|
|
2242
|
+
};
|
|
2243
|
+
if (isSurnameToken(titleCased, corpus)) return {
|
|
2244
|
+
text,
|
|
2245
|
+
type: TOKEN_TYPE.SURNAME,
|
|
2246
|
+
start,
|
|
2247
|
+
end
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
return {
|
|
2251
|
+
text,
|
|
2252
|
+
type: TOKEN_TYPE.OTHER,
|
|
2253
|
+
start,
|
|
2254
|
+
end
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2182
2257
|
if (!UPPER_START_RE.test(text)) return {
|
|
2183
2258
|
text,
|
|
2184
2259
|
type: TOKEN_TYPE.OTHER,
|
|
@@ -2223,7 +2298,7 @@ const classifyToken = (word, corpus) => {
|
|
|
2223
2298
|
const detectNameCorpus = (fullText, ctx = defaultContext) => {
|
|
2224
2299
|
const corpus = getCorpus(ctx);
|
|
2225
2300
|
if (!corpus) return [];
|
|
2226
|
-
const tokens = segmentWords(fullText).map((w) => classifyToken(w, corpus));
|
|
2301
|
+
const tokens = segmentWords(fullText).map((w) => classifyToken(w, corpus, fullText));
|
|
2227
2302
|
const entities = [];
|
|
2228
2303
|
const consumed = /* @__PURE__ */ new Set();
|
|
2229
2304
|
for (let i = 0; i < tokens.length; i++) {
|
|
@@ -2261,6 +2336,8 @@ const detectNameCorpus = (fullText, ctx = defaultContext) => {
|
|
|
2261
2336
|
else if (hasAbbreviation && hasCorpusName) score = .7;
|
|
2262
2337
|
else if (hasFirstName && chain.length === 1) {
|
|
2263
2338
|
if (isSentenceStart(fullText, token.start)) continue;
|
|
2339
|
+
const first = chain[0];
|
|
2340
|
+
if (first && ALL_UPPER_RE.test(first.text) && first.text.length >= 3) continue;
|
|
2264
2341
|
score = .5;
|
|
2265
2342
|
} else if (!hasFirstName && chain.length === 1 && chain[0]?.type === TOKEN_TYPE.SURNAME) continue;
|
|
2266
2343
|
else if (hasTitle && chain.length === 1) continue;
|
|
@@ -2287,6 +2364,115 @@ const detectNameCorpus = (fullText, ctx = defaultContext) => {
|
|
|
2287
2364
|
return entities;
|
|
2288
2365
|
};
|
|
2289
2366
|
//#endregion
|
|
2367
|
+
//#region src/detectors/signatures.ts
|
|
2368
|
+
const SLASH_S_RE = /\/s\/[ \t]*/g;
|
|
2369
|
+
const LABELLED_NAME_RE = /\b(?:By|Name)[ \t]*:[ \t]*(?:\/s\/[ \t]+)?([^\n]*?)(?=\s{3,}|[\t]|\n|$)/gi;
|
|
2370
|
+
const WITNESS_ANCHOR_RE = /\bIN WITNESS WHEREOF\b[^\n]*\n/gi;
|
|
2371
|
+
const NAME_PARTICLE = "(?:de|del|della|der|den|di|du|el|la|le|van|von|y|zu|af|ben|bin|al|d'|d’)";
|
|
2372
|
+
const CAP_TOKEN = "\\p{Lu}[\\p{L}\\p{M}.'\\-]{0,30}";
|
|
2373
|
+
const NAME_SHAPE_RE = new RegExp(`^${CAP_TOKEN}(?:[ \\t]+(?:${NAME_PARTICLE}|${CAP_TOKEN})){1,4}$`, "u");
|
|
2374
|
+
const MAX_NAME_LEN = 60;
|
|
2375
|
+
const POST_NOMINAL_SUFFIX_RE = /,\s*(?:Jr|Sr|II|III|IV|V|Esq|Esquire|M\.?D|Ph\.?D|J\.?D|LL\.?M|MBA|CPA|PE|RN|DDS|DVM|DO|MD|CFA|CFP)\.?\s*$/i;
|
|
2376
|
+
const COLUMN_SEPARATOR_RE = /\s{3,}|\t+/;
|
|
2377
|
+
const ORG_SUFFIX_RE = /\b(?:INC\.?|LLC|LLP|LP|CORP\.?|CORPORATION|LTD\.?|GMBH|AG|SE|KG|OHG|SA|SAS|SARL|S\.A\.?|S\.P\.A\.?|PLC|N\.A\.?|N\.V\.?|B\.V\.?|PTY\s+LTD\.?|CO\.|S\.R\.O\.?|A\.S\.?|Z\.S\.?|S\.\s*P\.?|LTDA\.?|EIRELI|EPP|S\/A)\b/i;
|
|
2378
|
+
const IMAGE_STUB_RE = /^(?:\[?img|\[image|\[logo|\(logo\))/i;
|
|
2379
|
+
const normaliseCandidate = (text) => {
|
|
2380
|
+
let candidate = text.trim();
|
|
2381
|
+
candidate = candidate.replace(POST_NOMINAL_SUFFIX_RE, "").trim();
|
|
2382
|
+
return candidate.split(COLUMN_SEPARATOR_RE)[0]?.trim() ?? candidate;
|
|
2383
|
+
};
|
|
2384
|
+
const isNameShape = (text) => {
|
|
2385
|
+
if (text.length === 0 || text.length > MAX_NAME_LEN) return false;
|
|
2386
|
+
if (text.length < 3) return false;
|
|
2387
|
+
if (!NAME_SHAPE_RE.test(text)) return false;
|
|
2388
|
+
return true;
|
|
2389
|
+
};
|
|
2390
|
+
const findLineEnd = (text, pos) => {
|
|
2391
|
+
const idx = text.indexOf("\n", pos);
|
|
2392
|
+
return idx === -1 ? text.length : idx;
|
|
2393
|
+
};
|
|
2394
|
+
const tryEmit = (results, fullText, start, end, score) => {
|
|
2395
|
+
const raw = fullText.slice(start, end);
|
|
2396
|
+
if (ORG_SUFFIX_RE.test(raw)) return false;
|
|
2397
|
+
const candidate = normaliseCandidate(raw);
|
|
2398
|
+
if (!isNameShape(candidate)) return false;
|
|
2399
|
+
const offset = raw.indexOf(candidate);
|
|
2400
|
+
if (offset < 0) return false;
|
|
2401
|
+
const absStart = start + offset;
|
|
2402
|
+
results.push({
|
|
2403
|
+
start: absStart,
|
|
2404
|
+
end: absStart + candidate.length,
|
|
2405
|
+
label: "person",
|
|
2406
|
+
text: candidate,
|
|
2407
|
+
score,
|
|
2408
|
+
source: DETECTION_SOURCES.TRIGGER
|
|
2409
|
+
});
|
|
2410
|
+
return true;
|
|
2411
|
+
};
|
|
2412
|
+
const tryEmitForwardLines = (results, fullText, fromPos, maxLines, score) => {
|
|
2413
|
+
let pos = fromPos;
|
|
2414
|
+
for (let i = 0; i < maxLines; i++) {
|
|
2415
|
+
if (pos >= fullText.length) return false;
|
|
2416
|
+
const lineEnd = findLineEnd(fullText, pos);
|
|
2417
|
+
const line = fullText.slice(pos, lineEnd).trim();
|
|
2418
|
+
if (line.length > 0 && !IMAGE_STUB_RE.test(line)) {
|
|
2419
|
+
if (tryEmit(results, fullText, pos, lineEnd, score)) return true;
|
|
2420
|
+
}
|
|
2421
|
+
pos = lineEnd + 1;
|
|
2422
|
+
}
|
|
2423
|
+
return false;
|
|
2424
|
+
};
|
|
2425
|
+
const findPrevLine = (fullText, pos) => {
|
|
2426
|
+
let cursor = pos - 1;
|
|
2427
|
+
while (cursor >= 0 && fullText.charAt(cursor) !== "\n") cursor--;
|
|
2428
|
+
while (cursor >= 0) {
|
|
2429
|
+
let lineStart = cursor;
|
|
2430
|
+
while (lineStart > 0 && fullText.charAt(lineStart - 1) !== "\n") lineStart -= 1;
|
|
2431
|
+
const lineEnd = cursor;
|
|
2432
|
+
const line = fullText.slice(lineStart, lineEnd).trim();
|
|
2433
|
+
if (line.length > 0 && !IMAGE_STUB_RE.test(line)) return {
|
|
2434
|
+
lineStart,
|
|
2435
|
+
lineEnd
|
|
2436
|
+
};
|
|
2437
|
+
cursor = lineStart - 1;
|
|
2438
|
+
}
|
|
2439
|
+
return null;
|
|
2440
|
+
};
|
|
2441
|
+
const detectSignatures = (fullText, _ctx = defaultContext) => {
|
|
2442
|
+
const results = [];
|
|
2443
|
+
SLASH_S_RE.lastIndex = 0;
|
|
2444
|
+
for (let m = SLASH_S_RE.exec(fullText); m !== null; m = SLASH_S_RE.exec(fullText)) {
|
|
2445
|
+
const afterMark = m.index + m[0].length;
|
|
2446
|
+
const lineEnd = findLineEnd(fullText, afterMark);
|
|
2447
|
+
if (fullText.slice(afterMark, lineEnd).trim().length > 0) {
|
|
2448
|
+
const firstCell = fullText.slice(afterMark, lineEnd).split(COLUMN_SEPARATOR_RE)[0] ?? "";
|
|
2449
|
+
if (firstCell.trim().length > 0) tryEmit(results, fullText, afterMark, afterMark + firstCell.length, .95);
|
|
2450
|
+
} else tryEmitForwardLines(results, fullText, lineEnd + 1, 4, .9);
|
|
2451
|
+
const prev = findPrevLine(fullText, m.index);
|
|
2452
|
+
if (prev) tryEmit(results, fullText, prev.lineStart, prev.lineEnd, .85);
|
|
2453
|
+
}
|
|
2454
|
+
LABELLED_NAME_RE.lastIndex = 0;
|
|
2455
|
+
for (let m = LABELLED_NAME_RE.exec(fullText); m !== null; m = LABELLED_NAME_RE.exec(fullText)) {
|
|
2456
|
+
const value = m[1];
|
|
2457
|
+
if (value === void 0) continue;
|
|
2458
|
+
const valueStart = m.index + m[0].length - value.length;
|
|
2459
|
+
const valueEnd = valueStart + value.length;
|
|
2460
|
+
if (value.trim().length === 0) {
|
|
2461
|
+
tryEmitForwardLines(results, fullText, valueEnd + 1, 3, .9);
|
|
2462
|
+
continue;
|
|
2463
|
+
}
|
|
2464
|
+
tryEmit(results, fullText, valueStart, valueEnd, .95);
|
|
2465
|
+
}
|
|
2466
|
+
WITNESS_ANCHOR_RE.lastIndex = 0;
|
|
2467
|
+
for (let m = WITNESS_ANCHOR_RE.exec(fullText); m !== null; m = WITNESS_ANCHOR_RE.exec(fullText)) {
|
|
2468
|
+
const search = fullText.slice(m.index, m.index + 600);
|
|
2469
|
+
const sentenceEnd = /[.:;]\s*\n|\n\s*\n/.exec(search);
|
|
2470
|
+
if (!sentenceEnd) continue;
|
|
2471
|
+
tryEmitForwardLines(results, fullText, m.index + sentenceEnd.index + sentenceEnd[0].length, 6, .85);
|
|
2472
|
+
}
|
|
2473
|
+
return results;
|
|
2474
|
+
};
|
|
2475
|
+
//#endregion
|
|
2290
2476
|
//#region src/config/titles.ts
|
|
2291
2477
|
/**
|
|
2292
2478
|
* Academic and professional title prefixes.
|
|
@@ -4106,6 +4292,47 @@ const MAX_ENTITY_LENGTH = {
|
|
|
4106
4292
|
organization: 80,
|
|
4107
4293
|
person: 60
|
|
4108
4294
|
};
|
|
4295
|
+
const MAX_ENTITY_WORDS = { organization: 8 };
|
|
4296
|
+
const OPEN_ENDED_SOURCES = new Set(["trigger", "coreference"]);
|
|
4297
|
+
const WORD_TOKEN_RE = /\p{L}[\p{L}\p{M}\p{N}'’\-./]*/gu;
|
|
4298
|
+
const countWordTokens = (text) => {
|
|
4299
|
+
let count = 0;
|
|
4300
|
+
WORD_TOKEN_RE.lastIndex = 0;
|
|
4301
|
+
while (WORD_TOKEN_RE.exec(text) !== null) count += 1;
|
|
4302
|
+
return count;
|
|
4303
|
+
};
|
|
4304
|
+
const ALL_CAPS_LINE_LETTER_THRESHOLD = 5;
|
|
4305
|
+
const ALL_CAPS_LINE_RATIO = .95;
|
|
4306
|
+
const ALL_CAPS_LINE_PROSE_EXTRA_LETTERS = 20;
|
|
4307
|
+
const ALL_CAPS_LINE_HEADING_WORD_LIMIT = 5;
|
|
4308
|
+
const SECTION_HEADING_PREFIX_RE = /^\s*(?:§\s*)?\d{1,3}(?:\.\d{1,3}){0,4}\.?\s*\p{Lu}/u;
|
|
4309
|
+
const LINE_LETTER_RE = /\p{L}/gu;
|
|
4310
|
+
const ENTITY_WORD_TOKEN_RE = /\p{L}[\p{L}\p{M}\p{N}'’\-]*/gu;
|
|
4311
|
+
const isAllCapsBoilerplateLine = (fullText, start, length) => {
|
|
4312
|
+
const lineStart = fullText.lastIndexOf("\n", start) + 1;
|
|
4313
|
+
const lineEndIdx = fullText.indexOf("\n", start + length);
|
|
4314
|
+
const line = fullText.slice(lineStart, lineEndIdx === -1 ? fullText.length : lineEndIdx);
|
|
4315
|
+
let letterCount = 0;
|
|
4316
|
+
let upperCount = 0;
|
|
4317
|
+
let outsideEntityLetters = 0;
|
|
4318
|
+
const entityRelStart = start - lineStart;
|
|
4319
|
+
const entityRelEnd = entityRelStart + length;
|
|
4320
|
+
LINE_LETTER_RE.lastIndex = 0;
|
|
4321
|
+
for (let m = LINE_LETTER_RE.exec(line); m !== null; m = LINE_LETTER_RE.exec(line)) {
|
|
4322
|
+
const ch = m[0];
|
|
4323
|
+
letterCount += 1;
|
|
4324
|
+
if (ch === ch.toUpperCase() && ch !== ch.toLowerCase()) upperCount += 1;
|
|
4325
|
+
if (m.index < entityRelStart || m.index >= entityRelEnd) outsideEntityLetters += 1;
|
|
4326
|
+
}
|
|
4327
|
+
if (letterCount <= ALL_CAPS_LINE_LETTER_THRESHOLD) return false;
|
|
4328
|
+
if (upperCount / letterCount < ALL_CAPS_LINE_RATIO) return false;
|
|
4329
|
+
if (SECTION_HEADING_PREFIX_RE.test(line)) return true;
|
|
4330
|
+
if (outsideEntityLetters >= ALL_CAPS_LINE_PROSE_EXTRA_LETTERS) return true;
|
|
4331
|
+
const entityText = fullText.slice(start, start + length);
|
|
4332
|
+
if ((entityText.match(ENTITY_WORD_TOKEN_RE) ?? []).length > ALL_CAPS_LINE_HEADING_WORD_LIMIT && !entityText.includes(",")) return true;
|
|
4333
|
+
return false;
|
|
4334
|
+
};
|
|
4335
|
+
const isAllCapsCandidate = (text) => text === text.toUpperCase() && /\p{Lu}/u.test(text);
|
|
4109
4336
|
const SECTION_NUMBER_RE = /^(?:§\s*)?\d{1,3}(?:\.\d{1,3}){0,4}\.?$/;
|
|
4110
4337
|
const STANDALONE_YEAR_RE = /^(?:19|20)\d{2}$/;
|
|
4111
4338
|
const NUMBER_ABBREV_RE = /(?:^|[\s(])(?:č|čís|nr|no|n)\.\s*$/i;
|
|
@@ -4245,6 +4472,9 @@ const filterFalsePositives = (entities, ctx = defaultContext, fullText) => {
|
|
|
4245
4472
|
if (TEMPLATE_PLACEHOLDER_RE.test(trimmed)) continue;
|
|
4246
4473
|
const maxLen = MAX_ENTITY_LENGTH[normalized.label];
|
|
4247
4474
|
if (maxLen && trimmed.length > maxLen && normalized.source !== "legal-form") continue;
|
|
4475
|
+
const maxWords = MAX_ENTITY_WORDS[normalized.label];
|
|
4476
|
+
if (maxWords && OPEN_ENDED_SOURCES.has(normalized.source) && countWordTokens(trimmed) > maxWords) continue;
|
|
4477
|
+
if (fullText && normalized.label === "organization" && isAllCapsCandidate(trimmed) && isAllCapsBoilerplateLine(fullText, normalized.start, trimmed.length)) continue;
|
|
4248
4478
|
if (SECTION_NUMBER_RE.test(trimmed) && normalized.source !== "trigger") continue;
|
|
4249
4479
|
if (STANDALONE_YEAR_RE.test(trimmed) && normalized.source !== "trigger") continue;
|
|
4250
4480
|
if (fullText && normalized.source !== "trigger" && /^\d/.test(trimmed) && NUMBER_ABBREV_RE.test(fullText.slice(Math.max(0, normalized.start - 10), normalized.start))) continue;
|
|
@@ -6310,7 +6540,7 @@ const mergeAdjacent = (entities, fullText) => {
|
|
|
6310
6540
|
break;
|
|
6311
6541
|
}
|
|
6312
6542
|
}
|
|
6313
|
-
if (!hasLockedBoundary$1(prev) && !(isLegalFormOrganization(prev)
|
|
6543
|
+
if (!hasLockedBoundary$1(prev) && !((isLegalFormOrganization(prev) || isLegalFormOrganization(entity)) && gap.includes(",")) && !gapOccupied && gap.length <= MAX_GAP && GAP_PATTERN.test(gap)) {
|
|
6314
6544
|
prev.end = entity.end;
|
|
6315
6545
|
prev.text = fullText.slice(prev.start, prev.end);
|
|
6316
6546
|
prev.score = Math.max(prev.score, entity.score);
|
|
@@ -7109,6 +7339,8 @@ const runPipeline = async (options) => {
|
|
|
7109
7339
|
const rawTriggerEntities = config.enableTriggerPhrases ? processTriggerMatches(regexMatches, slices.triggers.start, slices.triggers.end, fullText, search.triggerRules) : [];
|
|
7110
7340
|
const triggerEntities = filterAllowedLabels(rawTriggerEntities, preHotwordAllowedLabels);
|
|
7111
7341
|
if (triggerEntities.length > 0) log("trigger-phrases", `${triggerEntities.length} matches`);
|
|
7342
|
+
const signatureEntities = filterAllowedLabels(detectSignatures(fullText, ctx), preHotwordAllowedLabels);
|
|
7343
|
+
if (signatureEntities.length > 0) log("signatures", `${signatureEntities.length} matches`);
|
|
7112
7344
|
checkAbort(signal);
|
|
7113
7345
|
let rawNameCorpusEntities = [];
|
|
7114
7346
|
let nameCorpusEntities = [];
|
|
@@ -7165,6 +7397,7 @@ const runPipeline = async (options) => {
|
|
|
7165
7397
|
checkAbort(signal);
|
|
7166
7398
|
const preAddressEntities = [
|
|
7167
7399
|
...triggerEntities,
|
|
7400
|
+
...signatureEntities,
|
|
7168
7401
|
...regexEntities,
|
|
7169
7402
|
...legalFormEntities,
|
|
7170
7403
|
...nameCorpusEntities,
|