@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.
@@ -412,7 +412,6 @@ var words = [
412
412
  "ant",
413
413
  "antarctica",
414
414
  "antenna",
415
- "anthony",
416
415
  "anthropology",
417
416
  "anti",
418
417
  "antibodies",
@@ -6,7 +6,12 @@ var en = {
6
6
  "by and between",
7
7
  "is between"
8
8
  ],
9
- "directPrefixes": ["by"]
9
+ "directPrefixes": [
10
+ "by",
11
+ "among",
12
+ "amongst",
13
+ "between"
14
+ ]
10
15
  };
11
16
  var legal_form_leading_clauses_default = {
12
17
  _comment,
@@ -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 words = text.slice(0, match.index).match(/\p{L}[\p{L}\p{M}]*/gu) ?? [];
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 classifyToken = (word, corpus) => {
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 > 3 && ALL_UPPER_RE.test(text)) return {
2177
- text,
2178
- type: TOKEN_TYPE.OTHER,
2179
- start,
2180
- end
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) && isLegalFormOrganization(entity) && gap.includes(",")) && !gapOccupied && gap.length <= MAX_GAP && GAP_PATTERN.test(gap)) {
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,