@stll/anonymize-wasm 1.4.8 → 1.4.10

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/wasm.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"wasm.mjs","names":["charGroupsJson","m","CONNECTOR_RE","AND_TYPE_CONNECTOR_RE","UPPER_LETTER_RE","hasMiddleInitialBefore","m","CHUNK_SIZE","countriesData","PERSON_CHAIN_BREAK_RE","isInitialContinuationGap","segmenter","escapeRegex","amountWordsConfig","WHITESPACE_RE","_exhaustive","m","POSTAL_CODE_RE","EMPTY_GENERIC_ROLES","escapeRegex","isCallerOwnedEntity","WORD_CHAR_RE","isCallerOwnedEntity","isCallerOwnedEntity","addressStreetTypesJson","hasLockedBoundary","DEFAULT_CUSTOM_REGEX_SCORE","createAllowedLabelSet","labelIsAllowed","sigmoid"],"sources":["../../src/search-engine.ts","../../src/types.ts","../../src/context.ts","../../src/util/lang-loader.ts","../../src/config/legal-forms.ts","../../src/data/char-groups.json","../../src/util/char-groups.ts","../../src/detectors/legal-forms.ts","../../src/detectors/coreference.ts","../../src/detectors/gazetteer.ts","../../src/util/normalize.ts","../../src/data/countries.json","../../src/detectors/countries.ts","../../src/util/text.ts","../../src/detectors/names.ts","../../src/detectors/signatures.ts","../../src/config/titles.ts","../../src/data/amount-words.json","../../src/detectors/regex.ts","../../src/detectors/legal-forms-v2.ts","../../src/detectors/triggers.ts","../../src/regions.ts","../../src/util/homoglyphs.ts","../../src/filters/false-positives.ts","../../src/detectors/deny-list.ts","../../src/detectors/address-seeds.ts","../../src/detectors/org-propagation.ts","../../src/data/address-street-types.json","../../src/filters/confidence-boost.ts","../../src/filters/zone-classifier.ts","../../src/filters/hotword-rules.ts","../../src/filters/boundary-consistency.ts","../../src/build-unified-search.ts","../../src/unified-search.ts","../../src/util/entity-masking.ts","../../src/pipeline.ts","../../src/operators.ts","../../src/redact.ts","../../src/gliner/decoder.ts","../../src/gliner/token-decoder.ts","../../src/gliner/processor.ts","../../src/util/chunker.ts","../../src/util/levenshtein.ts","../../src/wasm.ts"],"sourcesContent":["/**\n * Late-bound TextSearch constructor.\n *\n * Native entry injects @stll/text-search,\n * WASM entry injects @stll/text-search-wasm.\n * Both expose the same API.\n */\n\n/* eslint-disable-next-line @typescript-eslint/no-explicit-any */\ntype TextSearchCtor = new (...args: any[]) => any;\n\nlet _TextSearch: TextSearchCtor | undefined;\n\nexport const initTextSearch = (ctor: TextSearchCtor): void => {\n _TextSearch = ctor;\n};\n\nexport const getTextSearch = (): TextSearchCtor => {\n if (!_TextSearch) {\n throw new Error(\n \"TextSearch not initialized. Import from \" +\n \"@stll/anonymize or @stll/anonymize-wasm, \" +\n \"not from internal modules.\",\n );\n }\n return _TextSearch;\n};\n","// Runtime-free constants live in `./constants`; re-exported\n// here for back-compat with existing call sites that import\n// from `@stll/anonymize` directly.\n//\n// `verbatimModuleSyntax` requires an explicit type-only\n// import for any name used locally as a type even when it\n// is also re-exported below — applies to `DetectionSource`\n// (used by `Entity`) and `OperatorType` (used by\n// `OperatorConfig`).\nimport type { DetectionSource, OperatorType } from \"./constants\";\n\nexport {\n DETECTION_SOURCES,\n DETECTOR_PRIORITY,\n type DetectionSource,\n} from \"./constants\";\n\n/**\n * A detected PII entity span in the source text.\n * Every detection layer produces these.\n */\nexport type Entity = {\n start: number;\n end: number;\n label: string;\n text: string;\n score: number;\n source: DetectionSource;\n sourceDetail?: \"custom-deny-list\" | \"custom-regex\" | \"gazetteer-extension\";\n};\n\n/**\n * Entity after human review. Extends the base Entity\n * with a review decision.\n */\nexport type ReviewDecision = \"confirmed\" | \"rejected\" | \"relabeled\";\n\nexport type ReviewedEntity = Entity & {\n decision?: ReviewDecision;\n originalLabel?: string;\n};\n\n/**\n * A single entry in the workspace-scoped gazetteer\n * (deny list). Persisted in IndexedDB.\n */\nexport type GazetteerEntry = {\n id: string;\n canonical: string;\n label: string;\n variants: string[];\n workspaceId: string;\n createdAt: number;\n source: \"manual\" | \"confirmed-from-model\";\n};\n\n/** Extraction strategy — closed discriminated union. */\nexport type TriggerStrategy =\n | {\n type: \"to-next-comma\";\n /**\n * Optional list of lowercase keywords that terminate\n * the value scan, in addition to commas/newlines. Useful\n * for triggers like court names that may continue past\n * a missing comma into adjacent clause text (\"Městským\n * soudem v Praze dne 1. 1. 2020\"); listing `\"dne\"` here\n * stops the scan at the date boundary. Matched on a\n * word-boundary, case-insensitive.\n */\n stopWords?: string[];\n /**\n * Hard cap on the captured span length, in characters,\n * regardless of where the next comma / stop char sits.\n * Use for triggers that label short formulaic phrases\n * (\"State of Delaware\") and must not absorb the rest\n * of a long forum-selection clause when the comma is\n * sentences away. Falls back to the default 100-char\n * fallback when omitted.\n */\n maxLength?: number;\n }\n | { type: \"to-end-of-line\" }\n | { type: \"n-words\"; count: number }\n | { type: \"company-id-value\" }\n | { type: \"address\"; maxChars?: number }\n | {\n /**\n * Extract the first regex match in the value text.\n * Useful for shape-bounded values that follow a\n * label on the same line as other fields, where\n * `to-end-of-line` would over-capture. The pattern\n * is anchored to the start of the (already\n * leading-whitespace-stripped) value, so use\n * `(?:.*?)` prefix only when intentional.\n */\n type: \"match-pattern\";\n pattern: string;\n flags?: string;\n };\n\n/** Validation rules — closed discriminated union. */\nexport type TriggerValidation =\n | { type: \"starts-uppercase\" }\n | { type: \"min-length\"; min: number }\n | { type: \"max-length\"; max: number }\n | { type: \"no-digits\" }\n | { type: \"has-digits\" }\n | {\n type: \"matches-pattern\";\n pattern: string;\n flags?: string;\n }\n /**\n * Run a named stdnum validator (checksum + length)\n * against the captured value. Keeps the trigger\n * path symmetrical with the formatted-regex\n * detectors so e.g. `CPF nº 00000000000` does not\n * survive as a tax-ID entity.\n */\n | { type: \"valid-id\"; validator: ValidIdValidator };\n\n/** Built-in stdnum validators that can be referenced\n * by `valid-id` validations. */\nexport type ValidIdValidator = \"br.cpf\" | \"br.cnpj\" | \"us.rtn\";\n\n/** Auto-generated trigger variants — closed set. */\nexport type TriggerExtension =\n | \"add-colon\"\n | \"add-trailing-space\"\n | \"add-colon-space\"\n | \"normalize-spaces\";\n\n/** V2 trigger config entry (JSON shape). */\nexport type TriggerGroupConfig = {\n id?: string;\n triggers: string[];\n label: string;\n strategy: TriggerStrategy;\n extensions?: TriggerExtension[];\n validations?: TriggerValidation[];\n /** When true, include the trigger text in the\n * entity span (e.g., court names). */\n includeTrigger?: boolean;\n};\n\n/** Compiled validation with pre-built regex. */\nexport type CompiledValidation =\n | { type: \"starts-uppercase\"; re: RegExp }\n | { type: \"min-length\"; min: number }\n | { type: \"max-length\"; max: number }\n | { type: \"no-digits\"; re: RegExp }\n | { type: \"has-digits\"; re: RegExp }\n | { type: \"matches-pattern\"; re: RegExp }\n | { type: \"valid-id\"; check: (value: string) => boolean };\n\n/**\n * Runtime rule — one per trigger string after\n * expansion. Fed to the Aho-Corasick automaton.\n */\nexport type TriggerRule = {\n trigger: string;\n label: string;\n strategy: TriggerStrategy;\n validations: CompiledValidation[];\n includeTrigger: boolean;\n};\n\nexport { OPERATOR_TYPES, type OperatorType } from \"./constants\";\n\n/** Per-label operator selection. Key is the entity label. */\nexport type OperatorConfig = {\n /** Operator per label. Missing labels default to \"replace\". */\n operators: Record<string, OperatorType>;\n /** Custom replacement string for the redact operator. */\n redactString: string;\n};\n\n/** Whether an operator produces a reversible redaction entry. */\ntype OperatorReversibility = \"reversible\" | \"irreversible\";\n\nexport type AnonymisationOperator = {\n type: OperatorType;\n reversibility: OperatorReversibility;\n /**\n * Apply the operator to a single entity occurrence.\n * Returns the replacement string to embed in the document.\n */\n apply: (\n text: string,\n label: string,\n placeholder: string,\n redactString: string,\n ) => string;\n};\n\n/**\n * Redacted document output with stable entity mapping.\n */\nexport type RedactionResult = {\n redactedText: string;\n /**\n * Maps placeholder to original text. Only populated for\n * reversible operators (replace). Empty for redact.\n */\n redactionMap: Map<string, string>;\n /** Maps placeholder to the operator that produced it. */\n operatorMap: Map<string, OperatorType>;\n entityCount: number;\n};\n\n/**\n * Configuration for the detection pipeline.\n */\nexport type DenyListCategory =\n | \"Names\"\n | \"Places\"\n | \"Addresses\"\n | \"Courts\"\n | \"Financial\"\n | \"Government\"\n | \"Healthcare\"\n | \"Education\"\n | \"Political\"\n | \"Organizations\"\n | \"International\";\n\n/**\n * Metadata for a single dictionary entry in the\n * deny-list system. Mirrors the shape from\n * the anonymize-data package so consumers can pass\n * pre-loaded data without a runtime dependency.\n */\nexport type DictionaryMeta = {\n label: string;\n category: DenyListCategory;\n country: string | null;\n};\n\n/**\n * Caller-supplied exact terms for deny-list matching.\n * These entries are merged with the published deny-list\n * dictionaries when `enableDenyList` is enabled.\n */\nexport type CustomDenyListEntry = {\n value: string;\n label: string;\n variants?: readonly string[];\n};\n\n/**\n * Caller-supplied regex detector. The pattern is passed\n * to the underlying text-search regex engine, so use its\n * supported regex syntax. Inline flags such as `(?i)` are\n * accepted when supported by that engine.\n */\nexport type CustomRegexPattern = {\n pattern: string;\n label: string;\n score?: number;\n};\n\n/**\n * Pre-loaded dictionary data for dependency injection.\n * Consumers that want name/city/deny-list detection\n * load dictionaries themselves (e.g. from the\n * anonymize-data package) and pass them here; the\n * anonymize package has zero cross-package imports.\n *\n * All fields are optional. When a field is absent,\n * the corresponding detection path is skipped (same\n * behavior as when no dictionaries are available).\n */\nexport type Dictionaries = {\n /**\n * First names per language code (e.g., \"cs\", \"de\").\n * Merged with legacy config names at init time.\n */\n firstNames?: Readonly<Record<string, readonly string[]>>;\n /**\n * Surnames per language code.\n * Merged with legacy config names at init time.\n */\n surnames?: Readonly<Record<string, readonly string[]>>;\n /**\n * Pre-loaded deny-list dictionaries keyed by\n * dictionary ID (e.g., \"courts/CZ\", \"banks/DE\").\n * Each value is the array of terms for that\n * dictionary.\n */\n denyList?: Readonly<Record<string, readonly string[]>>;\n /**\n * Metadata per dictionary ID. Required when\n * `denyList` is provided so the pipeline knows\n * labels, categories, and country filters.\n */\n denyListMeta?: Readonly<Record<string, DictionaryMeta>>;\n /**\n * Pre-loaded city names, already merged across\n * all desired countries.\n *\n * Prefer `citiesByCountry` when callers also pass\n * `denyListCountries` / `denyListRegions`; merged\n * city arrays cannot be scoped after injection.\n */\n cities?: readonly string[];\n /**\n * Pre-loaded city names keyed by ISO 3166-1 alpha-2\n * country code. When provided, the deny-list builder\n * applies `denyListCountries` / `denyListRegions`\n * before adding city patterns to the search automaton.\n */\n citiesByCountry?: Readonly<Record<string, readonly string[]>>;\n};\n\nexport type PipelineConfig = {\n threshold: number;\n enableTriggerPhrases: boolean;\n enableRegex: boolean;\n /**\n * Enables legal-form organization detection.\n * Required for typed callers; legacy untyped\n * callers that omit this field are treated as\n * enabled at runtime for backward compatibility.\n */\n enableLegalForms: boolean;\n /**\n * Enables first-name/surname/title corpus matching.\n * When deny-list mode is enabled, this also controls\n * whether name-corpus entries are injected into the\n * deny-list search automaton.\n */\n enableNameCorpus: boolean;\n /**\n * Optional language scope for first-name/surname\n * dictionaries, using the keys present in\n * `dictionaries.firstNames` / `dictionaries.surnames`\n * (for example `[\"en\", \"de\"]`). When omitted, all\n * injected name languages are used for backward\n * compatibility.\n */\n nameCorpusLanguages?: string[];\n enableDenyList: boolean;\n denyListCountries?: string[];\n denyListRegions?: string[];\n denyListExcludeCategories?: string[];\n /**\n * Caller-owned exact terms to match through the\n * deny-list layer. Requires `enableDenyList: true`.\n */\n customDenyList?: readonly CustomDenyListEntry[];\n /**\n * Caller-owned regex detectors. Requires\n * `enableRegex: true`.\n */\n customRegexes?: readonly CustomRegexPattern[];\n enableGazetteer: boolean;\n /**\n * Detect country names (ISO 3166-1 names, curated\n * aliases, alpha-3 codes). Defaults to true. Names\n * span all manifest languages plus widely-used\n * additions (Dutch, Russian, Chinese, Arabic, etc.).\n */\n enableCountries?: boolean;\n enableNer: boolean;\n enableConfidenceBoost: boolean;\n enableCoreference: boolean;\n enableZoneClassification?: boolean;\n enableHotwordRules?: boolean;\n /**\n * Requested output labels. An empty array means\n * \"do not filter by label\" for deterministic\n * detectors; NER falls back to DEFAULT_ENTITY_LABELS.\n */\n labels: string[];\n workspaceId: string;\n /**\n * Pre-loaded dictionary data for name, deny-list,\n * and city detection. When omitted, dictionary-based\n * detection paths are skipped. Consumers load from\n * the anonymize-data package and pass the data here.\n */\n dictionaries?: Dictionaries;\n};\n\nexport { DEFAULT_ENTITY_LABELS } from \"./constants\";\n\nexport const isLegalFormsEnabled = (\n config: Pick<PipelineConfig, \"enableLegalForms\">,\n): boolean => config.enableLegalForms !== false;\n","import type { UnifiedSearchInstance } from \"./build-unified-search\";\nimport type { Entity } from \"./types\";\n\n/**\n * Build a stable cache key for an entity that survives\n * shallow copies (spread). Uses position + label so the\n * key is identical for the original object and any\n * `{ ...entity }` copy produced by mergeAndDedup.\n */\nexport const corefKey = (e: Entity): string => `${e.start}:${e.end}:${e.label}`;\n\n/**\n * Compiled RegExp pattern used for coreference\n * definition extraction.\n */\nexport type DefinitionPattern = {\n pattern: RegExp;\n};\n\n/**\n * Cached data for the name corpus detector.\n * Populated by initNameCorpus; consumed by\n * detectNameCorpus and deny-list AC integration.\n */\nexport type NameCorpusData = {\n firstNames: ReadonlySet<string>;\n surnames: ReadonlySet<string>;\n titleTokens: ReadonlySet<string>;\n excludedWords: ReadonlySet<string>;\n /** Raw arrays exposed for deny-list AC integration. */\n firstNamesList: readonly string[];\n surnamesList: readonly string[];\n titlesList: readonly string[];\n excludedList: readonly string[];\n};\n\n/**\n * All cached state for a single pipeline run (or\n * sequence of runs sharing the same config). Replacing\n * module-level singletons with this object enables\n * concurrent pipelines with different configs and\n * simplifies testing.\n *\n * Each field starts null and is populated lazily on\n * first use by the corresponding loader function.\n */\nexport type PipelineContext = {\n // ── Unified search cache ──────────────────────\n search: UnifiedSearchInstance | null;\n searchKey: string;\n searchPromise: Promise<UnifiedSearchInstance> | null;\n\n // ── Name corpus ───────────────────────────────\n nameCorpus: NameCorpusData | null;\n nameCorpusKey: string;\n nameCorpusPromise: Promise<void> | null;\n\n // ── Deny-list data sets ───────────────────────\n stopwords: ReadonlySet<string> | null;\n stopwordsPromise: Promise<ReadonlySet<string>> | null;\n allowList: ReadonlySet<string> | null;\n allowListPromise: Promise<ReadonlySet<string>> | null;\n personStopwords: ReadonlySet<string> | null;\n personStopwordsPromise: Promise<ReadonlySet<string>> | null;\n addressStopwords: ReadonlySet<string> | null;\n addressStopwordsPromise: Promise<ReadonlySet<string>> | null;\n /** First-name exclusions for stopword filtering. */\n firstNameExclusions: ReadonlySet<string> | null;\n firstNameExclusionCorpusLen: number;\n\n // ── Generic roles (false-positive filter) ─────\n genericRoles: ReadonlySet<string> | null;\n genericRolesPromise: Promise<ReadonlySet<string>> | null;\n\n // ── Coreference ───────────────────────────────\n corefPatterns: DefinitionPattern[] | null;\n corefPatternsPromise: Promise<DefinitionPattern[]> | null;\n corefLoadAttempted: boolean;\n roleStopSet: ReadonlySet<string> | null;\n roleStopSetPromise: Promise<ReadonlySet<string>> | null;\n\n // ── Zone classifier ───────────────────────────\n zoneHeadingPatterns: RegExp[] | null;\n zoneSigningPatterns: RegExp[] | null;\n zoneInitPromise: Promise<void> | null;\n\n // ── Coreference source map ────────────────────\n /**\n * Maps coreference entities to their source entity\n * text. Populated by findCoreferenceSpans, consumed\n * by buildPlaceholderMap for consistent placeholder\n * numbering across aliases and source entities.\n *\n * Keyed by `start:end:label` composite string so\n * lookups survive shallow copies (e.g. from\n * mergeAndDedup's spread operator).\n */\n corefSourceMap: Map<string, string>;\n};\n\n/** Create a fresh, empty pipeline context. */\nexport const createPipelineContext = (): PipelineContext => ({\n search: null,\n searchKey: \"\",\n searchPromise: null,\n\n nameCorpus: null,\n nameCorpusKey: \"\",\n nameCorpusPromise: null,\n\n stopwords: null,\n stopwordsPromise: null,\n allowList: null,\n allowListPromise: null,\n personStopwords: null,\n personStopwordsPromise: null,\n addressStopwords: null,\n addressStopwordsPromise: null,\n firstNameExclusions: null,\n firstNameExclusionCorpusLen: 0,\n\n genericRoles: null,\n genericRolesPromise: null,\n\n corefPatterns: null,\n corefPatternsPromise: null,\n corefLoadAttempted: false,\n roleStopSet: null,\n roleStopSetPromise: null,\n\n zoneHeadingPatterns: null,\n zoneSigningPatterns: null,\n zoneInitPromise: null,\n\n corefSourceMap: new Map(),\n});\n\n/**\n * Module-level default context. Used when callers\n * don't provide an explicit context, preserving full\n * backward compatibility with the existing API.\n */\nexport const defaultContext: PipelineContext = createPipelineContext();\n","/**\n * Language manifest loader and config auto-discovery.\n *\n * Language manifest loader and config auto-discovery.\n *\n * Reads manifest.json to determine which languages have\n * which config types, then loads them via a static\n * registry of import() calls (string literals for\n * bundler compatibility).\n */\n\n// ── Types ────────────────────────────────────────────\n\ntype ManifestLanguage = {\n triggers?: boolean;\n coreference?: boolean;\n legalRoleHeads?: boolean;\n};\n\ntype Manifest = {\n languages: Record<string, ManifestLanguage>;\n};\n\ntype ConfigType = \"triggers\" | \"coreference\" | \"legalRoleHeads\";\n\n// ── Manifest loader (cached) ─────────────────────────\n\nlet _manifest: Manifest | null = null;\nlet _manifestPromise: Promise<Manifest> | null = null;\n\nconst loadManifest = (): Promise<Manifest> => {\n if (_manifest) {\n return Promise.resolve(_manifest);\n }\n if (_manifestPromise) {\n return _manifestPromise;\n }\n _manifestPromise = (async () => {\n try {\n const mod = await import(\"../data/manifest.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON manifest\n const parsed = (mod.default ?? mod) as Manifest;\n if (\n !parsed ||\n typeof parsed.languages !== \"object\" ||\n parsed.languages === null ||\n Array.isArray(parsed.languages)\n ) {\n console.warn(\n \"[anonymize] lang-loader: manifest has \" +\n \"unexpected structure, falling back \" +\n \"to hardcoded list\",\n );\n _manifest = { languages: {} };\n return _manifest;\n }\n _manifest = parsed;\n return _manifest;\n } catch (err) {\n // Manifest not available (old data package version\n // or unexpected import error). Cache the empty\n // result so we don't retry the failed import.\n console.warn(\n \"[anonymize] lang-loader: manifest not \" +\n \"available, falling back to hardcoded \" +\n \"language list:\",\n err,\n );\n _manifest = { languages: {} };\n return _manifest;\n }\n })();\n return _manifestPromise;\n};\n\n// ── Static import registries ─────────────────────────\n// String literals so bundlers can statically analyze\n// the import paths. Each registry maps language code\n// to a lazy loader thunk.\n\nconst TRIGGER_LOADERS: Record<string, () => Promise<unknown>> = {\n cs: () => import(\"../data/triggers.cs.json\"),\n de: () => import(\"../data/triggers.de.json\"),\n en: () => import(\"../data/triggers.en.json\"),\n es: () => import(\"../data/triggers.es.json\"),\n fr: () => import(\"../data/triggers.fr.json\"),\n hu: () => import(\"../data/triggers.hu.json\"),\n it: () => import(\"../data/triggers.it.json\"),\n pl: () => import(\"../data/triggers.pl.json\"),\n \"pt-br\": () => import(\"../data/triggers.pt-br.json\"),\n ro: () => import(\"../data/triggers.ro.json\"),\n sk: () => import(\"../data/triggers.sk.json\"),\n sv: () => import(\"../data/triggers.sv.json\"),\n};\n\nconst COREFERENCE_LOADERS: Record<string, () => Promise<unknown>> = {\n cs: () => import(\"../data/coreference.cs.json\"),\n de: () => import(\"../data/coreference.de.json\"),\n en: () => import(\"../data/coreference.en.json\"),\n es: () => import(\"../data/coreference.es.json\"),\n fr: () => import(\"../data/coreference.fr.json\"),\n it: () => import(\"../data/coreference.it.json\"),\n pl: () => import(\"../data/coreference.pl.json\"),\n \"pt-br\": () => import(\"../data/coreference.pt-br.json\"),\n sk: () => import(\"../data/coreference.sk.json\"),\n};\n\nconst LEGAL_ROLE_HEAD_LOADERS: Record<string, () => Promise<unknown>> = {\n cs: () => import(\"../data/legal-role-heads.cs.json\"),\n de: () => import(\"../data/legal-role-heads.de.json\"),\n en: () => import(\"../data/legal-role-heads.en.json\"),\n es: () => import(\"../data/legal-role-heads.es.json\"),\n fr: () => import(\"../data/legal-role-heads.fr.json\"),\n it: () => import(\"../data/legal-role-heads.it.json\"),\n pl: () => import(\"../data/legal-role-heads.pl.json\"),\n \"pt-br\": () => import(\"../data/legal-role-heads.pt-br.json\"),\n sk: () => import(\"../data/legal-role-heads.sk.json\"),\n};\n\nconst LOADER_REGISTRIES: Record<\n ConfigType,\n Record<string, () => Promise<unknown>>\n> = {\n triggers: TRIGGER_LOADERS,\n coreference: COREFERENCE_LOADERS,\n legalRoleHeads: LEGAL_ROLE_HEAD_LOADERS,\n};\n\n// ── Fallback language lists ──────────────────────────\n// Used when the manifest is unavailable (old data\n// package). Matches the hardcoded lists that existed\n// before the manifest was introduced.\n\nconst FALLBACK_LANGUAGES: Record<ConfigType, readonly string[]> = {\n triggers: [\n \"cs\",\n \"de\",\n \"en\",\n \"es\",\n \"fr\",\n \"hu\",\n \"it\",\n \"pl\",\n \"pt-br\",\n \"ro\",\n \"sk\",\n \"sv\",\n ],\n coreference: [\"cs\", \"de\", \"en\", \"es\", \"fr\", \"it\", \"pl\", \"pt-br\", \"sk\"],\n legalRoleHeads: [\"cs\", \"de\", \"en\", \"es\", \"fr\", \"it\", \"pl\", \"pt-br\", \"sk\"],\n};\n\n// ── Public API ───────────────────────────────────────\n\n/**\n * Load all config files of a given type for all\n * languages enabled in the manifest.\n *\n * Falls back to the hardcoded language list when the\n * manifest is unavailable (backward compatibility).\n */\nexport const loadLanguageConfigs = async <T extends NonNullable<unknown>>(\n configType: ConfigType,\n mapFn: (mod: unknown) => T,\n): Promise<T[]> => {\n const manifest = await loadManifest();\n const registry = LOADER_REGISTRIES[configType];\n\n // Determine which language codes to load\n const hasManifestLanguages = Object.keys(manifest.languages).length > 0;\n\n const codes = hasManifestLanguages\n ? Object.entries(manifest.languages)\n .filter(([code, lang]) => {\n if (!lang || typeof lang !== \"object\") {\n console.warn(\n `[anonymize] lang-loader: manifest ` +\n `entry for \"${code}\" is not an ` +\n `object, skipping`,\n );\n return false;\n }\n return lang[configType] === true;\n })\n .map(([code]) => code)\n : [...FALLBACK_LANGUAGES[configType]];\n\n // Use indexed assignment so results preserve\n // manifest declaration order regardless of import\n // resolution timing.\n const results: (T | undefined)[] = codes.map(() => undefined);\n\n const loads = codes.map(async (code, i) => {\n const loader = registry[code];\n if (!loader) {\n console.warn(\n `[anonymize] lang-loader: language \"${code}\" ` +\n `is enabled in the manifest for ` +\n `\"${configType}\" but has no loader in ` +\n `the static registry`,\n );\n return;\n }\n let mod: unknown;\n try {\n mod = await loader();\n } catch (err) {\n console.warn(\n `[anonymize] lang-loader: failed to ` +\n `import \"${configType}\" config for ` +\n `\"${code}\":`,\n err,\n );\n return;\n }\n let result: T;\n try {\n result = mapFn(mod);\n } catch (err) {\n console.warn(\n `[anonymize] lang-loader: mapFn failed ` +\n `for \"${code}\" (${configType}):`,\n err,\n );\n return;\n }\n results[i] = result;\n });\n\n await Promise.all(loads);\n return results.filter((r): r is T => r !== undefined);\n};\n\n/**\n * Reset cached manifest. Exposed for testing only.\n */\nexport const _resetManifestCache = (): void => {\n _manifest = null;\n _manifestPromise = null;\n};\n","/**\n * Known legal form suffixes. Shared between trigger\n * detection (reclassification), org-propagation (suffix\n * stripping), and the trailing-period strip in\n * sanitizeEntities. Add new entries in any order — the\n * export is sorted longest-first at module load so\n * consumers performing `endsWith` / regex-alternation\n * lookups always hit the most specific suffix\n * (\"Pty Ltd.\" before \"Pty Ltd\", \"spol. s r.o.\" before\n * \"s.r.o.\").\n */\nconst RAW_LEGAL_SUFFIXES = [\n // Czech\n \"spol. s r.o.\",\n \"s.r.o.\",\n \"s. r. o.\",\n \"a.s.\",\n \"a. s.\",\n \"v.o.s.\",\n \"v. o. s.\",\n \"k.s.\",\n \"k. s.\",\n \"z.s.\",\n \"z. s.\",\n \"z.ú.\",\n \"z. ú.\",\n \"o.p.s.\",\n \"o. p. s.\",\n \"s.p.\",\n \"s. p.\",\n // German / Austrian / Swiss\n \"GmbH\",\n \"AG\",\n \"SE\",\n \"KG\",\n \"OHG\",\n // English (UK/US/AU/IE). Title-case and ALL-CAPS\n // spellings both appear in real filings (party\n // captions and signature blocks render in caps);\n // the regex matches case-sensitively, so both\n // spellings need explicit entries.\n \"Ltd.\",\n \"Ltd\",\n \"LTD.\",\n \"LTD\",\n \"LLC\",\n \"LLP\",\n \"Inc.\",\n \"INC.\",\n \"Inc\",\n \"INC\",\n \"Corp.\",\n \"CORP.\",\n \"Corp\",\n \"CORP\",\n \"Corporation\",\n \"CORPORATION\",\n \"Co.\",\n \"CO.\",\n \"LP\",\n \"L.P.\",\n \"PLC\",\n \"plc\",\n \"N.A.\",\n \"N.V.\",\n \"B.V.\",\n \"Pty Ltd.\",\n \"Pty Ltd\",\n \"PTY LTD.\",\n \"PTY LTD\",\n // French / Iberian / Italian\n \"S.A.\",\n \"SA\",\n \"SAS\",\n \"SARL\",\n \"S.p.A.\",\n // Polish\n \"Sp. z o.o.\",\n \"Sp. k.\",\n \"Sp. j.\",\n // Brazilian / Portuguese. Both title-case and\n // all-caps spellings appear in BR contracts; the\n // reclassification regex is case-sensitive so each\n // spelling needs an explicit entry.\n \"Ltda.\",\n \"LTDA.\",\n \"Ltda\",\n \"LTDA\",\n \"S/A\",\n \"EIRELI\",\n \"EPP\",\n \"ME\",\n \"MEI\",\n];\n\nexport const LEGAL_SUFFIXES: readonly string[] = [...RAW_LEGAL_SUFFIXES].sort(\n (a, b) => b.length - a.length,\n);\n","","/**\n * Centralized Unicode character equivalence groups.\n *\n * Centralized Unicode character equivalence groups.\n *\n * Provides helpers to build regex character classes that\n * match all typographic variants of a character type\n * (dashes, spaces, quotes, etc.).\n *\n * The JSON is statically imported so the bundler inlines\n * it, avoiding a runtime require() that breaks browsers.\n */\n\nimport charGroupsJson from \"../data/char-groups.json\";\n\ntype CharEntry = {\n char: string;\n name: string;\n code: string;\n};\n\ntype CharGroup = {\n description: string;\n chars: readonly CharEntry[];\n};\n\ntype CharGroupsConfig = {\n _comment: string;\n groups: Record<string, CharGroup>;\n};\n\n/** Chars that need escaping inside a regex char class. */\nconst REGEX_CLASS_SPECIAL = /[\\\\\\]^-]/;\n\nconst escapeForCharClass = (ch: string): string =>\n REGEX_CLASS_SPECIAL.test(ch) ? `\\\\${ch}` : ch;\n\n// SAFETY: JSON shape matches CharGroupsConfig by contract.\nconst config = charGroupsJson as CharGroupsConfig;\n\n/**\n * Get the raw characters for a named group.\n * Throws if the group does not exist.\n */\nexport const charSet = (group: string): readonly string[] => {\n const g = config.groups[group];\n if (!g) {\n throw new Error(`Unknown char group: \"${group}\"`);\n }\n return g.chars.map((entry) => entry.char);\n};\n\n/**\n * Build a regex character class string for a named\n * group. E.g., charClass(\"dash\") returns a string\n * like \"[-\\u2013\\u2014\\u2010\\u2011\\u2212\\u2043]\".\n *\n * The hyphen-minus is placed first so it is treated\n * as a literal, not a range indicator.\n */\nexport const charClass = (group: string): string => {\n const chars = charSet(group);\n // Place hyphen-minus first to avoid range issues.\n const sorted = [...chars].sort((a, b) => {\n if (a === \"-\") return -1;\n if (b === \"-\") return 1;\n return a.localeCompare(b);\n });\n const escaped = sorted.map(escapeForCharClass);\n return `[${escaped.join(\"\")}]`;\n};\n\n/**\n * Return the inner content of a character class (without\n * the surrounding brackets). Useful for embedding a group\n * inside a larger character class, e.g.:\n * `[\\\\s&,.${charClassInner(\"dash\")}]`\n */\nexport const charClassInner = (group: string): string => {\n const chars = charSet(group);\n const sorted = [...chars].sort((a, b) => {\n if (a === \"-\") return -1;\n if (b === \"-\") return 1;\n return a.localeCompare(b);\n });\n return sorted.map(escapeForCharClass).join(\"\");\n};\n\n/**\n * Build a regex alternation pattern for a named group.\n * Unlike charClass, this uses (?:a|b|c) syntax which\n * is useful when chars need individual escaping or\n * when embedding in complex patterns.\n */\nexport const charPattern = (group: string): string => {\n const chars = charSet(group);\n // SAFETY: length === 1 guarantees index 0 exists.\n if (chars.length === 1) return chars[0]!;\n const escaped = chars.map((ch) => ch.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"));\n return `(?:${escaped.join(\"|\")})`;\n};\n\n// ── Pre-built constants for common groups ──────\n\n/** Regex char class matching all dash variants. */\nexport const DASH = charClass(\"dash\");\n\n/**\n * Inner content of the dash char class (no brackets).\n * For embedding inside a larger character class:\n * `[\\\\s&,.${DASH_INNER}]`\n */\nexport const DASH_INNER = charClassInner(\"dash\");\n\n/** Regex char class matching all space variants. */\nexport const SPACE = charClass(\"space\");\n\n/** Regex char class matching all double-quote variants. */\nexport const QUOTE_DOUBLE = charClass(\"quote-double\");\n\n/** Inner content of the double-quote char class (no brackets). */\nexport const QUOTE_DOUBLE_INNER = charClassInner(\"quote-double\");\n\n/** Regex char class matching all single-quote variants. */\nexport const QUOTE_SINGLE = charClass(\"quote-single\");\n\n/** Inner content of the single-quote char class (no brackets). */\nexport const QUOTE_SINGLE_INNER = charClassInner(\"quote-single\");\n\n/**\n * Inline opening-bracket characters that frequently introduce\n * a defined-term parenthetical immediately after an address or\n * organization span (e.g. `…GA 30326, USA (the \"Premises\")`).\n * Hardcoded rather than data-driven because the set is tiny,\n * regex-special, and not language-dependent.\n */\nexport const OPENING_BRACKETS_INNER = \"(\\\\[{\";\n\n/** Regex char class matching all slash variants. */\nexport const SLASH = charClass(\"slash\");\n\n/** Regex char class matching all dot variants. */\nexport const DOT = charClass(\"dot\");\n\n/** Regex char class matching all colon variants. */\nexport const COLON = charClass(\"colon\");\n","/**\n * Legal form detection for company/organization names.\n *\n * Detects company names by finding legal form suffixes\n * (s.r.o., GmbH, a.s., etc.) and extending backwards\n * to capture preceding capitalised words.\n *\n * Exports pattern definitions for the unified builder\n * and a match processor for post-processing.\n */\n\nimport type { Match } from \"@stll/text-search\";\n\nimport { LEGAL_SUFFIXES } from \"../config/legal-forms\";\nimport { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\nimport { DASH_INNER } from \"../util/char-groups\";\nimport { loadLanguageConfigs } from \"../util/lang-loader\";\n\n// Verb-like tokens that signal sentence context: when one of\n// these appears between a role-head opening and the legal form,\n// the match is a swept sentence fragment, not an organisation\n// name. Names like \"Client solutions Inc.\" or \"Vendor consulting\n// Ltd.\" don't contain any of these, so they pass through the\n// trim untouched. Lowercased; matched case-insensitively.\n//\n// Sourced from `data/sentence-verb-indicators.json` (per-\n// language so verb morphology stays next to other per-language\n// data). Loaded lazily; the seed below covers the most common\n// indicators across cs/en/de so the sync accessor keeps working\n// before `warmSentenceVerbIndicators()` resolves.\nconst SENTENCE_VERB_INDICATORS_SEED: ReadonlySet<string> = new Set([\n \"je\",\n \"jsou\",\n \"is\",\n \"are\",\n \"ist\",\n \"sind\",\n]);\n\nlet sentenceVerbIndicatorsCache: ReadonlySet<string> | null = null;\nlet sentenceVerbIndicatorsPromise: Promise<ReadonlySet<string>> | null = null;\n\nconst loadSentenceVerbIndicators = async (): Promise<ReadonlySet<string>> => {\n if (sentenceVerbIndicatorsCache) return sentenceVerbIndicatorsCache;\n if (sentenceVerbIndicatorsPromise) return sentenceVerbIndicatorsPromise;\n sentenceVerbIndicatorsPromise = (async () => {\n let data: Record<string, unknown> = {};\n try {\n const mod = await import(\"../data/sentence-verb-indicators.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON module shape\n const parsed =\n (mod as { default?: Record<string, unknown> }).default ?? mod;\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON module shape\n data = parsed as Record<string, unknown>;\n } catch (err) {\n console.warn(\n \"[anonymize] legal-forms: failed to load \" +\n \"sentence-verb-indicators.json, falling back \" +\n \"to seed list:\",\n err,\n );\n }\n const all = new Set<string>(SENTENCE_VERB_INDICATORS_SEED);\n for (const [key, value] of Object.entries(data)) {\n if (key.startsWith(\"_\")) continue;\n if (!Array.isArray(value)) continue;\n for (const verb of value) {\n if (typeof verb !== \"string\" || verb.length === 0) continue;\n all.add(verb.toLowerCase());\n }\n }\n sentenceVerbIndicatorsCache = all;\n return all;\n })();\n return sentenceVerbIndicatorsPromise;\n};\n\nexport const getSentenceVerbIndicatorsSync = (): ReadonlySet<string> =>\n sentenceVerbIndicatorsCache ?? SENTENCE_VERB_INDICATORS_SEED;\n\n// Horizontal whitespace as understood by DOCX text extraction.\n// `regex`/TextSearch does not treat NBSP variants as `\\s`, but\n// company names often contain them between words and legal forms.\nconst HSPACE = \"(?:[^\\\\S\\\\n]|[  ])\";\nconst UPPER = \"A-ZÁČĎÉĚÍŇÓŘŠŤÚŮÝŽÄÖÜÀÂÆÇÈÊËÎÏÔÙÛŸÑĄĆĘŁŃŚŹŻ\\\\u0130\";\nconst LEGAL_LIST_BOUNDARY_RE = new RegExp(\n `^[,;]${HSPACE}+(?=\\\\p{Lu}|(?:\\\\p{Lu}\\\\.${HSPACE}?){2,})`,\n \"u\",\n);\n\nconst ROMAN_NUMERAL_RE =\n /^(?=[IVXLCDM])M{0,3}(?:CM|CD|D?C{0,3})(?:XC|XL|L?X{0,3})(?:IX|IV|V?I{0,3})$/;\n\ntype LeadingClauseTrimConfig = {\n phrases?: readonly string[];\n directPrefixes?: readonly string[];\n};\n\ntype LeadingClauseTrims = {\n phrases: readonly string[];\n directPrefixes: readonly string[];\n};\n\nconst EMPTY_LEADING_CLAUSE_TRIMS: LeadingClauseTrims = {\n phrases: [],\n directPrefixes: [],\n};\n\nlet leadingClauseTrimsCache: LeadingClauseTrims | null = null;\nlet leadingClauseTrimsPromise: Promise<LeadingClauseTrims> | null = null;\n\nconst loadLeadingClauseTrims = async (): Promise<LeadingClauseTrims> => {\n if (leadingClauseTrimsCache) return leadingClauseTrimsCache;\n if (leadingClauseTrimsPromise) return leadingClauseTrimsPromise;\n leadingClauseTrimsPromise = (async () => {\n let data: Record<string, unknown> = {};\n try {\n const mod = await import(\"../data/legal-form-leading-clauses.json\");\n const parsed =\n (mod as { default?: Record<string, unknown> }).default ?? mod;\n data = parsed as Record<string, unknown>;\n } catch (err) {\n console.warn(\n \"[anonymize] legal-forms: failed to load \" +\n \"legal-form-leading-clauses.json:\",\n err,\n );\n }\n\n const phrases = new Set<string>();\n const directPrefixes = new Set<string>();\n for (const [key, value] of Object.entries(data)) {\n if (key.startsWith(\"_\") || typeof value !== \"object\" || value === null) {\n continue;\n }\n const config = value as LeadingClauseTrimConfig;\n for (const phrase of config.phrases ?? []) {\n if (typeof phrase === \"string\" && phrase.length > 0) {\n phrases.add(phrase);\n }\n }\n for (const prefix of config.directPrefixes ?? []) {\n if (typeof prefix === \"string\" && prefix.length > 0) {\n directPrefixes.add(prefix);\n }\n }\n }\n\n const result = {\n phrases: [...phrases],\n directPrefixes: [...directPrefixes],\n };\n leadingClauseTrimsCache = result;\n return result;\n })();\n return leadingClauseTrimsPromise;\n};\n\nconst getLeadingClauseTrimsSync = (): LeadingClauseTrims =>\n leadingClauseTrimsCache ?? EMPTY_LEADING_CLAUSE_TRIMS;\n\n// Generic legal/contract role words that should never appear\n// at the head of an organisation name. When a greedy regex\n// sweep includes one of these as the first word, the span is\n// a sentence fragment, not a real company (e.g. \"Vendor 1\n// owns an equity interest in the Acme s.r.o. company\"). The\n// processor trims back to the last real Cap-starting word in\n// that case. Per-language word lists live under\n// `data/legal-role-heads.<lang>.json`; loaded lazily and\n// cached on first use.\ntype LegalRoleHeadsConfig = {\n words: readonly string[];\n};\n\nlet legalRoleHeadsCache: ReadonlySet<string> | null = null;\nlet legalRoleHeadsPromise: Promise<ReadonlySet<string>> | null = null;\n\nconst loadLegalRoleHeads = async (): Promise<ReadonlySet<string>> => {\n if (legalRoleHeadsCache) return legalRoleHeadsCache;\n if (legalRoleHeadsPromise) return legalRoleHeadsPromise;\n legalRoleHeadsPromise = (async () => {\n const sets = await loadLanguageConfigs<LegalRoleHeadsConfig>(\n \"legalRoleHeads\",\n (mod) => {\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config shape\n const m = mod as {\n default?: LegalRoleHeadsConfig;\n };\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config shape\n return (m.default ?? mod) as LegalRoleHeadsConfig;\n },\n );\n const all = new Set<string>();\n for (const entry of sets) {\n if (!entry || !Array.isArray(entry.words)) continue;\n for (const word of entry.words) {\n if (typeof word === \"string\" && word.length > 0) {\n all.add(word.toLowerCase());\n }\n }\n }\n legalRoleHeadsCache = all;\n return all;\n })();\n return legalRoleHeadsPromise;\n};\n\n// Synchronous helper used inside `processLegalFormMatches`,\n// which is a sync function called once per pipeline run. The\n// pipeline calls `warmLegalRoleHeads()` before invoking it, so\n// the cache is populated by the time matches are processed.\nexport const getLegalRoleHeadsSync = (): ReadonlySet<string> =>\n legalRoleHeadsCache ?? new Set<string>();\n\nexport const warmLegalRoleHeads = async (): Promise<void> => {\n await Promise.all([\n loadLegalRoleHeads(),\n loadAllLegalSuffixes(),\n loadSentenceVerbIndicators(),\n loadClauseNounHeads(),\n loadConnectorProseHeads(),\n loadStructuralSingleCapPrefixes(),\n loadLeadingClauseTrims(),\n ]);\n};\n\n// Suffix anchoring during the role-head trim needs the FULL\n// legal-form vocabulary (not just the small `LEGAL_SUFFIXES`\n// propagation list). \"Vendor owns Acme Corp.\" has to anchor on\n// \"Corp.\" but `LEGAL_SUFFIXES` is Czech-leaning; load the same\n// JSON the pattern builder uses and flatten it once.\nlet allLegalSuffixesCache: readonly string[] | null = null;\nlet allLegalSuffixesPromise: Promise<readonly string[]> | null = null;\nlet normalizedLegalBoundarySuffixesCache: ReadonlySet<string> | null = null;\nlet normalizedInNameLegalFormWordsCache: ReadonlySet<string> | null = null;\n\nconst normalizeLegalSuffixToken = (suffix: string): string =>\n suffix.replace(/[.,\\s]/g, \"\");\n\nconst isBoundaryLegalSuffixForm = (form: string): boolean => {\n const normalized = normalizeLegalSuffixToken(form);\n if (normalized.length === 0) {\n return false;\n }\n if (LEGAL_SUFFIXES.includes(form)) {\n return true;\n }\n return /[.]/u.test(form) || normalized === normalized.toUpperCase();\n};\n\nconst loadAllLegalSuffixes = async (): Promise<readonly string[]> => {\n if (allLegalSuffixesCache) return allLegalSuffixesCache;\n if (allLegalSuffixesPromise) return allLegalSuffixesPromise;\n allLegalSuffixesPromise = (async () => {\n let data: Record<string, string[]>;\n try {\n const mod = await import(\"../data/legal-forms.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON module shape\n data = (mod as { default: Record<string, string[]> }).default;\n } catch {\n data = {};\n }\n const seen = new Set<string>();\n const out: string[] = [];\n for (const list of Object.values(data)) {\n for (const form of list) {\n if (typeof form !== \"string\" || form.length === 0) continue;\n if (seen.has(form)) continue;\n seen.add(form);\n out.push(form);\n }\n }\n for (const form of LEGAL_SUFFIXES) {\n if (!seen.has(form)) {\n seen.add(form);\n out.push(form);\n }\n }\n // Sort longest-first so multi-token suffixes like\n // \"spol. s r.o.\" anchor before nested shorter forms.\n out.sort((a, b) => b.length - a.length);\n allLegalSuffixesCache = out;\n normalizedLegalBoundarySuffixesCache = new Set(\n out\n .filter(isBoundaryLegalSuffixForm)\n .map(normalizeLegalSuffixToken)\n .filter((suffix) => suffix.length > 0),\n );\n normalizedInNameLegalFormWordsCache = new Set(\n out\n .filter((form) => !isBoundaryLegalSuffixForm(form) && !/\\s/u.test(form))\n .map(normalizeLegalSuffixToken)\n .filter((suffix) => suffix.length > 0),\n );\n return out;\n })();\n return allLegalSuffixesPromise;\n};\n\nconst getAllLegalSuffixesSync = (): readonly string[] =>\n allLegalSuffixesCache ?? LEGAL_SUFFIXES;\n\nconst getNormalizedLegalBoundarySuffixesSync = (): ReadonlySet<string> =>\n normalizedLegalBoundarySuffixesCache ??\n new Set(\n LEGAL_SUFFIXES.map(normalizeLegalSuffixToken).filter(\n (suffix) => suffix.length > 0,\n ),\n );\n\nconst getNormalizedInNameLegalFormWordsSync = (): ReadonlySet<string> =>\n normalizedInNameLegalFormWordsCache ?? new Set<string>();\n\n/**\n * Sync accessor for the full legal-form vocabulary\n * (`data/legal-forms.json` plus `LEGAL_SUFFIXES`,\n * longest-first). Falls back to `LEGAL_SUFFIXES` when\n * `warmLegalRoleHeads()` has not run yet. Exposed so the\n * trailing-period strip in `sanitizeEntities` can keep\n * pace with the detector vocabulary rather than only the\n * smaller `LEGAL_SUFFIXES` propagation list.\n */\nexport const getKnownLegalSuffixes = getAllLegalSuffixesSync;\n\n// Common contract clause nouns that appear in legal prose\n// between a sentence-verb and the company name. When the trim\n// scans forward for the org's first Cap word, these are skipped\n// like role-heads so we don't anchor on \"Agreement\" / \"License\"\n// in patterns such as \"Vendor signed Agreement with Acme Inc.\".\n//\n// Sourced from `data/clause-noun-heads.json` (per-language so\n// vocabulary stays next to other per-language data). Loaded\n// lazily; `warmLegalRoleHeads()` resolves the cache before\n// `processLegalFormMatches` runs.\nconst CLAUSE_NOUN_HEADS_SEED: ReadonlySet<string> = new Set([\n \"agreement\",\n \"contract\",\n]);\n\nlet clauseNounHeadsCache: ReadonlySet<string> | null = null;\nlet clauseNounHeadsPromise: Promise<ReadonlySet<string>> | null = null;\n\nconst loadClauseNounHeads = async (): Promise<ReadonlySet<string>> => {\n if (clauseNounHeadsCache) return clauseNounHeadsCache;\n if (clauseNounHeadsPromise) return clauseNounHeadsPromise;\n clauseNounHeadsPromise = (async () => {\n let data: Record<string, unknown> = {};\n try {\n const mod = await import(\"../data/clause-noun-heads.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON module shape\n const parsed =\n (mod as { default?: Record<string, unknown> }).default ?? mod;\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON module shape\n data = parsed as Record<string, unknown>;\n } catch (err) {\n console.warn(\n \"[anonymize] legal-forms: failed to load \" +\n \"clause-noun-heads.json, falling back to seed list:\",\n err,\n );\n }\n const all = new Set<string>(CLAUSE_NOUN_HEADS_SEED);\n for (const [key, value] of Object.entries(data)) {\n if (key.startsWith(\"_\")) continue;\n if (!Array.isArray(value)) continue;\n for (const noun of value) {\n if (typeof noun !== \"string\" || noun.length === 0) continue;\n all.add(noun.toLowerCase());\n }\n }\n clauseNounHeadsCache = all;\n return all;\n })();\n return clauseNounHeadsPromise;\n};\n\nexport const getClauseNounHeadsSync = (): ReadonlySet<string> =>\n clauseNounHeadsCache ?? CLAUSE_NOUN_HEADS_SEED;\n\nlet connectorProseHeadsCache: ReadonlySet<string> | null = null;\nlet connectorProseHeadsPromise: Promise<ReadonlySet<string>> | null = null;\n\nconst loadConnectorProseHeads = async (): Promise<ReadonlySet<string>> => {\n if (connectorProseHeadsCache) {\n return connectorProseHeadsCache;\n }\n if (connectorProseHeadsPromise) {\n return connectorProseHeadsPromise;\n }\n\n connectorProseHeadsPromise = (async () => {\n let data: { roles?: unknown } = {};\n try {\n const mod = await import(\"../data/generic-roles.json\");\n const parsed = (mod as { default?: { roles?: unknown } }).default ?? mod;\n data = parsed as { roles?: unknown };\n } catch (err) {\n console.warn(\n \"[anonymize] legal-forms: failed to load generic-roles.json:\",\n err,\n );\n }\n\n const all = new Set<string>();\n if (Array.isArray(data.roles)) {\n for (const role of data.roles) {\n if (typeof role === \"string\" && role.length > 0) {\n all.add(role.toLowerCase());\n }\n }\n }\n\n connectorProseHeadsCache = all;\n return all;\n })();\n\n return connectorProseHeadsPromise;\n};\n\nconst getConnectorProseHeadsSync = (): ReadonlySet<string> =>\n connectorProseHeadsCache ?? new Set<string>();\n\nlet structuralSingleCapPrefixesCache: ReadonlySet<string> | null = null;\nlet structuralSingleCapPrefixesPromise: Promise<ReadonlySet<string>> | null =\n null;\n\nconst loadStructuralSingleCapPrefixes = async (): Promise<\n ReadonlySet<string>\n> => {\n if (structuralSingleCapPrefixesCache) {\n return structuralSingleCapPrefixesCache;\n }\n if (structuralSingleCapPrefixesPromise) {\n return structuralSingleCapPrefixesPromise;\n }\n\n structuralSingleCapPrefixesPromise = (async () => {\n let data: Record<string, unknown> = {};\n try {\n const mod = await import(\"../data/structural-single-cap-prefixes.json\");\n const parsed =\n (mod as { default?: Record<string, unknown> }).default ?? mod;\n data = parsed as Record<string, unknown>;\n } catch (err) {\n console.warn(\n \"[anonymize] legal-forms: failed to load \" +\n \"structural-single-cap-prefixes.json:\",\n err,\n );\n }\n\n const all = new Set<string>();\n for (const [key, value] of Object.entries(data)) {\n if (key.startsWith(\"_\")) {\n continue;\n }\n if (!Array.isArray(value)) {\n continue;\n }\n for (const prefix of value) {\n if (typeof prefix !== \"string\" || prefix.length === 0) {\n continue;\n }\n all.add(prefix.toLowerCase());\n }\n }\n\n structuralSingleCapPrefixesCache = all;\n return all;\n })();\n\n return structuralSingleCapPrefixesPromise;\n};\n\nconst getStructuralSingleCapPrefixesSync = (): ReadonlySet<string> =>\n structuralSingleCapPrefixesCache ?? new Set<string>();\n\n// Used by the trim helpers below to escape literal suffix tokens\n// before injecting them into runtime-built regexes.\nconst escapeForRegex = (form: string): string =>\n form\n .replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\s+/g, `${HSPACE}+`)\n .replace(/\\\\\\./g, `\\\\.${HSPACE}?`);\n\n// ── Pattern builder for unified search ──────────────\n\n/**\n * Build legal form regex pattern strings.\n * Returns an array of regex strings for the unified\n * TextSearch builder. Empty if data package is not\n * installed.\n */\nexport const buildLegalFormPatterns = async (): Promise<string[]> => {\n // The legal-form detector was rewritten to the candidate +\n // validator architecture in `legal-forms-v2.ts`. The unified\n // search no longer carries any legal-form regex patterns —\n // suffix discovery now goes through an Aho-Corasick literal\n // pass and the rough span around each hit is handed to the\n // existing `processLegalFormMatches` validator chain.\n // Returning an empty list keeps the unified-search builder\n // happy without compiling the ~7 KB monolithic regex that\n // tripped the upstream text-search DFA on long preambles.\n return [];\n};\n\n// ── Backward extension ──────────────────────────────\n\nconst CONNECTOR_RE = /^(?:a|and|und|et|e|y|i|&)$/i;\n// Multi-char \"and\"-type connectors. When backward\n// extension hits one of these with exactly two\n// uppercase words behind it, the pattern looks like\n// \"<First> <Last> and <ORG>\" and we stop rather than\n// swallow the personal name into the org span.\nconst AND_TYPE_CONNECTOR_RE = /^(?:and|und|et)$/i;\nconst UPPER_LETTER_RE = /^\\p{Lu}/u;\n// Capitalised words that, when they begin a legal-form\n// match, signal the match is the tail of a multi-word\n// organisation name (\"Acme Widgets and Company, Inc.\",\n// \"The Bank of America and Trust Company, Inc.\").\n// In that mode the two-cap-words \"First Last and ORG\"\n// heuristic is suspended and a small set of in-name\n// prepositions (\"of\") are crossable during backward\n// extension. Capitalised-form only — lowercase \"trust\"\n// or \"bank\" are common verbs/nouns.\nconst COMPANY_SUFFIX_WORDS_RE =\n /^(?:Company|Co|Bank|Brothers|Bros|Sons|Group|Holdings|Trust|Partners|Associates|Corporation|Industries|Enterprises|Solutions|Systems|Services|Foundation|Institute)$/;\nconst IN_NAME_PREPOSITION_RE = /^(?:of|the)$/i;\nconst ENTITY_HEAD_WORD_RE = /^[\\p{L}\\p{M}&]+/u;\nconst BARE_SINGLE_CAP_LEGAL_FORM_RE = new RegExp(\n `^[${UPPER}](?:${HSPACE}+|,${HSPACE}*)`,\n \"u\",\n);\nconst STRUCTURAL_SINGLE_CAP_RE = new RegExp(\n `^([\\\\p{L}\\\\p{M}]+)${HSPACE}+[${UPPER}](?:[.${DASH_INNER}]?\\\\d{1,3})?(?:${HSPACE}+|,${HSPACE}*)`,\n \"u\",\n);\nconst isStructuralSingleCapMatch = (text: string): boolean => {\n const first = STRUCTURAL_SINGLE_CAP_RE.exec(text)?.[1];\n return (\n first !== undefined &&\n getStructuralSingleCapPrefixesSync().has(first.toLowerCase())\n );\n};\n\nconst findLastSuffixSeparator = (text: string): number =>\n Math.max(\n text.lastIndexOf(\" \"),\n text.lastIndexOf(\"\\t\"),\n text.lastIndexOf(\" \"),\n text.lastIndexOf(\" \"),\n text.lastIndexOf(\",\"),\n );\n\nconst stripDocxSpaces = (text: string): string => text.replace(/[  ]/g, \"\");\n\n/**\n * Find the word ending just before `pos` in `text`,\n * skipping any whitespace (not newlines).\n * Returns null if no word is found (e.g., at start\n * of text, or preceded by non-word chars like \".\").\n */\nconst findWordBefore = (\n text: string,\n pos: number,\n): { word: string; start: number } | null => {\n let scan = pos - 1;\n // Skip horizontal whitespace\n while (scan >= 0) {\n const ch = text.charAt(scan);\n if (ch === \"\\n\" || !/\\s/.test(ch)) break;\n scan--;\n }\n if (scan < 0 || text.charAt(scan) === \"\\n\") {\n return null;\n }\n\n const wordEnd = scan + 1;\n while (scan >= 0 && /[\\p{L}\\p{M}&]/u.test(text.charAt(scan))) {\n scan--;\n }\n const wordStart = scan + 1;\n const word = text.slice(wordStart, wordEnd);\n if (word.length === 0) return null;\n return { word, start: wordStart };\n};\n\nconst hasSingleCapPrefixBefore = (\n fullText: string,\n matchStart: number,\n): boolean => {\n const prev = findWordBefore(fullText, matchStart);\n return (\n prev !== null && prev.word.length === 1 && UPPER_LETTER_RE.test(prev.word)\n );\n};\n\nconst isBareSingleCapStructuralInnerMatch = (\n fullText: string,\n matchStart: number,\n text: string,\n): boolean => {\n if (!BARE_SINGLE_CAP_LEGAL_FORM_RE.test(text)) {\n return false;\n }\n\n const prev = findWordBefore(fullText, matchStart);\n return (\n prev !== null &&\n getStructuralSingleCapPrefixesSync().has(prev.word.toLowerCase())\n );\n};\n\nconst trimEmbeddedLegalFormListPrefix = (\n entityStart: number,\n entityText: string,\n): { entityStart: number; entityText: string } => {\n let cut = -1;\n\n for (const suffix of getAllLegalSuffixesSync()) {\n const suffixClean = suffix.replace(/[.,\\s]/g, \"\");\n if (suffixClean.length > 0 && ROMAN_NUMERAL_RE.test(suffixClean)) {\n continue;\n }\n\n let fromIndex = 0;\n while (fromIndex < entityText.length) {\n const suffixStart = entityText.indexOf(suffix, fromIndex);\n if (suffixStart === -1) {\n break;\n }\n fromIndex = suffixStart + suffix.length;\n\n const suffixEnd = suffixStart + suffix.length;\n if (suffixEnd >= entityText.length - 1) {\n continue;\n }\n\n const afterSuffix = entityText.slice(suffixEnd);\n const boundary = /^,\\s+(?=\\p{Lu})/u.exec(afterSuffix);\n if (boundary === null) {\n continue;\n }\n\n const nextStart = suffixEnd + boundary[0].length;\n const remainder = entityText.slice(nextStart);\n if (!getAllLegalSuffixesSync().some((form) => remainder.endsWith(form))) {\n continue;\n }\n\n cut = Math.max(cut, nextStart);\n }\n }\n\n if (cut <= 0) {\n return { entityStart, entityText };\n }\n\n return {\n entityStart: entityStart + cut,\n entityText: entityText.slice(cut),\n };\n};\n\nconst splitEmbeddedLegalFormList = (\n entityStart: number,\n entityText: string,\n): { entityStart: number; entityText: string }[] => {\n const cuts = [0];\n\n for (const suffix of getAllLegalSuffixesSync()) {\n const suffixClean = suffix.replace(/[.,\\s]/g, \"\");\n if (suffixClean.length > 0 && ROMAN_NUMERAL_RE.test(suffixClean)) {\n continue;\n }\n\n let fromIndex = 0;\n while (fromIndex < entityText.length) {\n const suffixStart = entityText.indexOf(suffix, fromIndex);\n if (suffixStart === -1) {\n break;\n }\n fromIndex = suffixStart + suffix.length;\n\n const suffixEnd = suffixStart + suffix.length;\n if (suffixEnd >= entityText.length - 1) {\n continue;\n }\n\n const afterSuffix = entityText.slice(suffixEnd);\n const boundary = LEGAL_LIST_BOUNDARY_RE.exec(afterSuffix);\n if (boundary === null) {\n continue;\n }\n\n // Cut at every list boundary that immediately\n // follows a legal-form suffix. The left segment\n // is a complete org; the right segment is filtered\n // below by the per-segment suffix gate. Previously\n // we required the *right* side to end in a suffix\n // too, which let lists like \"Morgan Stanley & Co.\n // LLC, Bank of America\" be captured as one org\n // because the trailing \"Bank of America\" lacks a\n // suffix.\n const nextStart = suffixEnd + boundary[0].length;\n cuts.push(nextStart);\n }\n }\n\n const uniqueCuts = [...new Set(cuts)].toSorted((a, b) => a - b);\n if (uniqueCuts.length === 1) {\n return [{ entityStart, entityText }];\n }\n\n const segments: { entityStart: number; entityText: string }[] = [];\n for (let index = 0; index < uniqueCuts.length; index++) {\n const start = uniqueCuts[index];\n const end = uniqueCuts[index + 1] ?? entityText.length;\n if (start === undefined) {\n continue;\n }\n const segmentText = entityText.slice(start, end).replace(/[,\\s;]+$/u, \"\");\n if (segmentText.length === 0) {\n continue;\n }\n // Skip the segment unless it ends with a recognised\n // legal-form suffix. This drops the right-hand side\n // of a list-cut when only the left side carries a\n // suffix (see split note above), letting other\n // detectors claim the remainder if appropriate.\n const endsWithSuffix = getAllLegalSuffixesSync().some((form) =>\n segmentText.endsWith(form),\n );\n if (!endsWithSuffix) {\n continue;\n }\n segments.push({\n entityStart: entityStart + start,\n entityText: segmentText,\n });\n }\n\n return segments;\n};\n\nconst hasDisallowedLineBreak = (text: string): boolean => {\n for (const match of text.matchAll(/\\n/gu)) {\n const index = match.index;\n if (index === undefined) {\n continue;\n }\n const before = text.slice(0, index);\n const after = text.slice(index + 1);\n const dottedDesignatorBefore = /\\.[^\\S\\n]*$/u.test(before);\n const legalSuffixAfter =\n /^[^\\S\\n]*(?:\\p{Lu}\\.[^\\S\\n]?){1,}\\p{Lu}?\\.?$/u.test(after);\n const allCapsSuffixAfter = /^[^\\S\\n]*\\p{Lu}{2,}\\.?$/u.test(after);\n if (!dottedDesignatorBefore || (!legalSuffixAfter && !allCapsSuffixAfter)) {\n return true;\n }\n }\n return false;\n};\n\nconst hasMiddleInitialBefore = (fullText: string, pos: number): boolean => {\n const previousWord = findWordBefore(fullText, pos);\n if (!previousWord) {\n return false;\n }\n\n let scan = previousWord.start - 1;\n while (scan >= 0 && (fullText[scan] === \" \" || fullText[scan] === \"\\t\")) {\n scan--;\n }\n\n return (\n scan >= 1 &&\n fullText[scan] === \".\" &&\n UPPER_LETTER_RE.test(fullText[scan - 1] ?? \"\")\n );\n};\n\n/**\n * Count consecutive uppercase-starting words immediately\n * before `pos`. Stops at the first non-upper word or at\n * text/line start. Used to disambiguate sentence prose\n * (\"<First> <Last> and <ORG>\", \"<Defined-Term> and\n * <ORG>\") from multi-word organisation names that span\n * an \"and\" connector (\"UniCredit Bank Czech Republic and\n * Slovakia, a.s.\").\n *\n * When `crossInNamePreps` is true, the walk also steps\n * over in-name lowercase prepositions (\"of\", \"the\") as\n * long as they sit between two upper words. This lets\n * the suffix-mode \"and\"-crossing logic see through\n * \"<Trust ← and ← America ← of ← Bank>\" and emit one\n * full organisation span.\n */\nconst countUpperWordsBefore = (\n fullText: string,\n pos: number,\n crossInNamePreps = false,\n): number => {\n let count = 0;\n let scan = pos;\n while (scan > 0) {\n const found = findWordBefore(fullText, scan);\n if (found) {\n if (UPPER_LETTER_RE.test(found.word)) {\n count++;\n scan = found.start;\n continue;\n }\n if (crossInNamePreps && IN_NAME_PREPOSITION_RE.test(found.word)) {\n // Only cross the preposition when it sits between\n // two uppercase words — never when it sentence-\n // starts the phrase.\n const prev = findWordBefore(fullText, found.start);\n if (!prev) break;\n if (!UPPER_LETTER_RE.test(prev.word)) break;\n scan = found.start;\n continue;\n }\n break;\n }\n\n let p = scan - 1;\n while (p >= 0 && (fullText[p] === \" \" || fullText[p] === \"\\t\")) {\n p--;\n }\n if (\n p >= 1 &&\n fullText[p] === \".\" &&\n UPPER_LETTER_RE.test(fullText[p - 1] ?? \"\")\n ) {\n count++;\n scan = p - 1;\n continue;\n }\n break;\n }\n return count;\n};\n\n/**\n * True when `word` is a recognized legal-form suffix\n * (case-sensitive against the legal-forms vocabulary).\n * Used when deciding whether to cross an \"and\" connector\n * during backward extension — if the word immediately\n * preceding the connector is itself a legal-form suffix,\n * the \"and\" sits between two organisation names rather\n * than inside one (\"Morgan Securities LLC and Allen &\n * Company LLC\"), so the walk must stop there.\n */\nconst isKnownLegalFormSuffix = (word: string): boolean => {\n if (word.length === 0) {\n return false;\n }\n return getNormalizedLegalBoundarySuffixesSync().has(word);\n};\n\nconst isInNameLegalFormWord = (word: string): boolean => {\n if (word.length === 0) {\n return false;\n }\n return getNormalizedInNameLegalFormWordsSync().has(word);\n};\n\n/**\n * If `pos` is immediately preceded (modulo horizontal\n * whitespace) by an initial-dot run like `J.P.`, `U.S.`,\n * or `N.A.`, return the position where the initial run\n * starts. Otherwise return `pos` unchanged. The run must\n * be word-bounded on the left so we never absorb a stray\n * sentence-ending dot.\n */\nconst skipInitialsBackward = (fullText: string, pos: number): number => {\n // Skip horizontal whitespace only — initials must sit\n // on the same line as the rest of the org name.\n let scan = pos - 1;\n while (scan >= 0) {\n const ch = fullText.charAt(scan);\n if (ch === \"\\n\" || !/\\s/.test(ch)) break;\n scan--;\n }\n if (scan < 0 || fullText.charAt(scan) !== \".\") return pos;\n // Match one or more `<Upper>.` tokens at the right\n // edge of `fullText[0..scan+1]`. Allows optional\n // single horizontal space between tokens\n // (\"J. P. Morgan\" as well as \"J.P. Morgan\").\n const scanLimit = Math.max(0, scan + 1 - 100);\n const head = fullText.slice(scanLimit, scan + 1);\n const initialsRe = /(?:\\p{Lu}\\.[^\\S\\n]?){2,}$/u;\n const match = initialsRe.exec(head);\n if (match === null) return pos;\n const start = scanLimit + match.index;\n if (start > 0) {\n const prevCh = fullText.charAt(start - 1);\n if (/[\\p{L}\\p{M}\\p{N}]/u.test(prevCh)) return pos;\n }\n return start;\n};\n\n/**\n * Extend a match backward through uppercase words and\n * lowercase connectors. Stops at start of text,\n * newline, or a word that doesn't qualify.\n *\n * Connectors (a, and, und, et, ...) are only consumed\n * when there is a valid word before them — a trailing\n * connector at an entity boundary is not consumed.\n * For multi-char \"and\"-type connectors we additionally\n * refuse to cross when exactly two uppercase words\n * precede them (\"First Last and ORG, Inc.\" shape) —\n * unless the match itself begins with a known company-\n * suffix word (\"…and Company, Inc.\"), in which case\n * the chain belongs to one organisation. In that\n * suffix-mode we also cross in-name prepositions\n * (\"Bank of America and Trust Company, Inc.\").\n */\nconst extendBackward = (\n fullText: string,\n matchStart: number,\n options: { forceSuffixMode?: boolean } = {},\n): number => {\n // Read the first word of the match to decide whether\n // we're inside a multi-word organisation name. Callers\n // that enter the walk from a known legal-form suffix\n // (Inc., Ltd., etc.) can pass `forceSuffixMode: true`\n // to enable in-name preposition crossing (\"Bank of\n // America Inc.\") without having to widen\n // COMPANY_SUFFIX_WORDS_RE to every legal-form suffix.\n const headWord =\n ENTITY_HEAD_WORD_RE.exec(fullText.slice(matchStart))?.[0] ?? \"\";\n const suffixMode =\n options.forceSuffixMode === true || COMPANY_SUFFIX_WORDS_RE.test(headWord);\n\n let pos = matchStart;\n\n while (pos > 0) {\n const found = findWordBefore(fullText, pos);\n if (!found) break;\n\n const { word, start: wordStart } = found;\n\n const isUpper = UPPER_LETTER_RE.test(word);\n const isConnector = CONNECTOR_RE.test(word);\n const isInNamePrep = suffixMode && IN_NAME_PREPOSITION_RE.test(word);\n\n if (isUpper) {\n // Uppercase word — always accept\n pos = wordStart;\n } else if (isConnector) {\n if (AND_TYPE_CONNECTOR_RE.test(word)) {\n // Decide whether the \"and\" sits inside one\n // organisation name or between two sentence\n // tokens. Heuristics, applied in order:\n //\n // 1. If the word immediately before the \"and\"\n // is itself a known legal-form suffix\n // (\"Morgan Securities LLC and Allen &\n // Company LLC\", \"Apple, Inc. and Microsoft\n // Corp.\"), the \"and\" separates two orgs —\n // break regardless of mode.\n // 2. A single uppercase word before the \"and\"\n // is almost always a defined-term clause\n // noun (\"the Company and Barclays Bank\n // PLC\", \"Company and Bank of America,\n // N.A.\") rather than part of the org\n // name — break regardless of mode.\n // 3. Outside suffix mode, also break on two\n // upper words (typical person-name\n // pattern: \"Paul Newman and Apple, Inc.\").\n // Three or more upper words signals a\n // real multi-word organisation name\n // (\"UniCredit Bank Czech Republic and\n // Slovakia, a.s.\"), so the walk crosses.\n // 4. In suffix mode the regex already\n // captured a leading legal-form suffix\n // word (\"…and Company, Inc.\"), so any\n // multi-word prefix should flow through.\n const prev = findWordBefore(fullText, wordStart);\n if (!prev) break;\n if (!UPPER_LETTER_RE.test(prev.word)) break;\n if (isKnownLegalFormSuffix(prev.word)) break;\n const upperWordsBefore = countUpperWordsBefore(\n fullText,\n wordStart,\n suffixMode,\n );\n const middleInitialBefore = hasMiddleInitialBefore(fullText, wordStart);\n if (\n upperWordsBefore <= 1 &&\n (getClauseNounHeadsSync().has(prev.word.toLowerCase()) ||\n getConnectorProseHeadsSync().has(prev.word.toLowerCase()))\n ) {\n break;\n }\n const personNameBoundary = suffixMode\n ? middleInitialBefore &&\n hasSingleCapPrefixBefore(fullText, matchStart)\n : (upperWordsBefore === 2 && !isInNameLegalFormWord(prev.word)) ||\n middleInitialBefore;\n if (personNameBoundary) {\n break;\n }\n pos = prev.start;\n } else {\n // Non-\"and\" connector (`&`, `e`, `y`, `i`,\n // standalone `a`). Cross when there's a valid\n // uppercase-starting word before it.\n const prev = findWordBefore(fullText, wordStart);\n if (!prev) break;\n const prevIsUpper = UPPER_LETTER_RE.test(prev.word);\n if (!prevIsUpper) break;\n pos = prev.start;\n }\n } else if (isInNamePrep) {\n // In suffix-mode only: cross lowercase in-name\n // prepositions (\"of\", \"the\") when the preceding\n // token is uppercase (\"Bank of America\").\n const prev = findWordBefore(fullText, wordStart);\n if (!prev) break;\n if (!UPPER_LETTER_RE.test(prev.word)) break;\n pos = prev.start;\n } else {\n break;\n }\n }\n\n // After the word walk finishes, absorb any leading\n // initial-dot run that the letter-based word scan\n // skipped over (\"J.P. Morgan Securities LLC\",\n // \"U.S. Bancorp Inc.\"). The walk above stops at the\n // dot because `findWordBefore` only consumes letters,\n // so without this step the entity start lands on the\n // first non-initial word.\n pos = skipInitialsBackward(fullText, pos);\n\n return pos;\n};\n\nconst trimLeadingClause = (text: string): { offset: number; text: string } => {\n let cut = -1;\n const trims = getLeadingClauseTrimsSync();\n const phraseAlternation = trims.phrases.map(escapeForRegex).join(\"|\");\n if (phraseAlternation.length > 0) {\n const phraseRe = new RegExp(\n `(?:^|\\\\s)(?:${phraseAlternation})${HSPACE}+`,\n \"giu\",\n );\n for (const match of text.matchAll(phraseRe)) {\n cut = Math.max(cut, match.index + match[0].length);\n }\n }\n\n const directPrefixAlternation = trims.directPrefixes\n .map(escapeForRegex)\n .join(\"|\");\n // \"among\" / \"amongst\" / \"between\" can legitimately\n // appear inside a title-case org name (\"Food For\n // Thought Among Friends LLC\", \"The Space In Between\n // LLC\"). For those prefixes we require a clause\n // separator (comma) immediately before the prefix,\n // which is the structural signal that distinguishes\n // a connector (\"Investment Agreement, dated as of\n // March 9, 2020, among Twitter, Inc.\") from a name\n // component.\n const COMMA_GATED_DIRECT_PREFIXES: ReadonlySet<string> = new Set([\n \"among\",\n \"amongst\",\n \"between\",\n ]);\n const verbIndicators = getSentenceVerbIndicatorsSync();\n if (directPrefixAlternation.length > 0) {\n const directPrefixRe = new RegExp(\n `\\\\b(?:${directPrefixAlternation})${HSPACE}+(?=\\\\p{Lu})`,\n \"giu\",\n );\n for (const match of text.matchAll(directPrefixRe)) {\n const matchedPrefix = match[0].trim().toLowerCase();\n const before = text.slice(0, match.index);\n if (COMMA_GATED_DIRECT_PREFIXES.has(matchedPrefix)) {\n // The comma gate distinguishes the connector use\n // (\"Investment Agreement, dated as of …, among Twitter,\n // Inc.\") from an in-name word (\"Food For Thought Among\n // Friends LLC\"). Lift the gate when a lowercase sentence-verb\n // token appears in the preceding text (\"This Agreement is\n // entered into between Acme Inc.\") — the verb is the\n // structural cue that this is clause prose, not a name\n // component. Title-case \"Is\" / \"Between\" inside a company\n // name stays protected because only lowercase forms count.\n const hasComma = /,\\s*$/u.test(before);\n const beforeWords = before.match(/\\p{L}[\\p{L}\\p{M}'’]*/gu) ?? [];\n const hasSentenceVerb = beforeWords.some(\n (word) =>\n /^\\p{Ll}/u.test(word) && verbIndicators.has(word.toLowerCase()),\n );\n if (!hasComma && !hasSentenceVerb) {\n continue;\n }\n }\n const words = before.match(/\\p{L}[\\p{L}\\p{M}'’]*/gu) ?? [];\n const hasProsePrefix =\n words.length >= 3 && words.some((word) => /\\p{Ll}/u.test(word));\n if (hasProsePrefix) {\n cut = Math.max(cut, match.index + match[0].length);\n }\n }\n }\n for (const match of text.matchAll(/,/gu)) {\n const comma = match.index;\n if (comma === undefined) {\n continue;\n }\n const before = text.slice(0, comma);\n if (!/\\d/u.test(before)) {\n continue;\n }\n const after = text.slice(comma + 1);\n const leadingWs = after.match(/^\\s*/u)?.[0].length ?? 0;\n const candidate = after.slice(leadingWs);\n const upperWords = candidate.match(/\\p{Lu}[\\p{L}\\p{M}\\p{N}]*/gu) ?? [];\n if (upperWords.length >= 3) {\n cut = Math.max(cut, comma + 1 + leadingWs);\n }\n }\n\n if (cut <= 0) {\n return { offset: 0, text };\n }\n\n const trimmed = text.slice(cut);\n const leadingWs = trimmed.match(/^\\s*/u)?.[0].length ?? 0;\n\n return {\n offset: cut + leadingWs,\n text: trimmed.slice(leadingWs),\n };\n};\n\n// ── Match processor ─────────────────────────────────\n\n/**\n * Process legal form matches from the unified search.\n * Receives all matches; filters to the legal forms\n * slice via sliceStart/sliceEnd.\n *\n * The role-head trimming step reads per-language data from\n * a cache that `runPipeline` warms via `warmLegalRoleHeads()`\n * before calling this. Callers that invoke\n * `processLegalFormMatches` directly (without going through\n * `runPipeline`) must `await warmLegalRoleHeads()` first;\n * otherwise the trim falls back to a no-op and sentence-\n * fragment fixes do not apply.\n */\nexport const processLegalFormMatches = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText?: string,\n options: { suppressExtendBackward?: boolean } = {},\n): Entity[] => {\n const results: Entity[] = [];\n\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) {\n continue;\n }\n\n const text = match.text.trimEnd();\n if (text.length < 5) {\n continue;\n }\n\n // Trim spans whose first word is a generic legal/contract\n // role IF the match also contains a sentence-verb signal\n // (\"owns\", \"je vlastníkem\", \"grants\") between the role head\n // and the trailing legal-form suffix. Without that strong\n // signal we keep the match intact — role words are also\n // legitimate components of organisation names (\"Client\n // Solutions Inc.\", \"Client solutions Inc.\", \"Vendor s.r.o.\",\n // \"Vendor consulting Ltd.\"). When the signal is present\n // we slice the match at the first uppercase-starting word\n // that follows the last sentence-verb (and skip any role-\n // head word that lands at the new start), so multi-word\n // names (\"Acme Holdings s.r.o.\"), in-name prepositions\n // (\"Bank of America Inc.\"), lowercase-tail Czech state\n // forms (\"Národní agentura pro komunikační a informační\n // technologie, s. p.\"), and multi-token legal suffixes\n // (\"spol. s r.o.\") all survive the trim.\n const roleHeads = getLegalRoleHeadsSync();\n // Match the first token. Hyphenated forms (\"Sous-traitant\",\n // \"co-contractant\") are valid role heads in some languages,\n // so consume any internal `-letter` runs alongside the\n // letter-only head. The lookup uses the full hyphenated form\n // first, then falls back to just the leading letter run when\n // the role-head set lists only the bare prefix.\n const firstWordMatch = /^[\\p{L}\\p{M}]+(?:-[\\p{L}\\p{M}]+)*/u.exec(text);\n let processedStart = match.start;\n let processedText = text;\n if (\n isStructuralSingleCapMatch(processedText) ||\n (fullText !== undefined &&\n isBareSingleCapStructuralInnerMatch(fullText, match.start, text))\n ) {\n continue;\n }\n // True when the role-head trim slices the match. The\n // subsequent extendBackward step is suppressed in that case\n // — extending back would re-absorb the very prose the trim\n // just removed (e.g. \"Vendor grants Licensee Acme Inc.\" →\n // trim to \"Acme Inc.\" → extendBackward walks back across\n // \"Licensee\" again and emits \"Licensee Acme Inc.\").\n let trimmed = false;\n const firstWordText = firstWordMatch?.[0] ?? \"\";\n const firstWordLeading = /^[\\p{L}\\p{M}]+/u.exec(firstWordText)?.[0] ?? \"\";\n const isRoleHead =\n firstWordMatch !== null &&\n (roleHeads.has(firstWordText.toLowerCase()) ||\n (firstWordLeading.length > 0 &&\n roleHeads.has(firstWordLeading.toLowerCase())));\n if (isRoleHead) {\n // Find the legal-form suffix's position inside `text` by\n // scanning the full legal-form vocabulary (loaded from\n // `data/legal-forms.json` in `warmLegalRoleHeads`-style\n // fashion). Sorted longest-first so multi-token suffixes\n // (\"spol. s r.o.\", \"akciová společnost\") anchor before\n // shorter nested forms (\"s.r.o.\", \"společnost\").\n let suffixOffset = -1;\n for (const suffix of getAllLegalSuffixesSync()) {\n const suffixIdx = text.lastIndexOf(suffix);\n if (suffixIdx !== -1 && suffixIdx + suffix.length >= text.length - 1) {\n suffixOffset = suffixIdx;\n break;\n }\n }\n if (suffixOffset < 0) {\n // Couldn't locate the suffix; fall through without\n // trimming. The greedy regex will still produce the\n // match — better some highlight than none.\n } else {\n // Scan the middle (between the role-head and the legal-\n // form suffix) for a sentence-verb token. Position of\n // the LAST verb determines where the org name starts.\n const midStart = firstWordMatch[0].length;\n const midEnd = suffixOffset;\n const midSection = text.slice(midStart, midEnd);\n const verbIndicators = getSentenceVerbIndicatorsSync();\n let lastVerbEndInMid = -1;\n for (const wordMatch of midSection.matchAll(\n // Match any word (capital or lowercase start); the\n // verb-indicator set lookup is lowercased so e.g.\n // title-cased \"Owns\" in \"Vendor Owns Acme Inc.\"\n // still counts as a sentence verb.\n /(?<![\\p{L}\\p{N}])[\\p{L}\\p{M}]+/gu,\n )) {\n if (\n wordMatch[0] !== undefined &&\n wordMatch.index !== undefined &&\n verbIndicators.has(wordMatch[0].toLowerCase())\n ) {\n lastVerbEndInMid = wordMatch.index + wordMatch[0].length;\n }\n }\n // Also treat a digit immediately after the role-head\n // (\"Vendor 1\", \"Prodávající 2\") as a sentence signal.\n // Numbered party references rarely appear in company\n // names but always appear in clause text.\n const digitAfterRole = /^\\s+\\d+(?:\\.|\\b)/u.test(midSection);\n // Appositive role-head detection: when the legal-form\n // regex matched a span starting at a role-head (\"Licensee\n // Acme Inc.\") but there's no verb in the matched mid\n // section, look at the preceding word in fullText. If\n // that word is a sentence verb (\"Vendor grants Licensee\n // Acme Inc.\"), the role-head is appositive prose and\n // should be skipped just like an in-match role token.\n let appositiveRoleHead = false;\n if (!digitAfterRole && lastVerbEndInMid === -1 && fullText) {\n const before = fullText.slice(\n Math.max(0, match.start - 40),\n match.start,\n );\n const prevWord = /(?<![\\p{L}\\p{N}])(\\p{L}[\\p{L}\\p{M}]*)\\s*$/u.exec(\n before,\n );\n if (\n prevWord !== null &&\n getSentenceVerbIndicatorsSync().has(prevWord[1]!.toLowerCase())\n ) {\n appositiveRoleHead = true;\n }\n }\n if (lastVerbEndInMid !== -1 || digitAfterRole || appositiveRoleHead) {\n // Pick the first Cap-starting word in `text` after\n // the last verb (or, if only a digit signal fired,\n // after the role-head itself). Skip role-heads\n // (\"Vendor grants Licensee Acme Inc.\") and clause\n // nouns (\"Vendor signed Agreement with Acme Inc.\")\n // so the anchor lands on the real company name.\n // When trim was triggered by an appositive role-head\n // (no in-match verb), the role-head itself is the\n // thing to skip — scan starts from after the role\n // head's first word.\n const scanStart =\n lastVerbEndInMid !== -1 ? midStart + lastVerbEndInMid : midStart;\n const capRe = /(?<![\\p{L}\\p{N}])\\p{Lu}[\\p{L}\\p{M}\\p{N}]*/gu;\n capRe.lastIndex = scanStart;\n const clauseNouns = getClauseNounHeadsSync();\n let capMatch: RegExpExecArray | null = null;\n for (\n let next = capRe.exec(text);\n next !== null;\n next = capRe.exec(text)\n ) {\n if (next.index >= suffixOffset) {\n break;\n }\n const lc = next[0].toLowerCase();\n if (roleHeads.has(lc) || clauseNouns.has(lc)) {\n continue;\n }\n capMatch = next;\n break;\n }\n if (capMatch === null) {\n // No real cap-word before the suffix; drop.\n continue;\n }\n processedStart = match.start + capMatch.index;\n processedText = text.slice(capMatch.index);\n trimmed = true;\n }\n }\n }\n\n if (processedText.includes(\"\\n\") && hasDisallowedLineBreak(processedText)) {\n continue;\n }\n\n // Extend backward through connectors if fullText\n // is available (captures \"Be a Future\" from just\n // \"Future s.r.o.\")\n let entityStart = processedStart;\n let entityText = processedText;\n if (fullText && !trimmed && options.suppressExtendBackward !== true) {\n const shouldExtendBackward =\n !BARE_SINGLE_CAP_LEGAL_FORM_RE.test(processedText);\n const extended = shouldExtendBackward\n ? extendBackward(fullText, processedStart)\n : processedStart;\n if (extended < processedStart) {\n entityStart = extended;\n entityText = fullText\n .slice(extended, processedStart + processedText.length)\n .trimEnd();\n }\n }\n\n for (const segment of splitEmbeddedLegalFormList(entityStart, entityText)) {\n entityStart = segment.entityStart;\n entityText = segment.entityText;\n\n const listTrim = trimEmbeddedLegalFormListPrefix(entityStart, entityText);\n entityStart = listTrim.entityStart;\n entityText = listTrim.entityText;\n\n const clauseTrim = trimLeadingClause(entityText);\n if (clauseTrim.offset > 0) {\n entityStart += clauseTrim.offset;\n entityText = clauseTrim.text;\n }\n\n if (entityText.includes(\"\\n\") && hasDisallowedLineBreak(entityText)) {\n continue;\n }\n\n // Reject all-caps matches only if the entire\n // surrounding line is all-caps (section headings\n // like \"KUPNÍ SMLOUVA\"). If only the company name\n // is all-caps (\"uzavřená s EAGLES BRNO, z.s.\"),\n // keep it — max 3 all-caps words are allowed.\n const getPrefixInfo = (value: string) => {\n const prefixEnd =\n value.lastIndexOf(\",\") !== -1\n ? value.lastIndexOf(\",\")\n : value.lastIndexOf(\" \");\n const prefixPart =\n prefixEnd > 0\n ? value.slice(0, prefixEnd).replace(/[^a-zA-ZÀ-ž]/g, \"\")\n : value.replace(/[^a-zA-ZÀ-ž]/g, \"\");\n return { prefixEnd, prefixPart };\n };\n let { prefixEnd, prefixPart } = getPrefixInfo(entityText);\n let isAllCapsMatch =\n prefixPart.length > 2 && prefixPart === prefixPart.toUpperCase();\n\n if (isAllCapsMatch && fullText) {\n // Boilerplate-vs-caption discrimination is\n // handled centrally by isAllCapsBoilerplateLine\n // in filters/false-positives.ts; we no longer\n // duplicate it here. Keep the local guard that\n // restores the original regex match if backward\n // extension swept too many cap words into the\n // prefix — the centralised post-filter still\n // sees that restored span.\n const wordCount =\n prefixPart.length > 0\n ? entityText\n .slice(0, prefixEnd > 0 ? prefixEnd : entityText.length)\n .trim()\n .split(/\\s+/).length\n : 0;\n if (wordCount > 3) {\n entityStart = match.start;\n entityText = text;\n }\n } else if (isAllCapsMatch) {\n // No fullText available — fall back to rejecting.\n continue;\n }\n\n // Reject Roman numeral suffixes\n const lastSuffixSeparator = findLastSuffixSeparator(entityText);\n const rawSuffix =\n lastSuffixSeparator !== -1\n ? entityText.slice(lastSuffixSeparator + 1)\n : \"\";\n const suffixClean = rawSuffix.replace(/[.,]/g, \"\");\n if (suffixClean.length > 0 && ROMAN_NUMERAL_RE.test(suffixClean)) {\n continue;\n }\n\n // Short ASCII-only suffixes (NA, PA, LP, PC) are\n // US-specific. Reject if the prefix contains non-\n // ASCII chars (Czech/Slovak diacritics) — a US\n // legal entity wouldn't have \"ÚČASTI MSP NA\".\n // Test for dots in the raw suffix (before dot\n // stripping) to protect Czech dotted forms like\n // \"a.s.\" and \"k.s.\".\n if (\n suffixClean.length <= 2 &&\n !/\\./.test(rawSuffix) &&\n /[^\\p{ASCII}]/u.test(\n stripDocxSpaces(\n entityText.slice(\n 0,\n lastSuffixSeparator !== -1\n ? lastSuffixSeparator\n : entityText.length,\n ),\n ),\n )\n ) {\n continue;\n }\n\n // Definitive legal forms (s.r.o., a.s., GmbH, etc.)\n // get score 0.95 to beat person names in dedup.\n results.push({\n start: entityStart,\n end: entityStart + entityText.length,\n label: \"organization\",\n text: entityText,\n score: 0.95,\n source: DETECTION_SOURCES.LEGAL_FORM,\n });\n }\n }\n\n return results;\n};\n","import { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\nimport type { DefinitionPattern, PipelineContext } from \"../context\";\nimport { corefKey, defaultContext } from \"../context\";\nimport { loadLanguageConfigs } from \"../util/lang-loader\";\nimport { getKnownLegalSuffixes } from \"./legal-forms\";\n\n/**\n * Case-insensitive set of every known legal-form suffix\n * with whitespace normalized away. Used by the\n * coreference scan to reject parenthetical define-term\n * captures whose alias is only a legal-form\n * abbreviation (e.g. \"(« SAS »)\" after \"ACME SAS\"),\n * which would otherwise propagate generic suffix\n * mentions as document-wide org aliases.\n */\nconst isLegalFormAlias = (alias: string): boolean => {\n const normalized = alias.replace(/\\s+/g, \"\").toLowerCase();\n if (normalized.length === 0) {\n return false;\n }\n for (const suffix of getKnownLegalSuffixes()) {\n if (suffix.replace(/\\s+/g, \"\").toLowerCase() === normalized) {\n return true;\n }\n }\n return false;\n};\n\ntype CoreferenceConfigRow = {\n pattern: string;\n flags: string;\n label: string;\n};\n\n/**\n * Load coreference definition patterns from per-language\n * JSON configs in src/data/. Uses the\n * language manifest for auto-discovery.\n */\nconst loadDefinitionPatterns = async (): Promise<DefinitionPattern[]> => {\n const patterns: DefinitionPattern[] = [];\n\n const allRows = await loadLanguageConfigs<readonly CoreferenceConfigRow[]>(\n \"coreference\",\n (mod) => {\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config\n const m = mod as {\n default?: readonly CoreferenceConfigRow[];\n };\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config\n return (m.default ?? mod) as readonly CoreferenceConfigRow[];\n },\n );\n\n for (const rows of allRows) {\n if (!Array.isArray(rows)) {\n console.warn(\n \"[anonymize] coreference: unexpected \" + \"config shape, skipping\",\n );\n continue;\n }\n for (const row of rows) {\n try {\n patterns.push({\n pattern: new RegExp(row.pattern, row.flags),\n });\n } catch (err) {\n console.warn(\n `[anonymize] coreference: invalid ` + `regex \"${row.pattern}\":`,\n err,\n );\n }\n }\n }\n\n return patterns;\n};\n\n/**\n * Load generic role terms that should NOT be treated\n * as PII coreferences. \"Prodávající\" (Seller),\n * \"Kupující\" (Buyer), etc. are legal roles, not\n * identifying information.\n */\nconst getRoleStopSet = async (\n ctx: PipelineContext,\n): Promise<ReadonlySet<string>> => {\n if (ctx.roleStopSet) return ctx.roleStopSet;\n if (ctx.roleStopSetPromise) return ctx.roleStopSetPromise;\n const promise = (async () => {\n let result: ReadonlySet<string>;\n try {\n const mod = await import(\"../data/generic-roles.json\");\n const data = (mod.default ?? mod) as {\n roles: string[];\n };\n result = new Set(data.roles.map((r: string) => r.toLowerCase()));\n } catch {\n result = new Set();\n }\n ctx.roleStopSet = result;\n return result;\n })();\n ctx.roleStopSetPromise = promise;\n return promise;\n};\n\nconst getDefinitionPatterns = async (\n ctx: PipelineContext,\n): Promise<DefinitionPattern[]> => {\n if (ctx.corefPatterns) {\n return ctx.corefPatterns;\n }\n if (ctx.corefPatternsPromise) {\n return ctx.corefPatternsPromise;\n }\n ctx.corefPatternsPromise = loadDefinitionPatterns();\n const patterns = await ctx.corefPatternsPromise;\n if (patterns.length === 0) {\n // All loads failed; cache empty array permanently\n // to avoid retrying dynamic imports and flooding\n // logs on every call in high-volume pipelines.\n ctx.corefPatterns = patterns;\n if (!ctx.corefLoadAttempted) {\n ctx.corefLoadAttempted = true;\n console.warn(\n \"[anonymize] coreference: no definition \" +\n \"patterns loaded; coreference detection \" +\n \"will be inactive\",\n );\n }\n return patterns;\n }\n ctx.corefPatterns = patterns;\n return patterns;\n};\n\nconst SEARCH_WINDOW = 200;\n\n/**\n * Check whether an alias has textual similarity to\n * the source entity. Prevents roles and structural\n * terms from being treated as name aliases.\n *\n * Three checks (any passes → similar):\n * 1. Word overlap: a word in the alias appears in the\n * entity (case-insensitive, min 2 chars)\n * 2. Initials: alias letters match first letters of\n * entity words (\"TB\" ↔ \"Tomas Bata\")\n * 3. Substring: alias is a substring of the entity\n * or vice versa (min 3 chars)\n */\nconst hasEntitySimilarity = (alias: string, entityText: string): boolean => {\n const aliasLower = alias.toLowerCase();\n const entityLower = entityText.toLowerCase();\n\n // Substring check (min 3 chars to avoid noise)\n if (aliasLower.length >= 3 && entityLower.includes(aliasLower)) {\n return true;\n }\n if (entityLower.length >= 3 && aliasLower.includes(entityLower)) {\n return true;\n }\n\n // Word overlap: split both into words, check for\n // any shared word of 2+ characters\n const aliasWords = aliasLower\n .split(/[\\s.,;:'\"()/-]+/)\n .filter((w) => w.length >= 2);\n const entityWords = entityLower\n .split(/[\\s.,;:'\"()/-]+/)\n .filter((w) => w.length >= 2);\n const entityWordSet = new Set(entityWords);\n for (const word of aliasWords) {\n if (entityWordSet.has(word)) {\n return true;\n }\n }\n\n // Initials: alias is all uppercase and each letter\n // matches the first letter of a consecutive run of\n // entity words. \"TP\" ↔ \"Ing. Tomáš Procházka\"\n // (skips \"Ing.\" and matches T+P). Check all\n // starting positions to handle title prefixes.\n if (\n /^[\\p{Lu}]+$/u.test(alias) &&\n alias.length >= 2 &&\n alias.length <= entityWords.length\n ) {\n for (let start = 0; start <= entityWords.length - alias.length; start++) {\n const initials = entityWords\n .slice(start, start + alias.length)\n .map((w) => w.charAt(0))\n .join(\"\");\n if (initials === aliasLower) {\n return true;\n }\n }\n }\n\n return false;\n};\n\ntype DefinedTerm = {\n alias: string;\n label: string;\n /** Position of the definition in the source text */\n definitionStart: number;\n /** Original entity text the alias refers to */\n sourceText: string;\n};\n\n/**\n * Scan for defined-term patterns near known entities.\n *\n * Legal documents universally follow:\n * \"Dr. Heinrich Muller (hereinafter 'the Seller')...\"\n *\n * After NER detects the entity, this function scans for\n * definitional patterns within +/-200 chars and extracts\n * the alias. Returns alias + label pairs that can be added\n * to the gazetteer for a full-text re-scan.\n */\n/**\n * Labels that can be the source of a coreference alias.\n * Only parties (person, organization) have defined-term\n * aliases in legal text. Dates, addresses, IDs do not.\n */\nconst COREF_SOURCE_LABELS = new Set([\"person\", \"organization\"]);\n\nexport const extractDefinedTerms = async (\n fullText: string,\n entities: Entity[],\n ctx: PipelineContext = defaultContext,\n): Promise<DefinedTerm[]> => {\n const [definitionPatterns, roleStops] = await Promise.all([\n getDefinitionPatterns(ctx),\n getRoleStopSet(ctx),\n ]);\n const terms: DefinedTerm[] = [];\n const seen = new Set<string>();\n\n // Sort entities by position for nearest-preceding lookup\n const sorted = [...entities].sort((a, b) => a.start - b.start);\n\n // Strategy: find all \"dále jen\" definitions in the\n // text, then for each one, find the nearest PRECEDING\n // person/organization entity. This is more accurate\n // than \"any entity within 200 chars\" because:\n // - The alias always refers to a party defined BEFORE\n // the parenthetical, not after\n // - Multiple entities (name, IČO, address) may appear\n // between the party name and the \"dále jen\"; only\n // the party name is the referent\n\n for (const { pattern } of definitionPatterns) {\n pattern.lastIndex = 0;\n\n for (\n let match = pattern.exec(fullText);\n match !== null;\n match = pattern.exec(fullText)\n ) {\n const alias = match[1]?.trim();\n if (!alias || alias.length < 2) {\n continue;\n }\n\n // Skip generic legal roles — \"Prodávající\",\n // \"Kupující\", \"Seller\", etc. are NOT PII.\n // Only track aliases that are themselves\n // identifying (initials, name fragments, etc.)\n if (roleStops.has(alias.toLowerCase())) {\n continue;\n }\n\n // Skip aliases that are nothing but a legal-form\n // abbreviation (\"SAS\", \"SARL\", \"S.A.\", \"GmbH\").\n // A parenthetical like `ACME SAS (« SAS »)` would\n // otherwise pass the substring-similarity check\n // against \"ACME SAS\" and cause every standalone\n // \"SAS\" mention in the document to be redacted as\n // an organization alias.\n if (isLegalFormAlias(alias)) {\n continue;\n }\n\n const defPos = match.index;\n\n // Find the nearest preceding person/org entity\n // within SEARCH_WINDOW chars before this definition\n let bestEntity: Entity | null = null;\n for (let i = sorted.length - 1; i >= 0; i--) {\n const e = sorted[i];\n if (e === undefined) {\n continue;\n }\n // Must be before the definition\n if (e.end > defPos) {\n continue;\n }\n // Must be within the search window\n if (defPos - e.end > SEARCH_WINDOW) {\n break;\n }\n // Must be a party label\n if (!COREF_SOURCE_LABELS.has(e.label)) {\n continue;\n }\n bestEntity = e;\n break;\n }\n\n if (bestEntity === null) {\n continue;\n }\n\n // Clause-boundary gate: reject if a semicolon or\n // sentence-ending period sits between the source\n // entity and the definition. Periods inside\n // abbreviations like \"r.č.\" should not block an\n // otherwise valid definition.\n const gapText = fullText.slice(bestEntity.end, defPos);\n if (/(?:;|\\.(?=\\s*(?:[\"'„‚(]*\\p{Lu}|$)))/u.test(gapText)) {\n continue;\n }\n\n // Similarity gate: the alias must have textual\n // overlap with the source entity. Prevents roles\n // (\"Executive\", \"Seller\") and structural terms\n // (\"Agreement\", \"Effective Date\") from being\n // treated as name aliases.\n if (!hasEntitySimilarity(alias, bestEntity.text)) {\n continue;\n }\n\n const key = `${alias.toLowerCase()}::${bestEntity.label}`;\n if (seen.has(key)) {\n continue;\n }\n seen.add(key);\n\n terms.push({\n alias,\n label: bestEntity.label,\n definitionStart: defPos,\n sourceText: bestEntity.text,\n });\n }\n }\n\n return terms;\n};\n\n/**\n * Check if a character is a Unicode word character\n * (letter, digit, or combining mark). Used for word\n * boundary checks in coreference matching.\n */\nconst isWordChar = (ch: string | undefined): boolean => {\n if (ch === undefined) {\n return false;\n }\n return /[\\p{L}\\p{M}\\p{N}]/u.test(ch);\n};\n\n/**\n * Find all occurrences of defined-term aliases in the\n * full text. Returns Entity spans for each match.\n *\n * Respects word boundaries: \"Kupující\" must not match\n * inside \"Kupujícímu\". A match is valid only if the\n * character before the start and after the end are NOT\n * word characters (letter/digit).\n *\n * Populates `ctx.corefSourceMap` with entries linking\n * each coref entity to its source entity text, for\n * consistent placeholder numbering.\n */\nexport const findCoreferenceSpans = (\n fullText: string,\n terms: DefinedTerm[],\n ctx: PipelineContext = defaultContext,\n): Entity[] => {\n const results: Entity[] = [];\n\n for (const term of terms) {\n let searchFrom = 0;\n while (searchFrom < fullText.length) {\n const idx = fullText.indexOf(term.alias, searchFrom);\n if (idx === -1) {\n break;\n }\n\n const matchEnd = idx + term.alias.length;\n\n // Word boundary check: the character before the\n // match and after the match must not be a word\n // character. This prevents \"Kupující\" matching\n // inside \"Kupujícímu\".\n const charBefore = idx > 0 ? fullText[idx - 1] : undefined;\n const charAfter = fullText[matchEnd];\n\n if (isWordChar(charBefore) || isWordChar(charAfter)) {\n // Not at a word boundary — skip\n searchFrom = idx + 1;\n continue;\n }\n\n const entity: Entity = {\n start: idx,\n end: matchEnd,\n label: term.label,\n text: term.alias,\n score: 0.95,\n source: DETECTION_SOURCES.COREFERENCE,\n };\n ctx.corefSourceMap.set(corefKey(entity), term.sourceText);\n results.push(entity);\n\n searchFrom = matchEnd;\n }\n }\n\n return results;\n};\n","import type { Match, PatternEntry } from \"@stll/text-search\";\n\nimport { DETECTION_SOURCES } from \"../types\";\nimport type { Entity, GazetteerEntry } from \"../types\";\nimport type { GazetteerData } from \"../build-unified-search\";\n\nconst MAX_EDIT_DISTANCE = 2;\nconst MIN_FUZZY_LENGTH = 4;\n// \" s.r.o.\" = 7 bytes (space + 6 chars). Must be\n// at least 7 to capture the most common Czech LLC\n// suffix without truncating the trailing dot.\nconst MAX_PREFIX_OVERSHOOT = 7;\n\n/**\n * Collect all searchable strings (canonical + variants)\n * from gazetteer entries, mapped to their labels and\n * entry IDs.\n */\nexport const buildSearchTerms = (\n entries: GazetteerEntry[],\n): Map<string, { label: string; entryId: string }> => {\n const terms = new Map<string, { label: string; entryId: string }>();\n for (const entry of entries) {\n const meta = {\n label: entry.label,\n entryId: entry.id,\n };\n terms.set(entry.canonical, meta);\n for (const variant of entry.variants) {\n terms.set(variant, meta);\n }\n }\n return terms;\n};\n\n/**\n * Build TextSearch-compatible patterns from gazetteer\n * entries. Returns:\n * - Exact literal patterns for all terms\n * - Fuzzy patterns (distance: 2) for terms >= 4 chars\n * - Parallel metadata arrays for post-processing\n *\n * Patterns are ordered: all exact first, then all\n * fuzzy. The isFuzzy array marks which are which.\n */\nexport const buildGazetteerPatterns = (\n entries: GazetteerEntry[],\n): {\n patterns: PatternEntry[];\n data: GazetteerData;\n} => {\n const terms = buildSearchTerms(entries);\n\n const patterns: PatternEntry[] = [];\n const labels: string[] = [];\n const isFuzzy: boolean[] = [];\n\n // Pass 1: exact literals (all terms).\n // Use per-pattern wholeWords: false because\n // user-supplied entries may contain dots or\n // special chars (e.g. \"a.s.\", \"AT&T\") that\n // break word-boundary matching. Deny-list and\n // street-type patterns use per-pattern\n // wholeWords: true instead.\n for (const [term, meta] of terms) {\n patterns.push({\n pattern: term,\n literal: true as const,\n wholeWords: false,\n });\n labels.push(meta.label);\n\n isFuzzy.push(false);\n }\n\n // Pass 2: fuzzy patterns (terms >= 4 chars).\n // These are separate patterns with distance: 2,\n // routed to @stll/fuzzy-search by TextSearch.\n for (const [term, meta] of terms) {\n if (term.length < MIN_FUZZY_LENGTH) {\n continue;\n }\n patterns.push({\n pattern: term,\n distance: MAX_EDIT_DISTANCE,\n });\n labels.push(meta.label);\n\n isFuzzy.push(true);\n }\n\n return {\n patterns,\n data: { labels, isFuzzy },\n };\n};\n\n/**\n * Process gazetteer matches from the unified literal\n * search. Receives all matches; filters to the\n * gazetteer slice via sliceStart/sliceEnd.\n *\n * Exact matches get score 0.9; fuzzy matches get\n * 0.85. Fuzzy matches that overlap an exact match\n * are dropped.\n *\n * For exact matches, attempts prefix extension for\n * legal suffixes (\"a.s.\", \"GmbH\", \"s.r.o.\" after\n * the matched term).\n */\nexport const processGazetteerMatches = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText: string,\n data: GazetteerData,\n): Entity[] => {\n const results: Entity[] = [];\n // Track exact-match spans for overlap filtering\n const exactSpans: Array<{\n start: number;\n end: number;\n }> = [];\n\n // Pass 1: exact matches (isFuzzy === false)\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) {\n continue;\n }\n const localIdx = idx - sliceStart;\n if (data.isFuzzy[localIdx]) {\n continue;\n }\n\n const label = data.labels[localIdx];\n if (!label) {\n continue;\n }\n\n // Try prefix extension for legal entity suffixes\n const extended = tryPrefixExtension(fullText, match.start, match.end);\n const isExtended = extended !== null;\n const end = extended?.end ?? match.end;\n const text = extended?.text ?? fullText.slice(match.start, match.end);\n\n exactSpans.push({\n start: match.start,\n end,\n });\n const entity: Entity = {\n start: match.start,\n end,\n label,\n text,\n score: 0.9,\n source: DETECTION_SOURCES.GAZETTEER,\n };\n if (isExtended) {\n entity.sourceDetail = \"gazetteer-extension\";\n }\n results.push(entity);\n }\n\n // Pass 2: fuzzy matches (isFuzzy === true),\n // skipping those that overlap exact spans\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) {\n continue;\n }\n const localIdx = idx - sliceStart;\n if (!data.isFuzzy[localIdx]) {\n continue;\n }\n\n // Skip distance-0 fuzzy hits (already caught\n // as exact matches above)\n if (match.distance === 0) {\n continue;\n }\n\n const label = data.labels[localIdx];\n if (!label) {\n continue;\n }\n\n // Skip if overlapping any exact span\n const overlapsExact = exactSpans.some(\n (e) => match.start < e.end && match.end > e.start,\n );\n if (overlapsExact) {\n continue;\n }\n\n const matchText = fullText.slice(match.start, match.end);\n results.push({\n start: match.start,\n end: match.end,\n label,\n text: matchText,\n score: 0.85,\n source: DETECTION_SOURCES.GAZETTEER,\n });\n }\n\n return results;\n};\n\n/**\n * Try to extend an exact match to capture one\n * trailing token (max 6 chars) that may be a legal\n * entity suffix (e.g., \"a.s.\", \"GmbH\", \"s.r.o.\").\n *\n * Does not validate the token against a legal-forms\n * list; false extensions are filtered by mergeAndDedup\n * when a legal-form detector produces a competing\n * entity with the correct span.\n */\nconst tryPrefixExtension = (\n fullText: string,\n start: number,\n end: number,\n): { end: number; text: string } | null => {\n const maxEnd = Math.min(end + MAX_PREFIX_OVERSHOOT, fullText.length);\n if (maxEnd <= end + 1) {\n return null;\n }\n\n const after = fullText.slice(end, maxEnd);\n if (!after.startsWith(\" \")) {\n return null;\n }\n const nextSpace = after.indexOf(\" \", 1);\n const suffixEnd = nextSpace !== -1 ? nextSpace : after.length;\n if (suffixEnd <= 1) {\n return null;\n }\n\n const newEnd = end + suffixEnd;\n return {\n end: newEnd,\n text: fullText.slice(start, newEnd),\n };\n};\n\n// Deprecated exports (kept for API compat).\n","/**\n * Normalize typographic variants for search matching.\n *\n * Legal documents (especially Czech/German) use\n * non-breaking spaces, smart quotes, and en/em dashes\n * that differ from their ASCII equivalents. Since all\n * replacements are same-length (single code unit →\n * single code unit), character offsets remain valid.\n *\n * Lives here (application layer) rather than in the\n * AC library: what to normalize is domain-specific.\n *\n * Uses a char-code lookup (`Map<number, number>`) and\n * `Uint16Array` instead of 7 sequential `replaceAll`\n * calls. For a 50 KB document this eliminates ~350 KB\n * of intermediate string allocations.\n *\n * When no replaceable characters are present (common\n * for plain-text inputs), a fast-path scan returns the\n * original string without any allocation. When special\n * characters exist, the string is scanned twice: once\n * to detect, once to build the replacement array.\n */\n\nconst replacementCode = (code: number): number => {\n switch (code) {\n case 0x00a0:\n case 0x2007:\n case 0x202f:\n return 0x0020;\n case 0x2013:\n case 0x2014:\n return 0x002d;\n case 0x201c:\n case 0x201d:\n return 0x0022;\n default:\n return code;\n }\n};\n\n/** Chunk size for `String.fromCharCode` to avoid\n * hitting the call-stack limit on very large strings. */\nconst CHUNK_SIZE = 8192;\n\nexport const normalizeForSearch = (text: string): string => {\n // Fast path: skip allocation when nothing to replace.\n let hasSpecial = false;\n for (let i = 0; i < text.length; i++) {\n const code = text.charCodeAt(i);\n if (replacementCode(code) !== code) {\n hasSpecial = true;\n break;\n }\n }\n if (!hasSpecial) return text;\n\n // Second pass: build output via Uint16Array.\n const len = text.length;\n const codes = new Uint16Array(len);\n for (let i = 0; i < len; i++) {\n const code = text.charCodeAt(i);\n codes[i] = replacementCode(code);\n }\n\n // Convert in chunks to avoid stack overflow on\n // spread of large arrays.\n if (len <= CHUNK_SIZE) {\n return String.fromCharCode(...codes);\n }\n\n let result = \"\";\n for (let offset = 0; offset < len; offset += CHUNK_SIZE) {\n const end = Math.min(offset + CHUNK_SIZE, len);\n result += String.fromCharCode(...codes.subarray(offset, end));\n }\n return result;\n};\n","","import type { Match, PatternEntry } from \"@stll/text-search\";\n\nimport { DETECTION_SOURCES } from \"../constants\";\nimport type { Entity } from \"../types\";\nimport { normalizeForSearch } from \"../util/normalize\";\n\nimport countriesData from \"../data/countries.json\" with { type: \"json\" };\n\nconst ENTITY_LABEL = \"country\";\nconst EXACT_SCORE = 0.95;\n\n/**\n * Whether to surface bare ISO 3166-1 alpha-2 codes\n * (\"US\", \"GB\", \"IT\") as country matches. Off by default\n * because two-letter sequences collide constantly with\n * English/legal prose (\"IT department\", \"OR\", pronouns).\n */\nconst INCLUDE_ALPHA2 = false;\n\n/**\n * CLDR canonical names that collide with common\n * English/global words. Filtered before registration so\n * they never become whole-word case-insensitive country\n * patterns. Examples: \"Island\" is Icelandic for Iceland\n * (also Da/No/Sv/Fi); \"Man\" is Norwegian for Isle of Man;\n * \"Indie\" is the Czech and Polish form of India and\n * collides with the English adjective (\"indie developer\").\n * All would flag every English occurrence as a country.\n */\nconst NAME_BLOCKLIST: ReadonlySet<string> = new Set(\n [\"man\", \"island\", \"indie\"].map((s) => s.toLowerCase()),\n);\n\n/**\n * Whether to surface ISO 3166-1 alpha-3 codes (\"USA\",\n * \"GBR\", \"JPN\") as country matches. Off by default\n * because alpha-3 codes collide case-insensitively with\n * common English/Spanish words: AND (Andorra), ARE (UAE),\n * CAN (Canada), PER (Peru), VAT (Vatican), COG, DOM,\n * FIN, GIN, LIE, MAR, NAM, PAN, TON, etc. A\n * case-sensitive matcher would solve this but the unified\n * literal search runs case-insensitive. Common alpha-3\n * forms (\"USA\") are covered by the curated aliases list\n * instead.\n */\nconst INCLUDE_ALPHA3 = false;\n\n/**\n * Pre-built country patterns + parallel label/source\n * metadata. Constructed once and reused across pipeline\n * runs.\n */\nexport type CountryData = {\n /** Maps local pattern index to entity label. Always \"country\". */\n labels: string[];\n /**\n * Maps local pattern index to the alpha-2 ISO code the\n * pattern resolves to. Used for downstream coreference /\n * placeholder grouping.\n */\n isoCodes: string[];\n /** Maps local pattern index to pattern variant kind. */\n variants: CountryVariant[];\n};\n\nexport type CountryVariant = \"name\" | \"alias\" | \"alpha3\" | \"alpha2\";\n\ntype RawCountryData = {\n codes: { alpha2: string[]; alpha3: string[] };\n names: Record<string, Record<string, string>>;\n aliases: Record<string, string[]>;\n};\n\ntype CountryPatterns = {\n patterns: PatternEntry[];\n data: CountryData;\n};\n\nlet cachedCountryPatterns: CountryPatterns | null = null;\n\n/**\n * Build country patterns for the literal search instance.\n * Returns patterns and parallel metadata arrays.\n *\n * Patterns are literal, case-insensitive, whole-word.\n * Each unique surface form appears once, keyed by its\n * resolved alpha-2 code so duplicate surface forms (e.g.,\n * \"Georgia\" the US state vs. the country) collapse to the\n * country entry.\n */\nexport const buildCountryPatterns = (): {\n patterns: PatternEntry[];\n data: CountryData;\n} => {\n if (cachedCountryPatterns !== null) {\n return cachedCountryPatterns;\n }\n\n const raw = countriesData as RawCountryData;\n\n // Map surface form (lowercased) → { isoCode, variant }.\n // First writer wins so name beats alias beats alpha3.\n const surfaceToMeta = new Map<\n string,\n { display: string; isoCode: string; variant: CountryVariant }\n >();\n\n const register = (\n surface: string,\n isoCode: string,\n variant: CountryVariant,\n ) => {\n const trimmed = surface.trim();\n if (trimmed.length === 0) return;\n // The unified literal search runs against\n // `normalizeForSearch(fullText)`, which rewrites NBSP / smart\n // quotes / en–em-dashes to their ASCII equivalents. CLDR\n // ships names with en-dashes (\"Kongo – Kinshasa\", \"Hongkong –\n // ZAO Číny\") and smart apostrophes; without the same\n // normalization on the pattern side those names would never\n // match real input. Replacements are same-length, so match\n // offsets in the original text stay valid.\n const normalized = normalizeForSearch(trimmed);\n const key = normalized.toLowerCase();\n if (NAME_BLOCKLIST.has(key)) return;\n if (!surfaceToMeta.has(key)) {\n surfaceToMeta.set(key, { display: normalized, isoCode, variant });\n }\n // Typographic-apostrophe variants. CLDR ships only the\n // curly form (\"Côte d’Ivoire\"); legal/OCR text routinely\n // uses the straight one (\"Côte d'Ivoire\"). Register both\n // so either renders match.\n if (trimmed.includes(\"’\") || trimmed.includes(\"‘\")) {\n const straight = normalizeForSearch(trimmed.replaceAll(/[‘’]/g, \"'\"));\n const straightKey = straight.toLowerCase();\n if (!surfaceToMeta.has(straightKey)) {\n surfaceToMeta.set(straightKey, {\n display: straight,\n isoCode,\n variant,\n });\n }\n }\n };\n\n // Canonical names per language, keyed by alpha-2 code.\n for (const perLang of Object.values(raw.names)) {\n for (const [code, name] of Object.entries(perLang)) {\n register(name, code, \"name\");\n }\n }\n\n // Curated aliases.\n for (const [isoCode, aliases] of Object.entries(raw.aliases)) {\n for (const alias of aliases) {\n register(alias, isoCode, \"alias\");\n }\n }\n\n // Alpha-3 (opt-in only; too many case-insensitive\n // collisions with common words — see INCLUDE_ALPHA3).\n if (INCLUDE_ALPHA3) {\n for (let i = 0; i < raw.codes.alpha2.length; i++) {\n const a2 = raw.codes.alpha2[i];\n const a3 = raw.codes.alpha3[i];\n if (a2 && a3) register(a3, a2, \"alpha3\");\n }\n }\n\n // Alpha-2 (opt-in only; too many false positives).\n if (INCLUDE_ALPHA2) {\n for (const code of raw.codes.alpha2) {\n register(code, code, \"alpha2\");\n }\n }\n\n const patterns: PatternEntry[] = [];\n const labels: string[] = [];\n const isoCodes: string[] = [];\n const variants: CountryVariant[] = [];\n\n for (const { display, isoCode, variant } of surfaceToMeta.values()) {\n patterns.push({\n pattern: display,\n literal: true,\n wholeWords: true,\n });\n labels.push(ENTITY_LABEL);\n isoCodes.push(isoCode);\n variants.push(variant);\n }\n\n cachedCountryPatterns = { patterns, data: { labels, isoCodes, variants } };\n return cachedCountryPatterns;\n};\n\n/**\n * Whether the match starts on a proper-noun character.\n * The unified literal search is case-insensitive, so\n * lowercase common nouns that share spelling with a\n * country name (\"turkey\" the bird, \"china\" the porcelain,\n * \"jordan\" the basketball player nickname) would otherwise\n * be flagged. CLDR canonical names and the curated alias\n * list are always proper nouns; require uppercase when the\n * first character is a letter so common-noun usage in\n * prose isn't redacted. Non-letter starts (digits in\n * \"U.S.A.\", etc.) are accepted as-is.\n */\nconst startsAsProperNoun = (text: string, start: number): boolean => {\n const ch = text.charAt(start);\n if (ch.length === 0) return false;\n const upper = ch.toUpperCase();\n const lower = ch.toLowerCase();\n // Non-letter character → no case to enforce.\n if (upper === lower) return true;\n return ch === upper;\n};\n\n/**\n * Convert raw country matches into Entity objects.\n * Filters to the country slice and emits one entity per\n * match. Score is constant (0.95) for all variants;\n * deterministic detector with no fuzzy paths.\n */\nexport const processCountryMatches = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText: string,\n data: CountryData,\n): Entity[] => {\n const results: Entity[] = [];\n\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) continue;\n\n const localIdx = idx - sliceStart;\n const label = data.labels[localIdx];\n if (!label) continue;\n\n if (!startsAsProperNoun(fullText, match.start)) continue;\n\n results.push({\n start: match.start,\n end: match.end,\n label,\n text: fullText.slice(match.start, match.end),\n score: EXACT_SCORE,\n source: DETECTION_SOURCES.COUNTRY,\n });\n }\n\n return results;\n};\n","/**\n * Shared text utilities for detectors.\n *\n * Extracted from names.ts and deny-list.ts to avoid\n * duplicating regex constants and helper functions.\n */\n\n/** Matches a string that starts with an uppercase letter. */\nexport const UPPER_START_RE = /^\\p{Lu}/u;\n\n/** Matches a string consisting entirely of uppercase letters. */\nexport const ALL_UPPER_RE = /^\\p{Lu}+$/u;\n\nconst SENTENCE_END_RE = /[.!?]/;\n\n/**\n * Detect whether a position is at the start of a sentence.\n * Looks backward past whitespace for sentence-ending\n * punctuation (.!?). Position 0 and positions preceded\n * only by whitespace are considered sentence starts.\n */\nexport const isSentenceStart = (text: string, pos: number): boolean => {\n if (pos === 0) {\n return true;\n }\n let i = pos - 1;\n while (i >= 0 && /\\s/.test(text[i] ?? \"\")) {\n i--;\n }\n if (i < 0) {\n return true;\n }\n return SENTENCE_END_RE.test(text[i] ?? \"\");\n};\n","import { DETECTION_SOURCES } from \"../types\";\nimport type { Dictionaries, Entity } from \"../types\";\nimport type { PipelineContext, NameCorpusData } from \"../context\";\nimport { defaultContext } from \"../context\";\nimport { ALL_UPPER_RE, UPPER_START_RE, isSentenceStart } from \"../util/text\";\n\n// ── Name corpus ──────────────────────────────────────\n// Per-language first names and surnames loaded from\n// injected dictionaries (optional) plus legacy\n// config/names-*.json for backwards compat. Merged at\n// init time across all configured languages.\n\n// ── Accessors (read from context) ────────────────────\n\nconst getCorpus = (ctx: PipelineContext): NameCorpusData | null =>\n ctx.nameCorpus;\n\n// Exported accessors for deny-list.ts AC integration.\nexport const getNameCorpusFirstNames = (\n ctx: PipelineContext = defaultContext,\n): readonly string[] => ctx.nameCorpus?.firstNamesList ?? [];\nexport const getNameCorpusSurnames = (\n ctx: PipelineContext = defaultContext,\n): readonly string[] => ctx.nameCorpus?.surnamesList ?? [];\nexport const getNameCorpusTitles = (\n ctx: PipelineContext = defaultContext,\n): readonly string[] => ctx.nameCorpus?.titlesList ?? [];\nexport const getNameCorpusExcluded = (\n ctx: PipelineContext = defaultContext,\n): readonly string[] => ctx.nameCorpus?.excludedList ?? [];\n\n/**\n * Load name corpus data from injected dictionaries\n * and legacy config files. Merges all sources.\n *\n * Safe to call multiple times; only loads once per\n * context. Must be called before detectNameCorpus or\n * the getNameCorpus*() accessors are used.\n *\n * @param dictionaries Optional pre-loaded dictionaries\n * with per-language first names and surnames. When\n * omitted, only legacy config files are used.\n */\nexport const initNameCorpus = (\n ctx: PipelineContext = defaultContext,\n dictionaries?: Dictionaries,\n languages?: readonly string[],\n): Promise<void> => {\n const languageKey = languages?.toSorted().join(\",\") ?? \"*\";\n if (ctx.nameCorpus && ctx.nameCorpusKey === languageKey) {\n return Promise.resolve();\n }\n if (ctx.nameCorpusPromise && ctx.nameCorpusKey === languageKey) {\n return ctx.nameCorpusPromise;\n }\n ctx.nameCorpus = null;\n ctx.nameCorpusKey = languageKey;\n const promise = (async () => {\n try {\n // Load legacy config files (backwards compat)\n const [\n legacyFirstMod,\n legacySurnameMod,\n titleMod,\n exclusionMod,\n commonWordsMod,\n ] = await Promise.all([\n import(\"../data/names-first.json\") as Promise<{\n default: { names: string[] };\n }>,\n import(\"../data/names-surnames.json\") as Promise<{\n default: { names: string[] };\n }>,\n import(\"../data/names-title-tokens.json\") as Promise<{\n default: { tokens: string[] };\n }>,\n import(\"../data/names-exclusions.json\") as Promise<{\n default: { words: string[] };\n }>,\n import(\"../data/common-words-en.json\") as Promise<{\n default: { words: string[] };\n }>,\n ]);\n\n // Merge: legacy config + injected per-language files\n const firstNames: string[] = [...legacyFirstMod.default.names];\n if (dictionaries?.firstNames) {\n const entries =\n languages === undefined\n ? Object.entries(dictionaries.firstNames)\n : Object.entries(dictionaries.firstNames).filter(([language]) =>\n languages.includes(language),\n );\n for (const [, names] of entries) {\n for (const name of names) {\n firstNames.push(name);\n }\n }\n }\n\n const surnames: string[] = [...legacySurnameMod.default.names];\n if (dictionaries?.surnames) {\n const entries =\n languages === undefined\n ? Object.entries(dictionaries.surnames)\n : Object.entries(dictionaries.surnames).filter(([language]) =>\n languages.includes(language),\n );\n for (const [, names] of entries) {\n for (const name of names) {\n surnames.push(name);\n }\n }\n }\n\n // Deduplicate (preserve first occurrence)\n const dedup = (arr: string[]): string[] => {\n const seen = new Set<string>();\n const result: string[] = [];\n for (const item of arr) {\n if (seen.has(item)) continue;\n seen.add(item);\n result.push(item);\n }\n return result;\n };\n\n const commonWords = new Set(\n commonWordsMod.default.words.map((word) => word.toLowerCase()),\n );\n const dedupFirst = dedup(firstNames);\n const dedupSurnames = dedup(surnames).filter(\n (name) => !commonWords.has(name.toLowerCase()),\n );\n const titles = titleMod.default.tokens;\n const exclusions = exclusionMod.default.words;\n\n ctx.nameCorpus = {\n firstNames: Object.freeze(new Set(dedupFirst)),\n surnames: Object.freeze(new Set(dedupSurnames)),\n titleTokens: Object.freeze(new Set(titles)),\n excludedWords: Object.freeze(new Set(exclusions)),\n firstNamesList: Object.freeze(dedupFirst),\n surnamesList: Object.freeze(dedupSurnames),\n titlesList: Object.freeze(titles),\n excludedList: Object.freeze(exclusions),\n };\n } catch (err) {\n // Reset so the next call retries the load rather\n // than returning this (already-resolved) failed\n // promise. Current awaiters still get a resolved\n // (not rejected) Promise; ctx.nameCorpus stays null.\n ctx.nameCorpusPromise = null;\n console.warn(\n \"[anonymize] Failed to load name corpus JSON\" +\n \" — name detection disabled:\",\n err,\n );\n }\n })();\n ctx.nameCorpusPromise = promise;\n return promise;\n};\n\n// ── Czech/Slovak suffix stripping ────────────────────\n// Case suffixes commonly appended to names in declined\n// Czech/Slovak text. Ordered longest-first.\n\nconst INFLECTION_SUFFIXES = [\n \"ovi\", // dative\n \"em\", // instrumental\n \"om\", // instrumental (some stems)\n \"ou\", // instrumental feminine\n \"é\", // dative/locative feminine\n \"a\", // genitive\n \"u\", // accusative/locative\n] as const;\n\n/**\n * Strip common Czech/Slovak case suffixes from a token.\n * Returns candidate base forms if stripping produces a\n * plausible name (capitalised, length >= 3).\n *\n * For the \"-ou\" instrumental feminine suffix, also yields\n * base + \"a\" (e.g., \"Editou\" → \"Edit\" and \"Edita\")\n * because Czech feminine names decline -a → -ou.\n */\nconst stripInflection = (token: string): string[] => {\n const candidates: string[] = [];\n for (const suffix of INFLECTION_SUFFIXES) {\n if (token.length > suffix.length + 2 && token.endsWith(suffix)) {\n const base = token.slice(0, -suffix.length);\n if (/^\\p{Lu}/u.test(base)) {\n candidates.push(base);\n // Czech feminine: -a → -ou (instrumental),\n // -a → -é (dative/locative), -a → -u (accusative)\n if (suffix === \"ou\" || suffix === \"é\" || suffix === \"u\") {\n candidates.push(`${base}a`);\n }\n // Czech feminine: -a → -ovi is not valid, but\n // -e → -em is (e.g., \"Kalhousem\" → \"Kalhous\")\n // which is already handled by stripping \"em\".\n }\n }\n }\n return candidates;\n};\n\n// ── Token types ──────────────────────────────────────\n\nconst TOKEN_TYPE = {\n NAME: \"name\",\n SURNAME: \"surname\",\n TITLE: \"title\",\n ABBREVIATION: \"abbreviation\",\n CAPITALIZED: \"capitalized\",\n OTHER: \"other\",\n} as const;\n\ntype TokenType = (typeof TOKEN_TYPE)[keyof typeof TOKEN_TYPE];\n\ntype ClassifiedToken = {\n text: string;\n type: TokenType;\n start: number;\n end: number;\n};\n\nconst PERSON_CHAIN_BREAK_RE = /[!?;:]/u;\n\nconst isInitialContinuationGap = (text: string, gap: string): boolean =>\n (/^\\p{Lu}$/u.test(text) && /^\\.[^\\S\\n]{1,2}$/u.test(gap)) ||\n /^[^\\S\\n]{1,2}(?:\\p{Lu}\\.[^\\S\\n]{1,2})+$/u.test(gap);\n\n// ── Helpers ──────────────────────────────────────────\n\n/**\n * Check if a token is in the first-name set, either\n * directly or after stripping Czech/Slovak inflection.\n */\nconst isFirstNameToken = (token: string, corpus: NameCorpusData): boolean => {\n if (corpus.firstNames.has(token)) {\n return true;\n }\n return stripInflection(token).some((b) => corpus.firstNames.has(b));\n};\n\n/**\n * Check if a token is in the surname set, either\n * directly or after stripping Czech/Slovak inflection.\n */\nconst isSurnameToken = (token: string, corpus: NameCorpusData): boolean => {\n if (corpus.surnames.has(token)) {\n return true;\n }\n return stripInflection(token).some((b) => corpus.surnames.has(b));\n};\n\n/**\n * Check if a token looks like a single-letter\n * abbreviation: \"J.\", \"M.\", etc.\n */\nconst isAbbreviation = (token: string): boolean =>\n token.length === 2 && /^\\p{Lu}$/u.test(token[0] ?? \"\") && token[1] === \".\";\n\n// ── Word segmentation ────────────────────────────────\n\nconst segmenter = new Intl.Segmenter(undefined, {\n granularity: \"word\",\n});\n\ntype WordSegment = {\n text: string;\n start: number;\n end: number;\n};\n\n/**\n * Split text into word segments using Intl.Segmenter.\n * Only returns segments flagged as words.\n */\nconst segmentWords = (fullText: string): WordSegment[] => {\n const words: WordSegment[] = [];\n for (const seg of segmenter.segment(fullText)) {\n if (seg.isWordLike) {\n words.push({\n text: seg.segment,\n start: seg.index,\n end: seg.index + seg.segment.length,\n });\n }\n }\n return words;\n};\n\n// ── Helpers for chain scoring ────────────────────────\n\n/** NAME or SURNAME — both represent corpus-matched tokens */\nconst isCorpusMatch = (type: TokenType): boolean =>\n type === TOKEN_TYPE.NAME || type === TOKEN_TYPE.SURNAME;\n\n// ── Token classification ─────────────────────────────\n\n// True when the line containing `start` is itself\n// predominantly upper-case (signature block, title\n// block, party caption). Used so the acronym filter\n// below can still match all-caps tokens that match\n// the name corpus in title-case (\"ELON R. MUSK\") while\n// still rejecting acronyms in mixed-case prose.\nconst ALL_CAPS_NAME_LINE_RATIO = 0.9;\nconst ALL_CAPS_NAME_LINE_MIN_LETTERS = 3;\n// Name-shape filter for the all-caps recovery path:\n// real party-caption and signature lines contain only\n// a handful of letter tokens and no digits (\"ELON R.\n// MUSK\", \"X HOLDINGS I, INC.\"). Disclosure/heading\n// lines that happen to include a corpus first name\n// (\"SERVICE MARK LICENSE\", \"ANNUAL STATEMENT OF\n// COMPLIANCE\") fail this check and stay OTHER.\nconst ALL_CAPS_NAME_LINE_MAX_TOKENS = 6;\nconst isAllCapsLineNameShaped = (fullText: string, start: number): boolean => {\n const lineStart = fullText.lastIndexOf(\"\\n\", start - 1) + 1;\n const lineEndIdx = fullText.indexOf(\"\\n\", start);\n const line = fullText.slice(\n lineStart,\n lineEndIdx === -1 ? fullText.length : lineEndIdx,\n );\n if (/\\d/.test(line)) return false;\n const tokens = line.match(/\\p{L}[\\p{L}\\p{M}'-]*/gu) ?? [];\n return tokens.length > 0 && tokens.length <= ALL_CAPS_NAME_LINE_MAX_TOKENS;\n};\n\nconst isAllCapsContextLine = (fullText: string, start: number): boolean => {\n const lineStart = fullText.lastIndexOf(\"\\n\", start - 1) + 1;\n const lineEndIdx = fullText.indexOf(\"\\n\", start);\n const line = fullText.slice(\n lineStart,\n lineEndIdx === -1 ? fullText.length : lineEndIdx,\n );\n let letters = 0;\n let upper = 0;\n for (const ch of line) {\n if (/\\p{L}/u.test(ch)) {\n letters += 1;\n if (ch === ch.toUpperCase() && ch !== ch.toLowerCase()) {\n upper += 1;\n }\n }\n }\n if (letters < ALL_CAPS_NAME_LINE_MIN_LETTERS) {\n return false;\n }\n return upper / letters >= ALL_CAPS_NAME_LINE_RATIO;\n};\n\nconst classifyToken = (\n word: WordSegment,\n corpus: NameCorpusData,\n fullText: string,\n): ClassifiedToken => {\n const { text, start, end } = word;\n const lower = text.toLowerCase();\n\n // Strip trailing period for title check (e.g., \"Ing.\")\n const stripped = text.endsWith(\".\") ? text.slice(0, -1).toLowerCase() : lower;\n\n if (corpus.titleTokens.has(stripped)) {\n return { text, type: TOKEN_TYPE.TITLE, start, end };\n }\n\n if (isAbbreviation(text)) {\n return {\n text,\n type: TOKEN_TYPE.ABBREVIATION,\n start,\n end,\n };\n }\n\n // `Intl.Segmenter` splits middle initials into a\n // letter word and a separate punctuation segment\n // (\"R.\" → word \"R\" + \".\"), so the standard\n // `isAbbreviation` check (which requires a length-2\n // \"X.\" token) misses them. Recognise a single\n // uppercase letter immediately followed by a \".\" in\n // the source text as an abbreviation too, so chains\n // like \"ADAM R. BARTOŠ\" don't break on the initial.\n //\n // Standalone enumerators (\"A. Definitions\",\n // \"Section R. Adam\") look identical to middle\n // initials at the token level. Distinguish by the\n // previous word on the same line: a middle initial\n // is always preceded by a name-corpus first name,\n // so only classify as ABBREVIATION when that\n // structural context is present.\n if (text.length === 1 && UPPER_START_RE.test(text) && fullText[end] === \".\") {\n const lineStart = fullText.lastIndexOf(\"\\n\", start - 1) + 1;\n const before = fullText.slice(lineStart, start).trimEnd();\n const lastWord = /\\p{L}[\\p{L}\\p{M}'-]*$/u.exec(before)?.[0];\n if (lastWord) {\n const lookup = (token: string): boolean =>\n isFirstNameToken(token, corpus) ||\n isFirstNameToken(\n (token[0] ?? \"\") + token.slice(1).toLowerCase(),\n corpus,\n );\n if (lookup(lastWord)) {\n return { text, type: TOKEN_TYPE.ABBREVIATION, start, end };\n }\n }\n return { text, type: TOKEN_TYPE.OTHER, start, end };\n }\n\n // Skip excluded words\n if (corpus.excludedWords.has(lower)) {\n return { text, type: TOKEN_TYPE.OTHER, start, end };\n }\n\n // Minimum length 3\n if (text.length < 3) {\n return { text, type: TOKEN_TYPE.OTHER, start, end };\n }\n\n // All-uppercase tokens >= 3 chars are usually\n // acronyms, but in a signature or title block they are\n // real names rendered in caps (\"ELON R. MUSK\", \"JAN\n // NOVÁK\"). Allow the corpus lookup in title-case only\n // when (a) the line itself is overwhelmingly upper-\n // case and (b) the line looks name-shaped — few\n // tokens, no digits — so all-caps disclosure prose\n // such as \"SERVICE MARK LICENSE\" doesn't surface\n // \"MARK\" as a person via the corpus.\n if (text.length >= 3 && ALL_UPPER_RE.test(text)) {\n if (\n isAllCapsContextLine(fullText, start) &&\n isAllCapsLineNameShaped(fullText, start)\n ) {\n const titleCased = (text[0] ?? \"\") + text.slice(1).toLowerCase();\n if (isFirstNameToken(titleCased, corpus)) {\n return { text, type: TOKEN_TYPE.NAME, start, end };\n }\n if (isSurnameToken(titleCased, corpus)) {\n return { text, type: TOKEN_TYPE.SURNAME, start, end };\n }\n }\n return { text, type: TOKEN_TYPE.OTHER, start, end };\n }\n\n // Must start with uppercase\n if (!UPPER_START_RE.test(text)) {\n return { text, type: TOKEN_TYPE.OTHER, start, end };\n }\n\n if (isFirstNameToken(text, corpus)) {\n return { text, type: TOKEN_TYPE.NAME, start, end };\n }\n\n if (isSurnameToken(text, corpus)) {\n return { text, type: TOKEN_TYPE.SURNAME, start, end };\n }\n\n // Capitalised word (not in corpus but starts uppercase)\n return {\n text,\n type: TOKEN_TYPE.CAPITALIZED,\n start,\n end,\n };\n};\n\n// ── Chain assembly ───────────────────────────────────\n\n/**\n * Detect person names by looking up tokens against the\n * name corpus, then chaining adjacent name-like tokens.\n *\n * Requires initNameCorpus() to have been called first.\n * If not initialized, returns an empty array.\n *\n * Scoring:\n * TITLE + NAME/SURNAME → 0.95\n * NAME + NAME/SURNAME → 0.9\n * SURNAME + NAME/SURNAME → 0.9\n * NAME + CAPITALIZED → 0.7\n * ABBREVIATION + NAME → 0.7\n * Standalone NAME → 0.5 (low confidence)\n * Standalone SURNAME → skip (too ambiguous)\n */\nexport const detectNameCorpus = (\n fullText: string,\n ctx: PipelineContext = defaultContext,\n): Entity[] => {\n const corpus = getCorpus(ctx);\n if (!corpus) {\n return [];\n }\n\n const words = segmentWords(fullText);\n const tokens = words.map((w) => classifyToken(w, corpus, fullText));\n const entities: Entity[] = [];\n const consumed = new Set<number>();\n\n for (let i = 0; i < tokens.length; i++) {\n if (consumed.has(i)) {\n continue;\n }\n\n const token = tokens[i];\n if (!token) {\n continue;\n }\n\n // Only start chains from TITLE, NAME, SURNAME,\n // or ABBREVIATION\n if (\n token.type !== TOKEN_TYPE.TITLE &&\n token.type !== TOKEN_TYPE.NAME &&\n token.type !== TOKEN_TYPE.SURNAME &&\n token.type !== TOKEN_TYPE.ABBREVIATION\n ) {\n continue;\n }\n\n // Build a chain of adjacent relevant tokens.\n // Max 5 tokens to prevent merging independent names\n // (e.g., \"Jan Novák Pavel Moc\" should be two entities).\n const MAX_CHAIN = 5;\n const chain: ClassifiedToken[] = [token];\n let j = i + 1;\n\n while (j < tokens.length && chain.length < MAX_CHAIN) {\n const next = tokens[j];\n if (!next) {\n break;\n }\n\n // Break chain if there's a newline between tokens\n const prev = chain.at(-1);\n if (prev) {\n const gap = fullText.slice(prev.end, next.start);\n const breaksOnPeriod =\n gap.includes(\".\") && !isInitialContinuationGap(prev.text, gap);\n if (\n gap.includes(\"\\n\") ||\n PERSON_CHAIN_BREAK_RE.test(gap) ||\n breaksOnPeriod\n ) {\n break;\n }\n }\n\n // Only chain NAME, SURNAME, TITLE, ABBREVIATION,\n // CAPITALIZED\n if (\n next.type === TOKEN_TYPE.NAME ||\n next.type === TOKEN_TYPE.SURNAME ||\n next.type === TOKEN_TYPE.TITLE ||\n next.type === TOKEN_TYPE.ABBREVIATION ||\n next.type === TOKEN_TYPE.CAPITALIZED\n ) {\n chain.push(next);\n j++;\n } else {\n break;\n }\n }\n\n // Score the chain\n const hasTitle = chain.some((t) => t.type === TOKEN_TYPE.TITLE);\n const hasCorpusName = chain.some((t) => isCorpusMatch(t.type));\n const hasFirstName = chain.some((t) => t.type === TOKEN_TYPE.NAME);\n const hasAbbreviation = chain.some(\n (t) => t.type === TOKEN_TYPE.ABBREVIATION,\n );\n const corpusCount = chain.filter((t) => isCorpusMatch(t.type)).length;\n const capitalizedCount = chain.filter(\n (t) => t.type === TOKEN_TYPE.CAPITALIZED,\n ).length;\n\n // Determine score based on chain composition\n let score: number;\n\n if (hasTitle && hasCorpusName) {\n // TITLE + NAME/SURNAME → high confidence\n score = 0.95;\n } else if (corpusCount >= 2) {\n // NAME + NAME, NAME + SURNAME, etc. → high confidence\n score = 0.9;\n } else if (hasCorpusName && capitalizedCount > 0) {\n // NAME/SURNAME + CAPITALIZED → medium confidence\n score = 0.7;\n } else if (hasAbbreviation && hasCorpusName) {\n // ABBREVIATION + NAME/SURNAME → medium confidence\n score = 0.7;\n } else if (hasFirstName && chain.length === 1) {\n // Standalone first NAME → low confidence\n // Skip if at sentence start (likely not a name)\n if (isSentenceStart(fullText, token.start)) {\n continue;\n }\n // Standalone all-caps corpus hits are too\n // ambiguous on their own to emit. \"MARK\" inside\n // \"SERVICE MARK LICENSE\" matches the corpus but\n // is plainly a common noun in context; we need\n // chain evidence (another name token, a title,\n // an abbreviation) before we trust an all-caps\n // first-name token as a person.\n const first = chain[0];\n if (first && ALL_UPPER_RE.test(first.text) && first.text.length >= 3) {\n continue;\n }\n score = 0.5;\n } else if (\n !hasFirstName &&\n chain.length === 1 &&\n chain[0]?.type === TOKEN_TYPE.SURNAME\n ) {\n // Standalone SURNAME → skip (too ambiguous alone)\n continue;\n } else if (hasTitle && chain.length === 1) {\n // Standalone TITLE → skip (not a name by itself)\n continue;\n } else {\n // No corpus match in chain → skip\n if (!hasCorpusName) {\n continue;\n }\n score = 0.5;\n }\n\n // Build entity span from first to last token in chain\n const first = chain.at(0);\n const last = chain.at(-1);\n if (!first || !last) {\n continue;\n }\n\n const start = first.start;\n const end = last.end;\n const text = fullText.slice(start, end);\n\n // Mark all chain tokens as consumed\n for (let k = i; k < i + chain.length; k++) {\n consumed.add(k);\n }\n\n entities.push({\n start,\n end,\n label: \"person\",\n text,\n score,\n source: DETECTION_SOURCES.REGEX,\n });\n }\n\n return entities;\n};\n","import type { Entity } from \"../types\";\nimport { DETECTION_SOURCES } from \"../types\";\nimport type { PipelineContext } from \"../context\";\nimport { defaultContext } from \"../context\";\n\n/*\n * Signature-block detector.\n *\n * Recognises the stereotyped shape of legal-document\n * signature blocks and emits high-confidence person\n * spans for the signatory name. Anchors we trust:\n *\n * • \"/s/\" — the canonical \"in lieu of physical\n * signature\" mark used across SEC and EDGAR\n * filings. Whatever follows on the same line (or,\n * when the mark is on its own line, the next\n * non-empty line) is the signer's printed name.\n *\n * • \"Name:\" / \"By:\" labels — explicitly-labelled\n * signature fields. When the value sits on the\n * same line we use it directly; when the label is\n * on its own line we walk forward to the next\n * non-empty non-image line.\n *\n * • \"IN WITNESS WHEREOF\" / \"In Witness Whereof\" —\n * the standard preamble. We walk past the preamble\n * sentence and try the next several short lines\n * until one validates as a name.\n *\n * Heuristics applied to every emission:\n * - Same-line tables (\"/s/ Jane Doe CEO 5/1/24\")\n * split on 3+ whitespace; only the first cell is\n * treated as the signer.\n * - Trailing post-nominal suffixes (\", Jr.\", \", M.D.\")\n * are stripped before validation.\n * - Lowercase name particles (\"van der\", \"de la\",\n * \"von\") are allowed between capitalised tokens.\n * - At least two tokens required; single capitalised\n * words (\"President\", \"Director\") are rejected.\n * - Lines containing legal-form suffixes (LLC, INC.,\n * GMBH, s.r.o., …) are rejected to stop party\n * captions sitting directly above a \"/s/\" mark\n * from being mis-tagged as persons.\n *\n * The detector emits with source TRIGGER (priority 4)\n * so signature-anchored detections outrank deny-list\n * heuristics that may otherwise mis-classify part of\n * the name (\"Sean D Reilly\" — deny-list previously\n * caught only \"Reilly\").\n */\n\nconst SLASH_S_RE = /\\/s\\/[ \\t]*/g;\n// Match `Name:` / `By:` anywhere on a line (anchored on\n// a word boundary), not just at line start. Capturing\n// is non-greedy and bounded by either the next column\n// boundary (3+ spaces / tab) or a newline, so two-column\n// signature blocks (\"Name: Priya Ramanathan Name:\n// Jonathan H. Whitaker\") emit a span per signer.\nconst LABELLED_NAME_RE =\n /\\b(?:By|Name)[ \\t]*:[ \\t]*(?:\\/s\\/[ \\t]+)?([^\\n]*?)(?=\\s{3,}|[\\t]|\\n|$)/gi;\nconst WITNESS_ANCHOR_RE = /\\bIN WITNESS WHEREOF\\b[^\\n]*\\n/gi;\n\n// Tokens allowed as lowercase name particles between\n// two cap tokens (\"Juan de la Cruz\", \"Hans van der\n// Meer\", \"Vincent van Gogh\", \"Jean d'Arc\"). Restricted\n// to a curated list of common particles so we don't\n// promote arbitrary lowercase prose into name shape.\nconst NAME_PARTICLE =\n \"(?:de|del|della|der|den|di|du|el|la|le|van|von|y|zu|af|ben|bin|al|d'|d’)\";\nconst CAP_TOKEN = \"\\\\p{Lu}[\\\\p{L}\\\\p{M}.'\\\\-]{0,30}\";\n// A name: starts with a cap token, then 1-4 more\n// tokens which may be cap tokens or lowercase\n// particles. Requires ≥2 tokens total so role titles\n// like \"President\" don't qualify.\nconst NAME_SHAPE_RE = new RegExp(\n `^${CAP_TOKEN}(?:[ \\\\t]+(?:${NAME_PARTICLE}|${CAP_TOKEN})){1,4}$`,\n \"u\",\n);\nconst MAX_NAME_LEN = 60;\n\n// Trailing post-nominal suffixes (\"John Smith, Jr.\",\n// \"Jane Doe, M.D.\", \"Alex Park, Esq.\"). Stripped\n// before validation so the comma doesn't poison the\n// name-shape check.\nconst POST_NOMINAL_SUFFIX_RE =\n /,\\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;\n\n// 3+ whitespace or any tab marks a column boundary in\n// signature tables (\"/s/ Jane Doe CEO 5/1/24\").\nconst COLUMN_SEPARATOR_RE = /\\s{3,}|\\t+/;\n\n// Quick recognition of common legal-form suffixes so we\n// can refuse to emit a previous-line \"name\" when that\n// line is actually a party caption. Case-insensitive;\n// matches in any token position.\nconst ORG_SUFFIX_RE =\n /\\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;\n\n// Lines that look like image stubs or section markers\n// — skip them when walking forward from a witness\n// anchor.\nconst IMAGE_STUB_RE = /^(?:\\[?img|\\[image|\\[logo|\\(logo\\))/i;\n\nconst normaliseCandidate = (text: string): string => {\n let candidate = text.trim();\n candidate = candidate.replace(POST_NOMINAL_SUFFIX_RE, \"\").trim();\n const cells = candidate.split(COLUMN_SEPARATOR_RE);\n const firstCell = cells[0]?.trim() ?? candidate;\n return firstCell;\n};\n\nconst isNameShape = (text: string): boolean => {\n if (text.length === 0 || text.length > MAX_NAME_LEN) return false;\n if (text.length < 3) return false;\n if (!NAME_SHAPE_RE.test(text)) return false;\n return true;\n};\n\nconst findLineEnd = (text: string, pos: number): number => {\n const idx = text.indexOf(\"\\n\", pos);\n return idx === -1 ? text.length : idx;\n};\n\n// Try to extract a normalised name from a span and emit\n// it as a person entity. Returns true when an entity was\n// emitted. Skips the emission if the source span looks\n// like an organisation caption (contains a legal-form\n// suffix) — these should be claimed by the legal-form\n// detector, not the signature detector.\nconst tryEmit = (\n results: Entity[],\n fullText: string,\n start: number,\n end: number,\n score: number,\n): boolean => {\n const raw = fullText.slice(start, end);\n if (ORG_SUFFIX_RE.test(raw)) return false;\n const candidate = normaliseCandidate(raw);\n if (!isNameShape(candidate)) return false;\n // Re-locate the candidate inside the raw slice so the\n // entity coordinates are tight.\n const offset = raw.indexOf(candidate);\n if (offset < 0) return false;\n const absStart = start + offset;\n results.push({\n start: absStart,\n end: absStart + candidate.length,\n label: \"person\",\n text: candidate,\n score,\n source: DETECTION_SOURCES.TRIGGER,\n });\n return true;\n};\n\n// Walk forward up to `maxLines` non-empty non-image\n// lines and emit the first one that validates as a\n// name. Codex P2: emits previously stopped at the\n// first non-empty line even if validation failed,\n// which prevented witness-block scans from reaching\n// the printed signer through intervening \"By:\" /\n// \"COMPANY:\" lines.\nconst tryEmitForwardLines = (\n results: Entity[],\n fullText: string,\n fromPos: number,\n maxLines: number,\n score: number,\n): boolean => {\n let pos = fromPos;\n for (let i = 0; i < maxLines; i++) {\n if (pos >= fullText.length) return false;\n const lineEnd = findLineEnd(fullText, pos);\n const line = fullText.slice(pos, lineEnd).trim();\n if (line.length > 0 && !IMAGE_STUB_RE.test(line)) {\n if (tryEmit(results, fullText, pos, lineEnd, score)) return true;\n }\n pos = lineEnd + 1;\n }\n return false;\n};\n\nconst findPrevLine = (\n fullText: string,\n pos: number,\n): { lineStart: number; lineEnd: number } | null => {\n let cursor = pos - 1;\n while (cursor >= 0 && fullText.charAt(cursor) !== \"\\n\") cursor--;\n while (cursor >= 0) {\n let lineStart = cursor;\n while (lineStart > 0 && fullText.charAt(lineStart - 1) !== \"\\n\") {\n lineStart -= 1;\n }\n const lineEnd = cursor;\n const line = fullText.slice(lineStart, lineEnd).trim();\n if (line.length > 0 && !IMAGE_STUB_RE.test(line)) {\n return { lineStart, lineEnd };\n }\n cursor = lineStart - 1;\n }\n return null;\n};\n\nexport const detectSignatures = (\n fullText: string,\n _ctx: PipelineContext = defaultContext,\n): Entity[] => {\n const results: Entity[] = [];\n\n // Pass 1: `/s/` marks.\n SLASH_S_RE.lastIndex = 0;\n for (\n let m = SLASH_S_RE.exec(fullText);\n m !== null;\n m = SLASH_S_RE.exec(fullText)\n ) {\n const afterMark = m.index + m[0].length;\n const lineEnd = findLineEnd(fullText, afterMark);\n const sameLine = fullText.slice(afterMark, lineEnd).trim();\n if (sameLine.length > 0) {\n // Column-aware: in a tabular signature row\n // (\"/s/ Jane Doe Chief Executive Officer\n // 5/1/2024\"), only the first cell after \"/s/\" is\n // the signer; subsequent cells are title/date.\n const rawSlice = fullText.slice(afterMark, lineEnd);\n const cells = rawSlice.split(COLUMN_SEPARATOR_RE);\n const firstCell = cells[0] ?? \"\";\n if (firstCell.trim().length > 0) {\n const firstCellEnd = afterMark + firstCell.length;\n tryEmit(results, fullText, afterMark, firstCellEnd, 0.95);\n }\n } else {\n tryEmitForwardLines(results, fullText, lineEnd + 1, 4, 0.9);\n }\n\n // Previous-line lookback. EDGAR documents often\n // print the signatory's name in ALL CAPS on its\n // own line directly above the \"/s/\" mark\n // (\"ELON R. MUSK\\n/s/ Elon R. Musk\"). Skip the\n // lookback when the previous line is a party\n // caption (contains a legal-form suffix) so a\n // line like \"TWITTER, INC.\\n/s/ Jane Doe\" doesn't\n // mis-emit the company name as a person.\n const prev = findPrevLine(fullText, m.index);\n if (prev) {\n tryEmit(results, fullText, prev.lineStart, prev.lineEnd, 0.85);\n }\n }\n\n // Pass 2: \"Name:\" / \"By:\" labels.\n LABELLED_NAME_RE.lastIndex = 0;\n for (\n let m = LABELLED_NAME_RE.exec(fullText);\n m !== null;\n m = LABELLED_NAME_RE.exec(fullText)\n ) {\n const value = m[1];\n if (value === undefined) continue;\n const valueStart = m.index + m[0].length - value.length;\n const valueEnd = valueStart + value.length;\n const trimmedValue = value.trim();\n if (trimmedValue.length === 0) {\n // Label sits on its own line — walk forward to the\n // next non-empty line for the printed name.\n tryEmitForwardLines(results, fullText, valueEnd + 1, 3, 0.9);\n continue;\n }\n tryEmit(results, fullText, valueStart, valueEnd, 0.95);\n }\n\n // Pass 3: \"IN WITNESS WHEREOF\" preamble.\n WITNESS_ANCHOR_RE.lastIndex = 0;\n for (\n let m = WITNESS_ANCHOR_RE.exec(fullText);\n m !== null;\n m = WITNESS_ANCHOR_RE.exec(fullText)\n ) {\n const search = fullText.slice(m.index, m.index + 600);\n // Permissive sentence terminator — `.`, `:`, `;`,\n // or just a newline closes the preamble. Some\n // contracts run the preamble straight into the\n // signature block with only a paragraph break.\n const sentenceEnd = /[.:;]\\s*\\n|\\n\\s*\\n/.exec(search);\n if (!sentenceEnd) continue;\n const scanFrom = m.index + sentenceEnd.index + sentenceEnd[0].length;\n tryEmitForwardLines(results, fullText, scanFrom, 6, 0.85);\n }\n\n return results;\n};\n","/**\n * Academic and professional title prefixes.\n * Plain text; the detector auto-escapes for regex.\n * Sorted longest-first at build time.\n */\nexport const TITLE_PREFIXES = [\n // Czech/Slovak pre-nominal\n \"Ing.\",\n \"Mgr.\",\n \"MgA.\",\n \"Bc.\",\n \"BcA.\",\n \"JUDr.\",\n \"MUDr.\",\n \"MVDr.\",\n \"MDDr.\",\n \"PhDr.\",\n \"RNDr.\",\n \"PaedDr.\",\n \"ThDr.\",\n \"ThLic.\",\n \"ICDr.\",\n \"RSDr.\",\n \"PharmDr.\",\n \"artD.\",\n \"akad.\",\n \"doc.\",\n \"prof.\",\n\n // Professor variants (AT/DE)\n \"ao. Univ.-Prof.\",\n \"o. Univ.-Prof.\",\n \"Univ.-Prof.\",\n \"Hon.-Prof.\",\n \"em. Prof.\",\n\n // German doctoral (compound before simple)\n \"Dr. med. dent.\",\n \"Dr. med. vet.\",\n \"Dr. med.\",\n \"Dr. rer. nat.\",\n \"Dr. rer. soc.\",\n \"Dr. rer. pol.\",\n \"Dr. sc. tech.\",\n \"Dr. sc. nat.\",\n \"Dr. sc. hum.\",\n \"Dr. iur.\",\n \"Dr. jur.\",\n \"Dr. theol.\",\n \"Dr. oec.\",\n \"Dr. techn.\",\n \"Dr. h. c.\",\n \"Dr. phil.\",\n \"Dr.-Ing.\",\n \"Dr. Ing.\",\n \"Dr.\",\n\n // German Diplom variants (longest first)\n \"Dipl.-Wirt.-Ing.\",\n \"Dipl.-Betriebsw.\",\n \"Dipl.-Inform.\",\n \"Dipl.-Volksw.\",\n \"Dipl.-Psych.\",\n \"Dipl.-Phys.\",\n \"Dipl.-Chem.\",\n \"Dipl.-Biol.\",\n \"Dipl.-Math.\",\n \"Dipl.-Päd.\",\n \"Dipl.-Soz.\",\n \"Dipl.-Kfm.\",\n \"Dipl.-Jur.\",\n \"Dipl. Ing.\",\n \"Dipl.-Ing.\",\n\n // Austrian Mag/Bakk variants\n \"Mag. rer. soc. oec.\",\n \"Mag. rer. nat.\",\n \"Mag. phil.\",\n \"Mag. iur.\",\n \"Mag. arch.\",\n \"Mag. pharm.\",\n \"Mag. (FH)\",\n \"Mag.\",\n \"Bakk. rer. nat.\",\n \"Bakk. techn.\",\n \"Bakk. phil.\",\n \"Bakk.\",\n\n // Swiss Lic variants\n \"Lic. phil.\",\n \"Lic. iur.\",\n \"Lic. oec.\",\n \"Lic. theol.\",\n \"Lic.\",\n\n // Other German/Austrian\n \"Priv.-Doz.\",\n \"PD\",\n \"RA\",\n] as const;\n\n/**\n * Courtesy/honorific titles that precede a person's\n * name. Sorted alphabetically. The detector escapes\n * dots and adds \\b for entries in HONORIFIC_BOUNDARY.\n */\nexport const HONORIFICS = [\n \"Avv.\",\n \"Dame\",\n \"Doamna\",\n \"Domnul\",\n \"Don\",\n \"Doña\",\n \"Dott.\",\n \"Judge\",\n \"Justice\",\n \"Lady\",\n \"Lord\",\n \"M.\",\n \"Madame\",\n \"Mademoiselle\",\n \"Maître\",\n \"Me\",\n \"Messrs\",\n \"Miss\",\n \"Mlle\",\n \"Mme\",\n \"Monsieur\",\n \"Mr\",\n \"Mrs\",\n \"Ms\",\n \"Pr\",\n \"Pr.\",\n \"President\",\n \"Señor\",\n \"Señora\",\n \"Sig.\",\n \"Sig.ra\",\n \"Signor\",\n \"Signora\",\n \"Signorina\",\n \"Sir\",\n \"Sr.\",\n \"Sra.\",\n] as const;\n\n/**\n * Honorifics that need \\b word-boundary anchors\n * (short or common words that could match mid-word).\n */\nexport const HONORIFIC_BOUNDARY = new Set([\n \"Don\",\n \"Doña\",\n \"M.\",\n \"Me\",\n \"Pr\",\n \"Pr.\",\n \"Señor\",\n \"Señora\",\n]);\n\n/**\n * Honorifics that are abbreviations: a dot after them is an\n * abbreviation dot (same sentence), so the detector keeps an\n * optional `.` between the title and the name (\"Mr. Smith\").\n * Every other honorific is a full word — a trailing dot ends a\n * sentence, so the detector must NOT consume it (otherwise a span\n * like \"President. The Employee\" crosses the sentence boundary).\n *\n * Maintained explicitly, NOT derived from \"ends in a dot\": \"Mr\" is\n * an abbreviation written without a dot, and \"Lord\" is a full word.\n */\nexport const HONORIFIC_ABBREVIATION = new Set([\n \"Avv.\",\n \"Dott.\",\n \"M.\",\n \"Me\",\n \"Messrs\",\n \"Mlle\",\n \"Mme\",\n \"Mr\",\n \"Mrs\",\n \"Ms\",\n \"Pr\",\n \"Pr.\",\n \"Sig.\",\n \"Sig.ra\",\n \"Sr.\",\n \"Sra.\",\n]);\n\n/**\n * Post-nominal degrees (comma or space separated after name).\n * Plain text; the detector auto-escapes for regex.\n */\nexport const POST_NOMINALS = [\n \"Ph.D.\",\n \"Ph.D\",\n \"CSc.\",\n \"DrSc.\",\n \"ArtD.\",\n \"D.Phil.\",\n \"DPhil.\",\n \"MPhil.\",\n \"MBA\",\n \"MPA\",\n \"LL.M.\",\n \"LL.B.\",\n \"M.Sc.\",\n \"B.Sc.\",\n \"MSc.\",\n \"BSc.\",\n \"M.Eng.\",\n \"B.Eng.\",\n \"M.A.\",\n \"B.A.\",\n \"JCD\",\n \"JD\",\n \"DiS.\",\n \"ACCA\",\n \"FCCA\",\n \"CIPM\",\n \"CIPT\",\n \"CIPP/E\",\n \"CIPP\",\n\n // UK senior barrister rank (King's/Queen's Counsel).\n // Bare-letter form (no comma required), so the\n // detector treats it like Ph.D./MBA: name + space + KC.\n \"KC\",\n \"QC\",\n] as const;\n","","import type { Match } from \"@stll/text-search\";\nimport type { Validator } from \"@stll/stdnum\";\nimport {\n at,\n au,\n be,\n bg,\n br,\n ch,\n cn,\n cz,\n cy,\n de,\n dk,\n ee,\n es,\n fi,\n fr,\n gb,\n gr,\n hr,\n hu,\n ie,\n it,\n lt,\n lu,\n lv,\n mt,\n nl,\n no,\n pl,\n pt,\n ro,\n se,\n si,\n sk,\n us,\n} from \"@stll/stdnum\";\nimport { toRegex } from \"@stll/stdnum/patterns\";\n\nimport {\n HONORIFIC_ABBREVIATION,\n HONORIFIC_BOUNDARY,\n HONORIFICS,\n POST_NOMINALS,\n TITLE_PREFIXES,\n} from \"../config/titles\";\nimport amountWordsConfig from \"../data/amount-words.json\";\nimport { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\nimport { DASH, DASH_INNER } from \"../util/char-groups\";\n\nconst MIN_PHONE_LENGTH = 7;\nconst MIN_MONTH_NAME_LENGTH = 3;\n\n// ── Shared helpers ──────────────────────────────────\n\nconst escapeTitle = (title: string): string =>\n title\n // eslint-disable-next-line no-useless-escape\n .replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\s+/g, \"\\\\s*\");\n\n/** Escape for use inside a regex alternation. */\nconst escapeRegex = (s: string): string =>\n // eslint-disable-next-line no-useless-escape\n s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\nconst escapeRegexPhrase = (s: string): string =>\n escapeRegex(s.trim()).replace(/\\s+/g, \"[^\\\\S\\\\n\\\\t]+\");\n\n/** Escape for use inside a regex character class. */\nconst escapeCharClass = (s: string): string => s.replace(/[\\]\\\\^-]/g, \"\\\\$&\");\n\nconst toSortedAlternation = (values: readonly string[]): string =>\n [\n ...new Set(\n values.map(escapeRegexPhrase).filter((value) => value.length > 0),\n ),\n ]\n .toSorted((a, b) => b.length - a.length)\n .join(\"|\");\n\nconst TITLE_PREFIX = TITLE_PREFIXES.toSorted((a, b) => b.length - a.length)\n .map(escapeTitle)\n .join(\"|\");\n\nconst POST_NOMINAL = POST_NOMINALS.toSorted((a, b) => b.length - a.length)\n .map(escapeTitle)\n .join(\"|\");\n\n// Unicode property classes keep the name-word pattern\n// language-agnostic: any uppercase letter followed by\n// lowercase letters works for cs/de/fr/it/es/sk and any\n// future language with cased scripts. The Rust regex\n// engine downstream (@stll/text-search) supports \\p{Lu}\n// and \\p{Ll} natively.\nconst NAME_WORD = `\\\\p{Lu}\\\\p{Ll}+`;\n\nconst PARTICLE =\n `(?:van der|van den|de la|della|` +\n `von|van|dos|ibn|ben|bin|del|zum|zur|ten|ter|` +\n `da|de|di|al|el|le|la|zu|af|av)`;\n\n// Non-newline whitespace. Tabs are admitted because\n// DOCX exports routinely place a TAB between an\n// academic title and the following name (table-cell\n// layouts like \"Ing.\\tStanislav Braňka\"); newlines\n// are not, so spans cannot bleed across paragraphs.\nconst SP = \"[^\\\\S\\\\n]\";\n\n/** Honorific alternation built from titles.ts config. Sorted\n * longest-first so e.g. \"Sig.ra\" wins over \"Sig.\". */\nconst buildHonorificAlt = (entries: readonly string[]): string =>\n [...entries]\n .toSorted((a, b) => b.length - a.length)\n .map((h) => {\n const escaped = escapeRegex(h);\n return HONORIFIC_BOUNDARY.has(h) ? `\\\\b${escaped}` : escaped;\n })\n .join(\"|\");\n\n// Abbreviation honorifics (\"Mr\", \"Sr.\") may be followed by an\n// abbreviation dot; full-word honorifics (\"President\", \"Lord\")\n// may not, so a sentence-ending period after them is not consumed\n// and the person span stops at the sentence boundary.\nconst HONORIFIC_ABBREV_ALT = buildHonorificAlt(\n HONORIFICS.filter((h) => HONORIFIC_ABBREVIATION.has(h)),\n);\nconst HONORIFIC_FULLWORD_ALT = buildHonorificAlt(\n HONORIFICS.filter((h) => !HONORIFIC_ABBREVIATION.has(h)),\n);\n\n// ── Pattern definitions ─────────────────────────────\n\nexport type RegexMeta = {\n label: string;\n score: number;\n sourceDetail?: Entity[\"sourceDetail\"];\n /** Post-match stdnum validator for confirmation. */\n validator?: Validator;\n};\n\ntype RegexDef = {\n pattern: string;\n label: string;\n score: number;\n validator?: Validator;\n};\n\ntype AmountWordsConfig = {\n percentages?: Array<{\n lang: string;\n keywords: string[];\n ones: string[];\n teens: string[];\n tens: string[];\n standalone?: string[];\n allowSpaceCompoundSeparator?: boolean;\n }>;\n magnitudeSuffixes?: Array<{\n lang: string;\n words?: string[];\n abbreviationsCaseInsensitive?: string[];\n abbreviationsCaseSensitive?: string[];\n }>;\n shareQuantityTerms?: Array<{\n lang: string;\n modifiers?: string[];\n nouns: string[];\n }>;\n};\n\nconst AMOUNT_WORDS = amountWordsConfig as AmountWordsConfig;\n\n// ── stdnum validator entries ────────────────────────\n// Each entry pairs a @stll/stdnum validator with a\n// label and confidence score. The pattern derived via\n// toRegex(validator).source is used as the regex; the\n// validator itself is stored in META for post-match\n// confirmation (see processRegexMatches).\n\ntype StdnumEntry = {\n validator: Validator;\n label: string;\n score: number;\n pattern: string;\n};\n\nconst toEntry = (\n validator: Validator,\n label: string,\n score: number,\n): StdnumEntry | null => {\n const pattern = toRegex(validator).source;\n if (!pattern) return null;\n return {\n validator,\n label,\n score,\n pattern,\n };\n};\n\n/**\n * Stdnum validators for national/company IDs.\n *\n * Selection criteria: only patterns specific enough\n * to avoid excessive false positives (country-prefixed\n * VAT numbers, structured personal IDs). Generic\n * digit-only patterns (e.g. \\d{8}) are excluded unless\n * the validator's checksum is strong enough to filter.\n */\nconst STDNUM_ENTRIES: readonly StdnumEntry[] = [\n // ── Original PR #28 patterns (were 15-21) ────────\n toEntry(hu.vat, \"tax identification number\", 0.95),\n toEntry(it.codiceFiscale, \"national identification number\", 0.95),\n // es.dni / es.nie omitted: stdnum patterns are over-fit to\n // the spec letter (`Q` / `X`) and miss real-world prefixes;\n // covered by the format-level ES_DNI / ES_NIE regex below.\n toEntry(se.personnummer, \"national identification number\", 0.9),\n toEntry(ro.cnp, \"national identification number\", 0.95),\n toEntry(fr.nir, \"social security number\", 0.9),\n\n // ── CZ validators ────────────────────────────────\n toEntry(cz.dic, \"tax identification number\", 0.95),\n // cz.ico and cz.rc omitted: cz.ico is \\d{8} (too\n // generic), cz.rc is \\d{6}/\\d{3,4} (handled by\n // pattern 7: czech birth number)\n\n // ── DE validators ────────────────────────────────\n toEntry(de.vat, \"tax identification number\", 0.95),\n toEntry(de.idnr, \"tax identification number\", 0.9),\n toEntry(de.stnr, \"tax identification number\", 0.9),\n toEntry(de.svnr, \"social security number\", 0.9),\n\n // ── PL validators ────────────────────────────────\n toEntry(pl.nip, \"tax identification number\", 0.95),\n toEntry(pl.pesel, \"national identification number\", 0.9),\n // pl.regon omitted: \\d{9,14} too generic\n\n // ── GB validators ────────────────────────────────\n toEntry(gb.vat, \"tax identification number\", 0.95),\n toEntry(gb.nino, \"social security number\", 0.95),\n // gb.utr omitted: \\d{10} too generic\n\n // ── AT validators ────────────────────────────────\n toEntry(at.uid, \"tax identification number\", 0.95),\n toEntry(at.tin, \"tax identification number\", 0.9),\n toEntry(at.businessid, \"registration number\", 0.95),\n\n // ── CH validators ────────────────────────────────\n // Swiss UID: CHE + 9 digits with checksum. It is a\n // company identifier; VAT-specific usage reuses the\n // same base number with tax suffixes, so the bare\n // shape is labelled as registration number.\n toEntry(ch.uid, \"registration number\", 0.95),\n\n // ── AU validators ────────────────────────────────\n toEntry(au.abn, \"tax identification number\", 0.9),\n toEntry(au.acn, \"registration number\", 0.9),\n\n // ── BE validators ────────────────────────────────\n toEntry(be.vat, \"tax identification number\", 0.95),\n toEntry(be.nn, \"national identification number\", 0.9),\n\n // ── NL validators ────────────────────────────────\n toEntry(nl.vat, \"tax identification number\", 0.95),\n // nl.bsn omitted: \\d{9} too generic\n\n // ── NO validators ────────────────────────────────\n toEntry(no.orgnr, \"registration number\", 0.9),\n toEntry(no.mva, \"tax identification number\", 0.95),\n\n // ── DK validators ────────────────────────────────\n toEntry(dk.vat, \"tax identification number\", 0.95),\n toEntry(dk.cpr, \"national identification number\", 0.9),\n\n // ── FI validators ────────────────────────────────\n toEntry(fi.vat, \"tax identification number\", 0.95),\n toEntry(fi.hetu, \"national identification number\", 0.95),\n toEntry(fi.ytunnus, \"registration number\", 0.9),\n\n // ── BG validators ────────────────────────────────\n toEntry(bg.vat, \"tax identification number\", 0.95),\n\n // ── SK validators ────────────────────────────────\n toEntry(sk.dic, \"tax identification number\", 0.95),\n // sk.ico: \\d{8} too generic; sk.rc overlaps with\n // czech birth number pattern\n\n // ── ES additional validators ─────────────────────\n // es.cif omitted: stdnum's candidatePattern is over-fit\n // to the spec letter and misses real-world prefixes;\n // covered by the format-level ES_CIF regex below.\n toEntry(es.vat, \"tax identification number\", 0.95),\n toEntry(es.nss, \"social security number\", 0.9),\n\n // ── FR additional validators ─────────────────────\n toEntry(fr.tva, \"tax identification number\", 0.95),\n toEntry(fr.siren, \"registration number\", 0.9),\n toEntry(fr.siret, \"registration number\", 0.9),\n\n // ── IT additional validators ─────────────────────\n toEntry(it.iva, \"tax identification number\", 0.95),\n\n // ── IE validators ────────────────────────────────\n toEntry(ie.vat, \"tax identification number\", 0.95),\n toEntry(ie.pps, \"national identification number\", 0.9),\n\n // ── PT validators ────────────────────────────────\n toEntry(pt.vat, \"tax identification number\", 0.95),\n toEntry(pt.cc, \"national identification number\", 0.9),\n\n // ── RO additional validators ─────────────────────\n toEntry(ro.vat, \"tax identification number\", 0.95),\n\n // ── GR validators ────────────────────────────────\n toEntry(gr.vat, \"tax identification number\", 0.95),\n\n // ── HR validators ────────────────────────────────\n toEntry(hr.vat, \"tax identification number\", 0.95),\n\n // ── SI validators ────────────────────────────────\n toEntry(si.vat, \"tax identification number\", 0.95),\n\n // ── LT validators ────────────────────────────────\n toEntry(lt.vat, \"tax identification number\", 0.95),\n toEntry(lt.asmens, \"national identification number\", 0.9),\n\n // ── LV validators ────────────────────────────────\n toEntry(lv.vat, \"tax identification number\", 0.95),\n\n // ── EE validators ────────────────────────────────\n toEntry(ee.vat, \"tax identification number\", 0.95),\n toEntry(ee.ik, \"national identification number\", 0.9),\n\n // ── CY validators ────────────────────────────────\n toEntry(cy.vat, \"tax identification number\", 0.95),\n\n // ── MT validators ────────────────────────────────\n toEntry(mt.vat, \"tax identification number\", 0.95),\n\n // ── LU validators ────────────────────────────────\n toEntry(lu.vat, \"tax identification number\", 0.95),\n\n // ── US validators ────────────────────────────────\n toEntry(us.ein, \"tax identification number\", 0.9),\n\n // ── BR validators ────────────────────────────────\n // CPF (personal tax ID, 11 digits, checksum). Higher\n // score than the generic phone patterns so the\n // overlap resolver prefers the tax-ID label.\n toEntry(br.cpf, \"tax identification number\", 0.95),\n toEntry(br.cnpj, \"tax identification number\", 0.95),\n\n // ── CN validators ────────────────────────────────\n // RIC (Resident Identity Card, 18-digit modern form:\n // region + YYYYMMDD + sequence + MOD 11-2 check digit,\n // last position may be `X`). The pattern is tightened\n // to a digit-only `\\d{17}[\\dX]` shape so it can't\n // match alphanumeric blobs that share the length\n // bucket; the validator then enforces the embedded\n // birth date and the checksum.\n //\n // The legacy 15-digit form is NOT covered: its bare\n // `\\d{15}` shape is shadowed by the `fr.nir` pattern\n // (also 15 digits) in the unified text-search engine,\n // which returns only one match per position and picks\n // the earlier-registered pattern. The modern 18-digit\n // form has dominated CN issuance since 1999, so the\n // gap is theoretical for current corpora.\n // Lookbehind/lookahead use an ASCII identifier class rather than\n // `\\w`: the text-search regex backend treats `\\w` as Unicode word\n // chars, which would have CJK label prefixes (`身份证号120…`) satisfy\n // the negative boundary and block matches in native-language\n // contexts. Restricting the boundary to ASCII identifier chars\n // still rejects the cases the boundary exists for — order/account\n // numbers (`Order 1201…`, `ID-1201…`).\n //\n // The check-digit class accepts both `X` and `x`: real-world IDs\n // are commonly written with the lowercase variant, and the stdnum\n // validator's compact step normalises the case before checksum.\n {\n validator: cn.ric,\n label: \"national identification number\",\n score: 0.95,\n pattern: \"(?<![A-Za-z0-9_])\\\\d{17}[\\\\dXx](?![A-Za-z0-9_])\",\n },\n].filter((e): e is StdnumEntry => e !== null);\n\n// ── Named pattern definitions ────────────────────────\n\nconst TITLED_PERSON: RegexDef = {\n pattern:\n `(?:${TITLE_PREFIX})` +\n `(?:${SP}+(?:${TITLE_PREFIX}))*` +\n `${SP}+` +\n `(?:${NAME_WORD})` +\n `(?:${SP}{1,4}(?:${PARTICLE}${SP}+)?` +\n `${NAME_WORD}){1,3}` +\n `(?:,?${SP}+(?:${POST_NOMINAL})(?:,?${SP}+(?:${POST_NOMINAL}))*)?`,\n label: \"person\",\n score: 0.95,\n};\n\nconst HONORIFIC_PERSON: RegexDef = {\n pattern:\n `(?:(?:${HONORIFIC_ABBREV_ALT})\\\\.?|(?:${HONORIFIC_FULLWORD_ALT}))` +\n `${SP}+${NAME_WORD}` +\n `(?:(?:${SP}|-){1,2}(?:${PARTICLE}${SP}+)?` +\n `${NAME_WORD}){0,3}` +\n `(?:${SP}+(?:QC|KC|SC|LJ|AG))?`,\n label: \"person\",\n score: 0.95,\n};\n\n// Bare post-nominal anchor: a multi-word capitalised\n// name followed by a UK senior-barrister rank\n// (KC/QC). Picks up \"John Smith KC\" /\n// \"Jane Doe-Robinson QC\" without requiring a title\n// prefix or honorific. Other post-nominals (Ph.D.,\n// MBA, …) are intentionally NOT in this alternation\n// — they over-fire on non-name patterns. KC/QC are\n// safe because the two-letter rank only ever follows\n// a real person name in UK prose.\nconst POSTNOMINAL_PERSON: RegexDef = {\n pattern:\n `${NAME_WORD}` +\n `(?:(?:${SP}|-){1,2}(?:${PARTICLE}${SP}+)?` +\n `${NAME_WORD}){1,3}` +\n // Either `Name KC` or `Name, KC` — UK convention\n // varies between Bar Council style (no comma) and\n // older legal-citation style (with comma).\n `,?${SP}+(?:KC|QC)\\\\b`,\n label: \"person\",\n score: 0.95,\n};\n\nconst IBAN: RegexDef = {\n pattern:\n `\\\\b[A-Z]{2}\\\\d{2}\\\\s?[\\\\dA-Z]{4}\\\\s?[\\\\dA-Z]{4}` +\n `\\\\s?[\\\\dA-Z]{4}\\\\s?[\\\\dA-Z]{4}` +\n `\\\\s?[\\\\dA-Z]{0,14}\\\\b`,\n label: \"iban\",\n score: 1,\n};\n\nconst EMAIL: RegexDef = {\n pattern: `\\\\b[\\\\w.+\\\\-]+@[\\\\w\\\\-]+(?:\\\\.[\\\\w\\\\-]+)+\\\\b`,\n label: \"email address\",\n score: 1,\n};\n\n// [^\\S\\n] instead of \\s: separators must not\n// match newlines (prevents cross-line bleeding).\nconst INTL_PHONE: RegexDef = {\n pattern:\n `\\\\+\\\\d{1,3}(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\(?\\\\d{2,4}\\\\)?` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{2,4}` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{0,4}\\\\b`,\n label: \"phone number\",\n score: 1,\n};\n\n// Czech phone numbers: mobiles start with 6/7,\n// landlines with 2-5. Restrict to [2-7] but require\n// the full 9-digit pattern to avoid matching monetary\n// amounts. The negative lookahead prevents bank\n// account patterns (digits/digits).\nconst CZ_PHONE: RegexDef = {\n pattern:\n `\\\\b[2-7]\\\\d{2}(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}` +\n `(?!(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d*/\\\\d)` +\n `(?![^\\\\S\\\\n]*(?:Kč|,-|korun|EUR|USD|€|\\\\$))\\\\b`,\n label: \"phone number\",\n score: 0.85,\n};\n\n/**\n * Phone numbers prefixed with \"tel.:\" or \"telefon:\".\n * Captures the number after the prefix, including\n * optional international code (+420).\n */\nconst TEL_PREFIX_PHONE: RegexDef = {\n pattern:\n `(?:\\\\b[Tt]el(?:efon)?\\\\.?\\\\s*:?\\\\s*)` +\n `(?:\\\\+?\\\\d{1,3}[^\\\\S\\\\n]?)?` +\n `\\\\d{3}(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}\\\\b`,\n label: \"phone number\",\n score: 0.95,\n};\n\n/**\n * US phone numbers in the (NNN) NNN-NNNN form. Dominant\n * shape in US notice blocks (\"(212) 735-3000\"); not\n * covered by INTL_PHONE (which requires a leading `+`)\n * or TEL_PREFIX_PHONE (which requires a `tel.:` label).\n *\n * The parenthesised area code is the constraint that\n * keeps this from matching random digit clusters —\n * `(212) 555-1212` looks like nothing else in a contract.\n * Score below INTL_PHONE (1) and TEL_PREFIX_PHONE (0.95)\n * so labelled / fully-qualified forms still win the\n * overlap resolver when both fire on the same span.\n */\nconst US_PAREN_PHONE: RegexDef = {\n pattern:\n `\\\\(\\\\d{3}\\\\)(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}` + `(?:[^\\\\S\\\\n]|[.\\\\-])\\\\d{4}\\\\b`,\n label: \"phone number\",\n score: 0.9,\n};\n\nconst CREDIT_CARD: RegexDef = {\n pattern:\n `\\\\b(?:4\\\\d{3}|5[1-5]\\\\d{2}|3[47]\\\\d{2})` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{4}(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{4}` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{2,4}\\\\b`,\n label: \"credit card number\",\n score: 1,\n};\n\nconst CZ_BIRTH_NUMBER: RegexDef = {\n pattern: `\\\\b\\\\d{6}/\\\\d{3,4}\\\\b`,\n label: \"birth number\",\n score: 1,\n validator: cz.rc,\n};\n\n// Czech commercial-register reference. Every Czech\n// legal entity in the public registry is uniquely\n// identified by a registry section letter (\"oddíl X\")\n// plus an insert number (\"vložka NNN\"). The full phrase\n// uniquely identifies the company, so we emit it as a\n// single registration-number entity rather than only\n// capturing the trailing digits.\n//\n// Tolerances:\n// - case-insensitive \"oddíl\" / \"vložka\";\n// - optional whitespace around comma and after each\n// keyword (DOCX exports add NBSPs and double\n// spaces);\n// - section letter is a single A-Z; insert number is\n// a 1-6 digit integer.\nconst CZ_COMMERCIAL_REGISTER: RegexDef = {\n pattern:\n `(?i)\\\\boddíl[^\\\\S\\\\n]+[A-Z]` +\n `[^\\\\S\\\\n]*,[^\\\\S\\\\n]*` +\n `vložka[^\\\\S\\\\n]+\\\\d{1,6}\\\\b`,\n label: \"registration number\",\n score: 0.95,\n};\n\nconst DATE_NUMERIC: RegexDef = {\n pattern:\n `\\\\b(?:\\\\d{1,2}[./]\\\\d{1,2}[./]\\\\d{2,4}` +\n `|\\\\d{4}-\\\\d{2}-\\\\d{2}` +\n `|\\\\d{4}\\\\.\\\\d{2}\\\\.\\\\d{2})\\\\b`,\n label: \"date\",\n score: 1,\n};\n\nconst DATE_CZ_SPACED: RegexDef = {\n pattern: `\\\\b\\\\d{1,2}\\\\.[^\\\\S\\\\n]+\\\\d{1,2}\\\\.[^\\\\S\\\\n]+\\\\d{4}\\\\b`,\n label: \"date\",\n score: 1,\n};\n\nconst IP_ADDRESS: RegexDef = {\n pattern:\n `\\\\b(?:(?:25[0-5]|2[0-4]\\\\d|[01]?\\\\d\\\\d?)\\\\.){3}` +\n `(?:25[0-5]|2[0-4]\\\\d|[01]?\\\\d\\\\d?)\\\\b`,\n label: \"ip address\",\n score: 1,\n};\n\nconst CZ_BANK_ACCOUNT: RegexDef = {\n pattern: `\\\\b(?:\\\\d{1,6}-)?\\\\d{6,10}/\\\\d{4}(?!\\\\d)`,\n label: \"bank account number\",\n score: 0.95,\n};\n\n// Hungarian Budapest landline (+36 1 XXX XXXX).\n// 2+ digit area codes handled by INTL_PHONE.\nconst HU_LANDLINE: RegexDef = {\n pattern:\n `\\\\+36(?:[^\\\\S\\\\n]|[.\\\\-])?1(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{4}\\\\b`,\n label: \"phone number\",\n score: 0.9,\n};\n\n// Czech license plates (SPZ/RZ).\n// New format: 3SJ 0753 — digit, two letters, space?,\n// four digits. Old format: 1A2 3456 — digit, letter,\n// digit, space?, four digits.\n\n// Czech/Slovak postal code: \"110 00\", \"120 00\".\n// The distinctive XXX XX format with mandatory\n// space is specific enough to avoid most false\n// positives.\nconst CZ_POSTAL: RegexDef = {\n pattern: `\\\\b\\\\d{3}[^\\\\S\\\\n]\\\\d{2}\\\\b`,\n label: \"address\",\n score: 0.7,\n};\n\n// Spanish postal code (CP): 5 digits preceded by a\n// CP marker (\"C.P.\", \"CP\", \"código postal\"). The\n// marker is required to avoid matching arbitrary\n// 5-digit numbers (document IDs, ISBNs, etc.).\n// The pattern uses `includeTrigger`-style prefixing:\n// the marker is part of the match span (length\n// trimmed downstream if needed).\nconst ES_POSTAL: RegexDef = {\n pattern:\n `\\\\b(?:C\\\\.?P\\\\.?|[Cc][óo]digo[^\\\\S\\\\n]+postal)` +\n `[^\\\\S\\\\n]{0,3}:?[^\\\\S\\\\n]{0,3}\\\\d{5}\\\\b`,\n label: \"address\",\n score: 0.7,\n};\n\n// Spanish DNI: 8 digits + 1 letter. Letter is a\n// checksum; post-match validator confirms.\n// Pattern derived from stdnum (es.dni) candidatePattern,\n// constrained for DFA compatibility.\nconst ES_DNI: RegexDef = {\n pattern: `\\\\b\\\\d{8}-?[A-Za-z]\\\\b`,\n label: \"national identification number\",\n score: 0.9,\n validator: es.dni,\n};\n\n// Spanish NIE: X/Y/Z + 7 digits + check letter.\n// Pattern derived from stdnum (es.nie) candidatePattern.\nconst ES_NIE: RegexDef = {\n pattern: `\\\\b[XYZxyz]-?\\\\d{7}-?[A-Za-z]\\\\b`,\n label: \"national identification number\",\n score: 0.95,\n validator: es.nie,\n};\n\n// Spanish CIF: org-type letter (A-H, J, N, P-S, U-W),\n// then 7 digits, then a check character (digit or A-J).\n// Pattern derived from stdnum (es.cif) candidatePattern.\nconst ES_CIF: RegexDef = {\n pattern: `\\\\b[A-HJNP-SUVWa-hjnp-suvw]-?\\\\d{7}-?[0-9A-Ja-j]\\\\b`,\n label: \"registration number\",\n score: 0.95,\n validator: es.cif,\n};\n\nconst AU_ABN_FORMATTED: RegexDef = {\n pattern: `\\\\b\\\\d{2}[^\\\\S\\\\n]\\\\d{3}[^\\\\S\\\\n]\\\\d{3}[^\\\\S\\\\n]\\\\d{3}\\\\b`,\n label: \"tax identification number\",\n score: 0.95,\n validator: au.abn,\n};\n\nconst NO_ORGNR_FORMATTED: RegexDef = {\n pattern: `\\\\b\\\\d{3}[^\\\\S\\\\n]\\\\d{3}[^\\\\S\\\\n]\\\\d{3}\\\\b`,\n label: \"registration number\",\n score: 0.9,\n validator: no.orgnr,\n};\n\nconst NO_MVA_FORMATTED: RegexDef = {\n pattern:\n `\\\\bNO[^\\\\S\\\\n]?\\\\d{3}[^\\\\S\\\\n]?\\\\d{3}` +\n `[^\\\\S\\\\n]?\\\\d{3}[^\\\\S\\\\n]?MVA\\\\b`,\n label: \"tax identification number\",\n score: 0.95,\n validator: no.mva,\n};\n\nconst US_EIN_FORMATTED: RegexDef = {\n pattern: `\\\\b\\\\d{2}${DASH}\\\\d{7}\\\\b`,\n label: \"tax identification number\",\n score: 0.95,\n validator: us.ein,\n};\n\n// Brazilian CEP (Código de Endereçamento Postal):\n// NNNNN-NNN. Distinctive 5-digit + hyphen + 3-digit\n// shape, but the bare form is indistinguishable from\n// non-address order/ticket/reference numbers\n// (\"Order 12345-678\"), so it is not emitted as an\n// active address regex. Instead the shape is consumed\n// by `processAddressSeeds` as a postal-code seed, so\n// expansion only fires when other street/city signals\n// cluster around it (e.g.\n// \"Rua Augusta, 123, 01001-000 São Paulo\").\n//\n// Kept here as documentation only.\n\n// Brazilian CPF (personal tax ID), formatted form\n// only: NNN.NNN.NNN-NN. Dotted/dashed form is matched\n// by the distinctive separators; the br.cpf validator\n// rejects placeholder values such as \"000.000.000-00\"\n// or other invalid checksums.\n//\n// Score must beat the generic phone patterns so the\n// overlap resolver assigns the tax-ID label.\nconst BR_CPF_FORMATTED: RegexDef = {\n pattern: `\\\\b\\\\d{3}\\\\.\\\\d{3}\\\\.\\\\d{3}${DASH}\\\\d{2}\\\\b`,\n label: \"tax identification number\",\n score: 0.95,\n validator: br.cpf,\n};\n\n// Brazilian CNPJ (company tax ID), formatted:\n// NN.NNN.NNN/NNNN-NN. The slash is unique to CNPJ\n// for shape, and the br.cnpj validator filters\n// placeholder values such as \"12.345.678/0001-00\".\nconst BR_CNPJ_FORMATTED: RegexDef = {\n pattern: `\\\\b\\\\d{2}\\\\.\\\\d{3}\\\\.\\\\d{3}/\\\\d{4}${DASH}\\\\d{2}\\\\b`,\n label: \"tax identification number\",\n score: 0.95,\n validator: br.cnpj,\n};\n\n// Brazilian RG (Registro Geral, state-issued identity).\n// Format is non-uniform across states; the most reliable\n// anchor is the trailing \"SSP/UF\" issuer marker. Captures\n// the number and the SSP suffix.\n// 12.345.678 SSP/DF\n// 45.678.901-2 SSP/SP\n// 32.456.789-X SSP/SP\nconst BR_RG_WITH_SSP: RegexDef = {\n pattern:\n `\\\\b\\\\d{1,3}\\\\.?\\\\d{3}\\\\.?\\\\d{3}` +\n `(?:${DASH}[0-9A-Za-z])?` +\n `[^\\\\S\\\\n]+SSP(?:/[A-Z]{2})?\\\\b`,\n label: \"national identification number\",\n score: 0.95,\n};\n\n// Brazilian OAB (lawyer registration). Format:\n// \"OAB/UF NNNNNN\" or \"OAB/UF NNN.NNN\" — two-letter\n// state code, optional \"nº\" / \"n.\" marker, then 4–6\n// digits with optional thousand separator dot.\nconst BR_OAB: RegexDef = {\n pattern:\n `\\\\bOAB/[A-Z]{2}[^\\\\S\\\\n]+(?:n[º°.][^\\\\S\\\\n]*)?` +\n `(?:\\\\d{1,3}(?:\\\\.\\\\d{3})+|\\\\d{4,6})\\\\b`,\n label: \"registration number\",\n score: 0.95,\n};\n\n// URL: scheme + host + optional port + path + query +\n// fragment. Trailing prose punctuation excluded but\n// ? = & # kept for query strings.\n// Allow missing // after http:/https: — common OCR\n// artifact (\"http:example.cz\" instead of\n// \"http://example.cz\"). Lookahead ensures bare scheme\n// is not matched in isolation (e.g., \"http:\" at EOL).\nconst URL: RegexDef = {\n pattern:\n `(?:https?://|https?:(?=[^\\\\s])|www\\\\.)` +\n `[\\\\w\\\\-]+(?:\\\\.[\\\\w\\\\-]+)+` +\n `(?::\\\\d+)?` +\n `(?:[/?#][^\\\\s)\\\\]>]*[^\\\\s.,;:!?)\\\\]>])?`,\n label: \"url\",\n score: 1,\n};\n\n// Bare domain: no protocol/www prefix, ends with a\n// known TLD. Catches \"fondkinematografie.cz\" etc.\n// Uses [a-zA-Z0-9] (no underscores — invalid in\n// hostnames). Short ambiguous TLDs (de, at, no, se,\n// fi, dk, be, it, uk) require at least one subdomain\n// dot to reduce false positives in European legal\n// text; unambiguous TLDs allow bare second-level\n// domains (e.g., \"fondkinematografie.cz\").\nconst LONG_TLDS =\n \"com|org|net|eu|cz|sk|pl|hu|ro|fr|es\" + \"|co\\\\.uk|nl|ch|info|io|dev\";\nconst SHORT_TLDS = \"de|at|be|se|fi|dk|no|it|uk\";\n// RFC 1123: labels cannot start or end with hyphen.\nconst HOST_LABEL = `[a-zA-Z0-9](?:[a-zA-Z0-9\\\\-]*[a-zA-Z0-9])?`;\nconst BARE_HOST = `\\\\b[a-zA-Z0-9][a-zA-Z0-9\\\\-]+[a-zA-Z0-9]`;\nconst PATH_SUFFIX = `(?:[/?#][^\\\\s)\\\\]>]*[^\\\\s.,;:!?)\\\\]>])?`;\nconst BARE_DOMAIN: RegexDef = {\n pattern:\n // Unambiguous TLDs: bare SLDs ok (one dot)\n `${BARE_HOST}(?:\\\\.${HOST_LABEL})*` +\n `\\\\.(?:${LONG_TLDS})\\\\b${PATH_SUFFIX}` +\n `|` +\n // Short/ambiguous TLDs: require subdomain (two+ dots)\n `${BARE_HOST}(?:\\\\.${HOST_LABEL})+` +\n `\\\\.(?:${SHORT_TLDS})\\\\b${PATH_SUFFIX}`,\n label: \"url\",\n score: 0.9,\n};\n\n// Full RFC 5952 IPv6. :: compressed form replaces\n// 1+ zero groups. Right side: 1-7 hex groups.\nconst IPV6_ADDRESS: RegexDef = {\n pattern:\n `\\\\b(?:[0-9a-fA-F]{1,4}:){7}` +\n `[0-9a-fA-F]{1,4}\\\\b` +\n `|\\\\b(?:[0-9a-fA-F]{1,4}:){1,7}:\\\\b` +\n `|::(?:[0-9a-fA-F]{1,4}:){0,6}` +\n `[0-9a-fA-F]{1,4}`,\n label: \"ip address\",\n score: 1,\n};\n\n// MAC: colon-only OR hyphen-only (no mixed).\nconst MAC_ADDRESS: RegexDef = {\n pattern:\n `\\\\b(?:[0-9a-fA-F]{2}:){5}` +\n `[0-9a-fA-F]{2}\\\\b` +\n `|\\\\b(?:[0-9a-fA-F]{2}-){5}` +\n `[0-9a-fA-F]{2}\\\\b`,\n label: \"mac address\",\n score: 1,\n};\n\n// SWIFT/BIC moved to trigger-based detection\n// (triggers.global.json) for better composability.\n\n// UK postcode (standard outward + inward). Covers:\n// \"SW1A 1AA\", \"EC4A 1AB\", \"M1 1AE\", \"B33 8TH\",\n// \"CR2 6XH\", \"DN55 1PT\", \"GIR 0AA\" (Girobank).\n// Strict format: outward area letter(s) + digit/digit\n// (or digit+letter) + space + inward digit + 2 letters.\n// Space between outward and inward is optional in\n// freely-typed text.\nconst UK_POSTCODE: RegexDef = {\n pattern:\n `\\\\b(?:` +\n // GIR 0AA — historic Girobank postcode\n `GIR[^\\\\S\\\\n]?0AA` +\n `|` +\n // Standard outward (1-2 letters, 1-2 digits, opt. letter)\n `[A-PR-UWYZ](?:[A-HK-Y][0-9](?:[0-9]|[ABEHMNPRV-Y])?|[0-9](?:[0-9]|[A-HJKPS-UW])?)` +\n `[^\\\\S\\\\n]?[0-9][ABD-HJLNP-UW-Z]{2}` +\n `)\\\\b`,\n label: \"address\",\n score: 0.9,\n};\n\n// UK National Insurance Number. Two-letter prefix +\n// six digits + optional suffix letter A-D. Per HMRC:\n// first letter not D/F/I/Q/U/V; second letter not\n// D/F/I/O/Q/U/V; and several explicit prefix blocks\n// (BG, GB, KN, NK, NT, TN, ZZ). The character classes\n// here enforce the per-position letter rules; the\n// stdnum `gb.nino` validator handles the blocked\n// prefixes at post-match time. Negative lookaheads\n// are avoided because the Rust DFA upstream rejects\n// them. Optional spaces between segments cover the\n// common printed form `AB 12 34 56 C` — stdnum's own\n// candidatePattern (`[A-Z]{2}\\d{6}[A-Z]`) misses it.\nconst UK_NINO: RegexDef = {\n pattern:\n `\\\\b[A-CEGHJ-PR-TWXYZ][A-CEGHJ-NPR-TWXYZ]` +\n `[^\\\\S\\\\n]?\\\\d{2}[^\\\\S\\\\n]?\\\\d{2}[^\\\\S\\\\n]?\\\\d{2}` +\n `[^\\\\S\\\\n]?[A-D]?\\\\b`,\n label: \"social security number\",\n score: 0.95,\n validator: gb.nino,\n};\n\n// 12-hour time: \"5:00 p.m.\", \"12:30 AM\", \"5:00p.m.\",\n// \"11:00 a.m. Eastern Time\". Captures HH:MM and the\n// am/pm marker; optional timezone suffix is not\n// included (it's not PII). Case spelled out explicitly\n// (no (?i)) because DFA compilation fails with\n// Unicode + case-insensitive flag on this pattern.\nconst TIME_12H: RegexDef = {\n pattern:\n `\\\\b(?:1[0-2]|0?[1-9]):[0-5]\\\\d` +\n `[^\\\\S\\\\n]?(?:[aApP]\\\\.?[mM]\\\\.?)` +\n `(?=[\\\\s,;!?)]|$)`,\n label: \"date\",\n score: 0.9,\n};\n\nconst PERCENT_NUMBER_BODY = `(?:\\\\d{1,3}(?:[.,]\\\\d{3})+(?:[.,]\\\\d{1,4})?|\\\\d+(?:[.,]\\\\d{1,4})?)`;\nconst PERCENT_NUMBER = `(?:[+${DASH_INNER}])?${PERCENT_NUMBER_BODY}`;\nconst PERCENT_TOKEN = `${PERCENT_NUMBER}[^\\\\S\\\\n]{0,2}%`;\nconst PERCENT_RANGE_NUMBER = `\\\\d+(?:[.,]\\\\d{1,4})?`;\nconst PERCENT_RANGE =\n `${PERCENT_RANGE_NUMBER}[^\\\\S\\\\n]*${DASH}[^\\\\S\\\\n]*` +\n `${PERCENT_RANGE_NUMBER}[^\\\\S\\\\n]{0,2}%`;\n\nconst buildPercentWordPattern = (config: AmountWordsConfig): string => {\n const phrases: string[] = [];\n for (const entry of config.percentages ?? []) {\n const ones = entry.ones.map(escapeRegex);\n const standalone = (entry.standalone ?? []).map(escapeRegex);\n const baseWords = [...ones, ...entry.teens, ...entry.tens].map(escapeRegex);\n const compoundSeparator = entry.allowSpaceCompoundSeparator\n ? `(?:${DASH}|[^\\\\S\\\\n]+)`\n : DASH;\n const compound =\n `(?:${baseWords.join(\"|\")})` +\n (ones.length > 0 ? `(?:${compoundSeparator}(?:${ones.join(\"|\")}))?` : \"\");\n const word = `(?:${[...standalone, compound].join(\"|\")})`;\n const keyword = `(?:${entry.keywords.map(escapeRegex).join(\"|\")})`;\n phrases.push(`${word}[^\\\\S\\\\n]+${keyword}`);\n }\n return phrases.length > 0 ? `(?i:(?:${phrases.join(\"|\")}))` : \"(?!)\";\n};\n\nconst PERCENT_WORD = buildPercentWordPattern(AMOUNT_WORDS);\n\n// Percentages and financial rates. Captures signed numeric\n// values with dot or comma decimals, grouped thousands, and\n// locale-style spacing before `%`. Also widens written-out\n// legal thresholds paired with a numeric parenthetical\n// (`fifty percent (50%)`) so the text does not disclose the\n// exact value after only the parenthesized token is redacted.\n// Percentages are not classically personally identifying, but\n// in legal text they routinely fingerprint specific debt\n// instruments (`3.875% Senior Notes due 2027`) and tax\n// brackets; labelling them as `monetary amount` keeps the\n// operator-side handling consistent with how other quantitative\n// identifiers are redacted.\nconst PERCENT_RATE: RegexDef = {\n pattern:\n `(?<![\\\\p{L}\\\\p{N}_.,])(?:` +\n `${PERCENT_WORD}[^\\\\S\\\\n]*\\\\([^\\\\S\\\\n]*${PERCENT_TOKEN}[^\\\\S\\\\n]*\\\\)` +\n `|${PERCENT_RANGE}` +\n `|${PERCENT_TOKEN}` +\n `)(?![\\\\p{L}\\\\p{N}_])`,\n label: \"monetary amount\",\n score: 0.85,\n};\n\n// ── Collected definitions ────────────────────────────\n\n/**\n * All static PII regex definitions. Scanned in a\n * single pass by @stll/regex-set (Rust DFA).\n *\n * Hand-written patterns (0-17) followed by\n * stdnum-derived patterns (18+). Each stdnum entry\n * has a post-match validator for confirmation.\n *\n * Monetary amount patterns are built dynamically from\n * currencies.json via `getCurrencyPatterns()`.\n *\n * Date patterns using written month names are built\n * dynamically from date-months.json via\n * `getDatePatterns()`.\n */\nconst ALL_REGEX_DEFS: readonly RegexDef[] = [\n TITLED_PERSON,\n HONORIFIC_PERSON,\n POSTNOMINAL_PERSON,\n IBAN,\n EMAIL,\n INTL_PHONE,\n CZ_PHONE,\n TEL_PREFIX_PHONE,\n US_PAREN_PHONE,\n CREDIT_CARD,\n CZ_BIRTH_NUMBER,\n CZ_COMMERCIAL_REGISTER,\n DATE_NUMERIC,\n DATE_CZ_SPACED,\n IP_ADDRESS,\n CZ_BANK_ACCOUNT,\n HU_LANDLINE,\n CZ_POSTAL,\n ES_POSTAL,\n ES_DNI,\n ES_NIE,\n ES_CIF,\n AU_ABN_FORMATTED,\n NO_ORGNR_FORMATTED,\n NO_MVA_FORMATTED,\n US_EIN_FORMATTED,\n BR_CPF_FORMATTED,\n BR_CNPJ_FORMATTED,\n BR_RG_WITH_SSP,\n BR_OAB,\n URL,\n IPV6_ADDRESS,\n MAC_ADDRESS,\n BARE_DOMAIN,\n UK_POSTCODE,\n UK_NINO,\n TIME_12H,\n PERCENT_RATE,\n ...STDNUM_ENTRIES,\n];\n\n/** Flat pattern array for text-search. */\nexport const REGEX_PATTERNS: readonly string[] = ALL_REGEX_DEFS.map(\n (d) => d.pattern,\n);\n\n/** Parallel metadata. Index = pattern index. */\nexport const REGEX_META: readonly RegexMeta[] = ALL_REGEX_DEFS.map(\n (d): RegexMeta => {\n const meta: RegexMeta = {\n label: d.label,\n score: d.score,\n };\n if (d.validator) {\n meta.validator = d.validator;\n }\n return meta;\n },\n);\n\n// ── Dynamic date patterns (22 languages) ────────────\n\n/**\n * JSON shape: language codes map to string arrays;\n * metadata keys (prefixed `_`) map to strings.\n * The `_` keys are skipped by `buildMonthAlternation`.\n */\ntype DateMonths = Record<string, string[] | string>;\n\n/**\n * Build month-name alternation from date-months.json.\n * Deduplicates across all 22 languages, filters names\n * shorter than 3 chars (too many false positives), and\n * sorts longest-first so the regex engine prefers the\n * longest match.\n */\nconst buildMonthAlternation = (months: DateMonths): string => {\n const seen = new Set<string>();\n for (const [key, value] of Object.entries(months)) {\n if (key.startsWith(\"_\")) continue;\n const names = Array.isArray(value) ? value : [value];\n for (const name of names) {\n // Strip trailing dots for the regex; date patterns\n // use `\\\\.?` after the alternation to match optional\n // abbreviation dots.\n const clean = name.replace(/\\.$/, \"\").toLowerCase();\n if (clean.length >= MIN_MONTH_NAME_LENGTH) {\n seen.add(clean);\n }\n }\n }\n return [...seen]\n .toSorted((a, b) => b.length - a.length)\n .map(escapeRegex)\n .join(\"|\");\n};\n\n/**\n * Build date patterns from a month-name alternation.\n * Returns 6 patterns covering the major written-date\n * formats across all supported languages.\n */\nconst buildDatePatternsFromMonths = (alt: string): string[] => {\n if (!alt) {\n // No month names survived filtering — return nothing\n // rather than emitting patterns with (?:) that match\n // arbitrary whitespace.\n return [];\n }\n // Optional time suffix: \"19:45:50\" or \"19:45\"\n const TIME = `(?:\\\\s+\\\\d{1,2}:\\\\d{2}(?::\\\\d{2})?)`;\n return [\n // a. DD[.] Month[.] YYYY [HH:MM[:SS]]\n `(?i)\\\\b\\\\d{1,2}\\\\.?\\\\s+(?:${alt})\\\\.?\\\\s+\\\\d{4}${TIME}?\\\\b`,\n // b. Month[.] DD[,] YYYY — \"March 7, 2023\" (US format)\n `(?i)\\\\b(?:${alt})\\\\.?\\\\s+\\\\d{1,2},?\\\\s+\\\\d{4}\\\\b`,\n // g. Month[.] DD — \"December 31\" (no year, US format)\n `(?i)\\\\b(?:${alt})\\\\.?\\\\s+\\\\d{1,2}(?=\\\\s|[.,;!?)]|$)`,\n // c. DDst/nd/rd/th Month[.] [YYYY] — \"1st January 2025\"\n `(?i)\\\\b\\\\d{1,2}(?:st|nd|rd|th)\\\\s+(?:${alt})\\\\.?` +\n `(?:\\\\s+\\\\d{4})?(?=\\\\s|[.,;!?)]|$)`,\n // d. Month[.] YYYY — \"October 1983\"\n `(?i)\\\\b(?:${alt})\\\\.?\\\\s+\\\\d{4}\\\\b`,\n // e. YYYY. Month[.] DD. — Hungarian \"2025. január 7.\"\n `(?i)\\\\b\\\\d{4}\\\\.\\\\s+(?:${alt})\\\\.?\\\\s+\\\\d{1,2}\\\\.?(?=\\\\s|[.,;!?)]|$)`,\n // f. DD de Month[.] [de] YYYY — Spanish \"7 de enero de 2025\"\n `(?i)\\\\b\\\\d{1,2}\\\\s+de\\\\s+(?:${alt})\\\\.?` + `(?:\\\\s+de)?\\\\s+\\\\d{4}\\\\b`,\n ];\n};\n\n/** Cached promise for date patterns. Loaded once. */\nlet datePatternPromise: Promise<string[]> | null = null;\n\nconst loadDatePatterns = async (): Promise<string[]> => {\n const mod = await import(\"../data/date-months.json\");\n // Dynamic import of JSON returns { default, ...keys }.\n // Use `default` if present (ESM wrapper), else the\n // module itself.\n const months: DateMonths = mod.default ?? mod;\n const alt = buildMonthAlternation(months);\n return buildDatePatternsFromMonths(alt);\n};\n\n/**\n * Get dynamically built date patterns from\n * date-months.json. Returns a cached promise; the JSON\n * is loaded only once.\n */\nexport const getDatePatterns = (): Promise<string[]> => {\n if (!datePatternPromise) {\n datePatternPromise = loadDatePatterns().catch((err) => {\n datePatternPromise = null;\n throw err;\n });\n }\n return datePatternPromise;\n};\n\n/** Date pattern metadata (all are score 1 dates). */\nexport const DATE_PATTERN_META: Readonly<RegexMeta> = Object.freeze({\n label: \"date\",\n score: 1,\n});\n\n// ── Dynamic currency patterns ──────────────────────\n\n/**\n * JSON shape from currencies.json: ISO 4217 codes,\n * common currency symbols, and local currency names.\n */\ntype CurrenciesData = {\n codes: string[];\n symbols: string[];\n localNames?: string[];\n};\n\ntype FinancialLexicons = {\n magnitudeOptional: string;\n magnitudeRequired: string;\n magnitudePrefilterTerms: readonly string[];\n quantityFollowerGuard: string;\n};\n\ntype CurrencyPatternEntry = {\n pattern: string;\n literal?: false;\n lazy: true;\n prefilterAny: readonly string[];\n prefilterCaseInsensitive: boolean;\n prefilterRegex?: RegExp;\n};\n\ntype MagnitudePattern = {\n optional: string;\n required: string;\n prefilterTerms: readonly string[];\n};\n\nconst buildMagnitudePattern = (config: AmountWordsConfig): MagnitudePattern => {\n const words: string[] = [];\n const caseInsensitiveAbbreviations: string[] = [];\n const caseSensitiveAbbreviations: string[] = [];\n\n for (const entry of config.magnitudeSuffixes ?? []) {\n words.push(...(entry.words ?? []));\n caseInsensitiveAbbreviations.push(\n ...(entry.abbreviationsCaseInsensitive ?? []),\n );\n caseSensitiveAbbreviations.push(\n ...(entry.abbreviationsCaseSensitive ?? []),\n );\n }\n\n const branches: string[] = [];\n const wordsAlt = toSortedAlternation(words);\n const abbreviationCiAlt = toSortedAlternation(caseInsensitiveAbbreviations);\n const abbreviationCsAlt = toSortedAlternation(caseSensitiveAbbreviations);\n\n if (wordsAlt) {\n branches.push(`[^\\\\S\\\\n\\\\t]+(?i:(?:${wordsAlt}))\\\\b`);\n }\n if (abbreviationCiAlt) {\n branches.push(`[^\\\\S\\\\n\\\\t]?(?i:${abbreviationCiAlt})\\\\b`);\n }\n if (abbreviationCsAlt) {\n branches.push(`[^\\\\S\\\\n\\\\t]?(?:${abbreviationCsAlt})\\\\b`);\n }\n\n const required = branches.length > 0 ? `(?:${branches.join(\"|\")})` : \"\";\n return {\n optional: required ? `${required}?` : \"\",\n required,\n prefilterTerms: [\n ...words,\n ...caseInsensitiveAbbreviations,\n ...caseSensitiveAbbreviations,\n ],\n };\n};\n\nconst buildQuantityFollowerGuard = (config: AmountWordsConfig): string => {\n const modifiers: string[] = [];\n const nouns: string[] = [];\n\n for (const entry of config.shareQuantityTerms ?? []) {\n modifiers.push(...(entry.modifiers ?? []));\n nouns.push(...entry.nouns);\n }\n\n const modifierAlt = toSortedAlternation(modifiers);\n const nounAlt = toSortedAlternation(nouns);\n if (!nounAlt) return \"\";\n\n const modifierPrefix = modifierAlt\n ? `(?:(?:${modifierAlt})[^\\\\S\\\\n\\\\t]+){0,3}`\n : \"\";\n\n return `(?![^\\\\S\\\\n\\\\t]+(?i:` + `${modifierPrefix}(?:${nounAlt}))\\\\b)`;\n};\n\nconst buildFinancialLexicons = (\n config: AmountWordsConfig,\n): FinancialLexicons => {\n const magnitude = buildMagnitudePattern(config);\n return Object.freeze({\n magnitudeOptional: magnitude.optional,\n magnitudeRequired: magnitude.required,\n magnitudePrefilterTerms: magnitude.prefilterTerms,\n quantityFollowerGuard: buildQuantityFollowerGuard(config),\n });\n};\n\nconst FINANCIAL_LEXICONS = buildFinancialLexicons(AMOUNT_WORDS);\n\n/**\n * Build symbol character class, code alternation,\n * and local-name alternation from currencies.json,\n * then return two monetary amount patterns: leading\n * symbol and trailing code/name.\n *\n * The number sub-pattern accepts both grouped\n * thousands (1,000) and plain integers (100000)\n * via `\\d{1,9}` to catch unformatted amounts.\n */\nconst buildCurrencyPatternEntries = (\n data: CurrenciesData,\n): CurrencyPatternEntry[] => {\n const symbols = data.symbols.map(escapeCharClass).join(\"\");\n\n // Build trailing alternation: ISO codes (case-\n // sensitive, always uppercase) + local names.\n // Local names that contain only ASCII letters\n // are wrapped in (?i:...) for case-insensitive\n // matching; abbreviations with non-ASCII or\n // punctuation (Kč, zł, Fr.) stay case-sensitive.\n // Sorted longest-first to avoid partial matches.\n const isAsciiAlpha = /^[a-zA-Z\\s]+$/;\n\n type CurrencyTermPart = { term: string; len: number; alt: string };\n const codeParts: CurrencyTermPart[] = data.codes.map((code) => ({\n term: code,\n len: code.length,\n alt: escapeRegex(code),\n }));\n const localNameParts: CurrencyTermPart[] = [];\n\n // Minimum length for case-insensitive wrapping.\n // Short abbreviations like \"Ft\" (2 chars) stay\n // case-sensitive to avoid collisions (Ft vs ft/feet).\n const MIN_CI_LENGTH = 3;\n\n if (data.localNames) {\n for (const name of data.localNames) {\n const escaped = escapeRegex(name);\n const wrapCI = isAsciiAlpha.test(name) && name.length >= MIN_CI_LENGTH;\n if (wrapCI) {\n localNameParts.push({\n term: name,\n len: name.length,\n alt: `(?i:${escaped})`,\n });\n } else {\n localNameParts.push({\n term: name,\n len: name.length,\n alt: escaped,\n });\n }\n }\n }\n\n // Also include currency symbols as trailing\n // alternatives (e.g., \"126 €\", \"8 190 £\").\n // These are common in European notation.\n const toPartAlternation = (parts: readonly CurrencyTermPart[]): string =>\n parts\n .toSorted((a, b) => b.len - a.len)\n .map((p) => p.alt)\n .join(\"|\");\n const codeAlt = toPartAlternation(codeParts);\n const localNameAlt = toPartAlternation(localNameParts);\n const codeTerms = codeParts.map((part) => part.term);\n const localNameTerms = localNameParts.map((part) => part.term);\n const trailingAlt = [...codeParts, ...localNameParts]\n .toSorted((a, b) => b.len - a.len)\n .map((p) => p.alt)\n .join(\"|\");\n\n if (!symbols && !trailingAlt) return [];\n\n // Number sub-pattern: grouped thousands OR plain\n // integer up to 9 digits (covers unformatted\n // amounts like \"100000 CZK\").\n const NUM = `(?:\\\\d{1,3}(?:[,.'[^\\\\S\\\\n\\\\t]]\\\\d{3})+` + `|\\\\d{1,9})`;\n const PREFILTER_NUM = `(?:\\\\d{1,3}(?:[,.'\\\\s]\\\\d{3})+|\\\\d{1,9})`;\n const PREFILTER_DECIMAL =\n `(?:[.,](?=\\\\d|[${DASH_INNER}])` +\n `\\\\s?(?:\\\\d{1,2}${DASH}?|${DASH}{1,2}))?`;\n\n const patterns: CurrencyPatternEntry[] = [];\n const lazyCurrencyPattern = (\n pattern: string,\n prefilterAny: readonly string[],\n prefilterCaseInsensitive: boolean,\n prefilterRegex?: RegExp,\n ): CurrencyPatternEntry => ({\n pattern,\n lazy: true,\n prefilterAny,\n prefilterCaseInsensitive,\n ...(prefilterRegex ? { prefilterRegex } : {}),\n });\n const makeLeadingPrefilter = (\n terms: readonly string[],\n caseInsensitive: boolean,\n ): RegExp | undefined => {\n const termAlt = toSortedAlternation(terms);\n if (!termAlt) return undefined;\n return new RegExp(\n `(?:^|[^\\\\p{L}\\\\p{N}_])(?:${termAlt})[^\\\\S\\\\n\\\\t]{0,2}\\\\d`,\n caseInsensitive ? \"iu\" : \"u\",\n );\n };\n const makeTrailingPrefilter = (\n terms: readonly string[],\n caseInsensitive: boolean,\n ): RegExp | undefined => {\n const termAlt = toSortedAlternation(terms);\n if (!termAlt) return undefined;\n return new RegExp(\n `${PREFILTER_NUM}${PREFILTER_DECIMAL}[^\\\\S\\\\n\\\\t]{0,4}(?:${termAlt})`,\n caseInsensitive ? \"iu\" : \"u\",\n );\n };\n const makeLeadingMagnitudePrefilter = (\n terms: readonly string[],\n caseInsensitive: boolean,\n ): RegExp | undefined => {\n const termAlt = toSortedAlternation(terms);\n const magnitudeAlt = toSortedAlternation(\n FINANCIAL_LEXICONS.magnitudePrefilterTerms,\n );\n if (!termAlt || !magnitudeAlt) return undefined;\n return new RegExp(\n `(?:^|[^\\\\p{L}\\\\p{N}_])(?:${termAlt})` +\n `[^\\\\S\\\\n\\\\t]{0,2}${PREFILTER_NUM}${PREFILTER_DECIMAL}` +\n `[^\\\\S\\\\n\\\\t]{0,8}(?:${magnitudeAlt})(?:$|[^\\\\p{L}\\\\p{N}_])`,\n caseInsensitive ? \"iu\" : \"u\",\n );\n };\n const makeLeadingSymbolMagnitudePrefilter = (): RegExp | undefined => {\n const magnitudeAlt = toSortedAlternation(\n FINANCIAL_LEXICONS.magnitudePrefilterTerms,\n );\n if (!magnitudeAlt) return undefined;\n return new RegExp(\n `(?:^|[^\\\\p{L}\\\\p{N}_])(?:[${symbols}])` +\n `[^\\\\S\\\\n\\\\t]{0,2}${PREFILTER_NUM}${PREFILTER_DECIMAL}` +\n `[^\\\\S\\\\n\\\\t]{0,8}(?:${magnitudeAlt})(?:$|[^\\\\p{L}\\\\p{N}_])`,\n \"iu\",\n );\n };\n const makeTrailingMagnitudePrefilter = (\n terms: readonly string[],\n caseInsensitive: boolean,\n ): RegExp | undefined => {\n const termAlt = toSortedAlternation(terms);\n const magnitudeAlt = toSortedAlternation(\n FINANCIAL_LEXICONS.magnitudePrefilterTerms,\n );\n if (!termAlt || !magnitudeAlt) return undefined;\n return new RegExp(\n `${PREFILTER_NUM}${PREFILTER_DECIMAL}` +\n `[^\\\\S\\\\n\\\\t]{0,8}(?:${magnitudeAlt})(?:$|[^\\\\p{L}\\\\p{N}_])` +\n `[\\\\s\\\\S]{0,24}(?:${termAlt})`,\n caseInsensitive ? \"iu\" : \"u\",\n );\n };\n\n // Decimal part: dot/comma must be followed by at\n // least one digit or dash. Without this, a trailing\n // sentence period (\"$25,000,000.\") gets consumed by\n // the optional group, breaking the \\b anchor.\n // Use lookahead (?=\\d|DASH) after [.,] to ensure\n // the separator is actually a decimal marker.\n const DECIMAL =\n `(?:[.,](?=\\\\d|[${DASH_INNER}])` +\n `[^\\\\S\\\\n\\\\t]?` +\n `(?:\\\\d{1,2}${DASH}?|${DASH}{1,2}))?`;\n const END = `(?:\\\\b|(?=\\\\s|[.,;!?)]|$))`;\n\n const MAGNITUDE_OPTIONAL = FINANCIAL_LEXICONS.magnitudeOptional;\n const MAGNITUDE_REQUIRED = FINANCIAL_LEXICONS.magnitudeRequired;\n\n // Leading symbol: $100, €1,000.50, € 100000.\n // Magnitude-bearing forms ($25 million, $2bn)\n // are a separate pattern: making the magnitude\n // suffix optional in this very broad symbol pattern\n // forces the regex engine to do much more work on\n // EDGAR-style contracts with large numeric sections.\n if (symbols) {\n patterns.push(\n lazyCurrencyPattern(\n `(?:[${symbols}])` + `[^\\\\S\\\\n\\\\t]?` + `${NUM}${DECIMAL}${END}`,\n data.symbols,\n true,\n ),\n );\n if (MAGNITUDE_REQUIRED) {\n patterns.push(\n lazyCurrencyPattern(\n `(?:[${symbols}])` +\n `[^\\\\S\\\\n\\\\t]?` +\n `${NUM}${DECIMAL}${MAGNITUDE_REQUIRED}${END}`,\n data.symbols,\n true,\n makeLeadingSymbolMagnitudePrefilter(),\n ),\n );\n }\n }\n\n // Leading multi-char code: \"Kč 10,—\", \"Fr. 500\",\n // \"EUR 1.5 billion\".\n if (codeAlt) {\n patterns.push(\n lazyCurrencyPattern(\n `\\\\b(?:${codeAlt})` + `[^\\\\S\\\\n\\\\t]{0,2}` + `${NUM}${DECIMAL}${END}`,\n codeTerms,\n false,\n makeLeadingPrefilter(codeTerms, false),\n ),\n );\n if (MAGNITUDE_REQUIRED) {\n patterns.push(\n lazyCurrencyPattern(\n `\\\\b(?:${codeAlt})` +\n `[^\\\\S\\\\n\\\\t]{0,2}` +\n `${NUM}${DECIMAL}${MAGNITUDE_REQUIRED}${END}`,\n codeTerms,\n false,\n makeLeadingMagnitudePrefilter(codeTerms, false),\n ),\n );\n }\n }\n if (localNameAlt) {\n patterns.push(\n lazyCurrencyPattern(\n `\\\\b(?:${localNameAlt})` +\n `[^\\\\S\\\\n\\\\t]{0,2}` +\n `${NUM}${DECIMAL}${END}`,\n localNameTerms,\n true,\n makeLeadingPrefilter(localNameTerms, true),\n ),\n );\n if (MAGNITUDE_REQUIRED) {\n patterns.push(\n lazyCurrencyPattern(\n `\\\\b(?:${localNameAlt})` +\n `[^\\\\S\\\\n\\\\t]{0,2}` +\n `${NUM}${DECIMAL}${MAGNITUDE_REQUIRED}${END}`,\n localNameTerms,\n true,\n makeLeadingMagnitudePrefilter(localNameTerms, true),\n ),\n );\n }\n }\n\n // Trailing code/name: 100 USD, 1,000.50 CZK,\n // 100000 Kč, 500 korun, 100 Fr., 25 million USD,\n // $25 million USD.\n // Magnitude sits between the number and the code so\n // \"100 million USD\" parses naturally; the existing\n // 0-4 whitespace span absorbs the separator.\n const optionalLeadingSymbol = symbols\n ? `(?<![\\\\p{L}\\\\p{N}_])(?:[${symbols}][^\\\\S\\\\n\\\\t]?)?`\n : \"\\\\b\";\n if (codeAlt) {\n patterns.push(\n lazyCurrencyPattern(\n `${optionalLeadingSymbol}${NUM}${DECIMAL}` +\n `[^\\\\S\\\\n\\\\t]{0,4}` +\n `(?:${codeAlt})${END}`,\n codeTerms,\n false,\n makeTrailingPrefilter(codeTerms, false),\n ),\n );\n if (MAGNITUDE_REQUIRED) {\n patterns.push(\n lazyCurrencyPattern(\n `${optionalLeadingSymbol}${NUM}${DECIMAL}${MAGNITUDE_REQUIRED}` +\n `[^\\\\S\\\\n\\\\t]{0,4}` +\n `(?:${codeAlt})${FINANCIAL_LEXICONS.quantityFollowerGuard}${END}`,\n codeTerms,\n false,\n makeTrailingMagnitudePrefilter(codeTerms, false),\n ),\n );\n }\n }\n if (localNameAlt) {\n patterns.push(\n lazyCurrencyPattern(\n `${optionalLeadingSymbol}${NUM}${DECIMAL}` +\n `[^\\\\S\\\\n\\\\t]{0,4}` +\n `(?:${localNameAlt})${END}`,\n localNameTerms,\n true,\n makeTrailingPrefilter(localNameTerms, true),\n ),\n );\n if (MAGNITUDE_REQUIRED) {\n patterns.push(\n lazyCurrencyPattern(\n `${optionalLeadingSymbol}${NUM}${DECIMAL}${MAGNITUDE_REQUIRED}` +\n `[^\\\\S\\\\n\\\\t]{0,4}` +\n `(?:${localNameAlt})${FINANCIAL_LEXICONS.quantityFollowerGuard}${END}`,\n localNameTerms,\n true,\n makeTrailingMagnitudePrefilter(localNameTerms, true),\n ),\n );\n }\n }\n\n if (symbols) {\n const trailingSymbolPrefilter = new RegExp(\n `${PREFILTER_NUM}${PREFILTER_DECIMAL}[^\\\\S\\\\n\\\\t]{0,4}[${symbols}]`,\n \"u\",\n );\n patterns.push(\n lazyCurrencyPattern(\n `${NUM}${DECIMAL}${MAGNITUDE_OPTIONAL}` +\n `[^\\\\S\\\\n\\\\t]{0,4}` +\n `(?:[${symbols}])${END}`,\n data.symbols,\n true,\n trailingSymbolPrefilter,\n ),\n );\n }\n\n return patterns;\n};\n\n/** Cached promise for currency patterns. Loaded once. */\nlet currencyPatternPromise: Promise<string[]> | null = null;\nlet currencyPatternEntryPromise: Promise<CurrencyPatternEntry[]> | null = null;\n\nconst loadCurrencyPatternEntries = async (): Promise<\n CurrencyPatternEntry[]\n> => {\n const mod = await import(\"../data/currencies.json\");\n const data: CurrenciesData = mod.default ?? mod;\n return buildCurrencyPatternEntries(data);\n};\n\nconst loadCurrencyPatterns = async (): Promise<string[]> =>\n (await loadCurrencyPatternEntries()).map((entry) => entry.pattern);\n\n/**\n * Get dynamically built monetary amount patterns from\n * currencies.json. Returns a cached promise; the JSON\n * is loaded only once.\n */\nexport const getCurrencyPatterns = (): Promise<string[]> => {\n if (!currencyPatternPromise) {\n currencyPatternPromise = loadCurrencyPatterns().catch((err) => {\n currencyPatternPromise = null;\n throw err;\n });\n }\n return currencyPatternPromise;\n};\n\nexport const getCurrencyPatternEntries = (): Promise<\n CurrencyPatternEntry[]\n> => {\n if (!currencyPatternEntryPromise) {\n currencyPatternEntryPromise = loadCurrencyPatternEntries().catch((err) => {\n currencyPatternEntryPromise = null;\n throw err;\n });\n }\n return currencyPatternEntryPromise;\n};\n\n/** Currency pattern metadata (score 0.9). */\nexport const CURRENCY_PATTERN_META: Readonly<RegexMeta> = Object.freeze({\n label: \"monetary amount\",\n score: 0.9,\n});\n\n// ── Public API ──────────────────────────────────────\n\n/**\n * Process regex matches from the unified search.\n * Receives all matches; filters to the regex slice\n * via sliceStart/sliceEnd. Local index into META is\n * match.pattern - sliceStart.\n *\n * For stdnum-derived patterns (those with a validator\n * in META), the matched text is passed through the\n * validator's validate() method. If validation fails,\n * the match is discarded as a false positive.\n */\nexport const processRegexMatches = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n meta_: readonly RegexMeta[],\n): Entity[] => {\n const results: Entity[] = [];\n\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) {\n continue;\n }\n\n const localIdx = idx - sliceStart;\n const meta = meta_[localIdx];\n if (!meta) {\n continue;\n }\n if (\n meta.sourceDetail !== \"custom-regex\" &&\n meta.label === \"phone number\" &&\n match.text.length < MIN_PHONE_LENGTH\n ) {\n continue;\n }\n\n // Post-match validation: if the pattern came from\n // a stdnum validator, compact (strip separators)\n // then validate. The candidate regex may capture\n // spaced/dashed variants that validate() rejects\n // without compaction.\n if (meta.validator) {\n const compacted = meta.validator.compact(match.text);\n const result = meta.validator.validate(compacted);\n if (!result.valid) {\n continue;\n }\n }\n\n const entity: Entity = {\n start: match.start,\n end: match.end,\n label: meta.label,\n text: match.text,\n score: meta.score,\n source: DETECTION_SOURCES.REGEX,\n };\n if (meta.sourceDetail) {\n entity.sourceDetail = meta.sourceDetail;\n }\n results.push(entity);\n }\n\n return results;\n};\n\n// ── Dynamic signing clause patterns ────────────────\n\ntype SigningClauseConfig = {\n patterns: Array<{\n lang: string;\n prefix: string;\n suffix: string;\n prepositions: string[];\n }>;\n};\n\n/**\n * Build signing clause place-name patterns from\n * signing-clauses.json. Each pattern captures the\n * city/place name from contract signing locations.\n *\n * The place name sub-pattern:\n * \\p{Lu}\\p{Ll}+ (capitalized word)\n * optionally followed by preposition + capitalized\n * word (for \"nad Nisou\", \"am Main\", etc.)\n * optionally followed by more capitalized words\n * (for \"Hradec Králové\", \"New York\", etc.)\n */\nconst buildSigningClausePatterns = (data: SigningClauseConfig): string[] => {\n const patterns: string[] = [];\n\n for (const entry of data.patterns) {\n const prepAlt =\n entry.prepositions.length > 0 ? entry.prepositions.join(\"|\") : null;\n\n // Place name: Uppercase word, optionally with\n // preposition + uppercase, optionally more caps\n const place = prepAlt\n ? `(\\\\p{Lu}\\\\p{Ll}+` +\n `(?:\\\\s+(?:${prepAlt})\\\\s+\\\\p{Lu}\\\\p{Ll}+)*` +\n `(?:\\\\s+\\\\p{Lu}\\\\p{Ll}+)*)`\n : `(\\\\p{Lu}\\\\p{Ll}+(?:[- ]\\\\p{Lu}\\\\p{Ll}+)*)`;\n\n const full =\n `(?:^|\\\\n|[^\\\\S\\\\n])` +\n entry.prefix +\n place +\n (entry.suffix ? `(?:${entry.suffix})` : \"\");\n\n patterns.push(full);\n }\n\n return patterns;\n};\n\nexport const SIGNING_CLAUSE_META: Readonly<RegexMeta> = {\n label: \"address\",\n score: 0.9,\n};\n\nlet signingPatternPromise: Promise<string[]> | null = null;\n\nconst loadSigningPatterns = async (): Promise<string[]> => {\n const mod = await import(\"../data/signing-clauses.json\");\n const data: SigningClauseConfig = mod.default ?? mod;\n return buildSigningClausePatterns(data);\n};\n\nexport const getSigningClausePatterns = (): Promise<string[]> => {\n if (!signingPatternPromise) {\n signingPatternPromise = loadSigningPatterns().catch((err) => {\n signingPatternPromise = null;\n throw err;\n });\n }\n return signingPatternPromise;\n};\n","/**\n * Legal-form ORG detection — candidate + validator architecture.\n *\n * Replaces only the front-end of the v1 path: where v1 builds a\n * ~7 KB monolithic regex (greedy head + tail + ~600-suffix\n * alternation + nested Unicode-aware lookarounds) and feeds the\n * whole thing into `@stll/text-search`, v2 splits the work in two:\n *\n * 1. AC-flavoured literal lookup over the ~330-entry suffix\n * lexicon — small per-suffix patterns the regex backend\n * handles in microseconds, no DFA blowup against long\n * preambles with embedded parentheticals.\n * 2. A small TS-side backward walker that constructs the rough\n * span around each suffix occurrence (head words + connectors\n * + lowercase tail tokens), matching the shape v1's regex\n * would have produced.\n *\n * The rough span is then handed to v1's `processLegalFormMatches`\n * unchanged — that function already implements every validator\n * the v1 pipeline depends on (role-head sentence-verb trim, leading\n * clause trim, embedded list split, line-break / single-cap /\n * all-caps-line rejection, post-match accented-letter boundary,\n * etc.). The wins are:\n *\n * - 17–730× speedup vs the monolithic regex (see PR description).\n * - Sidesteps the upstream text-search DFA bug that drops every\n * match on long preambles with embedded `(this \"Agreement\")`.\n * - No carved-up validator logic — the 1671-line code-side\n * validation chain is reused as-is.\n */\n\nimport type { Match, TextSearch } from \"@stll/text-search\";\n\nimport { getTextSearch } from \"../search-engine\";\nimport type { Entity } from \"../types\";\nimport { normalizeForSearch } from \"../util/normalize\";\nimport {\n getClauseNounHeadsSync,\n getKnownLegalSuffixes,\n getLegalRoleHeadsSync,\n getSentenceVerbIndicatorsSync,\n processLegalFormMatches,\n warmLegalRoleHeads,\n} from \"./legal-forms\";\n\n// Normalised suffix set for the \"word before `and` is itself a\n// legal-form suffix\" boundary check — strip dots/spaces so \"LLC\",\n// \"Inc.\", and \"s.r.o.\" all reduce to a comparable token.\nconst normalizeSuffixToken = (s: string): string =>\n s.replace(/[.,\\s]/g, \"\").toLowerCase();\n\nlet cachedNormalizedSuffixes: ReadonlySet<string> | null = null;\nconst getNormalizedSuffixSet = (): ReadonlySet<string> => {\n if (cachedNormalizedSuffixes !== null) return cachedNormalizedSuffixes;\n const out = new Set<string>();\n for (const s of getKnownLegalSuffixes()) {\n const n = normalizeSuffixToken(s);\n if (n.length > 0) out.add(n);\n }\n cachedNormalizedSuffixes = out;\n return out;\n};\n\nconst isLegalFormSuffixWord = (word: string): boolean => {\n const n = normalizeSuffixToken(word);\n if (n.length === 0) return false;\n return getNormalizedSuffixSet().has(n);\n};\n\n// ── Suffix index (AC-flavoured literal pass) ────────────────────\n\nlet cachedSuffixSearch: { ts: TextSearch; suffixes: readonly string[] } | null =\n null;\n\nconst getSuffixSearch = (): {\n ts: TextSearch;\n suffixes: readonly string[];\n} => {\n const suffixes = getKnownLegalSuffixes();\n if (cachedSuffixSearch !== null && cachedSuffixSearch.suffixes === suffixes) {\n return cachedSuffixSearch;\n }\n // `getKnownLegalSuffixes` already returns the list sorted\n // longest-first, which gives the regex backend longest-match-\n // first behaviour for overlapping forms like `LLP` vs `LLLP` vs\n // `PLLC`.\n const patterns = suffixes.map((suffix) => ({\n pattern: suffix,\n literal: true as const,\n }));\n // Use the injected TextSearch constructor so the wasm build\n // (`@stll/anonymize-wasm`) gets its own indirection — a static\n // import from `@stll/text-search` here would bypass the\n // `initTextSearch`/`getTextSearch` wiring in `src/wasm.ts`.\n const Ctor = getTextSearch();\n const ts = new Ctor(patterns);\n cachedSuffixSearch = { ts, suffixes };\n return cachedSuffixSearch;\n};\n\n// ── Boundary helpers ────────────────────────────────────────────\n\nconst ANY_LETTER_RE = /\\p{L}/u;\nconst ANY_LETTER_OR_DIGIT_RE = /[\\p{L}\\p{N}]/u;\n\nconst isTrailingBoundary = (fullText: string, end: number): boolean => {\n if (end >= fullText.length) return true;\n const next = fullText.charAt(end);\n // Reject when the suffix bleeds into a real word — covers both\n // ASCII (`LLCx`) and accented Latin (`AGÊNCIA`). Digit\n // continuations (`LLC123`) are not org-name boundaries either.\n if (ANY_LETTER_RE.test(next)) return false;\n if (/\\d/.test(next)) return false;\n return true;\n};\n\nconst isLeadingSeparator = (fullText: string, suffixStart: number): boolean => {\n if (suffixStart === 0) return true;\n const prev = fullText.charAt(suffixStart - 1);\n // The separator between the head and the suffix is at most\n // a single space, comma, or run of those — anything else\n // means we'd be slicing into a real word.\n if (ANY_LETTER_OR_DIGIT_RE.test(prev)) return false;\n // A dot preceded by a letter is the inside of a longer dotted\n // abbreviation (`18 U.S.C.` → the `S.C.` hit is inside the citation,\n // not the start of a fresh suffix). The same shape is fine when a\n // CJK/space precedes the dot; only a Latin letter signals continuation.\n if (\n prev === \".\" &&\n suffixStart >= 2 &&\n ANY_LETTER_RE.test(fullText.charAt(suffixStart - 2))\n ) {\n return false;\n }\n return true;\n};\n\n// ── Greedy backward walker ──────────────────────────────────────\n//\n// Emits a rough span around each suffix occurrence, large enough\n// that v1's `processLegalFormMatches` validators can decide how\n// much (if any) to trim. The walk admits the same token shapes\n// v1's regex `(head + optional lowercase tail)` admits:\n//\n// head : CapWord | ALL-CAPS | single Cap | digit\n// tail : LowerWord | CapWord | ALL-CAPS | digit\n// connected by a SimpleSep (HSPACE, comma, dash, dot, &)\n// or LowerConnector (a / and / und / et / e / y / i)\n//\n// Up to 20 tokens total, matching the per-prefix cap in\n// `buildPatternString`. The walker never crosses a hard newline.\n\nconst HEAD_TOKEN_CAP = 20;\n\nconst TOKEN_RE = /[\\p{L}\\p{N}'’.&-]+/u;\n\n// Treat NBSP (U+00A0) and narrow NBSP (U+202F) as whitespace —\n// DOCX exports routinely use them between name tokens and the\n// trailing legal-form suffix (\"Acme s.r.o.\").\nconst isInterTokenWs = (ch: string): boolean =>\n ch === \" \" || ch === \"\\t\" || ch === \" \" || ch === \" \" || ch === \",\";\n\nconst findTokenBefore = (\n fullText: string,\n pos: number,\n): { start: number; end: number; text: string } | null => {\n let end = pos;\n // Skip horizontal whitespace / commas back. Refuse hard newlines.\n while (end > 0) {\n const ch = fullText.charAt(end - 1);\n if (ch === \"\\n\") return null;\n if (isInterTokenWs(ch) || ch === \";\") {\n end--;\n continue;\n }\n break;\n }\n if (end === 0) return null;\n let start = end;\n while (start > 0) {\n const ch = fullText.charAt(start - 1);\n if (ch === \"\\n\") break;\n if (!TOKEN_RE.test(ch)) break;\n start--;\n }\n if (start === end) return null;\n return { start, end, text: fullText.slice(start, end) };\n};\n\n// `Cena. KB poskytla úvěr.` — the AC suffix lookup catches `KB`,\n// the backward walker grabs `Cena.` because the `.` is admitted\n// by TOKEN_RE. Reject candidates whose backward span crosses a\n// sentence-ending period — `<Lu><Ll>+\\.<space>` immediately\n// before the suffix anchor is the structural marker.\nconst crossesSentenceEnd = (\n fullText: string,\n candidateStart: number,\n suffixStart: number,\n): boolean => {\n // Sentence-end shape immediately inside the candidate range\n // signals that the walker swept across a sentence boundary\n // (`...Cena. KB poskytla úvěr.` or `... Co.\\nLLC. Except for\n // Goldman Sachs & Co. LLC`). Two shapes count:\n // `<Cap><lowercase{2,}>.<whitespace>` — ordinary sentence\n // like `Cena.<space>`.\n // `<Cap>{2,}\\.<whitespace>` — all-caps acronym or\n // legal-form suffix used\n // sentence-finally like\n // `LLC.<space>Except`.\n const slice = fullText.slice(candidateStart, suffixStart);\n return /\\p{Lu}\\p{Ll}{2,}\\.\\s/u.test(slice) || /\\p{Lu}{2,}\\.\\s/u.test(slice);\n};\n\nconst UPPER_LETTER_RE = /^\\p{Lu}/u;\nconst LOWER_LETTER_RE = /^\\p{Ll}/u;\nconst DIGIT_RE = /^\\d/;\nconst CONNECTOR_RE = /^(?:a|and|und|et|e|y|i|&)$/i;\n\nconst isAcceptableToken = (tok: string): boolean => {\n if (tok.length === 0) return false;\n if (UPPER_LETTER_RE.test(tok)) return true;\n if (DIGIT_RE.test(tok)) return true;\n if (CONNECTOR_RE.test(tok)) return true;\n // Lowercase tail tokens (`pracovní`, `plošiny`, `s`, `r`, `o`)\n // are admitted — v1's tail allowed up to 10 of them. The\n // role-head trim downstream rips them out when the leading\n // chunk turns out to be clause prose.\n if (LOWER_LETTER_RE.test(tok)) return true;\n return false;\n};\n\n// Multi-char \"and\"-type connectors. v1's `extendBackward` refuses\n// to cross one when only one uppercase word precedes it (\"Paul\n// Newman and Apple, Inc.\" → \"Apple, Inc.\") because the leading\n// pattern looks like a person name. Three or more upper words\n// before the connector is a real multi-word org name and the\n// walker crosses (\"UniCredit Bank Czech Republic and Slovakia,\n// a.s.\"). v2 applies the same rule on the way back.\nconst AND_TYPE_CONNECTOR_RE = /^(?:and|und|et)$/i;\n\n// \"Elon R. Musk and X Corp.\" — the middle initial `R.` is a Cap\n// token but the whole shape is a personal name, not a multi-word\n// org. v1 looks for the `<Initial>.<HSPACE><Surname>` shape\n// immediately before the connector. The check here looks at the\n// 32 chars preceding the connector and asks whether they end in\n// `<Lu>.<space><Word><whitespace>` — i.e. an initial + dot, then\n// a final surname token right before the connector.\nconst MIDDLE_INITIAL_RE = /\\p{Lu}\\.[^\\S\\n]+\\p{L}[\\p{L}\\p{M}'’]*[^\\S\\n]*$/u;\nconst hasMiddleInitialBefore = (fullText: string, pos: number): boolean => {\n const slice = fullText.slice(Math.max(0, pos - 32), pos);\n return MIDDLE_INITIAL_RE.test(slice);\n};\n\n/**\n * Count consecutive uppercase-starting tokens immediately before\n * `pos`. Stops at the first non-upper token, a hard newline, or\n * text start.\n */\nconst countUpperBefore = (fullText: string, pos: number): number => {\n let scan = pos;\n let count = 0;\n while (true) {\n const tok = findTokenBefore(fullText, scan);\n if (!tok) break;\n if (!UPPER_LETTER_RE.test(tok.text)) break;\n count++;\n scan = tok.start;\n }\n return count;\n};\n\n// Once at least one capitalised token has been admitted, refuse to\n// bridge more than this many consecutive lowercase tokens. The bridge\n// cap protects against sentence-prose sweeps like\n// `Specifikace a přesná poloha místností v areálu Základní školy …`\n// where a sentence-start CapWord precedes a long prose run. Long\n// lowercase TAILS preceding the first CapWord stay admitted, so\n// Czech state-form names with many descriptor tokens\n// (`Národní agentura pro podporu rozvoje vzdělávání … republiky,\n// z.s.`) survive — the cap only fires after a CapWord is already in\n// the span.\nconst MAX_LOWER_BRIDGE = 4;\n\nconst walkBackward = (fullText: string, suffixStart: number): number => {\n let pos = suffixStart;\n let stepsLeft = HEAD_TOKEN_CAP;\n let leftmostCapPos = -1;\n let lowerBridgeRun = 0;\n\n while (stepsLeft > 0) {\n const tok = findTokenBefore(fullText, pos);\n if (!tok) break;\n if (!isAcceptableToken(tok.text)) break;\n\n // Clause-descriptor boundary. A lowercase token that is\n // ITSELF a known legal-form descriptor (`corporation`,\n // `company`, `s.r.o.`, …) followed by a comma marks a clause\n // break (`Delaware corporation, X Holdings I, Inc.` — the\n // comma sits between two separate entities). Without the\n // suffix-word gate this would also fire inside long Czech\n // names whose lowercase tail is part of the name\n // (`Krajská správa, příspěvková organizace`), so we require\n // both the suffix-word match AND a CapWord already accepted\n // to the right.\n if (LOWER_LETTER_RE.test(tok.text) && leftmostCapPos >= 0) {\n const afterTok = fullText.slice(tok.end, pos);\n if (/^[,;]/.test(afterTok) && isLegalFormSuffixWord(tok.text)) break;\n }\n\n // Connector boundary checks. We're considering crossing a\n // connector — three reasons not to:\n //\n // (a) The word immediately BEFORE the connector is itself\n // a legal-form suffix (`Morgan Securities LLC and Allen\n // & Company LLC`, `RELAKA s.r.o. a AGROBIOPLYN s.r.o.`\n // — the connector sits between two complete orgs, not\n // inside one). Applies to every connector.\n // (b) (`and`-type only) ≤2 uppercase tokens precede the\n // connector — `Paul Newman and X, Inc.` shape.\n // (c) (`and`-type only) the previous token is a middle\n // initial (`Elon R. Musk and X Corp.` — the personal\n // name extends through the initial dot).\n //\n // Three uppercase tokens or more before an `and`-type connector\n // signal a real multi-word org name (`UniCredit Bank Czech\n // Republic and Slovakia, a.s.`) and the walker crosses.\n if (CONNECTOR_RE.test(tok.text)) {\n const prevPeek = findTokenBefore(fullText, tok.start);\n if (prevPeek && isLegalFormSuffixWord(prevPeek.text)) break;\n if (AND_TYPE_CONNECTOR_RE.test(tok.text)) {\n const upperBefore = countUpperBefore(fullText, tok.start);\n if (upperBefore <= 2) break;\n if (hasMiddleInitialBefore(fullText, tok.start)) break;\n }\n }\n\n if (UPPER_LETTER_RE.test(tok.text)) {\n leftmostCapPos = tok.start;\n lowerBridgeRun = 0;\n } else if (LOWER_LETTER_RE.test(tok.text)) {\n if (leftmostCapPos >= 0) {\n lowerBridgeRun++;\n if (lowerBridgeRun > MAX_LOWER_BRIDGE) break;\n }\n } else {\n lowerBridgeRun = 0;\n }\n pos = tok.start;\n stepsLeft--;\n }\n\n return leftmostCapPos < 0 ? suffixStart : leftmostCapPos;\n};\n\nconst CAP_TOKEN_RE = /(?<![\\p{L}\\p{N}])\\p{Lu}[\\p{L}\\p{M}\\p{N}'’.&-]*/gu;\nconst LOWER_WORD_RE = /(?<![\\p{L}\\p{N}])\\p{Ll}[\\p{L}\\p{M}'’]*/gu;\n\n/**\n * Post-walker verb gate. The walker admits any lowercase token so\n * Czech and German extended state-form tails (`Krajská správa,\n * příspěvková organizace`, `Národní agentura pro komunikační a\n * informační technologie, s. p.`) keep their full vocabulary. The\n * trade-off is that prose sentences ending in a legal-form descriptor\n * (`...převádí na příspěvková organizace.`) get swept too. When the\n * candidate text contains a known lowercase sentence-verb token we\n * slide `candidateStart` forward to the first capitalised token after\n * the last verb; if no capitalised token follows the verb, the\n * candidate is prose and gets dropped.\n */\nconst trimToFirstCapAfterVerb = (\n fullText: string,\n candidateStart: number,\n suffixStart: number,\n): number => {\n if (candidateStart >= suffixStart) return candidateStart;\n const head = fullText.slice(candidateStart, suffixStart);\n const verbIndicators = getSentenceVerbIndicatorsSync();\n let lastVerbEnd = -1;\n for (const wordMatch of head.matchAll(LOWER_WORD_RE)) {\n if (\n wordMatch.index !== undefined &&\n verbIndicators.has(wordMatch[0].toLowerCase())\n ) {\n lastVerbEnd = wordMatch.index + wordMatch[0].length;\n }\n }\n if (lastVerbEnd < 0) return candidateStart;\n // Skip role-heads (`Licensee`, `Buyer`, `Prodávající`) and clause\n // nouns (`Agreement`, `Section`, `Schedule`) that appear between\n // the verb and the real organisation name — both behave like\n // appositive labels, not company names.\n const roleHeads = getLegalRoleHeadsSync();\n const clauseNouns = getClauseNounHeadsSync();\n CAP_TOKEN_RE.lastIndex = lastVerbEnd;\n for (\n let next = CAP_TOKEN_RE.exec(head);\n next !== null;\n next = CAP_TOKEN_RE.exec(head)\n ) {\n const word = next[0].toLowerCase();\n if (roleHeads.has(word) || clauseNouns.has(word)) continue;\n return candidateStart + next.index;\n }\n return suffixStart;\n};\n\n// ── Public API ──────────────────────────────────────────────────\n\nexport const warmLegalFormsV2 = warmLegalRoleHeads;\n\n/**\n * Synthesise the `Match` objects v1's `processLegalFormMatches`\n * expects. Pattern index 0 with `sliceStart=0, sliceEnd=1` lets\n * every synthesised match pass the slice gate without depending on\n * a real unified-search pattern table.\n */\ntype SynthMatch = Match & { text: string };\n\nconst synthMatch = (\n start: number,\n end: number,\n fullText: string,\n): SynthMatch => ({\n start,\n end,\n pattern: 0,\n text: fullText.slice(start, end),\n});\n\nexport const detectLegalFormsV2 = (fullText: string): Entity[] => {\n const { ts } = getSuffixSearch();\n const searchText = normalizeForSearch(fullText);\n const candidates: SynthMatch[] = [];\n const trimmedCandidates = new Set<SynthMatch>();\n\n for (const match of ts.findIter(searchText)) {\n const suffixStart = match.start;\n const suffixEnd = match.end;\n\n // SEC EDGAR line wrap: `Goldman Sachs & Co.\\nLLC` — terminal\n // suffix on its own line after a dotted business designator.\n // Allow crossing a single newline ONLY when the line above\n // ends in `<word>.` (a dotted abbreviation). Indentation on\n // the suffix line is preserved verbatim in EDGAR HTML, so the\n // newline check skips any leading horizontal whitespace first.\n let effectiveSuffixStart = suffixStart;\n {\n let scan = suffixStart;\n while (scan > 0) {\n const ch = fullText.charAt(scan - 1);\n if (ch === \" \" || ch === \"\\t\") {\n scan--;\n continue;\n }\n break;\n }\n if (scan > 0 && fullText.charAt(scan - 1) === \"\\n\") {\n let p = scan - 1;\n while (p > 0 && fullText.charAt(p - 1) === \" \") p--;\n if (p > 0 && fullText.charAt(p - 1) === \".\") {\n effectiveSuffixStart = p;\n }\n }\n }\n\n // The leading-separator check runs on the ORIGINAL suffix start\n // so the dotted-abbreviation rejection (`18 U.S.C.` → reject the\n // inner `S.C.` hit) does not also fire on legitimate line-wrap\n // candidates whose remapped anchor lands on a dotted designator\n // (`Goldman Sachs & Co.\\nLLC` → LLC's original prev char is a\n // space, not a dotted-citation continuation).\n if (!isLeadingSeparator(fullText, suffixStart)) continue;\n if (!isTrailingBoundary(fullText, suffixEnd)) continue;\n\n const walkerStart = walkBackward(fullText, effectiveSuffixStart);\n if (walkerStart >= effectiveSuffixStart) continue; // no head body\n if (crossesSentenceEnd(fullText, walkerStart, effectiveSuffixStart))\n continue;\n const candidateStart = trimToFirstCapAfterVerb(\n fullText,\n walkerStart,\n effectiveSuffixStart,\n );\n if (candidateStart >= effectiveSuffixStart) continue;\n const candidate = synthMatch(candidateStart, suffixEnd, fullText);\n if (candidateStart !== walkerStart) trimmedCandidates.add(candidate);\n candidates.push(candidate);\n }\n\n if (candidates.length === 0) return [];\n\n // When the AC pass finds both a nested suffix and the outer\n // suffix on the same span (`Goldman Sachs & Co.` + `Goldman\n // Sachs & Co.\\nLLC` for the line-wrap case), prefer the\n // longer span — the validator pipeline emits one entity per\n // candidate, so otherwise we'd ship both. Pure overlap; the\n // legitimate sibling-org split is already done by\n // splitEmbeddedLegalFormList inside processLegalFormMatches.\n const dedupedCandidates = dropOverlapping(candidates);\n\n // Hand the rough spans to v1's full validator pipeline —\n // role-head trim, leading-clause trim, embedded-list split,\n // line-break check, all-caps-line rejection, post-match\n // accented-letter boundary — all already implemented there.\n //\n // Split by whether the verb trim moved the start. Trimmed\n // candidates skip v1's `extendBackward` because re-extending\n // would walk back across the prose the trim just removed\n // (`Vendor grants Licensee Acme Inc.` → trim to `Acme Inc.`\n // → extend back through `Licensee`). Un-trimmed candidates\n // still benefit from `extendBackward` for in-name preposition\n // crossings the v2 walker doesn't perform (`The Bank of\n // America and Trust Company, Inc.`).\n const trimmed: SynthMatch[] = [];\n const untrimmed: SynthMatch[] = [];\n for (const c of dedupedCandidates) {\n (trimmedCandidates.has(c) ? trimmed : untrimmed).push(c);\n }\n return [\n ...processLegalFormMatches(trimmed, 0, 1, fullText, {\n suppressExtendBackward: true,\n }),\n ...processLegalFormMatches(untrimmed, 0, 1, fullText),\n ];\n};\n\nconst dropOverlapping = (candidates: SynthMatch[]): SynthMatch[] => {\n const sorted = [...candidates].sort(\n (a, b) => a.start - b.start || b.end - a.end,\n );\n const out: SynthMatch[] = [];\n for (const c of sorted) {\n const last = out.at(-1);\n if (last && c.start >= last.start && c.end <= last.end) continue;\n out.push(c);\n }\n return out;\n};\n","import type { Match } from \"@stll/text-search\";\nimport { br, us } from \"@stll/stdnum\";\nimport type { Validator } from \"@stll/stdnum\";\n\nimport { DETECTION_SOURCES } from \"../types\";\nimport type {\n CompiledValidation,\n Entity,\n TriggerGroupConfig,\n TriggerRule,\n TriggerValidation,\n ValidIdValidator,\n} from \"../types\";\nimport { POST_NOMINALS } from \"../config/titles\";\nimport { getKnownLegalSuffixes } from \"./legal-forms\";\nimport { loadLanguageConfigs } from \"../util/lang-loader\";\nimport { DASH } from \"../util/char-groups\";\n\nconst VALID_ID_VALIDATORS: Record<ValidIdValidator, Validator> = {\n \"br.cpf\": br.cpf,\n \"br.cnpj\": br.cnpj,\n \"us.rtn\": us.rtn,\n};\n\nconst TRIGGER_SCORE = 0.95;\nconst WHITESPACE_RE = /\\s+/;\nconst LETTER_RE = /\\p{L}/u;\n/**\n * Decimal-comma pattern: comma followed by digit or\n * dash notation (\"0,05%\", \"1.529,50 Kč\", \"98.000,- Kč\").\n */\nconst DECIMAL_COMMA_RE = new RegExp(`^,(?:\\\\d|${DASH}{1,2})`);\n\n/**\n * Post-nominal degree regex. When a comma-stop is\n * followed by a known post-nominal (Ph.D., CSc., MBA\n * etc.), skip the comma and degree, then continue.\n */\nconst POST_NOMINAL_RE = new RegExp(\n `^,\\\\s*(?:${POST_NOMINALS.toSorted((a, b) => b.length - a.length)\n .map((d) =>\n d.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\").replace(/\\\\\\./g, \"\\\\.\\\\s*\"),\n )\n .join(\"|\")})\\\\.?`,\n \"i\",\n);\n\n// Definitive legal form suffixes (case-sensitive).\n// When a person-labeled trigger captures text containing\n// one of these, the entity is reclassified as\n// \"organization\". No \"i\" flag: short uppercase-only\n// forms (SE, SA, AG) must NOT match Czech/Slovak\n// reflexive pronouns \"se\", \"sa\" which appear in\n// person-trigger captures like\n// \"Ing. Jan Novák, se sídlem...\".\n//\nlet cachedLegalFormCheckSource: readonly string[] | null = null;\nlet cachedLegalFormCheckForms: readonly string[] = [];\nconst LEGAL_FORM_ALNUM_RE = /[\\p{L}\\p{N}]/u;\nconst LETTER_ONLY_LEGAL_FORM_RE = /^[\\p{L}\\p{M}]+$/u;\n\nconst getLegalFormCheckForms = (): readonly string[] => {\n const source = getKnownLegalSuffixes();\n if (cachedLegalFormCheckSource !== source) {\n cachedLegalFormCheckSource = source;\n cachedLegalFormCheckForms = source;\n }\n return cachedLegalFormCheckForms;\n};\n\nconst hasKnownLegalFormSuffix = (text: string): boolean => {\n for (const form of getLegalFormCheckForms()) {\n let fromIndex = 0;\n while (fromIndex < text.length) {\n const start = text.indexOf(form, fromIndex);\n if (start === -1) {\n break;\n }\n const end = start + form.length;\n fromIndex = start + 1;\n\n // Dot-free letter-only forms get Unicode-aware word\n // boundaries so short uppercase forms (\"SE\", \"AG\") and\n // longer single-word forms (\"Branch\", \"Limited\") don't\n // match as substrings of unrelated tokens. Forms with\n // punctuation already terminate naturally.\n if (!LETTER_ONLY_LEGAL_FORM_RE.test(form)) {\n return true;\n }\n if (\n !LEGAL_FORM_ALNUM_RE.test(text[start - 1] ?? \"\") &&\n !LEGAL_FORM_ALNUM_RE.test(text[end] ?? \"\")\n ) {\n return true;\n }\n }\n }\n return false;\n};\n\n// ── Validation compilation ─────────────────────────\n\nconst compileValidations = (\n validations: TriggerValidation[],\n): CompiledValidation[] =>\n validations.map((v): CompiledValidation => {\n switch (v.type) {\n case \"starts-uppercase\":\n return {\n type: \"starts-uppercase\",\n re: /^\\p{Lu}/u,\n };\n case \"min-length\":\n return { type: \"min-length\", min: v.min };\n case \"max-length\":\n return { type: \"max-length\", max: v.max };\n case \"no-digits\":\n return { type: \"no-digits\", re: /\\d/ };\n case \"has-digits\":\n return { type: \"has-digits\", re: /\\d/ };\n case \"matches-pattern\":\n return {\n type: \"matches-pattern\",\n // Strip g/y flags: the compiled regex is shared\n // across all rules in the group and must be\n // stateless (no lastIndex advancement).\n re: new RegExp(v.pattern, (v.flags ?? \"\").replace(/[gy]/g, \"\")),\n };\n case \"valid-id\": {\n const validator = VALID_ID_VALIDATORS[v.validator];\n if (!validator) {\n throw new Error(\n `Unknown valid-id validator: ${JSON.stringify(v.validator)}`,\n );\n }\n return {\n type: \"valid-id\",\n check: (value) => {\n // stdnum validators expect compact digits only;\n // strip formatting (spaces, dots, dashes,\n // slashes) so dotted/spaced IDs still validate.\n const compact = value.replace(/[\\s.\\-/]/g, \"\");\n return validator.validate(compact).valid;\n },\n };\n }\n default: {\n // TriggerValidation is a public export; custom\n // configs may bypass the build-time validator.\n const _exhaustive: never = v;\n throw new Error(\n `Unknown validation type: ${JSON.stringify(_exhaustive)}`,\n );\n }\n }\n });\n\nconst applyValidations = (\n text: string,\n validations: CompiledValidation[],\n): boolean => {\n for (const v of validations) {\n switch (v.type) {\n case \"starts-uppercase\":\n if (!v.re.test(text)) return false;\n break;\n case \"min-length\":\n if (text.length < v.min) return false;\n break;\n case \"max-length\":\n if (text.length > v.max) return false;\n break;\n case \"no-digits\":\n if (v.re.test(text)) return false;\n break;\n case \"has-digits\":\n if (!v.re.test(text)) return false;\n break;\n case \"matches-pattern\":\n if (!v.re.test(text)) return false;\n break;\n case \"valid-id\":\n if (!v.check(text)) return false;\n break;\n default: {\n const _exhaustive: never = v;\n throw new Error(\n `Unknown compiled validation type: ${JSON.stringify(_exhaustive)}`,\n );\n }\n }\n }\n return true;\n};\n\n// ── Trigger expansion ──────────────────────────────\n\nconst expandTriggerGroups = (groups: TriggerGroupConfig[]): TriggerRule[] => {\n const rules: TriggerRule[] = [];\n for (const group of groups) {\n const extensions = group.extensions ?? [];\n const compiled = compileValidations(group.validations ?? []);\n\n // Generate trigger variants from the original\n // trigger strings. Extensions are applied once to\n // the base set only (not combinatorially); e.g.,\n // [\"add-colon\", \"normalize-spaces\"] produces\n // \"trigger:\", \"trigger\\u00A0\", but NOT\n // \"trigger\\u00A0:\". This is intentional to avoid\n // exponential variant growth.\n const allTriggers = new Set(group.triggers);\n for (const trigger of group.triggers) {\n if (extensions.includes(\"add-colon\") && !trigger.endsWith(\":\"))\n allTriggers.add(`${trigger}:`);\n if (extensions.includes(\"add-trailing-space\") && !trigger.endsWith(\" \"))\n allTriggers.add(`${trigger} `);\n if (\n extensions.includes(\"add-colon-space\") &&\n !trigger.endsWith(\": \") &&\n !trigger.endsWith(\":\")\n )\n allTriggers.add(`${trigger}: `);\n if (extensions.includes(\"normalize-spaces\")) {\n if (trigger.includes(\" \")) {\n allTriggers.add(trigger.replace(/ /g, \"\\u00A0\"));\n }\n }\n }\n\n const includeTrigger = group.includeTrigger ?? false;\n\n for (const trigger of allTriggers) {\n rules.push({\n trigger,\n label: group.label,\n strategy: group.strategy,\n validations: compiled,\n includeTrigger,\n });\n }\n }\n return rules;\n};\n\n// ── Pattern builder for unified search ──────────────\n\ntype TriggerPatterns = {\n patterns: string[];\n rules: TriggerRule[];\n};\n\nlet triggerPatternsPromise: Promise<TriggerPatterns> | null = null;\n\n/**\n * Build trigger patterns and rules from data configs.\n * Returns string[] for the unified TextSearch\n * builder and the parallel rules array.\n */\nconst loadTriggerPatterns = async (): Promise<TriggerPatterns> => {\n const rules: TriggerRule[] = [];\n\n const allGroups = await loadLanguageConfigs<readonly TriggerGroupConfig[]>(\n \"triggers\",\n (mod) => {\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config\n const m = mod as {\n default?: readonly TriggerGroupConfig[];\n };\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config\n return (m.default ?? mod) as readonly TriggerGroupConfig[];\n },\n );\n for (const groups of allGroups) {\n if (!Array.isArray(groups)) {\n console.warn(\n \"[anonymize] triggers: unexpected \" + \"config shape, skipping\",\n );\n continue;\n }\n rules.push(...expandTriggerGroups(groups as TriggerGroupConfig[]));\n }\n\n // Load global triggers (language-agnostic)\n try {\n const globalMod = await import(\"../data/triggers.global.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config\n const globalGroups = ((globalMod as { default?: unknown }).default ??\n globalMod) as TriggerGroupConfig[];\n if (Array.isArray(globalGroups)) {\n rules.push(...expandTriggerGroups(globalGroups));\n }\n } catch (err) {\n // Only suppress \"module not found\"; re-throw\n // other errors (JSON parse, etc.).\n if (\n !(err instanceof Error) ||\n !err.message.includes(\"Cannot find module\")\n ) {\n throw err;\n }\n }\n\n // Load year-words from JSON dictionary and create\n // date triggers: \"rok 2022\" → date entity.\n try {\n const yearMod = await import(\"../data/year-words.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON\n const data = ((yearMod as { default?: unknown }).default ??\n yearMod) as Record<string, string | string[]>;\n const seen = new Set<string>();\n const yearValidation = compileValidations([\n {\n type: \"matches-pattern\",\n pattern: \"^(?:19|20)\\\\d{2}\\\\.?$\",\n },\n ]);\n for (const [key, words] of Object.entries(data)) {\n if (key.startsWith(\"_\") || !Array.isArray(words)) {\n continue;\n }\n for (const word of words) {\n const lc = word.toLowerCase();\n if (seen.has(lc)) continue;\n seen.add(lc);\n rules.push({\n trigger: word,\n label: \"date\",\n strategy: { type: \"n-words\", count: 1 },\n validations: yearValidation,\n includeTrigger: false,\n });\n }\n }\n } catch {\n // year-words.json not available — skip\n }\n\n // Warn about cross-group trigger duplicates.\n // Duplicates cause redundant AC matches but are\n // not fatal (mergeAndDedup handles overlap).\n const seen = new Map<string, { label: string; strategy: string }>();\n for (const rule of rules) {\n const key = rule.trigger.toLowerCase();\n const prev = seen.get(key);\n if (prev !== undefined) {\n const labelDiff = prev.label !== rule.label;\n const stratDiff = prev.strategy !== rule.strategy.type;\n if (labelDiff || stratDiff) {\n console.warn(\n `[anonymize] duplicate trigger` +\n ` \"${rule.trigger}\":` +\n (labelDiff\n ? ` labels \"${prev.label}\" vs` + ` \"${rule.label}\"`\n : \"\") +\n (stratDiff\n ? ` strategies \"${prev.strategy}\" vs` + ` \"${rule.strategy.type}\"`\n : \"\"),\n );\n }\n }\n seen.set(key, {\n label: rule.label,\n strategy: rule.strategy.type,\n });\n }\n\n // Build patterns from lowercased trigger strings.\n // rules[i] corresponds to patterns[i].\n // Plain lowercased strings — the unified builder\n // sets caseInsensitive globally on the AC.\n const patterns: string[] = rules.map((r) => r.trigger.toLowerCase());\n\n // Warm the address stop-keywords cache so the\n // synchronous `extractValue` (address strategy) can\n // read it without an async hop.\n await loadAddressStopKeywords();\n\n return { patterns, rules };\n};\n\nexport const buildTriggerPatterns = async (): Promise<TriggerPatterns> => {\n triggerPatternsPromise ??= loadTriggerPatterns();\n return triggerPatternsPromise;\n};\n\n// ── Value extraction ────────────────────────────────\n\nconst LEADING_PUNCT = /^[„\"\"»«'\"()\\s]+/;\nconst TRAILING_PUNCT = /[\"\"»«'\"()\\s]+$/;\n\nconst stripQuotes = (value: {\n start: number;\n end: number;\n text: string;\n}): {\n start: number;\n end: number;\n text: string;\n} | null => {\n const leadingMatch = LEADING_PUNCT.exec(value.text);\n const leadingLen = leadingMatch ? leadingMatch[0].length : 0;\n const stripped = value.text.slice(leadingLen).replace(TRAILING_PUNCT, \"\");\n if (stripped.length === 0) {\n return null;\n }\n return {\n start: value.start + leadingLen,\n end: value.start + leadingLen + stripped.length,\n text: stripped,\n };\n};\n\n/**\n * Hard stop characters for to-next-comma scanning. A closing\n * parenthesis or closing bracket terminates a clause just like\n * an opening one: `State of New York or any other jurisdiction)`\n * is the tail of a parenthesised insertion, not the start of a\n * larger phrase that should be absorbed into a jurisdiction span.\n */\nconst COMMA_STOP_CHARS = new Set([\"\\n\", \"(\", \")\", \"[\", \"]\", \"\\t\", \";\"]);\n\n/**\n * Sentence-terminator detection: a period that genuinely ends\n * one clause and starts another. Used by `to-next-comma` so\n * governing-law clauses (\"…State of New York. SECTION 2…\")\n * don't sweep across the period.\n *\n * Three positive signals (any one terminates):\n * 1. Long lowercase tail before the dot (>= 5 letters) —\n * catches \"…construction. SECTION 2…\", \"…vykonává.\".\n * 2. Currency/amount tail (zł, Kč, USD, €) — catches\n * \"…w kwocie 1000 zł. Termin płatności…\".\n * 3. Proper-noun head: a capitalized word of >= 4 letters\n * ending in lowercase, with no internal uppercase, AND\n * a substantial next clause (Capital + >= 2 lowercase).\n * Catches short city names that the lowercase-tail rule\n * misses: \"…z siedzibą w Łódź. Kapitał zakładowy…\",\n * \"…seat in Brno. Section 2…\".\n *\n * The rule must NOT fire on:\n * - title abbreviations: \"Mr.\", \"Mrs.\", \"Dr.\", \"Hon.\",\n * \"Sr.\", \"Jr.\" (head <= 3 chars or insufficient Ll tail)\n * - degree abbreviations: \"Ph.D.\", \"RNDr.\", \"MUDr.\",\n * \"Ing.\" (internal periods or internal uppercase block\n * the proper-noun pattern)\n * - street-type abbreviations: \"Ste.\", \"Ave.\", \"Inc.\",\n * \"ul.\", \"al.\", \"nábř.\" (lowercase initials or\n * insufficient Ll tail)\n * - small-word lowercase abbreviations: \"prof.\", \"inż.\",\n * \"hab.\" (no leading uppercase, so the proper-noun rule\n * can't fire)\n */\nconst NEXT_IS_SENTENCE_START_RE = /^\\.(?:\\s+\\p{Lu}|\\s*$)/u;\nconst SENTENCE_TAIL_RE = /\\p{Ll}{5,}$/u;\n/**\n * Proper-noun tail: capital letter + >= 3 lowercase letters,\n * preceded by a non-letter and non-period (so we don't slice\n * into the middle of an acronym or a multi-dot abbreviation).\n * The 4-character minimum excludes 2–3-char titles (\"Mr\",\n * \"Mrs\", \"Dr\", \"Inc\", \"Ste\"); the all-lowercase tail\n * excludes mixed-case degrees (\"RNDr\", \"MUDr\").\n */\nconst PROPER_NOUN_HEAD_RE = /(?:^|[^\\p{L}.])\\p{Lu}\\p{Ll}{3,}$/u;\n/**\n * Next clause begins with a real word: capital + >= 2\n * lowercase letters. Filters cases where a capitalized\n * abbreviation (e.g., \"Smith Inc.\") follows a proper noun,\n * which would otherwise look sentence-like.\n */\nconst NEXT_IS_REAL_SENTENCE_RE = /^\\.\\s+\\p{Lu}\\p{Ll}{2,}/u;\n/**\n * Short currency-abbreviation tail (zł, Kč, gr, Ft, kr,\n * лв, USD, PLN, EUR, …). When a period follows one of\n * these and the next token starts uppercase, treat it as\n * a sentence boundary even though the tail is too short\n * to satisfy `SENTENCE_TAIL_RE`. Without this, amount\n * triggers using `to-next-comma` swallow the following\n * clause: `\"w kwocie 1000 zł. Termin płatności…\"`.\n *\n * Currency codes are typically uppercase (`USD`, `PLN`)\n * but appear lowercased in informal writing (`pln`, `eur`);\n * local names mix case (`zł`, `Kč`, `Ft`); symbols\n * (`€`, `$`, `£`) appear after the amount as well. The\n * negative lookbehind on a letter ensures the abbreviation\n * is matched only as a standalone token; symbols are\n * matched unconditionally since they are not letters.\n */\nconst CURRENCY_TAIL_RE =\n /(?:(?<![\\p{L}])(?:zł|Kč|gr|Ft|kr|лв|USD|PLN|EUR|CZK|GBP|CHF|HUF|RON|SEK|NOK|DKK)|[€$£])$/iu;\n// Numeric sentence tail: a digit immediately before the\n// period (e.g. monetary amounts \"R$ 1.000,00.\" or\n// \"EUR 5.000.\") signals a sentence end too, so amount\n// triggers do not consume the next clause.\nconst NUMERIC_SENTENCE_TAIL_RE = /\\d$/;\n\nconst isSentenceTerminator = (text: string, periodIndex: number): boolean => {\n const tail = text.slice(periodIndex);\n if (!NEXT_IS_SENTENCE_START_RE.test(tail)) {\n return false;\n }\n const head = text.slice(0, periodIndex);\n if (\n SENTENCE_TAIL_RE.test(head) ||\n CURRENCY_TAIL_RE.test(head) ||\n NUMERIC_SENTENCE_TAIL_RE.test(head)\n ) {\n return true;\n }\n // Proper-noun head (short city names like \"Łódź.\",\n // \"Brno.\", \"York.\") gated by a real-word next clause\n // to avoid breaking on title chains (\"Mrs. Smith Inc.\").\n return PROPER_NOUN_HEAD_RE.test(head) && NEXT_IS_REAL_SENTENCE_RE.test(tail);\n};\n\n/**\n * Field-label keywords that terminate address scanning.\n * When a comma in the address strategy is followed by\n * one of these, the address stops before the keyword.\n *\n * The list is sourced from\n * `data/address-stop-keywords.json` (per-language so new\n * languages can drop in their own labels without\n * touching this file). `loadAddressStopKeywords` unions\n * every language into a single longest-first array;\n * `getAddressStopKeywordsSync` returns the cached union\n * and falls back to a seed list so the strategy keeps\n * working before the warmup promise resolves.\n *\n * The address strategy doesn't know the document\n * language, so a flat union is intentional: any\n * language's labels can appear in any address.\n */\nconst ADDRESS_STOP_KEYWORDS_SEED: readonly string[] = [\n \"číslo účtu\",\n \"registrační\",\n \"zastoupen\",\n \"bankovní\",\n \"e-mail\",\n \"telefon\",\n \"jednatel\",\n \"ředitel\",\n \"datová\",\n \"vložka\",\n \"sp.zn.\",\n \"oddíl\",\n \"swift\",\n \"email\",\n \"iban\",\n \"dič\",\n \"ičo\",\n \"tel\",\n \"č.ú.\",\n \"bic\",\n \"ič\",\n];\n\nlet addressStopKeywordsCache: readonly string[] | null = null;\nlet addressStopKeywordsPromise: Promise<readonly string[]> | null = null;\n\nconst loadAddressStopKeywords = async (): Promise<readonly string[]> => {\n if (addressStopKeywordsCache) return addressStopKeywordsCache;\n if (addressStopKeywordsPromise) return addressStopKeywordsPromise;\n addressStopKeywordsPromise = (async () => {\n let data: Record<string, unknown> = {};\n try {\n const mod = await import(\"../data/address-stop-keywords.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON module shape\n const parsed =\n (mod as { default?: Record<string, unknown> }).default ?? mod;\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON module shape\n data = parsed as Record<string, unknown>;\n } catch (err) {\n console.warn(\n \"[anonymize] triggers: failed to load \" +\n \"address-stop-keywords.json, falling back to \" +\n \"seed list:\",\n err,\n );\n }\n const seen = new Set<string>();\n const out: string[] = [];\n const addAll = (list: readonly string[]): void => {\n for (const kw of list) {\n if (typeof kw !== \"string\" || kw.length === 0) continue;\n const lower = kw.toLowerCase();\n if (seen.has(lower)) continue;\n seen.add(lower);\n out.push(lower);\n }\n };\n for (const [key, value] of Object.entries(data)) {\n if (key.startsWith(\"_\")) continue;\n if (!Array.isArray(value)) continue;\n addAll(value as readonly string[]);\n }\n addAll(ADDRESS_STOP_KEYWORDS_SEED);\n // Sort longest-first so multi-word labels\n // (\"bankové spojenie\", \"číslo účtu\") match\n // before nested shorter ones (\"č.\").\n out.sort((a, b) => b.length - a.length);\n addressStopKeywordsCache = out;\n return out;\n })();\n return addressStopKeywordsPromise;\n};\n\nconst getAddressStopKeywordsSync = (): readonly string[] =>\n addressStopKeywordsCache ?? ADDRESS_STOP_KEYWORDS_SEED;\n\n/**\n * Warm the address-stop-keywords cache. Pipeline callers\n * await this before invoking trigger detection so the\n * synchronous `extractValue` path uses the merged list\n * instead of the seed fallback.\n */\nexport const warmAddressStopKeywords = async (): Promise<void> => {\n await loadAddressStopKeywords();\n};\n\n// Hard cap for unterminated trigger values. Applies to\n// strategies that scan forward until a delimiter\n// (`to-next-comma`, `to-end-of-line`). Prevents a missing\n// delimiter — common in HTML-flattened or single-paragraph\n// PDFs where a whole signature block lives on one line —\n// from turning a trigger into a multi-hundred-character\n// entity. 100 chars covers normal full-name + address\n// lines while bounding pathological inputs.\nconst MAX_TRIGGER_VALUE_LEN = 100;\nconst MIN_TRIGGER_PHONE_DIGITS = 5;\nconst TRIGGER_LOOKAHEAD_MARGIN = 128;\nconst LINE_TRIGGER_LOOKAHEAD = 2_048;\nconst MATCH_PATTERN_LOOKAHEAD = 512;\nconst PHONE_VALUE_START_RE = /^[+(\\d]/;\nconst ISO_DATE_PREFIX_RE = /^\\d{4}-\\d{2}-\\d{2}\\b/;\nconst INLINE_FIELD_LABEL_RE = /\\b[\\p{L}][\\p{L}\\p{M} /-]{1,32}:/u;\nconst INLINE_FIELD_LABEL_STOP_RE =\n /(?:^|[^\\S\\n\\t])[\\p{L}][\\p{L}\\p{M} /-]{1,32}:/u;\n\nconst capAtWordBoundary = (valueText: string, cap: number): number => {\n let capped = cap;\n const isWordChar = (i: number): boolean =>\n /[\\p{L}\\p{N}]/u.test(valueText[i] ?? \"\");\n while (capped > 0 && isWordChar(capped - 1) && isWordChar(capped)) {\n capped--;\n }\n return capped;\n};\n\nconst isPlausiblePhoneTriggerValue = (value: string): boolean => {\n const trimmed = value.trimStart();\n if (!PHONE_VALUE_START_RE.test(trimmed)) {\n return false;\n }\n if (ISO_DATE_PREFIX_RE.test(trimmed)) {\n return false;\n }\n if (INLINE_FIELD_LABEL_RE.test(trimmed)) {\n return false;\n }\n let digits = 0;\n for (const ch of trimmed) {\n if (/\\d/.test(ch)) {\n digits++;\n }\n }\n return digits >= MIN_TRIGGER_PHONE_DIGITS;\n};\n\nconst getTriggerLookahead = (strategy: TriggerRule[\"strategy\"]): number => {\n switch (strategy.type) {\n case \"to-next-comma\":\n return (strategy.maxLength ?? 100) + TRIGGER_LOOKAHEAD_MARGIN;\n case \"to-end-of-line\":\n return LINE_TRIGGER_LOOKAHEAD;\n case \"n-words\":\n return strategy.count * 64 + TRIGGER_LOOKAHEAD_MARGIN;\n case \"company-id-value\":\n return 256;\n case \"address\":\n return (strategy.maxChars ?? 120) + TRIGGER_LOOKAHEAD_MARGIN;\n case \"match-pattern\":\n return MATCH_PATTERN_LOOKAHEAD;\n default: {\n const _exhaustive: never = strategy;\n throw new Error(\n `Unknown trigger strategy: ${JSON.stringify(_exhaustive)}`,\n );\n }\n }\n};\n\nconst extractValue = (\n text: string,\n triggerEnd: number,\n strategy: TriggerRule[\"strategy\"],\n label?: string,\n): {\n start: number;\n end: number;\n text: string;\n} | null => {\n const lookaheadEnd = Math.min(\n text.length,\n triggerEnd + getTriggerLookahead(strategy),\n );\n const remaining = text.slice(triggerEnd, lookaheadEnd);\n // Strip leading whitespace, colons, semicolons —\n // triggers are often followed by \": \\t\\t\\t\" in\n // formatted documents.\n const stripped = remaining.replace(/^[\\s:;]+/, \"\");\n const trimmedOffset = remaining.length - stripped.length;\n const valueStart = triggerEnd + trimmedOffset;\n const valueText = stripped;\n\n if (valueText.length === 0) {\n return null;\n }\n\n switch (strategy.type) {\n case \"to-next-comma\": {\n // Stop at comma, newline, or opening parenthesis.\n // Parens mark defined-term clauses in legal text\n // (e.g., \"(dále jen ...)\") and should not be\n // captured as part of a name/address.\n // When a comma is followed by a known post-nominal\n // degree (Ph.D., CSc., MBA), skip it and continue\n // so \"RNDr. Filipem Hartvichem, Ph.D., CSc.\"\n // captures the full name with degrees.\n // Also stop at any of the trigger's configured\n // `stopWords` so e.g. a court trigger (\"Městským\n // soudem v Praze\") doesn't sweep into the following\n // clause (\"dne 1. 1. 2020\") when the comma is\n // missing.\n const stopWords = strategy.stopWords ?? [];\n const stopWordRe =\n stopWords.length > 0\n ? new RegExp(\n `^(?:${stopWords\n .map((w) => w.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"))\n .join(\"|\")})(?![\\\\p{L}\\\\p{N}])`,\n \"iu\",\n )\n : null;\n let end = 0;\n\n while (end < valueText.length) {\n const ch = valueText[end];\n // Hard stops: newline, paren, tab, semicolon\n if (ch !== undefined && COMMA_STOP_CHARS.has(ch)) {\n break;\n }\n // Sentence terminator: a period that genuinely ends\n // a clause (\"…State of Louisiana shall govern its\n // construction. SECTION 3.05…\"). The helper rejects\n // abbreviation dots (Ste./RNDr./Mr.) by requiring a\n // real word before the period.\n if (ch === \".\" && isSentenceTerminator(valueText, end)) {\n break;\n }\n // Trigger-supplied stop words (e.g. \"dne\", \"vložka\"\n // for court triggers). Only check at word starts so\n // a stopword that happens to appear inside a longer\n // token doesn't terminate prematurely.\n if (\n stopWordRe !== null &&\n (end === 0 || !/[\\p{L}\\p{N}]/u.test(valueText[end - 1] ?? \"\")) &&\n stopWordRe.test(valueText.slice(end))\n ) {\n break;\n }\n // Comma: for person triggers, check if followed\n // by a post-nominal degree (Ph.D., CSc.) and\n // skip past it. Non-person triggers stop here.\n if (ch === \",\") {\n const afterComma = valueText.slice(end);\n // Decimal separator: comma followed by digit or\n // dash notation (\"0,05%\", \"1.529,50 Kč\",\n // \"98.000,- Kč\", \"98.000,-- Kč\")\n if (DECIMAL_COMMA_RE.test(afterComma)) {\n end++;\n continue;\n }\n const degreeMatch =\n label === \"person\" ? POST_NOMINAL_RE.exec(afterComma) : null;\n if (degreeMatch) {\n // Skip the comma + degree, continue scan\n end += degreeMatch[0].length;\n continue;\n }\n // Regular comma — stop here\n break;\n }\n end++;\n }\n\n // Per-trigger hard length cap. Applies even when a\n // stop char was found, so jurisdiction triggers\n // (\"State of Delaware\") cannot absorb forum-selection\n // clauses that legitimately end at a comma sentences\n // away (\"…in the event any dispute arises out of…\").\n // When no stop char was found, the cap also acts as\n // the unterminated-value fallback; default to 100\n // when the strategy does not configure one. When\n // capping, retreat to the previous word boundary so\n // the returned span never ends mid-word.\n const lengthCap = strategy.maxLength ?? 100;\n if (end > lengthCap) {\n end = capAtWordBoundary(valueText, lengthCap);\n }\n\n const rawSlice = valueText.slice(0, end);\n const extracted = rawSlice.trim();\n if (extracted.length === 0) {\n return null;\n }\n const trailingSpaces = rawSlice.length - rawSlice.trimEnd().length;\n return {\n start: valueStart,\n end: valueStart + end - trailingSpaces,\n text: extracted,\n };\n }\n\n case \"to-end-of-line\": {\n // Only capture on the SAME line as the trigger. The\n // generic leading-whitespace strip above swallows\n // newlines, which would let a header-style trigger\n // (\"Bankovní spojení\\nKupní cena bude uhrazena na ...\")\n // consume the next line wholesale. If the strip\n // crossed a newline, the trigger has no inline value\n // and the strategy emits nothing.\n const consumed = remaining.length - valueText.length;\n if (consumed > 0 && remaining.slice(0, consumed).includes(\"\\n\")) {\n return null;\n }\n // Stop at newline or tab (tab separates cells\n // in DOCX table rows).\n const LINE_STOPS = [\"\\n\", \"\\t\"];\n let end = valueText.length;\n let foundLineStop = false;\n for (const ch of LINE_STOPS) {\n const idx = valueText.indexOf(ch);\n if (idx !== -1 && idx < end) {\n end = idx;\n foundLineStop = true;\n }\n }\n if (label === \"phone number\") {\n const inlineLabel = INLINE_FIELD_LABEL_STOP_RE.exec(\n valueText.slice(0, end),\n );\n if (inlineLabel) {\n end = inlineLabel.index;\n foundLineStop = true;\n }\n }\n // Cap only when no real line delimiter was found. HTML-\n // flattened text and signature blocks routinely pack\n // hundreds of chars (a chain of \"Phone:\" / \"Name:\"\n // pseudo-fields) onto one logical line; newline-terminated\n // values should still capture through the delimiter.\n if (!foundLineStop) {\n end = capAtWordBoundary(\n valueText,\n Math.min(end, MAX_TRIGGER_VALUE_LEN),\n );\n }\n const rawSlice = valueText.slice(0, end);\n const extracted = rawSlice.trim();\n if (extracted.length === 0) {\n return null;\n }\n const trailingSpaces = rawSlice.length - rawSlice.trimEnd().length;\n return {\n start: valueStart,\n end: valueStart + end - trailingSpaces,\n text: extracted,\n };\n }\n\n case \"n-words\": {\n // Respect tab as a cell boundary (DOCX table\n // rows use tabs between columns).\n const tabIdx = valueText.indexOf(\"\\t\");\n const cellText = tabIdx !== -1 ? valueText.slice(0, tabIdx) : valueText;\n // Skip punctuation-only tokens (colons, dashes)\n // so \"datová schránka : hsaxra8\" captures\n // \"hsaxra8\" not \":\"\n const PUNCT_ONLY = /^[\\p{P}\\p{S}]+$/u;\n // Skip generic \"number\" markers (PT: \"nº/n°/n.\",\n // FR/IT/ES use \"n°\", general \"№\") so triggers\n // like \"OAB/SP nº 123456\" capture the actual\n // identifier, not the marker.\n const NUMBER_MARKER = /^(?:n[ºo°.]|№)$/i;\n const allTokens = cellText.split(WHITESPACE_RE);\n const words = allTokens\n .filter((w) => !PUNCT_ONLY.test(w) && !NUMBER_MARKER.test(w))\n .slice(0, strategy.count);\n if (words.length === 0) {\n return null;\n }\n // Find span of the first real word\n const firstWord = words[0];\n if (firstWord === undefined) {\n return null;\n }\n const firstIdx = cellText.indexOf(firstWord);\n let actualEnd = firstIdx + firstWord.length;\n // If multiple words, extend to last one\n let searchPos = actualEnd;\n for (let wi = 1; wi < words.length; wi++) {\n const w = words[wi];\n if (w === undefined) {\n break;\n }\n const wIdx = cellText.indexOf(w, searchPos);\n if (wIdx === -1) break;\n actualEnd = wIdx + w.length;\n searchPos = actualEnd;\n }\n return {\n start: valueStart + firstIdx,\n end: valueStart + actualEnd,\n text: cellText.slice(firstIdx, actualEnd),\n };\n }\n\n case \"company-id-value\": {\n // Work from the raw remaining text (before the\n // upstream trimStart) so we can require at least\n // a colon or whitespace separator between the\n // keyword and value (e.g., \"IČO: 12345678\" or\n // \"IČO 12345678\", but not \"IČO12345678\").\n //\n // Exception: when the trigger itself ends in a\n // number-marker character (\"n°\", \"№\", \"#\"), the\n // value can follow the marker without any\n // intervening separator (\"SIREN n°123456789\"). In\n // that case we accept an empty separator too.\n const raw = text.slice(triggerEnd);\n const triggerLastChar = text[triggerEnd - 1] ?? \"\";\n const allowEmptySep =\n triggerLastChar === \"°\" ||\n triggerLastChar === \"º\" ||\n triggerLastChar === \"№\" ||\n triggerLastChar === \"#\";\n const sepRe = allowEmptySep ? /^(?:\\s*:\\s*|\\s+|)/ : /^(?:\\s*:\\s*|\\s+)/;\n const sepMatch = sepRe.exec(raw);\n if (!sepMatch) {\n return null;\n }\n let afterSep = raw.slice(sepMatch[0].length);\n // Skip a leading number-label word (e.g.\n // \"PESEL nr 44051401458\", \"NIP numer 1234567890\",\n // \"REGON № 123456789\", \"RG nº 12.345.678-9\",\n // \"CPF n° 123.456.789-00\") that may appear between\n // the trigger and the value. The label is consumed\n // along with its trailing separator so the\n // case-insensitive prefix regex below cannot mistake\n // it for a VAT country prefix like \"cz\"/\"pl\".\n const labelMatch =\n /^(?:nr\\.?|numer|n[ºo°.]|№|no\\.?)(?:\\s*:\\s*|\\s+)/i.exec(afterSep);\n let labelOffset = 0;\n if (labelMatch) {\n labelOffset = labelMatch[0].length;\n afterSep = afterSep.slice(labelOffset);\n }\n // Value shape: optional country prefix (e.g. \"CZ\",\n // \"PL\", \"FR\"), optional whitespace, an optional\n // 2-char alphanumeric key with optional trailing\n // space (covers spaced French VAT keys whose first\n // char is a letter, like \"FR A1 123456789\" or\n // \"FR AB 123456789\"), then a digit, then 4+ value\n // chars. The trailing class permits letters so\n // alphanumeric VAT keys like \"FR1A123456789\" and\n // French NIR Corsican department codes like\n // \"1 84 12 2A 075 …\" are captured. A letter is only\n // admitted when it is glued directly to a preceding\n // digit (`(?<=\\d)[A-Z]`), so the regex cannot grow\n // past whitespace into a one-letter prose word\n // (\"SIREN 123456789 a son siège\" stops at \"9\").\n // Case-insensitive so lowercase variants\n // (\"DIČ cz12345678\", \"VAT number pl1234567890\")\n // still validate via stdnum downstream. An optional\n // 2-char alphanumeric key with optional trailing\n // space covers spaced French VAT keys whose first\n // char is a letter, like \"FR A1 123456789\" or\n // \"FR AB 123456789\". Dots are permitted inside the\n // value so dotted IDs such as Brazilian RG\n // (\"12.345.678-9\") and dotted CPF/CNPJ values\n // introduced by triggers are captured. Require at\n // least two leading digits before the dot-permitting\n // tail so single-digit dotted dates (\"6.11.2025\")\n // after triggers like \"DNI\" or \"RG\" do not slide\n // in. Stricter checksum validation for CPF/CNPJ runs\n // in the regex detector. A letter is only admitted\n // when it is glued directly to a preceding digit\n // (`(?<=\\d)[A-Z]`), so the regex cannot grow past\n // whitespace into a one-letter prose word\n // (\"SIREN 123456789 a son siège\" stops at \"9\").\n // This still captures alphanumeric VAT keys like\n // \"FR1A123456789\" and French NIR Corsican department\n // codes like \"1 84 12 2A 075 …\".\n const idMatch =\n /^[A-Z]{0,6}\\s?(?:[A-Z0-9]{2}\\s?)?\\d{2}(?:(?<=\\d)[A-Z]|[\\d\\s.\\-/]){3,}/i.exec(\n afterSep,\n );\n if (!idMatch) {\n return null;\n }\n // Optional trailing check letter must sit flush against\n // the last digit of the base match (no intervening\n // whitespace). This captures Spanish DNI / NIE / CIF /\n // NIF check letters (e.g. `DNI 12345678Z`,\n // `NIF A12345678J`) without sliding into the next word.\n const baseEndsOnDigit = /\\d$/.test(idMatch[0]);\n const trailingLetterMatch = baseEndsOnDigit\n ? /^[A-Z]/i.exec(afterSep.slice(idMatch[0].length))\n : null;\n const idRaw = trailingLetterMatch\n ? idMatch[0] + trailingLetterMatch[0]\n : idMatch[0];\n const idText = idRaw.trim();\n const leadingSpaces = idMatch[0].length - idMatch[0].trimStart().length;\n const idStart =\n triggerEnd +\n sepMatch[0].length +\n labelOffset +\n // idMatch.index is always 0 (anchored ^ regex)\n leadingSpaces;\n return {\n start: idStart,\n end: idStart + idText.length,\n text: idText,\n };\n }\n\n case \"address\": {\n // Walk through comma-separated segments.\n // Continue through a comma if the next segment\n // starts with a digit (PSČ like \"110 00\") or\n // an uppercase letter (city like \"Praha\").\n // Stop at: newline, opening paren \"(\", tab,\n // or a period that is NOT an abbreviation\n // (abbreviation = period followed by a space\n // and a lowercase letter, e.g. \"nábř. \").\n const maxLen = strategy.maxChars ?? 120;\n const UPPER_RE = /\\p{Lu}/u;\n const stopKeywords = getAddressStopKeywordsSync();\n let end = 0;\n\n while (end < valueText.length && end < maxLen) {\n const ch = valueText[end];\n\n // Hard stops: newline, opening paren.\n // Tabs are NOT hard stops — they appear as\n // formatting in structured documents between\n // trigger and value.\n if (ch === \"\\n\" || ch === \"(\") {\n break;\n }\n\n // Whitespace boundary: when an address has no\n // commas (e.g. headline-style triggers like\n // \"Adresse : 10 rue de la Paix Email : a@b.fr\"),\n // the comma-scoped stop-keyword check below\n // never fires. Re-check the stop list at every\n // whitespace boundary so a following field\n // label terminates the address before its value.\n if (ch === \" \" || ch === \"\\t\") {\n const afterWs = valueText.slice(end).trimStart().toLowerCase();\n const hitsKeyword = stopKeywords.some((kw) => {\n if (!afterWs.startsWith(kw)) return false;\n const next = afterWs[kw.length];\n return next === undefined || /[\\s:;.,!?()\\d]/.test(next);\n });\n if (hitsKeyword) {\n break;\n }\n }\n\n // Period: stop unless it's an abbreviation.\n // Abbreviation patterns in Czech addresses:\n // \"nábř. Kpt.\" — period + space + letter\n // \"č.p.\" — period + letter (no space)\n // \"1000/7.\" — period at end of address\n if (ch === \".\") {\n const next = valueText[end + 1];\n const afterNext = valueText[end + 2];\n // Field-label check: if the text after the\n // period (with optional space) begins with a\n // known stop-keyword (e.g. \"C.F.\", \"P.IVA\"),\n // treat the period as a clause boundary even\n // when followed by a letter/digit. Without\n // this, \"Via Roma 1. C.F. 12345678901\" would\n // absorb the tax-id label into the address.\n const afterPeriod = valueText\n .slice(end + 1)\n .replace(/^\\s+/, \"\")\n .toLowerCase();\n if (afterPeriod.length > 0) {\n const hitsKeywordAfterPeriod = getAddressStopKeywordsSync().some(\n (kw) => {\n if (!afterPeriod.startsWith(kw)) return false;\n const afterKw = afterPeriod[kw.length];\n return afterKw === undefined || /[\\s:;.,!?()\\d]/.test(afterKw);\n },\n );\n if (hitsKeywordAfterPeriod) {\n break;\n }\n }\n // \"č.p.\" or \"Kpt.J\" — period immediately\n // followed by letter or digit\n if (next !== undefined && (/\\p{L}/u.test(next) || /\\d/.test(next))) {\n end++;\n continue;\n }\n // \"nábř. Kpt.\" or \"ul. nová\" — period +\n // space + any letter or digit. Treat as an\n // abbreviation unless this is a genuine\n // sentence boundary (real word before, then\n // \". \" + uppercase start) — that case\n // happens when the address has already ended\n // and the next clause begins, e.g.\n // \"z siedzibą w Warszawie. Kapitał…\" /\n // \"Adresse : 10 rue de la Paix. Jean Dupont…\".\n if (\n next === \" \" &&\n afterNext !== undefined &&\n (/\\p{L}/u.test(afterNext) || /\\d/.test(afterNext))\n ) {\n if (isSentenceTerminator(valueText, end)) {\n break;\n }\n end++;\n continue;\n }\n // Period at end of address — stop but\n // don't include the period\n break;\n }\n\n // Comma: look ahead to see if address continues\n if (ch === \",\") {\n // Skip comma + whitespace, check next char\n let peek = end + 1;\n while (\n peek < valueText.length &&\n (valueText[peek] === \" \" || valueText[peek] === \"\\t\")\n ) {\n peek++;\n }\n const peekCh = valueText[peek];\n if (peekCh === undefined) {\n break;\n }\n // Check for field-label keywords after\n // comma before continuing. Keywords like\n // \"IČ\", \"DIČ\" start new fields. Loaded\n // from per-language JSON config so every\n // enabled language's stop words apply.\n const afterComma = valueText\n .slice(end + 1)\n .trimStart()\n .toLowerCase();\n const hitsKeyword = getAddressStopKeywordsSync().some((kw) => {\n if (!afterComma.startsWith(kw)) return false;\n // Guard: next char must be a delimiter\n // or digit to avoid truncating city\n // names like \"Telč\" on \"tel\". Digits\n // are included so \"IČ25672541\" (no\n // space) still triggers a stop.\n const next = afterComma[kw.length];\n return next === undefined || /[\\s:;.,!?()\\d]/.test(next);\n });\n if (hitsKeyword) {\n break;\n }\n // Continue if next segment starts with\n // digit (PSČ) or uppercase (city name)\n if (/\\d/.test(peekCh) || UPPER_RE.test(peekCh)) {\n end++;\n continue;\n }\n // Otherwise stop at this comma\n break;\n }\n\n end++;\n }\n\n // When the loop stopped at maxLen, trim back to\n // the last word boundary so fixPartialWords does\n // not extend the entity beyond the configured max.\n if (end >= maxLen) {\n const lastSpace = valueText.lastIndexOf(\" \", end - 1);\n if (lastSpace > 0) {\n end = lastSpace;\n }\n }\n\n const rawSlice = valueText.slice(0, end);\n const extracted = rawSlice.trim();\n if (extracted.length === 0) {\n return null;\n }\n const trailingSpaces = rawSlice.length - rawSlice.trimEnd().length;\n return {\n start: valueStart,\n end: valueStart + end - trailingSpaces,\n text: extracted,\n };\n }\n\n case \"match-pattern\": {\n // Anchor the configured pattern to the start of the\n // value text (after the generic leading-whitespace/\n // colon strip above). This prevents a missing or\n // placeholder value from stealing the next numeric\n // field on the same line: e.g. with a phone trigger\n // applied to `Téléphone : non communiqué SIREN :\n // 123456789`, an unanchored search would pull the\n // SIREN digits into the phone entity. Stops at the\n // first newline so a header-style trigger cannot\n // pull a value from a following line. The compiled\n // regex strips `g`/`y` flags so it stays stateless\n // across calls.\n const nlIdx = valueText.indexOf(\"\\n\");\n const searchText = nlIdx === -1 ? valueText : valueText.slice(0, nlIdx);\n if (searchText.length === 0) {\n return null;\n }\n const flags = (strategy.flags ?? \"\").replace(/[gy]/g, \"\");\n // Wrap the pattern in a non-capturing group so a\n // leading anchor authored in the config (e.g. `^`)\n // and authored alternation precedence still work\n // when the engine prepends its own start anchor.\n const anchoredSource = `^(?:${strategy.pattern})`;\n let re: RegExp;\n try {\n re = new RegExp(anchoredSource, flags);\n } catch {\n return null;\n }\n const m = re.exec(searchText);\n if (!m || m[0].length === 0) {\n return null;\n }\n const matchStart = m.index;\n const matchEnd = matchStart + m[0].length;\n return {\n start: valueStart + matchStart,\n end: valueStart + matchEnd,\n text: m[0],\n };\n }\n\n default:\n return null;\n }\n};\n\n// ── Match processor ─────────────────────────────────\n\n/**\n * Process trigger matches from the unified search.\n * Receives all matches; filters to the trigger slice\n * via sliceStart/sliceEnd. Uses fullText for value\n * extraction (the unified search runs on lowercased\n * text, but extraction needs original casing).\n */\nexport const processTriggerMatches = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText: string,\n rules: readonly TriggerRule[],\n): Entity[] => {\n const results: Entity[] = [];\n\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) {\n continue;\n }\n\n const localIdx = idx - sliceStart;\n\n // Left word-boundary: reject if preceded by a\n // letter (prevents partial keyword matches).\n if (match.start > 0 && LETTER_RE.test(fullText[match.start - 1] ?? \"\")) {\n continue;\n }\n\n const rule = rules[localIdx];\n if (!rule) {\n continue;\n }\n\n // Right word-boundary: reject only when the trigger\n // ends with a letter AND is followed by another\n // letter (which would mean the keyword bleeds into\n // a longer word, e.g. \"pan\" inside \"pana\"). Triggers\n // ending in punctuation (`:`, `'`, `’`, `°`, `.`, …)\n // or whitespace are already self-bounded: whatever\n // follows cannot extend the keyword token, so any\n // following character (letter, digit, etc.) is fine.\n const triggerLastChar = rule.trigger.at(-1) ?? \"\";\n if (\n LETTER_RE.test(triggerLastChar) &&\n LETTER_RE.test(fullText[match.end] ?? \"\")\n ) {\n continue;\n }\n\n const triggerEnd = match.end;\n const rawValue = extractValue(\n fullText,\n triggerEnd,\n rule.strategy,\n rule.label,\n );\n const value = rawValue ? stripQuotes(rawValue) : null;\n\n if (value) {\n // Apply declarative validations to the captured\n // value text (not the full entity including\n // trigger). This is intentional: validations\n // like min-length should test the extracted\n // value, not the trigger keyword itself.\n if (!applyValidations(value.text, rule.validations)) {\n continue;\n }\n\n // Label-shape invariant: a phone-number entity\n // must start like a phone value and contain enough\n // digits. Triggers like\n // a multilingual \"Phone:\" / \"PHONE:\" / \"Tel.:\"\n // can fire on signature blocks where the digit\n // value is blank (\"Phone: Date: 2026-05-15 ...\");\n // without this check the strategy emits a long\n // high-priority phone entity that can overlap and\n // suppress the later real phone number.\n if (\n rule.label === \"phone number\" &&\n !isPlausiblePhoneTriggerValue(value.text)\n ) {\n continue;\n }\n\n // When includeTrigger is set, the entity span\n // starts at the trigger match, not the value.\n const entityStart = rule.includeTrigger ? match.start : value.start;\n const entityEnd = value.end;\n const entityText = fullText.slice(entityStart, entityEnd);\n\n // Legal form reclassification: any person-labeled\n // trigger whose captured text contains a definitive\n // legal form suffix is reclassified as organization.\n // This is universal — every person with a legal form\n // is an organisation, no per-group config needed.\n const effectiveLabel =\n rule.label === \"person\" && hasKnownLegalFormSuffix(entityText)\n ? \"organization\"\n : rule.label;\n\n results.push({\n start: entityStart,\n end: entityEnd,\n label: effectiveLabel,\n text: entityText,\n score: TRIGGER_SCORE,\n source: DETECTION_SOURCES.TRIGGER,\n });\n }\n }\n\n return results;\n};\n\n// Re-export for testing\nexport { expandTriggerGroups, compileValidations, applyValidations };\n","/**\n * Geographic regions and country code mappings for\n * scoping deny list dictionaries.\n */\n\nexport const REGIONS = {\n Global: null,\n International: null,\n Europe: [\n \"AL\",\n \"AD\",\n \"AT\",\n \"BE\",\n \"BA\",\n \"BG\",\n \"HR\",\n \"CY\",\n \"CZ\",\n \"DK\",\n \"EE\",\n \"FI\",\n \"FR\",\n \"DE\",\n \"GR\",\n \"HU\",\n \"IS\",\n \"IE\",\n \"IT\",\n \"XK\",\n \"LV\",\n \"LI\",\n \"LT\",\n \"LU\",\n \"MD\",\n \"ME\",\n \"MK\",\n \"MT\",\n \"MC\",\n \"NL\",\n \"NO\",\n \"PL\",\n \"PT\",\n \"RO\",\n \"RS\",\n \"SK\",\n \"SI\",\n \"ES\",\n \"SE\",\n \"CH\",\n \"UA\",\n \"GB\",\n ],\n Americas: [\n \"US\",\n \"CA\",\n \"MX\",\n \"BR\",\n \"AR\",\n \"CL\",\n \"CO\",\n \"PE\",\n \"EC\",\n \"VE\",\n \"UY\",\n \"PY\",\n \"BO\",\n \"CR\",\n \"PA\",\n \"DO\",\n \"GT\",\n \"HN\",\n \"SV\",\n \"NI\",\n \"CU\",\n ],\n AsiaPacific: [\n \"AU\",\n \"NZ\",\n \"JP\",\n \"KR\",\n \"CN\",\n \"TW\",\n \"SG\",\n \"MY\",\n \"TH\",\n \"VN\",\n \"PH\",\n \"ID\",\n \"IN\",\n \"PK\",\n \"BD\",\n \"LK\",\n \"NP\",\n \"HK\",\n \"MO\",\n ],\n MENA: [\n \"AE\",\n \"SA\",\n \"IL\",\n \"TR\",\n \"EG\",\n \"JO\",\n \"LB\",\n \"IQ\",\n \"IR\",\n \"QA\",\n \"KW\",\n \"BH\",\n \"OM\",\n \"MA\",\n \"TN\",\n \"DZ\",\n \"LY\",\n \"SY\",\n \"YE\",\n \"PS\",\n ],\n SubSaharanAfrica: [\n \"ZA\",\n \"NG\",\n \"KE\",\n \"GH\",\n \"TZ\",\n \"ET\",\n \"SN\",\n \"CI\",\n \"CM\",\n \"UG\",\n \"RW\",\n \"MZ\",\n \"AO\",\n \"ZW\",\n \"BW\",\n \"NA\",\n \"MU\",\n ],\n EU: [\n \"AT\",\n \"BE\",\n \"BG\",\n \"HR\",\n \"CY\",\n \"CZ\",\n \"DK\",\n \"EE\",\n \"FI\",\n \"FR\",\n \"DE\",\n \"GR\",\n \"HU\",\n \"IE\",\n \"IT\",\n \"LV\",\n \"LT\",\n \"LU\",\n \"MT\",\n \"NL\",\n \"PL\",\n \"PT\",\n \"RO\",\n \"SK\",\n \"SI\",\n \"ES\",\n \"SE\",\n ],\n DACH: [\"DE\", \"AT\", \"CH\"],\n Nordics: [\"DK\", \"SE\", \"NO\", \"FI\", \"IS\"],\n CEE: [\"CZ\", \"SK\", \"PL\", \"HU\", \"RO\", \"BG\", \"HR\", \"SI\", \"LT\", \"LV\", \"EE\"],\n Anglosphere: [\"GB\", \"US\", \"CA\", \"AU\", \"NZ\", \"IE\"],\n Benelux: [\"BE\", \"NL\", \"LU\"],\n GulfStates: [\"AE\", \"SA\", \"QA\", \"KW\", \"BH\", \"OM\"],\n SouthAsia: [\"IN\", \"PK\", \"BD\", \"LK\", \"NP\"],\n EastAsia: [\"CN\", \"JP\", \"KR\", \"TW\"],\n SoutheastAsia: [\"SG\", \"MY\", \"TH\", \"VN\", \"PH\", \"ID\"],\n Oceania: [\"AU\", \"NZ\"],\n} as const;\n\nexport type RegionId = keyof typeof REGIONS;\n\ntype RegionArrays = {\n [K in RegionId]: (typeof REGIONS)[K];\n};\n\ntype NonNullRegion = {\n [K in RegionId as RegionArrays[K] extends null ? never : K]: RegionArrays[K];\n};\n\nexport type CountryCode = NonNullRegion[keyof NonNullRegion][number];\n\n/**\n * Expand region names to country codes and merge with\n * explicit country codes. Returns null when both inputs\n * are empty/undefined (meaning \"match all countries\").\n */\nexport const resolveCountries = (\n regions?: string[],\n countries?: string[],\n): Set<string> | null => {\n const hasRegions = regions && regions.length > 0;\n const hasCountries = countries && countries.length > 0;\n\n if (!hasRegions && !hasCountries) {\n return null;\n }\n\n const result = new Set<string>();\n\n const isRegion = (name: string): name is RegionId => name in REGIONS;\n\n if (hasRegions) {\n for (const name of regions) {\n if (!isRegion(name)) {\n continue;\n }\n const codes = REGIONS[name];\n if (codes === null) {\n // Global/International: return null (all)\n return null;\n }\n for (const code of codes) {\n result.add(code);\n }\n }\n }\n\n if (hasCountries) {\n for (const code of countries) {\n result.add(code);\n }\n }\n\n return result;\n};\n","/**\n * Fold Cyrillic and Greek lookalike characters to their\n * Latin equivalents for stop-list lookups.\n *\n * Adversarial inputs and OCR artifacts can produce\n * mixed-script tokens like \"mаnager\" (with a Cyrillic\n * `а`) that visually read as Latin but bypass a Latin-\n * keyed stop-list. Folding before lookup closes that\n * gap.\n *\n * Only used inside stop-list / false-positive checks —\n * never on stored entity text. Genuine Cyrillic words\n * never pass through this path. Replacements are same-\n * length (single code unit → single code unit), so\n * offsets remain valid if a caller needs them.\n *\n * Mappings are restricted to letters that are visually\n * indistinguishable from Latin in common fonts. Glyphs\n * whose lowercase form does not resemble Latin (e.g.\n * Cyrillic lowercase `в`, which looks nothing like\n * lowercase `b`) are omitted from the lowercase\n * section even when their uppercase form is included.\n */\n\nconst REPLACEMENTS: readonly [number, number][] = [\n // ── Cyrillic uppercase ───────────────────────────────\n [0x0410, 0x0041], // А → A\n [0x0412, 0x0042], // В → B\n [0x0415, 0x0045], // Е → E\n [0x041a, 0x004b], // К → K\n [0x041c, 0x004d], // М → M\n [0x041d, 0x0048], // Н → H\n [0x041e, 0x004f], // О → O\n [0x0420, 0x0050], // Р → P\n [0x0421, 0x0043], // С → C\n [0x0422, 0x0054], // Т → T\n [0x0425, 0x0058], // Х → X\n // ── Cyrillic lowercase ───────────────────────────────\n [0x0430, 0x0061], // а → a\n [0x0435, 0x0065], // е → e\n [0x043e, 0x006f], // о → o\n [0x0440, 0x0070], // р → p\n [0x0441, 0x0063], // с → c\n [0x0443, 0x0079], // у → y\n [0x0445, 0x0078], // х → x\n // ── Other Cyrillic (Ukrainian/Belarusian/Serbian) ────\n [0x0456, 0x0069], // і → i\n [0x0458, 0x006a], // ј → j\n // ── Greek uppercase ──────────────────────────────────\n [0x0391, 0x0041], // Α → A\n [0x0392, 0x0042], // Β → B\n [0x0395, 0x0045], // Ε → E\n [0x0397, 0x0048], // Η → H\n [0x0399, 0x0049], // Ι → I\n [0x039a, 0x004b], // Κ → K\n [0x039c, 0x004d], // Μ → M\n [0x039d, 0x004e], // Ν → N\n [0x039f, 0x004f], // Ο → O\n [0x03a1, 0x0050], // Ρ → P\n [0x03a4, 0x0054], // Τ → T\n [0x03a5, 0x0059], // Υ → Y\n [0x03a7, 0x0058], // Χ → X\n [0x0396, 0x005a], // Ζ → Z\n // ── Greek lowercase ──────────────────────────────────\n [0x03b1, 0x0061], // α → a\n [0x03ba, 0x006b], // κ → k\n [0x03bf, 0x006f], // ο → o\n [0x03c1, 0x0070], // ρ → p\n];\n\nconst CHAR_MAP = new Map<number, number>(REPLACEMENTS);\n\n/** Chunk size for `String.fromCharCode` to avoid hitting\n * the call-stack limit on very large strings. */\nconst CHUNK_SIZE = 8192;\n\nexport const normalizeHomoglyphs = (text: string): string => {\n let hasSpecial = false;\n for (let i = 0; i < text.length; i++) {\n if (CHAR_MAP.has(text.charCodeAt(i))) {\n hasSpecial = true;\n break;\n }\n }\n if (!hasSpecial) return text;\n\n const len = text.length;\n const codes = new Uint16Array(len);\n for (let i = 0; i < len; i++) {\n const code = text.charCodeAt(i);\n codes[i] = CHAR_MAP.get(code) ?? code;\n }\n\n if (len <= CHUNK_SIZE) {\n return String.fromCharCode(...codes);\n }\n\n let result = \"\";\n for (let offset = 0; offset < len; offset += CHUNK_SIZE) {\n const end = Math.min(offset + CHUNK_SIZE, len);\n result += String.fromCharCode(...codes.subarray(offset, end));\n }\n return result;\n};\n","import type { Entity } from \"../types\";\nimport type { PipelineContext } from \"../context\";\nimport { defaultContext } from \"../context\";\nimport { getPersonStopwords } from \"../detectors/deny-list\";\nimport { normalizeHomoglyphs } from \"../util/homoglyphs\";\n\nconst TEMPLATE_PLACEHOLDER_RE = /^(?:\\.{3,}|_{3,}|\\[[\\w\\s]+\\]|\\{[\\w\\s]+\\})$/;\n\n// Patterns that indicate a genuine address (not prose).\nconst POSTAL_CODE_RE = /\\d{3}\\s?\\d{2}/;\nconst HAS_DIGIT_RE = /\\d/;\n// Address-component anchors not derived from per-language\n// street-type vocabulary: Czech house/parcel number forms\n// (č.p., č.ev., č.) and \"sídliště\" (housing estate) which\n// is a settlement type rather than a street type. Polish,\n// pt-BR, English, etc. street vocabulary is loaded from\n// address-street-types.json via initAddressComponents.\nconst ADDRESS_COMPONENT_EXTRA_RE =\n /(?:^|\\s)(?:č\\.p\\.|č\\.ev\\.|č\\.|sídliště)(?=[\\s,./]|$)/i;\n\n// Bare French \"cours\" is ambiguous: \"Cours Mirabeau\" is a\n// genuine street, but \"au cours du contrat\" is prose. Real\n// addresses either include digits (caught by the prior\n// rule) or a capitalized proper-name token right after\n// `cours`. Returns true when the only address-component\n// token in `text` is bare lowercase `cours` AND it is not\n// followed by a capitalized name token, i.e. the entity is\n// almost certainly prose and should be rejected.\nconst BARE_COURS_PROSE_RE = /(?:^|\\s)cours(?!\\s+\\p{Lu})(?=[\\s,./]|$)/u;\nconst isOnlyAmbiguousCours = (text: string): boolean => {\n if (!BARE_COURS_PROSE_RE.test(text)) return false;\n // Strip every bare \"cours\" occurrence; if anything else\n // still matches a street-type word, the entity has\n // other street-type evidence and is not \"only cours\".\n const stripped = text.replace(/(?:^|\\s)cours(?=[\\s,./]|$)/gi, \" \");\n return !hasAddressComponent(stripped);\n};\n\n// Jurisdiction patterns: \"State of X\", \"Commonwealth of X\",\n// \"District of X\", \"Territory of X\"\n// — valid address entities without digits or street words.\nconst JURISDICTION_RE = /^(?:state|commonwealth|district|territory)\\s+of\\s+/i;\n\n// Max entity text length by label. Prevents runaway\n// trigger extractions (e.g., \"město Dobříš i okolních\n// obcí...\") from producing absurdly long entities.\nconst MAX_ENTITY_LENGTH: Partial<Record<string, number>> = {\n organization: 80,\n person: 60,\n};\n\n// Per-label upper bound on word count. Real-world\n// organisation names cap out well below this even for\n// long firm names (\"European Bank for Reconstruction\n// and Development\" — 6). A trigger or coreference span\n// running past this is almost certainly absorbing prose.\n// Only open-ended sources (trigger, coreference) are\n// subject to this cap: gazetteer/NER/regex detections\n// are bounded by their dictionary, model, or pattern\n// and may legitimately span longer names like \"The\n// University of Texas Health Science Center at Houston\".\nconst MAX_ENTITY_WORDS: Partial<Record<string, number>> = {\n organization: 8,\n};\nconst OPEN_ENDED_SOURCES: ReadonlySet<string> = new Set([\n \"trigger\",\n \"coreference\",\n]);\nconst WORD_TOKEN_RE = /\\p{L}[\\p{L}\\p{M}\\p{N}'’\\-./]*/gu;\nconst countWordTokens = (text: string): number => {\n let count = 0;\n WORD_TOKEN_RE.lastIndex = 0;\n while (WORD_TOKEN_RE.exec(text) !== null) count += 1;\n return count;\n};\n\n// Lines whose visible letters are overwhelmingly\n// uppercase are EITHER:\n// - heading/boilerplate prose (SEC securities legends,\n// \"THIS INSTRUMENT AND ANY SECURITIES ...\" clauses,\n// numbered section headings such as \"17.NO\n// ASSIGNMENT.\"),\n// - or genuine party captions where a real org name\n// happens to be rendered in caps on its own line\n// (\"TWITTER, INC.\", \"X HOLDINGS I, INC.\").\n// The old guard rejected both cases. We now keep\n// captions: the deciding signal is whether the line\n// contains substantial *prose* outside the candidate\n// span. SEC legends sprawl across the line; captions\n// occupy nearly all of it. A numbered section heading\n// (\"17.NO ASSIGNMENT.\") is also rejected outright\n// because its all-caps span is part of the section\n// title, not a party name.\nconst ALL_CAPS_LINE_LETTER_THRESHOLD = 5;\nconst ALL_CAPS_LINE_RATIO = 0.95;\nconst ALL_CAPS_LINE_PROSE_EXTRA_LETTERS = 20;\n// A genuine party caption has the shape \"Name[, ]Suffix\"\n// — short, with a comma marking the suffix break (or no\n// suffix-style separator at all). Heading lines such as\n// \"REGISTRATION STATEMENT\" or Czech \"RÁMCOVÁ DOHODA NA\n// POSKYTOVÁNÍ PRÁVNÍCH SLUŽEB\" are longer prose-style\n// sequences without a comma. The word-count cutoff is\n// deliberately permissive; legitimate firm names like\n// \"European Bank for Reconstruction and Development\"\n// (6 words) keep their comma + legal-form suffix, so\n// they retain the suffix break test above.\nconst ALL_CAPS_LINE_HEADING_WORD_LIMIT = 5;\n// Section-number prefix at the start of a line, with\n// optional leading whitespace so indented headings\n// (\" 17. NO ASSIGNMENT.\", \"§ 3\") still match. The\n// trailing uppercase character anchors the title token.\nconst SECTION_HEADING_PREFIX_RE =\n /^\\s*(?:§\\s*)?\\d{1,3}(?:\\.\\d{1,3}){0,4}\\.?\\s*\\p{Lu}/u;\nconst LINE_LETTER_RE = /\\p{L}/gu;\nconst ENTITY_WORD_TOKEN_RE = /\\p{L}[\\p{L}\\p{M}\\p{N}'’-]*/gu;\nconst isAllCapsBoilerplateLine = (\n fullText: string,\n start: number,\n length: number,\n): boolean => {\n const lineStart = fullText.lastIndexOf(\"\\n\", start) + 1;\n const lineEndIdx = fullText.indexOf(\"\\n\", start + length);\n const line = fullText.slice(\n lineStart,\n lineEndIdx === -1 ? fullText.length : lineEndIdx,\n );\n let letterCount = 0;\n let upperCount = 0;\n let outsideEntityLetters = 0;\n const entityRelStart = start - lineStart;\n const entityRelEnd = entityRelStart + length;\n LINE_LETTER_RE.lastIndex = 0;\n for (\n let m = LINE_LETTER_RE.exec(line);\n m !== null;\n m = LINE_LETTER_RE.exec(line)\n ) {\n const ch = m[0];\n letterCount += 1;\n if (ch === ch.toUpperCase() && ch !== ch.toLowerCase()) {\n upperCount += 1;\n }\n if (m.index < entityRelStart || m.index >= entityRelEnd) {\n outsideEntityLetters += 1;\n }\n }\n if (letterCount <= ALL_CAPS_LINE_LETTER_THRESHOLD) return false;\n if (upperCount / letterCount < ALL_CAPS_LINE_RATIO) return false;\n // Numbered section headings are always boilerplate.\n if (SECTION_HEADING_PREFIX_RE.test(line)) return true;\n // SEC legend / paragraph: substantial all-caps prose\n // outside the entity span.\n if (outsideEntityLetters >= ALL_CAPS_LINE_PROSE_EXTRA_LETTERS) return true;\n // Heading-shape inside the entity itself: many words\n // and no comma to mark a Name+Suffix break. Czech\n // section titles (\"RÁMCOVÁ DOHODA NA POSKYTOVÁNÍ\n // PRÁVNÍCH SLUŽEB\") match here. Real captions like\n // \"ACME CORPORATION\" (2 words, no comma) and\n // \"TWITTER, INC.\" (comma) survive.\n const entityText = fullText.slice(start, start + length);\n const entityWordCount = (entityText.match(ENTITY_WORD_TOKEN_RE) ?? []).length;\n if (\n entityWordCount > ALL_CAPS_LINE_HEADING_WORD_LIMIT &&\n !entityText.includes(\",\")\n ) {\n return true;\n }\n return false;\n};\nconst isAllCapsCandidate = (text: string): boolean =>\n text === text.toUpperCase() && /\\p{Lu}/u.test(text);\n// Section/clause numbers: \"§ 3\", \"3.2.1\", \"12.\" but NOT\n// dates like \"4.3.2026\" or long digit strings like IČO.\n// A section number has 1-3 digit groups of 1-3 digits each,\n// never ending with a 4-digit group (that's a year).\nconst SECTION_NUMBER_RE = /^(?:§\\s*)?\\d{1,3}(?:\\.\\d{1,3}){0,4}\\.?$/;\nconst STANDALONE_YEAR_RE = /^(?:19|20)\\d{2}$/;\n\n// Number-abbreviation prefixes: \"č.\", \"Nr.\", \"No.\", \"nr.\",\n// \"no.\", \"n.\", \"čís.\" — when a numeric entity is preceded\n// by one of these, it's a reference number, not PII.\nconst NUMBER_ABBREV_RE = /(?:^|[\\s(])(?:č|čís|nr|no|n)\\.\\s*$/i;\nconst SIGNING_CLAUSE_ADDRESS_RE = /^(?:v|ve)\\s+[^\\d,\\n]{1,40},?\\s+dne$/iu;\nconst PERSON_TRAILING_NOUNS: ReadonlySet<string> = new Set([\n \"association\",\n \"period\",\n \"reform\",\n]);\nconst LEGAL_FORM_HEADING_RE = /\\b(?:agreement|amendment|contract|exhibit)\\b/iu;\nconst LEADING_ARTIFACT_RE = /^(?:\\.\\s)+/u;\nconst ADDRESS_ROLE_PREFIX_RE =\n /^(?:prodávajícího|kupujícího|objednatele|zhotovitele|pronajímatele|dodavatele|odběratele|zaměstnance|zaměstnavatele|nájemce)\\s+(?=(?:\\p{Lu}|\\d|ul\\.?|ulice|nám\\.?|náměstí|tř\\.?|třída|nábř\\.?|nábřeží|č\\.p\\.?|č\\.ev\\.?|sídliště))/iu;\nconst ADDRESS_INLINE_ABBREV_AFTER_RE =\n /^(?:\\p{Lu}[\\p{L}\\p{M}]{0,3}\\.|ul\\.?|nám\\.?|tř\\.?|nábř\\.?|č\\.p\\.?|č\\.ev\\.?)/u;\nconst ADDRESS_INLINE_ABBREV_BEFORE_RE =\n /(?:^|[\\s,])(?:st|ave|rd|dr|blvd|ln|hwy|pkwy|cir|ct|pl|sq|ter|trl|ste|apt|bldg|fl|ul|nám|tř|nábř|č\\.p|č\\.ev)$/iu;\nconst ADDRESS_CONTINUATION_WORD_RE =\n /^(?:suite|building|floor|unit|apartment|room|tower|wing|block|bldg|ste|apt|fl)\\b/iu;\n\nconst trimTrailingAddressProse = (text: string): string => {\n for (const match of text.matchAll(/\\.(?=\\s+\\p{Lu})/gu)) {\n const cutoff = match.index;\n if (cutoff === undefined) {\n continue;\n }\n const before = text.slice(0, cutoff);\n if (!HAS_DIGIT_RE.test(before)) {\n continue;\n }\n const after = text.slice(cutoff + 1).trimStart();\n if (\n after.length < 5 ||\n ADDRESS_INLINE_ABBREV_AFTER_RE.test(after) ||\n ADDRESS_INLINE_ABBREV_BEFORE_RE.test(before.trimEnd()) ||\n ADDRESS_CONTINUATION_WORD_RE.test(after)\n ) {\n continue;\n }\n return before.trimEnd();\n }\n\n return text;\n};\n\nconst normalizeEntity = (entity: Entity): Entity | null => {\n let start = entity.start;\n let text = entity.text;\n\n const trimLeading = (re: RegExp) => {\n const match = re.exec(text);\n if (!match) {\n return;\n }\n start += match[0].length;\n text = text.slice(match[0].length);\n };\n\n trimLeading(LEADING_ARTIFACT_RE);\n trimLeading(/^\\s+/u);\n\n if (entity.label === \"address\") {\n trimLeading(ADDRESS_ROLE_PREFIX_RE);\n text = trimTrailingAddressProse(text);\n }\n\n const trailingMatch = /[,\\s]+$/u.exec(text);\n if (trailingMatch) {\n text = text.slice(0, text.length - trailingMatch[0].length);\n }\n\n if (text.length === 0) {\n return null;\n }\n\n return {\n ...entity,\n start,\n end: start + text.length,\n text,\n };\n};\n\n// ── Document-structure headings (lazy-loaded from JSON) ──\n//\n// Per-language list of heading words (`příloha`, `anlage`,\n// `schedule`, …) that the trigger detector emits as organisations\n// when they precede an ordinal-abbreviation+digit shape\n// (`č. 2`, `Nr. 3`, `No. 4`). The set is loaded once and cached on\n// the module — the heading vocabulary is language-data, not state.\n\nlet cachedHeadingRe: RegExp | null = null;\nlet cachedHeadingPromise: Promise<RegExp> | null = null;\nconst ORDINAL_MARKER = \"(?:č|no|nr|n)\\\\.?\";\n\nconst buildHeadingRegex = (words: readonly string[]): RegExp => {\n if (words.length === 0) {\n return /[\\s\\S](?!)/u;\n }\n const sorted = [...words].sort((a, b) => b.length - a.length);\n const escaped = sorted\n .map((w) => w.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"))\n .join(\"|\");\n return new RegExp(\n `^(?:${escaped})[\\\\s\\\\u00a0]+(?:${ORDINAL_MARKER}|#)[\\\\s\\\\u00a0]*\\\\d`,\n \"iu\",\n );\n};\n\nconst loadHeadingWords = async (): Promise<readonly string[]> => {\n try {\n const mod = await import(\"../data/document-structure-headings.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON shape\n const data = (mod as { default?: Record<string, unknown> }).default ?? mod;\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON shape\n const entries = Object.entries(data as Record<string, unknown>);\n const out: string[] = [];\n const seen = new Set<string>();\n for (const [key, value] of entries) {\n if (key.startsWith(\"_\")) continue;\n if (!Array.isArray(value)) continue;\n for (const word of value) {\n if (typeof word !== \"string\" || word.length === 0) continue;\n const lc = word.toLowerCase();\n if (seen.has(lc)) continue;\n seen.add(lc);\n out.push(lc);\n }\n }\n return out;\n } catch {\n return [];\n }\n};\n\nexport const loadDocumentStructureHeadings = async (): Promise<void> => {\n if (cachedHeadingRe) return;\n cachedHeadingPromise ??= loadHeadingWords().then(buildHeadingRegex);\n cachedHeadingRe = await cachedHeadingPromise;\n};\n\nconst isDocumentStructureHeading = (text: string): boolean => {\n const re = cachedHeadingRe;\n if (!re) return false;\n return re.test(text);\n};\n\n// ── Generic roles (lazy-loaded from JSON) ────────────\n\nconst EMPTY_GENERIC_ROLES: ReadonlySet<string> = new Set();\n\n/**\n * Load generic-roles.json and cache the result on the\n * given context. Must be awaited during pipeline init\n * so the sync accessor is populated before\n * filterFalsePositives runs.\n */\nexport const loadGenericRoles = (\n ctx: PipelineContext = defaultContext,\n): Promise<ReadonlySet<string>> => {\n if (ctx.genericRolesPromise) {\n return ctx.genericRolesPromise;\n }\n ctx.genericRolesPromise = (async () => {\n try {\n const mod: {\n default?: { roles?: string[] };\n } = await import(\"../data/generic-roles.json\");\n const set: ReadonlySet<string> = new Set(mod.default?.roles ?? []);\n ctx.genericRoles = set;\n return set;\n } catch {\n const empty: ReadonlySet<string> = new Set();\n ctx.genericRoles = empty;\n return empty;\n }\n })();\n return ctx.genericRolesPromise;\n};\n\n/** Sync accessor — returns empty set before init. */\nconst getGenericRoles = (ctx: PipelineContext): ReadonlySet<string> =>\n ctx.genericRoles ?? EMPTY_GENERIC_ROLES;\n\n// ── Street-type vocabulary (lazy-loaded from JSON) ───\n//\n// Builds a single regex from address-street-types.json\n// that recognises any per-language street word (Polish\n// \"aleja\", \"ulicy\"; English \"Street\", \"Avenue\"; etc.) as\n// a genuine address component. Falls back to a permissive\n// match-nothing regex before init.\n\n// Baseline regex used when address-street-types.json\n// has not been loaded yet (e.g. callers using\n// `filterFalsePositives` directly without going through\n// `runPipeline`). Mirrors the previous hardcoded Czech\n// street-word anchors so trigger-sourced digitless\n// Czech addresses (\"Národní třída\") still survive the\n// digit gate before initAddressComponents() runs.\nconst STREET_TYPES_SEED_RE =\n /(?:^|\\s)(?:ul\\.|ulice|nám\\.|náměstí|tř\\.|třída|nábř\\.|nábřeží|bulvár)(?=[\\s,./]|$)/i;\nlet _streetTypesRe: RegExp = STREET_TYPES_SEED_RE;\nlet _streetTypesPromise: Promise<void> | null = null;\n\nconst escapeRegex = (s: string): string =>\n s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\nconst loadStreetTypeRegex = async (): Promise<void> => {\n try {\n const mod: { default?: Record<string, string[] | string> } =\n await import(\"../data/address-street-types.json\");\n const data = mod.default ?? {};\n const words = new Set<string>();\n for (const [key, val] of Object.entries(data)) {\n if (key.startsWith(\"_\")) continue;\n if (!Array.isArray(val)) continue;\n for (const w of val) {\n if (typeof w === \"string\" && w.length > 0) {\n words.add(w.toLowerCase());\n }\n }\n }\n if (words.size === 0) {\n _streetTypesRe = STREET_TYPES_SEED_RE;\n return;\n }\n // Longest first so \"aleja\" doesn't shadow \"al.\"\n const alternation = [...words]\n .sort((a, b) => b.length - a.length)\n .map(escapeRegex)\n .join(\"|\");\n _streetTypesRe = new RegExp(\n `(?:^|\\\\s)(?:${alternation})(?=[\\\\s,./]|$)`,\n \"iu\",\n );\n } catch {\n _streetTypesRe = STREET_TYPES_SEED_RE;\n }\n};\n\n/** Ensure street-type vocabulary is loaded. */\nexport const initAddressComponents = (): Promise<void> => {\n if (!_streetTypesPromise) {\n _streetTypesPromise = loadStreetTypeRegex();\n }\n return _streetTypesPromise;\n};\n\n/**\n * True when `text` contains a recognised address\n * component: either a per-language street word (loaded\n * from JSON) or a language-agnostic anchor such as a\n * Czech house number form.\n */\nconst hasAddressComponent = (text: string): boolean =>\n _streetTypesRe.test(text) || ADDRESS_COMPONENT_EXTRA_RE.test(text);\n\nconst isCallerOwnedEntity = (entity: Entity): boolean =>\n entity.sourceDetail === \"custom-deny-list\" ||\n entity.sourceDetail === \"custom-regex\";\n\n/**\n * Filter out entities that are likely false positives:\n * template placeholders, clause/section numbers,\n * standalone years, and generic legal role terms.\n *\n * Runs as a post-processing step after all detection\n * layers have merged.\n */\nexport const filterFalsePositives = (\n entities: Entity[],\n ctx: PipelineContext = defaultContext,\n fullText?: string,\n): Entity[] => {\n const filtered: Entity[] = [];\n const roles = getGenericRoles(ctx);\n\n for (const entity of entities) {\n if (isCallerOwnedEntity(entity)) {\n filtered.push(entity);\n continue;\n }\n\n const normalized = normalizeEntity(entity);\n if (!normalized) {\n continue;\n }\n\n // Strip leading \". \" artifacts from trigger extraction\n // after abbreviations (\"dat. nar.\", \"č.p.\").\n const trimmed = normalized.text;\n\n if (isCallerOwnedEntity(normalized)) {\n filtered.push(normalized);\n continue;\n }\n\n if (TEMPLATE_PLACEHOLDER_RE.test(trimmed)) {\n continue;\n }\n // Reject entities exceeding max length for their\n // label (prevents runaway trigger extractions).\n // Exempt legal-form entities: their span is already\n // bounded by the regex pattern, not open-ended.\n const maxLen = MAX_ENTITY_LENGTH[normalized.label];\n if (\n maxLen &&\n trimmed.length > maxLen &&\n normalized.source !== \"legal-form\"\n ) {\n continue;\n }\n // Word-count cap. The byte-length cap above only\n // catches truly runaway spans; a 70-char trigger\n // capture full of short jargon words still slips\n // through despite being clearly prose. Org names in\n // practice cap out at 6 words even for verbose firm\n // names; 8 leaves headroom without admitting a full\n // boilerplate clause.\n const maxWords = MAX_ENTITY_WORDS[normalized.label];\n if (\n maxWords &&\n OPEN_ENDED_SOURCES.has(normalized.source) &&\n countWordTokens(trimmed) > maxWords\n ) {\n continue;\n }\n // SEC-style legends, numbered section headings\n // (\"17.NO ASSIGNMENT.\"), and other boilerplate\n // disclosure blocks render as all-uppercase lines.\n // Detectors anchored to uppercase tokens otherwise\n // emit bigrams like \"SECURITIES ACT\" or\n // \"REGISTRATION STATEMENT\" as organization spans,\n // and the legal-form regex matches headings such as\n // \"NO ASSIGNMENT\". Real party captions almost always\n // carry a legal-form suffix (\"ACME CORPORATION\")\n // and survive via the legal-forms detector's own\n // 3-word-on-mixed-case pathway, so we gate every\n // all-caps organization candidate whose surrounding\n // line is itself all-caps regardless of source.\n if (\n fullText &&\n normalized.label === \"organization\" &&\n isAllCapsCandidate(trimmed) &&\n isAllCapsBoilerplateLine(fullText, normalized.start, trimmed.length)\n ) {\n continue;\n }\n // Section numbers (§ 3, 3.2.1, 12.) are false\n // positives unless they were captured by a trigger\n // phrase (e.g., \"č.p. 92\" is an address, not a\n // section number).\n if (SECTION_NUMBER_RE.test(trimmed) && normalized.source !== \"trigger\") {\n continue;\n }\n // Standalone years (2022, 1995) without a trigger\n // context are noise. Trigger-sourced years are\n // valid (\"rok 2022\", \"year 2019\").\n if (STANDALONE_YEAR_RE.test(trimmed) && normalized.source !== \"trigger\") {\n continue;\n }\n\n // Numeric entities preceded by a number abbreviation\n // (\"č.\", \"Nr.\", \"No.\") are usually reference\n // numbers, not PII. Apply this only to non-trigger\n // entities: trigger-based detections intentionally\n // use these abbreviations as their semantic anchor\n // (e.g. \"parc. č. 852/2\", \"LV č. 154\",\n // \"Flurstück Nr. 1234\").\n if (\n fullText &&\n normalized.source !== \"trigger\" &&\n /^\\d/.test(trimmed) &&\n NUMBER_ABBREV_RE.test(\n fullText.slice(Math.max(0, normalized.start - 10), normalized.start),\n )\n ) {\n continue;\n }\n\n if (\n normalized.label === \"registration number\" &&\n /^[\\p{L}]{1,2}$/u.test(trimmed)\n ) {\n continue;\n }\n\n // Document-structure headings (Czech `Příloha č.2`, German\n // `Anlage Nr. 3`, English `Schedule No. 4`) get captured by the\n // trigger detector as organizations. They are scaffolding, not\n // party references. The heading vocabulary lives in\n // `data/document-structure-headings.json`; the regex composes the\n // current word set with the cross-language ordinal abbreviations.\n if (\n normalized.label === \"organization\" &&\n isDocumentStructureHeading(trimmed)\n ) {\n continue;\n }\n\n // Person names never contain digits.\n // \"Solution Pack ABL90 Flex\" → reject.\n if (normalized.label === \"person\" && HAS_DIGIT_RE.test(trimmed)) {\n continue;\n }\n\n // Demonstrative pronouns and other words that collide\n // with rare given names in the corpus (e.g. Czech\n // \"Tato\" — both a sentence-opening demonstrative and\n // an Italian/Iberian first name). The deny-list path\n // applies this filter itself; this catches the same\n // tokens when they leak out of NER or any other\n // single-token person source.\n if (normalized.label === \"person\") {\n const trimmedToken = trimmed.replace(/[.,;:!?]+$/u, \"\").trim();\n if (\n !/\\s/.test(trimmedToken) &&\n getPersonStopwords(ctx).has(trimmedToken.toLowerCase())\n ) {\n continue;\n }\n }\n\n if (normalized.label === \"person\") {\n const tokens = trimmed.split(/\\s+/u);\n // Fold homoglyphs *before* lowercasing: uppercase\n // Greek lookalikes (Α, Ε, …) lowercase to Greek\n // lowercase code points that aren't in the\n // homoglyph map, so the order matters.\n const last = tokens.at(-1)?.replace(/[.,;:!?]+$/u, \"\");\n const lastFolded = last\n ? normalizeHomoglyphs(last).toLowerCase()\n : undefined;\n if (\n tokens.length > 1 &&\n lastFolded &&\n PERSON_TRAILING_NOUNS.has(lastFolded)\n ) {\n continue;\n }\n }\n\n if (\n (normalized.label === \"person\" || normalized.label === \"organization\") &&\n roles.has(normalizeHomoglyphs(trimmed).toLowerCase())\n ) {\n continue;\n }\n\n if (\n normalized.label === \"organization\" &&\n normalized.source === \"legal-form\" &&\n trimmed === trimmed.toUpperCase() &&\n LEGAL_FORM_HEADING_RE.test(trimmed)\n ) {\n continue;\n }\n\n // Reject long address entities that look like prose:\n // no digits, no postal code, no known address\n // component (street abbreviations, etc.).\n if (\n normalized.label === \"address\" &&\n trimmed.length > 40 &&\n !POSTAL_CODE_RE.test(trimmed) &&\n !HAS_DIGIT_RE.test(trimmed) &&\n !hasAddressComponent(trimmed) &&\n !JURISDICTION_RE.test(trimmed)\n ) {\n continue;\n }\n\n // Reject ANY trigger-sourced address without digits\n // and without a known street-type word. Catches\n // non-address text like \"Nejsme plátci DPH !\".\n // Exempt jurisdiction patterns (\"State of ...\",\n // \"Commonwealth of ...\") which are valid addresses\n // without digits. The street-type fallback covers\n // non-Czech vocabulary loaded from JSON, so e.g.\n // Italian `Via Roma` is preserved.\n //\n // Special case: French \"cours\" is highly ambiguous\n // (\"au cours du contrat\" vs. \"Cours Mirabeau\"). When\n // bare `cours` is the only street-type token and the\n // entity has no digits, require it to look like a\n // proper-name address (capitalized token following).\n if (\n normalized.label === \"address\" &&\n normalized.source === \"trigger\" &&\n !HAS_DIGIT_RE.test(trimmed) &&\n !hasAddressComponent(trimmed) &&\n !JURISDICTION_RE.test(trimmed)\n ) {\n continue;\n }\n if (\n normalized.label === \"address\" &&\n normalized.source === \"trigger\" &&\n !HAS_DIGIT_RE.test(trimmed) &&\n isOnlyAmbiguousCours(trimmed)\n ) {\n continue;\n }\n\n if (\n normalized.label === \"address\" &&\n SIGNING_CLAUSE_ADDRESS_RE.test(trimmed)\n ) {\n continue;\n }\n\n filtered.push(normalized);\n }\n\n return filtered;\n};\n","import type { Match } from \"@stll/text-search\";\n\nimport {\n getNameCorpusFirstNames,\n getNameCorpusSurnames,\n getNameCorpusTitles,\n initNameCorpus,\n} from \"./names\";\nimport { resolveCountries } from \"../regions\";\nimport { DETECTION_SOURCES } from \"../types\";\nimport type {\n Dictionaries,\n DictionaryMeta,\n Entity,\n PipelineConfig,\n} from \"../types\";\nimport type { PipelineContext } from \"../context\";\nimport { defaultContext } from \"../context\";\nimport { loadGenericRoles } from \"../filters/false-positives\";\nimport { normalizeForSearch } from \"../util/normalize\";\nimport { ALL_UPPER_RE, UPPER_START_RE } from \"../util/text\";\nimport { DASH } from \"../util/char-groups\";\n\nexport type DenyListConfig = Pick<\n PipelineConfig,\n | \"enableDenyList\"\n | \"enableNameCorpus\"\n | \"nameCorpusLanguages\"\n | \"denyListCountries\"\n | \"denyListRegions\"\n | \"denyListExcludeCategories\"\n | \"customDenyList\"\n | \"dictionaries\"\n | \"enableCountries\"\n>;\n\n// ── Allow list (lazy-loaded from JSON) ───────────────\n\nconst loadAllowList = (ctx: PipelineContext): Promise<ReadonlySet<string>> => {\n if (ctx.allowListPromise) return ctx.allowListPromise;\n ctx.allowListPromise = (async () => {\n try {\n const mod: {\n default?: { words?: string[] };\n } = await import(\"../data/allow-list.json\");\n const set: ReadonlySet<string> = new Set(mod.default?.words ?? []);\n ctx.allowList = set;\n return set;\n } catch {\n const empty: ReadonlySet<string> = new Set();\n ctx.allowList = empty;\n return empty;\n }\n })();\n return ctx.allowListPromise;\n};\n\nconst EMPTY_ALLOW_LIST: ReadonlySet<string> = new Set();\n\n/** Sync accessor — returns empty set before init. */\nconst getAllowList = (ctx: PipelineContext): ReadonlySet<string> =>\n ctx.allowList ?? EMPTY_ALLOW_LIST;\n\nlet commonWordsPromise: Promise<ReadonlySet<string>> | null = null;\nlet commonWordsCache: ReadonlySet<string> | null = null;\n\nconst loadCommonWords = (): Promise<ReadonlySet<string>> => {\n if (commonWordsCache) return Promise.resolve(commonWordsCache);\n if (commonWordsPromise) return commonWordsPromise;\n commonWordsPromise = (async () => {\n try {\n const mod: { default?: { words?: string[] } } =\n await import(\"../data/common-words-en.json\");\n const set: ReadonlySet<string> = new Set(\n (mod.default?.words ?? []).map((word) => word.toLowerCase()),\n );\n commonWordsCache = set;\n return set;\n } catch {\n const empty: ReadonlySet<string> = new Set();\n commonWordsCache = empty;\n return empty;\n }\n })();\n return commonWordsPromise;\n};\n\n/**\n * Curated dictionary entries that are pure dotted\n * single-letter acronyms (e.g. `S.C.`, `D.N.J.`, `C.E.C.`)\n * need targeted suffix guards. The AC search matches\n * case-insensitively on token boundaries where `.` is not\n * a word character, so `S.C.` can match inside `U.S.C.`.\n * Two-segment non-address aliases are too noisy and are\n * dropped at build time; longer official aliases stay\n * searchable and are only suppressed when the source text\n * shows they are the tail of a longer dotted token.\n * Caller-supplied custom entries are exempted.\n */\nconst DOTTED_ACRONYM_RE = /^(?=.{3,}$)\\p{L}(?:\\.\\p{L}){0,3}\\.?$/u;\n\nconst isCuratedNoiseAcronym = (normalized: string): boolean =>\n DOTTED_ACRONYM_RE.test(normalized);\n\nconst dottedAcronymSegmentCount = (normalized: string): number =>\n normalized.split(\".\").filter(Boolean).length;\n\nconst isShortCuratedNoiseAcronym = (normalized: string): boolean =>\n isCuratedNoiseAcronym(normalized) &&\n dottedAcronymSegmentCount(normalized) <= 2;\n\nconst isDottedAcronymSuffixCollision = (\n fullText: string,\n start: number,\n matchText: string,\n): boolean =>\n isCuratedNoiseAcronym(matchText) &&\n /[\\p{L}]\\.$/u.test(fullText.slice(Math.max(0, start - 2), start));\n\n/**\n * Common EU given names present in the stopwords-iso dataset\n * but absent from the first-name corpus. Without this\n * supplementary set, these names would pass through the\n * corpus-based filter and remain in the stopwords, silently\n * suppressing person detection.\n *\n * Sourced from EU member state birth registries (top-100\n * names) cross-referenced with stopwords.json.\n */\nconst SUPPLEMENTARY_NAME_EXCLUSIONS: ReadonlySet<string> = new Set([\n \"ana\",\n \"ben\",\n \"dan\",\n \"eden\",\n \"ella\",\n \"ina\",\n \"jo\",\n \"kai\",\n \"lena\",\n \"may\",\n \"mia\",\n \"sam\",\n \"sara\",\n \"sue\",\n \"tim\",\n \"tom\",\n]);\n\n/**\n * Names from the first-name corpus (lowercased) that also\n * appear in the stopwords-iso dataset, plus supplementary\n * common EU given names not in the corpus. These must be\n * kept out of global STOPWORDS so that person detection is\n * not silently suppressed for real given names.\n *\n * Computed lazily after initNameCorpus() has populated\n * the first-name corpus. Re-builds if corpus size changes.\n */\nconst getFirstNameExclusions = (ctx: PipelineContext): ReadonlySet<string> => {\n const corpus = getNameCorpusFirstNames(ctx);\n // Re-build if corpus has been populated since last call\n if (\n ctx.firstNameExclusions &&\n corpus.length === ctx.firstNameExclusionCorpusLen\n ) {\n return ctx.firstNameExclusions;\n }\n ctx.firstNameExclusionCorpusLen = corpus.length;\n const set: ReadonlySet<string> = new Set([\n ...corpus.map((n) => n.toLowerCase()),\n ...SUPPLEMENTARY_NAME_EXCLUSIONS,\n ]);\n ctx.firstNameExclusions = set;\n return set;\n};\n\n/**\n * Global stopwords: common words across 23 EU languages\n * sourced from the stopwords-iso dataset (MIT license).\n * Checked case-insensitively against matches.\n *\n * Entries that collide with the first-name corpus are\n * excluded so they can still be detected as person names.\n *\n * Regenerate: bun packages/data/scripts/generate-stopwords.ts\n */\n\n// INVARIANT: must be called after initNameCorpus() has\n// resolved, so getFirstNameExclusions() sees the full\n// corpus. buildDenyList() enforces this ordering.\nconst loadStopwords = (ctx: PipelineContext): Promise<ReadonlySet<string>> => {\n if (ctx.stopwordsPromise) return ctx.stopwordsPromise;\n ctx.stopwordsPromise = (async () => {\n try {\n const mod: { default?: string[] } =\n await import(\"../data/stopwords.json\");\n const list = (mod.default ?? []).filter(\n (w: string) => !getFirstNameExclusions(ctx).has(w),\n );\n const set: ReadonlySet<string> = new Set(list);\n ctx.stopwords = set;\n return set;\n } catch (err) {\n console.warn(\n \"[anonymize] Failed to load stopwords.json\" +\n \" — stopword filtering disabled:\",\n err,\n );\n const empty: ReadonlySet<string> = new Set();\n ctx.stopwords = empty;\n return empty;\n }\n })();\n return ctx.stopwordsPromise;\n};\n\nconst EMPTY_STOPWORDS: ReadonlySet<string> = new Set();\n\n/** Sync accessor — returns empty set before init. */\nconst getStopwords = (ctx: PipelineContext): ReadonlySet<string> =>\n ctx.stopwords ?? EMPTY_STOPWORDS;\n\n// ── Person stopwords (lazy-loaded from JSON) ─────────\n\nconst loadPersonStopwords = (\n ctx: PipelineContext,\n): Promise<ReadonlySet<string>> => {\n if (ctx.personStopwordsPromise) {\n return ctx.personStopwordsPromise;\n }\n ctx.personStopwordsPromise = (async () => {\n try {\n const mod: {\n default?: { words?: string[] };\n } = await import(\"../data/person-stopwords.json\");\n const set: ReadonlySet<string> = new Set(mod.default?.words ?? []);\n ctx.personStopwords = set;\n return set;\n } catch {\n const empty: ReadonlySet<string> = new Set();\n ctx.personStopwords = empty;\n return empty;\n }\n })();\n return ctx.personStopwordsPromise;\n};\n\nconst EMPTY_PERSON_STOPWORDS: ReadonlySet<string> = new Set();\n\n/** Sync accessor — returns empty set before init. */\nexport const getPersonStopwords = (ctx: PipelineContext): ReadonlySet<string> =>\n ctx.personStopwords ?? EMPTY_PERSON_STOPWORDS;\n\n// ── Address stopwords (single-token city collisions) ──\n\nconst loadAddressStopwords = (\n ctx: PipelineContext,\n): Promise<ReadonlySet<string>> => {\n if (ctx.addressStopwordsPromise) {\n return ctx.addressStopwordsPromise;\n }\n ctx.addressStopwordsPromise = (async () => {\n try {\n const mod: { default?: { words?: string[] } } =\n await import(\"../data/address-stopwords.json\");\n const set: ReadonlySet<string> = new Set(mod.default?.words ?? []);\n ctx.addressStopwords = set;\n return set;\n } catch {\n const empty: ReadonlySet<string> = new Set();\n ctx.addressStopwords = empty;\n return empty;\n }\n })();\n return ctx.addressStopwordsPromise;\n};\n\nconst EMPTY_ADDRESS_STOPWORDS: ReadonlySet<string> = new Set();\n\nconst getAddressStopwords = (ctx: PipelineContext): ReadonlySet<string> =>\n ctx.addressStopwords ?? EMPTY_ADDRESS_STOPWORDS;\n\n/**\n * Word characters in unicode property notation. The check is\n * \"single-token\" — no internal whitespace — and we keep dashes\n * tokens like \"K-12\" out by requiring the surface to be a single\n * uninterrupted run of letters or marks.\n */\nconst SINGLE_WORD_RE = /^\\p{L}+$/u;\n\n/**\n * Format-level address signals — structurally numeric or\n * 2-letter-state patterns, language-agnostic. The street-type\n * vocabulary is loaded from `address-street-types.json` so new\n * languages contribute via data, not code.\n * - `,\\s*[A-Z]{2}\\b` → US state abbreviation after a comma\n * - `\\b\\d{5}(?:-\\d{4})?\\b` → US ZIP / ZIP+4\n * - `\\b\\d{3}\\s\\d{2}\\b` → Czech/Slovak postal block (140 00)\n * - `\\b\\d{2}-\\d{3}\\b` → Polish postal code (00-950)\n */\nconst ADDRESS_FORMAT_RE =\n /,\\s*\\p{Lu}{2}\\b|\\b\\d{5}(?:-\\d{4})?\\b|\\b\\d{3}\\s\\d{2}\\b|\\b\\d{2}-\\d{3}\\b/u;\n\nlet cachedStreetTypeRe: RegExp | null = null;\nlet streetTypeReLoaded = false;\n\nconst loadStreetTypeRe = async (): Promise<RegExp | null> => {\n if (streetTypeReLoaded) return cachedStreetTypeRe;\n try {\n const mod: { default?: Record<string, unknown> } =\n await import(\"../data/address-street-types.json\");\n const config = mod.default ?? {};\n const words: string[] = [];\n for (const value of Object.values(config)) {\n if (!Array.isArray(value)) continue;\n for (const word of value) {\n if (typeof word === \"string\" && word.length > 0) words.push(word);\n }\n }\n if (words.length === 0) {\n cachedStreetTypeRe = null;\n } else {\n words.sort((a, b) => b.length - a.length);\n const isLetterDigit = (c: string): boolean => /[\\p{L}\\p{N}]/u.test(c);\n const wordLikeTail: string[] = [];\n const punctTail: string[] = [];\n for (const w of words) {\n const escaped = w.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const last = w.at(-1) ?? \"\";\n // Entries ending in a letter/digit need the trailing\n // negative lookahead to enforce a word boundary\n // (`Avenue` should not match inside `Avenues`). Entries\n // ending in punctuation like `Av.` or `C/` cannot\n // continue into another letter/digit and a trailing\n // lookahead would actually exclude valid forms such as\n // `C/Mayor` (no space).\n if (isLetterDigit(last)) {\n wordLikeTail.push(escaped);\n } else {\n punctTail.push(escaped);\n }\n }\n const branches: string[] = [];\n if (wordLikeTail.length > 0) {\n branches.push(`(?:${wordLikeTail.join(\"|\")})(?![\\\\p{L}\\\\p{N}])`);\n }\n if (punctTail.length > 0) {\n branches.push(`(?:${punctTail.join(\"|\")})`);\n }\n // Unicode-class lookarounds rather than `\\b` so entries\n // ending in `.` or `/` (Spanish `Av.`, `C/`) still match\n // when followed by whitespace OR a letter (`C/Mayor`);\n // `\\b` does not fire after non-word characters and a\n // uniform trailing lookahead would block the no-space\n // form.\n cachedStreetTypeRe = new RegExp(\n `(?<![\\\\p{L}\\\\p{N}])(?:${branches.join(\"|\")})`,\n \"iu\",\n );\n }\n } catch {\n cachedStreetTypeRe = null;\n }\n streetTypeReLoaded = true;\n return cachedStreetTypeRe;\n};\n\nconst getStreetTypeRe = (): RegExp | null =>\n streetTypeReLoaded ? cachedStreetTypeRe : null;\n\nconst hasAdjacentAddressEvidence = (\n fullText: string,\n start: number,\n end: number,\n): boolean => {\n const window = fullText.slice(\n Math.max(0, start - 40),\n Math.min(fullText.length, end + 40),\n );\n if (ADDRESS_FORMAT_RE.test(window)) return true;\n const streetRe = getStreetTypeRe();\n return streetRe !== null && streetRe.test(window);\n};\n\n/**\n * Capitalised words that almost never start a person name. When a\n * single-token surname candidate is immediately followed by one of\n * these, the \"next-word is uppercase\" promotion heuristic would\n * otherwise turn section headings (\"Purchase Price↵The Purchaser\n * undertakes…\") into spurious person hits. Kept narrow on purpose;\n * the surrounding pipeline still chains real names via the deny-list\n * cascade when both halves are surnames.\n */\nconst SENTENCE_STARTER_WORDS: ReadonlySet<string> = new Set([\n \"The\",\n \"This\",\n \"These\",\n \"Those\",\n \"An\",\n \"Any\",\n \"All\",\n \"Each\",\n \"Every\",\n \"No\",\n \"Now\",\n \"Whereas\",\n \"Whereby\",\n \"Wherein\",\n \"Whereof\",\n \"Notwithstanding\",\n \"Subject\",\n \"In\",\n \"On\",\n \"At\",\n \"By\",\n \"For\",\n \"If\",\n \"Upon\",\n \"Unless\",\n \"Until\",\n \"Provided\",\n \"Pursuant\",\n \"Such\",\n]);\n\nconst PERSON_CHAIN_BREAK_RE = /[!?;:]|,/u;\nconst WORD_CHAR_RE = /[\\p{L}\\p{N}]/u;\nconst CURATED_PATTERN_SYNTAX_RE = /[|\\\\]/g;\n\nconst stripCuratedPatternSyntax = (value: string): string =>\n value.includes(\"|\") || value.includes(\"\\\\\")\n ? value.replace(CURATED_PATTERN_SYNTAX_RE, \"\")\n : value;\n\nconst isInitialContinuationGap = (text: string, gap: string): boolean =>\n (/^\\p{Lu}$/u.test(text) && /^\\.[^\\S\\n]{1,2}$/u.test(gap)) ||\n /^[^\\S\\n]{1,2}(?:\\p{Lu}\\.[^\\S\\n]{1,2})+$/u.test(gap);\n\n/**\n * Source tag for each pattern in the automaton.\n * \"deny-list\" = standard deny list entry\n * \"city\" = city dictionary entry\n * \"custom-deny-list\" = caller-owned exact term\n * \"first-name\" = name corpus first name\n * \"surname\" = name corpus surname\n * \"title\" = academic/professional title\n */\ntype PatternSource =\n | \"deny-list\"\n | \"city\"\n | \"custom-deny-list\"\n | \"first-name\"\n | \"surname\"\n | \"title\";\n\ntype PatternLabels = string | string[];\ntype PatternSources = PatternSource | PatternSource[];\n\nconst EMPTY_PATTERN_LABELS: readonly string[] = [];\nconst EMPTY_PATTERN_SOURCES: readonly PatternSource[] = [];\n\nconst patternLabels = (\n labels: PatternLabels | undefined,\n): readonly string[] => {\n if (labels === undefined) {\n return EMPTY_PATTERN_LABELS;\n }\n return Array.isArray(labels) ? labels : [labels];\n};\n\nconst patternSources = (\n sources: PatternSources | undefined,\n): readonly PatternSource[] => {\n if (sources === undefined) {\n return EMPTY_PATTERN_SOURCES;\n }\n return Array.isArray(sources) ? sources : [sources];\n};\n\nconst addPatternLabel = (\n list: PatternLabels[],\n index: number,\n label: string,\n): void => {\n const existing = list[index];\n if (existing === undefined) {\n list[index] = label;\n return;\n }\n if (Array.isArray(existing)) {\n if (!existing.includes(label)) {\n existing.push(label);\n }\n return;\n }\n if (existing !== label) {\n list[index] = [existing, label];\n }\n};\n\nconst addPatternSource = (\n list: PatternSources[],\n index: number,\n source: PatternSource,\n): void => {\n const existing = list[index];\n if (existing === undefined) {\n list[index] = source;\n return;\n }\n if (Array.isArray(existing)) {\n if (!existing.includes(source)) {\n existing.push(source);\n }\n return;\n }\n if (existing !== source) {\n list[index] = [existing, source];\n }\n};\n\nconst addOptionalPatternLabel = (\n list: (PatternLabels | undefined)[],\n index: number,\n label: string,\n): void => {\n const existing = list[index];\n if (existing === undefined) {\n list[index] = label;\n return;\n }\n if (Array.isArray(existing)) {\n if (!existing.includes(label)) {\n existing.push(label);\n }\n return;\n }\n if (existing !== label) {\n list[index] = [existing, label];\n }\n};\n\n/**\n * Pre-built deny list data. Constructed once by\n * `buildDenyList`, reused across `processDenyListMatches`\n * calls. Contains PatternEntry[] for the unified builder\n * plus parallel label/source arrays for post-processing.\n */\nexport type DenyListData = {\n /**\n * Maps pattern index → entity labels (plural).\n * Same pattern can have multiple labels when it\n * appears in multiple dictionaries (e.g., \"Denver\"\n * is both a person name and a city name).\n */\n labels: PatternLabels[];\n /** Maps pattern index → labels contributed by custom entries. */\n customLabels: (PatternLabels | undefined)[];\n /** Maps pattern index → original pattern text. */\n originals: string[];\n /** Maps pattern index → source types (plural). */\n sources: PatternSources[];\n};\n\nconst getCityEntries = (\n dictionaries: Dictionaries | undefined,\n allowedCountries: ReadonlySet<string> | null,\n): readonly string[] => {\n const byCountry = dictionaries?.citiesByCountry;\n if (!byCountry) {\n return dictionaries?.cities ?? [];\n }\n\n const result: string[] = [];\n const append = (entries: readonly string[] | undefined) => {\n if (!entries) {\n return;\n }\n for (const entry of entries) {\n result.push(entry);\n }\n };\n\n if (allowedCountries === null) {\n for (const entries of Object.values(byCountry)) {\n append(entries);\n }\n return result;\n }\n\n for (const country of allowedCountries) {\n append(byCountry[country.toUpperCase()]);\n }\n\n return result;\n};\n\n/**\n * Resolve which dictionaries to load based on country\n * and category filters, then build the deny list data.\n * The returned data provides PatternEntry[] for the\n * unified builder and parallel arrays for\n * post-processing.\n *\n * Dictionary data is injected via `config.dictionaries`.\n * Returns null if no dictionaries are provided.\n */\nexport const buildDenyList = async (\n config: DenyListConfig,\n ctx: PipelineContext = defaultContext,\n): Promise<DenyListData | null> => {\n // Pre-load name corpus so getNameCorpus*() accessors\n // and getFirstNameExclusions() are populated before\n // stopwords filtering runs.\n await initNameCorpus(ctx, config.dictionaries, config.nameCorpusLanguages);\n // Pre-load all JSON data so sync accessors are\n // populated before processDenyListMatches runs.\n await Promise.all([\n loadStopwords(ctx),\n loadAllowList(ctx),\n loadPersonStopwords(ctx),\n loadAddressStopwords(ctx),\n loadCommonWords(),\n loadStreetTypeRe(),\n loadGenericRoles(ctx),\n ]);\n const commonWords = await loadCommonWords();\n\n const dictionaries = config.dictionaries;\n const hasDenyList = dictionaries?.denyList && dictionaries?.denyListMeta;\n const hasCustomDenyList =\n config.customDenyList !== undefined && config.customDenyList.length > 0;\n const allowedCountries = resolveCountries(\n config.denyListRegions,\n config.denyListCountries,\n );\n const cityEntries = getCityEntries(dictionaries, allowedCountries);\n const hasCities = cityEntries.length > 0;\n\n // No dictionary data available — skip deny-list building\n if (!hasDenyList && !hasCities && !hasCustomDenyList) {\n // Still build name corpus entries if available\n return buildNameCorpusOnly(config, ctx);\n }\n\n const excluded = config.denyListExcludeCategories;\n const excludeCategories = excluded ? new Set(excluded) : new Set<string>();\n\n const patternList: string[] = [];\n const labelList: PatternLabels[] = [];\n const customLabelList: (PatternLabels | undefined)[] = [];\n const sourceList: PatternSources[] = [];\n // Maps lowercased pattern → index in patternList\n // for accumulating labels from multiple dictionaries\n const patternIndex = new Map<string, number>();\n\n const addDenyListEntry = (\n entry: string,\n label: string,\n source: PatternSource = \"deny-list\",\n ) => {\n // Strip | and \\ only for curated data — these caused the 12K FP\n // bug (| creates empty regex alternation, \\ is\n // an escape prefix). Caller-owned custom terms stay exact.\n const normalized =\n source === \"custom-deny-list\"\n ? normalizeForSearch(entry)\n : stripCuratedPatternSyntax(normalizeForSearch(entry));\n if (normalized.length === 0) {\n return;\n }\n const lower = normalized.toLowerCase();\n if (source !== \"custom-deny-list\" && label !== \"address\") {\n if (SINGLE_WORD_RE.test(normalized) && commonWords.has(lower)) {\n return;\n }\n if (isShortCuratedNoiseAcronym(normalized)) {\n return;\n }\n }\n const existing = patternIndex.get(lower);\n if (existing !== undefined) {\n addPatternLabel(labelList, existing, label);\n addPatternSource(sourceList, existing, source);\n if (\n source === \"custom-deny-list\" &&\n !patternLabels(customLabelList[existing]).includes(label)\n ) {\n addOptionalPatternLabel(customLabelList, existing, label);\n }\n } else {\n patternIndex.set(lower, patternList.length);\n patternList.push(normalized);\n labelList.push(label);\n if (source === \"custom-deny-list\") {\n customLabelList[patternList.length - 1] = label;\n }\n sourceList.push(source);\n }\n };\n\n // Load dictionaries from injected data\n if (hasDenyList) {\n const denyListData = dictionaries.denyList!;\n const metaData = dictionaries.denyListMeta!;\n const useScopedNameCorpus = config.nameCorpusLanguages !== undefined;\n\n for (const [id, entries] of Object.entries(denyListData)) {\n const meta: DictionaryMeta | undefined = metaData[id];\n if (!meta) {\n continue;\n }\n\n if (!config.enableNameCorpus && meta.category === \"Names\") {\n continue;\n }\n\n if (useScopedNameCorpus && meta.category === \"Names\") {\n continue;\n }\n\n if (excludeCategories.has(meta.category)) {\n continue;\n }\n\n // enableCountries: false must zero-out country redaction\n // across both the new country detector and the legacy\n // `countries/translations` dictionary, which ships Czech\n // declensions (\"České republiky\", \"Slovenskou republikou\",\n // …) the CLDR canonicals don't carry. Without this gate,\n // toggling the flag would silently keep the legacy path\n // active.\n if (meta.label === \"country\" && config.enableCountries === false) {\n continue;\n }\n\n if (allowedCountries !== null && meta.country !== null) {\n if (!allowedCountries.has(meta.country)) {\n continue;\n }\n }\n\n for (const entry of entries) {\n addDenyListEntry(entry, meta.label);\n }\n }\n }\n\n // Add pre-loaded city entries\n if (hasCities && !excludeCategories.has(\"Places\")) {\n for (const entry of cityEntries) {\n addDenyListEntry(entry, \"address\", \"city\");\n }\n }\n\n if (hasCustomDenyList) {\n for (const entry of config.customDenyList!) {\n addDenyListEntry(entry.value, entry.label, \"custom-deny-list\");\n for (const variant of entry.variants ?? []) {\n addDenyListEntry(variant, entry.label, \"custom-deny-list\");\n }\n }\n }\n\n // Add name corpus entries — accumulate labels\n // for entries that already exist from deny-list.\n appendNameCorpusEntries(\n config,\n ctx,\n patternList,\n labelList,\n sourceList,\n patternIndex,\n );\n\n if (patternList.length === 0) {\n return null;\n }\n\n return {\n labels: labelList,\n customLabels: customLabelList,\n originals: patternList,\n sources: sourceList,\n };\n};\n\n/**\n * Build deny-list data containing only name corpus\n * entries (no external dictionaries). Used when no\n * dictionary data is injected but name corpus is\n * enabled.\n */\nconst buildNameCorpusOnly = (\n config: DenyListConfig,\n ctx: PipelineContext,\n): DenyListData | null => {\n if (!config.enableNameCorpus) {\n return null;\n }\n\n const excluded = config.denyListExcludeCategories;\n const excludeCategories = excluded ? new Set(excluded) : new Set<string>();\n if (excludeCategories.has(\"Names\")) {\n return null;\n }\n\n const patternList: string[] = [];\n const labelList: PatternLabels[] = [];\n const customLabelList: (PatternLabels | undefined)[] = [];\n const sourceList: PatternSources[] = [];\n const patternIndex = new Map<string, number>();\n\n appendNameCorpusEntries(\n config,\n ctx,\n patternList,\n labelList,\n sourceList,\n patternIndex,\n );\n\n if (patternList.length === 0) {\n return null;\n }\n\n return {\n labels: labelList,\n customLabels: customLabelList,\n originals: patternList,\n sources: sourceList,\n };\n};\n\n/**\n * Append name corpus entries (first names, surnames,\n * titles) to the pattern arrays. Shared between\n * buildDenyList and buildNameCorpusOnly.\n */\nconst appendNameCorpusEntries = (\n config: DenyListConfig,\n ctx: PipelineContext,\n patternList: string[],\n labelList: PatternLabels[],\n sourceList: PatternSources[],\n patternIndex: Map<string, number>,\n): void => {\n const excluded = config.denyListExcludeCategories;\n const excludeCategories = excluded ? new Set(excluded) : new Set<string>();\n\n if (!config.enableNameCorpus || excludeCategories.has(\"Names\")) {\n return;\n }\n\n const addNameEntry = (name: string, source: PatternSource) => {\n // Normalize same as deny-list entries so name\n // patterns match against normalizeForSearch(text).\n const normalized = stripCuratedPatternSyntax(normalizeForSearch(name));\n if (normalized.length === 0) {\n return;\n }\n if (isCuratedNoiseAcronym(normalized)) {\n return;\n }\n const lower = normalized.toLowerCase();\n const existing = patternIndex.get(lower);\n if (existing !== undefined) {\n addPatternLabel(labelList, existing, \"person\");\n addPatternSource(sourceList, existing, source);\n } else {\n patternIndex.set(lower, patternList.length);\n patternList.push(normalized);\n labelList.push(\"person\");\n sourceList.push(source);\n }\n };\n\n for (const name of getNameCorpusFirstNames(ctx)) {\n addNameEntry(name, \"first-name\");\n }\n for (const name of getNameCorpusSurnames(ctx)) {\n addNameEntry(name, \"surname\");\n }\n for (const title of getNameCorpusTitles(ctx)) {\n const norm = stripCuratedPatternSyntax(normalizeForSearch(title));\n if (norm.length === 0) continue;\n const lower = norm.toLowerCase();\n const existing = patternIndex.get(lower);\n if (existing !== undefined) {\n addPatternSource(sourceList, existing, \"title\");\n } else {\n patternIndex.set(lower, patternList.length);\n patternList.push(norm);\n labelList.push(\"person\");\n sourceList.push(\"title\");\n }\n }\n};\n\ntype RawMatch = {\n start: number;\n end: number;\n /** All labels for this pattern (e.g., [\"person\", \"address\"]). */\n labels: readonly string[];\n customLabels: readonly string[];\n sources: readonly PatternSource[];\n text: string;\n patternIdx: number;\n};\n\nconst customMatchHasValidEdges = (\n fullText: string,\n start: number,\n end: number,\n pattern: string,\n): boolean => {\n if (!WORD_CHAR_RE.test(pattern)) {\n return true;\n }\n const prev = fullText[start - 1] ?? \"\";\n const next = fullText[end] ?? \"\";\n if (WORD_CHAR_RE.test(prev)) {\n return false;\n }\n if (WORD_CHAR_RE.test(next)) {\n return false;\n }\n return true;\n};\n\n/**\n * Ensure all deny-list support data (stopwords, allow\n * list, person stopwords, generic roles) is loaded on\n * the given context. Call this before\n * processDenyListMatches / filterFalsePositives when\n * the search instance was built on a different context\n * (e.g. cachedSearch).\n */\nexport const ensureDenyListData = async (\n ctx: PipelineContext = defaultContext,\n dictionaries?: Dictionaries,\n nameCorpusLanguages?: readonly string[],\n): Promise<void> => {\n // INVARIANT: initNameCorpus must resolve before\n // loadStopwords so first-name exclusions are\n // available when computing the stopword set.\n await initNameCorpus(ctx, dictionaries, nameCorpusLanguages);\n await Promise.all([\n loadStopwords(ctx),\n loadAllowList(ctx),\n loadPersonStopwords(ctx),\n loadAddressStopwords(ctx),\n loadStreetTypeRe(),\n loadGenericRoles(ctx),\n ]);\n};\n\n// ── Match processor ─────────────────────────────────\n\n/**\n * Process deny list matches from the unified search.\n * Receives all matches; filters to the deny list slice\n * via sliceStart/sliceEnd. Local index into data.labels,\n * data.originals, data.sources is match.pattern - sliceStart.\n *\n * Two-pass approach to reduce false positives:\n * 1. Collect all matches (case-insensitive,\n * whole-word via Rust automaton)\n * 2. Require uppercase start in source text\n * 3. For person names, require at least one\n * mid-sentence occurrence to prove proper noun\n * 4. Return all occurrences of validated terms\n */\nexport const processDenyListMatches = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText: string,\n data: DenyListData,\n ctx: PipelineContext = defaultContext,\n): Entity[] => {\n // Pass 1: collect valid matches grouped by pattern\n const matchesByPattern = new Map<number, RawMatch[]>();\n\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) {\n continue;\n }\n\n const localIdx = idx - sliceStart;\n const sources = patternSources(data.sources[localIdx]);\n\n // Use original text for display; normalized was\n // only for the AC search.\n const matchText = fullText.slice(match.start, match.end);\n const sourceChar = fullText[match.start] ?? \"\";\n const keyword = matchText.toLowerCase();\n\n const labels = patternLabels(data.labels[localIdx]);\n const pattern = data.originals[localIdx] ?? \"\";\n const customPatternLabels = patternLabels(data.customLabels[localIdx]);\n const customEdgesAreValid = customMatchHasValidEdges(\n fullText,\n match.start,\n match.end,\n pattern,\n );\n const customLabels = customEdgesAreValid ? customPatternLabels : [];\n if (labels.length === 0 && customLabels.length === 0) {\n continue;\n }\n\n // All-uppercase acronym patterns (\"OIL\", \"OP\", \"BIS\")\n // case-fold to common English words under the AC's\n // caseInsensitive flag and match mixed-case occurrences\n // (\"Oil\", \"Op\"). Require all-uppercase patterns to\n // match all-uppercase source text so acronym dictionary\n // entries cannot collide with everyday prose.\n const patternIsAcronym =\n pattern.length > 0 && pattern.length <= 5 && ALL_UPPER_RE.test(pattern);\n const acronymMatchesAcronym =\n !patternIsAcronym || ALL_UPPER_RE.test(matchText);\n\n const passesCuratedFilters =\n UPPER_START_RE.test(sourceChar) &&\n !getStopwords(ctx).has(keyword) &&\n !getAllowList(ctx).has(keyword) &&\n acronymMatchesAcronym &&\n !ALL_UPPER_RE.test(matchText);\n const curatedLabels = passesCuratedFilters\n ? labels.filter(\n (label) =>\n !customPatternLabels.includes(label) && customEdgesAreValid,\n )\n : [];\n const suffixCollision = isDottedAcronymSuffixCollision(\n fullText,\n match.start,\n matchText,\n );\n const filteredCuratedLabels = suffixCollision ? [] : curatedLabels;\n\n if (filteredCuratedLabels.length === 0 && customLabels.length === 0) {\n continue;\n }\n\n const entry: RawMatch = {\n start: match.start,\n end: match.end,\n labels: filteredCuratedLabels,\n customLabels,\n sources,\n text: matchText,\n patternIdx: localIdx,\n };\n\n const existing = matchesByPattern.get(localIdx);\n if (existing) {\n existing.push(entry);\n } else {\n matchesByPattern.set(localIdx, [entry]);\n }\n }\n\n // Pass 2: process all matches\n const results: Entity[] = [];\n const nameHits: RawMatch[] = [];\n\n for (const [, matches] of Array.from(matchesByPattern)) {\n const first = matches[0];\n if (!first) {\n continue;\n }\n\n for (const m of matches) {\n for (const label of m.customLabels) {\n results.push({\n start: m.start,\n end: m.end,\n label,\n text: m.text,\n score: 0.9,\n source: DETECTION_SOURCES.DENY_LIST,\n sourceDetail: \"custom-deny-list\",\n });\n }\n }\n\n // Curated labels are evaluated per match because\n // custom-only matches can share a pattern with\n // curated matches while failing curated FP filters.\n for (const m of matches) {\n if (m.labels.includes(\"person\")) {\n const keyword = m.text.toLowerCase();\n if (!getPersonStopwords(ctx).has(keyword)) {\n nameHits.push(m);\n }\n }\n\n const nonPersonLabels = m.labels.filter((l) => l !== \"person\");\n // Single-token city-dictionary collisions (Price, Union,\n // Brent, Time, …) are common English words that GeoNames\n // also knows as tiny villages. Drop them so \"Purchase\n // Price\" / \"European Union\" prose stops getting tagged\n // as an address — but only when there's no surrounding\n // address context. \"Union, WA 98592\" or \"Price, UT\" stay,\n // because a state abbreviation or ZIP nearby confirms the\n // city interpretation.\n const isStopwordSingleAddress =\n SINGLE_WORD_RE.test(m.text) &&\n getAddressStopwords(ctx).has(m.text.toLowerCase());\n const suppressAddress =\n isStopwordSingleAddress &&\n !hasAdjacentAddressEvidence(fullText, m.start, m.end);\n for (const label of nonPersonLabels) {\n if (label === \"address\" && suppressAddress) {\n continue;\n }\n results.push({\n start: m.start,\n end: m.end,\n label,\n text: m.text,\n score: 0.9,\n source: DETECTION_SOURCES.DENY_LIST,\n });\n }\n }\n }\n\n // Pass 2b: person hits — chain adjacent hits and\n // extend to following capitalised words.\n nameHits.sort((a, b) => a.start - b.start);\n\n const nameConsumed = new Set<number>();\n for (let i = 0; i < nameHits.length; i++) {\n if (nameConsumed.has(i)) {\n continue;\n }\n const hit = nameHits[i];\n if (!hit) {\n continue;\n }\n\n // Build chain of adjacent person hits\n const chain: RawMatch[] = [hit];\n let j = i + 1;\n\n while (j < nameHits.length && chain.length < 5) {\n const next = nameHits[j];\n if (!next) {\n break;\n }\n const prev = chain.at(-1);\n if (!prev) {\n break;\n }\n\n const gap = fullText.slice(prev.end, next.start);\n const breaksOnPeriod =\n gap.includes(\".\") && !isInitialContinuationGap(prev.text, gap);\n if (\n gap.length > 4 ||\n gap.length === 0 ||\n gap.includes(\"\\n\") ||\n gap.includes(\"\\t\") ||\n PERSON_CHAIN_BREAK_RE.test(gap) ||\n breaksOnPeriod\n ) {\n break;\n }\n\n chain.push(next);\n j++;\n }\n\n // Mark chain members consumed\n for (let k = i; k < i + chain.length; k++) {\n nameConsumed.add(k);\n }\n\n // Extend to following capitalised word (for\n // unknown surnames not in the corpus)\n const first = chain.at(0);\n const last = chain.at(-1);\n if (!first || !last) {\n continue;\n }\n\n // Skip the trailing-capitalised-word extension when the\n // chain sits inside a defined-term quote\n // (`\"Bond Hedge Transactions\"`, `\"Blue Sky Laws\"`).\n // Legal prose uses curly or straight quotes to introduce\n // capitalised noun phrases that are not personal names;\n // chaining beyond the name corpus inside that bracketed\n // context produces unstable spans like\n // `\"Bond Hedge Transactions\"`-as-person.\n const insideDefinedTermQuote = isSuppressibleDefinedTermQuote(\n fullText,\n first.start,\n ctx,\n );\n\n if (insideDefinedTermQuote) {\n continue;\n }\n\n const extended = extendPersonName(fullText, first.start, last.end, ctx);\n\n // Score: chained names get 0.9, single names 0.5\n const score = chain.length >= 2 ? 0.9 : 0.5;\n\n // Single-word deny-list matches are too noisy:\n // \"Rate\", \"Server\", \"Code\" etc. are surnames but\n // also common English words. Only accept single-\n // word matches when the next word is also uppercase\n // (likely a full name: \"Alena Zemanová\"). Skip\n // sentence-starter articles (\"The Purchaser…\")\n // which otherwise turn section headings like\n // \"Purchase Price↵The Purchaser…\" into person hits.\n if (chain.length === 1) {\n const afterEnd = last.end;\n const rest = fullText.slice(afterEnd).trimStart();\n // Require Cap + lowercase: filters out acronyms like\n // \"EU\", \"USA\" so \"Rady EU\" doesn't read as a name.\n const nextIsUpper = rest.length > 1 && /^\\p{Lu}\\p{Ll}/u.test(rest);\n if (!nextIsUpper) {\n continue;\n }\n // Reject sentence-starter articles (\"The Purchaser…\")\n // so section headings followed by a sentence don't\n // get promoted to person hits.\n const nextWord = /^\\p{L}+/u.exec(rest)?.[0] ?? \"\";\n if (SENTENCE_STARTER_WORDS.has(nextWord)) {\n continue;\n }\n }\n\n results.push({\n start: first.start,\n end: extended.end,\n label: \"person\",\n text: extended.text,\n score,\n source: DETECTION_SOURCES.DENY_LIST,\n });\n }\n\n // Post-process: extend city/address matches to\n // include adjacent trailing district numbers (e.g.,\n // \"Praha 1\", \"Brno 2\"). Czech and Slovak cities\n // commonly have numbered districts that are part of\n // the address.\n extendCityDistricts(results, fullText);\n\n return results;\n};\n\n/**\n * Extend address-label entities to absorb adjacent\n * integers: trailing district numbers (\"Praha 1\") and\n * leading postal codes (\"80336 München\").\n * Mutates the entities in place.\n */\n// District suffixes: digits (\"Praha 1\") or Roman\n// numerals (\"Příbram II\", \"Brno III\")\n// Valid Roman numerals only (I-XXX range, no invalid\n// combos like IC, LC, VC). Covers district suffixes\n// up to Praha XXX which is more than enough.\n// Roman numeral district suffixes II-XXX. Standalone\n// \"I\" and \"V\" excluded: \"V\" clashes with Czech\n// preposition \"in\"; \"I\" is too ambiguous.\nconst ROMAN_DISTRICT =\n \"XXX|XXIX|XXVIII|XXVII|XXVI|XXV|XXIV|XXIII\" +\n \"|XXII|XXI|XX|XIX|XVIII|XVII|XVI|XV|XIV|XIII\" +\n \"|XII|XI|X|IX|VIII|VII|VI|IV|III|II\";\nconst DISTRICT_SUFFIX_RE = new RegExp(\n `^ (\\\\d{1,2}(?!\\\\d)|(?:${ROMAN_DISTRICT}))` + `(?=[\\\\s,;.)\"\\\\n]|$)`,\n);\n// Postal code before city: \"163 00 \", \"16300 \",\n// \"16300 - \" (with dash separator).\nconst POSTAL_PREFIX_RE = new RegExp(\n `(?:\\\\d{5}|\\\\d{3}\\\\s\\\\d{2})\\\\s*${DASH}?\\\\s*$`,\n);\n\n// Words that must NOT be absorbed into an address span\n// when they follow a postal-code + city pattern. Party\n// roles, organizational nouns, and common legal terms.\nconst TRAILING_WORD_EXCLUSIONS: ReadonlySet<string> = new Set([\n // CZ/SK party roles\n \"nájemce\",\n \"pronajímatel\",\n \"kupující\",\n \"prodávající\",\n \"objednatel\",\n \"zhotovitel\",\n \"dodavatel\",\n \"odběratel\",\n \"věřitel\",\n \"dlužník\",\n \"zadavatel\",\n \"uchazeč\",\n \"příjemce\",\n \"plátce\",\n // Organizational nouns\n \"správa\",\n \"sekretariát\",\n \"kancelář\",\n \"odbor\",\n \"oddělení\",\n \"úřad\",\n \"inspekce\",\n \"agentura\",\n // Legal clause starters\n \"článek\",\n \"smlouva\",\n \"dodatek\",\n \"příloha\",\n \"předmět\",\n \"podmínky\",\n \"ustanovení\",\n]);\n\nconst extendCityDistricts = (entities: Entity[], fullText: string): void => {\n for (const entity of entities) {\n if (entity.label !== \"address\") {\n continue;\n }\n if (entity.sourceDetail === \"custom-deny-list\") {\n continue;\n }\n\n // Trailing: \"Praha\" + \" 1\" → \"Praha 1\"\n // Trailing: \"Praha\" + \" 1\" → \"Praha 1\"\n const afterMatch = fullText.slice(entity.end);\n const suffixM = DISTRICT_SUFFIX_RE.exec(afterMatch);\n if (suffixM) {\n entity.end += suffixM[0].length;\n entity.text = fullText.slice(entity.start, entity.end);\n }\n\n // Dash-separated district name:\n // \"Praha 10 - Strašnice\", \"Havířov – Město\"\n const afterDistrict = fullText.slice(entity.end);\n const dashDistrictM = /^[ \\t]{1,4}[-–][ \\t]*(\\p{Lu}\\p{Ll}+)/u.exec(\n afterDistrict,\n );\n if (dashDistrictM && !dashDistrictM[0].includes(\"\\n\")) {\n entity.end += dashDistrictM[0].length;\n entity.text = fullText.slice(entity.start, entity.end);\n }\n\n // Leading: \"80336 \" + \"München\" → \"80336 München\"\n // Absorbs 3-5 digit postal codes before the city.\n const beforeMatch = fullText.slice(\n Math.max(0, entity.start - 10),\n entity.start,\n );\n const prefixM = POSTAL_PREFIX_RE.exec(beforeMatch);\n if (prefixM) {\n entity.start -= prefixM[0].length;\n entity.text = fullText.slice(entity.start, entity.end);\n }\n\n // Trailing uppercase word: \"434 01\" + \" Most\" →\n // \"434 01 Most\". Absorb if the next word starts\n // with uppercase and is on the same line.\n // Guard: skip party-role or organizational terms.\n const afterExt = fullText.slice(entity.end);\n // Max 4 spaces gap — more means different column\n const trailingWordM = /^[\\s]{1,4}(\\p{Lu}\\p{Ll}+)/u.exec(afterExt);\n if (trailingWordM && !trailingWordM[0].includes(\"\\n\")) {\n const candidate = (trailingWordM[1] ?? \"\").toLowerCase();\n if (!TRAILING_WORD_EXCLUSIONS.has(candidate)) {\n entity.end += trailingWordM[0].length;\n entity.text = fullText.slice(entity.start, entity.end);\n }\n }\n }\n};\n\n/**\n * Extend a person name match to include subsequent\n * capitalized words. \"Pavel\" + \" Heřmánek\" → \"Pavel\n * Heřmánek\". Stops at lowercase words, punctuation,\n * or end of text. Also extends backward if preceded\n * by a capitalized word (for \"Miroslav Braňka\" when\n * only \"Braňka\" matched).\n */\n/**\n * Defined-term marker: an opening typographic or straight\n * quote enclosing the chain start, AND a closing quote\n * within a short window followed by a\n * definitional cue (`means`, `shall mean`, `shall have\n * the meaning(s)`, `refers to`). Legal documents reserve\n * this construction for defined terms; the contents are\n * not personal names even when individual tokens collide\n * with the name corpus.\n *\n * Plain quotations like `\"John Unknown\" said ...` do NOT\n * count: there is no definitional cue, so the trailing\n * surname extension is still allowed to absorb `Unknown`.\n */\nconst OPENING_QUOTES = new Set(['\"', \"'\", \"“\", \"„\", \"‟\", \"‘\", \"‛\", \"«\"]);\nconst CLOSING_QUOTES = new Set(['\"', \"'\", \"”\", \"’\", \"»\", \"“\"]);\nconst DEFINED_TERM_CUE_RE =\n /^[\\s,]*(?:means?|shall\\s+means?|shall\\s+have\\s+the\\s+meanings?|refers?\\s+to|has\\s+the\\s+meanings?|is\\s+defined)\\b/iu;\nconst DEFINED_TERM_LOOKAHEAD = 120;\nconst DEFINED_TERM_LOOKBEHIND = 80;\nconst EMPTY_GENERIC_ROLES: ReadonlySet<string> = new Set();\n\ntype DefinedTermQuote = {\n content: string;\n afterClosingQuote: string;\n};\n\nconst isLetter = (ch: string | undefined): boolean =>\n ch !== undefined && /^\\p{L}$/u.test(ch);\n\nconst isApostropheInsideWord = (text: string, index: number): boolean =>\n isLetter(text[index - 1]) && isLetter(text[index + 1]);\n\nconst isQuoteBoundary = (text: string, index: number): boolean => {\n const ch = text[index];\n if (ch !== \"'\" && ch !== \"’\") {\n return true;\n }\n return !isApostropheInsideWord(text, index);\n};\n\nconst findDefinedTermQuoteContent = (\n text: string,\n start: number,\n): DefinedTermQuote | null => {\n const min = Math.max(0, start - DEFINED_TERM_LOOKBEHIND);\n let quoteStart = -1;\n for (let i = start - 1; i >= min; i--) {\n const ch = text[i];\n if (ch === \"\\n\") {\n break;\n }\n if (ch && OPENING_QUOTES.has(ch) && isQuoteBoundary(text, i)) {\n quoteStart = i;\n break;\n }\n if (ch && CLOSING_QUOTES.has(ch) && isQuoteBoundary(text, i)) {\n break;\n }\n }\n if (quoteStart === -1) {\n return null;\n }\n\n const max = Math.min(text.length, quoteStart + 1 + DEFINED_TERM_LOOKAHEAD);\n for (let i = start; i < max; i++) {\n const ch = text[i];\n if (!ch || !CLOSING_QUOTES.has(ch) || !isQuoteBoundary(text, i)) {\n continue;\n }\n const after = text.slice(i + 1, max);\n if (!DEFINED_TERM_CUE_RE.test(after)) {\n return null;\n }\n return {\n content: text.slice(quoteStart + 1, i),\n afterClosingQuote: after,\n };\n }\n\n return null;\n};\n\nconst FIRST_WORD_RE = /^\\p{L}+/u;\nconst WORD_RE = /\\p{L}+/gu;\n\nconst startsWithKnownFirstName = (\n quoteContent: string,\n ctx: PipelineContext,\n): boolean => {\n const firstWord = FIRST_WORD_RE.exec(quoteContent.trim())?.[0];\n if (!firstWord) {\n return false;\n }\n const firstNames = new Set(\n getNameCorpusFirstNames(ctx).map((name) => name.toLowerCase()),\n );\n return firstNames.has(firstWord.toLowerCase());\n};\n\nconst hasPersonRoleDefinition = (\n afterClosingQuote: string,\n ctx: PipelineContext,\n): boolean => {\n const roleWords =\n afterClosingQuote\n .replace(DEFINED_TERM_CUE_RE, \"\")\n .match(WORD_RE)\n ?.slice(0, 8) ?? [];\n if (roleWords.length === 0) {\n return false;\n }\n\n const genericRoles = ctx.genericRoles ?? EMPTY_GENERIC_ROLES;\n return roleWords.some((word) => genericRoles.has(word.toLowerCase()));\n};\n\nconst isSuppressibleDefinedTermQuote = (\n text: string,\n start: number,\n ctx: PipelineContext,\n): boolean => {\n const definedTermQuote = findDefinedTermQuoteContent(text, start);\n if (definedTermQuote === null) {\n return false;\n }\n\n const words = definedTermQuote.content.match(WORD_RE) ?? [];\n\n // A quoted defined term can itself be a real person:\n // `\"John Smith\" shall mean the employee...`. Preserve those\n // when the definition itself points at a legal/business role\n // from dictionary data. Legal terms such as `\"Bond Hedge\"`\n // stay suppressible even if their first token collides with\n // a given-name corpus entry.\n if (\n words.length >= 2 &&\n startsWithKnownFirstName(definedTermQuote.content, ctx) &&\n hasPersonRoleDefinition(definedTermQuote.afterClosingQuote, ctx)\n ) {\n return false;\n }\n\n return words.length >= 2;\n};\n\nconst extendPersonName = (\n text: string,\n start: number,\n end: number,\n ctx: PipelineContext,\n): { end: number; text: string } => {\n let newEnd = end;\n\n // Extend forward: skip whitespace, check if next\n // word starts with uppercase\n let pos = newEnd;\n while (pos < text.length) {\n // Skip single whitespace\n if (pos < text.length && text[pos] === \" \") {\n const wordStart = pos + 1;\n if (wordStart >= text.length) {\n break;\n }\n\n const char = text[wordStart] ?? \"\";\n if (!UPPER_START_RE.test(char)) {\n break;\n }\n\n // Find end of this word\n let wordEnd = wordStart;\n while (wordEnd < text.length && !/\\s/.test(text[wordEnd] ?? \"\")) {\n wordEnd++;\n }\n\n // Skip trailing punctuation (commas, periods,\n // typographic closing quotes). Curly quotes survive\n // normalisation because they often appear inside\n // defined-term clauses (`\"Blue Sky Laws\"`); strip\n // them so the allow-list / stopword check sees the\n // bare word.\n const word = text.slice(wordStart, wordEnd);\n const stripped = word.replace(/[,;.”\"’'“»]+$/, \"\");\n if (stripped.length < 2) {\n break;\n }\n\n // Don't extend into stopwords or person stopwords.\n // The global allow list is intentionally NOT consulted\n // here: real surnames such as `Law`, `Tesla`, or\n // `Vote` are common English words and live on the\n // allow list to suppress single-token noise, but they\n // are legitimate name extensions when preceded by a\n // first name in plain prose (`John Law`, `Elon\n // Tesla`). Defined-term contexts (`\"Blue Sky Laws\"`,\n // `\"Bond Hedge Transactions\"`) are filtered earlier by\n // `isInsideDefinedTermQuote`, so by the time\n // `extendPersonName` runs we are in ordinary prose and\n // the allow-list block would only swallow real\n // surnames.\n const lower = stripped.toLowerCase();\n if (getStopwords(ctx).has(lower) || getPersonStopwords(ctx).has(lower)) {\n break;\n }\n\n newEnd = wordStart + stripped.length;\n pos = newEnd;\n } else {\n break;\n }\n }\n\n return {\n end: newEnd,\n text: text.slice(start, newEnd),\n };\n};\n","/**\n * Address detection via seed expansion.\n *\n * 1. Find \"address seeds\" — high-confidence address\n * components (postal codes, cities, street types)\n * 2. Cluster nearby seeds (within ~150 chars)\n * 3. Expand each cluster to the full address span\n * 4. Score by seed diversity (more types = higher)\n *\n * Language-agnostic: street type words and boundary\n * words are loaded from data dictionaries, not\n * hardcoded per language.\n */\n\nimport type { Match } from \"@stll/text-search\";\n\nimport { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\nimport {\n DASH,\n DASH_INNER,\n OPENING_BRACKETS_INNER,\n QUOTE_DOUBLE_INNER,\n QUOTE_SINGLE_INNER,\n} from \"../util/char-groups\";\n\n/**\n * Trailing chars that should never end an address span. Combines\n * structural separators, opening brackets, and typographic quote\n * variants so a span like `… GA 30326, USA (the \"Premises\")`\n * does not pull the parenthetical opener into the address.\n * The prime (`′`) catches measurement tails (`5′`) that the\n * cluster occasionally absorbs.\n */\nconst ADDRESS_TRAILING_TRIM_RE = new RegExp(\n `[,;:\\\\s${OPENING_BRACKETS_INNER}${QUOTE_DOUBLE_INNER}${QUOTE_SINGLE_INNER}′]`,\n \"u\",\n);\nconst POSTAL_ADJACENT = `\\\\p{L}\\\\p{N}_${DASH_INNER}`;\nconst POSTAL_CODE_RE = new RegExp(\n `(?<![${POSTAL_ADJACENT}])` +\n `(?:\\\\d{3}\\\\s\\\\d{2}|\\\\d{2}${DASH}\\\\d{3}|\\\\d{5}${DASH}\\\\d{3}|\\\\d{5}${DASH}\\\\d{4})` +\n `(?![${POSTAL_ADJACENT}])`,\n \"gu\",\n);\nconst BR_CEP_SHAPE_RE = new RegExp(`^\\\\d{5}${DASH}\\\\d{3}$`, \"u\");\nconst US_ZIP_PLUS_FOUR_SHAPE_RE = new RegExp(`^\\\\d{5}${DASH}\\\\d{4}$`, \"u\");\nconst US_STATE_ABBREV =\n \"A[KLRZ]|C[AOT]|D[CE]|F[LM]|G[AU]|HI|I[ADLN]|K[SY]|LA|\" +\n \"M[ADEHINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|\" +\n \"UT|V[AIT]|W[AIVY]\";\nconst US_STATE_ABBREV_BEFORE_ZIP_RE = new RegExp(\n `(?:^|[^A-Za-z0-9])(${US_STATE_ABBREV})\\\\s*,?\\\\s*$`,\n \"u\",\n);\nconst US_ZIP_CONTEXT_WINDOW = 120;\nconst US_CITY_ZIP_GAP_RE = /^[\\s,]+$/u;\nconst HOUSE_NUMBER_BEFORE_STREET_RE =\n /\\b\\d{1,6}(?:[-/]\\d{1,6})?\\s+(?:\\p{Lu}\\p{L}+[^\\S\\n\\t]+){0,4}$/u;\nconst HOUSE_NUMBER_AFTER_STREET_RE = /^[^\\S\\n\\t]+\\d{1,6}(?:[-/]\\d{1,6})?\\b/u;\n\n// ── Seed types ──────────────────────────────────────\n\ntype SeedType =\n | \"street-word\"\n | \"house-number\"\n | \"postal-code\"\n | \"city\"\n | \"state\"\n | \"address-trigger\";\n\ntype Seed = {\n type: SeedType;\n start: number;\n end: number;\n text: string;\n};\n\n// ── Dictionary loading ──────────────────────────────\n\ntype DictionaryConfig = Record<string, string[] | string>;\n\nlet cachedBoundaryRe: RegExp | null = null;\n\nconst loadBoundaryWords = async (): Promise<DictionaryConfig> => {\n try {\n const mod = await import(\"../data/address-boundaries.json\");\n return mod.default as DictionaryConfig;\n } catch {\n return {};\n }\n};\n\n// ── pt-BR CEP context gating ────────────────────────\n//\n// The bare `\\d{5}-\\d{3}` CEP shape collides with non-\n// address identifiers (\"Order 12345-678\"), so the seed\n// is only accepted when a pt-BR cue word appears within\n// the cluster window around it. The cue regex is built\n// once from the pt-BR entries of `address-street-types`\n// and `address-boundaries` (no hardcoded language strings\n// in TS). The window matches the seed-cluster gap so a\n// CEP that would otherwise cluster with a non-BR `city`\n// seed is filtered out before clustering.\n\nconst BR_CEP_CONTEXT_WINDOW = 200;\n\nlet cachedBrCepContextRe: RegExp | null = null;\nlet cachedBrCepContextPromise: Promise<RegExp | null> | null = null;\n\nconst loadBrCueWords = async (): Promise<readonly string[]> => {\n const sources = await Promise.all([\n (async () => {\n try {\n const mod = await import(\"../data/address-street-types.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON shape\n return (mod.default as DictionaryConfig)[\"pt-br\"];\n } catch {\n return undefined;\n }\n })(),\n (async () => {\n try {\n const mod = await import(\"../data/address-boundaries.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON shape\n return (mod.default as DictionaryConfig)[\"pt-br\"];\n } catch {\n return undefined;\n }\n })(),\n ]);\n\n const out: string[] = [];\n const seen = new Set<string>();\n for (const entry of sources) {\n if (!Array.isArray(entry)) continue;\n for (const word of entry) {\n if (typeof word !== \"string\" || word.length === 0) continue;\n const key = word.toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(word);\n }\n }\n return out;\n};\n\nconst getBrCepContextRe = async (): Promise<RegExp | null> => {\n if (cachedBrCepContextRe !== null) {\n return cachedBrCepContextRe;\n }\n if (cachedBrCepContextPromise) {\n return cachedBrCepContextPromise;\n }\n cachedBrCepContextPromise = (async () => {\n const words = await loadBrCueWords();\n if (words.length === 0) return null;\n const escaped = words\n .toSorted((a, b) => b.length - a.length)\n .map((w) => w.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"));\n const re = new RegExp(\n `(?<![\\\\p{L}\\\\p{N}])(?:${escaped.join(\"|\")})(?![\\\\p{L}\\\\p{N}])`,\n \"iu\",\n );\n cachedBrCepContextRe = re;\n return re;\n })();\n return cachedBrCepContextPromise;\n};\n\nconst hasBrCueNearby = (\n fullText: string,\n start: number,\n end: number,\n re: RegExp,\n): boolean => {\n const windowStart = Math.max(0, start - BR_CEP_CONTEXT_WINDOW);\n const windowEnd = Math.min(fullText.length, end + BR_CEP_CONTEXT_WINDOW);\n const window = fullText.slice(windowStart, windowEnd);\n // Build a fresh non-global RegExp per call — sharing\n // would carry lastIndex across calls.\n const probe = new RegExp(re.source, re.flags.replace(\"g\", \"\"));\n return probe.test(window);\n};\n\ntype UsZipPlusFourContext = {\n stateSeed: Seed | null;\n hasContext: boolean;\n};\n\nconst getUsStateSeedBeforeZip = (\n fullText: string,\n start: number,\n): Seed | null => {\n const stateWindowStart = Math.max(0, start - 24);\n const stateWindow = fullText.slice(stateWindowStart, start);\n const match = US_STATE_ABBREV_BEFORE_ZIP_RE.exec(stateWindow);\n const state = match?.[1];\n if (!match || !state) {\n return null;\n }\n\n const stateOffset = match[0].indexOf(state);\n const stateStart = stateWindowStart + match.index + stateOffset;\n return {\n type: \"state\",\n start: stateStart,\n end: stateStart + state.length,\n text: state,\n };\n};\n\nconst hasHouseNumberNearStreetWord = (\n fullText: string,\n seed: Seed,\n): boolean => {\n if (/\\d/.test(seed.text)) {\n return true;\n }\n\n const before = fullText.slice(Math.max(0, seed.start - 50), seed.start);\n if (HOUSE_NUMBER_BEFORE_STREET_RE.test(before)) {\n return true;\n }\n\n const after = fullText.slice(\n seed.end,\n Math.min(fullText.length, seed.end + 24),\n );\n return HOUSE_NUMBER_AFTER_STREET_RE.test(after);\n};\n\nconst isLowercaseStreetWordInProse = (fullText: string, seed: Seed): boolean =>\n /^\\p{Ll}/u.test(seed.text) &&\n /^\\s+\\p{Ll}/u.test(fullText.slice(seed.end, seed.end + 16)) &&\n !hasHouseNumberNearStreetWord(fullText, seed);\n\nconst getUsZipPlusFourContext = (\n fullText: string,\n start: number,\n seeds: readonly Seed[],\n): UsZipPlusFourContext => {\n const stateSeed = getUsStateSeedBeforeZip(fullText, start);\n if (stateSeed !== null) {\n return { stateSeed, hasContext: true };\n }\n\n const hasContext = seeds.some((seed) => {\n if (Math.abs(seed.start - start) > US_ZIP_CONTEXT_WINDOW) {\n return false;\n }\n if (seed.type === \"address-trigger\") {\n return true;\n }\n if (seed.type === \"city\" && seed.end <= start) {\n const gap = fullText.slice(seed.end, start);\n return US_CITY_ZIP_GAP_RE.test(gap);\n }\n if (seed.type === \"street-word\") {\n return hasHouseNumberNearStreetWord(fullText, seed);\n }\n return false;\n });\n return { stateSeed: null, hasContext };\n};\n\n/**\n * Build regex for boundary words. Matches any\n * boundary word preceded by a word boundary.\n */\nconst getBoundaryRe = async (): Promise<RegExp> => {\n if (cachedBoundaryRe) {\n return cachedBoundaryRe;\n }\n const config = await loadBoundaryWords();\n const words: string[] = [];\n for (const entries of Object.values(config)) {\n if (!Array.isArray(entries)) {\n continue;\n }\n for (const word of entries) {\n // Escape regex special chars\n words.push(word.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"));\n }\n }\n // Sort longest first so longer phrases match first\n words.sort((a, b) => b.length - a.length);\n // Word-boundary lookarounds via unicode letter/number\n // classes — `\\b` doesn't fire after non-word chars, so a\n // phrase ending in `.` (e.g. `sp. zn.`, `r.č.`, Italian\n // `C.F.`, Spanish `con C.I.F.`) would never match with\n // `\\b...\\b`. The lookarounds anchor on the absence of a\n // letter/digit on either side, which works regardless of\n // the phrase's last character.\n cachedBoundaryRe =\n words.length > 0\n ? new RegExp(\n `(?<![\\\\p{L}\\\\p{N}])(?:${words.join(\"|\")})(?![\\\\p{L}\\\\p{N}])`,\n \"iu\",\n )\n : /(?!)/;\n return cachedBoundaryRe;\n};\n\n// ── Pattern builder for unified search ──────────────\n\n/**\n * Build street type patterns for the unified search.\n * Returns string[] for the unified TextSearch\n * builder. Empty if data package is not installed.\n */\nlet streetTypePatternsPromise: Promise<string[]> | null = null;\n\nconst loadStreetTypePatterns = async (): Promise<string[]> => {\n let config: DictionaryConfig;\n try {\n const mod = await import(\"../data/address-street-types.json\");\n config = mod.default as DictionaryConfig;\n } catch {\n return [];\n }\n\n // Plain strings — the unified builder sets\n // caseInsensitive + wholeWords globally.\n const words: string[] = [];\n for (const values of Object.values(config)) {\n if (!Array.isArray(values)) {\n continue;\n }\n for (const word of values) {\n words.push(word);\n }\n }\n return words;\n};\n\nexport const buildStreetTypePatterns = async (): Promise<string[]> => {\n streetTypePatternsPromise ??= loadStreetTypePatterns();\n return streetTypePatternsPromise;\n};\n\n// ── Seed collection ─────────────────────────────────\n\nconst collectSeeds = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText: string,\n existingEntities: Entity[],\n brCepContextRe: RegExp | null,\n): Seed[] => {\n const seeds: Seed[] = [];\n\n // 1. Street type words from unified search matches\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) {\n continue;\n }\n const seed = {\n type: \"street-word\",\n start: match.start,\n end: match.end,\n text: match.text,\n } satisfies Seed;\n if (isLowercaseStreetWordInProse(fullText, seed)) {\n continue;\n }\n seeds.push(seed);\n }\n\n // 2. Cities and postal codes from existing entities\n for (const e of existingEntities) {\n if (e.label !== \"address\") {\n continue;\n }\n if (e.sourceDetail === \"custom-deny-list\") {\n continue;\n }\n if (e.source === \"deny-list\") {\n seeds.push({\n type: \"city\",\n start: e.start,\n end: e.end,\n text: e.text,\n });\n } else if (e.source === \"trigger\" && /^\\d/.test(e.text)) {\n seeds.push({\n type: \"postal-code\",\n start: e.start,\n end: e.end,\n text: e.text,\n });\n } else if (e.source === \"trigger\") {\n seeds.push({\n type: \"address-trigger\",\n start: e.start,\n end: e.end,\n text: e.text,\n });\n }\n }\n\n // 3. Standalone postal codes (multiple formats):\n // Czech/Slovak: NNN NN (e.g., 140 00)\n // Polish: NN-NNN (e.g., 00-950)\n // Brazilian CEP: NNNNN-NNN (e.g., 01001-000)\n // US ZIP+4: NNNNN-NNNN (e.g., 94304-1050)\n // The CEP shape collides with bare order/ticket numbers\n // (\"Order 12345-678\"), and the cluster's other seed\n // could be a non-BR deny-list city. To prevent that, the\n // CEP-shaped seed is only kept when a pt-BR cue word\n // (rua/avenida/CNPJ/CPF/RG/…) appears within the cluster\n // window around it. The Czech/Slovak and Polish shapes\n // are distinctive enough not to need a similar gate. US\n // ZIP+4 seeds are only kept with nearby address evidence\n // because business/order IDs often share the same shape.\n const postalRe = POSTAL_CODE_RE;\n postalRe.lastIndex = 0;\n let postalMatch;\n while ((postalMatch = postalRe.exec(fullText)) !== null) {\n const start = postalMatch.index;\n const end = start + postalMatch[0].length;\n const alreadyCovered = seeds.some((s) => s.start <= start && s.end >= end);\n if (alreadyCovered) {\n continue;\n }\n const isCepShape = BR_CEP_SHAPE_RE.test(postalMatch[0]);\n if (\n isCepShape &&\n (brCepContextRe === null ||\n !hasBrCueNearby(fullText, start, end, brCepContextRe))\n ) {\n continue;\n }\n const isUsZipPlusFourShape = US_ZIP_PLUS_FOUR_SHAPE_RE.test(postalMatch[0]);\n if (isUsZipPlusFourShape) {\n const usContext = getUsZipPlusFourContext(fullText, start, seeds);\n if (!usContext.hasContext) {\n continue;\n }\n const stateSeed = usContext.stateSeed;\n if (stateSeed !== null) {\n const hasStateSeed = seeds.some(\n (seed) =>\n seed.start === stateSeed.start && seed.end === stateSeed.end,\n );\n if (!hasStateSeed) {\n seeds.push(stateSeed);\n }\n }\n }\n seeds.push({\n type: \"postal-code\",\n start,\n end,\n text: postalMatch[0],\n });\n }\n\n // 3b. Italian CAP: 5 consecutive digits followed by a\n // capitalised word (\"41012 Carpi\", \"41012 MODENA\"). The\n // capitalised word requirement keeps random 5-digit IDs\n // (years, order numbers) out, and the explicit\n // address-evidence-within-80-chars gate keeps a stray\n // \"12345 Paris\" reference from accidentally clustering\n // into an address in non-Italian text. A bare \"via\"\n // street-word does not count on its own — it is also a\n // common English preposition matched case-insensitively\n // (\"sent via form 12345 Paris\"), so require either an\n // address trigger / city / postal seed nearby, or a\n // street-word other than a standalone lowercase \"via\".\n const itCapRe = /\\b\\d{5}(?=\\s+\\p{Lu}\\p{L}+)/gu;\n let itCapMatch;\n while ((itCapMatch = itCapRe.exec(fullText)) !== null) {\n const start = itCapMatch.index;\n const end = start + itCapMatch[0].length;\n const alreadyCovered = seeds.some((s) => s.start <= start && s.end >= end);\n if (alreadyCovered) continue;\n const hasNearbyAddressEvidence = seeds.some((s) => {\n if (Math.abs(s.start - start) > 80) return false;\n if (s.type === \"address-trigger\") return true;\n if (s.type === \"city\") return true;\n if (s.type === \"postal-code\") return true;\n if (s.type === \"street-word\") {\n // Reject the bare English preposition \"via\" as the\n // sole signal; any longer or non-\"via\" street word\n // (Piazza, Viale, Corso, Via Roma) still qualifies.\n return s.text.toLowerCase() !== \"via\";\n }\n return false;\n });\n if (!hasNearbyAddressEvidence) continue;\n seeds.push({\n type: \"postal-code\",\n start,\n end,\n text: itCapMatch[0],\n });\n }\n\n // 4. Street name + house number pattern:\n // [CapitalizedWord] [number](/[number])?,\n // e.g., \"Olbrachtova 1929/62,\" or \"Kamínky 5,\"\n const streetNumRe =\n /\\b(\\p{Lu}\\p{Ll}{2,})\\s+(\\d{1,5}(?:\\/\\d{1,5})?)\\s*[,\\n]/gu;\n let streetMatch;\n while ((streetMatch = streetNumRe.exec(fullText)) !== null) {\n const matchedStreet = streetMatch[1];\n const matchedNum = streetMatch[2];\n if (!matchedStreet || !matchedNum) {\n continue;\n }\n const start = streetMatch.index;\n const end = start + matchedStreet.length + 1 + matchedNum.length;\n seeds.push({\n type: \"street-word\",\n start,\n end,\n text: fullText.slice(start, end),\n });\n }\n\n return seeds.sort((a, b) => a.start - b.start);\n};\n\n// ── Cluster nearby seeds ────────────────────────────\n\ntype SeedCluster = {\n seeds: Seed[];\n start: number;\n end: number;\n};\n\nconst clusterSeeds = (seeds: Seed[], maxGap: number): SeedCluster[] => {\n const first = seeds[0];\n if (!first) {\n return [];\n }\n\n const clusters: SeedCluster[] = [];\n let current: SeedCluster = {\n seeds: [first],\n start: first.start,\n end: first.end,\n };\n\n for (let i = 1; i < seeds.length; i++) {\n const seed = seeds.at(i);\n if (!seed) {\n continue;\n }\n if (seed.start - current.end <= maxGap) {\n current.seeds.push(seed);\n current.end = Math.max(current.end, seed.end);\n } else {\n clusters.push(current);\n current = {\n seeds: [seed],\n start: seed.start,\n end: seed.end,\n };\n }\n }\n clusters.push(current);\n\n return clusters;\n};\n\n// ── Score a cluster ─────────────────────────────────\n\nconst scoreCluster = (cluster: SeedCluster): number => {\n const types = new Set(cluster.seeds.map((s) => s.type));\n\n // Need at least 2 different seed types for an\n // address (e.g., city + postal code, or street\n // word + house number)\n if (types.size < 2) {\n return 0;\n }\n\n let score = 0.5;\n\n if (types.has(\"postal-code\")) score += 0.15;\n if (types.has(\"city\")) score += 0.15;\n if (types.has(\"state\")) score += 0.15;\n if (types.has(\"street-word\")) score += 0.15;\n if (types.has(\"address-trigger\")) score += 0.1;\n\n return Math.min(score, 0.95);\n};\n\n// ── Expand cluster to full address span ─────────────\n\nconst NON_ADDRESS_LABELS = new Set([\n \"registration number\",\n \"tax identification number\",\n \"national identification number\",\n \"social security number\",\n \"birth number\",\n \"identity card number\",\n \"person\",\n \"bank account number\",\n \"email address\",\n \"phone number\",\n \"organization\",\n \"iban\",\n]);\n\nconst expandCluster = async (\n fullText: string,\n cluster: SeedCluster,\n existingEntities: Entity[],\n): Promise<{ start: number; end: number }> => {\n const { start, end } = cluster;\n const seedTypes = new Set(cluster.seeds.map((seed) => seed.type));\n\n // Find the nearest non-address entity to the LEFT\n let leftBound = 0;\n for (const e of existingEntities) {\n if (\n NON_ADDRESS_LABELS.has(e.label) &&\n e.end <= start &&\n e.end > leftBound\n ) {\n leftBound = e.end;\n }\n }\n\n // Expand left: include preceding capitalized words\n // and numbers (street name before the street type)\n let leftPos = start;\n while (leftPos > leftBound) {\n let p = leftPos - 1;\n while (p >= 0 && (fullText[p] === \" \" || fullText[p] === \",\")) {\n p--;\n }\n if (p < 0) break;\n\n let wordEnd = p + 1;\n while (p >= 0 && /\\S/.test(fullText[p] ?? \"\")) {\n p--;\n }\n const word = fullText.slice(p + 1, wordEnd);\n\n if (word.length < 2 || (!/^\\p{Lu}/u.test(word) && !/^\\d/.test(word))) {\n break;\n }\n\n if (fullText.slice(p + 1, leftPos).includes(\"\\n\")) {\n break;\n }\n\n leftPos = p + 1;\n }\n\n const hasExpandableAddressContext =\n seedTypes.has(\"street-word\") ||\n seedTypes.has(\"house-number\") ||\n seedTypes.has(\"postal-code\") ||\n seedTypes.has(\"address-trigger\");\n\n if (!hasExpandableAddressContext) {\n return {\n start: Math.min(leftPos, start),\n end,\n };\n }\n\n // Expand right: include following text until we\n // hit a boundary word, non-address entity,\n // double newline, or 200 char cap.\n let rightPos = end;\n const remaining = fullText.slice(rightPos);\n let nearestBoundary = Math.min(remaining.length, 200);\n\n // Stop at dictionary-defined boundary words\n const boundaryRe = await getBoundaryRe();\n const boundaryMatch = boundaryRe.exec(remaining);\n if (boundaryMatch && boundaryMatch.index < nearestBoundary) {\n nearestBoundary = boundaryMatch.index;\n }\n\n // Stop at non-address entities\n for (const e of existingEntities) {\n if (!NON_ADDRESS_LABELS.has(e.label)) {\n continue;\n }\n const offset = e.start - rightPos;\n if (offset > 0 && offset < nearestBoundary) {\n nearestBoundary = offset;\n }\n }\n\n // Stop at double newline (paragraph break)\n const doubleNewline = remaining.indexOf(\"\\n\\n\");\n if (doubleNewline !== -1 && doubleNewline < nearestBoundary) {\n nearestBoundary = doubleNewline;\n }\n\n const expanded = remaining.slice(0, nearestBoundary).trimEnd();\n rightPos = end + expanded.length;\n\n while (\n rightPos > end &&\n ADDRESS_TRAILING_TRIM_RE.test(fullText[rightPos - 1] ?? \"\")\n ) {\n rightPos--;\n }\n\n return {\n start: Math.min(leftPos, start),\n end: Math.max(rightPos, end),\n };\n};\n\n// Allow a span containing exactly one line break only when the cluster\n// carries independent address evidence on both sides of the break: a\n// \"street\" seed (street-word or house-number) above the newline AND a\n// \"destination\" seed (postal-code or city) below (or vice-versa).\n// This admits the dominant US notice-block shape\n//\n// One American Road\n// Cleveland, Ohio 44144-2398\n//\n// while rejecting both the single-line case that got right-expanded\n// across a newline (all seeds above the break, prose below) and the\n// structurally over-reaching case with two or more internal newlines.\nconst STREET_SEED_TYPES: ReadonlySet<SeedType> = new Set([\n \"street-word\",\n \"house-number\",\n]);\nconst DESTINATION_SEED_TYPES: ReadonlySet<SeedType> = new Set([\n \"postal-code\",\n \"city\",\n]);\n\ntype NewlineBoundaryResolution =\n | { kind: \"keep\" }\n | { kind: \"drop\" }\n | { kind: \"trim\"; relativeEnd: number };\n\nconst resolveNewlineBoundary = (\n spanStart: number,\n text: string,\n cluster: SeedCluster,\n): NewlineBoundaryResolution => {\n const newlines = (text.match(/\\n/gu) ?? []).length;\n if (newlines === 0) return { kind: \"keep\" };\n if (newlines > 1) return { kind: \"drop\" };\n\n const relativeNewline = text.indexOf(\"\\n\");\n const newlineAbs = spanStart + relativeNewline;\n\n let streetAbove = false;\n let streetBelow = false;\n let destAbove = false;\n let destBelow = false;\n for (const seed of cluster.seeds) {\n const isAbove = seed.end <= newlineAbs;\n const isStreet = STREET_SEED_TYPES.has(seed.type);\n const isDest = DESTINATION_SEED_TYPES.has(seed.type);\n if (isStreet && isAbove) streetAbove = true;\n if (isStreet && !isAbove) streetBelow = true;\n if (isDest && isAbove) destAbove = true;\n if (isDest && !isAbove) destBelow = true;\n }\n\n // True multi-line notice block: street and destination evidence\n // straddle the newline (`One American Road\\nCleveland, Ohio 44144`).\n if ((streetAbove && destBelow) || (streetBelow && destAbove)) {\n return { kind: \"keep\" };\n }\n\n // Inline address followed by unrelated prose on the next line\n // (`650 Page Mill Road, Palo Alto, CA 94304-1050.\\nPlease review…`).\n // The expansion walked through the newline up to its 200-char cap,\n // but the complete address sits entirely above the break. Trim back\n // to the line above instead of dropping the whole span.\n if (streetAbove && destAbove) {\n return { kind: \"trim\", relativeEnd: relativeNewline };\n }\n\n return { kind: \"drop\" };\n};\n\n// Normalise CRLF paragraph breaks to LF for the address-seed\n// newline-boundary check. Windows/PDF-extracted contracts commonly use\n// `\\r\\n`, which leaves the expanded text with paragraph breaks the\n// LF-only `\\n\\n` stop in `expandCluster` doesn't catch and inflates the\n// internal-newline count past the single-newline cap.\nconst normaliseLineBreaks = (text: string): string =>\n text.replace(/\\r\\n?/gu, \"\\n\");\n\n// ── Public API ──────────────────────────────────────\n\n/**\n * Process address seeds from the unified search.\n * Receives all matches; filters to the street types\n * slice via sliceStart/sliceEnd. Uses fullText and\n * existingEntities for seed collection, clustering,\n * expansion, and scoring.\n *\n * Runs as a post-processor after all other detectors,\n * using their output as seed sources.\n */\nexport const processAddressSeeds = async (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText: string,\n existingEntities: Entity[],\n): Promise<Entity[]> => {\n const brCepContextRe = await getBrCepContextRe();\n const seeds = collectSeeds(\n allMatches,\n sliceStart,\n sliceEnd,\n fullText,\n existingEntities,\n brCepContextRe,\n );\n const clusters = clusterSeeds(seeds, 150);\n\n const results: Entity[] = [];\n\n for (const cluster of clusters) {\n const score = scoreCluster(cluster);\n if (score < 0.6) {\n continue;\n }\n\n const { start, end } = await expandCluster(\n fullText,\n cluster,\n existingEntities,\n );\n const rawText = fullText.slice(start, end);\n const resolution = resolveNewlineBoundary(\n start,\n normaliseLineBreaks(rawText),\n cluster,\n );\n if (resolution.kind === \"drop\") continue;\n const effectiveText =\n resolution.kind === \"trim\"\n ? rawText.slice(0, resolution.relativeEnd).trim()\n : rawText.trim();\n if (effectiveText.length < 5 || effectiveText.length > 300) continue;\n\n results.push({\n start,\n end: start + effectiveText.length,\n label: \"address\",\n text: effectiveText,\n score,\n source: DETECTION_SOURCES.REGEX,\n });\n }\n\n return results;\n};\n","import { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\nimport { LEGAL_SUFFIXES } from \"../config/legal-forms\";\n\nconst TRAILING_SEP = /[,\\s]+$/;\nconst WORD_CHAR_RE = /[\\p{L}\\p{N}]/u;\nconst ORG_PROPAGATION_SCORE = 0.9;\n// Common determiners that prefix a defined-term shorthand for\n// the originating company in mid-document references. After\n// declaring `Acme s.r.o.` (or `Acme Corp.`) we treat\n// \"Společnost Acme\", \"Společnosti Acme\" (Czech declensions),\n// \"the Company Acme\", or \"die Gesellschaft Acme\" as the same\n// organisation and extend the highlighted span to cover them.\n// Match is case-insensitive and word-bounded.\nconst ORG_DETERMINER_RE =\n /(?<![\\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;\n\nconst isCallerOwnedEntity = (entity: Entity): boolean =>\n entity.sourceDetail === \"custom-deny-list\" ||\n entity.sourceDetail === \"custom-regex\";\n\ntype Seed = {\n baseName: string;\n label: string;\n};\n\n/**\n * After the main detection pass, collect organization\n * entities with a legal form suffix, strip the suffix\n * to get the base name, and re-scan the full text for\n * bare mentions of that base name. Returns new entities\n * for occurrences not already covered.\n */\nexport const propagateOrgNames = (\n entities: Entity[],\n fullText: string,\n): Entity[] => {\n const seeds: Seed[] = [];\n const seenBases = new Set<string>();\n\n for (const e of entities) {\n if (e.label !== \"organization\") continue;\n if (isCallerOwnedEntity(e)) continue;\n for (const suffix of LEGAL_SUFFIXES) {\n if (e.text.endsWith(suffix)) {\n const base = e.text\n .slice(0, -suffix.length)\n .replace(TRAILING_SEP, \"\")\n .trim();\n if (base.length >= 3 && !seenBases.has(base)) {\n seenBases.add(base);\n seeds.push({\n baseName: base,\n label: e.label,\n });\n }\n break;\n }\n }\n }\n\n if (seeds.length === 0) return [];\n\n // Build a mutable array of already-covered spans\n // for overlap checks. Updated as new entities are\n // emitted to prevent duplicate propagation.\n const covered: [number, number][] = entities.map((e) => [e.start, e.end]);\n const isOverlapping = (start: number, end: number): boolean =>\n covered.some(([cs, ce]) => start < ce && end > cs);\n\n const results: Entity[] = [];\n\n for (const seed of seeds) {\n const { baseName, label } = seed;\n let searchFrom = 0;\n while (searchFrom < fullText.length) {\n const idx = fullText.indexOf(baseName, searchFrom);\n if (idx === -1) break;\n\n const matchEnd = idx + baseName.length;\n\n // Word boundary: reject if preceded or followed\n // by a letter or digit (prevents substring\n // matches like \"ACME\" inside \"ACME2\").\n const prevCh = fullText[idx - 1] ?? \"\";\n const nextCh = fullText[matchEnd] ?? \"\";\n if (WORD_CHAR_RE.test(prevCh) || WORD_CHAR_RE.test(nextCh)) {\n searchFrom = idx + 1;\n continue;\n }\n\n // Extend the span backward to include a Czech / English\n // \"Společnost\"/\"the Company\"-style determiner if present.\n // Without this, after `(\"Společnost Acme\")` the\n // propagator would only highlight the bare \"Acme\" in\n // later mentions like \"Společnost Acme\" — losing the\n // determiner that's part of the referring phrase.\n let spanStart = idx;\n const lookbackStart = Math.max(0, idx - 40);\n const lookback = fullText.slice(lookbackStart, idx);\n const determinerMatch = ORG_DETERMINER_RE.exec(lookback);\n if (determinerMatch !== null) {\n // The match string may include a leading separator\n // char (the alternation accepts `^` or `[\\s ]`) and\n // trailing whitespace; the capture group is the\n // determiner itself, so locate it inside the wider\n // match to skip whatever separators were consumed.\n const determiner = determinerMatch[1] ?? \"\";\n const offsetInMatch = determinerMatch[0].indexOf(determiner);\n spanStart = lookbackStart + determinerMatch.index + offsetInMatch;\n }\n\n // Skip if already covered by an existing entity\n // or a previously propagated result.\n if (!isOverlapping(spanStart, matchEnd)) {\n results.push({\n start: spanStart,\n end: matchEnd,\n label,\n text: fullText.slice(spanStart, matchEnd),\n score: ORG_PROPAGATION_SCORE,\n source: DETECTION_SOURCES.COREFERENCE,\n });\n covered.push([spanStart, matchEnd]);\n }\n\n searchFrom = matchEnd;\n }\n }\n\n return results;\n};\n","","import addressStreetTypesJson from \"../data/address-street-types.json\";\nimport type { Entity } from \"../types\";\n\n// Capitalised words that look like the start of an\n// `[Uppercase] [number]` address (Czech: \"Vinohradská 12\")\n// but in contract prose introduce a section, clause, or\n// document reference instead (\"Section 6\", \"Article 9\").\n// Listed here so the `bareHouseRe` near-address scan does\n// not promote them to address spans. Module-level to avoid\n// allocation in a hot loop.\nconst BARE_STOPWORDS = new Set([\n // ── Czech ────────────────────────────────────────\n \"Příloha\",\n \"Smlouva\",\n \"Článek\",\n \"Dodatek\",\n \"Celkem\",\n \"Strana\",\n \"Faktura\",\n \"Částka\",\n \"Položka\",\n \"Kapitola\",\n \"Zákon\",\n \"Vyhláška\",\n \"Nařízení\",\n \"Usnesení\",\n \"Rozsudek\",\n \"Bod\",\n \"Odstavec\",\n \"Záloha\",\n \"Zbývá\",\n \"Dne\",\n \"Platba\",\n \"Datum\",\n \"Splatnost\",\n \"Variabilní\",\n \"Konstantní\",\n \"Specifický\",\n // ── English ──────────────────────────────────────\n \"Section\",\n \"Sections\",\n \"Article\",\n \"Articles\",\n \"Schedule\",\n \"Schedules\",\n \"Exhibit\",\n \"Exhibits\",\n \"Annex\",\n \"Annexes\",\n \"Appendix\",\n \"Appendices\",\n \"Clause\",\n \"Clauses\",\n \"Chapter\",\n \"Chapters\",\n \"Paragraph\",\n \"Paragraphs\",\n \"Subsection\",\n \"Subsections\",\n \"Form\",\n \"Page\",\n \"Pages\",\n \"Item\",\n \"Items\",\n \"Note\",\n \"Notes\",\n \"Rule\",\n \"Rules\",\n \"Attachment\",\n \"Attachments\",\n \"Volume\",\n \"Volumes\",\n \"Book\",\n \"Books\",\n \"Part\",\n \"Parts\",\n]);\n\nconst NEAR_MISS_BAND = 0.15;\nconst BOOST_PER_NEIGHBOUR = 0.05;\nconst CONTEXT_WINDOW_CHARS = 150;\nconst HIGH_CONFIDENCE_FLOOR = 0.9;\n\nconst isCallerOwnedEntity = (entity: Entity): boolean =>\n entity.sourceDetail === \"custom-deny-list\" ||\n entity.sourceDetail === \"custom-regex\";\n\n/**\n * Boost confidence of near-miss NER entities that appear\n * near high-confidence detections (regex, trigger phrase).\n *\n * If an NER entity scored between (threshold - 0.15) and\n * threshold, count how many confirmed entities exist within\n * a 150-char window. Add +0.05 per co-located entity.\n * If the boosted score crosses the threshold, include it.\n *\n * Only mutates score on near-miss entities; high-confidence\n * entities pass through unchanged.\n */\nexport const boostNearMissEntities = (\n entities: Entity[],\n threshold: number,\n): Entity[] => {\n const nearMissBand = Math.max(0, threshold - NEAR_MISS_BAND);\n const confirmed = entities.filter((e) => e.score >= HIGH_CONFIDENCE_FLOOR);\n\n const boosted: Entity[] = [];\n\n for (const entity of entities) {\n if (entity.score >= threshold) {\n boosted.push(entity);\n continue;\n }\n\n if (entity.score < nearMissBand) {\n continue;\n }\n\n const midpoint = (entity.start + entity.end) / 2;\n let neighbourCount = 0;\n\n for (const anchor of confirmed) {\n const anchorMid = (anchor.start + anchor.end) / 2;\n if (Math.abs(midpoint - anchorMid) <= CONTEXT_WINDOW_CHARS) {\n neighbourCount++;\n }\n }\n\n const boostedScore = entity.score + neighbourCount * BOOST_PER_NEIGHBOUR;\n\n if (boostedScore >= threshold) {\n boosted.push({ ...entity, score: boostedScore });\n }\n }\n\n return boosted;\n};\n\n// ── Address backward scan ────────────────────────────\n\nconst UPPER_WORD_RE = /\\p{Lu}/u;\n\n/** Header zone: top 15% of document */\nconst HEADER_ZONE_FRACTION = 0.15;\n\n/** Context window for address adjacency */\nconst STREET_CONTEXT_WINDOW = 200;\n\n// ── Preposition data (lazy-loaded from JSON) ────────\n\ntype PrepositionData = {\n address: Record<string, string[] | string>;\n temporal: Record<string, string[] | string>;\n};\n\nlet _addressPreps: ReadonlySet<string> | null = null;\nlet _temporalPreps: ReadonlySet<string> | null = null;\nlet _prepsPromise: Promise<void> | null = null;\n\nconst loadPrepositions = async (): Promise<void> => {\n try {\n const mod = await import(\"../data/address-prepositions.json\");\n const data: PrepositionData = mod.default ?? mod;\n // Merge all languages into flat sets\n const addr = new Set<string>();\n const temp = new Set<string>();\n for (const words of Object.values(data.address)) {\n if (Array.isArray(words)) {\n for (const w of words) addr.add(w.toLowerCase());\n }\n }\n for (const words of Object.values(data.temporal)) {\n if (Array.isArray(words)) {\n for (const w of words) temp.add(w.toLowerCase());\n }\n }\n _addressPreps = addr;\n _temporalPreps = temp;\n } catch {\n _addressPreps = new Set();\n _temporalPreps = new Set();\n }\n};\n\n/** Ensure preposition data is loaded. */\nexport const initPrepositions = (): Promise<void> => {\n if (!_prepsPromise) {\n _prepsPromise = loadPrepositions();\n }\n return _prepsPromise;\n};\n\nconst getAddressPreps = (): ReadonlySet<string> => _addressPreps ?? new Set();\n\nconst getTemporalPreps = (): ReadonlySet<string> => _temporalPreps ?? new Set();\n\n// ── Street type abbreviations (lazy-loaded) ─────────\n\nconst buildStreetAbbrevs = (\n data: Record<string, unknown>,\n): ReadonlySet<string> => {\n const abbrevs = new Set<string>();\n for (const [key, words] of Object.entries(data)) {\n if (key.startsWith(\"_\")) continue;\n if (!Array.isArray(words)) continue;\n for (const word of words) {\n if (typeof word === \"string\" && word.includes(\".\")) {\n abbrevs.add(word.toLowerCase());\n }\n }\n }\n return abbrevs;\n};\n\nlet _streetAbbrevs: ReadonlySet<string> | null = buildStreetAbbrevs(\n addressStreetTypesJson,\n);\nlet _streetAbbrevsPromise: Promise<void> | null = null;\n\nconst loadStreetAbbrevs = async (): Promise<void> => {\n try {\n const mod = await import(\"../data/address-street-types.json\");\n const data: Record<string, string[] | string> = mod.default ?? mod;\n _streetAbbrevs = buildStreetAbbrevs(data);\n } catch {\n _streetAbbrevs ??= new Set();\n }\n};\n\n/** Ensure street abbreviation data is loaded. */\nexport const initStreetAbbrevs = (): Promise<void> => {\n if (!_streetAbbrevsPromise) {\n _streetAbbrevsPromise = loadStreetAbbrevs();\n }\n return _streetAbbrevsPromise;\n};\n\nexport const getStreetAbbrevs = (): ReadonlySet<string> =>\n _streetAbbrevs ?? new Set();\n\n/**\n * Scan backwards from known address entities and\n * house number patterns to find street names.\n *\n * Strategy (a): from a house number like \"2512/2a\",\n * walk left to find the first uppercase word — that's\n * the street name start. \"Mezi úvozy 2512/2a\" →\n * captures \"Mezi úvozy 2512/2a\".\n *\n * Strategy (b): if a colon \":\" appears within 3 chars\n * before the street start, boost confidence. Colons\n * signal \"label: value\" pairs universal in contracts.\n *\n * Strategy (c): in the header zone (top 15% of doc),\n * be more aggressive — detect street patterns even\n * without nearby address entities.\n */\nexport const detectStreetPatternsNearAddresses = (\n fullText: string,\n existingEntities: Entity[],\n): Entity[] => {\n const results: Entity[] = [];\n const contextEntities = existingEntities.filter(\n (entity) => !(entity.label === \"address\" && isCallerOwnedEntity(entity)),\n );\n const addressEntities = contextEntities.filter((e) => e.label === \"address\");\n const headerEnd = Math.floor(fullText.length * HEADER_ZONE_FRACTION);\n\n // Find all house number positions in the text.\n // Slash-style house numbers only: either with a letter\n // suffix (\"2512/2a\") or primary >= 100 (\"853/12\").\n // This excludes date-like patterns (\"31/12\", \"1/1\").\n // Require either a letter suffix (2512/2a) or a\n // primary part > 31 to avoid matching Czech slash\n // dates like \"31/12\" or \"1/1\". Slash house numbers\n // in Czech addresses almost always have primary > 99\n // or carry a letter suffix.\n const houseNumRe =\n /\\b(?:\\d{1,4}\\/\\d+[a-zA-Z]\\b|\\d{3,4}\\/\\d+\\b|(?:1[3-9]|[2-9]\\d)\\/\\d{3,}\\b)/g;\n houseNumRe.lastIndex = 0;\n\n for (\n let m = houseNumRe.exec(fullText);\n m !== null;\n m = houseNumRe.exec(fullText)\n ) {\n const numStart = m.index;\n const numEnd = numStart + m[0].length;\n\n // Skip if already covered by an existing entity\n if (existingEntities.some((e) => e.start <= numStart && e.end >= numEnd)) {\n continue;\n }\n\n // Is this near a known address entity OR in header?\n const inHeader = numStart < headerEnd;\n const nearAddress = addressEntities.some(\n (e) =>\n Math.abs(e.start - numEnd) < STREET_CONTEXT_WINDOW ||\n Math.abs(e.end - numStart) < STREET_CONTEXT_WINDOW,\n );\n\n if (!inHeader && !nearAddress) {\n continue;\n }\n\n // Backward scan: find street name before the\n // house number. Walk left over whitespace, then\n // collect words that start with uppercase.\n let scanPos = numStart - 1;\n\n // Skip whitespace before the number (including\n // non-breaking spaces from PDF extraction)\n while (scanPos >= 0 && /[\\s\\u00A0]/.test(fullText[scanPos] ?? \"\")) {\n scanPos--;\n }\n\n if (scanPos < 0) {\n continue;\n }\n\n // Track if any temporal preposition was passed\n // through during the scan (e.g., \"do\", \"od\").\n // If so, the result is likely a date expression\n // even if wordCount > 1 (\"Praha do 225/1\").\n let hasTemporalPrep = false;\n\n // Collect words backwards until we hit:\n // - a non-letter character (except space/dot)\n // - a newline\n // - start of text\n // - a lowercase-only word (not a street name)\n let streetStart = scanPos + 1;\n let wordCount = 0;\n const MAX_WORDS = 5;\n\n while (scanPos >= 0 && wordCount < MAX_WORDS) {\n // Find end of current word. If we're on a dot,\n // include it (street abbreviations: \"ul.\", \"nám.\")\n let wordEnd = scanPos + 1;\n const hasDot = fullText[scanPos] === \".\";\n if (hasDot) {\n scanPos--;\n }\n\n // Walk back through word chars (letters, marks,\n // and digits for tokens like \"28\" in \"28. října\").\n while (scanPos >= 0 && /[\\p{L}\\p{M}\\d]/u.test(fullText[scanPos] ?? \"\")) {\n scanPos--;\n }\n const wordStart = scanPos + 1;\n const rawWord = fullText.slice(wordStart, wordEnd);\n // Strip trailing dot for word checks\n const word = hasDot ? rawWord.slice(0, -1) : rawWord;\n\n if (word.length === 0) {\n break;\n }\n\n // Check if this is a known street abbreviation\n // (e.g., \"ul.\", \"nám.\", \"tř.\", \"nábř.\")\n const isStreetAbbrev =\n hasDot && getStreetAbbrevs().has(rawWord.toLowerCase());\n\n // Word must start with uppercase (street name),\n // be a known preposition, a street abbreviation,\n // or a digit-starting token (e.g., \"28.\" in\n // \"28. října 1168/102\").\n const isUpper = UPPER_WORD_RE.test(word[0] ?? \"\");\n const isPrep = getAddressPreps().has(word.toLowerCase());\n const isDigitToken = /^\\d{1,2}$/.test(word);\n\n if (!isUpper && !isPrep && !isStreetAbbrev && !isDigitToken) {\n break;\n }\n\n // Track temporal prepositions passed through\n if (isPrep && getTemporalPreps().has(word.toLowerCase())) {\n hasTemporalPrep = true;\n }\n\n streetStart = wordStart;\n wordCount++;\n\n // Skip whitespace before this word (inc. NBSP)\n while (scanPos >= 0 && /[\\s\\u00A0]/.test(fullText[scanPos] ?? \"\")) {\n scanPos--;\n }\n\n // Stop at newline, tab, comma, semicolon\n const prevCh = fullText[scanPos];\n if (\n prevCh === \"\\n\" ||\n prevCh === \"\\t\" ||\n prevCh === \";\" ||\n prevCh === undefined\n ) {\n break;\n }\n\n // Comma: stop (it separates address from\n // previous clause)\n if (prevCh === \",\") {\n break;\n }\n }\n\n if (wordCount === 0) {\n continue;\n }\n\n const streetText = fullText.slice(streetStart, numEnd);\n\n // Skip if too short (single digit without name)\n if (streetText.length < 4) {\n continue;\n }\n\n // Guard: reject if any temporal preposition was\n // encountered during the backward scan. Catches\n // both \"do 225/1\" (wordCount=1) and \"Praha do\n // 225/1\" (wordCount=2).\n if (hasTemporalPrep) {\n continue;\n }\n\n // Skip if already covered\n if (\n existingEntities.some((e) => e.start <= streetStart && e.end >= numEnd)\n ) {\n continue;\n }\n\n // Colon boost: if \":\" appears within 5 chars\n // before street start, this is a \"label: value\"\n // pair — very high confidence.\n const beforeStreet = fullText.slice(\n Math.max(0, streetStart - 5),\n streetStart,\n );\n const hasColon = beforeStreet.includes(\":\");\n let score = 0.8;\n if (hasColon) {\n score = 0.95;\n } else if (inHeader) {\n score = 0.85;\n }\n\n results.push({\n start: streetStart,\n end: numEnd,\n label: \"address\",\n text: streetText,\n score,\n source: \"regex\",\n });\n }\n\n // ── Second scan: bare house numbers near addresses ─\n // \"Vinohradská 46\" near \"Praha 2\" → address.\n // Uppercase word + space + 1-3 digit number, no slash.\n // Capped at 3 digits to exclude year numbers (2024).\n const bareHouseRe = /(?<=\\s|^)(\\p{Lu}\\p{Ll}[\\p{Ll}\\p{Lu}]+\\s+\\d{1,3})\\b/gu;\n bareHouseRe.lastIndex = 0;\n\n // Merge existing + newly found entities for proximity\n const allAddr = [...addressEntities, ...results];\n\n for (\n let m = bareHouseRe.exec(fullText);\n m !== null;\n m = bareHouseRe.exec(fullText)\n ) {\n const captured = m[1];\n if (captured === undefined) continue;\n\n const start = m.index;\n const end = start + captured.length;\n\n // Must be on the same line as a confirmed address\n // entity and within 50 chars.\n const nearAddr = allAddr.some((e) => {\n const dist = Math.min(Math.abs(e.start - end), Math.abs(e.end - start));\n if (dist > 50) return false;\n // Ensure same line: no newline between\n const lo = Math.min(e.start, start);\n const hi = Math.max(e.end, end);\n const between = fullText.slice(lo, hi);\n return !between.includes(\"\\n\");\n });\n\n if (!nearAddr) continue;\n\n // Extract the uppercase word to check stopwords\n const spaceIdx = captured.search(/\\s+\\d/);\n const word = spaceIdx > 0 ? captured.slice(0, spaceIdx) : captured;\n\n if (BARE_STOPWORDS.has(word)) continue;\n\n // Skip if overlapping an existing entity\n const allEntities = [...existingEntities, ...results];\n const overlaps = allEntities.some((e) => e.start < end && e.end > start);\n if (overlaps) continue;\n\n results.push({\n start,\n end,\n label: \"address\",\n text: captured,\n score: 0.75,\n source: \"regex\",\n });\n }\n\n return results;\n};\n\n// ── Orphan street lines in header zone ──────────────\n\n// Orphan street: first word uppercase, subsequent words\n// can be lowercase (Czech: \"Karlínské náměstí 7\",\n// \"Pražská ulice 12\") or uppercase (\"Národní třída 1\").\n// House number requires 2+ digits to avoid matching\n// contract headings like \"Příloha 1\" or \"Smlouva 3\".\n// Inner whitespace is constrained to non-newline runs so a\n// table-of-contents block like\n// `Litigation\\n \\n \\n26\\n` does not accidentally satisfy\n// the pattern: a real street + number sits on a single line.\nconst ORPHAN_STREET_RE =\n /^[^\\S\\n]*(\\p{Lu}[\\p{Ll}\\p{Lu}]+(?:[^\\S\\n]+[\\p{Lu}\\p{Ll}][\\p{Ll}]+)*[^\\S\\n]+\\d{2,4}[a-zA-Z]?)[^\\S\\n]*$/gmu;\n\n/**\n * In the header zone (top 15%), find standalone lines\n * matching \"[Uppercase word(s)] [number]\" that sit\n * between other detected entities. These are almost\n * certainly street addresses in party definitions.\n *\n * Example: \"Evropská 710\" on its own line between\n * an organization entity and a postal code entity.\n */\nexport const detectOrphanStreetLines = (\n fullText: string,\n existingEntities: Entity[],\n): Entity[] => {\n const headerEnd = Math.floor(fullText.length * HEADER_ZONE_FRACTION);\n const contextEntities = existingEntities.filter(\n (entity) => !(entity.label === \"address\" && isCallerOwnedEntity(entity)),\n );\n const results: Entity[] = [];\n ORPHAN_STREET_RE.lastIndex = 0;\n\n for (\n let m = ORPHAN_STREET_RE.exec(fullText);\n m !== null;\n m = ORPHAN_STREET_RE.exec(fullText)\n ) {\n const captured = m[1];\n if (captured === undefined) {\n continue;\n }\n const start = m.index + m[0].indexOf(captured);\n const end = start + captured.length;\n\n // Only in header zone\n if (start >= headerEnd) {\n continue;\n }\n\n // Skip if already covered\n if (existingEntities.some((e) => e.start <= start && e.end >= end)) {\n continue;\n }\n\n // Must have a nearby entity (within 200 chars)\n const hasContext = contextEntities.some(\n (e) => Math.abs(e.start - end) < 200 || Math.abs(e.end - start) < 200,\n );\n if (!hasContext) {\n continue;\n }\n\n results.push({\n start,\n end,\n label: \"address\",\n text: captured,\n score: 0.85,\n source: \"regex\",\n });\n }\n\n return results;\n};\n","import type { Entity } from \"../types\";\nimport type { PipelineContext } from \"../context\";\nimport { defaultContext } from \"../context\";\n\n// ── Zone types ───────────────────────────────────\n\nexport type DocumentZone = \"header\" | \"signature\" | \"body\" | \"table\";\n\nexport type ZoneSpan = {\n zone: DocumentZone;\n start: number;\n end: number;\n};\n\n// ── Score adjustments per zone ───────────────────\n\n/**\n * Additive score adjustments per document zone.\n * Header and signature blocks are dense with PII;\n * tables often contain structured identifying data.\n */\nexport const ZONE_SCORE_ADJUSTMENTS = {\n header: 0.1,\n signature: 0.15,\n body: 0,\n table: 0.05,\n} as const satisfies Record<DocumentZone, number>;\n\n// ── Lazy-loaded config ───────────────────────────\n\ntype SectionHeadingsConfig = {\n patterns: Array<{ re: string; flags: string }>;\n};\n\ntype SigningClauseConfig = {\n patterns: Array<{\n lang: string;\n prefix: string;\n suffix: string;\n prepositions: string[];\n }>;\n};\n\nconst loadSectionHeadings = async (): Promise<RegExp[]> => {\n const mod = await import(\"../data/section-headings.json\");\n const data: SectionHeadingsConfig = mod.default ?? mod;\n return data.patterns.map((p) => new RegExp(p.re, p.flags));\n};\n\nconst loadSigningClauses = async (): Promise<RegExp[]> => {\n const mod = await import(\"../data/signing-clauses.json\");\n const data: SigningClauseConfig = mod.default ?? mod;\n return data.patterns.map((p) => {\n // Build a pattern that matches the signing\n // clause prefix at the start of a line.\n // Note: prefix/suffix are regex fragments by\n // design (e.g. `(?:V|Ve)\\\\s+`), not literals.\n const prefix = p.prefix || \"\";\n const suffix = p.suffix || \"\";\n // Include prepositions so multi-word place\n // names like \"Ústí nad Labem\" are matched,\n // consistent with buildSigningClausePatterns\n // in detectors/regex.ts.\n const prepAlt = p.prepositions.length > 0 ? p.prepositions.join(\"|\") : null;\n const place = prepAlt\n ? `\\\\p{Lu}\\\\p{Ll}+` +\n `(?:\\\\s+(?:${prepAlt})` +\n `\\\\s+\\\\p{Lu}\\\\p{Ll}+)*` +\n `(?:\\\\s+\\\\p{Lu}\\\\p{Ll}+)*`\n : `\\\\p{Lu}\\\\p{Ll}+` + `(?:[- ]\\\\p{Lu}\\\\p{Ll}+)*`;\n // Anchor to the start of the line. Each line is\n // already split on \\n, so ^ suffices without m.\n const combined = `^\\\\s*(?:${prefix}${place}${suffix})`;\n return new RegExp(combined, \"u\");\n });\n};\n\n/**\n * Ensure config data is loaded. Call once before\n * classifyZones. Safe to call multiple times.\n */\nexport const initZoneClassifier = (\n ctx: PipelineContext = defaultContext,\n): Promise<void> => {\n if (ctx.zoneInitPromise) return ctx.zoneInitPromise;\n ctx.zoneInitPromise = Promise.all([\n loadSectionHeadings(),\n loadSigningClauses(),\n ])\n .then(([headings, clauses]) => {\n ctx.zoneHeadingPatterns = headings;\n ctx.zoneSigningPatterns = clauses;\n })\n .catch((err: unknown) => {\n // Clear cached promise so a subsequent call\n // can retry after a transient failure.\n ctx.zoneInitPromise = null;\n throw err;\n });\n return ctx.zoneInitPromise;\n};\n\n// ── Table detection ──────────────────────────────\n\nconst MIN_TABS_FOR_TABLE = 2;\n\nconst isTableLine = (line: string): boolean => {\n let tabCount = 0;\n for (const ch of line) {\n if (ch === \"\\t\") tabCount++;\n if (tabCount >= MIN_TABS_FOR_TABLE) return true;\n }\n return false;\n};\n\n// ── Zone classification ──────────────────────────\n\n/**\n * Classify a document into zones based on\n * structural heuristics. Zones are non-overlapping\n * and cover the entire text.\n *\n * Must call `initZoneClassifier()` first.\n */\nexport const classifyZones = (\n fullText: string,\n ctx: PipelineContext = defaultContext,\n): ZoneSpan[] => {\n if (fullText.length === 0) return [];\n\n const headingRes = ctx.zoneHeadingPatterns;\n const signingRes = ctx.zoneSigningPatterns;\n\n if (!headingRes || !signingRes) {\n console.warn(\n \"[anonymize] classifyZones called before \" +\n \"initZoneClassifier(); returning body-only\",\n );\n return [{ zone: \"body\", start: 0, end: fullText.length }];\n }\n\n const lines = fullText.split(\"\\n\");\n const zones: ZoneSpan[] = [];\n\n // Find header end: first section heading line\n let headerEndLine = -1;\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n if (line === undefined) continue;\n for (const re of headingRes) {\n if (re.test(line)) {\n headerEndLine = i;\n break;\n }\n }\n if (headerEndLine !== -1) break;\n }\n\n // Find signature start: last signing clause line\n let signatureStartLine = -1;\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i];\n if (line === undefined) continue;\n for (const re of signingRes) {\n if (re.test(line)) {\n signatureStartLine = i;\n break;\n }\n }\n if (signatureStartLine !== -1) break;\n }\n\n // Build offset map: line index -> char offset\n const lineOffsets: number[] = [];\n let offset = 0;\n for (const line of lines) {\n lineOffsets.push(offset);\n // +1 for the newline character\n offset += line.length + 1;\n }\n\n // Determine zone boundaries\n let headerEndOffset =\n headerEndLine >= 0 ? (lineOffsets[headerEndLine] ?? 0) : 0;\n\n const signatureStartOffset =\n signatureStartLine >= 0\n ? (lineOffsets[signatureStartLine] ?? fullText.length)\n : fullText.length;\n\n // Guard: if the signing clause appears before\n // the first section heading, the zones would\n // overlap. Treat as degenerate layout: drop the\n // header so the signature zone takes priority.\n if (\n headerEndLine > 0 &&\n signatureStartLine >= 0 &&\n headerEndOffset > signatureStartOffset\n ) {\n headerEndLine = -1;\n headerEndOffset = 0;\n }\n\n // Add header zone if detected. Using > 0 (not\n // >= 0) intentionally: when the section heading is\n // on line 0, there is no preamble to classify as a\n // header — everything starts as body.\n if (headerEndLine > 0) {\n zones.push({\n zone: \"header\",\n start: 0,\n end: headerEndOffset,\n });\n }\n\n // Scan body region for table zones\n const bodyStart = headerEndLine > 0 ? headerEndOffset : 0;\n const bodyEnd =\n signatureStartLine >= 0 ? signatureStartOffset : fullText.length;\n\n let tableStart = -1;\n for (\n let i = Math.max(headerEndLine, 0);\n i < (signatureStartLine >= 0 ? signatureStartLine : lines.length);\n i++\n ) {\n const line = lines[i];\n if (line === undefined) continue;\n const lineStart = lineOffsets[i] ?? 0;\n const lineEnd = lineStart + line.length;\n\n if (isTableLine(line)) {\n if (tableStart === -1) {\n tableStart = lineStart;\n }\n } else if (tableStart !== -1) {\n zones.push({\n zone: \"table\",\n start: tableStart,\n end: lineStart,\n });\n tableStart = -1;\n }\n\n // Close table at the end of body range\n if (\n i ===\n (signatureStartLine >= 0 ? signatureStartLine - 1 : lines.length - 1) &&\n tableStart !== -1\n ) {\n zones.push({\n zone: \"table\",\n start: tableStart,\n end: Math.min(lineEnd + 1, bodyEnd),\n });\n tableStart = -1;\n }\n }\n\n // Fill body gaps between header/table/signature\n const sortedSpecial = zones.toSorted((a, b) => a.start - b.start);\n\n let cursor = bodyStart;\n const bodyZones: ZoneSpan[] = [];\n for (const span of sortedSpecial) {\n if (span.zone === \"header\") continue;\n if (span.start > cursor) {\n bodyZones.push({\n zone: \"body\",\n start: cursor,\n end: span.start,\n });\n }\n cursor = Math.max(cursor, span.end);\n }\n if (cursor < bodyEnd) {\n bodyZones.push({\n zone: \"body\",\n start: cursor,\n end: bodyEnd,\n });\n }\n\n for (const z of bodyZones) zones.push(z);\n\n // Add signature zone if detected\n if (signatureStartLine >= 0) {\n zones.push({\n zone: \"signature\",\n start: signatureStartOffset,\n end: fullText.length,\n });\n }\n\n return zones.toSorted((a, b) => a.start - b.start);\n};\n\n// ── Entity zone lookup ───────────────────────────\n\n/**\n * Find which zone an entity's midpoint falls in.\n * Returns \"body\" if no zone matches (defensive).\n */\nconst findZone = (midpoint: number, zones: ZoneSpan[]): DocumentZone => {\n for (const span of zones) {\n if (midpoint >= span.start && midpoint < span.end) {\n return span.zone;\n }\n }\n return \"body\";\n};\n\n/**\n * Apply zone-based score adjustments to entities.\n * Entities in header/signature/table zones get a\n * small additive boost reflecting the higher PII\n * density in those regions.\n *\n * Returns a new array; does not mutate inputs.\n */\nexport const applyZoneAdjustments = (\n entities: Entity[],\n zones: ZoneSpan[],\n): Entity[] => {\n if (zones.length === 0) {\n return entities.map((e) => ({ ...e }));\n }\n\n const result: Entity[] = [];\n for (const entity of entities) {\n const midpoint = (entity.start + entity.end) / 2;\n const zone = findZone(midpoint, zones);\n const adjustment = ZONE_SCORE_ADJUSTMENTS[zone];\n\n if (adjustment > 0) {\n result.push({\n ...entity,\n score: Math.min(1, entity.score + adjustment),\n });\n } else {\n result.push({ ...entity });\n }\n }\n return result;\n};\n","import type { Match, PatternEntry } from \"@stll/text-search\";\n\nimport { getTextSearch } from \"../search-engine\";\nimport type { Entity } from \"../types\";\n\n// ── Types ───────────────────────────────────────────\n\nexport type HotwordRule = {\n hotwords: string[];\n targetLabels: string[];\n scoreAdjustment: number;\n reclassifyTo?: string;\n proximityBefore: number;\n proximityAfter: number;\n};\n\ntype HotwordRulesConfig = {\n rules: HotwordRule[];\n};\n\n// ── Lazy-loaded state ───────────────────────────────\n\nlet rules: HotwordRule[] | null = null;\nlet search: { findIter: (text: string) => Match[] } | null = null;\n/**\n * Maps each TextSearch pattern index back to the\n * rule index that owns it, so a single AC scan\n * resolves all hotword hits to their rule.\n */\nlet patternToRule: number[] | null = null;\nlet initPromise: Promise<void> | null = null;\n\n// ── Init ────────────────────────────────────────────\n\nconst loadRules = async (): Promise<void> => {\n const mod = await import(\"../data/hotword-rules.json\");\n const data: HotwordRulesConfig = mod.default ?? mod;\n const loaded = data.rules;\n\n // Build a flat pattern list and the reverse map.\n const patterns: PatternEntry[] = [];\n const mapping: number[] = [];\n\n for (let ruleIdx = 0; ruleIdx < loaded.length; ruleIdx++) {\n const rule = loaded[ruleIdx];\n if (!rule) continue;\n for (const hw of rule.hotwords) {\n patterns.push({\n pattern: hw,\n literal: true,\n caseInsensitive: true,\n });\n mapping.push(ruleIdx);\n }\n }\n\n const builtSearch =\n patterns.length > 0\n ? new (getTextSearch())(patterns, {\n overlapStrategy: \"all\",\n caseInsensitive: true,\n wholeWords: true,\n })\n : null;\n\n // Assign all module state atomically after all\n // failable operations succeed, so a mid-flight\n // error cannot leave a half-initialized state.\n patternToRule = mapping;\n search = builtSearch;\n rules = loaded;\n};\n\n/**\n * Load hotword rules from the data package.\n * Safe to call multiple times; subsequent calls\n * are no-ops.\n */\nexport const initHotwordRules = async (): Promise<void> => {\n if (rules !== null) return;\n if (initPromise !== null) return initPromise;\n initPromise = loadRules().catch((err) => {\n // Reset so callers can retry on transient failure.\n initPromise = null;\n throw err;\n });\n return initPromise;\n};\n\n/**\n * Expand requested output labels with any source labels\n * that hotword rules may reclassify into them.\n *\n * Example: requesting only \"date of birth\" still needs\n * \"date\" candidates to survive until the hotword pass.\n * If rules are not initialized, or if no labels were\n * requested, returns the input labels unchanged.\n */\nexport const expandLabelsForHotwordRules = (\n requestedLabels: readonly string[],\n): readonly string[] => {\n if (rules === null || requestedLabels.length === 0) {\n return requestedLabels;\n }\n\n const requested = new Set(requestedLabels);\n const expanded = new Set(requestedLabels);\n\n for (const rule of rules) {\n if (rule.reclassifyTo === undefined || !requested.has(rule.reclassifyTo)) {\n continue;\n }\n for (const label of rule.targetLabels) {\n expanded.add(label);\n }\n }\n\n return [...expanded];\n};\n\n// ── Application ─────────────────────────────────────\n\n/**\n * Apply hotword context rules to detected entities.\n *\n * Scans `fullText` once with a single AC automaton\n * for all hotwords across all rules, then checks\n * proximity to each entity. Distance-decayed\n * adjustment: closer hotwords give a stronger boost.\n *\n * Returns a new array; input entities are not mutated.\n */\nexport const applyHotwordRules = (\n entities: Entity[],\n fullText: string,\n): Entity[] => {\n if (\n rules === null ||\n rules.length === 0 ||\n search === null ||\n patternToRule === null\n ) {\n return entities;\n }\n\n // Single scan for all hotword positions.\n const hits = search.findIter(fullText);\n if (hits.length === 0) return entities;\n\n // Group hits by rule index for fast lookup.\n const hitsByRule = new Map<number, Match[]>();\n for (const hit of hits) {\n const ruleIdx = patternToRule[hit.pattern];\n if (ruleIdx === undefined) continue;\n let bucket = hitsByRule.get(ruleIdx);\n if (bucket === undefined) {\n bucket = [];\n hitsByRule.set(ruleIdx, bucket);\n }\n bucket.push(hit);\n }\n\n const result: Entity[] = [];\n\n for (const entity of entities) {\n let bestAdjustment = 0;\n let bestReclassify: string | undefined;\n\n for (let ruleIdx = 0; ruleIdx < rules.length; ruleIdx++) {\n const rule = rules[ruleIdx];\n if (!rule) continue;\n\n // Check if entity label matches this rule.\n if (!rule.targetLabels.includes(entity.label)) {\n continue;\n }\n\n const ruleHits = hitsByRule.get(ruleIdx);\n if (ruleHits === undefined) continue;\n\n for (const hit of ruleHits) {\n // Asymmetric proximity check.\n // Hotword BEFORE entity: hotword end <=\n // entity start, distance = entity.start -\n // hit.end\n // Hotword AFTER entity: hotword start >=\n // entity end, distance = hit.start -\n // entity.end\n let distance: number;\n let maxDistance: number;\n\n if (hit.end <= entity.start) {\n // Hotword is before the entity.\n distance = entity.start - hit.end;\n maxDistance = rule.proximityBefore;\n } else if (hit.start >= entity.end) {\n // Hotword is after the entity.\n distance = hit.start - entity.end;\n maxDistance = rule.proximityAfter;\n } else {\n // Hotword overlaps the entity: distance 0,\n // use larger window for max.\n distance = 0;\n maxDistance = Math.max(rule.proximityBefore, rule.proximityAfter);\n }\n\n if (distance > maxDistance) continue;\n\n // Distance-decayed adjustment.\n const decay = maxDistance === 0 ? 1 : 1 - distance / maxDistance;\n const adj = rule.scoreAdjustment * decay;\n\n if (Math.abs(adj) > Math.abs(bestAdjustment)) {\n bestAdjustment = adj;\n // Only carry reclassification from boost\n // rules; a penalty winner must not silently\n // overwrite a label set by a closer positive\n // rule.\n if (adj > 0) {\n bestReclassify = rule.reclassifyTo;\n } else {\n bestReclassify = undefined;\n }\n }\n }\n }\n\n if (bestAdjustment === 0) {\n result.push(entity);\n continue;\n }\n\n const newScore = Math.min(1, Math.max(0, entity.score + bestAdjustment));\n const newLabel =\n bestReclassify !== undefined ? bestReclassify : entity.label;\n\n result.push({\n ...entity,\n score: newScore,\n label: newLabel,\n });\n }\n\n return result;\n};\n","import type { Entity } from \"../types\";\nimport { DETECTION_SOURCES } from \"../types\";\n\n/** Max gap (in chars) between entities to merge. */\nconst MAX_GAP = 3;\n\nconst hasLockedBoundary = (entity: Entity): boolean =>\n entity.sourceDetail === \"custom-deny-list\" ||\n entity.sourceDetail === \"custom-regex\";\n\n/**\n * Characters allowed in the gap between two adjacent\n * same-label entities that should be merged: spaces,\n * tabs, commas, and hyphens. Uses `[ \\t,-]` instead\n * of `\\s` to avoid merging entities across newlines.\n */\nconst GAP_PATTERN = /^[ \\t,-]+$/;\n\nconst isLegalFormOrganization = (entity: Entity): boolean =>\n entity.label === \"organization\" &&\n entity.source === DETECTION_SOURCES.LEGAL_FORM;\n\n/**\n * Build a set of word boundary offsets for the full\n * text using `Intl.Segmenter`. Returns a sorted array\n * of offsets where words start and end.\n */\nconst buildWordBoundaries = (text: string): Set<number> => {\n // Use \"und\" (undetermined) locale for consistent\n // word-boundary results across environments.\n const segmenter = new Intl.Segmenter(\"und\", {\n granularity: \"word\",\n });\n const boundaries = new Set<number>();\n for (const seg of segmenter.segment(text)) {\n if (!seg.isWordLike) continue;\n boundaries.add(seg.index);\n boundaries.add(seg.index + seg.segment.length);\n }\n return boundaries;\n};\n\n/**\n * Characters that act as hard stops when scanning\n * backward for a word boundary. Entity boundaries\n * should never extend past these.\n */\nconst WORD_START_STOPS = new Set([\n \"\\n\",\n \"\\r\",\n \",\",\n \";\",\n \"(\",\n \")\",\n \"[\",\n \"]\",\n \"&\",\n]);\n\n/**\n * Find the word-start offset at or before `pos`.\n * Scans left until a word boundary is found.\n */\nconst wordStartAt = (\n pos: number,\n boundaries: Set<number>,\n text: string,\n): number => {\n let p = pos;\n while (p > 0 && !boundaries.has(p)) {\n const prev = text[p - 1];\n if (prev !== undefined && WORD_START_STOPS.has(prev)) {\n return p;\n }\n p--;\n }\n return p;\n};\n\n/**\n * Characters that act as hard stops when scanning\n * forward for a word boundary. Entity boundaries\n * should never extend past these.\n */\nconst WORD_END_STOPS = new Set([\n \"\\n\",\n \"\\r\",\n \",\",\n \";\",\n \".\",\n \"(\",\n \")\",\n \"[\",\n \"]\",\n \"&\",\n]);\n\n/**\n * Find the word-end offset at or after `pos`.\n * Scans right until a word boundary is found.\n */\nconst wordEndAt = (\n pos: number,\n boundaries: Set<number>,\n text: string,\n): number => {\n let p = pos;\n while (p < text.length && !boundaries.has(p)) {\n const ch = text[p];\n if (ch !== undefined && WORD_END_STOPS.has(ch)) {\n return p;\n }\n p++;\n }\n return p;\n};\n\n/**\n * Binary search: find the leftmost index in `arr`\n * where `arr[index].start >= value`.\n */\nconst lowerBound = (arr: Entity[], value: number): number => {\n let lo = 0;\n let hi = arr.length;\n while (lo < hi) {\n const mid = (lo + hi) >>> 1;\n const el = arr[mid];\n if (el && el.start < value) {\n lo = mid + 1;\n } else {\n hi = mid;\n }\n }\n return lo;\n};\n\n/**\n * Merge adjacent same-label entities separated only by\n * whitespace, comma, or hyphen (max 3 chars). Also\n * merges same-label entities that partially overlap\n * (which can happen after word-boundary expansion).\n *\n * Looks for the last same-label entity in the result\n * (not just the very last entity) so that an\n * intervening different-label entity does not prevent\n * merging.\n *\n * Uses binary search for the `gapOccupied` check and\n * a Map for O(1) same-label prev lookup. O(n log n).\n */\nconst mergeAdjacent = (entities: Entity[], fullText: string): Entity[] => {\n const sorted = entities.toSorted((a, b) => a.start - b.start);\n const result: Entity[] = [];\n // O(1) lookup for the last same-label entity in\n // result, replacing the O(n) backward scan.\n const lastByLabel = new Map<string, Entity>();\n\n for (const entity of sorted) {\n if (hasLockedBoundary(entity)) {\n const copy = { ...entity };\n result.push(copy);\n continue;\n }\n\n const prev = lastByLabel.get(entity.label);\n\n if (!prev) {\n const copy = { ...entity };\n result.push(copy);\n lastByLabel.set(entity.label, copy);\n continue;\n }\n\n // Handle overlap created by fixPartialWords:\n // two same-label entities may now partially overlap\n // after word-boundary expansion.\n if (!hasLockedBoundary(prev) && entity.start < prev.end) {\n prev.end = Math.max(prev.end, entity.end);\n prev.text = fullText.slice(prev.start, prev.end);\n prev.score = Math.max(prev.score, entity.score);\n continue;\n }\n\n const gap = fullText.slice(prev.end, entity.start);\n // GAP_PATTERN uses `+` quantifier, so empty gaps\n // (zero-gap / touching entities) won't match.\n // Also reject merging when a different-label entity\n // occupies the gap range (would create cross-label\n // overlap). Use binary search to find candidates\n // in the gap range instead of scanning all entities.\n const gapStart = prev.end;\n const gapEnd = entity.start;\n const searchIdx = lowerBound(sorted, gapStart);\n let gapOccupied = false;\n for (let k = searchIdx; k < sorted.length; k++) {\n const other = sorted[k];\n if (!other || other.start >= gapEnd) break;\n if (other.label !== entity.label && other.end > gapStart) {\n gapOccupied = true;\n break;\n }\n }\n\n // Touching entities (entity.start === prev.end) leave a zero-\n // length gap that GAP_PATTERN's `+` quantifier refuses to match;\n // the two would stay split even though they sit flush against\n // each other. `cs-address-psc` emits a leading-space variant of\n // the postal code that ends up flush with the preceding street\n // address (`Kamínky 302/16, Brno` + ` 634 00`); treat them as\n // mergeable.\n const isMergeableGap =\n gap.length === 0 || (gap.length <= MAX_GAP && GAP_PATTERN.test(gap));\n if (\n !hasLockedBoundary(prev) &&\n !(\n // A legal-form match has a precise regex-bounded\n // extent; never extend it across a comma into a\n // sibling org span, even if that sibling came\n // from a probabilistic source. This stops list\n // items like \"Morgan Stanley & Co. LLC, Bank of\n // America Merrill Lynch\" from collapsing into a\n // single span just because the deny-list detected\n // \"Bank of America\" next door.\n (\n (isLegalFormOrganization(prev) || isLegalFormOrganization(entity)) &&\n gap.includes(\",\")\n )\n ) &&\n // Country entities are atomic; never merge two\n // countries across a separator. \"USA, Czechia, and\n // Mexico\" must stay three distinct spans, not one.\n entity.label !== \"country\" &&\n !gapOccupied &&\n isMergeableGap\n ) {\n // Merge into prev\n prev.end = entity.end;\n prev.text = fullText.slice(prev.start, prev.end);\n prev.score = Math.max(prev.score, entity.score);\n } else {\n const copy = { ...entity };\n result.push(copy);\n lastByLabel.set(entity.label, copy);\n }\n }\n\n return result;\n};\n\n/**\n * Fix partial-word boundaries by extending entity\n * start/end to the nearest word boundary. Does not\n * extend across newlines or into spans occupied by\n * different-label entities.\n *\n * Uses binary search to skip irrelevant entries when\n * clamping at cross-label neighbors. O(n log n) in\n * the common case; O(n^2) worst case when many\n * same-label entities precede a cross-label boundary.\n */\nconst fixPartialWords = (entities: Entity[], fullText: string): Entity[] => {\n const boundaries = buildWordBoundaries(fullText);\n const sorted = entities.toSorted((a, b) => a.start - b.start);\n\n // Build a secondary array sorted by end position\n // for efficient \"nearest entity ending before me\"\n // lookups. Each entry tracks the original entity.\n const byEnd = sorted\n .map((e, idx) => ({ entity: e, idx }))\n .sort((a, b) => a.entity.end - b.entity.end);\n const endPositions = byEnd.map((x) => x.entity.end);\n\n return sorted.map((e, eIdx) => {\n if (hasLockedBoundary(e)) {\n return e;\n }\n // The entity's stored `text` is its source-of-truth display;\n // an upstream collapse pass (e.g. monetary `273,- Kč` →\n // `273,- Kč`) intentionally divorces `text` from\n // `fullText.slice(start, end)`. Letting `wordStartAt`/`wordEndAt`\n // recompute the span here would overwrite the entity text with\n // the raw slice and lose the collapsed-out characters. Treat\n // such entities as having locked boundaries — their detector\n // already chose the right span.\n if (e.text !== fullText.slice(e.start, e.end)) {\n return e;\n }\n\n let newStart = wordStartAt(e.start, boundaries, fullText);\n let newEnd = wordEndAt(e.end, boundaries, fullText);\n\n // Clamp start: find different-label entities whose\n // end is in (newStart, e.start]. We search byEnd\n // for entities with end > newStart and end <= e.start.\n // Binary search for the first entry with\n // end > newStart.\n let lo = 0;\n let hi = endPositions.length;\n while (lo < hi) {\n const mid = (lo + hi) >>> 1;\n if ((endPositions[mid] ?? Number.POSITIVE_INFINITY) <= newStart) {\n lo = mid + 1;\n } else {\n hi = mid;\n }\n }\n // Scan forward from lo; all entries have end >\n // newStart. Stop when end > e.start.\n for (let k = lo; k < byEnd.length; k++) {\n const entry = byEnd[k];\n if (!entry || entry.entity.end > e.start) break;\n if (entry.idx === eIdx) continue;\n if (entry.entity.label === e.label) continue;\n // This entity's end is in (newStart, e.start]\n // and has a different label: clamp.\n newStart = Math.max(newStart, entry.entity.end);\n }\n\n // Clamp end: find different-label entities whose\n // start is in [e.end, newEnd). Use the start-sorted\n // array with binary search.\n const startIdx = lowerBound(sorted, e.end);\n for (let k = startIdx; k < sorted.length; k++) {\n const other = sorted[k];\n if (!other || other.start >= newEnd) break;\n if (other === e) continue;\n if (other.label === e.label) continue;\n // This entity's start is in [e.end, newEnd)\n // and has a different label: clamp.\n newEnd = Math.min(newEnd, other.start);\n }\n\n if (newStart === e.start && newEnd === e.end) {\n return e;\n }\n return {\n ...e,\n start: newStart,\n end: newEnd,\n text: fullText.slice(newStart, newEnd),\n };\n });\n};\n\n/**\n * Deduplicate entities with identical [start, end, label].\n * Keeps the entry with the highest score.\n */\nconst deduplicateSpans = (entities: Entity[]): Entity[] => {\n const seen = new Map<string, Entity>();\n for (const entity of entities) {\n const key = `${entity.start}:${entity.end}:${entity.label}`;\n const existing = seen.get(key);\n if (!existing || entity.score > existing.score) {\n seen.set(key, entity);\n }\n }\n return [...seen.values()];\n};\n\n/**\n * Remove nested same-label entities. If a shorter\n * entity is fully contained within a longer entity\n * of the same label, drop the shorter one.\n *\n * Uses a \"max end seen\" sweep per label. O(n) after\n * the initial sort.\n */\nconst removeNestedSameLabel = (entities: Entity[]): Entity[] => {\n // Sort by start asc, then by length desc so the\n // longer entity comes first.\n const sorted = entities.toSorted((a, b) => {\n if (a.start !== b.start) return a.start - b.start;\n return b.end - a.end;\n });\n\n const result: Entity[] = [];\n // Track the furthest end seen per label. Any entity\n // whose end <= maxEnd for its label is nested inside\n // a previously seen entity of the same label.\n const maxEndByLabel = new Map<string, number>();\n\n for (const entity of sorted) {\n const maxEnd = maxEndByLabel.get(entity.label);\n if (maxEnd !== undefined && entity.end <= maxEnd) {\n // Nested inside a same-label entity: skip.\n continue;\n }\n maxEndByLabel.set(entity.label, entity.end);\n result.push(entity);\n }\n\n return result;\n};\n\n/**\n * Resolve cross-label overlaps that can arise when\n * `fixPartialWords` independently expands two\n * different-label entities toward the same word\n * boundary. The entity with the higher score (or\n * longer span on tie) keeps its boundary; the other\n * is trimmed so the overlap disappears.\n *\n * Preserved existing structure: sorted + early break\n * already gives good amortized behavior. O(n^2)\n * worst case but rare in practice.\n */\nconst resolveCrossLabelOverlaps = (\n entities: Entity[],\n fullText: string,\n): Entity[] => {\n const sorted = entities\n .map((e) => ({ ...e }))\n .sort((a, b) => a.start - b.start);\n\n for (let i = 0; i < sorted.length; i++) {\n for (let j = i + 1; j < sorted.length; j++) {\n const a = sorted[i];\n const b = sorted[j];\n if (!a || !b) continue;\n if (b.start >= a.end) break; // no overlap\n if (a.label === b.label) continue;\n\n // Skip full containment (one entity fully\n // inside another). Cross-label nesting is\n // valid and should be preserved.\n const aContainsB = a.start <= b.start && a.end >= b.end;\n const bContainsA = b.start <= a.start && b.end >= a.end;\n if (aContainsB || bContainsA) continue;\n\n // Partial overlap. Higher score wins; on tie\n // the longer span wins.\n const aLen = a.end - a.start;\n const bLen = b.end - b.start;\n const aLocked = hasLockedBoundary(a);\n const bLocked = hasLockedBoundary(b);\n const aWins =\n aLocked !== bLocked\n ? aLocked\n : a.score > b.score || (a.score === b.score && aLen >= bLen);\n\n if (aWins) {\n // Trim b's start to a's end\n b.start = a.end;\n b.text = fullText.slice(b.start, b.end);\n } else {\n // Trim a's end to b's start. Because the\n // array is sorted by start and a.end can\n // only decrease, all remaining j will have\n // b.start >= a.end so the break fires\n // immediately.\n a.end = b.start;\n a.text = fullText.slice(a.start, a.end);\n }\n }\n }\n\n // Drop any entity that was trimmed to zero width.\n return sorted.filter((e) => e.start < e.end);\n};\n\n/**\n * Post-processing pass for entity boundary consistency.\n * Runs after mergeAndDedup, before false-positive\n * filtering.\n *\n * 1. Fix partial-word boundaries (respects cross-label\n * neighbors to avoid introducing new overlaps)\n * 2. Resolve any remaining cross-label overlaps\n * 3. Deduplicate identical [start, end, label] spans\n * 4. Merge adjacent same-label entities (catches any\n * new adjacency/overlap from step 1)\n * 5. Remove nested same-label entities\n */\nexport const enforceBoundaryConsistency = (\n entities: Entity[],\n fullText: string,\n): Entity[] => {\n const fixed = fixPartialWords(entities, fullText);\n const resolved = resolveCrossLabelOverlaps(fixed, fullText);\n const deduped = deduplicateSpans(resolved);\n const merged = mergeAdjacent(deduped, fullText);\n return removeNestedSameLabel(merged);\n};\n","/**\n * Build the unified search instances from all\n * detector pattern sources.\n *\n * Two TextSearch instances (not one) to avoid\n * 200K per-pattern object allocations:\n * 1. regex + triggers + legal-forms (mixed, ~140\n * patterns, caseInsensitive for trigger AC)\n * 2. deny-list + street-types + gazetteer\n * (caseInsensitive, overlap \"all\";\n * deny-list/street-type use per-pattern\n * wholeWords: true; gazetteer exact use\n * wholeWords: false; gazetteer fuzzy use\n * distance: 2 via @stll/fuzzy-search)\n *\n * All patterns are PatternEntry objects with\n * per-pattern literal/wholeWords settings.\n */\n\nimport type { PatternEntry, TextSearch } from \"@stll/text-search\";\n\nimport { getTextSearch } from \"./search-engine\";\n\nimport {\n isLegalFormsEnabled,\n type GazetteerEntry,\n type PipelineConfig,\n} from \"./types\";\nimport type { RegexMeta } from \"./detectors/regex\";\nimport type { TriggerRule } from \"./types\";\nimport type { DenyListData } from \"./detectors/deny-list\";\nimport type { PipelineContext } from \"./context\";\nimport { defaultContext } from \"./context\";\n\nimport {\n REGEX_PATTERNS,\n REGEX_META,\n getCurrencyPatternEntries,\n CURRENCY_PATTERN_META,\n getDatePatterns,\n DATE_PATTERN_META,\n getSigningClausePatterns,\n SIGNING_CLAUSE_META,\n} from \"./detectors/regex\";\nimport { buildTriggerPatterns } from \"./detectors/triggers\";\nimport { buildDenyList } from \"./detectors/deny-list\";\nimport { buildStreetTypePatterns } from \"./detectors/address-seeds\";\nimport { buildGazetteerPatterns } from \"./detectors/gazetteer\";\nimport { buildCountryPatterns, type CountryData } from \"./detectors/countries\";\nimport { expandLabelsForHotwordRules } from \"./filters/hotword-rules\";\n\nconst DEFAULT_CUSTOM_REGEX_SCORE = 0.9;\nconst ALNUM_RE = /[\\p{L}\\p{N}]/u;\n\ntype PatternSlice = {\n start: number;\n end: number;\n};\n\nconst createAllowedLabelSet = (\n labels: readonly string[],\n): ReadonlySet<string> | null => (labels.length > 0 ? new Set(labels) : null);\n\nconst labelIsAllowed = (\n label: string,\n allowedLabels: ReadonlySet<string> | null,\n): boolean => allowedLabels === null || allowedLabels.has(label);\n\nexport type GazetteerData = {\n /** Maps local pattern index to entry label. */\n labels: string[];\n /**\n * Whether each pattern is fuzzy (distance > 0).\n * Used by the post-processor to assign scores.\n */\n isFuzzy: boolean[];\n};\n\nexport type UnifiedSearchInstance = {\n /** Regex + triggers + legal-forms. */\n tsRegex: TextSearch;\n /** Caller-owned custom regexes, isolated for overlap preservation. */\n tsCustomRegex: TextSearch;\n /** Deny-list + street-types + gazetteer. */\n tsLiterals: TextSearch;\n slices: {\n regex: PatternSlice;\n customRegex: PatternSlice;\n legalForms: PatternSlice;\n triggers: PatternSlice;\n denyList: PatternSlice;\n streetTypes: PatternSlice;\n gazetteer: PatternSlice;\n countries: PatternSlice;\n };\n regexMeta: readonly RegexMeta[];\n customRegexMeta: readonly RegexMeta[];\n triggerRules: readonly TriggerRule[];\n denyListData: DenyListData | null;\n gazetteerData: GazetteerData | null;\n countryData: CountryData | null;\n};\n\nexport const buildUnifiedSearch = async (\n config: PipelineConfig,\n gazetteerEntries: GazetteerEntry[] = [],\n ctx: PipelineContext = defaultContext,\n): Promise<UnifiedSearchInstance> => {\n const legalFormsEnabled = isLegalFormsEnabled(config);\n const searchLabels =\n config.enableHotwordRules === true\n ? expandLabelsForHotwordRules(config.labels)\n : config.labels;\n const allowedLabels = createAllowedLabelSet(searchLabels);\n const customRegexes = config.enableRegex\n ? (config.customRegexes ?? []).filter((entry) =>\n labelIsAllowed(entry.label, allowedLabels),\n )\n : [];\n // Legal-form detection lives in `detectors/legal-forms-v2.ts`\n // as an AC suffix pass + TS-side validator; the unified search\n // no longer carries legal-form regex patterns. `legalFormsEnabled`\n // still gates whether the v2 detector runs in the pipeline, but\n // its pattern slice is always empty.\n const [\n triggers,\n denyListData,\n streetTypes,\n currencyPatterns,\n datePatterns,\n signingPatterns,\n ] = await Promise.all([\n config.enableTriggerPhrases\n ? buildTriggerPatterns()\n : Promise.resolve({\n patterns: [] as string[],\n rules: [] as TriggerRule[],\n }),\n config.enableDenyList ? buildDenyList(config, ctx) : Promise.resolve(null),\n buildStreetTypePatterns(),\n config.enableRegex && labelIsAllowed(\"monetary amount\", allowedLabels)\n ? getCurrencyPatternEntries()\n : Promise.resolve([] as PatternEntry[]),\n config.enableRegex && labelIsAllowed(\"date\", allowedLabels)\n ? getDatePatterns()\n : Promise.resolve([] as string[]),\n config.enableRegex && labelIsAllowed(\"address\", allowedLabels)\n ? getSigningClausePatterns()\n : Promise.resolve([] as string[]),\n ]);\n // Read but never populated: the legal-form slice in the unified\n // search is permanently empty after the v2 rewrite. Tracking it\n // here as a 0-length slice keeps the downstream slice math\n // (start/end offsets for the regex meta) compatible with code\n // that hasn't migrated to v2-aware indexing yet.\n const legalForms: readonly string[] = [];\n void legalFormsEnabled;\n\n // ── Instance 1: regex + triggers + legal-forms ──\n // Trigger patterns are lowercased strings with\n // caseInsensitive on the AC. Regex patterns have\n // their own (?i) flags (caseInsensitive on AC\n // is ignored for regex since they route to\n // RegexSet). Legal-form patterns are regex too.\n //\n // Currency patterns (from currencies.json) are\n // appended after the static regex patterns; their\n // meta is spliced into regexMeta at the same offset.\n const allRegex: PatternEntry[] = [];\n const regexMeta: RegexMeta[] = [];\n if (config.enableRegex) {\n for (const [index, pattern] of REGEX_PATTERNS.entries()) {\n const meta = REGEX_META[index];\n if (!meta || !labelIsAllowed(meta.label, allowedLabels)) {\n continue;\n }\n allRegex.push(pattern);\n regexMeta.push(meta);\n }\n }\n for (const pattern of currencyPatterns) {\n allRegex.push(pattern);\n regexMeta.push(CURRENCY_PATTERN_META);\n }\n for (const pattern of datePatterns) {\n allRegex.push(pattern);\n regexMeta.push(DATE_PATTERN_META);\n }\n for (const pattern of signingPatterns) {\n allRegex.push(pattern);\n regexMeta.push(SIGNING_CLAUSE_META);\n }\n const customRegexMeta: RegexMeta[] = customRegexes.map((entry) => ({\n label: entry.label,\n score: entry.score ?? DEFAULT_CUSTOM_REGEX_SCORE,\n sourceDetail: \"custom-regex\" as const,\n }));\n\n let offset = 0;\n\n const regexSlice = {\n start: offset,\n end: offset + allRegex.length,\n };\n offset = regexSlice.end;\n\n const customRegexSlice = {\n start: 0,\n end: customRegexes.length,\n };\n\n const legalFormsSlice = {\n start: offset,\n end: offset + legalForms.length,\n };\n offset = legalFormsSlice.end;\n\n const triggersSlice = {\n start: offset,\n end: offset + triggers.patterns.length,\n };\n\n // Trigger patterns need caseInsensitive on AC\n // (only ~120 objects, not 200K). Regex/legal-form\n // patterns are bare strings (auto-classified).\n const triggerEntries = triggers.patterns.map((p) => ({\n pattern: p,\n literal: true as const,\n caseInsensitive: true,\n }));\n\n const regexAllPatterns = [...allRegex, ...legalForms, ...triggerEntries];\n\n // TextSearch uses static complexity routing for\n // regex patterns: common regexes share bounded\n // chunks, while high-risk patterns are isolated.\n const tsRegex = new (getTextSearch())(regexAllPatterns);\n const tsCustomRegex = new (getTextSearch())(\n customRegexes.map((entry) => entry.pattern),\n {\n overlapStrategy: \"all\",\n },\n );\n\n // ── Instance 2: deny-list + street-types + gaz ──\n // Deny-list and street-type patterns are plain\n // strings (allLiteral). Gazetteer adds exact\n // literals plus fuzzy PatternEntry objects for\n // terms >= 4 chars.\n offset = 0;\n\n const denyListOriginals = denyListData?.originals ?? [];\n const denyListSlice = {\n start: offset,\n end: offset + denyListOriginals.length,\n };\n offset = denyListSlice.end;\n\n const streetTypesSlice = {\n start: offset,\n end: offset + streetTypes.length,\n };\n offset = streetTypesSlice.end;\n\n // Gazetteer patterns (exact + fuzzy)\n const gazResult =\n config.enableGazetteer && gazetteerEntries.length > 0\n ? buildGazetteerPatterns(gazetteerEntries)\n : null;\n\n const gazetteerSlice = {\n start: offset,\n end: offset + (gazResult?.patterns.length ?? 0),\n };\n offset = gazetteerSlice.end;\n\n // Country patterns: ISO 3166-1 names, curated aliases,\n // alpha-3 codes. Literal + case-insensitive + whole-word.\n const countryResult =\n config.enableCountries === false ||\n !labelIsAllowed(\"country\", allowedLabels)\n ? null\n : buildCountryPatterns();\n\n const countriesSlice = {\n start: offset,\n end: offset + (countryResult?.patterns.length ?? 0),\n };\n\n // Build the combined pattern array.\n // Deny-list and street-type patterns use\n // per-pattern wholeWords: true (they are\n // known tokens). Gazetteer exact patterns\n // already set wholeWords: false in\n // buildGazetteerPatterns. The global\n // wholeWords is false so fuzzy patterns\n // (which don't support per-pattern override)\n // match without word-boundary constraints.\n const wrapWholeWord = (s: string, wholeWords: boolean): PatternEntry => ({\n pattern: s,\n literal: true as const,\n wholeWords,\n });\n const customDenyListNeedsWholeWords = (pattern: string): boolean => {\n const first = pattern.at(0) ?? \"\";\n const last = pattern.at(-1) ?? \"\";\n return ALNUM_RE.test(first) && ALNUM_RE.test(last);\n };\n const literalPatternText = (entry: PatternEntry): string => {\n if (typeof entry === \"string\") return entry;\n if (entry instanceof RegExp) {\n throw new Error(\"Expected literal country pattern, got RegExp\");\n }\n if (entry.pattern instanceof RegExp) {\n throw new Error(\"Expected literal country pattern, got RegExp entry\");\n }\n return entry.pattern;\n };\n const hasCustomDenyListPatterns =\n denyListData?.sources.some((sources) =>\n sources.includes(\"custom-deny-list\"),\n ) ?? false;\n const canUseGlobalWholeWordLiterals =\n !hasCustomDenyListPatterns && gazResult === null;\n const literalAllPatterns: PatternEntry[] | string[] =\n canUseGlobalWholeWordLiterals\n ? [\n ...denyListOriginals,\n ...streetTypes,\n ...(countryResult?.patterns.map(literalPatternText) ?? []),\n ]\n : [\n ...denyListOriginals.map((pattern, index) =>\n wrapWholeWord(\n pattern,\n (denyListData?.sources[index] ?? []).includes(\"custom-deny-list\")\n ? customDenyListNeedsWholeWords(pattern)\n : true,\n ),\n ),\n ...streetTypes.map((pattern) => wrapWholeWord(pattern, true)),\n ...(gazResult?.patterns ?? []),\n ...(countryResult?.patterns ?? []),\n ];\n\n const tsLiterals =\n literalAllPatterns.length > 0\n ? new (getTextSearch())(literalAllPatterns, {\n ...(canUseGlobalWholeWordLiterals\n ? { allLiteral: true, wholeWords: true }\n : {}),\n caseInsensitive: true,\n overlapStrategy: \"all\",\n })\n : new (getTextSearch())([]);\n\n return {\n tsRegex,\n tsCustomRegex,\n tsLiterals,\n slices: {\n regex: regexSlice,\n customRegex: customRegexSlice,\n legalForms: legalFormsSlice,\n triggers: triggersSlice,\n denyList: denyListSlice,\n streetTypes: streetTypesSlice,\n gazetteer: gazetteerSlice,\n countries: countriesSlice,\n },\n regexMeta,\n customRegexMeta,\n triggerRules: triggers.rules,\n denyListData,\n gazetteerData: gazResult?.data ?? null,\n countryData: countryResult?.data ?? null,\n };\n};\n","/**\n * Run the two-instance unified search and return\n * raw matches. Two passes instead of six:\n * 1. regex + triggers + legal-forms\n * 2. deny-list + street-types (normalized text)\n */\n\nimport type { Match } from \"@stll/text-search\";\nimport type { UnifiedSearchInstance } from \"./build-unified-search\";\nimport { normalizeForSearch } from \"./util/normalize\";\n\nexport type UnifiedResult = {\n /** All matches from both instances combined. */\n regexMatches: Match[];\n customRegexMatches: Match[];\n literalMatches: Match[];\n};\n\nexport const runUnifiedSearch = (\n instance: UnifiedSearchInstance,\n fullText: string,\n): UnifiedResult => {\n // Pass 1: regex + triggers + legal-forms\n // on original text (regex patterns encode\n // their own case flags)\n const regexMatches = instance.tsRegex.findIter(fullText);\n const customRegexMatches = instance.tsCustomRegex.findIter(fullText);\n\n // Pass 2: deny-list + street-types on\n // normalized text (NBSP, smart quotes folded)\n const normalized = normalizeForSearch(fullText);\n const literalMatches = instance.tsLiterals.findIter(normalized);\n\n return { regexMatches, customRegexMatches, literalMatches };\n};\n","import type { Entity } from \"../types\";\n\nconst MASK_TOKEN = \"[MASKED]\";\nconst MASK_LEN = MASK_TOKEN.length;\n\ntype OffsetSegment = {\n /** Start of this mask token in masked text */\n maskedStart: number;\n /** End of this mask token in masked text */\n maskedEnd: number;\n /** Cumulative shift: original - masked */\n shift: number;\n /** Original start of the masked span */\n origStart: number;\n /** Original end of the masked span */\n origEnd: number;\n};\n\nexport type MaskResult = {\n maskedText: string;\n /**\n * Maps masked-text offsets back to original offsets.\n * Returns null if the span overlaps a masked region.\n */\n offsetMap: (\n maskedStart: number,\n maskedEnd: number,\n ) => { start: number; end: number } | null;\n};\n\n/**\n * Replace detected entity spans with placeholder tokens.\n * Each entity span is replaced with \"[MASKED]\" (fixed\n * length). Returns the masked text and an offset mapping\n * function.\n */\nexport const maskDetectedSpans = (\n fullText: string,\n entities: Entity[],\n): MaskResult => {\n if (entities.length === 0) {\n return {\n maskedText: fullText,\n offsetMap: (s, e) => ({ start: s, end: e }),\n };\n }\n\n // Sort by start, then longest span first\n const sorted = entities.toSorted(\n (a, b) => a.start - b.start || b.end - a.end,\n );\n\n // Merge overlapping spans so we don't double-mask.\n // Adjacent (touching) spans are kept separate so each\n // produces its own [MASKED] token, preserving token\n // boundaries for the NER model.\n const spans: { start: number; end: number }[] = [];\n const first = sorted[0];\n if (!first) {\n return {\n maskedText: fullText,\n offsetMap: (s, e) => ({ start: s, end: e }),\n };\n }\n let cur = {\n start: first.start,\n end: first.end,\n };\n for (let i = 1; i < sorted.length; i++) {\n const s = sorted[i];\n if (!s) continue;\n if (s.start < cur.end) {\n cur.end = Math.max(cur.end, s.end);\n } else {\n spans.push(cur);\n cur = { start: s.start, end: s.end };\n }\n }\n spans.push(cur);\n\n // Build masked text and offset segments\n const segments: OffsetSegment[] = [];\n const parts: string[] = [];\n let prev = 0;\n let cumulativeShift = 0;\n\n for (const span of spans) {\n parts.push(fullText.slice(prev, span.start));\n parts.push(MASK_TOKEN);\n\n const origLen = span.end - span.start;\n const delta = origLen - MASK_LEN;\n cumulativeShift += delta;\n\n // Position of mask token in masked text\n const maskedStart = span.start - (cumulativeShift - delta);\n const maskedEnd = maskedStart + MASK_LEN;\n\n segments.push({\n maskedStart,\n maskedEnd,\n shift: cumulativeShift,\n origStart: span.start,\n origEnd: span.end,\n });\n\n prev = span.end;\n }\n parts.push(fullText.slice(prev));\n\n const maskedText = parts.join(\"\");\n\n const offsetMap = (\n maskedStart: number,\n maskedEnd: number,\n ): { start: number; end: number } | null => {\n // Check if span overlaps any masked region\n for (const seg of segments) {\n if (maskedStart < seg.maskedEnd && maskedEnd > seg.maskedStart) {\n return null;\n }\n }\n\n // Find cumulative shift for this position.\n // Since the overlap check above guarantees both\n // endpoints fall within the same gap zone, a\n // single shift value applies to both.\n let shift = 0;\n for (const seg of segments) {\n if (maskedStart >= seg.maskedEnd) {\n shift = seg.shift;\n } else {\n break;\n }\n }\n\n return {\n start: maskedStart + shift,\n end: maskedEnd + shift,\n };\n };\n\n return { maskedText, offsetMap };\n};\n\n/**\n * Map NER entities from masked-text offsets back to\n * original-text offsets. Discards any NER entity whose\n * mapped span overlaps a masked (rule-detected) region.\n */\nexport const unmaskNerEntities = (\n nerEntities: Entity[],\n maskResult: MaskResult,\n fullText: string,\n): Entity[] => {\n const result: Entity[] = [];\n\n for (const ner of nerEntities) {\n // offsetMap returns null when the mapped span\n // overlaps any merged rule-entity region, so no\n // secondary overlap check is needed.\n const mapped = maskResult.offsetMap(ner.start, ner.end);\n if (mapped === null) continue;\n\n result.push({\n ...ner,\n start: mapped.start,\n end: mapped.end,\n text: fullText.slice(mapped.start, mapped.end),\n });\n }\n\n return result;\n};\n","import {\n extractDefinedTerms,\n findCoreferenceSpans,\n} from \"./detectors/coreference\";\nimport { processGazetteerMatches } from \"./detectors/gazetteer\";\nimport { processCountryMatches } from \"./detectors/countries\";\nimport { detectNameCorpus, initNameCorpus } from \"./detectors/names\";\nimport { detectSignatures } from \"./detectors/signatures\";\nimport { processRegexMatches } from \"./detectors/regex\";\nimport {\n getKnownLegalSuffixes,\n warmLegalRoleHeads,\n} from \"./detectors/legal-forms\";\nimport { detectLegalFormsV2 } from \"./detectors/legal-forms-v2\";\nimport {\n processTriggerMatches,\n warmAddressStopKeywords,\n} from \"./detectors/triggers\";\nimport {\n ensureDenyListData,\n processDenyListMatches,\n} from \"./detectors/deny-list\";\nimport { processAddressSeeds } from \"./detectors/address-seeds\";\nimport { propagateOrgNames } from \"./detectors/org-propagation\";\nimport {\n boostNearMissEntities,\n detectStreetPatternsNearAddresses,\n getStreetAbbrevs,\n detectOrphanStreetLines,\n initPrepositions,\n initStreetAbbrevs,\n} from \"./filters/confidence-boost\";\nimport {\n filterFalsePositives,\n initAddressComponents,\n loadDocumentStructureHeadings,\n loadGenericRoles,\n} from \"./filters/false-positives\";\nimport {\n applyZoneAdjustments,\n classifyZones,\n initZoneClassifier,\n type ZoneSpan,\n} from \"./filters/zone-classifier\";\nimport {\n applyHotwordRules,\n expandLabelsForHotwordRules,\n initHotwordRules,\n} from \"./filters/hotword-rules\";\nimport { enforceBoundaryConsistency } from \"./filters/boundary-consistency\";\nimport type {\n Dictionaries,\n Entity,\n GazetteerEntry,\n PipelineConfig,\n} from \"./types\";\nimport {\n DEFAULT_ENTITY_LABELS,\n DETECTION_SOURCES,\n DETECTOR_PRIORITY,\n isLegalFormsEnabled,\n} from \"./types\";\nimport {\n buildUnifiedSearch,\n type UnifiedSearchInstance,\n} from \"./build-unified-search\";\nimport { runUnifiedSearch } from \"./unified-search\";\nimport { maskDetectedSpans, unmaskNerEntities } from \"./util/entity-masking\";\nimport type { PipelineContext } from \"./context\";\nimport { defaultContext } from \"./context\";\n\n/**\n * Sources backed by curated literal dictionaries.\n * Longer matches from these sources are more specific,\n * so the containment rule trusts their length.\n */\nconst LITERAL_SOURCES: ReadonlySet<string> = new Set([\n \"deny-list\",\n \"gazetteer\",\n]);\n\nconst isCallerOwnedEntity = (entity: Entity): boolean =>\n entity.sourceDetail === \"custom-deny-list\" ||\n entity.sourceDetail === \"custom-regex\";\n\nconst hasLockedBoundary = (entity: Entity): boolean =>\n isCallerOwnedEntity(entity);\n\nconst LITERAL_BOUNDARY_PUNCT_RE = /^[\"“„‟‘‛'«]|[\"”’'»!.]$/u;\n\n// Bare postal-code shapes used by the address-containment rule.\n// Covers Czech `160 00`, German/EU `12345`, US ZIP / ZIP+4\n// (`94304`, `94304-1050`), and the standard `\\d{3} \\d{2}` /\n// `\\d{2}-\\d{3}` continental variants. Surrounding whitespace\n// is allowed so the trigger detector's trimmed span still matches.\nconst BARE_POSTAL_CODE_RE =\n /^\\s*(?:\\d{3}\\s?\\d{2}|\\d{2}[-–]\\d{3}|\\d{5}(?:[-–]\\d{3,4})?)\\s*$/u;\n\nconst hasCuratedLiteralBoundary = (entity: Entity): boolean =>\n LITERAL_SOURCES.has(entity.source) &&\n entity.label !== \"person\" &&\n entity.sourceDetail !== \"gazetteer-extension\" &&\n LITERAL_BOUNDARY_PUNCT_RE.test(entity.text);\n\nconst shouldReplace = (a: Entity, b: Entity): boolean => {\n const aLen = a.end - a.start;\n const bLen = b.end - b.start;\n const aCallerOwned = isCallerOwnedEntity(a);\n const bCallerOwned = isCallerOwnedEntity(b);\n if (aCallerOwned !== bCallerOwned) {\n return aCallerOwned;\n }\n\n // Containment: when a literal-match entity (deny-list\n // or gazetteer) fully contains a shorter entity with\n // the same label, prefer the longer one. Curated\n // dictionary entries are more specific when longer:\n // \"656 91 Brno\" (deny-list) should beat \"656 91\"\n // (regex) even though regex has higher priority.\n // Non-literal sources (trigger, regex, NER) are\n // excluded because their length does not reliably\n // indicate accuracy.\n if (\n a.label === b.label &&\n LITERAL_SOURCES.has(a.source) &&\n a.start <= b.start &&\n a.end >= b.end &&\n aLen > bLen\n ) {\n return true;\n }\n if (\n a.label === b.label &&\n LITERAL_SOURCES.has(b.source) &&\n b.start <= a.start &&\n b.end >= a.end &&\n bLen > aLen\n ) {\n return false;\n }\n\n // Postal-code inside a fuller address: when a longer same-label\n // address span fully contains a shorter span whose text is just\n // a bare postal code (Czech `160 00`, German `12345`, US `94304`\n // or `94304-1050`), the shorter span is a fragment of the same\n // data point. The longer span wins regardless of source priority.\n // Bare postal-code text without surrounding street/city evidence\n // is the only narrow case where containment beats the priority\n // comparison; field-label trims (\\`… IČ\\`, \\`… oddíl\\`) and other\n // address-vs-address comparisons stay on the standard path.\n if (\n a.label === \"address\" &&\n b.label === \"address\" &&\n a.start <= b.start &&\n a.end >= b.end &&\n aLen > bLen &&\n BARE_POSTAL_CODE_RE.test(b.text)\n ) {\n return true;\n }\n if (\n a.label === \"address\" &&\n b.label === \"address\" &&\n b.start <= a.start &&\n b.end >= a.end &&\n bLen > aLen &&\n BARE_POSTAL_CODE_RE.test(a.text)\n ) {\n return false;\n }\n\n // Legal-form containment: a v2 legal-form entity span anchors on\n // a suffix and grows back through CapWords + connectors, so its\n // length DOES reliably indicate accuracy. When such a span fully\n // contains a shorter same-label entity from a higher-priority\n // detector (typically a trigger reclassifying a city name like\n // `Prahy` inside `Technologie hlavního města Prahy, a. s.`), the\n // legal-form span wins regardless of source priority.\n if (\n a.label === b.label &&\n a.source === DETECTION_SOURCES.LEGAL_FORM &&\n a.start <= b.start &&\n a.end >= b.end &&\n aLen > bLen\n ) {\n return true;\n }\n if (\n a.label === b.label &&\n b.source === DETECTION_SOURCES.LEGAL_FORM &&\n b.start <= a.start &&\n b.end >= a.end &&\n bLen > aLen\n ) {\n return false;\n }\n\n // Same-start same-label longest-wins rule for shape-extending\n // detectors. For labels where the entity naturally extends to\n // include trailing context (a date that grows from `21.` to\n // `21. März 1968`, a monetary amount that grows from `273,-`\n // to `273,- Kč`), the longer span at the same offset is the\n // correct boundary regardless of which detector emitted it.\n // Without this rule the priority comparison below picks the\n // shorter-but-higher-priority trigger and discards the full\n // entity. The list is intentionally narrow — `address`,\n // `organization`, `person` keep the priority semantics\n // because their detectors don't have the same \"trigger\n // captures a prefix, regex captures the whole shape\"\n // relationship.\n const LONGEST_WINS_LABELS: ReadonlySet<string> = new Set([\n \"date\",\n \"date of birth\",\n \"monetary amount\",\n \"phone number\",\n \"email address\",\n \"url\",\n ]);\n if (\n a.label === b.label &&\n a.start === b.start &&\n aLen !== bLen &&\n LONGEST_WINS_LABELS.has(a.label)\n ) {\n return aLen > bLen;\n }\n\n // Cross-label containment for country: a country token\n // contained inside a longer person or organization span\n // is almost always a first-name collision (\"Chad Smith\",\n // \"Georgia Smith\", \"Jordan Williams\"). The longer span\n // carries more evidence — keep it and drop the country.\n if (\n a.label === \"country\" &&\n (b.label === \"person\" || b.label === \"organization\") &&\n b.start <= a.start &&\n b.end >= a.end &&\n bLen > aLen\n ) {\n return false;\n }\n if (\n b.label === \"country\" &&\n (a.label === \"person\" || a.label === \"organization\") &&\n a.start <= b.start &&\n a.end >= b.end &&\n aLen > bLen\n ) {\n return true;\n }\n\n const aPri = DETECTOR_PRIORITY[a.source] ?? 0;\n const bPri = DETECTOR_PRIORITY[b.source] ?? 0;\n if (aPri !== bPri) return aPri > bPri;\n return a.score > b.score || (a.score === b.score && aLen > bLen);\n};\n\n/** Labels where colons are structurally significant. */\nconst COLON_LABELS = new Set([\"ip address\", \"mac address\"]);\n\n/**\n * Labels whose entities should have a trailing sentence\n * `.` stripped during sanitisation. Restricted to\n * proper-noun-style labels where a final period is\n * almost always the sentence terminator that ran into\n * the capture, not a structural part of the value.\n * Numeric labels (`date`, `date of birth`, `phone\n * number`, `monetary amount`, `time`) and `person`\n * stay out — German writes `21. März`, post-nominal\n * degrees write `M.Sc.`, times write `5:00 p.m.`, and\n * stripping the dot would corrupt those spans.\n */\nconst PERIOD_STRIPPED_LABELS: ReadonlySet<string> = new Set([\n \"organization\",\n \"location\",\n \"address\",\n]);\nconst ADDRESS_FINAL_TOKEN_RE = /(?:^|[\\s,])([\\p{L}\\p{M}.]+\\.)$/u;\nconst LOCATION_FINAL_DOTTED_ABBREV_RE = /(?:^|[\\s,])(?:\\p{Lu}\\.){2,}$/u;\n\nconst hasKnownAddressFinalAbbrev = (text: string): boolean => {\n const finalToken = ADDRESS_FINAL_TOKEN_RE.exec(text)?.[1];\n if (!finalToken) {\n return false;\n }\n return getStreetAbbrevs().has(finalToken.toLowerCase());\n};\n\nconst hasLocationFinalAbbrev = (text: string): boolean =>\n LOCATION_FINAL_DOTTED_ABBREV_RE.test(text);\n\n/**\n * Labels whose detectors emit precise, evidence-backed spans. When\n * one of these fires at the exact same offsets as a fuzzier\n * `address` hit (city dictionary lookup, address-seed cluster), the\n * `address` label is almost always a dictionary collision — \"March\n * 15\" the date getting tagged as an address because \"March\" appears\n * in a city corpus, or \"Brent Phillips\" emitted as both `person`\n * and `address` because \"Brent\" is a UK city. The precise detector\n * wins.\n */\nconst PRECISE_OVER_ADDRESS: ReadonlySet<string> = new Set([\n \"person\",\n \"date\",\n \"date of birth\",\n \"phone number\",\n \"email address\",\n \"monetary amount\",\n \"iban\",\n \"bank account number\",\n \"tax identification number\",\n \"registration number\",\n \"identity card number\",\n \"national identification number\",\n \"passport number\",\n \"credit card number\",\n]);\n\n/**\n * Labels the `person` chain wins against at identical offsets. The\n * chain carries adjacent-name evidence (deny-list surname plus a\n * capitalised follow-up) a single-token dictionary collision does\n * not. Kept narrow: organizations are NOT here — \"Morgan Stanley\"\n * legitimately appears in both the org and name dictionaries, and\n * the existing detector priority is the right tie-breaker there.\n *\n * `country` is included because the country detector runs at the\n * same offsets as deny-list person matches for names like `Chad`,\n * `Georgia`, `Jordan` (all valid first names AND country names).\n * Letting a higher-priority country span win there would mark\n * `Chad Smith` as country + leave `Smith` unredacted.\n */\nconst PERSON_PREFERRED_OVER: ReadonlySet<string> = new Set([\n \"address\",\n \"country\",\n \"land parcel\",\n]);\n\nconst resolveSameSpanLabelConflicts = (entities: Entity[]): Entity[] => {\n if (entities.length < 2) return entities;\n const byOffsets = new Map<string, Entity[]>();\n for (const entity of entities) {\n const key = `${entity.start}:${entity.end}`;\n const list = byOffsets.get(key);\n if (list) {\n list.push(entity);\n } else {\n byOffsets.set(key, [entity]);\n }\n }\n const dropped = new Set<Entity>();\n for (const [, group] of byOffsets) {\n if (group.length < 2) continue;\n const labels = new Set(group.map((e) => e.label));\n if (labels.size < 2) continue;\n\n const hasPerson = labels.has(\"person\");\n const hasPreciseNonAddress = [...labels].some(\n (l) => l !== \"address\" && PRECISE_OVER_ADDRESS.has(l),\n );\n\n // Person-preferred drops are decided up front so the\n // priority-based pass below doesn't keep a higher-pri country\n // span (e.g., `Chad` from the country detector at priority 3)\n // while also dropping the lower-pri person hit (`Chad` from\n // the deny-list at priority 2) for being below the same group's\n // max. Without this, person tokens that happen to be country\n // names would be flipped to `country` and the surrounding\n // surname left exposed.\n const yieldingToPerson = new Set<Entity>();\n if (hasPerson) {\n for (const e of group) {\n if (hasLockedBoundary(e)) continue;\n if (PERSON_PREFERRED_OVER.has(e.label)) {\n yieldingToPerson.add(e);\n }\n }\n }\n\n // When entities at the same offsets have different labels,\n // also let detector priority break ties: a `legal-form`\n // organization hit (priority 3) should keep its label over a\n // coincident `deny-list` person hit (priority 2). Compute the\n // max priority over entities NOT already yielding to person,\n // so the person hit isn't accidentally crowded out by the\n // priority of the very entity it's beating.\n let maxPriority = -1;\n for (const e of group) {\n if (hasLockedBoundary(e)) continue;\n if (yieldingToPerson.has(e)) continue;\n const pri = DETECTOR_PRIORITY[e.source] ?? 0;\n if (pri > maxPriority) maxPriority = pri;\n }\n\n for (const e of group) {\n // Caller-owned spans (custom deny-list / custom regex) carry\n // explicit user intent; never drop them in favour of a\n // detector-generated label.\n if (hasLockedBoundary(e)) continue;\n if (yieldingToPerson.has(e)) {\n dropped.add(e);\n continue;\n }\n const pri = DETECTOR_PRIORITY[e.source] ?? 0;\n if (pri < maxPriority) {\n dropped.add(e);\n continue;\n }\n if (hasPreciseNonAddress && e.label === \"address\") {\n dropped.add(e);\n }\n }\n }\n if (dropped.size === 0) return entities;\n return entities.filter((e) => !dropped.has(e));\n};\n\n/**\n * Trailing typographic punctuation that detectors\n * occasionally swallow when a capture runs to the end\n * of a sentence or quoted phrase. Stripped from every\n * non-literal, non-locked entity. Curated dictionary and\n * gazetteer entries with punctuation that is clearly part of\n * the literal (`Hello bank!`, `\"Juez y parte\"`) keep their\n * own boundaries. Generated/extended spans from the same\n * sources still pass through cleanup so dangling punctuation\n * does not become part of the redaction\n * (e.g. `Bond Hedge Documentation\"` →\n * `Bond Hedge Documentation`).\n *\n * `)` is deliberately omitted — monetary amounts are\n * extended to include trailing \"(slovy ...)\" / \"(in\n * words ...)\" parentheticals where the closing paren\n * is structural, and stripping it would leave the open\n * paren dangling. `.` is also omitted because the\n * trailing-period rule below has label-aware handling\n * (legal-form abbreviations keep their dot).\n */\nconst TRAILING_PUNCT_CLASS = `[\"“”‘’'»!?]`;\n\n/**\n * Leading typographic punctuation that detectors\n * occasionally swallow when a capture starts at an\n * opening quote. `(` is deliberately omitted — it is\n * almost always the opening of a structural\n * parenthetical (registration number group, monetary\n * \"(slovy ...)\" extension) that the detector\n * intentionally captured.\n */\nconst LEADING_PUNCT_CLASS = `[\"“”‘’'«¿¡]`;\nconst LEADING_ELLIPSIS_RE = /^(?:\\.{2,}|…+)/u;\nconst ELLIPSIS_PREFIX_LABELS: ReadonlySet<string> = new Set([\n \"date\",\n \"date of birth\",\n \"monetary amount\",\n \"phone number\",\n \"email address\",\n \"url\",\n \"time\",\n]);\nconst STRIP_BY_LABEL = {\n colon: /[\\s,;]+/,\n default: /[\\s:,;]+/,\n} as const;\nconst LEADING_TRIM_BY_LABEL = {\n colon: new RegExp(\n `^(?:\\\\.\\\\s|${STRIP_BY_LABEL.colon.source}|${LEADING_PUNCT_CLASS})+`,\n ),\n default: new RegExp(\n `^(?:\\\\.\\\\s|${STRIP_BY_LABEL.default.source}|${LEADING_PUNCT_CLASS})+`,\n ),\n} as const;\nconst TRAILING_TRIM_BY_LABEL = {\n colon: new RegExp(\n `(?:${STRIP_BY_LABEL.colon.source}|${TRAILING_PUNCT_CLASS})+$`,\n ),\n default: new RegExp(\n `(?:${STRIP_BY_LABEL.default.source}|${TRAILING_PUNCT_CLASS})+$`,\n ),\n} as const;\n\n/** Strip leading/trailing whitespace and punctuation. */\nexport const sanitizeEntities = (entities: Entity[]): Entity[] =>\n entities.flatMap((e) => {\n if (hasLockedBoundary(e) || hasCuratedLiteralBoundary(e)) {\n return [e];\n }\n\n const stripKind = COLON_LABELS.has(e.label) ? \"colon\" : \"default\";\n // Also strip leading dots followed by whitespace —\n // artifact from trigger extraction after abbreviations\n // like \"dat. nar.\" or \"č.p.\" where the extraction\n // starts at the trailing dot of the abbreviation.\n // The typographic-punctuation passes run in a loop\n // alongside the whitespace strip so combinations like\n // `\"Some Org\",` or ` \"Name\" ` collapse cleanly.\n const leadRe = LEADING_TRIM_BY_LABEL[stripKind];\n const trailRe = TRAILING_TRIM_BY_LABEL[stripKind];\n // Sentence-tail ellipsis: a date or other shape-extending\n // entity that sits at the end of a clause sometimes gets\n // captured with the preceding `...` / `…` run still attached\n // (`V Praze, dne ...2. 2. 2026` → `...2. 2. 2026`). The\n // generic punctuation strip below doesn't touch them because\n // the trailing dots aren't followed by whitespace. Strip the\n // run explicitly for the labels that have this shape — numeric\n // values where leading punctuation is never structurally part\n // of the entity.\n const ellipsisStripped = ELLIPSIS_PREFIX_LABELS.has(e.label)\n ? e.text.replace(LEADING_ELLIPSIS_RE, \"\")\n : e.text;\n const leadTrimmed = ellipsisStripped.replace(leadRe, \"\");\n const lead = e.text.length - leadTrimmed.length;\n let cleaned = leadTrimmed.replace(trailRe, \"\");\n // Trailing-period strip for proper-noun labels that\n // don't end in a legal-form abbreviation. Trigger\n // and NER captures often include the sentence\n // terminator\n // (\"Krajského soudu v Praze.\" → \"Krajského soudu v Praze\",\n // \"State of Delaware.\" → \"State of Delaware\").\n // Numeric labels (`date`, `phone number`, `monetary\n // amount`, `time`, etc.) and the `person` label keep\n // their trailing period — German dates write `21.`,\n // post-nominals write `M.Sc.`, times write `p.m.`,\n // all of which are structurally significant.\n // Literal deny-list and gazetteer spans whose\n // punctuation is part of the dictionary entry are\n // skipped above. For everything else, keep the period\n // when it follows the FULL detector vocabulary\n // (data/legal-forms.json plus `LEGAL_SUFFIXES`), not\n // only the small propagation list, so detected forms\n // like \"Acme Kft.\" or \"Bank of America, N.A.\" retain\n // their final dot.\n if (\n PERIOD_STRIPPED_LABELS.has(e.label) &&\n cleaned.endsWith(\".\") &&\n !LITERAL_SOURCES.has(e.source)\n ) {\n const known = getKnownLegalSuffixes();\n const keepsPeriod =\n known.some((suffix) => cleaned.endsWith(suffix)) ||\n (e.label === \"address\" && hasKnownAddressFinalAbbrev(cleaned)) ||\n (e.label === \"location\" && hasLocationFinalAbbrev(cleaned));\n if (!keepsPeriod) {\n cleaned = cleaned.slice(0, -1).trimEnd();\n }\n }\n // After the period strip, re-run the trailing-punctuation\n // pass in case the period sat between the entity and\n // already-stripped quotes/parens (e.g., `Foo.\"` →\n // `Foo.` → `Foo`).\n cleaned = cleaned.replace(trailRe, \"\");\n if (cleaned.length === 0) return [];\n // Reject entities with no alphanumeric content\n if (!/[\\p{L}\\p{N}]/u.test(cleaned)) return [];\n // Collapse internal whitespace runs (address entities\n // spanning multiple lines in structured documents)\n const collapsed = cleaned.replace(/\\s*\\n\\s*/g, \" \").replace(/\\s{2,}/g, \" \");\n if (collapsed === e.text) return [e];\n return [\n {\n ...e,\n start: e.start + lead,\n end: e.start + lead + collapsed.length,\n text: collapsed,\n },\n ];\n });\n\nexport const mergeAndDedup = (...layers: Entity[][]): Entity[] => {\n const all: Entity[] = [];\n for (const layer of layers) {\n for (const entity of layer) {\n all.push(entity);\n }\n }\n if (all.length === 0) return [];\n\n const sorted = all.toSorted((a, b) => a.start - b.start);\n\n // Single-pass sweep-line: since entities are sorted\n // by start, a new entity can only overlap the tail\n // of merged[] (all earlier entries end before it).\n const first = sorted[0];\n if (!first) return [];\n const merged: Entity[] = [{ ...first }];\n\n for (let i = 1; i < sorted.length; i++) {\n const entity = sorted[i];\n const last = merged.at(-1);\n if (!entity || !last) continue;\n\n if (last.end <= entity.start) {\n // No overlap: append.\n merged.push({ ...entity });\n continue;\n }\n\n const overlapStart = (() => {\n for (let j = merged.length - 1; j >= 0; j--) {\n const existing = merged[j];\n if (!existing || existing.end <= entity.start) {\n return j + 1;\n }\n }\n return 0;\n })();\n const overlaps = merged.slice(overlapStart);\n const hasPartialOverlap = overlaps.some(\n (existing) =>\n existing.start !== entity.start || existing.end !== entity.end,\n );\n\n if (!hasPartialOverlap) {\n const sameLabelIndex = overlaps.findIndex(\n (existing) => existing.label === entity.label,\n );\n if (sameLabelIndex === -1) {\n merged.push({ ...entity });\n continue;\n }\n\n const actualIndex = overlapStart + sameLabelIndex;\n const sameLabel = merged[actualIndex];\n if (sameLabel && shouldReplace(entity, sameLabel)) {\n merged[actualIndex] = { ...entity };\n }\n continue;\n }\n\n if (overlaps.every((existing) => shouldReplace(entity, existing))) {\n merged.splice(overlapStart, overlaps.length, { ...entity });\n }\n }\n\n return resolveSameSpanLabelConflicts(sanitizeEntities(merged));\n};\n\nexport type NerInferenceFn = (\n fullText: string,\n labels: string[],\n threshold: number,\n signal?: AbortSignal,\n) => Promise<Entity[]>;\n\n/**\n * Extend monetary amount entities to include a trailing\n * \"(slovy ...)\" or \"(slovně ...)\" parenthetical that\n * spells out the amount in words. Common in Czech legal\n * documents, e.g. \"1 529,-Kč (slovy jeden-tisíc)\".\n *\n * Keywords are loaded from amount-words.json config so\n * new languages can be added without code changes.\n */\ntype AmountWordsConfig = {\n patterns: { lang: string; keywords: string[] }[];\n};\n\nconst escapeRegex = (s: string): string =>\n s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\nlet amountWordsRe: RegExp | null = null;\nlet amountWordsLoaded = false;\n\nconst getAmountWordsRe = async (): Promise<RegExp> => {\n if (amountWordsLoaded && amountWordsRe) {\n return amountWordsRe;\n }\n try {\n const mod = await import(\"./data/amount-words.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON module shape\n const data = (mod as { default: AmountWordsConfig }).default;\n const keywords = data.patterns.flatMap((p) => p.keywords);\n const alt = keywords.map(escapeRegex).join(\"|\");\n amountWordsRe = new RegExp(\n `^[,;]?[^\\\\S\\\\n]*(\\\\((?:${alt})[:\\\\s][^)\\\\n]{1,120}\\\\))`,\n \"i\",\n );\n } catch {\n // Fallback: original Czech-only pattern\n amountWordsRe = /^[,;]?[^\\S\\n]*(\\((?:slovy|slovně)[:\\s][^)\\n]{1,120}\\))/i;\n }\n amountWordsLoaded = true;\n return amountWordsRe;\n};\n\nconst extendMonetaryAmountWords = (\n entities: Entity[],\n fullText: string,\n re: RegExp,\n): Entity[] =>\n entities.map((e) => {\n if (e.label !== \"monetary amount\" || isCallerOwnedEntity(e)) {\n return e;\n }\n const after = fullText.slice(e.end);\n const m = re.exec(after);\n if (!m) return e;\n const newEnd = e.end + m[0].length;\n return {\n ...e,\n end: newEnd,\n text: fullText.slice(e.start, newEnd),\n };\n });\n\nlet monetaryTrailingCurrencyRe: RegExp | null = null;\nlet monetaryTrailingCurrencyLoaded = false;\n\ntype CurrenciesData = {\n codes?: string[];\n symbols?: string[];\n localNames?: string[];\n};\n\nconst getMonetaryTrailingCurrencyRe = async (): Promise<RegExp | null> => {\n if (monetaryTrailingCurrencyLoaded) return monetaryTrailingCurrencyRe;\n try {\n const mod = await import(\"./data/currencies.json\");\n const data: CurrenciesData = mod.default ?? mod;\n const codes = (data.codes ?? []).filter((c) => /^[A-Z]{2,4}$/.test(c));\n const names = (data.localNames ?? []).filter((n) => n.length > 0);\n const parts: string[] = [];\n if (names.length > 0) {\n parts.push(names.map(escapeRegex).join(\"|\"));\n }\n if (codes.length > 0) {\n parts.push(codes.map(escapeRegex).join(\"|\"));\n }\n if (parts.length === 0) {\n monetaryTrailingCurrencyRe = null;\n } else {\n const alt = parts.join(\"|\");\n monetaryTrailingCurrencyRe = new RegExp(\n `^([^\\\\S\\\\n\\\\t]{0,4})(${alt})(?![\\\\p{L}\\\\p{N}])`,\n \"u\",\n );\n }\n } catch {\n monetaryTrailingCurrencyRe = null;\n }\n monetaryTrailingCurrencyLoaded = true;\n return monetaryTrailingCurrencyRe;\n};\n\n// Extend a monetary-amount entity to include a trailing currency\n// code/name when one sits within a short whitespace gap after the\n// captured span (`273,-` followed by ` Kč`, `1 000` followed by\n// ` CZK`). The unified regex backend occasionally drops the longer\n// currency pattern in favour of a shorter NUM-only match depending\n// on Rust regex DFA construction order; this post-process pass\n// re-attaches the suffix from \\`currencies.json\\` so the boundary\n// is the same regardless of which match resolved.\nconst extendMonetaryTrailingCurrency = (\n entities: Entity[],\n fullText: string,\n re: RegExp | null,\n): Entity[] => {\n if (!re) return entities;\n return entities.map((e) => {\n if (e.label !== \"monetary amount\" || isCallerOwnedEntity(e)) return e;\n if (/\\p{L}/u.test(e.text.slice(-1) ?? \"\")) return e;\n const after = fullText.slice(e.end);\n const m = re.exec(after);\n if (!m) return e;\n const newEnd = e.end + m[0].length;\n return {\n ...e,\n end: newEnd,\n text: fullText.slice(e.start, newEnd),\n };\n });\n};\n\ntype AllowedLabelSet = ReadonlySet<string> | null;\n\nconst createAllowedLabelSetFromLabels = (\n labels: readonly string[],\n): AllowedLabelSet => (labels.length > 0 ? new Set(labels) : null);\n\nconst createAllowedLabelSet = (config: PipelineConfig): AllowedLabelSet =>\n createAllowedLabelSetFromLabels(config.labels);\n\nconst DEFAULT_CUSTOM_REGEX_SCORE = 0.9;\n\nconst filterAllowedLabels = (\n entities: Entity[],\n allowedLabels: AllowedLabelSet,\n): Entity[] => {\n if (!allowedLabels) {\n return entities;\n }\n return entities.filter((e) => allowedLabels.has(e.label));\n};\n\nconst labelIsAllowed = (\n label: string,\n allowedLabels: AllowedLabelSet,\n): boolean => !allowedLabels || allowedLabels.has(label);\n\n// MISC is intentionally a label without detection — only the\n// custom deny-list path produces it. Asking the NER schema for\n// MISC would invite zero-shot guesses that contradict that\n// contract and cause over-redaction.\nconst NON_NER_LABELS: ReadonlySet<string> = new Set([\"misc\"]);\n\nconst getRequestedNerLabels = (\n config: PipelineConfig,\n expandForHotwords = false,\n): readonly string[] => {\n const labels =\n config.labels.length > 0 ? config.labels : DEFAULT_ENTITY_LABELS;\n const expanded = expandForHotwords\n ? expandLabelsForHotwordRules(labels)\n : labels;\n return expanded.filter((label) => !NON_NER_LABELS.has(label));\n};\n\nconst checkAbort = (signal?: AbortSignal): void => {\n if (signal?.aborted) {\n throw new DOMException(\"Pipeline aborted\", \"AbortError\");\n }\n};\n\nconst configKey = (\n config: PipelineConfig,\n gazetteerEntries: GazetteerEntry[],\n): string => {\n const legalFormsEnabled = isLegalFormsEnabled(config);\n const customDenyFingerprint =\n config.enableDenyList && config.customDenyList\n ? config.customDenyList\n .map((entry) =>\n JSON.stringify({\n label: entry.label,\n value: entry.value,\n variants: [...(entry.variants ?? [])].sort(),\n }),\n )\n .sort()\n .join(\"\\n\")\n : \"\";\n const customRegexFingerprint =\n config.enableRegex && config.customRegexes\n ? config.customRegexes\n .map((entry) =>\n JSON.stringify({\n label: entry.label,\n pattern: entry.pattern,\n score: entry.score ?? DEFAULT_CUSTOM_REGEX_SCORE,\n }),\n )\n .sort()\n .join(\"\\n\")\n : \"\";\n // Gazetteer fingerprint: sorted entry IDs,\n // canonical forms, labels, and variants.\n // Skip when gazetteer is disabled to avoid\n // unnecessary cache misses.\n const gazFingerprint =\n config.enableGazetteer && gazetteerEntries.length > 0\n ? gazetteerEntries\n .map(\n (e) =>\n `${e.id}:${e.canonical}:${e.label}:${[...e.variants].sort().join(\",\")}`,\n )\n .toSorted()\n .join(\";\")\n : \"\";\n return (\n `${config.enableDenyList}:` +\n `${config.enableTriggerPhrases}:` +\n `${legalFormsEnabled}:` +\n `${config.enableNameCorpus}:` +\n `${config.nameCorpusLanguages?.toSorted().join(\",\") ?? \"\"}:` +\n `${config.enableRegex}:` +\n `${config.labels.toSorted().join(\",\")}:` +\n `${config.denyListCountries?.toSorted().join(\",\") ?? \"\"}:` +\n `${config.denyListRegions?.toSorted().join(\",\") ?? \"\"}:` +\n `${config.denyListExcludeCategories?.toSorted().join(\",\") ?? \"\"}:` +\n `${customDenyFingerprint}:` +\n `${customRegexFingerprint}:` +\n `${config.enableGazetteer}:${gazFingerprint}:` +\n `${config.enableCountries !== false}`\n );\n};\n\ntype SharedSearchCacheValue =\n | Promise<UnifiedSearchInstance>\n | UnifiedSearchInstance;\n\n// Prepared search instances are immutable after construction\n// except for lazy regex slots, which only memoize their own\n// RegexSet. Scope the shared cache by dictionary object identity\n// so callers with different workspace dictionaries do not share\n// deny-list automata accidentally.\nconst sharedSearchByDictionaries = new WeakMap<\n Dictionaries,\n Map<string, SharedSearchCacheValue>\n>();\nconst sharedSearchWithoutDictionaries = new Map<\n string,\n SharedSearchCacheValue\n>();\n\nconst sharedSearchCacheFor = (\n dictionaries: Dictionaries | undefined,\n): Map<string, SharedSearchCacheValue> => {\n if (dictionaries === undefined) {\n return sharedSearchWithoutDictionaries;\n }\n const cached = sharedSearchByDictionaries.get(dictionaries);\n if (cached !== undefined) {\n return cached;\n }\n const created = new Map<string, SharedSearchCacheValue>();\n sharedSearchByDictionaries.set(dictionaries, created);\n return created;\n};\n\nconst ensureSearchSupportData = async (\n config: PipelineConfig,\n ctx: PipelineContext,\n): Promise<void> => {\n if (!config.enableDenyList) {\n return;\n }\n await ensureDenyListData(\n ctx,\n config.dictionaries,\n config.nameCorpusLanguages,\n );\n};\n\n/**\n * Get or build a cached search instance. Cache state\n * lives on the provided PipelineContext first. A\n * dictionary-scoped process cache prevents fresh\n * contexts with the same immutable dictionary bundle\n * from rebuilding identical automata.\n */\nconst getCachedSearch = async (\n config: PipelineConfig,\n gazetteerEntries: GazetteerEntry[],\n ctx: PipelineContext,\n): Promise<UnifiedSearchInstance> => {\n const key = configKey(config, gazetteerEntries);\n if (ctx.search && ctx.searchKey === key) {\n return ctx.search;\n }\n if (ctx.searchPromise && ctx.searchKey === key) {\n return ctx.searchPromise;\n }\n\n const sharedCache = sharedSearchCacheFor(config.dictionaries);\n const shared = sharedCache.get(key);\n if (shared !== undefined) {\n const result = await shared;\n await ensureSearchSupportData(config, ctx);\n ctx.search = result;\n ctx.searchKey = key;\n ctx.searchPromise = null;\n return result;\n }\n\n // Build new search. Null the cached instance first\n // so concurrent callers don't use stale data while\n // the new build is in flight.\n ctx.search = null;\n ctx.searchKey = key;\n const promise = buildUnifiedSearch(config, gazetteerEntries, ctx);\n ctx.searchPromise = promise;\n sharedCache.set(key, promise);\n let result: UnifiedSearchInstance;\n try {\n result = await promise;\n } catch (err) {\n if (sharedCache.get(key) === promise) {\n sharedCache.delete(key);\n }\n throw err;\n }\n if (sharedCache.get(key) === promise) {\n sharedCache.set(key, result);\n }\n // Guard: another call may have replaced the key\n // while we were awaiting. Only cache if still ours.\n if (ctx.searchKey === key) {\n ctx.search = result;\n }\n return result;\n};\n\nexport type PipelineSearchOptions = {\n config: PipelineConfig;\n gazetteerEntries?: GazetteerEntry[];\n context?: PipelineContext;\n};\n\n/**\n * Pre-build and cache the unified search instance for a\n * pipeline configuration. Use the same context in\n * `runPipeline` to reuse the prepared automata without\n * passing `cachedSearch` around manually.\n */\nexport const preparePipelineSearch = ({\n config,\n gazetteerEntries = [],\n context,\n}: PipelineSearchOptions): Promise<UnifiedSearchInstance> =>\n getCachedSearch(config, gazetteerEntries, context ?? defaultContext);\n\n/**\n * Options for {@link runPipeline}.\n *\n * @property cachedSearch Pre-built search instance.\n * When provided, `config` and `gazetteerEntries`\n * are not used for building; the caller must\n * ensure the instance matches both parameters.\n */\nexport type PipelineOptions = {\n fullText: string;\n config: PipelineConfig;\n gazetteerEntries: GazetteerEntry[];\n nerInference?: NerInferenceFn | null;\n onProgress?: (step: string, detail: string) => void;\n cachedSearch?: UnifiedSearchInstance;\n signal?: AbortSignal;\n context?: PipelineContext;\n};\n\n/**\n * Run the full detection pipeline.\n *\n * Two TextSearch instances scan the text (regex +\n * literals). Results are dispatched to each\n * detector's post-processor by pattern index range.\n *\n * Pass an AbortSignal to cancel the pipeline between\n * stages. Throws a DOMException with name \"AbortError\"\n * when cancelled.\n *\n * Pass an optional `context` to isolate cached state\n * from other pipeline runs. If omitted, a module-level\n * default context is used (backward compatible).\n */\nexport const runPipeline = async (\n options: PipelineOptions,\n): Promise<Entity[]> => {\n const {\n fullText,\n config,\n gazetteerEntries,\n nerInference = null,\n onProgress,\n cachedSearch,\n signal,\n context,\n } = options;\n const ctx = context ?? defaultContext;\n const allowedLabels = createAllowedLabelSet(config);\n const legalFormsEnabled = isLegalFormsEnabled(config);\n\n const log = (step: string, detail: string) => {\n onProgress?.(step, detail);\n };\n\n checkAbort(signal);\n\n // Ensure generic-roles data, zone config, hotword\n // rules, and prepositions are loaded before the\n // pipeline runs. All are no-ops after the first call.\n let zoneInitOk = false;\n const enableHotwords = config.enableHotwordRules === true;\n let hotwordInitOk = false;\n const hotwordInit = enableHotwords\n ? initHotwordRules()\n .then(() => {\n hotwordInitOk = true;\n })\n .catch((err: unknown) => {\n log(\"hotwords\", \"init failed; skipping\");\n console.warn(\"[anonymize] hotword rules init failed\", err);\n })\n : Promise.resolve();\n if (config.enableZoneClassification) {\n const zoneInit = initZoneClassifier(ctx)\n .then(() => {\n zoneInitOk = true;\n })\n .catch((err: unknown) => {\n log(\"zones\", \"init failed; skipping\");\n console.warn(\"[anonymize] zone classifier init failed\", err);\n });\n await Promise.all([\n loadGenericRoles(ctx),\n loadDocumentStructureHeadings(),\n initPrepositions(),\n initStreetAbbrevs(),\n initAddressComponents(),\n warmAddressStopKeywords(),\n zoneInit,\n hotwordInit,\n ]);\n } else {\n await Promise.all([\n loadGenericRoles(ctx),\n loadDocumentStructureHeadings(),\n initPrepositions(),\n initStreetAbbrevs(),\n initAddressComponents(),\n warmAddressStopKeywords(),\n hotwordInit,\n ]);\n }\n\n // When a pre-built search is provided, buildDenyList\n // was skipped for this context. Ensure stopwords,\n // allow list, and person stopwords are loaded so\n // processDenyListMatches filters correctly.\n if (cachedSearch && config.enableDenyList) {\n await ensureDenyListData(\n ctx,\n config.dictionaries,\n config.nameCorpusLanguages,\n );\n }\n\n // Classify document zones once up front\n let zones: ZoneSpan[] = [];\n if (config.enableZoneClassification && zoneInitOk) {\n zones = classifyZones(fullText, ctx);\n if (zones.length > 0) {\n const zoneNames = [...new Set(zones.map((z) => z.zone))];\n log(\"zones\", zoneNames.join(\", \"));\n }\n }\n\n checkAbort(signal);\n\n const hotwordsActive = enableHotwords && hotwordInitOk;\n const preHotwordAllowedLabels = hotwordsActive\n ? createAllowedLabelSetFromLabels(\n expandLabelsForHotwordRules(config.labels),\n )\n : allowedLabels;\n const searchConfig = hotwordsActive\n ? {\n ...config,\n labels: [...expandLabelsForHotwordRules(config.labels)],\n }\n : config;\n\n const search =\n cachedSearch ??\n (await getCachedSearch(searchConfig, gazetteerEntries, ctx));\n\n checkAbort(signal);\n\n // Two-pass scan (regex + literals)\n const { regexMatches, customRegexMatches, literalMatches } = runUnifiedSearch(\n search,\n fullText,\n );\n const { slices } = search;\n\n const rawRegexEntities = config.enableRegex\n ? processRegexMatches(\n regexMatches,\n slices.regex.start,\n slices.regex.end,\n search.regexMeta,\n )\n : [];\n const rawCustomRegexEntities = config.enableRegex\n ? processRegexMatches(\n customRegexMatches,\n slices.customRegex.start,\n slices.customRegex.end,\n search.customRegexMeta,\n )\n : [];\n const customRegexEntities = filterAllowedLabels(\n rawCustomRegexEntities,\n preHotwordAllowedLabels,\n );\n const regexEntities = filterAllowedLabels(\n [...rawRegexEntities, ...customRegexEntities],\n preHotwordAllowedLabels,\n );\n if (regexEntities.length > 0) log(\"regex\", `${regexEntities.length} matches`);\n\n if (legalFormsEnabled || config.enableTriggerPhrases) {\n // Populate the per-language legal-role-head cache so the\n // synchronous match processor below can read it. Cheap and\n // idempotent — only the first call kicks the loads.\n // Triggers also need this: the trigger reclassification step\n // (person → organization when the captured text contains a\n // legal-form suffix) reads `getKnownLegalSuffixes()`, which\n // falls back to the seed list until the cache is warmed.\n await warmLegalRoleHeads();\n }\n const rawLegalFormEntities = legalFormsEnabled\n ? detectLegalFormsV2(fullText)\n : [];\n const legalFormEntities = filterAllowedLabels(\n rawLegalFormEntities,\n preHotwordAllowedLabels,\n );\n if (legalFormEntities.length > 0)\n log(\"legal-forms\", `${legalFormEntities.length} matches`);\n\n if (config.enableTriggerPhrases) {\n // Populate the address-stop-keywords cache so the\n // synchronous address strategy uses the merged\n // per-language list instead of the seed fallback.\n await warmAddressStopKeywords();\n }\n const rawTriggerEntities = config.enableTriggerPhrases\n ? processTriggerMatches(\n regexMatches,\n slices.triggers.start,\n slices.triggers.end,\n fullText,\n search.triggerRules,\n )\n : [];\n const triggerEntities = filterAllowedLabels(\n rawTriggerEntities,\n preHotwordAllowedLabels,\n );\n if (triggerEntities.length > 0)\n log(\"trigger-phrases\", `${triggerEntities.length} matches`);\n\n // Signature-block recognition. Always on: the\n // anchors (\"/s/\", \"Name:\", \"By:\", \"IN WITNESS\n // WHEREOF\") are unambiguous legal-document markers\n // and produce no matches in unrelated prose.\n const rawSignatureEntities = detectSignatures(fullText, ctx);\n const signatureEntities = filterAllowedLabels(\n rawSignatureEntities,\n preHotwordAllowedLabels,\n );\n if (signatureEntities.length > 0)\n log(\"signatures\", `${signatureEntities.length} matches`);\n\n checkAbort(signal);\n\n let rawNameCorpusEntities: Entity[] = [];\n let nameCorpusEntities: Entity[] = [];\n if (config.enableNameCorpus && !config.enableDenyList) {\n await initNameCorpus(ctx, config.dictionaries, config.nameCorpusLanguages);\n checkAbort(signal);\n rawNameCorpusEntities = detectNameCorpus(fullText, ctx);\n nameCorpusEntities = filterAllowedLabels(\n rawNameCorpusEntities,\n preHotwordAllowedLabels,\n );\n log(\"name-corpus\", `${nameCorpusEntities.length} matches`);\n }\n\n const rawDenyListEntities =\n config.enableDenyList && search.denyListData\n ? processDenyListMatches(\n literalMatches,\n slices.denyList.start,\n slices.denyList.end,\n fullText,\n search.denyListData,\n ctx,\n )\n : [];\n const denyListEntities = filterAllowedLabels(\n rawDenyListEntities,\n preHotwordAllowedLabels,\n );\n if (denyListEntities.length > 0)\n log(\"deny-list\", `${denyListEntities.length} matches`);\n\n // Gazetteer: unified into tsLiterals\n const rawGazetteerEntities =\n config.enableGazetteer && search.gazetteerData\n ? processGazetteerMatches(\n literalMatches,\n slices.gazetteer.start,\n slices.gazetteer.end,\n fullText,\n search.gazetteerData,\n )\n : [];\n const gazetteerEntities = filterAllowedLabels(\n rawGazetteerEntities,\n preHotwordAllowedLabels,\n );\n if (gazetteerEntities.length > 0)\n log(\"gazetteer\", `${gazetteerEntities.length} matches`);\n\n const rawCountryEntities = search.countryData\n ? processCountryMatches(\n literalMatches,\n slices.countries.start,\n slices.countries.end,\n fullText,\n search.countryData,\n )\n : [];\n const countryEntities = filterAllowedLabels(\n rawCountryEntities,\n preHotwordAllowedLabels,\n );\n if (countryEntities.length > 0)\n log(\"countries\", `${countryEntities.length} matches`);\n\n checkAbort(signal);\n\n const rawCustomDenyListEntities = rawDenyListEntities.filter((entity) =>\n isCallerOwnedEntity(entity),\n );\n const rawCuratedDenyListEntities = rawDenyListEntities.filter(\n (entity) => !isCallerOwnedEntity(entity),\n );\n const customDenyListEntities = filterAllowedLabels(\n rawCustomDenyListEntities,\n preHotwordAllowedLabels,\n );\n\n const ruleContextEntities = [\n ...rawTriggerEntities,\n ...rawRegexEntities,\n ...customRegexEntities,\n ...rawLegalFormEntities,\n ...rawNameCorpusEntities,\n ...rawCuratedDenyListEntities,\n ...customDenyListEntities,\n ...rawGazetteerEntities,\n ];\n const nerMaskEntities = [\n ...rawTriggerEntities,\n ...rawRegexEntities,\n ...customRegexEntities,\n ...rawLegalFormEntities,\n ...rawNameCorpusEntities,\n ...rawCuratedDenyListEntities,\n ...customDenyListEntities,\n ...rawGazetteerEntities,\n ];\n\n // NER (mask rule-detected spans so the model doesn't\n // produce contradictory boundaries for known entities)\n let rawNerEntities: Entity[] = [];\n let nerEntities: Entity[] = [];\n const requestedNerLabels = getRequestedNerLabels(config, hotwordsActive);\n if (config.enableNer && nerInference && requestedNerLabels.length > 0) {\n const maskResult = maskDetectedSpans(fullText, nerMaskEntities);\n log(\"ner\", \"running inference...\");\n const rawNer = await nerInference(\n maskResult.maskedText,\n [...requestedNerLabels],\n config.threshold,\n signal,\n );\n rawNerEntities = unmaskNerEntities(rawNer, maskResult, fullText);\n nerEntities = filterAllowedLabels(rawNerEntities, preHotwordAllowedLabels);\n const masked = rawNer.length - rawNerEntities.length;\n const labelFiltered = rawNerEntities.length - nerEntities.length;\n log(\n \"ner\",\n `${nerEntities.length} entities` +\n (masked > 0 ? ` (${masked} masked)` : \"\") +\n (labelFiltered > 0 ? ` (${labelFiltered} label-filtered)` : \"\"),\n );\n }\n\n checkAbort(signal);\n\n // Address seed expansion\n const preAddressEntities = [\n ...triggerEntities,\n ...signatureEntities,\n ...regexEntities,\n ...legalFormEntities,\n ...nameCorpusEntities,\n ...denyListEntities,\n ...gazetteerEntities,\n ...countryEntities,\n ...nerEntities,\n ];\n const addressSeedEntities = labelIsAllowed(\"address\", allowedLabels)\n ? await processAddressSeeds(\n literalMatches,\n slices.streetTypes.start,\n slices.streetTypes.end,\n fullText,\n [...ruleContextEntities, ...rawNerEntities],\n )\n : [];\n if (addressSeedEntities.length > 0)\n log(\"address-seeds\", `${addressSeedEntities.length} expanded`);\n\n checkAbort(signal);\n\n // Zone-based score adjustment: apply before\n // threshold filtering so entities in PII-dense zones\n // can cross the threshold.\n const zoneAdjusted = applyZoneAdjustments(\n [...preAddressEntities, ...addressSeedEntities],\n zones,\n );\n\n // Hotword context rules: boost or reclassify\n // entities near relevant keywords. Applied after\n // zone adjustments so both effects stack.\n const preBoostEntities = (() => {\n if (!hotwordsActive) {\n return zoneAdjusted;\n }\n const hotwordCandidates = zoneAdjusted.filter(\n (entity) => !isCallerOwnedEntity(entity),\n );\n const callerOwnedEntities = zoneAdjusted.filter(isCallerOwnedEntity);\n return [\n ...filterAllowedLabels(\n applyHotwordRules(hotwordCandidates, fullText),\n allowedLabels,\n ),\n ...callerOwnedEntities,\n ];\n })();\n\n // Confidence boost + threshold filter\n let allEntities: Entity[];\n if (config.enableConfidenceBoost) {\n allEntities = boostNearMissEntities(preBoostEntities, config.threshold);\n const boosted =\n allEntities.length -\n preBoostEntities.filter((e) => e.score >= config.threshold).length;\n if (boosted > 0) log(\"confidence-boost\", `${boosted} near-miss promoted`);\n } else {\n allEntities = preBoostEntities.filter((e) => e.score >= config.threshold);\n }\n\n // Street patterns near existing addresses\n // (e.g. \"Ostrovni 225/1\" near \"110 00 Praha 1\")\n const streetPatterns = labelIsAllowed(\"address\", allowedLabels)\n ? detectStreetPatternsNearAddresses(fullText, allEntities)\n : [];\n if (streetPatterns.length > 0) {\n allEntities = [...allEntities, ...streetPatterns];\n log(\n \"street-context\",\n `${streetPatterns.length} street patterns near addresses`,\n );\n }\n\n // Orphan street lines in header zone\n const orphanStreets = labelIsAllowed(\"address\", allowedLabels)\n ? detectOrphanStreetLines(fullText, allEntities)\n : [];\n if (orphanStreets.length > 0) {\n allEntities = [...allEntities, ...orphanStreets];\n log(\"orphan-streets\", `${orphanStreets.length} header street lines`);\n }\n\n // Merge + dedup\n const rawMerged = mergeAndDedup(allEntities);\n log(\"merge\", `${rawMerged.length} after dedup`);\n\n // Extend monetary amounts to include trailing\n // \"(slovy ...)\" or \"(slovně ...)\" parentheticals.\n // Runs after dedup so each monetary span is unique,\n // preventing duplicate extensions from clobbering\n // unrelated entities between e.end and newEnd.\n const monetaryAmountWordsRe = await getAmountWordsRe();\n const monetaryTrailingRe = await getMonetaryTrailingCurrencyRe();\n const mergedWithCurrency = extendMonetaryTrailingCurrency(\n rawMerged,\n fullText,\n monetaryTrailingRe,\n );\n const mergedExtended = extendMonetaryAmountWords(\n mergedWithCurrency,\n fullText,\n monetaryAmountWordsRe,\n );\n\n // Boundary consistency (merge adjacent, fix partial\n // words, remove nested same-label)\n const consistent = enforceBoundaryConsistency(mergedExtended, fullText);\n if (consistent.length < mergedExtended.length)\n log(\n \"boundary\",\n `${mergedExtended.length - consistent.length} consolidated`,\n );\n\n // Organization name propagation: strip legal form\n // suffixes from detected orgs and re-scan for bare\n // mentions of the base name. Gated by enableCoreference\n // since this is a coreference-like pass. Propagated\n // entities are filtered by the configured threshold to\n // ensure they respect the caller's confidence floor.\n let postOrgEntities = consistent;\n if (\n config.enableCoreference &&\n labelIsAllowed(\"organization\", allowedLabels)\n ) {\n const orgPropagated = propagateOrgNames(consistent, fullText);\n const thresholded = orgPropagated.filter(\n (e) => e.score >= config.threshold,\n );\n if (thresholded.length > 0) {\n postOrgEntities = mergeAndDedup(consistent, thresholded);\n log(\"org-propagation\", `${thresholded.length} base names`);\n }\n }\n\n // False-positive filtering\n const merged = filterFalsePositives(postOrgEntities, ctx, fullText);\n if (merged.length < postOrgEntities.length)\n log(\"filter\", `removed ${postOrgEntities.length - merged.length} FPs`);\n\n checkAbort(signal);\n\n // Coreference\n // Clear stale entries unconditionally so a reused\n // context doesn't leak sourceText across documents.\n ctx.corefSourceMap.clear();\n if (config.enableCoreference) {\n // Coreference's alias filter rejects parenthetical\n // captures that are nothing but a legal-form suffix\n // (\"(« SAS »)\"), which needs the full vocabulary from\n // `data/legal-forms.json`. Warm it here so configs\n // that enable coreference without legal-form detection\n // still see the complete suffix set.\n if (!legalFormsEnabled) {\n await warmLegalRoleHeads();\n }\n const coreferenceSeeds = merged.filter(\n (entity) => !isCallerOwnedEntity(entity),\n );\n const terms = await extractDefinedTerms(fullText, coreferenceSeeds, ctx);\n if (terms.length > 0) {\n log(\"coreference\", `${terms.length} defined terms`);\n const corefSpans = findCoreferenceSpans(fullText, terms, ctx);\n if (corefSpans.length > 0) {\n log(\"coreference-rescan\", `${corefSpans.length} aliases`);\n const corefMerged = mergeAndDedup(merged, corefSpans);\n const corefConsistent = enforceBoundaryConsistency(\n corefMerged,\n fullText,\n );\n return sanitizeEntities(\n filterAllowedLabels(\n filterFalsePositives(corefConsistent, ctx, fullText),\n allowedLabels,\n ),\n );\n }\n }\n }\n\n // Re-sanitize: enforceBoundaryConsistency may adjust\n // entity boundaries after mergeAndDedup's sanitization,\n // potentially re-introducing whitespace or punctuation.\n return sanitizeEntities(filterAllowedLabels(merged, allowedLabels));\n};\n","import type {\n AnonymisationOperator,\n OperatorConfig,\n OperatorType,\n} from \"./types\";\n\n// ── Operator registry ──────────────────────────────────\n\nconst replaceOperator: AnonymisationOperator = {\n type: \"replace\",\n reversibility: \"reversible\",\n apply: (_text, _label, placeholder) => placeholder,\n};\n\nconst redactOperator: AnonymisationOperator = {\n type: \"redact\",\n reversibility: \"irreversible\",\n apply: (_text, _label, _placeholder, redactString) => redactString,\n};\n\nexport const OPERATOR_REGISTRY = {\n replace: replaceOperator,\n redact: redactOperator,\n} as const satisfies Record<OperatorType, AnonymisationOperator>;\n\nconst DEFAULT_REDACT_STRING = \"[REDACTED]\";\n\n/**\n * Default operator config: replace for all labels.\n * Preserves existing pipeline behaviour.\n */\nexport const DEFAULT_OPERATOR_CONFIG: OperatorConfig = {\n operators: {},\n redactString: DEFAULT_REDACT_STRING,\n};\n\n/**\n * Resolve the operator for a label, falling back to \"replace\".\n */\nexport const resolveOperator = (\n config: OperatorConfig,\n label: string,\n): OperatorType => config.operators[label] ?? \"replace\";\n","import {\n DEFAULT_OPERATOR_CONFIG,\n OPERATOR_REGISTRY,\n resolveOperator,\n} from \"./operators\";\nimport type {\n Entity,\n OperatorConfig,\n OperatorType,\n RedactionResult,\n} from \"./types\";\nimport type { PipelineContext } from \"./context\";\nimport { corefKey, defaultContext } from \"./context\";\n\nconst WHITESPACE_RE = /\\s+/g;\nconst PHONE_NOISE_RE = /[()\\s-]/g;\n// Strip all separators the ID detectors accept so the\n// same real-world value canonicalises to one placeholder:\n// - whitespace and `-` for IBAN, NIP, REGON, etc.\n// - `/` for birth numbers (\"900101/1234\") and Czech\n// bank accounts (\"123-4567/0100\").\n// - `.` for credit cards (\"4111.1111.1111.1111\") and\n// other dotted IDs.\nconst ID_SEPARATOR_RE = /[\\s\\-/.]/g;\n\n/**\n * Normalize entity text so that surface-form variations\n * of the same real-world value map to a single canonical\n * key. Lowercased emails, stripped phone formatting, etc.\n */\nconst normalizeEntityText = (label: string, text: string): string => {\n const upper = label.toUpperCase().replace(WHITESPACE_RE, \"_\");\n\n if (upper === \"EMAIL_ADDRESS\" || upper === \"EMAIL\") {\n return text.toLowerCase().trim();\n }\n if (upper === \"PHONE_NUMBER\" || upper === \"PHONE\") {\n return text.replace(PHONE_NOISE_RE, \"\");\n }\n if (\n upper === \"IBAN\" ||\n upper === \"BANK_ACCOUNT_NUMBER\" ||\n upper === \"TAX_IDENTIFICATION_NUMBER\" ||\n upper === \"REGISTRATION_NUMBER\" ||\n upper === \"NATIONAL_IDENTIFICATION_NUMBER\" ||\n upper === \"SOCIAL_SECURITY_NUMBER\" ||\n upper === \"BIRTH_NUMBER\" ||\n upper === \"IDENTITY_CARD_NUMBER\" ||\n upper === \"PASSPORT_NUMBER\" ||\n upper === \"CREDIT_CARD_NUMBER\"\n ) {\n return text.replace(ID_SEPARATOR_RE, \"\").toUpperCase();\n }\n if (\n upper === \"PERSON\" ||\n upper === \"ORGANIZATION\" ||\n upper === \"ADDRESS\" ||\n upper === \"LAND_PARCEL\" ||\n upper === \"MISC\"\n ) {\n return text.replace(WHITESPACE_RE, \" \").toLowerCase().trim();\n }\n return text.trim();\n};\n\n/**\n * Build a stable mapping from entity text to numbered\n * placeholders. Same real-world value always maps to the\n * same placeholder (e.g., \"Dr. Muller\" and \"Dr. Muller\"\n * both become [PERSON_1]).\n *\n * Placeholder format: [LABEL_N] where LABEL is uppercase\n * and N is a 1-based counter per label.\n *\n * @param ctx Pipeline context. Must be the same instance\n * passed to `runPipeline` (or `findCoreferenceSpans`)\n * so coreference placeholder links are preserved.\n * Defaults to `defaultContext` for single-tenant usage.\n */\nexport const buildPlaceholderMap = (\n entities: Entity[],\n ctx: PipelineContext = defaultContext,\n): Map<string, string> => {\n const counters = new Map<string, number>();\n const textLabelToPlaceholder = new Map<string, string>();\n const normalizedToPlaceholder = new Map<string, string>();\n\n const sorted = entities.toSorted((a, b) => a.start - b.start);\n\n for (const entity of sorted) {\n const compositeKey = `${entity.label}\\0${entity.text}`;\n if (textLabelToPlaceholder.has(compositeKey)) {\n continue;\n }\n\n const labelKey = entity.label.toUpperCase().replace(WHITESPACE_RE, \"_\");\n\n // Coreference side-channel: if this entity is a\n // coref alias, look up the source entity's\n // placeholder so both get the same number.\n const sourceText = ctx.corefSourceMap.get(corefKey(entity));\n if (sourceText !== undefined) {\n const sourceNormalized = normalizeEntityText(entity.label, sourceText);\n const sourceNormalizedKey = `${labelKey}\\0${sourceNormalized}`;\n const sourceExisting = normalizedToPlaceholder.get(sourceNormalizedKey);\n if (sourceExisting) {\n textLabelToPlaceholder.set(compositeKey, sourceExisting);\n continue;\n }\n }\n\n const normalized = normalizeEntityText(entity.label, entity.text);\n const normalizedKey = `${labelKey}\\0${normalized}`;\n const existing = normalizedToPlaceholder.get(normalizedKey);\n if (existing) {\n textLabelToPlaceholder.set(compositeKey, existing);\n continue;\n }\n\n const count = (counters.get(labelKey) ?? 0) + 1;\n counters.set(labelKey, count);\n\n const placeholder = `[${labelKey}_${count}]`;\n textLabelToPlaceholder.set(compositeKey, placeholder);\n normalizedToPlaceholder.set(normalizedKey, placeholder);\n }\n\n return textLabelToPlaceholder;\n};\n\n/**\n * Apply redactions to the source text, replacing each\n * confirmed entity span using the configured operator.\n *\n * Co-references are consistent: if the same text appears\n * multiple times, all occurrences get the same placeholder.\n *\n * @param ctx Pipeline context. Must be the same instance\n * passed to `runPipeline` (or `findCoreferenceSpans`)\n * so coreference placeholder links are preserved.\n * Defaults to `defaultContext` for single-tenant usage.\n */\nexport const redactText = (\n fullText: string,\n entities: Entity[],\n config: OperatorConfig = DEFAULT_OPERATOR_CONFIG,\n ctx: PipelineContext = defaultContext,\n): RedactionResult => {\n if (entities.length === 0) {\n return {\n redactedText: fullText,\n redactionMap: new Map(),\n operatorMap: new Map(),\n entityCount: 0,\n };\n }\n\n const placeholderMap = buildPlaceholderMap(entities, ctx);\n\n const sorted = entities.toSorted((a, b) => a.start - b.start);\n\n // Remove overlapping spans (keep first occurrence)\n const nonOverlapping: Entity[] = [];\n let lastEnd = 0;\n for (const entity of sorted) {\n if (entity.start >= lastEnd) {\n nonOverlapping.push(entity);\n lastEnd = entity.end;\n }\n }\n\n const parts: string[] = [];\n const redactionMap = new Map<string, string>();\n const operatorMap = new Map<string, OperatorType>();\n let cursor = 0;\n\n for (const entity of nonOverlapping) {\n if (entity.start > cursor) {\n parts.push(fullText.slice(cursor, entity.start));\n }\n\n const placeholder =\n placeholderMap.get(`${entity.label}\\0${entity.text}`) ??\n `[${entity.label.toUpperCase().replace(/\\s+/g, \"_\")}]`;\n\n const opType = resolveOperator(config, entity.label);\n const operator = OPERATOR_REGISTRY[opType];\n\n const replacement = operator.apply(\n entity.text,\n entity.label,\n placeholder,\n config.redactString,\n );\n\n parts.push(replacement);\n // operatorMap is keyed by the conceptual placeholder\n // ([LABEL_N]), not by the replacement text. For \"redact\"\n // operators the placeholder never appears in the output;\n // the map is only consulted via exportRedactionKey which\n // iterates redactionMap (replace entries only).\n operatorMap.set(placeholder, opType);\n\n // Only populate redactionMap for reversible operators\n if (\n operator.reversibility === \"reversible\" &&\n !redactionMap.has(placeholder)\n ) {\n redactionMap.set(placeholder, entity.text);\n }\n\n cursor = entity.end;\n }\n\n if (cursor < fullText.length) {\n parts.push(fullText.slice(cursor));\n }\n\n return {\n redactedText: parts.join(\"\"),\n redactionMap,\n operatorMap,\n entityCount: nonOverlapping.length,\n };\n};\n\n/**\n * Serialize the redaction key to JSON for export.\n * Includes operator metadata so the export is self-describing.\n */\nexport const exportRedactionKey = (\n redactionMap: Map<string, string>,\n operatorMap: Map<string, OperatorType>,\n): string => {\n const entries: Record<string, { original: string; operator: OperatorType }> =\n {};\n\n for (const [placeholder, value] of redactionMap) {\n entries[placeholder] = {\n original: value,\n operator: operatorMap.get(placeholder) ?? \"replace\",\n };\n }\n\n return JSON.stringify({ entries }, null, 2);\n};\n\n/**\n * De-anonymise text using a redaction key.\n * Replaces placeholders back with original values.\n * Only works for reversible operators (replace).\n */\nexport const deanonymise = (\n redactedText: string,\n redactionMap: Map<string, string>,\n): string => {\n let result = redactedText;\n\n for (const [placeholder, original] of redactionMap) {\n result = result.replaceAll(placeholder, original);\n }\n\n return result;\n};\n","/**\n * Decode ONNX model output into entity spans.\n *\n * The span-level model outputs a logits tensor of shape\n * [batch, inputLength, maxWidth, numEntities]. This module\n * applies sigmoid, thresholds, and greedy non-overlapping\n * selection to produce the final entity list.\n */\nimport type { RawInferenceResult } from \"./types\";\n\ntype Span = [string, number, number, string, number];\n\nconst sigmoid = (x: number): number => 1 / (1 + Math.exp(-x));\n\n/** Check if two spans overlap (optionally allowing multi-label). */\nconst hasOverlapping = (\n a: number[],\n b: number[],\n multiLabel: boolean,\n): boolean => {\n const a0 = a[0] ?? 0;\n const a1 = a[1] ?? 0;\n const b0 = b[0] ?? 0;\n const b1 = b[1] ?? 0;\n if (a0 === b0 && a1 === b1) {\n return !multiLabel;\n }\n return !(a0 > b1 || b0 > a1);\n};\n\n/**\n * Greedy non-overlapping span selection.\n * Sorts by score descending, keeps each span only if it\n * doesn't overlap with already-selected spans.\n */\nconst greedySearch = (\n spans: Span[],\n flatNer: boolean,\n multiLabel: boolean,\n): Span[] => {\n const sorted = spans.toSorted((a, b) => b[4] - a[4]);\n const selected: Span[] = [];\n\n for (const span of sorted) {\n const overlaps = selected.some((s) => {\n if (flatNer) {\n return hasOverlapping([span[1], span[2]], [s[1], s[2]], multiLabel);\n }\n // Non-flat: also allow nested spans\n const isNested =\n (span[1] <= s[1] && span[2] >= s[2]) ||\n (s[1] <= span[1] && s[2] >= span[2]);\n if (isNested) {\n return false;\n }\n return hasOverlapping([span[1], span[2]], [s[1], s[2]], multiLabel);\n });\n\n if (!overlaps) {\n selected.push(span);\n }\n }\n\n return selected.toSorted((a, b) => a[1] - b[1]);\n};\n\n/**\n * Decode span-level model logits into entity results.\n */\nexport const decodeSpans = (\n batchSize: number,\n inputLength: number,\n maxWidth: number,\n numEntities: number,\n texts: string[],\n batchIds: number[],\n batchWordsStartIdx: number[][],\n batchWordsEndIdx: number[][],\n idToClass: Record<number, string>,\n modelOutput: ArrayLike<number>,\n flatNer: boolean,\n threshold: number,\n multiLabel: boolean,\n): RawInferenceResult => {\n const spans: Span[][] = Array.from({ length: batchSize }, () => []);\n\n const batchPadding = inputLength * maxWidth * numEntities;\n const startTokenPadding = maxWidth * numEntities;\n const endTokenPadding = numEntities;\n\n for (let id = 0; id < modelOutput.length; id++) {\n const batch = Math.floor(id / batchPadding);\n const startToken = Math.floor(id / startTokenPadding) % inputLength;\n const endToken = startToken + (Math.floor(id / endTokenPadding) % maxWidth);\n const entity = id % numEntities;\n\n const prob = sigmoid(modelOutput[id] ?? 0);\n\n const batchStarts = batchWordsStartIdx[batch];\n const batchEnds = batchWordsEndIdx[batch];\n const batchSpans = spans[batch];\n if (!batchStarts || !batchEnds || !batchSpans) {\n continue;\n }\n\n if (\n prob >= threshold &&\n startToken < batchStarts.length &&\n endToken < batchEnds.length\n ) {\n const globalBatch = batchIds[batch] ?? 0;\n const startIdx = batchStarts[startToken] ?? 0;\n const endIdx = batchEnds[endToken] ?? 0;\n const spanText = (texts[globalBatch] ?? \"\").slice(startIdx, endIdx);\n batchSpans.push([\n spanText,\n startIdx,\n endIdx,\n idToClass[entity + 1] ?? \"\",\n prob,\n ]);\n }\n }\n\n return spans.map((batchSpans) =>\n greedySearch(batchSpans, flatNer, multiLabel),\n );\n};\n","/**\n * Token-level BIO decoder for GLiNER TokenGLiNER models\n * (e.g., gliner-pii-edge-v1.0).\n *\n * These models output logits of shape [B, L, C, 3] where:\n * B = batch size\n * L = number of words (text_lengths)\n * C = number of entity classes\n * 3 = BIO tags: [B(egin), I(nside), O(utside)]\n *\n * This decoder converts BIO-tagged logits into entity spans\n * with character offsets.\n */\nimport type { RawInferenceResult } from \"./types\";\n\ntype Span = [string, number, number, string, number];\n\nconst sigmoid = (x: number): number => 1 / (1 + Math.exp(-x));\n\nconst B_TAG = 0;\nconst I_TAG = 1;\n\n/**\n * Decode token-level BIO logits into entity spans.\n *\n * For each word, checks if the B(egin) logit for any class\n * exceeds the threshold. If so, extends the span by consuming\n * subsequent I(nside) tokens of the same class.\n */\nexport const decodeTokenSpans = (\n batchSize: number,\n numWords: number,\n numEntities: number,\n texts: string[],\n batchIds: number[],\n batchWordsStartIdx: number[][],\n batchWordsEndIdx: number[][],\n idToClass: Record<number, string>,\n modelOutput: ArrayLike<number>,\n threshold: number,\n): RawInferenceResult => {\n const results: Span[][] = Array.from({ length: batchSize }, () => []);\n\n const wordStride = numEntities * 3;\n const batchStride = numWords * wordStride;\n\n for (let b = 0; b < batchSize; b++) {\n const batchOffset = b * batchStride;\n const starts = batchWordsStartIdx[b];\n const ends = batchWordsEndIdx[b];\n const globalBatch = batchIds[b] ?? 0;\n const text = texts[globalBatch] ?? \"\";\n const batchSpans = results[b];\n\n if (!starts || !ends || !batchSpans) {\n continue;\n }\n\n const actualWords = starts.length;\n\n for (let e = 0; e < numEntities; e++) {\n let w = 0;\n\n while (w < actualWords) {\n const bLogitIdx = batchOffset + w * wordStride + e * 3 + B_TAG;\n const bScore = sigmoid(modelOutput[bLogitIdx] ?? 0);\n\n if (bScore < threshold) {\n w++;\n continue;\n }\n\n const spanStart = w;\n let spanEnd = w;\n let maxScore = bScore;\n\n while (spanEnd + 1 < actualWords) {\n const iLogitIdx =\n batchOffset + (spanEnd + 1) * wordStride + e * 3 + I_TAG;\n const iScore = sigmoid(modelOutput[iLogitIdx] ?? 0);\n\n if (iScore < threshold) {\n break;\n }\n\n spanEnd++;\n maxScore = Math.max(maxScore, iScore);\n }\n\n const charStart = starts[spanStart] ?? 0;\n const charEnd = ends[spanEnd] ?? 0;\n const spanText = text.slice(charStart, charEnd);\n const label = idToClass[e + 1] ?? \"\";\n\n if (spanText.trim().length > 0 && label) {\n batchSpans.push([spanText, charStart, charEnd, label, maxScore]);\n }\n\n w = spanEnd + 1;\n }\n }\n\n const selected: Span[] = [];\n for (const span of batchSpans.toSorted((x, y) => y[4] - x[4])) {\n const overlaps = selected.some((s) => span[1] < s[2] && span[2] > s[1]);\n if (!overlaps) {\n selected.push(span);\n }\n }\n\n results[b] = selected.toSorted((x, y) => x[1] - y[1]);\n }\n\n return results;\n};\n","/**\n * Tokenization and input preparation for GLiNER span model.\n *\n * Splits text into word tokens, encodes via HuggingFace\n * tokenizer, and builds the span index grid that the ONNX\n * model expects as input.\n */\nimport type { Encoding, Tokenizer } from \"@huggingface/tokenizers\";\n\nconst segmenter = new Intl.Segmenter(undefined, {\n granularity: \"word\",\n});\n\n/** Tokenize text into words with character offsets. */\nexport const tokenizeText = (\n text: string,\n): [words: string[], starts: number[], ends: number[]] => {\n const words: string[] = [];\n const starts: number[] = [];\n const ends: number[] = [];\n\n for (const { segment, index, isWordLike } of segmenter.segment(text)) {\n // Include words and numeric segments (Intl.Segmenter\n // marks pure digit sequences as non-word-like, but the\n // NER model needs them for entity detection).\n if (!isWordLike && !/\\d/u.test(segment)) {\n continue;\n }\n words.push(segment);\n starts.push(index);\n ends.push(index + segment.length);\n }\n\n return [words, starts, ends];\n};\n\n/** Build entity label <-> id mappings. */\nconst createMappings = (\n labels: string[],\n): {\n classToId: Record<string, number>;\n idToClass: Record<number, string>;\n} => {\n const classToId: Record<string, number> = {};\n const idToClass: Record<number, string> = {};\n for (let i = 0; i < labels.length; i++) {\n const label = labels[i];\n if (label === undefined) {\n continue;\n }\n const id = i + 1;\n classToId[label] = id;\n idToClass[id] = label;\n }\n return { classToId, idToClass };\n};\n\n/** Prepend entity prompt tokens to word tokens. */\nconst prepareTextInputs = (\n batchTokens: string[][],\n entities: string[],\n): [inputTexts: string[][], textLengths: number[], promptLengths: number[]] => {\n const inputTexts: string[][] = [];\n const promptLengths: number[] = [];\n const textLengths: number[] = [];\n\n for (const tokens of batchTokens) {\n textLengths.push(tokens.length);\n\n const prompt: string[] = [];\n for (const ent of entities) {\n prompt.push(\"<<ENT>>\");\n prompt.push(ent);\n }\n prompt.push(\"<<SEP>>\");\n\n promptLengths.push(prompt.length);\n inputTexts.push([...prompt, ...tokens]);\n }\n\n return [inputTexts, textLengths, promptLengths];\n};\n\n/** Encode word sequences into token IDs with masks. */\nconst encodeInputs = (\n tokenizer: Tokenizer,\n texts: string[][],\n promptLengths: number[],\n): [\n inputIds: number[][],\n attentionMasks: number[][],\n wordsMasks: number[][],\n] => {\n // Resolve special token IDs dynamically instead of\n // hardcoding DeBERTa-v3 values. Fallbacks (1, 2) match\n // the current model but future models may differ.\n const clsTokenId = tokenizer.token_to_id(\"[CLS]\") ?? 1;\n const sepTokenId = tokenizer.token_to_id(\"[SEP]\") ?? 2;\n\n const allInputIds: number[][] = [];\n const allAttentionMasks: number[][] = [];\n const allWordsMasks: number[][] = [];\n\n for (let idx = 0; idx < texts.length; idx++) {\n const promptLength = promptLengths[idx] ?? 0;\n const words = texts[idx];\n if (!words) {\n continue;\n }\n const wordsMask: number[] = [0];\n const inputIds: number[] = [clsTokenId];\n const attentionMask: number[] = [1];\n\n let wordCounter = 1;\n for (let wordId = 0; wordId < words.length; wordId++) {\n const word = words[wordId];\n if (word === undefined) {\n continue;\n }\n // encode() returns { ids, tokens, attention_mask }\n // with BOS/EOS; strip them with slice(1, -1)\n const encoded: Encoding = tokenizer.encode(word);\n const wordTokens: number[] = encoded.ids.slice(1, -1);\n\n for (let tokenId = 0; tokenId < wordTokens.length; tokenId++) {\n attentionMask.push(1);\n if (wordId < promptLength) {\n wordsMask.push(0);\n } else if (tokenId === 0) {\n wordsMask.push(wordCounter);\n wordCounter++;\n } else {\n wordsMask.push(0);\n }\n inputIds.push(wordTokens[tokenId] ?? 0);\n }\n }\n\n inputIds.push(sepTokenId);\n wordsMask.push(0);\n attentionMask.push(1);\n\n allInputIds.push(inputIds);\n allAttentionMasks.push(attentionMask);\n allWordsMasks.push(wordsMask);\n }\n\n return [allInputIds, allAttentionMasks, allWordsMasks];\n};\n\n/** Build span index pairs and masks for the model. */\nconst prepareSpans = (\n batchTokens: string[][],\n maxWidth: number,\n): { spanIdxs: number[][][]; spanMasks: boolean[][] } => {\n const spanIdxs: number[][][] = [];\n const spanMasks: boolean[][] = [];\n\n for (const tokens of batchTokens) {\n const len = tokens.length;\n const idx: number[][] = [];\n const mask: boolean[] = [];\n\n for (let i = 0; i < len; i++) {\n for (let j = 0; j < maxWidth; j++) {\n const endIdx = Math.min(i + j, len - 1);\n idx.push([i, endIdx]);\n mask.push(endIdx < len);\n }\n }\n\n spanIdxs.push(idx);\n spanMasks.push(mask);\n }\n\n return { spanIdxs, spanMasks };\n};\n\n/** Pad a 2D or 3D array to uniform inner length. */\nexport const padArray = <T>(arr: T[][], dimensions: number = 2): T[][] => {\n if (arr.length === 0) {\n return [];\n }\n const maxLength = Math.max(...arr.map((sub) => sub.length));\n // For 3D arrays, infer inner dimension from the first\n // non-empty element (arr[0] may be empty when a batch\n // element had zero word tokens).\n let finalDim = 0;\n if (dimensions === 3) {\n for (const sub of arr) {\n if (sub.length > 0) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- 3D arrays have number[] inner elements\n const inner = sub[0] as unknown as number[];\n finalDim = inner.length;\n break;\n }\n }\n }\n\n return arr.map((sub) => {\n const padCount = maxLength - sub.length;\n const fill =\n dimensions === 3\n ? Array.from({ length: padCount }, () =>\n Array.from<number>({ length: finalDim }).fill(0),\n )\n : Array.from<number>({ length: padCount }).fill(0);\n // SAFETY: fill values (zero arrays) match the shape of T\n // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- generic padding for ONNX tensor arrays\n return [...sub, ...(fill as T[])] as T[];\n });\n};\n\n/** Prepare a complete batch for ONNX inference. */\nexport const prepareBatch = (\n tokenizer: Tokenizer,\n texts: string[],\n entities: string[],\n maxWidth: number,\n): {\n inputsIds: number[][];\n attentionMasks: number[][];\n wordsMasks: number[][];\n textLengths: number[];\n spanIdxs: number[][][];\n spanMasks: boolean[][];\n idToClass: Record<number, string>;\n batchTokens: string[][];\n batchWordsStartIdx: number[][];\n batchWordsEndIdx: number[][];\n} => {\n const batchTokens: string[][] = [];\n const batchWordsStartIdx: number[][] = [];\n const batchWordsEndIdx: number[][] = [];\n\n for (const text of texts) {\n const [words, starts, ends] = tokenizeText(text);\n batchTokens.push(words);\n batchWordsStartIdx.push(starts);\n batchWordsEndIdx.push(ends);\n }\n\n const { idToClass } = createMappings(entities);\n\n const [inputTokens, textLengths, promptLengths] = prepareTextInputs(\n batchTokens,\n entities,\n );\n\n let [inputsIds, attentionMasks, wordsMasks] = encodeInputs(\n tokenizer,\n inputTokens,\n promptLengths,\n );\n\n inputsIds = padArray(inputsIds);\n attentionMasks = padArray(attentionMasks);\n wordsMasks = padArray(wordsMasks);\n\n let { spanIdxs, spanMasks } = prepareSpans(batchTokens, maxWidth);\n\n spanIdxs = padArray(spanIdxs, 3);\n spanMasks = padArray(spanMasks);\n\n return {\n inputsIds,\n attentionMasks,\n wordsMasks,\n textLengths,\n spanIdxs,\n spanMasks,\n idToClass,\n batchTokens,\n batchWordsStartIdx,\n batchWordsEndIdx,\n };\n};\n","import type { Entity } from \"../types\";\n\nconst MAX_CHUNK_CHARS = 1500;\nconst OVERLAP_CHARS = 50;\nconst MIN_CHUNK_LENGTH = 10;\n\n/**\n * Split text into overlapping chunks for GLiNER's\n * ~512 token context window. Character-based splitting\n * (rough approximation of token limits).\n *\n * Tries to break at sentence boundaries when possible.\n */\nexport const chunkText = (text: string): string[] => {\n const chunks: string[] = [];\n let offset = 0;\n\n while (offset < text.length) {\n let end = Math.min(offset + MAX_CHUNK_CHARS, text.length);\n\n if (end < text.length) {\n const slice = text.slice(offset, end);\n const lastPeriod = slice.lastIndexOf(\". \");\n if (lastPeriod > MAX_CHUNK_CHARS * 0.5) {\n end = offset + lastPeriod + 2;\n }\n }\n\n const chunk = text.slice(offset, end);\n if (chunk.trim().length > MIN_CHUNK_LENGTH) {\n chunks.push(chunk);\n }\n offset = Math.max(offset + 1, end - OVERLAP_CHARS);\n }\n\n return chunks;\n};\n\n/**\n * Compute the byte offset of each chunk within the\n * original document text.\n */\nexport const computeChunkOffsets = (\n fullText: string,\n chunks: string[],\n): number[] => {\n const offsets: number[] = [];\n let searchFrom = 0;\n\n for (const chunk of chunks) {\n const idx = fullText.indexOf(chunk, searchFrom);\n offsets.push(idx !== -1 ? idx : searchFrom);\n searchFrom =\n idx !== -1 ? idx + Math.max(1, chunk.length - OVERLAP_CHARS) : searchFrom;\n }\n\n return offsets;\n};\n\nconst POSITION_THRESHOLD = 5;\n\n/**\n * Merge entities from overlapping chunks back to\n * document-level offsets. Deduplicates entities that\n * appear in overlap regions (keeps highest score).\n *\n * Dedup invariant: each incoming entity is compared\n * against the highest-scored same-label near-dup in\n * its proximity window. If it loses, it is dropped.\n * This does NOT guarantee that all pairwise near-dup\n * relationships in the output are resolved; a lower-\n * scored entity can survive if the bridging entity\n * that would have replaced it was itself dropped by\n * a higher-scored match.\n *\n * Uses a reverse-scan over the sorted merged array\n * so each entity only compares against nearby\n * predecessors — O(n * w) average where w is the max\n * entities per POSITION_THRESHOLD window, O(n²) worst\n * case when replacements dominate (splice is O(n)).\n */\nexport const mergeChunkEntities = (\n chunkOffsets: number[],\n chunkResults: Entity[][],\n): Entity[] => {\n const allEntities: Entity[] = [];\n\n for (let i = 0; i < chunkResults.length; i++) {\n const offset = chunkOffsets[i] ?? 0;\n const entities = chunkResults[i];\n if (!entities) {\n continue;\n }\n for (const entity of entities) {\n allEntities.push({\n ...entity,\n start: entity.start + offset,\n end: entity.end + offset,\n });\n }\n }\n\n const sorted = allEntities.toSorted((a, b) => a.start - b.start);\n const merged: Entity[] = [];\n\n for (const entity of sorted) {\n let bestDupIndex = -1;\n let bestDupScore = -1;\n\n // Reverse-scan the full proximity window. Collect\n // the highest-scored same-label match so we dedup\n // against the strongest existing entity, not just\n // the nearest one.\n for (let j = merged.length - 1; j >= 0; j--) {\n const existing = merged[j];\n if (existing === undefined) {\n continue;\n }\n // merged is kept sorted by start (splice+push\n // maintains this); elements further back have\n // even smaller starts, so we can break early.\n if (entity.start - existing.start >= POSITION_THRESHOLD) {\n break;\n }\n if (\n existing.label === entity.label &&\n Math.abs(existing.end - entity.end) < POSITION_THRESHOLD &&\n existing.score > bestDupScore\n ) {\n bestDupIndex = j;\n bestDupScore = existing.score;\n }\n }\n\n if (bestDupIndex !== -1) {\n const existing = merged[bestDupIndex];\n if (existing !== undefined && entity.score > existing.score) {\n // Replace with winner. Splice out the old entry\n // and re-insert at the end to maintain sorted\n // order (entity.start >= all prior starts).\n merged.splice(bestDupIndex, 1);\n merged.push({ ...entity });\n }\n // Entity loses to the best match and is dropped.\n // Other lower-scored near-dups of this entity are\n // not revisited (see docstring dedup invariant).\n } else {\n merged.push({ ...entity });\n }\n }\n\n return merged;\n};\n","/**\n * Compute the Levenshtein edit distance between two\n * strings. O(n*m) time, O(min(n,m)) space using a\n * single-row DP approach.\n */\nexport const levenshtein = (rawA: string, rawB: string): number => {\n if (rawA === rawB) {\n return 0;\n }\n if (rawA.length === 0) {\n return rawB.length;\n }\n if (rawB.length === 0) {\n return rawA.length;\n }\n\n const [shorter, longer] =\n rawA.length <= rawB.length ? [rawA, rawB] : [rawB, rawA];\n\n const aLen = shorter.length;\n const bLen = longer.length;\n const row = new Uint16Array(aLen + 1);\n\n for (let i = 0; i <= aLen; i++) {\n row[i] = i;\n }\n\n for (let j = 1; j <= bLen; j++) {\n let prev = row[0] ?? 0;\n row[0] = j;\n\n for (let i = 1; i <= aLen; i++) {\n const cost = shorter[i - 1] === longer[j - 1] ? 0 : 1;\n const temp = row[i] ?? 0;\n row[i] = Math.min((row[i] ?? 0) + 1, (row[i - 1] ?? 0) + 1, prev + cost);\n prev = temp;\n }\n }\n\n return row[aLen] ?? 0;\n};\n","/* Browser/WASM entry point — loads TextSearch\n * from @stll/text-search-wasm and re-exports\n * the full anonymize API. */\n\nimport { TextSearch } from \"@stll/text-search-wasm\";\n\nimport { initTextSearch } from \"./search-engine\";\n\ninitTextSearch(TextSearch);\n\nexport * from \"./index-shared\";\n"],"mappings":"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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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,EAAE,OAAO,EAAE,QAAQ,GAAG;EAKpD,MAAM,cAAc,qBAAqB,KAAK,MAAM;EACpD,IAAI,CAAC,aAAa;EAElB,oBAAoB,SAAS,UADZ,EAAE,QAAQ,YAAY,QAAQ,YAAY,GAAG,QACb,GAAG,GAAI;CAC1D;CAEA,OAAO;AACT;;;;;;;;AC7RA,MAAa,iBAAiB;CAE5B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;AACF;;;;;;AAOA,MAAa,aAAa;CACxB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;AAMA,MAAa,qBAAqB,IAAI,IAAI;CACxC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;;;;;;AAaD,MAAa,yBAAyB,IAAI,IAAI;CAC5C;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;AAMD,MAAa,gBAAgB;CAC3B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAKA;CACA;A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IACP,MAAM;IACN,OAAO;IACP,QAAQ,kBAAkB;GAC5B,CAAC;EACH;CACF;CAEA,OAAO;AACT;;;;;;;AC91CA,MAAa,UAAU;CACrB,QAAQ;CACR,eAAe;CACf,QAAQ;EACN;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,UAAU;EACR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,aAAa;EACX;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,MAAM;EACJ;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,kBAAkB;EAChB;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,IAAI;EACF;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,MAAM;EAAC;EAAM;EAAM;CAAI;CACvB,SAAS;EAAC;EAAM;EAAM;EAAM;EAAM;CAAI;CACtC,KAAK;EAAC;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;CAAI;CACtE,aAAa;EAAC;EAAM;EAAM;EAAM;EAAM;EAAM;CAAI;CAChD,SAAS;EAAC;EAAM;EAAM;CAAI;CAC1B,YAAY;EAAC;EAAM;EAAM;EAAM;EAAM;EAAM;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"}
1
+ {"version":3,"file":"wasm.mjs","names":["charGroupsJson","m","CONNECTOR_RE","AND_TYPE_CONNECTOR_RE","UPPER_LETTER_RE","hasMiddleInitialBefore","m","CHUNK_SIZE","countriesData","PERSON_CHAIN_BREAK_RE","isInitialContinuationGap","deduplicateSpans","segmenter","escapeRegex","amountWordsConfig","WHITESPACE_RE","_exhaustive","m","POSTAL_CODE_RE","EMPTY_GENERIC_ROLES","escapeRegex","isCallerOwnedEntity","WORD_CHAR_RE","isCallerOwnedEntity","isCallerOwnedEntity","addressStreetTypesJson","hasLockedBoundary","DEFAULT_CUSTOM_REGEX_SCORE","createAllowedLabelSet","labelIsAllowed","sigmoid"],"sources":["../../src/search-engine.ts","../../src/types.ts","../../src/context.ts","../../src/util/lang-loader.ts","../../src/config/legal-forms.ts","../../src/data/char-groups.json","../../src/util/char-groups.ts","../../src/detectors/legal-forms.ts","../../src/detectors/coreference.ts","../../src/detectors/gazetteer.ts","../../src/util/normalize.ts","../../src/data/countries.json","../../src/detectors/countries.ts","../../src/config/titles.ts","../../src/util/text.ts","../../src/detectors/names.ts","../../src/detectors/signatures.ts","../../src/data/amount-words.json","../../src/detectors/regex.ts","../../src/detectors/legal-forms-v2.ts","../../src/detectors/triggers.ts","../../src/regions.ts","../../src/util/homoglyphs.ts","../../src/filters/false-positives.ts","../../src/detectors/deny-list.ts","../../src/detectors/address-seeds.ts","../../src/detectors/org-propagation.ts","../../src/data/address-street-types.json","../../src/filters/confidence-boost.ts","../../src/filters/zone-classifier.ts","../../src/filters/hotword-rules.ts","../../src/filters/boundary-consistency.ts","../../src/build-unified-search.ts","../../src/unified-search.ts","../../src/util/entity-masking.ts","../../src/pipeline.ts","../../src/operators.ts","../../src/redact.ts","../../src/gliner/decoder.ts","../../src/gliner/token-decoder.ts","../../src/gliner/processor.ts","../../src/util/chunker.ts","../../src/util/levenshtein.ts","../../src/wasm.ts"],"sourcesContent":["/**\n * Late-bound TextSearch constructor.\n *\n * Native entry injects @stll/text-search,\n * WASM entry injects @stll/text-search-wasm.\n * Both expose the same API.\n */\n\n/* eslint-disable-next-line @typescript-eslint/no-explicit-any */\ntype TextSearchCtor = new (...args: any[]) => any;\n\nlet _TextSearch: TextSearchCtor | undefined;\n\nexport const initTextSearch = (ctor: TextSearchCtor): void => {\n _TextSearch = ctor;\n};\n\nexport const getTextSearch = (): TextSearchCtor => {\n if (!_TextSearch) {\n throw new Error(\n \"TextSearch not initialized. Import from \" +\n \"@stll/anonymize or @stll/anonymize-wasm, \" +\n \"not from internal modules.\",\n );\n }\n return _TextSearch;\n};\n","// Runtime-free constants live in `./constants`; re-exported\n// here for back-compat with existing call sites that import\n// from `@stll/anonymize` directly.\n//\n// `verbatimModuleSyntax` requires an explicit type-only\n// import for any name used locally as a type even when it\n// is also re-exported below — applies to `DetectionSource`\n// (used by `Entity`) and `OperatorType` (used by\n// `OperatorConfig`).\nimport type { DetectionSource, OperatorType } from \"./constants\";\nimport { DETECTION_SOURCES } from \"./constants\";\n\nexport {\n DETECTION_SOURCES,\n DETECTOR_PRIORITY,\n type DetectionSource,\n} from \"./constants\";\n\n/**\n * Fields shared by every entity span in the source text.\n */\ntype EntityBase = {\n start: number;\n end: number;\n label: string;\n text: string;\n score: number;\n sourceDetail?: \"custom-deny-list\" | \"custom-regex\" | \"gazetteer-extension\";\n};\n\n/**\n * A PII entity span found by a primary detection layer\n * (regex, NER, legal forms, deny list, ...).\n */\nexport type DetectedEntity = EntityBase & {\n source: Exclude<DetectionSource, typeof DETECTION_SOURCES.COREFERENCE>;\n};\n\n/**\n * An alias mention of a previously detected entity: a\n * defined term (\"the Seller\") or a propagated bare\n * mention (\"Acme\" after \"Acme Corp.\").\n *\n * `corefSourceText` is required by construction, so an\n * alias cannot exist without the link back to its source\n * entity. Placeholder numbering reads it to give the\n * alias the same placeholder as the source. The link\n * travels with the entity instead of living in a\n * side-channel map that a producer could forget to\n * write — or that a later pass could clear.\n */\nexport type CorefAliasEntity = EntityBase & {\n source: typeof DETECTION_SOURCES.COREFERENCE;\n /** Full text of the source entity this alias refers to. */\n corefSourceText: string;\n};\n\n/**\n * A detected PII entity span in the source text.\n * Every detection layer produces these.\n */\nexport type Entity = DetectedEntity | CorefAliasEntity;\n\n/**\n * Entity after human review. Extends the base Entity\n * with a review decision.\n */\nexport type ReviewDecision = \"confirmed\" | \"rejected\" | \"relabeled\";\n\nexport type ReviewedEntity = Entity & {\n decision?: ReviewDecision;\n originalLabel?: string;\n};\n\n/**\n * A single entry in the workspace-scoped gazetteer\n * (deny list). Persisted in IndexedDB.\n */\nexport type GazetteerEntry = {\n id: string;\n canonical: string;\n label: string;\n variants: string[];\n workspaceId: string;\n createdAt: number;\n source: \"manual\" | \"confirmed-from-model\";\n};\n\n/** Extraction strategy — closed discriminated union. */\nexport type TriggerStrategy =\n | {\n type: \"to-next-comma\";\n /**\n * Optional list of lowercase keywords that terminate\n * the value scan, in addition to commas/newlines. Useful\n * for triggers like court names that may continue past\n * a missing comma into adjacent clause text (\"Městským\n * soudem v Praze dne 1. 1. 2020\"); listing `\"dne\"` here\n * stops the scan at the date boundary. Matched on a\n * word-boundary, case-insensitive.\n */\n stopWords?: string[];\n /**\n * Hard cap on the captured span length, in characters,\n * regardless of where the next comma / stop char sits.\n * Use for triggers that label short formulaic phrases\n * (\"State of Delaware\") and must not absorb the rest\n * of a long forum-selection clause when the comma is\n * sentences away. Falls back to the default 100-char\n * fallback when omitted.\n */\n maxLength?: number;\n }\n | { type: \"to-end-of-line\" }\n | { type: \"n-words\"; count: number }\n | { type: \"company-id-value\" }\n | { type: \"address\"; maxChars?: number }\n | {\n /**\n * Extract the first regex match in the value text.\n * Useful for shape-bounded values that follow a\n * label on the same line as other fields, where\n * `to-end-of-line` would over-capture. The pattern\n * is anchored to the start of the (already\n * leading-whitespace-stripped) value, so use\n * `(?:.*?)` prefix only when intentional.\n */\n type: \"match-pattern\";\n pattern: string;\n flags?: string;\n };\n\n/** Validation rules — closed discriminated union. */\nexport type TriggerValidation =\n | { type: \"starts-uppercase\" }\n | { type: \"min-length\"; min: number }\n | { type: \"max-length\"; max: number }\n | { type: \"no-digits\" }\n | { type: \"has-digits\" }\n | {\n type: \"matches-pattern\";\n pattern: string;\n flags?: string;\n }\n /**\n * Run a named stdnum validator (checksum + length)\n * against the captured value. Keeps the trigger\n * path symmetrical with the formatted-regex\n * detectors so e.g. `CPF nº 00000000000` does not\n * survive as a tax-ID entity.\n */\n | { type: \"valid-id\"; validator: ValidIdValidator };\n\n/** Built-in stdnum validators that can be referenced\n * by `valid-id` validations. */\nexport type ValidIdValidator = \"br.cpf\" | \"br.cnpj\" | \"us.rtn\";\n\n/** Auto-generated trigger variants — closed set. */\nexport type TriggerExtension =\n | \"add-colon\"\n | \"add-trailing-space\"\n | \"add-colon-space\"\n | \"normalize-spaces\";\n\n/** V2 trigger config entry (JSON shape). */\nexport type TriggerGroupConfig = {\n id?: string;\n triggers: string[];\n label: string;\n strategy: TriggerStrategy;\n extensions?: TriggerExtension[];\n validations?: TriggerValidation[];\n /** When true, include the trigger text in the\n * entity span (e.g., court names). */\n includeTrigger?: boolean;\n};\n\n/** Compiled validation with pre-built regex. */\nexport type CompiledValidation =\n | { type: \"starts-uppercase\"; re: RegExp }\n | { type: \"min-length\"; min: number }\n | { type: \"max-length\"; max: number }\n | { type: \"no-digits\"; re: RegExp }\n | { type: \"has-digits\"; re: RegExp }\n | { type: \"matches-pattern\"; re: RegExp }\n | { type: \"valid-id\"; check: (value: string) => boolean };\n\n/**\n * Runtime rule — one per trigger string after\n * expansion. Fed to the Aho-Corasick automaton.\n */\nexport type TriggerRule = {\n trigger: string;\n label: string;\n strategy: TriggerStrategy;\n validations: CompiledValidation[];\n includeTrigger: boolean;\n};\n\nexport { OPERATOR_TYPES, type OperatorType } from \"./constants\";\n\n/** Per-label operator selection. Key is the entity label. */\nexport type OperatorConfig = {\n /** Operator per label. Missing labels default to \"replace\". */\n operators: Record<string, OperatorType>;\n /** Custom replacement string for the redact operator. */\n redactString: string;\n};\n\n/** Whether an operator produces a reversible redaction entry. */\ntype OperatorReversibility = \"reversible\" | \"irreversible\";\n\nexport type AnonymisationOperator = {\n type: OperatorType;\n reversibility: OperatorReversibility;\n /**\n * Apply the operator to a single entity occurrence.\n * Returns the replacement string to embed in the document.\n */\n apply: (\n text: string,\n label: string,\n placeholder: string,\n redactString: string,\n ) => string;\n};\n\n/**\n * Redacted document output with stable entity mapping.\n */\nexport type RedactionResult = {\n redactedText: string;\n /**\n * Maps placeholder to original text. Only populated for\n * reversible operators (replace). Empty for redact.\n */\n redactionMap: Map<string, string>;\n /** Maps placeholder to the operator that produced it. */\n operatorMap: Map<string, OperatorType>;\n entityCount: number;\n};\n\n/**\n * Configuration for the detection pipeline.\n */\nexport type DenyListCategory =\n | \"Names\"\n | \"Places\"\n | \"Addresses\"\n | \"Courts\"\n | \"Financial\"\n | \"Government\"\n | \"Healthcare\"\n | \"Education\"\n | \"Political\"\n | \"Organizations\"\n | \"International\";\n\n/**\n * Metadata for a single dictionary entry in the\n * deny-list system. Mirrors the shape from\n * the anonymize-data package so consumers can pass\n * pre-loaded data without a runtime dependency.\n */\nexport type DictionaryMeta = {\n label: string;\n category: DenyListCategory;\n country: string | null;\n};\n\n/**\n * Caller-supplied exact terms for deny-list matching.\n * These entries are merged with the published deny-list\n * dictionaries when `enableDenyList` is enabled.\n */\nexport type CustomDenyListEntry = {\n value: string;\n label: string;\n variants?: readonly string[];\n};\n\n/**\n * Caller-supplied regex detector. The pattern is passed\n * to the underlying text-search regex engine, so use its\n * supported regex syntax. Inline flags such as `(?i)` are\n * accepted when supported by that engine.\n */\nexport type CustomRegexPattern = {\n pattern: string;\n label: string;\n score?: number;\n};\n\n/**\n * Pre-loaded dictionary data for dependency injection.\n * Consumers that want name/city/deny-list detection\n * load dictionaries themselves (e.g. from the\n * anonymize-data package) and pass them here; the\n * anonymize package has zero cross-package imports.\n *\n * All fields are optional. When a field is absent,\n * the corresponding detection path is skipped (same\n * behavior as when no dictionaries are available).\n */\nexport type Dictionaries = {\n /**\n * First names per language code (e.g., \"cs\", \"de\").\n * Merged with legacy config names at init time.\n */\n firstNames?: Readonly<Record<string, readonly string[]>>;\n /**\n * Surnames per language code.\n * Merged with legacy config names at init time.\n */\n surnames?: Readonly<Record<string, readonly string[]>>;\n /**\n * Non-Western name tokens per locale code\n * (e.g., \"in\", \"ar\", \"ja-latn\", \"ko\", \"zh-latn\",\n * \"th\", \"vi\", \"fil\", \"id\"). Merged with bundled\n * names-nw-*.json data at init time.\n */\n nonWesternNames?: Readonly<Record<string, readonly string[]>>;\n /**\n * Pre-loaded deny-list dictionaries keyed by\n * dictionary ID (e.g., \"courts/CZ\", \"banks/DE\").\n * Each value is the array of terms for that\n * dictionary.\n */\n denyList?: Readonly<Record<string, readonly string[]>>;\n /**\n * Metadata per dictionary ID. Required when\n * `denyList` is provided so the pipeline knows\n * labels, categories, and country filters.\n */\n denyListMeta?: Readonly<Record<string, DictionaryMeta>>;\n /**\n * Pre-loaded city names, already merged across\n * all desired countries.\n *\n * Prefer `citiesByCountry` when callers also pass\n * `denyListCountries` / `denyListRegions`; merged\n * city arrays cannot be scoped after injection.\n */\n cities?: readonly string[];\n /**\n * Pre-loaded city names keyed by ISO 3166-1 alpha-2\n * country code. When provided, the deny-list builder\n * applies `denyListCountries` / `denyListRegions`\n * before adding city patterns to the search automaton.\n */\n citiesByCountry?: Readonly<Record<string, readonly string[]>>;\n};\n\nexport type PipelineConfig = {\n threshold: number;\n enableTriggerPhrases: boolean;\n enableRegex: boolean;\n /**\n * Enables legal-form organization detection.\n * Required for typed callers; legacy untyped\n * callers that omit this field are treated as\n * enabled at runtime for backward compatibility.\n */\n enableLegalForms: boolean;\n /**\n * Enables first-name/surname/title corpus matching.\n * When deny-list mode is enabled, this also controls\n * whether name-corpus entries are injected into the\n * deny-list search automaton.\n */\n enableNameCorpus: boolean;\n /**\n * Optional language scope for first-name/surname\n * dictionaries, using the keys present in\n * `dictionaries.firstNames` / `dictionaries.surnames`\n * (for example `[\"en\", \"de\"]`). When omitted, all\n * injected name languages are used for backward\n * compatibility.\n */\n nameCorpusLanguages?: string[];\n enableDenyList: boolean;\n denyListCountries?: string[];\n denyListRegions?: string[];\n denyListExcludeCategories?: string[];\n /**\n * Caller-owned exact terms to match through the\n * deny-list layer. Requires `enableDenyList: true`.\n */\n customDenyList?: readonly CustomDenyListEntry[];\n /**\n * Caller-owned regex detectors. Requires\n * `enableRegex: true`.\n */\n customRegexes?: readonly CustomRegexPattern[];\n enableGazetteer: boolean;\n /**\n * Detect country names (ISO 3166-1 names, curated\n * aliases, alpha-3 codes). Defaults to true. Names\n * span all manifest languages plus widely-used\n * additions (Dutch, Russian, Chinese, Arabic, etc.).\n */\n enableCountries?: boolean;\n enableNer: boolean;\n enableConfidenceBoost: boolean;\n enableCoreference: boolean;\n enableZoneClassification?: boolean;\n enableHotwordRules?: boolean;\n /**\n * Requested output labels. An empty array means\n * \"do not filter by label\" for deterministic\n * detectors; NER falls back to DEFAULT_ENTITY_LABELS.\n */\n labels: string[];\n workspaceId: string;\n /**\n * Pre-loaded dictionary data for name, deny-list,\n * and city detection. When omitted, dictionary-based\n * detection paths are skipped. Consumers load from\n * the anonymize-data package and pass the data here.\n */\n dictionaries?: Dictionaries;\n};\n\nexport { DEFAULT_ENTITY_LABELS } from \"./constants\";\n\nexport const isLegalFormsEnabled = (\n config: Pick<PipelineConfig, \"enableLegalForms\">,\n): boolean => config.enableLegalForms !== false;\n","import type { UnifiedSearchInstance } from \"./build-unified-search\";\nimport type { Entity } from \"./types\";\n\n/**\n * Build a stable cache key for an entity that survives\n * shallow copies (spread). Uses position + label so the\n * key is identical for the original object and any\n * `{ ...entity }` copy produced by mergeAndDedup.\n *\n * @deprecated No longer used internally: coref alias\n * links travel on the entities themselves\n * (`corefSourceText`). Kept for API compatibility.\n */\nexport const corefKey = (e: Entity): string => `${e.start}:${e.end}:${e.label}`;\n\n/**\n * Compiled RegExp pattern used for coreference\n * definition extraction.\n */\nexport type DefinitionPattern = {\n pattern: RegExp;\n};\n\n/**\n * Cached data for the name corpus detector.\n * Populated by initNameCorpus; consumed by\n * detectNameCorpus and deny-list AC integration.\n */\nexport type NameCorpusData = {\n firstNames: ReadonlySet<string>;\n surnames: ReadonlySet<string>;\n titleTokens: ReadonlySet<string>;\n /** Abbreviation-style titles whose trailing dot is\n * part of the title, not a sentence boundary.\n * Contains the lowercase, dot-stripped form\n * (e.g., \"dr\", \"smt\", \"atty\"). */\n titleAbbreviations: ReadonlySet<string>;\n excludedWords: ReadonlySet<string>;\n /** Non-Western name tokens merged across all locales. */\n nonWesternNames: ReadonlySet<string>;\n /** All-caps acronyms excluded from name detection. */\n excludedAllCaps: ReadonlySet<string>;\n /** Raw arrays exposed for deny-list AC integration. */\n firstNamesList: readonly string[];\n surnamesList: readonly string[];\n titlesList: readonly string[];\n excludedList: readonly string[];\n nonWesternNamesList: readonly string[];\n excludedAllCapsList: readonly string[];\n};\n\n/**\n * All cached state for a single pipeline run (or\n * sequence of runs sharing the same config). Replacing\n * module-level singletons with this object enables\n * concurrent pipelines with different configs and\n * simplifies testing.\n *\n * Each field starts null and is populated lazily on\n * first use by the corresponding loader function.\n */\nexport type PipelineContext = {\n // ── Unified search cache ──────────────────────\n search: UnifiedSearchInstance | null;\n searchKey: string;\n searchPromise: Promise<UnifiedSearchInstance> | null;\n\n // ── Name corpus ───────────────────────────────\n nameCorpus: NameCorpusData | null;\n nameCorpusKey: string;\n nameCorpusPromise: Promise<void> | null;\n\n // ── Deny-list data sets ───────────────────────\n stopwords: ReadonlySet<string> | null;\n stopwordsPromise: Promise<ReadonlySet<string>> | null;\n allowList: ReadonlySet<string> | null;\n allowListPromise: Promise<ReadonlySet<string>> | null;\n personStopwords: ReadonlySet<string> | null;\n personStopwordsPromise: Promise<ReadonlySet<string>> | null;\n addressStopwords: ReadonlySet<string> | null;\n addressStopwordsPromise: Promise<ReadonlySet<string>> | null;\n /** First-name exclusions for stopword filtering. */\n firstNameExclusions: ReadonlySet<string> | null;\n firstNameExclusionCorpusLen: number;\n\n // ── Generic roles (false-positive filter) ─────\n genericRoles: ReadonlySet<string> | null;\n genericRolesPromise: Promise<ReadonlySet<string>> | null;\n\n // ── Coreference ───────────────────────────────\n corefPatterns: DefinitionPattern[] | null;\n corefPatternsPromise: Promise<DefinitionPattern[]> | null;\n corefLoadAttempted: boolean;\n roleStopSet: ReadonlySet<string> | null;\n roleStopSetPromise: Promise<ReadonlySet<string>> | null;\n\n // ── Zone classifier ───────────────────────────\n zoneHeadingPatterns: RegExp[] | null;\n zoneSigningPatterns: RegExp[] | null;\n zoneInitPromise: Promise<void> | null;\n};\n\n/** Create a fresh, empty pipeline context. */\nexport const createPipelineContext = (): PipelineContext => ({\n search: null,\n searchKey: \"\",\n searchPromise: null,\n\n nameCorpus: null,\n nameCorpusKey: \"\",\n nameCorpusPromise: null,\n\n stopwords: null,\n stopwordsPromise: null,\n allowList: null,\n allowListPromise: null,\n personStopwords: null,\n personStopwordsPromise: null,\n addressStopwords: null,\n addressStopwordsPromise: null,\n firstNameExclusions: null,\n firstNameExclusionCorpusLen: 0,\n\n genericRoles: null,\n genericRolesPromise: null,\n\n corefPatterns: null,\n corefPatternsPromise: null,\n corefLoadAttempted: false,\n roleStopSet: null,\n roleStopSetPromise: null,\n\n zoneHeadingPatterns: null,\n zoneSigningPatterns: null,\n zoneInitPromise: null,\n});\n\n/**\n * Module-level default context. Used when callers\n * don't provide an explicit context, preserving full\n * backward compatibility with the existing API.\n */\nexport const defaultContext: PipelineContext = createPipelineContext();\n","/**\n * Language manifest loader and config auto-discovery.\n *\n * Language manifest loader and config auto-discovery.\n *\n * Reads manifest.json to determine which languages have\n * which config types, then loads them via a static\n * registry of import() calls (string literals for\n * bundler compatibility).\n */\n\n// ── Types ────────────────────────────────────────────\n\ntype ManifestLanguage = {\n triggers?: boolean;\n coreference?: boolean;\n legalRoleHeads?: boolean;\n};\n\ntype Manifest = {\n languages: Record<string, ManifestLanguage>;\n};\n\ntype ConfigType = \"triggers\" | \"coreference\" | \"legalRoleHeads\";\n\n// ── Manifest loader (cached) ─────────────────────────\n\nlet _manifest: Manifest | null = null;\nlet _manifestPromise: Promise<Manifest> | null = null;\n\nconst loadManifest = (): Promise<Manifest> => {\n if (_manifest) {\n return Promise.resolve(_manifest);\n }\n if (_manifestPromise) {\n return _manifestPromise;\n }\n _manifestPromise = (async () => {\n try {\n const mod = await import(\"../data/manifest.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON manifest\n const parsed = (mod.default ?? mod) as Manifest;\n if (\n !parsed ||\n typeof parsed.languages !== \"object\" ||\n parsed.languages === null ||\n Array.isArray(parsed.languages)\n ) {\n console.warn(\n \"[anonymize] lang-loader: manifest has \" +\n \"unexpected structure, falling back \" +\n \"to hardcoded list\",\n );\n _manifest = { languages: {} };\n return _manifest;\n }\n _manifest = parsed;\n return _manifest;\n } catch (err) {\n // Manifest not available (old data package version\n // or unexpected import error). Cache the empty\n // result so we don't retry the failed import.\n console.warn(\n \"[anonymize] lang-loader: manifest not \" +\n \"available, falling back to hardcoded \" +\n \"language list:\",\n err,\n );\n _manifest = { languages: {} };\n return _manifest;\n }\n })();\n return _manifestPromise;\n};\n\n// ── Static import registries ─────────────────────────\n// String literals so bundlers can statically analyze\n// the import paths. Each registry maps language code\n// to a lazy loader thunk.\n\nconst TRIGGER_LOADERS: Record<string, () => Promise<unknown>> = {\n cs: () => import(\"../data/triggers.cs.json\"),\n de: () => import(\"../data/triggers.de.json\"),\n en: () => import(\"../data/triggers.en.json\"),\n es: () => import(\"../data/triggers.es.json\"),\n fr: () => import(\"../data/triggers.fr.json\"),\n hu: () => import(\"../data/triggers.hu.json\"),\n it: () => import(\"../data/triggers.it.json\"),\n pl: () => import(\"../data/triggers.pl.json\"),\n \"pt-br\": () => import(\"../data/triggers.pt-br.json\"),\n ro: () => import(\"../data/triggers.ro.json\"),\n sk: () => import(\"../data/triggers.sk.json\"),\n sv: () => import(\"../data/triggers.sv.json\"),\n};\n\nconst COREFERENCE_LOADERS: Record<string, () => Promise<unknown>> = {\n cs: () => import(\"../data/coreference.cs.json\"),\n de: () => import(\"../data/coreference.de.json\"),\n en: () => import(\"../data/coreference.en.json\"),\n es: () => import(\"../data/coreference.es.json\"),\n fr: () => import(\"../data/coreference.fr.json\"),\n it: () => import(\"../data/coreference.it.json\"),\n pl: () => import(\"../data/coreference.pl.json\"),\n \"pt-br\": () => import(\"../data/coreference.pt-br.json\"),\n sk: () => import(\"../data/coreference.sk.json\"),\n};\n\nconst LEGAL_ROLE_HEAD_LOADERS: Record<string, () => Promise<unknown>> = {\n cs: () => import(\"../data/legal-role-heads.cs.json\"),\n de: () => import(\"../data/legal-role-heads.de.json\"),\n en: () => import(\"../data/legal-role-heads.en.json\"),\n es: () => import(\"../data/legal-role-heads.es.json\"),\n fr: () => import(\"../data/legal-role-heads.fr.json\"),\n it: () => import(\"../data/legal-role-heads.it.json\"),\n pl: () => import(\"../data/legal-role-heads.pl.json\"),\n \"pt-br\": () => import(\"../data/legal-role-heads.pt-br.json\"),\n sk: () => import(\"../data/legal-role-heads.sk.json\"),\n};\n\nconst LOADER_REGISTRIES: Record<\n ConfigType,\n Record<string, () => Promise<unknown>>\n> = {\n triggers: TRIGGER_LOADERS,\n coreference: COREFERENCE_LOADERS,\n legalRoleHeads: LEGAL_ROLE_HEAD_LOADERS,\n};\n\n// ── Fallback language lists ──────────────────────────\n// Used when the manifest is unavailable (old data\n// package). Matches the hardcoded lists that existed\n// before the manifest was introduced.\n\nconst FALLBACK_LANGUAGES: Record<ConfigType, readonly string[]> = {\n triggers: [\n \"cs\",\n \"de\",\n \"en\",\n \"es\",\n \"fr\",\n \"hu\",\n \"it\",\n \"pl\",\n \"pt-br\",\n \"ro\",\n \"sk\",\n \"sv\",\n ],\n coreference: [\"cs\", \"de\", \"en\", \"es\", \"fr\", \"it\", \"pl\", \"pt-br\", \"sk\"],\n legalRoleHeads: [\"cs\", \"de\", \"en\", \"es\", \"fr\", \"it\", \"pl\", \"pt-br\", \"sk\"],\n};\n\n// ── Public API ───────────────────────────────────────\n\n/**\n * Load all config files of a given type for all\n * languages enabled in the manifest.\n *\n * Falls back to the hardcoded language list when the\n * manifest is unavailable (backward compatibility).\n */\nexport const loadLanguageConfigs = async <T extends NonNullable<unknown>>(\n configType: ConfigType,\n mapFn: (mod: unknown) => T,\n): Promise<T[]> => {\n const manifest = await loadManifest();\n const registry = LOADER_REGISTRIES[configType];\n\n // Determine which language codes to load\n const hasManifestLanguages = Object.keys(manifest.languages).length > 0;\n\n const codes = hasManifestLanguages\n ? Object.entries(manifest.languages)\n .filter(([code, lang]) => {\n if (!lang || typeof lang !== \"object\") {\n console.warn(\n `[anonymize] lang-loader: manifest ` +\n `entry for \"${code}\" is not an ` +\n `object, skipping`,\n );\n return false;\n }\n return lang[configType] === true;\n })\n .map(([code]) => code)\n : [...FALLBACK_LANGUAGES[configType]];\n\n // Use indexed assignment so results preserve\n // manifest declaration order regardless of import\n // resolution timing.\n const results: (T | undefined)[] = codes.map(() => undefined);\n\n const loads = codes.map(async (code, i) => {\n const loader = registry[code];\n if (!loader) {\n console.warn(\n `[anonymize] lang-loader: language \"${code}\" ` +\n `is enabled in the manifest for ` +\n `\"${configType}\" but has no loader in ` +\n `the static registry`,\n );\n return;\n }\n let mod: unknown;\n try {\n mod = await loader();\n } catch (err) {\n console.warn(\n `[anonymize] lang-loader: failed to ` +\n `import \"${configType}\" config for ` +\n `\"${code}\":`,\n err,\n );\n return;\n }\n let result: T;\n try {\n result = mapFn(mod);\n } catch (err) {\n console.warn(\n `[anonymize] lang-loader: mapFn failed ` +\n `for \"${code}\" (${configType}):`,\n err,\n );\n return;\n }\n results[i] = result;\n });\n\n await Promise.all(loads);\n return results.filter((r): r is T => r !== undefined);\n};\n\n/**\n * Reset cached manifest. Exposed for testing only.\n */\nexport const _resetManifestCache = (): void => {\n _manifest = null;\n _manifestPromise = null;\n};\n","/**\n * Known legal form suffixes. Shared between trigger\n * detection (reclassification), org-propagation (suffix\n * stripping), and the trailing-period strip in\n * sanitizeEntities. Add new entries in any order — the\n * export is sorted longest-first at module load so\n * consumers performing `endsWith` / regex-alternation\n * lookups always hit the most specific suffix\n * (\"Pty Ltd.\" before \"Pty Ltd\", \"spol. s r.o.\" before\n * \"s.r.o.\").\n */\nconst RAW_LEGAL_SUFFIXES = [\n // Czech\n \"spol. s r.o.\",\n \"s.r.o.\",\n \"s. r. o.\",\n \"a.s.\",\n \"a. s.\",\n \"v.o.s.\",\n \"v. o. s.\",\n \"k.s.\",\n \"k. s.\",\n \"z.s.\",\n \"z. s.\",\n \"z.ú.\",\n \"z. ú.\",\n \"o.p.s.\",\n \"o. p. s.\",\n \"s.p.\",\n \"s. p.\",\n // German / Austrian / Swiss\n \"GmbH\",\n \"AG\",\n \"SE\",\n \"KG\",\n \"OHG\",\n // English (UK/US/AU/IE). Title-case and ALL-CAPS\n // spellings both appear in real filings (party\n // captions and signature blocks render in caps);\n // the regex matches case-sensitively, so both\n // spellings need explicit entries.\n \"Ltd.\",\n \"Ltd\",\n \"LTD.\",\n \"LTD\",\n \"LLC\",\n \"LLP\",\n \"Inc.\",\n \"INC.\",\n \"Inc\",\n \"INC\",\n \"Corp.\",\n \"CORP.\",\n \"Corp\",\n \"CORP\",\n \"Corporation\",\n \"CORPORATION\",\n \"Co.\",\n \"CO.\",\n \"LP\",\n \"L.P.\",\n \"PLC\",\n \"plc\",\n \"N.A.\",\n \"N.V.\",\n \"B.V.\",\n \"Pty Ltd.\",\n \"Pty Ltd\",\n \"PTY LTD.\",\n \"PTY LTD\",\n // French / Iberian / Italian\n \"S.A.\",\n \"SA\",\n \"SAS\",\n \"SARL\",\n \"S.p.A.\",\n // Polish\n \"Sp. z o.o.\",\n \"Sp. k.\",\n \"Sp. j.\",\n // Brazilian / Portuguese. Both title-case and\n // all-caps spellings appear in BR contracts; the\n // reclassification regex is case-sensitive so each\n // spelling needs an explicit entry.\n \"Ltda.\",\n \"LTDA.\",\n \"Ltda\",\n \"LTDA\",\n \"S/A\",\n \"EIRELI\",\n \"EPP\",\n \"ME\",\n \"MEI\",\n];\n\nexport const LEGAL_SUFFIXES: readonly string[] = [...RAW_LEGAL_SUFFIXES].sort(\n (a, b) => b.length - a.length,\n);\n","","/**\n * Centralized Unicode character equivalence groups.\n *\n * Centralized Unicode character equivalence groups.\n *\n * Provides helpers to build regex character classes that\n * match all typographic variants of a character type\n * (dashes, spaces, quotes, etc.).\n *\n * The JSON is statically imported so the bundler inlines\n * it, avoiding a runtime require() that breaks browsers.\n */\n\nimport charGroupsJson from \"../data/char-groups.json\";\n\ntype CharEntry = {\n char: string;\n name: string;\n code: string;\n};\n\ntype CharGroup = {\n description: string;\n chars: readonly CharEntry[];\n};\n\ntype CharGroupsConfig = {\n _comment: string;\n groups: Record<string, CharGroup>;\n};\n\n/** Chars that need escaping inside a regex char class. */\nconst REGEX_CLASS_SPECIAL = /[\\\\\\]^-]/;\n\nconst escapeForCharClass = (ch: string): string =>\n REGEX_CLASS_SPECIAL.test(ch) ? `\\\\${ch}` : ch;\n\n// SAFETY: JSON shape matches CharGroupsConfig by contract.\nconst config = charGroupsJson as CharGroupsConfig;\n\n/**\n * Get the raw characters for a named group.\n * Throws if the group does not exist.\n */\nexport const charSet = (group: string): readonly string[] => {\n const g = config.groups[group];\n if (!g) {\n throw new Error(`Unknown char group: \"${group}\"`);\n }\n return g.chars.map((entry) => entry.char);\n};\n\n/**\n * Build a regex character class string for a named\n * group. E.g., charClass(\"dash\") returns a string\n * like \"[-\\u2013\\u2014\\u2010\\u2011\\u2212\\u2043]\".\n *\n * The hyphen-minus is placed first so it is treated\n * as a literal, not a range indicator.\n */\nexport const charClass = (group: string): string => {\n const chars = charSet(group);\n // Place hyphen-minus first to avoid range issues.\n const sorted = [...chars].sort((a, b) => {\n if (a === \"-\") return -1;\n if (b === \"-\") return 1;\n return a.localeCompare(b);\n });\n const escaped = sorted.map(escapeForCharClass);\n return `[${escaped.join(\"\")}]`;\n};\n\n/**\n * Return the inner content of a character class (without\n * the surrounding brackets). Useful for embedding a group\n * inside a larger character class, e.g.:\n * `[\\\\s&,.${charClassInner(\"dash\")}]`\n */\nexport const charClassInner = (group: string): string => {\n const chars = charSet(group);\n const sorted = [...chars].sort((a, b) => {\n if (a === \"-\") return -1;\n if (b === \"-\") return 1;\n return a.localeCompare(b);\n });\n return sorted.map(escapeForCharClass).join(\"\");\n};\n\n/**\n * Build a regex alternation pattern for a named group.\n * Unlike charClass, this uses (?:a|b|c) syntax which\n * is useful when chars need individual escaping or\n * when embedding in complex patterns.\n */\nexport const charPattern = (group: string): string => {\n const chars = charSet(group);\n // SAFETY: length === 1 guarantees index 0 exists.\n if (chars.length === 1) return chars[0]!;\n const escaped = chars.map((ch) => ch.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"));\n return `(?:${escaped.join(\"|\")})`;\n};\n\n// ── Pre-built constants for common groups ──────\n\n/** Regex char class matching all dash variants. */\nexport const DASH = charClass(\"dash\");\n\n/**\n * Inner content of the dash char class (no brackets).\n * For embedding inside a larger character class:\n * `[\\\\s&,.${DASH_INNER}]`\n */\nexport const DASH_INNER = charClassInner(\"dash\");\n\n/** Regex char class matching all space variants. */\nexport const SPACE = charClass(\"space\");\n\n/** Regex char class matching all double-quote variants. */\nexport const QUOTE_DOUBLE = charClass(\"quote-double\");\n\n/** Inner content of the double-quote char class (no brackets). */\nexport const QUOTE_DOUBLE_INNER = charClassInner(\"quote-double\");\n\n/** Regex char class matching all single-quote variants. */\nexport const QUOTE_SINGLE = charClass(\"quote-single\");\n\n/** Inner content of the single-quote char class (no brackets). */\nexport const QUOTE_SINGLE_INNER = charClassInner(\"quote-single\");\n\n/**\n * Inline opening-bracket characters that frequently introduce\n * a defined-term parenthetical immediately after an address or\n * organization span (e.g. `…GA 30326, USA (the \"Premises\")`).\n * Hardcoded rather than data-driven because the set is tiny,\n * regex-special, and not language-dependent.\n */\nexport const OPENING_BRACKETS_INNER = \"(\\\\[{\";\n\n/** Regex char class matching all slash variants. */\nexport const SLASH = charClass(\"slash\");\n\n/** Regex char class matching all dot variants. */\nexport const DOT = charClass(\"dot\");\n\n/** Regex char class matching all colon variants. */\nexport const COLON = charClass(\"colon\");\n","/**\n * Legal form detection for company/organization names.\n *\n * Detects company names by finding legal form suffixes\n * (s.r.o., GmbH, a.s., etc.) and extending backwards\n * to capture preceding capitalised words.\n *\n * Exports pattern definitions for the unified builder\n * and a match processor for post-processing.\n */\n\nimport type { Match } from \"@stll/text-search\";\n\nimport { LEGAL_SUFFIXES } from \"../config/legal-forms\";\nimport { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\nimport { DASH_INNER } from \"../util/char-groups\";\nimport { loadLanguageConfigs } from \"../util/lang-loader\";\n\n// Verb-like tokens that signal sentence context: when one of\n// these appears between a role-head opening and the legal form,\n// the match is a swept sentence fragment, not an organisation\n// name. Names like \"Client solutions Inc.\" or \"Vendor consulting\n// Ltd.\" don't contain any of these, so they pass through the\n// trim untouched. Lowercased; matched case-insensitively.\n//\n// Sourced from `data/sentence-verb-indicators.json` (per-\n// language so verb morphology stays next to other per-language\n// data). Loaded lazily; the seed below covers the most common\n// indicators across cs/en/de so the sync accessor keeps working\n// before `warmSentenceVerbIndicators()` resolves.\nconst SENTENCE_VERB_INDICATORS_SEED: ReadonlySet<string> = new Set([\n \"je\",\n \"jsou\",\n \"is\",\n \"are\",\n \"ist\",\n \"sind\",\n]);\n\nlet sentenceVerbIndicatorsCache: ReadonlySet<string> | null = null;\nlet sentenceVerbIndicatorsPromise: Promise<ReadonlySet<string>> | null = null;\n\nconst loadSentenceVerbIndicators = async (): Promise<ReadonlySet<string>> => {\n if (sentenceVerbIndicatorsCache) return sentenceVerbIndicatorsCache;\n if (sentenceVerbIndicatorsPromise) return sentenceVerbIndicatorsPromise;\n sentenceVerbIndicatorsPromise = (async () => {\n let data: Record<string, unknown> = {};\n try {\n const mod = await import(\"../data/sentence-verb-indicators.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON module shape\n const parsed =\n (mod as { default?: Record<string, unknown> }).default ?? mod;\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON module shape\n data = parsed as Record<string, unknown>;\n } catch (err) {\n console.warn(\n \"[anonymize] legal-forms: failed to load \" +\n \"sentence-verb-indicators.json, falling back \" +\n \"to seed list:\",\n err,\n );\n }\n const all = new Set<string>(SENTENCE_VERB_INDICATORS_SEED);\n for (const [key, value] of Object.entries(data)) {\n if (key.startsWith(\"_\")) continue;\n if (!Array.isArray(value)) continue;\n for (const verb of value) {\n if (typeof verb !== \"string\" || verb.length === 0) continue;\n all.add(verb.toLowerCase());\n }\n }\n sentenceVerbIndicatorsCache = all;\n return all;\n })();\n return sentenceVerbIndicatorsPromise;\n};\n\nexport const getSentenceVerbIndicatorsSync = (): ReadonlySet<string> =>\n sentenceVerbIndicatorsCache ?? SENTENCE_VERB_INDICATORS_SEED;\n\n// Horizontal whitespace as understood by DOCX text extraction.\n// `regex`/TextSearch does not treat NBSP variants as `\\s`, but\n// company names often contain them between words and legal forms.\nconst HSPACE = \"(?:[^\\\\S\\\\n]|[  ])\";\nconst UPPER = \"A-ZÁČĎÉĚÍŇÓŘŠŤÚŮÝŽÄÖÜÀÂÆÇÈÊËÎÏÔÙÛŸÑĄĆĘŁŃŚŹŻ\\\\u0130\";\nconst LEGAL_LIST_BOUNDARY_RE = new RegExp(\n `^[,;]${HSPACE}+(?=\\\\p{Lu}|(?:\\\\p{Lu}\\\\.${HSPACE}?){2,})`,\n \"u\",\n);\n\nconst ROMAN_NUMERAL_RE =\n /^(?=[IVXLCDM])M{0,3}(?:CM|CD|D?C{0,3})(?:XC|XL|L?X{0,3})(?:IX|IV|V?I{0,3})$/;\n\ntype LeadingClauseTrimConfig = {\n phrases?: readonly string[];\n directPrefixes?: readonly string[];\n};\n\ntype LeadingClauseTrims = {\n phrases: readonly string[];\n directPrefixes: readonly string[];\n};\n\nconst EMPTY_LEADING_CLAUSE_TRIMS: LeadingClauseTrims = {\n phrases: [],\n directPrefixes: [],\n};\n\nlet leadingClauseTrimsCache: LeadingClauseTrims | null = null;\nlet leadingClauseTrimsPromise: Promise<LeadingClauseTrims> | null = null;\n\nconst loadLeadingClauseTrims = async (): Promise<LeadingClauseTrims> => {\n if (leadingClauseTrimsCache) return leadingClauseTrimsCache;\n if (leadingClauseTrimsPromise) return leadingClauseTrimsPromise;\n leadingClauseTrimsPromise = (async () => {\n let data: Record<string, unknown> = {};\n try {\n const mod = await import(\"../data/legal-form-leading-clauses.json\");\n const parsed =\n (mod as { default?: Record<string, unknown> }).default ?? mod;\n data = parsed as Record<string, unknown>;\n } catch (err) {\n console.warn(\n \"[anonymize] legal-forms: failed to load \" +\n \"legal-form-leading-clauses.json:\",\n err,\n );\n }\n\n const phrases = new Set<string>();\n const directPrefixes = new Set<string>();\n for (const [key, value] of Object.entries(data)) {\n if (key.startsWith(\"_\") || typeof value !== \"object\" || value === null) {\n continue;\n }\n const config = value as LeadingClauseTrimConfig;\n for (const phrase of config.phrases ?? []) {\n if (typeof phrase === \"string\" && phrase.length > 0) {\n phrases.add(phrase);\n }\n }\n for (const prefix of config.directPrefixes ?? []) {\n if (typeof prefix === \"string\" && prefix.length > 0) {\n directPrefixes.add(prefix);\n }\n }\n }\n\n const result = {\n phrases: [...phrases],\n directPrefixes: [...directPrefixes],\n };\n leadingClauseTrimsCache = result;\n return result;\n })();\n return leadingClauseTrimsPromise;\n};\n\nconst getLeadingClauseTrimsSync = (): LeadingClauseTrims =>\n leadingClauseTrimsCache ?? EMPTY_LEADING_CLAUSE_TRIMS;\n\n// Generic legal/contract role words that should never appear\n// at the head of an organisation name. When a greedy regex\n// sweep includes one of these as the first word, the span is\n// a sentence fragment, not a real company (e.g. \"Vendor 1\n// owns an equity interest in the Acme s.r.o. company\"). The\n// processor trims back to the last real Cap-starting word in\n// that case. Per-language word lists live under\n// `data/legal-role-heads.<lang>.json`; loaded lazily and\n// cached on first use.\ntype LegalRoleHeadsConfig = {\n words: readonly string[];\n};\n\nlet legalRoleHeadsCache: ReadonlySet<string> | null = null;\nlet legalRoleHeadsPromise: Promise<ReadonlySet<string>> | null = null;\n\nconst loadLegalRoleHeads = async (): Promise<ReadonlySet<string>> => {\n if (legalRoleHeadsCache) return legalRoleHeadsCache;\n if (legalRoleHeadsPromise) return legalRoleHeadsPromise;\n legalRoleHeadsPromise = (async () => {\n const sets = await loadLanguageConfigs<LegalRoleHeadsConfig>(\n \"legalRoleHeads\",\n (mod) => {\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config shape\n const m = mod as {\n default?: LegalRoleHeadsConfig;\n };\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config shape\n return (m.default ?? mod) as LegalRoleHeadsConfig;\n },\n );\n const all = new Set<string>();\n for (const entry of sets) {\n if (!entry || !Array.isArray(entry.words)) continue;\n for (const word of entry.words) {\n if (typeof word === \"string\" && word.length > 0) {\n all.add(word.toLowerCase());\n }\n }\n }\n legalRoleHeadsCache = all;\n return all;\n })();\n return legalRoleHeadsPromise;\n};\n\n// Synchronous helper used inside `processLegalFormMatches`,\n// which is a sync function called once per pipeline run. The\n// pipeline calls `warmLegalRoleHeads()` before invoking it, so\n// the cache is populated by the time matches are processed.\nexport const getLegalRoleHeadsSync = (): ReadonlySet<string> =>\n legalRoleHeadsCache ?? new Set<string>();\n\nexport const warmLegalRoleHeads = async (): Promise<void> => {\n await Promise.all([\n loadLegalRoleHeads(),\n loadAllLegalSuffixes(),\n loadSentenceVerbIndicators(),\n loadClauseNounHeads(),\n loadConnectorProseHeads(),\n loadStructuralSingleCapPrefixes(),\n loadLeadingClauseTrims(),\n ]);\n};\n\n// Suffix anchoring during the role-head trim needs the FULL\n// legal-form vocabulary (not just the small `LEGAL_SUFFIXES`\n// propagation list). \"Vendor owns Acme Corp.\" has to anchor on\n// \"Corp.\" but `LEGAL_SUFFIXES` is Czech-leaning; load the same\n// JSON the pattern builder uses and flatten it once.\nlet allLegalSuffixesCache: readonly string[] | null = null;\nlet allLegalSuffixesPromise: Promise<readonly string[]> | null = null;\nlet normalizedLegalBoundarySuffixesCache: ReadonlySet<string> | null = null;\nlet normalizedInNameLegalFormWordsCache: ReadonlySet<string> | null = null;\n\nconst normalizeLegalSuffixToken = (suffix: string): string =>\n suffix.replace(/[.,\\s]/g, \"\");\n\nconst isBoundaryLegalSuffixForm = (form: string): boolean => {\n const normalized = normalizeLegalSuffixToken(form);\n if (normalized.length === 0) {\n return false;\n }\n if (LEGAL_SUFFIXES.includes(form)) {\n return true;\n }\n return /[.]/u.test(form) || normalized === normalized.toUpperCase();\n};\n\nconst loadAllLegalSuffixes = async (): Promise<readonly string[]> => {\n if (allLegalSuffixesCache) return allLegalSuffixesCache;\n if (allLegalSuffixesPromise) return allLegalSuffixesPromise;\n allLegalSuffixesPromise = (async () => {\n let data: Record<string, string[]>;\n try {\n const mod = await import(\"../data/legal-forms.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON module shape\n data = (mod as { default: Record<string, string[]> }).default;\n } catch {\n data = {};\n }\n const seen = new Set<string>();\n const out: string[] = [];\n for (const list of Object.values(data)) {\n for (const form of list) {\n if (typeof form !== \"string\" || form.length === 0) continue;\n if (seen.has(form)) continue;\n seen.add(form);\n out.push(form);\n }\n }\n for (const form of LEGAL_SUFFIXES) {\n if (!seen.has(form)) {\n seen.add(form);\n out.push(form);\n }\n }\n // Sort longest-first so multi-token suffixes like\n // \"spol. s r.o.\" anchor before nested shorter forms.\n out.sort((a, b) => b.length - a.length);\n allLegalSuffixesCache = out;\n normalizedLegalBoundarySuffixesCache = new Set(\n out\n .filter(isBoundaryLegalSuffixForm)\n .map(normalizeLegalSuffixToken)\n .filter((suffix) => suffix.length > 0),\n );\n normalizedInNameLegalFormWordsCache = new Set(\n out\n .filter((form) => !isBoundaryLegalSuffixForm(form) && !/\\s/u.test(form))\n .map(normalizeLegalSuffixToken)\n .filter((suffix) => suffix.length > 0),\n );\n return out;\n })();\n return allLegalSuffixesPromise;\n};\n\nconst getAllLegalSuffixesSync = (): readonly string[] =>\n allLegalSuffixesCache ?? LEGAL_SUFFIXES;\n\nconst getNormalizedLegalBoundarySuffixesSync = (): ReadonlySet<string> =>\n normalizedLegalBoundarySuffixesCache ??\n new Set(\n LEGAL_SUFFIXES.map(normalizeLegalSuffixToken).filter(\n (suffix) => suffix.length > 0,\n ),\n );\n\nconst getNormalizedInNameLegalFormWordsSync = (): ReadonlySet<string> =>\n normalizedInNameLegalFormWordsCache ?? new Set<string>();\n\n/**\n * Sync accessor for the full legal-form vocabulary\n * (`data/legal-forms.json` plus `LEGAL_SUFFIXES`,\n * longest-first). Falls back to `LEGAL_SUFFIXES` when\n * `warmLegalRoleHeads()` has not run yet. Exposed so the\n * trailing-period strip in `sanitizeEntities` can keep\n * pace with the detector vocabulary rather than only the\n * smaller `LEGAL_SUFFIXES` propagation list.\n */\nexport const getKnownLegalSuffixes = getAllLegalSuffixesSync;\n\n// Common contract clause nouns that appear in legal prose\n// between a sentence-verb and the company name. When the trim\n// scans forward for the org's first Cap word, these are skipped\n// like role-heads so we don't anchor on \"Agreement\" / \"License\"\n// in patterns such as \"Vendor signed Agreement with Acme Inc.\".\n//\n// Sourced from `data/clause-noun-heads.json` (per-language so\n// vocabulary stays next to other per-language data). Loaded\n// lazily; `warmLegalRoleHeads()` resolves the cache before\n// `processLegalFormMatches` runs.\nconst CLAUSE_NOUN_HEADS_SEED: ReadonlySet<string> = new Set([\n \"agreement\",\n \"contract\",\n]);\n\nlet clauseNounHeadsCache: ReadonlySet<string> | null = null;\nlet clauseNounHeadsPromise: Promise<ReadonlySet<string>> | null = null;\n\nconst loadClauseNounHeads = async (): Promise<ReadonlySet<string>> => {\n if (clauseNounHeadsCache) return clauseNounHeadsCache;\n if (clauseNounHeadsPromise) return clauseNounHeadsPromise;\n clauseNounHeadsPromise = (async () => {\n let data: Record<string, unknown> = {};\n try {\n const mod = await import(\"../data/clause-noun-heads.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON module shape\n const parsed =\n (mod as { default?: Record<string, unknown> }).default ?? mod;\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON module shape\n data = parsed as Record<string, unknown>;\n } catch (err) {\n console.warn(\n \"[anonymize] legal-forms: failed to load \" +\n \"clause-noun-heads.json, falling back to seed list:\",\n err,\n );\n }\n const all = new Set<string>(CLAUSE_NOUN_HEADS_SEED);\n for (const [key, value] of Object.entries(data)) {\n if (key.startsWith(\"_\")) continue;\n if (!Array.isArray(value)) continue;\n for (const noun of value) {\n if (typeof noun !== \"string\" || noun.length === 0) continue;\n all.add(noun.toLowerCase());\n }\n }\n clauseNounHeadsCache = all;\n return all;\n })();\n return clauseNounHeadsPromise;\n};\n\nexport const getClauseNounHeadsSync = (): ReadonlySet<string> =>\n clauseNounHeadsCache ?? CLAUSE_NOUN_HEADS_SEED;\n\nlet connectorProseHeadsCache: ReadonlySet<string> | null = null;\nlet connectorProseHeadsPromise: Promise<ReadonlySet<string>> | null = null;\n\nconst loadConnectorProseHeads = async (): Promise<ReadonlySet<string>> => {\n if (connectorProseHeadsCache) {\n return connectorProseHeadsCache;\n }\n if (connectorProseHeadsPromise) {\n return connectorProseHeadsPromise;\n }\n\n connectorProseHeadsPromise = (async () => {\n let data: { roles?: unknown } = {};\n try {\n const mod = await import(\"../data/generic-roles.json\");\n const parsed = (mod as { default?: { roles?: unknown } }).default ?? mod;\n data = parsed as { roles?: unknown };\n } catch (err) {\n console.warn(\n \"[anonymize] legal-forms: failed to load generic-roles.json:\",\n err,\n );\n }\n\n const all = new Set<string>();\n if (Array.isArray(data.roles)) {\n for (const role of data.roles) {\n if (typeof role === \"string\" && role.length > 0) {\n all.add(role.toLowerCase());\n }\n }\n }\n\n connectorProseHeadsCache = all;\n return all;\n })();\n\n return connectorProseHeadsPromise;\n};\n\nconst getConnectorProseHeadsSync = (): ReadonlySet<string> =>\n connectorProseHeadsCache ?? new Set<string>();\n\nlet structuralSingleCapPrefixesCache: ReadonlySet<string> | null = null;\nlet structuralSingleCapPrefixesPromise: Promise<ReadonlySet<string>> | null =\n null;\n\nconst loadStructuralSingleCapPrefixes = async (): Promise<\n ReadonlySet<string>\n> => {\n if (structuralSingleCapPrefixesCache) {\n return structuralSingleCapPrefixesCache;\n }\n if (structuralSingleCapPrefixesPromise) {\n return structuralSingleCapPrefixesPromise;\n }\n\n structuralSingleCapPrefixesPromise = (async () => {\n let data: Record<string, unknown> = {};\n try {\n const mod = await import(\"../data/structural-single-cap-prefixes.json\");\n const parsed =\n (mod as { default?: Record<string, unknown> }).default ?? mod;\n data = parsed as Record<string, unknown>;\n } catch (err) {\n console.warn(\n \"[anonymize] legal-forms: failed to load \" +\n \"structural-single-cap-prefixes.json:\",\n err,\n );\n }\n\n const all = new Set<string>();\n for (const [key, value] of Object.entries(data)) {\n if (key.startsWith(\"_\")) {\n continue;\n }\n if (!Array.isArray(value)) {\n continue;\n }\n for (const prefix of value) {\n if (typeof prefix !== \"string\" || prefix.length === 0) {\n continue;\n }\n all.add(prefix.toLowerCase());\n }\n }\n\n structuralSingleCapPrefixesCache = all;\n return all;\n })();\n\n return structuralSingleCapPrefixesPromise;\n};\n\nconst getStructuralSingleCapPrefixesSync = (): ReadonlySet<string> =>\n structuralSingleCapPrefixesCache ?? new Set<string>();\n\n// Used by the trim helpers below to escape literal suffix tokens\n// before injecting them into runtime-built regexes.\nconst escapeForRegex = (form: string): string =>\n form\n .replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\s+/g, `${HSPACE}+`)\n .replace(/\\\\\\./g, `\\\\.${HSPACE}?`);\n\n// ── Pattern builder for unified search ──────────────\n\n/**\n * Build legal form regex pattern strings.\n * Returns an array of regex strings for the unified\n * TextSearch builder. Empty if data package is not\n * installed.\n */\nexport const buildLegalFormPatterns = async (): Promise<string[]> => {\n // The legal-form detector was rewritten to the candidate +\n // validator architecture in `legal-forms-v2.ts`. The unified\n // search no longer carries any legal-form regex patterns —\n // suffix discovery now goes through an Aho-Corasick literal\n // pass and the rough span around each hit is handed to the\n // existing `processLegalFormMatches` validator chain.\n // Returning an empty list keeps the unified-search builder\n // happy without compiling the ~7 KB monolithic regex that\n // tripped the upstream text-search DFA on long preambles.\n return [];\n};\n\n// ── Backward extension ──────────────────────────────\n\nconst CONNECTOR_RE = /^(?:a|and|und|et|e|y|i|&)$/i;\n// Multi-char \"and\"-type connectors. When backward\n// extension hits one of these with exactly two\n// uppercase words behind it, the pattern looks like\n// \"<First> <Last> and <ORG>\" and we stop rather than\n// swallow the personal name into the org span.\nconst AND_TYPE_CONNECTOR_RE = /^(?:and|und|et)$/i;\nconst UPPER_LETTER_RE = /^\\p{Lu}/u;\n// Capitalised words that, when they begin a legal-form\n// match, signal the match is the tail of a multi-word\n// organisation name (\"Acme Widgets and Company, Inc.\",\n// \"The Bank of America and Trust Company, Inc.\").\n// In that mode the two-cap-words \"First Last and ORG\"\n// heuristic is suspended and a small set of in-name\n// prepositions (\"of\") are crossable during backward\n// extension. Capitalised-form only — lowercase \"trust\"\n// or \"bank\" are common verbs/nouns.\nconst COMPANY_SUFFIX_WORDS_RE =\n /^(?:Company|Co|Bank|Brothers|Bros|Sons|Group|Holdings|Trust|Partners|Associates|Corporation|Industries|Enterprises|Solutions|Systems|Services|Foundation|Institute)$/;\nconst IN_NAME_PREPOSITION_RE = /^(?:of|the)$/i;\nconst ENTITY_HEAD_WORD_RE = /^[\\p{L}\\p{M}&]+/u;\nconst BARE_SINGLE_CAP_LEGAL_FORM_RE = new RegExp(\n `^[${UPPER}](?:${HSPACE}+|,${HSPACE}*)`,\n \"u\",\n);\nconst STRUCTURAL_SINGLE_CAP_RE = new RegExp(\n `^([\\\\p{L}\\\\p{M}]+)${HSPACE}+[${UPPER}](?:[.${DASH_INNER}]?\\\\d{1,3})?(?:${HSPACE}+|,${HSPACE}*)`,\n \"u\",\n);\nconst isStructuralSingleCapMatch = (text: string): boolean => {\n const first = STRUCTURAL_SINGLE_CAP_RE.exec(text)?.[1];\n return (\n first !== undefined &&\n getStructuralSingleCapPrefixesSync().has(first.toLowerCase())\n );\n};\n\nconst findLastSuffixSeparator = (text: string): number =>\n Math.max(\n text.lastIndexOf(\" \"),\n text.lastIndexOf(\"\\t\"),\n text.lastIndexOf(\" \"),\n text.lastIndexOf(\" \"),\n text.lastIndexOf(\",\"),\n );\n\nconst stripDocxSpaces = (text: string): string => text.replace(/[  ]/g, \"\");\n\n/**\n * Find the word ending just before `pos` in `text`,\n * skipping any whitespace (not newlines).\n * Returns null if no word is found (e.g., at start\n * of text, or preceded by non-word chars like \".\").\n */\nconst findWordBefore = (\n text: string,\n pos: number,\n): { word: string; start: number } | null => {\n let scan = pos - 1;\n // Skip horizontal whitespace\n while (scan >= 0) {\n const ch = text.charAt(scan);\n if (ch === \"\\n\" || !/\\s/.test(ch)) break;\n scan--;\n }\n if (scan < 0 || text.charAt(scan) === \"\\n\") {\n return null;\n }\n\n const wordEnd = scan + 1;\n while (scan >= 0 && /[\\p{L}\\p{M}&]/u.test(text.charAt(scan))) {\n scan--;\n }\n const wordStart = scan + 1;\n const word = text.slice(wordStart, wordEnd);\n if (word.length === 0) return null;\n return { word, start: wordStart };\n};\n\nconst hasSingleCapPrefixBefore = (\n fullText: string,\n matchStart: number,\n): boolean => {\n const prev = findWordBefore(fullText, matchStart);\n return (\n prev !== null && prev.word.length === 1 && UPPER_LETTER_RE.test(prev.word)\n );\n};\n\nconst isBareSingleCapStructuralInnerMatch = (\n fullText: string,\n matchStart: number,\n text: string,\n): boolean => {\n if (!BARE_SINGLE_CAP_LEGAL_FORM_RE.test(text)) {\n return false;\n }\n\n const prev = findWordBefore(fullText, matchStart);\n return (\n prev !== null &&\n getStructuralSingleCapPrefixesSync().has(prev.word.toLowerCase())\n );\n};\n\nconst trimEmbeddedLegalFormListPrefix = (\n entityStart: number,\n entityText: string,\n): { entityStart: number; entityText: string } => {\n let cut = -1;\n\n for (const suffix of getAllLegalSuffixesSync()) {\n const suffixClean = suffix.replace(/[.,\\s]/g, \"\");\n if (suffixClean.length > 0 && ROMAN_NUMERAL_RE.test(suffixClean)) {\n continue;\n }\n\n let fromIndex = 0;\n while (fromIndex < entityText.length) {\n const suffixStart = entityText.indexOf(suffix, fromIndex);\n if (suffixStart === -1) {\n break;\n }\n fromIndex = suffixStart + suffix.length;\n\n const suffixEnd = suffixStart + suffix.length;\n if (suffixEnd >= entityText.length - 1) {\n continue;\n }\n\n const afterSuffix = entityText.slice(suffixEnd);\n const boundary = /^,\\s+(?=\\p{Lu})/u.exec(afterSuffix);\n if (boundary === null) {\n continue;\n }\n\n const nextStart = suffixEnd + boundary[0].length;\n const remainder = entityText.slice(nextStart);\n if (!getAllLegalSuffixesSync().some((form) => remainder.endsWith(form))) {\n continue;\n }\n\n cut = Math.max(cut, nextStart);\n }\n }\n\n if (cut <= 0) {\n return { entityStart, entityText };\n }\n\n return {\n entityStart: entityStart + cut,\n entityText: entityText.slice(cut),\n };\n};\n\nconst splitEmbeddedLegalFormList = (\n entityStart: number,\n entityText: string,\n): { entityStart: number; entityText: string }[] => {\n const cuts = [0];\n\n for (const suffix of getAllLegalSuffixesSync()) {\n const suffixClean = suffix.replace(/[.,\\s]/g, \"\");\n if (suffixClean.length > 0 && ROMAN_NUMERAL_RE.test(suffixClean)) {\n continue;\n }\n\n let fromIndex = 0;\n while (fromIndex < entityText.length) {\n const suffixStart = entityText.indexOf(suffix, fromIndex);\n if (suffixStart === -1) {\n break;\n }\n fromIndex = suffixStart + suffix.length;\n\n const suffixEnd = suffixStart + suffix.length;\n if (suffixEnd >= entityText.length - 1) {\n continue;\n }\n\n const afterSuffix = entityText.slice(suffixEnd);\n const boundary = LEGAL_LIST_BOUNDARY_RE.exec(afterSuffix);\n if (boundary === null) {\n continue;\n }\n\n // Cut at every list boundary that immediately\n // follows a legal-form suffix. The left segment\n // is a complete org; the right segment is filtered\n // below by the per-segment suffix gate. Previously\n // we required the *right* side to end in a suffix\n // too, which let lists like \"Morgan Stanley & Co.\n // LLC, Bank of America\" be captured as one org\n // because the trailing \"Bank of America\" lacks a\n // suffix.\n const nextStart = suffixEnd + boundary[0].length;\n cuts.push(nextStart);\n }\n }\n\n const uniqueCuts = [...new Set(cuts)].toSorted((a, b) => a - b);\n if (uniqueCuts.length === 1) {\n return [{ entityStart, entityText }];\n }\n\n const segments: { entityStart: number; entityText: string }[] = [];\n for (let index = 0; index < uniqueCuts.length; index++) {\n const start = uniqueCuts[index];\n const end = uniqueCuts[index + 1] ?? entityText.length;\n if (start === undefined) {\n continue;\n }\n const segmentText = entityText.slice(start, end).replace(/[,\\s;]+$/u, \"\");\n if (segmentText.length === 0) {\n continue;\n }\n // Skip the segment unless it ends with a recognised\n // legal-form suffix. This drops the right-hand side\n // of a list-cut when only the left side carries a\n // suffix (see split note above), letting other\n // detectors claim the remainder if appropriate.\n const endsWithSuffix = getAllLegalSuffixesSync().some((form) =>\n segmentText.endsWith(form),\n );\n if (!endsWithSuffix) {\n continue;\n }\n segments.push({\n entityStart: entityStart + start,\n entityText: segmentText,\n });\n }\n\n return segments;\n};\n\nconst hasDisallowedLineBreak = (text: string): boolean => {\n for (const match of text.matchAll(/\\n/gu)) {\n const index = match.index;\n if (index === undefined) {\n continue;\n }\n const before = text.slice(0, index);\n const after = text.slice(index + 1);\n const dottedDesignatorBefore = /\\.[^\\S\\n]*$/u.test(before);\n const legalSuffixAfter =\n /^[^\\S\\n]*(?:\\p{Lu}\\.[^\\S\\n]?){1,}\\p{Lu}?\\.?$/u.test(after);\n const allCapsSuffixAfter = /^[^\\S\\n]*\\p{Lu}{2,}\\.?$/u.test(after);\n if (!dottedDesignatorBefore || (!legalSuffixAfter && !allCapsSuffixAfter)) {\n return true;\n }\n }\n return false;\n};\n\nconst hasMiddleInitialBefore = (fullText: string, pos: number): boolean => {\n const previousWord = findWordBefore(fullText, pos);\n if (!previousWord) {\n return false;\n }\n\n let scan = previousWord.start - 1;\n while (scan >= 0 && (fullText[scan] === \" \" || fullText[scan] === \"\\t\")) {\n scan--;\n }\n\n return (\n scan >= 1 &&\n fullText[scan] === \".\" &&\n UPPER_LETTER_RE.test(fullText[scan - 1] ?? \"\")\n );\n};\n\n/**\n * Count consecutive uppercase-starting words immediately\n * before `pos`. Stops at the first non-upper word or at\n * text/line start. Used to disambiguate sentence prose\n * (\"<First> <Last> and <ORG>\", \"<Defined-Term> and\n * <ORG>\") from multi-word organisation names that span\n * an \"and\" connector (\"UniCredit Bank Czech Republic and\n * Slovakia, a.s.\").\n *\n * When `crossInNamePreps` is true, the walk also steps\n * over in-name lowercase prepositions (\"of\", \"the\") as\n * long as they sit between two upper words. This lets\n * the suffix-mode \"and\"-crossing logic see through\n * \"<Trust ← and ← America ← of ← Bank>\" and emit one\n * full organisation span.\n */\nconst countUpperWordsBefore = (\n fullText: string,\n pos: number,\n crossInNamePreps = false,\n): number => {\n let count = 0;\n let scan = pos;\n while (scan > 0) {\n const found = findWordBefore(fullText, scan);\n if (found) {\n if (UPPER_LETTER_RE.test(found.word)) {\n count++;\n scan = found.start;\n continue;\n }\n if (crossInNamePreps && IN_NAME_PREPOSITION_RE.test(found.word)) {\n // Only cross the preposition when it sits between\n // two uppercase words — never when it sentence-\n // starts the phrase.\n const prev = findWordBefore(fullText, found.start);\n if (!prev) break;\n if (!UPPER_LETTER_RE.test(prev.word)) break;\n scan = found.start;\n continue;\n }\n break;\n }\n\n let p = scan - 1;\n while (p >= 0 && (fullText[p] === \" \" || fullText[p] === \"\\t\")) {\n p--;\n }\n if (\n p >= 1 &&\n fullText[p] === \".\" &&\n UPPER_LETTER_RE.test(fullText[p - 1] ?? \"\")\n ) {\n count++;\n scan = p - 1;\n continue;\n }\n break;\n }\n return count;\n};\n\n/**\n * True when `word` is a recognized legal-form suffix\n * (case-sensitive against the legal-forms vocabulary).\n * Used when deciding whether to cross an \"and\" connector\n * during backward extension — if the word immediately\n * preceding the connector is itself a legal-form suffix,\n * the \"and\" sits between two organisation names rather\n * than inside one (\"Morgan Securities LLC and Allen &\n * Company LLC\"), so the walk must stop there.\n */\nconst isKnownLegalFormSuffix = (word: string): boolean => {\n if (word.length === 0) {\n return false;\n }\n return getNormalizedLegalBoundarySuffixesSync().has(word);\n};\n\nconst isInNameLegalFormWord = (word: string): boolean => {\n if (word.length === 0) {\n return false;\n }\n return getNormalizedInNameLegalFormWordsSync().has(word);\n};\n\n/**\n * If `pos` is immediately preceded (modulo horizontal\n * whitespace) by an initial-dot run like `J.P.`, `U.S.`,\n * or `N.A.`, return the position where the initial run\n * starts. Otherwise return `pos` unchanged. The run must\n * be word-bounded on the left so we never absorb a stray\n * sentence-ending dot.\n */\nconst skipInitialsBackward = (fullText: string, pos: number): number => {\n // Skip horizontal whitespace only — initials must sit\n // on the same line as the rest of the org name.\n let scan = pos - 1;\n while (scan >= 0) {\n const ch = fullText.charAt(scan);\n if (ch === \"\\n\" || !/\\s/.test(ch)) break;\n scan--;\n }\n if (scan < 0 || fullText.charAt(scan) !== \".\") return pos;\n // Match one or more `<Upper>.` tokens at the right\n // edge of `fullText[0..scan+1]`. Allows optional\n // single horizontal space between tokens\n // (\"J. P. Morgan\" as well as \"J.P. Morgan\").\n const scanLimit = Math.max(0, scan + 1 - 100);\n const head = fullText.slice(scanLimit, scan + 1);\n const initialsRe = /(?:\\p{Lu}\\.[^\\S\\n]?){2,}$/u;\n const match = initialsRe.exec(head);\n if (match === null) return pos;\n const start = scanLimit + match.index;\n if (start > 0) {\n const prevCh = fullText.charAt(start - 1);\n if (/[\\p{L}\\p{M}\\p{N}]/u.test(prevCh)) return pos;\n }\n return start;\n};\n\n/**\n * Extend a match backward through uppercase words and\n * lowercase connectors. Stops at start of text,\n * newline, or a word that doesn't qualify.\n *\n * Connectors (a, and, und, et, ...) are only consumed\n * when there is a valid word before them — a trailing\n * connector at an entity boundary is not consumed.\n * For multi-char \"and\"-type connectors we additionally\n * refuse to cross when exactly two uppercase words\n * precede them (\"First Last and ORG, Inc.\" shape) —\n * unless the match itself begins with a known company-\n * suffix word (\"…and Company, Inc.\"), in which case\n * the chain belongs to one organisation. In that\n * suffix-mode we also cross in-name prepositions\n * (\"Bank of America and Trust Company, Inc.\").\n */\nconst extendBackward = (\n fullText: string,\n matchStart: number,\n options: { forceSuffixMode?: boolean } = {},\n): number => {\n // Read the first word of the match to decide whether\n // we're inside a multi-word organisation name. Callers\n // that enter the walk from a known legal-form suffix\n // (Inc., Ltd., etc.) can pass `forceSuffixMode: true`\n // to enable in-name preposition crossing (\"Bank of\n // America Inc.\") without having to widen\n // COMPANY_SUFFIX_WORDS_RE to every legal-form suffix.\n const headWord =\n ENTITY_HEAD_WORD_RE.exec(fullText.slice(matchStart))?.[0] ?? \"\";\n const suffixMode =\n options.forceSuffixMode === true || COMPANY_SUFFIX_WORDS_RE.test(headWord);\n\n let pos = matchStart;\n\n while (pos > 0) {\n const found = findWordBefore(fullText, pos);\n if (!found) break;\n\n const { word, start: wordStart } = found;\n\n const isUpper = UPPER_LETTER_RE.test(word);\n const isConnector = CONNECTOR_RE.test(word);\n const isInNamePrep = suffixMode && IN_NAME_PREPOSITION_RE.test(word);\n\n if (isUpper) {\n // Uppercase word — always accept\n pos = wordStart;\n } else if (isConnector) {\n if (AND_TYPE_CONNECTOR_RE.test(word)) {\n // Decide whether the \"and\" sits inside one\n // organisation name or between two sentence\n // tokens. Heuristics, applied in order:\n //\n // 1. If the word immediately before the \"and\"\n // is itself a known legal-form suffix\n // (\"Morgan Securities LLC and Allen &\n // Company LLC\", \"Apple, Inc. and Microsoft\n // Corp.\"), the \"and\" separates two orgs —\n // break regardless of mode.\n // 2. A single uppercase word before the \"and\"\n // is almost always a defined-term clause\n // noun (\"the Company and Barclays Bank\n // PLC\", \"Company and Bank of America,\n // N.A.\") rather than part of the org\n // name — break regardless of mode.\n // 3. Outside suffix mode, also break on two\n // upper words (typical person-name\n // pattern: \"Paul Newman and Apple, Inc.\").\n // Three or more upper words signals a\n // real multi-word organisation name\n // (\"UniCredit Bank Czech Republic and\n // Slovakia, a.s.\"), so the walk crosses.\n // 4. In suffix mode the regex already\n // captured a leading legal-form suffix\n // word (\"…and Company, Inc.\"), so any\n // multi-word prefix should flow through.\n const prev = findWordBefore(fullText, wordStart);\n if (!prev) break;\n if (!UPPER_LETTER_RE.test(prev.word)) break;\n if (isKnownLegalFormSuffix(prev.word)) break;\n const upperWordsBefore = countUpperWordsBefore(\n fullText,\n wordStart,\n suffixMode,\n );\n const middleInitialBefore = hasMiddleInitialBefore(fullText, wordStart);\n if (\n upperWordsBefore <= 1 &&\n (getClauseNounHeadsSync().has(prev.word.toLowerCase()) ||\n getConnectorProseHeadsSync().has(prev.word.toLowerCase()))\n ) {\n break;\n }\n const personNameBoundary = suffixMode\n ? middleInitialBefore &&\n hasSingleCapPrefixBefore(fullText, matchStart)\n : (upperWordsBefore === 2 && !isInNameLegalFormWord(prev.word)) ||\n middleInitialBefore;\n if (personNameBoundary) {\n break;\n }\n pos = prev.start;\n } else {\n // Non-\"and\" connector (`&`, `e`, `y`, `i`,\n // standalone `a`). Cross when there's a valid\n // uppercase-starting word before it.\n const prev = findWordBefore(fullText, wordStart);\n if (!prev) break;\n const prevIsUpper = UPPER_LETTER_RE.test(prev.word);\n if (!prevIsUpper) break;\n pos = prev.start;\n }\n } else if (isInNamePrep) {\n // In suffix-mode only: cross lowercase in-name\n // prepositions (\"of\", \"the\") when the preceding\n // token is uppercase (\"Bank of America\").\n const prev = findWordBefore(fullText, wordStart);\n if (!prev) break;\n if (!UPPER_LETTER_RE.test(prev.word)) break;\n pos = prev.start;\n } else {\n break;\n }\n }\n\n // After the word walk finishes, absorb any leading\n // initial-dot run that the letter-based word scan\n // skipped over (\"J.P. Morgan Securities LLC\",\n // \"U.S. Bancorp Inc.\"). The walk above stops at the\n // dot because `findWordBefore` only consumes letters,\n // so without this step the entity start lands on the\n // first non-initial word.\n pos = skipInitialsBackward(fullText, pos);\n\n return pos;\n};\n\nconst trimLeadingClause = (text: string): { offset: number; text: string } => {\n let cut = -1;\n const trims = getLeadingClauseTrimsSync();\n const phraseAlternation = trims.phrases.map(escapeForRegex).join(\"|\");\n if (phraseAlternation.length > 0) {\n const phraseRe = new RegExp(\n `(?:^|\\\\s)(?:${phraseAlternation})${HSPACE}+`,\n \"giu\",\n );\n for (const match of text.matchAll(phraseRe)) {\n cut = Math.max(cut, match.index + match[0].length);\n }\n }\n\n const directPrefixAlternation = trims.directPrefixes\n .map(escapeForRegex)\n .join(\"|\");\n // \"among\" / \"amongst\" / \"between\" can legitimately\n // appear inside a title-case org name (\"Food For\n // Thought Among Friends LLC\", \"The Space In Between\n // LLC\"). For those prefixes we require a clause\n // separator (comma) immediately before the prefix,\n // which is the structural signal that distinguishes\n // a connector (\"Investment Agreement, dated as of\n // March 9, 2020, among Twitter, Inc.\") from a name\n // component.\n const COMMA_GATED_DIRECT_PREFIXES: ReadonlySet<string> = new Set([\n \"among\",\n \"amongst\",\n \"between\",\n ]);\n const verbIndicators = getSentenceVerbIndicatorsSync();\n if (directPrefixAlternation.length > 0) {\n const directPrefixRe = new RegExp(\n `\\\\b(?:${directPrefixAlternation})${HSPACE}+(?=\\\\p{Lu})`,\n \"giu\",\n );\n for (const match of text.matchAll(directPrefixRe)) {\n const matchedPrefix = match[0].trim().toLowerCase();\n const before = text.slice(0, match.index);\n if (COMMA_GATED_DIRECT_PREFIXES.has(matchedPrefix)) {\n // The comma gate distinguishes the connector use\n // (\"Investment Agreement, dated as of …, among Twitter,\n // Inc.\") from an in-name word (\"Food For Thought Among\n // Friends LLC\"). Lift the gate when a lowercase sentence-verb\n // token appears in the preceding text (\"This Agreement is\n // entered into between Acme Inc.\") — the verb is the\n // structural cue that this is clause prose, not a name\n // component. Title-case \"Is\" / \"Between\" inside a company\n // name stays protected because only lowercase forms count.\n const hasComma = /,\\s*$/u.test(before);\n const beforeWords = before.match(/\\p{L}[\\p{L}\\p{M}'’]*/gu) ?? [];\n const hasSentenceVerb = beforeWords.some(\n (word) =>\n /^\\p{Ll}/u.test(word) && verbIndicators.has(word.toLowerCase()),\n );\n if (!hasComma && !hasSentenceVerb) {\n continue;\n }\n }\n const words = before.match(/\\p{L}[\\p{L}\\p{M}'’]*/gu) ?? [];\n const hasProsePrefix =\n words.length >= 3 && words.some((word) => /\\p{Ll}/u.test(word));\n if (hasProsePrefix) {\n cut = Math.max(cut, match.index + match[0].length);\n }\n }\n }\n for (const match of text.matchAll(/,/gu)) {\n const comma = match.index;\n if (comma === undefined) {\n continue;\n }\n const before = text.slice(0, comma);\n if (!/\\d/u.test(before)) {\n continue;\n }\n const after = text.slice(comma + 1);\n const leadingWs = after.match(/^\\s*/u)?.[0].length ?? 0;\n const candidate = after.slice(leadingWs);\n const upperWords = candidate.match(/\\p{Lu}[\\p{L}\\p{M}\\p{N}]*/gu) ?? [];\n if (upperWords.length >= 3) {\n cut = Math.max(cut, comma + 1 + leadingWs);\n }\n }\n\n if (cut <= 0) {\n return { offset: 0, text };\n }\n\n const trimmed = text.slice(cut);\n const leadingWs = trimmed.match(/^\\s*/u)?.[0].length ?? 0;\n\n return {\n offset: cut + leadingWs,\n text: trimmed.slice(leadingWs),\n };\n};\n\n// ── Match processor ─────────────────────────────────\n\n/**\n * Process legal form matches from the unified search.\n * Receives all matches; filters to the legal forms\n * slice via sliceStart/sliceEnd.\n *\n * The role-head trimming step reads per-language data from\n * a cache that `runPipeline` warms via `warmLegalRoleHeads()`\n * before calling this. Callers that invoke\n * `processLegalFormMatches` directly (without going through\n * `runPipeline`) must `await warmLegalRoleHeads()` first;\n * otherwise the trim falls back to a no-op and sentence-\n * fragment fixes do not apply.\n */\nexport const processLegalFormMatches = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText?: string,\n options: { suppressExtendBackward?: boolean } = {},\n): Entity[] => {\n const results: Entity[] = [];\n\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) {\n continue;\n }\n\n const text = match.text.trimEnd();\n if (text.length < 5) {\n continue;\n }\n\n // Trim spans whose first word is a generic legal/contract\n // role IF the match also contains a sentence-verb signal\n // (\"owns\", \"je vlastníkem\", \"grants\") between the role head\n // and the trailing legal-form suffix. Without that strong\n // signal we keep the match intact — role words are also\n // legitimate components of organisation names (\"Client\n // Solutions Inc.\", \"Client solutions Inc.\", \"Vendor s.r.o.\",\n // \"Vendor consulting Ltd.\"). When the signal is present\n // we slice the match at the first uppercase-starting word\n // that follows the last sentence-verb (and skip any role-\n // head word that lands at the new start), so multi-word\n // names (\"Acme Holdings s.r.o.\"), in-name prepositions\n // (\"Bank of America Inc.\"), lowercase-tail Czech state\n // forms (\"Národní agentura pro komunikační a informační\n // technologie, s. p.\"), and multi-token legal suffixes\n // (\"spol. s r.o.\") all survive the trim.\n const roleHeads = getLegalRoleHeadsSync();\n // Match the first token. Hyphenated forms (\"Sous-traitant\",\n // \"co-contractant\") are valid role heads in some languages,\n // so consume any internal `-letter` runs alongside the\n // letter-only head. The lookup uses the full hyphenated form\n // first, then falls back to just the leading letter run when\n // the role-head set lists only the bare prefix.\n const firstWordMatch = /^[\\p{L}\\p{M}]+(?:-[\\p{L}\\p{M}]+)*/u.exec(text);\n let processedStart = match.start;\n let processedText = text;\n if (\n isStructuralSingleCapMatch(processedText) ||\n (fullText !== undefined &&\n isBareSingleCapStructuralInnerMatch(fullText, match.start, text))\n ) {\n continue;\n }\n // True when the role-head trim slices the match. The\n // subsequent extendBackward step is suppressed in that case\n // — extending back would re-absorb the very prose the trim\n // just removed (e.g. \"Vendor grants Licensee Acme Inc.\" →\n // trim to \"Acme Inc.\" → extendBackward walks back across\n // \"Licensee\" again and emits \"Licensee Acme Inc.\").\n let trimmed = false;\n const firstWordText = firstWordMatch?.[0] ?? \"\";\n const firstWordLeading = /^[\\p{L}\\p{M}]+/u.exec(firstWordText)?.[0] ?? \"\";\n const isRoleHead =\n firstWordMatch !== null &&\n (roleHeads.has(firstWordText.toLowerCase()) ||\n (firstWordLeading.length > 0 &&\n roleHeads.has(firstWordLeading.toLowerCase())));\n if (isRoleHead) {\n // Find the legal-form suffix's position inside `text` by\n // scanning the full legal-form vocabulary (loaded from\n // `data/legal-forms.json` in `warmLegalRoleHeads`-style\n // fashion). Sorted longest-first so multi-token suffixes\n // (\"spol. s r.o.\", \"akciová společnost\") anchor before\n // shorter nested forms (\"s.r.o.\", \"společnost\").\n let suffixOffset = -1;\n for (const suffix of getAllLegalSuffixesSync()) {\n const suffixIdx = text.lastIndexOf(suffix);\n if (suffixIdx !== -1 && suffixIdx + suffix.length >= text.length - 1) {\n suffixOffset = suffixIdx;\n break;\n }\n }\n if (suffixOffset < 0) {\n // Couldn't locate the suffix; fall through without\n // trimming. The greedy regex will still produce the\n // match — better some highlight than none.\n } else {\n // Scan the middle (between the role-head and the legal-\n // form suffix) for a sentence-verb token. Position of\n // the LAST verb determines where the org name starts.\n const midStart = firstWordMatch[0].length;\n const midEnd = suffixOffset;\n const midSection = text.slice(midStart, midEnd);\n const verbIndicators = getSentenceVerbIndicatorsSync();\n let lastVerbEndInMid = -1;\n for (const wordMatch of midSection.matchAll(\n // Match any word (capital or lowercase start); the\n // verb-indicator set lookup is lowercased so e.g.\n // title-cased \"Owns\" in \"Vendor Owns Acme Inc.\"\n // still counts as a sentence verb.\n /(?<![\\p{L}\\p{N}])[\\p{L}\\p{M}]+/gu,\n )) {\n if (\n wordMatch[0] !== undefined &&\n wordMatch.index !== undefined &&\n verbIndicators.has(wordMatch[0].toLowerCase())\n ) {\n lastVerbEndInMid = wordMatch.index + wordMatch[0].length;\n }\n }\n // Also treat a digit immediately after the role-head\n // (\"Vendor 1\", \"Prodávající 2\") as a sentence signal.\n // Numbered party references rarely appear in company\n // names but always appear in clause text.\n const digitAfterRole = /^\\s+\\d+(?:\\.|\\b)/u.test(midSection);\n // Appositive role-head detection: when the legal-form\n // regex matched a span starting at a role-head (\"Licensee\n // Acme Inc.\") but there's no verb in the matched mid\n // section, look at the preceding word in fullText. If\n // that word is a sentence verb (\"Vendor grants Licensee\n // Acme Inc.\"), the role-head is appositive prose and\n // should be skipped just like an in-match role token.\n let appositiveRoleHead = false;\n if (!digitAfterRole && lastVerbEndInMid === -1 && fullText) {\n const before = fullText.slice(\n Math.max(0, match.start - 40),\n match.start,\n );\n const prevWord = /(?<![\\p{L}\\p{N}])(\\p{L}[\\p{L}\\p{M}]*)\\s*$/u.exec(\n before,\n );\n if (\n prevWord !== null &&\n getSentenceVerbIndicatorsSync().has(prevWord[1]!.toLowerCase())\n ) {\n appositiveRoleHead = true;\n }\n }\n if (lastVerbEndInMid !== -1 || digitAfterRole || appositiveRoleHead) {\n // Pick the first Cap-starting word in `text` after\n // the last verb (or, if only a digit signal fired,\n // after the role-head itself). Skip role-heads\n // (\"Vendor grants Licensee Acme Inc.\") and clause\n // nouns (\"Vendor signed Agreement with Acme Inc.\")\n // so the anchor lands on the real company name.\n // When trim was triggered by an appositive role-head\n // (no in-match verb), the role-head itself is the\n // thing to skip — scan starts from after the role\n // head's first word.\n const scanStart =\n lastVerbEndInMid !== -1 ? midStart + lastVerbEndInMid : midStart;\n const capRe = /(?<![\\p{L}\\p{N}])\\p{Lu}[\\p{L}\\p{M}\\p{N}]*/gu;\n capRe.lastIndex = scanStart;\n const clauseNouns = getClauseNounHeadsSync();\n let capMatch: RegExpExecArray | null = null;\n for (\n let next = capRe.exec(text);\n next !== null;\n next = capRe.exec(text)\n ) {\n if (next.index >= suffixOffset) {\n break;\n }\n const lc = next[0].toLowerCase();\n if (roleHeads.has(lc) || clauseNouns.has(lc)) {\n continue;\n }\n capMatch = next;\n break;\n }\n if (capMatch === null) {\n // No real cap-word before the suffix; drop.\n continue;\n }\n processedStart = match.start + capMatch.index;\n processedText = text.slice(capMatch.index);\n trimmed = true;\n }\n }\n }\n\n if (processedText.includes(\"\\n\") && hasDisallowedLineBreak(processedText)) {\n continue;\n }\n\n // Extend backward through connectors if fullText\n // is available (captures \"Be a Future\" from just\n // \"Future s.r.o.\")\n let entityStart = processedStart;\n let entityText = processedText;\n if (fullText && !trimmed && options.suppressExtendBackward !== true) {\n const shouldExtendBackward =\n !BARE_SINGLE_CAP_LEGAL_FORM_RE.test(processedText);\n const extended = shouldExtendBackward\n ? extendBackward(fullText, processedStart)\n : processedStart;\n if (extended < processedStart) {\n entityStart = extended;\n entityText = fullText\n .slice(extended, processedStart + processedText.length)\n .trimEnd();\n }\n }\n\n for (const segment of splitEmbeddedLegalFormList(entityStart, entityText)) {\n entityStart = segment.entityStart;\n entityText = segment.entityText;\n\n const listTrim = trimEmbeddedLegalFormListPrefix(entityStart, entityText);\n entityStart = listTrim.entityStart;\n entityText = listTrim.entityText;\n\n const clauseTrim = trimLeadingClause(entityText);\n if (clauseTrim.offset > 0) {\n entityStart += clauseTrim.offset;\n entityText = clauseTrim.text;\n }\n\n if (entityText.includes(\"\\n\") && hasDisallowedLineBreak(entityText)) {\n continue;\n }\n\n // Reject all-caps matches only if the entire\n // surrounding line is all-caps (section headings\n // like \"KUPNÍ SMLOUVA\"). If only the company name\n // is all-caps (\"uzavřená s EAGLES BRNO, z.s.\"),\n // keep it — max 3 all-caps words are allowed.\n const getPrefixInfo = (value: string) => {\n const prefixEnd =\n value.lastIndexOf(\",\") !== -1\n ? value.lastIndexOf(\",\")\n : value.lastIndexOf(\" \");\n const prefixPart =\n prefixEnd > 0\n ? value.slice(0, prefixEnd).replace(/[^a-zA-ZÀ-ž]/g, \"\")\n : value.replace(/[^a-zA-ZÀ-ž]/g, \"\");\n return { prefixEnd, prefixPart };\n };\n let { prefixEnd, prefixPart } = getPrefixInfo(entityText);\n let isAllCapsMatch =\n prefixPart.length > 2 && prefixPart === prefixPart.toUpperCase();\n\n if (isAllCapsMatch && fullText) {\n // Boilerplate-vs-caption discrimination is\n // handled centrally by isAllCapsBoilerplateLine\n // in filters/false-positives.ts; we no longer\n // duplicate it here. Keep the local guard that\n // restores the original regex match if backward\n // extension swept too many cap words into the\n // prefix — the centralised post-filter still\n // sees that restored span.\n const wordCount =\n prefixPart.length > 0\n ? entityText\n .slice(0, prefixEnd > 0 ? prefixEnd : entityText.length)\n .trim()\n .split(/\\s+/).length\n : 0;\n if (wordCount > 3) {\n entityStart = match.start;\n entityText = text;\n }\n } else if (isAllCapsMatch) {\n // No fullText available — fall back to rejecting.\n continue;\n }\n\n // Reject Roman numeral suffixes\n const lastSuffixSeparator = findLastSuffixSeparator(entityText);\n const rawSuffix =\n lastSuffixSeparator !== -1\n ? entityText.slice(lastSuffixSeparator + 1)\n : \"\";\n const suffixClean = rawSuffix.replace(/[.,]/g, \"\");\n if (suffixClean.length > 0 && ROMAN_NUMERAL_RE.test(suffixClean)) {\n continue;\n }\n\n // Short ASCII-only suffixes (NA, PA, LP, PC) are\n // US-specific. Reject if the prefix contains non-\n // ASCII chars (Czech/Slovak diacritics) — a US\n // legal entity wouldn't have \"ÚČASTI MSP NA\".\n // Test for dots in the raw suffix (before dot\n // stripping) to protect Czech dotted forms like\n // \"a.s.\" and \"k.s.\".\n if (\n suffixClean.length <= 2 &&\n !/\\./.test(rawSuffix) &&\n /[^\\p{ASCII}]/u.test(\n stripDocxSpaces(\n entityText.slice(\n 0,\n lastSuffixSeparator !== -1\n ? lastSuffixSeparator\n : entityText.length,\n ),\n ),\n )\n ) {\n continue;\n }\n\n // Definitive legal forms (s.r.o., a.s., GmbH, etc.)\n // get score 0.95 to beat person names in dedup.\n results.push({\n start: entityStart,\n end: entityStart + entityText.length,\n label: \"organization\",\n text: entityText,\n score: 0.95,\n source: DETECTION_SOURCES.LEGAL_FORM,\n });\n }\n }\n\n return results;\n};\n","import { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\nimport type { DefinitionPattern, PipelineContext } from \"../context\";\nimport { defaultContext } from \"../context\";\nimport { loadLanguageConfigs } from \"../util/lang-loader\";\nimport { getKnownLegalSuffixes } from \"./legal-forms\";\n\n/**\n * Case-insensitive set of every known legal-form suffix\n * with whitespace normalized away. Used by the\n * coreference scan to reject parenthetical define-term\n * captures whose alias is only a legal-form\n * abbreviation (e.g. \"(« SAS »)\" after \"ACME SAS\"),\n * which would otherwise propagate generic suffix\n * mentions as document-wide org aliases.\n */\nconst isLegalFormAlias = (alias: string): boolean => {\n const normalized = alias.replace(/\\s+/g, \"\").toLowerCase();\n if (normalized.length === 0) {\n return false;\n }\n for (const suffix of getKnownLegalSuffixes()) {\n if (suffix.replace(/\\s+/g, \"\").toLowerCase() === normalized) {\n return true;\n }\n }\n return false;\n};\n\ntype CoreferenceConfigRow = {\n pattern: string;\n flags: string;\n label: string;\n};\n\n/**\n * Load coreference definition patterns from per-language\n * JSON configs in src/data/. Uses the\n * language manifest for auto-discovery.\n */\nconst loadDefinitionPatterns = async (): Promise<DefinitionPattern[]> => {\n const patterns: DefinitionPattern[] = [];\n\n const allRows = await loadLanguageConfigs<readonly CoreferenceConfigRow[]>(\n \"coreference\",\n (mod) => {\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config\n const m = mod as {\n default?: readonly CoreferenceConfigRow[];\n };\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config\n return (m.default ?? mod) as readonly CoreferenceConfigRow[];\n },\n );\n\n for (const rows of allRows) {\n if (!Array.isArray(rows)) {\n console.warn(\n \"[anonymize] coreference: unexpected \" + \"config shape, skipping\",\n );\n continue;\n }\n for (const row of rows) {\n try {\n patterns.push({\n pattern: new RegExp(row.pattern, row.flags),\n });\n } catch (err) {\n console.warn(\n `[anonymize] coreference: invalid ` + `regex \"${row.pattern}\":`,\n err,\n );\n }\n }\n }\n\n return patterns;\n};\n\n/**\n * Load generic role terms that should NOT be treated\n * as PII coreferences. \"Prodávající\" (Seller),\n * \"Kupující\" (Buyer), etc. are legal roles, not\n * identifying information.\n */\nconst getRoleStopSet = async (\n ctx: PipelineContext,\n): Promise<ReadonlySet<string>> => {\n if (ctx.roleStopSet) return ctx.roleStopSet;\n if (ctx.roleStopSetPromise) return ctx.roleStopSetPromise;\n const promise = (async () => {\n let result: ReadonlySet<string>;\n try {\n const mod = await import(\"../data/generic-roles.json\");\n const data = (mod.default ?? mod) as {\n roles: string[];\n };\n result = new Set(data.roles.map((r: string) => r.toLowerCase()));\n } catch {\n result = new Set();\n }\n ctx.roleStopSet = result;\n return result;\n })();\n ctx.roleStopSetPromise = promise;\n return promise;\n};\n\nconst getDefinitionPatterns = async (\n ctx: PipelineContext,\n): Promise<DefinitionPattern[]> => {\n if (ctx.corefPatterns) {\n return ctx.corefPatterns;\n }\n if (ctx.corefPatternsPromise) {\n return ctx.corefPatternsPromise;\n }\n ctx.corefPatternsPromise = loadDefinitionPatterns();\n const patterns = await ctx.corefPatternsPromise;\n if (patterns.length === 0) {\n // All loads failed; cache empty array permanently\n // to avoid retrying dynamic imports and flooding\n // logs on every call in high-volume pipelines.\n ctx.corefPatterns = patterns;\n if (!ctx.corefLoadAttempted) {\n ctx.corefLoadAttempted = true;\n console.warn(\n \"[anonymize] coreference: no definition \" +\n \"patterns loaded; coreference detection \" +\n \"will be inactive\",\n );\n }\n return patterns;\n }\n ctx.corefPatterns = patterns;\n return patterns;\n};\n\nconst SEARCH_WINDOW = 200;\n\n/**\n * Check whether an alias has textual similarity to\n * the source entity. Prevents roles and structural\n * terms from being treated as name aliases.\n *\n * Three checks (any passes → similar):\n * 1. Word overlap: a word in the alias appears in the\n * entity (case-insensitive, min 2 chars)\n * 2. Initials: alias letters match first letters of\n * entity words (\"TB\" ↔ \"Tomas Bata\")\n * 3. Substring: alias is a substring of the entity\n * or vice versa (min 3 chars)\n */\nconst hasEntitySimilarity = (alias: string, entityText: string): boolean => {\n const aliasLower = alias.toLowerCase();\n const entityLower = entityText.toLowerCase();\n\n // Substring check (min 3 chars to avoid noise)\n if (aliasLower.length >= 3 && entityLower.includes(aliasLower)) {\n return true;\n }\n if (entityLower.length >= 3 && aliasLower.includes(entityLower)) {\n return true;\n }\n\n // Word overlap: split both into words, check for\n // any shared word of 2+ characters\n const aliasWords = aliasLower\n .split(/[\\s.,;:'\"()/-]+/)\n .filter((w) => w.length >= 2);\n const entityWords = entityLower\n .split(/[\\s.,;:'\"()/-]+/)\n .filter((w) => w.length >= 2);\n const entityWordSet = new Set(entityWords);\n for (const word of aliasWords) {\n if (entityWordSet.has(word)) {\n return true;\n }\n }\n\n // Initials: alias is all uppercase and each letter\n // matches the first letter of a consecutive run of\n // entity words. \"TP\" ↔ \"Ing. Tomáš Procházka\"\n // (skips \"Ing.\" and matches T+P). Check all\n // starting positions to handle title prefixes.\n if (\n /^[\\p{Lu}]+$/u.test(alias) &&\n alias.length >= 2 &&\n alias.length <= entityWords.length\n ) {\n for (let start = 0; start <= entityWords.length - alias.length; start++) {\n const initials = entityWords\n .slice(start, start + alias.length)\n .map((w) => w.charAt(0))\n .join(\"\");\n if (initials === aliasLower) {\n return true;\n }\n }\n }\n\n return false;\n};\n\ntype DefinedTerm = {\n alias: string;\n label: string;\n /** Position of the definition in the source text */\n definitionStart: number;\n /** Original entity text the alias refers to */\n sourceText: string;\n};\n\n/**\n * Scan for defined-term patterns near known entities.\n *\n * Legal documents universally follow:\n * \"Dr. Heinrich Muller (hereinafter 'the Seller')...\"\n *\n * After NER detects the entity, this function scans for\n * definitional patterns within +/-200 chars and extracts\n * the alias. Returns alias + label pairs that can be added\n * to the gazetteer for a full-text re-scan.\n */\n/**\n * Labels that can be the source of a coreference alias.\n * Only parties (person, organization) have defined-term\n * aliases in legal text. Dates, addresses, IDs do not.\n */\nconst COREF_SOURCE_LABELS = new Set([\"person\", \"organization\"]);\n\nexport const extractDefinedTerms = async (\n fullText: string,\n entities: Entity[],\n ctx: PipelineContext = defaultContext,\n): Promise<DefinedTerm[]> => {\n const [definitionPatterns, roleStops] = await Promise.all([\n getDefinitionPatterns(ctx),\n getRoleStopSet(ctx),\n ]);\n const terms: DefinedTerm[] = [];\n const seen = new Set<string>();\n\n // Sort entities by position for nearest-preceding lookup\n const sorted = [...entities].sort((a, b) => a.start - b.start);\n\n // Strategy: find all \"dále jen\" definitions in the\n // text, then for each one, find the nearest PRECEDING\n // person/organization entity. This is more accurate\n // than \"any entity within 200 chars\" because:\n // - The alias always refers to a party defined BEFORE\n // the parenthetical, not after\n // - Multiple entities (name, IČO, address) may appear\n // between the party name and the \"dále jen\"; only\n // the party name is the referent\n\n for (const { pattern } of definitionPatterns) {\n pattern.lastIndex = 0;\n\n for (\n let match = pattern.exec(fullText);\n match !== null;\n match = pattern.exec(fullText)\n ) {\n const alias = match[1]?.trim();\n if (!alias || alias.length < 2) {\n continue;\n }\n\n // Skip generic legal roles — \"Prodávající\",\n // \"Kupující\", \"Seller\", etc. are NOT PII.\n // Only track aliases that are themselves\n // identifying (initials, name fragments, etc.)\n if (roleStops.has(alias.toLowerCase())) {\n continue;\n }\n\n // Skip aliases that are nothing but a legal-form\n // abbreviation (\"SAS\", \"SARL\", \"S.A.\", \"GmbH\").\n // A parenthetical like `ACME SAS (« SAS »)` would\n // otherwise pass the substring-similarity check\n // against \"ACME SAS\" and cause every standalone\n // \"SAS\" mention in the document to be redacted as\n // an organization alias.\n if (isLegalFormAlias(alias)) {\n continue;\n }\n\n const defPos = match.index;\n\n // Find the nearest preceding person/org entity\n // within SEARCH_WINDOW chars before this definition\n let bestEntity: Entity | null = null;\n for (let i = sorted.length - 1; i >= 0; i--) {\n const e = sorted[i];\n if (e === undefined) {\n continue;\n }\n // Must be before the definition\n if (e.end > defPos) {\n continue;\n }\n // Must be within the search window\n if (defPos - e.end > SEARCH_WINDOW) {\n break;\n }\n // Must be a party label\n if (!COREF_SOURCE_LABELS.has(e.label)) {\n continue;\n }\n bestEntity = e;\n break;\n }\n\n if (bestEntity === null) {\n continue;\n }\n\n // Clause-boundary gate: reject if a semicolon or\n // sentence-ending period sits between the source\n // entity and the definition. Periods inside\n // abbreviations like \"r.č.\" should not block an\n // otherwise valid definition.\n const gapText = fullText.slice(bestEntity.end, defPos);\n if (/(?:;|\\.(?=\\s*(?:[\"'„‚(]*\\p{Lu}|$)))/u.test(gapText)) {\n continue;\n }\n\n // Similarity gate: the alias must have textual\n // overlap with the source entity. Prevents roles\n // (\"Executive\", \"Seller\") and structural terms\n // (\"Agreement\", \"Effective Date\") from being\n // treated as name aliases.\n if (!hasEntitySimilarity(alias, bestEntity.text)) {\n continue;\n }\n\n const key = `${alias.toLowerCase()}::${bestEntity.label}`;\n if (seen.has(key)) {\n continue;\n }\n seen.add(key);\n\n terms.push({\n alias,\n label: bestEntity.label,\n definitionStart: defPos,\n sourceText: bestEntity.text,\n });\n }\n }\n\n return terms;\n};\n\n/**\n * Check if a character is a Unicode word character\n * (letter, digit, or combining mark). Used for word\n * boundary checks in coreference matching.\n */\nconst isWordChar = (ch: string | undefined): boolean => {\n if (ch === undefined) {\n return false;\n }\n return /[\\p{L}\\p{M}\\p{N}]/u.test(ch);\n};\n\n/**\n * Find all occurrences of defined-term aliases in the\n * full text. Returns Entity spans for each match.\n *\n * Respects word boundaries: \"Kupující\" must not match\n * inside \"Kupujícímu\". A match is valid only if the\n * character before the start and after the end are NOT\n * word characters (letter/digit).\n *\n * Each returned alias carries `corefSourceText` linking\n * it to its source entity text, for consistent\n * placeholder numbering.\n *\n * @param _ctx Unused. Kept for signature compatibility;\n * alias links now travel on the entities themselves.\n */\nexport const findCoreferenceSpans = (\n fullText: string,\n terms: DefinedTerm[],\n _ctx: PipelineContext = defaultContext,\n): Entity[] => {\n const results: Entity[] = [];\n\n for (const term of terms) {\n let searchFrom = 0;\n while (searchFrom < fullText.length) {\n const idx = fullText.indexOf(term.alias, searchFrom);\n if (idx === -1) {\n break;\n }\n\n const matchEnd = idx + term.alias.length;\n\n // Word boundary check: the character before the\n // match and after the match must not be a word\n // character. This prevents \"Kupující\" matching\n // inside \"Kupujícímu\".\n const charBefore = idx > 0 ? fullText[idx - 1] : undefined;\n const charAfter = fullText[matchEnd];\n\n if (isWordChar(charBefore) || isWordChar(charAfter)) {\n // Not at a word boundary — skip\n searchFrom = idx + 1;\n continue;\n }\n\n results.push({\n start: idx,\n end: matchEnd,\n label: term.label,\n text: term.alias,\n score: 0.95,\n source: DETECTION_SOURCES.COREFERENCE,\n corefSourceText: term.sourceText,\n });\n\n searchFrom = matchEnd;\n }\n }\n\n return results;\n};\n","import type { Match, PatternEntry } from \"@stll/text-search\";\n\nimport { DETECTION_SOURCES } from \"../types\";\nimport type { Entity, GazetteerEntry } from \"../types\";\nimport type { GazetteerData } from \"../build-unified-search\";\n\nconst MAX_EDIT_DISTANCE = 2;\nconst MIN_FUZZY_LENGTH = 4;\n// \" s.r.o.\" = 7 bytes (space + 6 chars). Must be\n// at least 7 to capture the most common Czech LLC\n// suffix without truncating the trailing dot.\nconst MAX_PREFIX_OVERSHOOT = 7;\n\n/**\n * Collect all searchable strings (canonical + variants)\n * from gazetteer entries, mapped to their labels and\n * entry IDs.\n */\nexport const buildSearchTerms = (\n entries: GazetteerEntry[],\n): Map<string, { label: string; entryId: string }> => {\n const terms = new Map<string, { label: string; entryId: string }>();\n for (const entry of entries) {\n const meta = {\n label: entry.label,\n entryId: entry.id,\n };\n terms.set(entry.canonical, meta);\n for (const variant of entry.variants) {\n terms.set(variant, meta);\n }\n }\n return terms;\n};\n\n/**\n * Build TextSearch-compatible patterns from gazetteer\n * entries. Returns:\n * - Exact literal patterns for all terms\n * - Fuzzy patterns (distance: 2) for terms >= 4 chars\n * - Parallel metadata arrays for post-processing\n *\n * Patterns are ordered: all exact first, then all\n * fuzzy. The isFuzzy array marks which are which.\n */\nexport const buildGazetteerPatterns = (\n entries: GazetteerEntry[],\n): {\n patterns: PatternEntry[];\n data: GazetteerData;\n} => {\n const terms = buildSearchTerms(entries);\n\n const patterns: PatternEntry[] = [];\n const labels: string[] = [];\n const isFuzzy: boolean[] = [];\n\n // Pass 1: exact literals (all terms).\n // Use per-pattern wholeWords: false because\n // user-supplied entries may contain dots or\n // special chars (e.g. \"a.s.\", \"AT&T\") that\n // break word-boundary matching. Deny-list and\n // street-type patterns use per-pattern\n // wholeWords: true instead.\n for (const [term, meta] of terms) {\n patterns.push({\n pattern: term,\n literal: true as const,\n wholeWords: false,\n });\n labels.push(meta.label);\n\n isFuzzy.push(false);\n }\n\n // Pass 2: fuzzy patterns (terms >= 4 chars).\n // These are separate patterns with distance: 2,\n // routed to @stll/fuzzy-search by TextSearch.\n for (const [term, meta] of terms) {\n if (term.length < MIN_FUZZY_LENGTH) {\n continue;\n }\n patterns.push({\n pattern: term,\n distance: MAX_EDIT_DISTANCE,\n });\n labels.push(meta.label);\n\n isFuzzy.push(true);\n }\n\n return {\n patterns,\n data: { labels, isFuzzy },\n };\n};\n\n/**\n * Process gazetteer matches from the unified literal\n * search. Receives all matches; filters to the\n * gazetteer slice via sliceStart/sliceEnd.\n *\n * Exact matches get score 0.9; fuzzy matches get\n * 0.85. Fuzzy matches that overlap an exact match\n * are dropped.\n *\n * For exact matches, attempts prefix extension for\n * legal suffixes (\"a.s.\", \"GmbH\", \"s.r.o.\" after\n * the matched term).\n */\nexport const processGazetteerMatches = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText: string,\n data: GazetteerData,\n): Entity[] => {\n const results: Entity[] = [];\n // Track exact-match spans for overlap filtering\n const exactSpans: Array<{\n start: number;\n end: number;\n }> = [];\n\n // Pass 1: exact matches (isFuzzy === false)\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) {\n continue;\n }\n const localIdx = idx - sliceStart;\n if (data.isFuzzy[localIdx]) {\n continue;\n }\n\n const label = data.labels[localIdx];\n if (!label) {\n continue;\n }\n\n // Try prefix extension for legal entity suffixes\n const extended = tryPrefixExtension(fullText, match.start, match.end);\n const isExtended = extended !== null;\n const end = extended?.end ?? match.end;\n const text = extended?.text ?? fullText.slice(match.start, match.end);\n\n exactSpans.push({\n start: match.start,\n end,\n });\n const entity: Entity = {\n start: match.start,\n end,\n label,\n text,\n score: 0.9,\n source: DETECTION_SOURCES.GAZETTEER,\n };\n if (isExtended) {\n entity.sourceDetail = \"gazetteer-extension\";\n }\n results.push(entity);\n }\n\n // Pass 2: fuzzy matches (isFuzzy === true),\n // skipping those that overlap exact spans\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) {\n continue;\n }\n const localIdx = idx - sliceStart;\n if (!data.isFuzzy[localIdx]) {\n continue;\n }\n\n // Skip distance-0 fuzzy hits (already caught\n // as exact matches above)\n if (match.distance === 0) {\n continue;\n }\n\n const label = data.labels[localIdx];\n if (!label) {\n continue;\n }\n\n // Skip if overlapping any exact span\n const overlapsExact = exactSpans.some(\n (e) => match.start < e.end && match.end > e.start,\n );\n if (overlapsExact) {\n continue;\n }\n\n const matchText = fullText.slice(match.start, match.end);\n results.push({\n start: match.start,\n end: match.end,\n label,\n text: matchText,\n score: 0.85,\n source: DETECTION_SOURCES.GAZETTEER,\n });\n }\n\n return results;\n};\n\n/**\n * Try to extend an exact match to capture one\n * trailing token (max 6 chars) that may be a legal\n * entity suffix (e.g., \"a.s.\", \"GmbH\", \"s.r.o.\").\n *\n * Does not validate the token against a legal-forms\n * list; false extensions are filtered by mergeAndDedup\n * when a legal-form detector produces a competing\n * entity with the correct span.\n */\nconst tryPrefixExtension = (\n fullText: string,\n start: number,\n end: number,\n): { end: number; text: string } | null => {\n const maxEnd = Math.min(end + MAX_PREFIX_OVERSHOOT, fullText.length);\n if (maxEnd <= end + 1) {\n return null;\n }\n\n const after = fullText.slice(end, maxEnd);\n if (!after.startsWith(\" \")) {\n return null;\n }\n const nextSpace = after.indexOf(\" \", 1);\n const suffixEnd = nextSpace !== -1 ? nextSpace : after.length;\n if (suffixEnd <= 1) {\n return null;\n }\n\n const newEnd = end + suffixEnd;\n return {\n end: newEnd,\n text: fullText.slice(start, newEnd),\n };\n};\n\n// Deprecated exports (kept for API compat).\n","/**\n * Normalize typographic variants for search matching.\n *\n * Legal documents (especially Czech/German) use\n * non-breaking spaces, smart quotes, and en/em dashes\n * that differ from their ASCII equivalents. Since all\n * replacements are same-length (single code unit →\n * single code unit), character offsets remain valid.\n *\n * Lives here (application layer) rather than in the\n * AC library: what to normalize is domain-specific.\n *\n * Uses a char-code lookup (`Map<number, number>`) and\n * `Uint16Array` instead of 7 sequential `replaceAll`\n * calls. For a 50 KB document this eliminates ~350 KB\n * of intermediate string allocations.\n *\n * When no replaceable characters are present (common\n * for plain-text inputs), a fast-path scan returns the\n * original string without any allocation. When special\n * characters exist, the string is scanned twice: once\n * to detect, once to build the replacement array.\n */\n\nconst replacementCode = (code: number): number => {\n switch (code) {\n case 0x00a0:\n case 0x2007:\n case 0x202f:\n return 0x0020;\n case 0x2013:\n case 0x2014:\n return 0x002d;\n case 0x201c:\n case 0x201d:\n return 0x0022;\n default:\n return code;\n }\n};\n\n/** Chunk size for `String.fromCharCode` to avoid\n * hitting the call-stack limit on very large strings. */\nconst CHUNK_SIZE = 8192;\n\nexport const normalizeForSearch = (text: string): string => {\n // Fast path: skip allocation when nothing to replace.\n let hasSpecial = false;\n for (let i = 0; i < text.length; i++) {\n const code = text.charCodeAt(i);\n if (replacementCode(code) !== code) {\n hasSpecial = true;\n break;\n }\n }\n if (!hasSpecial) return text;\n\n // Second pass: build output via Uint16Array.\n const len = text.length;\n const codes = new Uint16Array(len);\n for (let i = 0; i < len; i++) {\n const code = text.charCodeAt(i);\n codes[i] = replacementCode(code);\n }\n\n // Convert in chunks to avoid stack overflow on\n // spread of large arrays.\n if (len <= CHUNK_SIZE) {\n return String.fromCharCode(...codes);\n }\n\n let result = \"\";\n for (let offset = 0; offset < len; offset += CHUNK_SIZE) {\n const end = Math.min(offset + CHUNK_SIZE, len);\n result += String.fromCharCode(...codes.subarray(offset, end));\n }\n return result;\n};\n","","import type { Match, PatternEntry } from \"@stll/text-search\";\n\nimport { DETECTION_SOURCES } from \"../constants\";\nimport type { Entity } from \"../types\";\nimport { normalizeForSearch } from \"../util/normalize\";\n\nimport countriesData from \"../data/countries.json\" with { type: \"json\" };\n\nconst ENTITY_LABEL = \"country\";\nconst EXACT_SCORE = 0.95;\n\n/**\n * Whether to surface bare ISO 3166-1 alpha-2 codes\n * (\"US\", \"GB\", \"IT\") as country matches. Off by default\n * because two-letter sequences collide constantly with\n * English/legal prose (\"IT department\", \"OR\", pronouns).\n */\nconst INCLUDE_ALPHA2 = false;\n\n/**\n * CLDR canonical names that collide with common\n * English/global words. Filtered before registration so\n * they never become whole-word case-insensitive country\n * patterns. Examples: \"Island\" is Icelandic for Iceland\n * (also Da/No/Sv/Fi); \"Man\" is Norwegian for Isle of Man;\n * \"Indie\" is the Czech and Polish form of India and\n * collides with the English adjective (\"indie developer\").\n * All would flag every English occurrence as a country.\n */\nconst NAME_BLOCKLIST: ReadonlySet<string> = new Set(\n [\"man\", \"island\", \"indie\"].map((s) => s.toLowerCase()),\n);\n\n/**\n * Whether to surface ISO 3166-1 alpha-3 codes (\"USA\",\n * \"GBR\", \"JPN\") as country matches. Off by default\n * because alpha-3 codes collide case-insensitively with\n * common English/Spanish words: AND (Andorra), ARE (UAE),\n * CAN (Canada), PER (Peru), VAT (Vatican), COG, DOM,\n * FIN, GIN, LIE, MAR, NAM, PAN, TON, etc. A\n * case-sensitive matcher would solve this but the unified\n * literal search runs case-insensitive. Common alpha-3\n * forms (\"USA\") are covered by the curated aliases list\n * instead.\n */\nconst INCLUDE_ALPHA3 = false;\n\n/**\n * Pre-built country patterns + parallel label/source\n * metadata. Constructed once and reused across pipeline\n * runs.\n */\nexport type CountryData = {\n /** Maps local pattern index to entity label. Always \"country\". */\n labels: string[];\n /**\n * Maps local pattern index to the alpha-2 ISO code the\n * pattern resolves to. Used for downstream coreference /\n * placeholder grouping.\n */\n isoCodes: string[];\n /** Maps local pattern index to pattern variant kind. */\n variants: CountryVariant[];\n};\n\nexport type CountryVariant = \"name\" | \"alias\" | \"alpha3\" | \"alpha2\";\n\ntype RawCountryData = {\n codes: { alpha2: string[]; alpha3: string[] };\n names: Record<string, Record<string, string>>;\n aliases: Record<string, string[]>;\n};\n\ntype CountryPatterns = {\n patterns: PatternEntry[];\n data: CountryData;\n};\n\nlet cachedCountryPatterns: CountryPatterns | null = null;\n\n/**\n * Build country patterns for the literal search instance.\n * Returns patterns and parallel metadata arrays.\n *\n * Patterns are literal, case-insensitive, whole-word.\n * Each unique surface form appears once, keyed by its\n * resolved alpha-2 code so duplicate surface forms (e.g.,\n * \"Georgia\" the US state vs. the country) collapse to the\n * country entry.\n */\nexport const buildCountryPatterns = (): {\n patterns: PatternEntry[];\n data: CountryData;\n} => {\n if (cachedCountryPatterns !== null) {\n return cachedCountryPatterns;\n }\n\n const raw = countriesData as RawCountryData;\n\n // Map surface form (lowercased) → { isoCode, variant }.\n // First writer wins so name beats alias beats alpha3.\n const surfaceToMeta = new Map<\n string,\n { display: string; isoCode: string; variant: CountryVariant }\n >();\n\n const register = (\n surface: string,\n isoCode: string,\n variant: CountryVariant,\n ) => {\n const trimmed = surface.trim();\n if (trimmed.length === 0) return;\n // The unified literal search runs against\n // `normalizeForSearch(fullText)`, which rewrites NBSP / smart\n // quotes / en–em-dashes to their ASCII equivalents. CLDR\n // ships names with en-dashes (\"Kongo – Kinshasa\", \"Hongkong –\n // ZAO Číny\") and smart apostrophes; without the same\n // normalization on the pattern side those names would never\n // match real input. Replacements are same-length, so match\n // offsets in the original text stay valid.\n const normalized = normalizeForSearch(trimmed);\n const key = normalized.toLowerCase();\n if (NAME_BLOCKLIST.has(key)) return;\n if (!surfaceToMeta.has(key)) {\n surfaceToMeta.set(key, { display: normalized, isoCode, variant });\n }\n // Typographic-apostrophe variants. CLDR ships only the\n // curly form (\"Côte d’Ivoire\"); legal/OCR text routinely\n // uses the straight one (\"Côte d'Ivoire\"). Register both\n // so either renders match.\n if (trimmed.includes(\"’\") || trimmed.includes(\"‘\")) {\n const straight = normalizeForSearch(trimmed.replaceAll(/[‘’]/g, \"'\"));\n const straightKey = straight.toLowerCase();\n if (!surfaceToMeta.has(straightKey)) {\n surfaceToMeta.set(straightKey, {\n display: straight,\n isoCode,\n variant,\n });\n }\n }\n };\n\n // Canonical names per language, keyed by alpha-2 code.\n for (const perLang of Object.values(raw.names)) {\n for (const [code, name] of Object.entries(perLang)) {\n register(name, code, \"name\");\n }\n }\n\n // Curated aliases.\n for (const [isoCode, aliases] of Object.entries(raw.aliases)) {\n for (const alias of aliases) {\n register(alias, isoCode, \"alias\");\n }\n }\n\n // Alpha-3 (opt-in only; too many case-insensitive\n // collisions with common words — see INCLUDE_ALPHA3).\n if (INCLUDE_ALPHA3) {\n for (let i = 0; i < raw.codes.alpha2.length; i++) {\n const a2 = raw.codes.alpha2[i];\n const a3 = raw.codes.alpha3[i];\n if (a2 && a3) register(a3, a2, \"alpha3\");\n }\n }\n\n // Alpha-2 (opt-in only; too many false positives).\n if (INCLUDE_ALPHA2) {\n for (const code of raw.codes.alpha2) {\n register(code, code, \"alpha2\");\n }\n }\n\n const patterns: PatternEntry[] = [];\n const labels: string[] = [];\n const isoCodes: string[] = [];\n const variants: CountryVariant[] = [];\n\n for (const { display, isoCode, variant } of surfaceToMeta.values()) {\n patterns.push({\n pattern: display,\n literal: true,\n wholeWords: true,\n });\n labels.push(ENTITY_LABEL);\n isoCodes.push(isoCode);\n variants.push(variant);\n }\n\n cachedCountryPatterns = { patterns, data: { labels, isoCodes, variants } };\n return cachedCountryPatterns;\n};\n\n/**\n * Whether the match starts on a proper-noun character.\n * The unified literal search is case-insensitive, so\n * lowercase common nouns that share spelling with a\n * country name (\"turkey\" the bird, \"china\" the porcelain,\n * \"jordan\" the basketball player nickname) would otherwise\n * be flagged. CLDR canonical names and the curated alias\n * list are always proper nouns; require uppercase when the\n * first character is a letter so common-noun usage in\n * prose isn't redacted. Non-letter starts (digits in\n * \"U.S.A.\", etc.) are accepted as-is.\n */\nconst startsAsProperNoun = (text: string, start: number): boolean => {\n const ch = text.charAt(start);\n if (ch.length === 0) return false;\n const upper = ch.toUpperCase();\n const lower = ch.toLowerCase();\n // Non-letter character → no case to enforce.\n if (upper === lower) return true;\n return ch === upper;\n};\n\n/**\n * Convert raw country matches into Entity objects.\n * Filters to the country slice and emits one entity per\n * match. Score is constant (0.95) for all variants;\n * deterministic detector with no fuzzy paths.\n */\nexport const processCountryMatches = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText: string,\n data: CountryData,\n): Entity[] => {\n const results: Entity[] = [];\n\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) continue;\n\n const localIdx = idx - sliceStart;\n const label = data.labels[localIdx];\n if (!label) continue;\n\n if (!startsAsProperNoun(fullText, match.start)) continue;\n\n results.push({\n start: match.start,\n end: match.end,\n label,\n text: fullText.slice(match.start, match.end),\n score: EXACT_SCORE,\n source: DETECTION_SOURCES.COUNTRY,\n });\n }\n\n return results;\n};\n","/**\n * Academic and professional title prefixes.\n * Plain text; the detector auto-escapes for regex.\n * Sorted longest-first at build time.\n */\nexport const TITLE_PREFIXES = [\n // Czech/Slovak pre-nominal\n \"Ing.\",\n \"Mgr.\",\n \"MgA.\",\n \"Bc.\",\n \"BcA.\",\n \"JUDr.\",\n \"MUDr.\",\n \"MVDr.\",\n \"MDDr.\",\n \"PhDr.\",\n \"RNDr.\",\n \"PaedDr.\",\n \"ThDr.\",\n \"ThLic.\",\n \"ICDr.\",\n \"RSDr.\",\n \"PharmDr.\",\n \"artD.\",\n \"akad.\",\n \"doc.\",\n \"prof.\",\n\n // Professor variants (AT/DE)\n \"ao. Univ.-Prof.\",\n \"o. Univ.-Prof.\",\n \"Univ.-Prof.\",\n \"Hon.-Prof.\",\n \"em. Prof.\",\n\n // German doctoral (compound before simple)\n \"Dr. med. dent.\",\n \"Dr. med. vet.\",\n \"Dr. med.\",\n \"Dr. rer. nat.\",\n \"Dr. rer. soc.\",\n \"Dr. rer. pol.\",\n \"Dr. sc. tech.\",\n \"Dr. sc. nat.\",\n \"Dr. sc. hum.\",\n \"Dr. iur.\",\n \"Dr. jur.\",\n \"Dr. theol.\",\n \"Dr. oec.\",\n \"Dr. techn.\",\n \"Dr. h. c.\",\n \"Dr. phil.\",\n \"Dr.-Ing.\",\n \"Dr. Ing.\",\n \"Dr.\",\n\n // German Diplom variants (longest first)\n \"Dipl.-Wirt.-Ing.\",\n \"Dipl.-Betriebsw.\",\n \"Dipl.-Inform.\",\n \"Dipl.-Volksw.\",\n \"Dipl.-Psych.\",\n \"Dipl.-Phys.\",\n \"Dipl.-Chem.\",\n \"Dipl.-Biol.\",\n \"Dipl.-Math.\",\n \"Dipl.-Päd.\",\n \"Dipl.-Soz.\",\n \"Dipl.-Kfm.\",\n \"Dipl.-Jur.\",\n \"Dipl. Ing.\",\n \"Dipl.-Ing.\",\n\n // Austrian Mag/Bakk variants\n \"Mag. rer. soc. oec.\",\n \"Mag. rer. nat.\",\n \"Mag. phil.\",\n \"Mag. iur.\",\n \"Mag. arch.\",\n \"Mag. pharm.\",\n \"Mag. (FH)\",\n \"Mag.\",\n \"Bakk. rer. nat.\",\n \"Bakk. techn.\",\n \"Bakk. phil.\",\n \"Bakk.\",\n\n // Swiss Lic variants\n \"Lic. phil.\",\n \"Lic. iur.\",\n \"Lic. oec.\",\n \"Lic. theol.\",\n \"Lic.\",\n\n // Other German/Austrian\n \"Priv.-Doz.\",\n \"PD\",\n \"RA\",\n] as const;\n\n/**\n * Courtesy/honorific titles that precede a person's\n * name. Sorted alphabetically. The detector escapes\n * dots and adds \\b for entries in HONORIFIC_BOUNDARY.\n */\nexport const HONORIFICS = [\n \"Avv.\",\n \"Dame\",\n \"Doamna\",\n \"Domnul\",\n \"Don\",\n \"Doña\",\n \"Dott.\",\n \"Judge\",\n \"Justice\",\n \"Lady\",\n \"Lord\",\n \"M.\",\n \"Madame\",\n \"Mademoiselle\",\n \"Maître\",\n \"Me\",\n \"Messrs\",\n \"Miss\",\n \"Mlle\",\n \"Mme\",\n \"Monsieur\",\n \"Mr\",\n \"Mrs\",\n \"Ms\",\n \"Pr\",\n \"Pr.\",\n \"President\",\n \"Señor\",\n \"Señora\",\n \"Sig.\",\n \"Sig.ra\",\n \"Signor\",\n \"Signora\",\n \"Signorina\",\n \"Sir\",\n \"Sr.\",\n \"Sra.\",\n] as const;\n\n/**\n * Honorifics that need \\b word-boundary anchors\n * (short or common words that could match mid-word).\n */\nexport const HONORIFIC_BOUNDARY = new Set([\n \"Don\",\n \"Doña\",\n \"M.\",\n \"Me\",\n \"Pr\",\n \"Pr.\",\n \"Señor\",\n \"Señora\",\n]);\n\n/**\n * Honorifics that are abbreviations: a dot after them is an\n * abbreviation dot (same sentence), so the detector keeps an\n * optional `.` between the title and the name (\"Mr. Smith\").\n * Every other honorific is a full word; a trailing dot ends a\n * sentence, so the detector must NOT consume it (otherwise a span\n * like \"President. The Employee\" crosses the sentence boundary).\n *\n * Maintained explicitly, NOT derived from \"ends in a dot\": \"Mr\" is\n * an abbreviation written without a dot, and \"Lord\" is a full word.\n */\nexport const HONORIFIC_ABBREVIATION = new Set([\n \"Avv.\",\n \"Dott.\",\n \"M.\",\n \"Me\",\n \"Messrs\",\n \"Mlle\",\n \"Mme\",\n \"Mr\",\n \"Mrs\",\n \"Ms\",\n \"Pr\",\n \"Pr.\",\n \"Sig.\",\n \"Sig.ra\",\n \"Sr.\",\n \"Sra.\",\n]);\n\n/**\n * Post-nominal degrees (comma or space separated after name).\n * Plain text; the detector auto-escapes for regex.\n */\nexport const POST_NOMINALS = [\n \"Ph.D.\",\n \"Ph.D\",\n \"CSc.\",\n \"DrSc.\",\n \"ArtD.\",\n \"D.Phil.\",\n \"DPhil.\",\n \"MPhil.\",\n \"MBA\",\n \"MPA\",\n \"LL.M.\",\n \"LL.B.\",\n \"M.Sc.\",\n \"B.Sc.\",\n \"MSc.\",\n \"BSc.\",\n \"M.Eng.\",\n \"B.Eng.\",\n \"M.A.\",\n \"B.A.\",\n \"JCD\",\n \"JD\",\n \"DiS.\",\n \"ACCA\",\n \"FCCA\",\n \"CIPM\",\n \"CIPT\",\n \"CIPP/E\",\n \"CIPP\",\n\n // UK senior barrister rank (King's/Queen's Counsel).\n // Bare-letter form (no comma required), so the\n // detector treats it like Ph.D./MBA: name + space + KC.\n \"KC\",\n \"QC\",\n] as const;\n\nexport const NONWESTERN_HONORIFICS = {\n in: [\n \"Sri\",\n \"Shri\",\n \"Smt\",\n \"Smt.\",\n \"Kumari\",\n \"Pandit\",\n \"Pt.\",\n \"Adv.\",\n \"Adv\",\n \"Justice\",\n \"Hon'ble\",\n ],\n ar: [\n \"Al-\",\n \"El-\",\n \"Sheikh\",\n \"Shaikh\",\n \"Sheikha\",\n \"Ustaz\",\n \"Ustaza\",\n \"Abu\",\n \"Umm\",\n ],\n \"zh-Latn\": [\n \"Encik\",\n \"Puan\",\n \"Datuk\",\n \"Dato'\",\n \"Dato\",\n \"Tan Sri\",\n \"Tun\",\n ] as const,\n \"ja-Latn\": [\"-san\", \"-sama\", \"-sensei\", \"Mr.\", \"Ms.\"] as const,\n ko: [\"Sunsaeng\", \"Gyosu\"] as const,\n th: [\n \"Khun\",\n \"Nai\",\n \"Nang\",\n \"Khunying\",\n \"Luang\",\n \"Phra\",\n \"Mom\",\n \"Momluang\",\n \"Mommuen\",\n \"Momratchawong\",\n ] as const,\n vi: [\"Ông\", \"Ong\", \"Ba\", \"Co\"] as const,\n fil: [\n \"Atty.\",\n \"Atty\",\n \"Ginoo\",\n \"Ginang\",\n \"Binibini\",\n \"G.\",\n \"Gng.\",\n \"Bb.\",\n ] as const,\n id: [\n \"Bapak\",\n \"Ibu\",\n \"Pak\",\n \"Bu\",\n \"Raden\",\n \"Tuan\",\n \"Nyonya\",\n \"Nona\",\n \"Haji\",\n \"Hajjah\",\n \"H.\",\n \"Hj.\",\n \"S.H.\",\n \"S.H\",\n \"Ir.\",\n \"Drs.\",\n ] as const,\n} as const;\n","/**\n * Shared text utilities for detectors.\n *\n * Extracted from names.ts and deny-list.ts to avoid\n * duplicating regex constants and helper functions.\n */\n\n/** Matches a string that starts with an uppercase letter. */\nexport const UPPER_START_RE = /^\\p{Lu}/u;\n\n/** Matches a string consisting entirely of uppercase letters. */\nexport const ALL_UPPER_RE = /^\\p{Lu}+$/u;\n\nconst SENTENCE_END_RE = /[.!?]/;\n\n/**\n * Detect whether a position is at the start of a sentence.\n * Looks backward past whitespace for sentence-ending\n * punctuation (.!?). Position 0 and positions preceded\n * only by whitespace are considered sentence starts.\n */\nexport const isSentenceStart = (text: string, pos: number): boolean => {\n if (pos === 0) {\n return true;\n }\n let i = pos - 1;\n while (i >= 0 && /\\s/.test(text[i] ?? \"\")) {\n i--;\n }\n if (i < 0) {\n return true;\n }\n return SENTENCE_END_RE.test(text[i] ?? \"\");\n};\n","import { DETECTION_SOURCES } from \"../types\";\nimport type { Dictionaries, Entity } from \"../types\";\nimport type { PipelineContext, NameCorpusData } from \"../context\";\nimport { defaultContext } from \"../context\";\nimport {\n HONORIFIC_ABBREVIATION,\n NONWESTERN_HONORIFICS,\n TITLE_PREFIXES,\n} from \"../config/titles\";\nimport { ALL_UPPER_RE, UPPER_START_RE, isSentenceStart } from \"../util/text\";\n\n// ── Name corpus ──────────────────────────────────────\n// Per-language first names and surnames loaded from\n// injected dictionaries (optional) plus legacy\n// config/names-*.json for backwards compat. Merged at\n// init time across all configured languages.\n\n// ── Accessors (read from context) ────────────────────\n\nconst getCorpus = (ctx: PipelineContext): NameCorpusData | null =>\n ctx.nameCorpus;\n\n// Exported accessors for deny-list.ts AC integration.\nexport const getNameCorpusFirstNames = (\n ctx: PipelineContext = defaultContext,\n): readonly string[] => ctx.nameCorpus?.firstNamesList ?? [];\nexport const getNameCorpusSurnames = (\n ctx: PipelineContext = defaultContext,\n): readonly string[] => ctx.nameCorpus?.surnamesList ?? [];\nexport const getNameCorpusTitles = (\n ctx: PipelineContext = defaultContext,\n): readonly string[] => ctx.nameCorpus?.titlesList ?? [];\nexport const getNameCorpusExcluded = (\n ctx: PipelineContext = defaultContext,\n): readonly string[] => ctx.nameCorpus?.excludedList ?? [];\nexport const getNameCorpusNonWesternNames = (\n ctx: PipelineContext = defaultContext,\n): readonly string[] => ctx.nameCorpus?.nonWesternNamesList ?? [];\n\nconst NONWESTERN_LOCALE_KEYS = [\n \"in\",\n \"ar\",\n \"ja-latn\",\n \"ko\",\n \"zh-latn\",\n \"th\",\n \"vi\",\n \"fil\",\n \"id\",\n] as const;\n\nconst normalizeCorpusLanguage = (language: string): string =>\n language.toLowerCase();\n\nconst getScopedNonWesternLocaleKeys = (\n languages: readonly string[] | undefined,\n): readonly (typeof NONWESTERN_LOCALE_KEYS)[number][] => {\n if (languages === undefined) {\n return NONWESTERN_LOCALE_KEYS;\n }\n const allowed = new Set(languages.map(normalizeCorpusLanguage));\n return NONWESTERN_LOCALE_KEYS.filter((locale) => allowed.has(locale));\n};\n\nconst getScopedNonWesternHonorifics = (\n languages: readonly string[] | undefined,\n): string[] => {\n const entries = Object.entries(NONWESTERN_HONORIFICS);\n if (languages === undefined) {\n return entries.flatMap(([, forms]) => forms);\n }\n const allowed = new Set(languages.map(normalizeCorpusLanguage));\n return entries\n .filter(([locale]) => allowed.has(normalizeCorpusLanguage(locale)))\n .flatMap(([, forms]) => forms);\n};\n\n/**\n * Load name corpus data from injected dictionaries\n * and legacy config files. Merges all sources.\n *\n * Safe to call multiple times; only loads once per\n * context. Must be called before detectNameCorpus or\n * the getNameCorpus*() accessors are used.\n *\n * @param dictionaries Optional pre-loaded dictionaries\n * with per-language first names and surnames. When\n * omitted, only legacy config files are used.\n */\nexport const initNameCorpus = (\n ctx: PipelineContext = defaultContext,\n dictionaries?: Dictionaries,\n languages?: readonly string[],\n): Promise<void> => {\n const languageKey = languages?.toSorted().join(\",\") ?? \"*\";\n if (ctx.nameCorpus && ctx.nameCorpusKey === languageKey) {\n return Promise.resolve();\n }\n if (ctx.nameCorpusPromise && ctx.nameCorpusKey === languageKey) {\n return ctx.nameCorpusPromise;\n }\n ctx.nameCorpus = null;\n ctx.nameCorpusKey = languageKey;\n const promise = (async () => {\n try {\n // Load legacy config files (backwards compat)\n const [\n legacyFirstMod,\n legacySurnameMod,\n titleMod,\n exclusionMod,\n commonWordsMod,\n ] = await Promise.all([\n import(\"../data/names-first.json\") as Promise<{\n default: { names: string[] };\n }>,\n import(\"../data/names-surnames.json\") as Promise<{\n default: { names: string[] };\n }>,\n import(\"../data/names-title-tokens.json\") as Promise<{\n default: { tokens: string[] };\n }>,\n import(\"../data/names-exclusions.json\") as Promise<{\n default: { words: string[] };\n }>,\n import(\"../data/common-words-en.json\") as Promise<{\n default: { words: string[] };\n }>,\n ]);\n\n // Merge: legacy config + injected per-language files\n const firstNames: string[] = [...legacyFirstMod.default.names];\n if (dictionaries?.firstNames) {\n const entries =\n languages === undefined\n ? Object.entries(dictionaries.firstNames)\n : Object.entries(dictionaries.firstNames).filter(([language]) =>\n languages.includes(language),\n );\n for (const [, names] of entries) {\n for (const name of names) {\n firstNames.push(name);\n }\n }\n }\n\n const surnames: string[] = [...legacySurnameMod.default.names];\n if (dictionaries?.surnames) {\n const entries =\n languages === undefined\n ? Object.entries(dictionaries.surnames)\n : Object.entries(dictionaries.surnames).filter(([language]) =>\n languages.includes(language),\n );\n for (const [, names] of entries) {\n for (const name of names) {\n surnames.push(name);\n }\n }\n }\n\n // Deduplicate (preserve first occurrence)\n const dedup = (arr: string[]): string[] => {\n const seen = new Set<string>();\n const result: string[] = [];\n for (const item of arr) {\n if (seen.has(item)) continue;\n seen.add(item);\n result.push(item);\n }\n return result;\n };\n\n const commonWords = new Set(\n commonWordsMod.default.words.map((word) => word.toLowerCase()),\n );\n const dedupFirst = dedup(firstNames);\n const dedupSurnames = dedup(surnames).filter(\n (name) => !commonWords.has(name.toLowerCase()),\n );\n\n // Merge non-Western honorifics into title tokens.\n // Strip trailing dots/dashes and lowercase so they\n // match the lowercased form used in classifyToken.\n const titles = [...titleMod.default.tokens];\n const scopedNonWesternHonorifics =\n getScopedNonWesternHonorifics(languages);\n for (const form of scopedNonWesternHonorifics) {\n titles.push(form.replace(/[.-]+$/u, \"\").toLowerCase());\n }\n const dedupTitles = dedup(titles);\n\n // Build abbreviation-style title set: titles whose\n // trailing dot is part of the abbreviation, not a\n // sentence boundary. Used by chain assembly to avoid\n // breaking chains on \"Dr. Smith\", \"Smt. Irani\", etc.\n const titleAbbrSet = new Set<string>();\n // Western titles with trailing dots (Ing., Dr., etc.)\n for (const prefix of TITLE_PREFIXES) {\n if (prefix.endsWith(\".\")) {\n titleAbbrSet.add(prefix.replace(/[.-]+$/u, \"\").toLowerCase());\n }\n }\n // Explicit abbreviation honorifics (Mr, Ms, etc.)\n for (const form of HONORIFIC_ABBREVIATION) {\n titleAbbrSet.add(form.replace(/[.-]+$/u, \"\").toLowerCase());\n }\n // Non-Western honorifics with dotted forms (Smt., Pt., Adv., etc.)\n for (const form of scopedNonWesternHonorifics) {\n if (form.endsWith(\".\")) {\n titleAbbrSet.add(form.replace(/[.-]+$/u, \"\").toLowerCase());\n }\n }\n\n const exclusions = exclusionMod.default.words;\n\n // ── Load non-Western name tokens ────────────────\n // Per-locale JSON files + optional injected data.\n const nwLocaleKeys = getScopedNonWesternLocaleKeys(languages);\n const [nwNameMods, nwExcludedMod] = await Promise.all([\n Promise.all(\n nwLocaleKeys.map(\n (locale) =>\n import(`../data/names-nw-${locale}.json`) as Promise<{\n default: { names: string[] };\n }>,\n ),\n ),\n import(\"../data/names-nw-excluded-allcaps.json\") as Promise<{\n default: { words: string[] };\n }>,\n ]);\n\n const nonWesternNames: string[] = [];\n for (const mod of nwNameMods) {\n for (const name of mod.default.names) {\n nonWesternNames.push(name);\n }\n }\n if (dictionaries?.nonWesternNames) {\n const entries =\n languages === undefined\n ? Object.entries(dictionaries.nonWesternNames)\n : (() => {\n const allowed = new Set(languages.map(normalizeCorpusLanguage));\n return Object.entries(dictionaries.nonWesternNames).filter(\n ([language]) =>\n allowed.has(normalizeCorpusLanguage(language)),\n );\n })();\n for (const [, names] of entries) {\n for (const name of names) {\n nonWesternNames.push(name);\n }\n }\n }\n const dedupNonWestern = dedup(nonWesternNames);\n const dedupExcludedAllCaps = dedup(nwExcludedMod.default.words);\n\n ctx.nameCorpus = {\n firstNames: Object.freeze(new Set(dedupFirst)),\n surnames: Object.freeze(new Set(dedupSurnames)),\n titleTokens: Object.freeze(new Set(dedupTitles)),\n titleAbbreviations: Object.freeze(titleAbbrSet),\n excludedWords: Object.freeze(new Set(exclusions)),\n nonWesternNames: Object.freeze(new Set(dedupNonWestern)),\n excludedAllCaps: Object.freeze(new Set(dedupExcludedAllCaps)),\n firstNamesList: Object.freeze(dedupFirst),\n surnamesList: Object.freeze(dedupSurnames),\n titlesList: Object.freeze(dedupTitles),\n excludedList: Object.freeze(exclusions),\n nonWesternNamesList: Object.freeze(dedupNonWestern),\n excludedAllCapsList: Object.freeze(dedupExcludedAllCaps),\n };\n } catch (err) {\n // Reset so the next call retries the load rather\n // than returning this (already-resolved) failed\n // promise. Current awaiters still get a resolved\n // (not rejected) Promise; ctx.nameCorpus stays null.\n ctx.nameCorpusPromise = null;\n console.warn(\n \"[anonymize] Failed to load name corpus JSON\" +\n \" — name detection disabled:\",\n err,\n );\n }\n })();\n ctx.nameCorpusPromise = promise;\n return promise;\n};\n\n// ── Czech/Slovak declension paradigm ─────────────────\n// Full case paradigm (genitive, dative, accusative,\n// vocative, locative, instrumental) for Czech and\n// Slovak first names and surnames. One rule table\n// drives both directions:\n// - expandNameDeclensions() generates declined\n// variants injected as deny-list AC patterns;\n// - stripInflection() maps a declined token back to\n// nominative candidates for corpus lookup.\n// Rules are gated purely by the orthographic shape of\n// the nominative ending; never by individual names.\n\ntype DeclensionRule = {\n /** Nominative ending replaced by each form (\"\" appends). */\n ending: string;\n /** Shape gate tested against the lowercased nominative. */\n gate: RegExp;\n /** Case endings substituted for `ending`. */\n forms: readonly string[];\n};\n\nconst DECLENSION_RULES: readonly DeclensionRule[] = [\n // Masculine, hard/neutral consonant final (Jan, Novák):\n // gen/acc -a, dat/loc/voc -u, dat/loc -ovi, instr -em\n // (cs) / -om (sk). ł counts as a hard consonant so\n // bundled Polish names (Paweł) decline in cs/sk text.\n {\n ending: \"\",\n gate: /[bdfghklmnpqrstvwxzł]$/u,\n forms: [\"a\", \"u\", \"ovi\", \"em\", \"om\"],\n },\n // Masculine vocative -e after non-velar hard consonants\n // (Jane, Adame). Velars take -u (Nováku) and r\n // palatalises (Petře), so they are not licensed here.\n { ending: \"\", gate: /[bdflmnpstvwz]$/u, forms: [\"e\"] },\n // Masculine, soft consonant final (Tomáš, Ondřej, Kráľ):\n // cs gen/acc -e, dat/loc/voc -i, sk gen/acc -a,\n // dat/loc -ovi, instr -em (cs) / -om (sk).\n {\n ending: \"\",\n gate: /[cčďťňřšžjľ]$/u,\n forms: [\"e\", \"i\", \"a\", \"ovi\", \"em\", \"om\"],\n },\n // Fleeting -e-: Czech drops the e before case endings\n // (Marek → Marka, Pavel → Pavla/Pavle, Němec → Němce).\n // The consonant rules above keep the Slovak forms that\n // retain the e (Mareka, Marekovi).\n {\n ending: \"ek\",\n gate: /[^aeiouyáéěíóôúůý]ek$/u,\n forms: [\"ka\", \"ku\", \"kovi\", \"kem\", \"kom\"],\n },\n {\n ending: \"el\",\n gate: /[^aeiouyáéěíóôúůý]el$/u,\n forms: [\"la\", \"lu\", \"le\", \"lovi\", \"lem\", \"lom\"],\n },\n {\n ending: \"ec\",\n gate: /[^aeiouyáéěíóôúůý]ec$/u,\n forms: [\"ce\", \"ci\", \"covi\", \"cem\", \"com\"],\n },\n // -a final, hard stem (Jana, Svoboda): gen -y, acc -u,\n // voc -o, instr -ou, masculine dat/loc -ovi.\n {\n ending: \"a\",\n gate: /[^cčďťňřšžji]a$/u,\n forms: [\"y\", \"u\", \"o\", \"ou\", \"ovi\"],\n },\n // -a final, soft stem (Saša): gen -i instead of -y.\n {\n ending: \"a\",\n gate: /[cčďťňřšžj]a$/u,\n forms: [\"i\", \"u\", \"o\", \"ou\", \"ovi\"],\n },\n // -ia final (Mária, Lívia): sk gen -ie, dat/loc -ii,\n // acc -iu, instr -iou.\n { ending: \"a\", gate: /ia$/u, forms: [\"e\", \"i\", \"u\", \"ou\"] },\n // Feminine dative/locative with regular stem\n // alternation (Jitka → Jitce, Barbora → Barboře cs /\n // Barbore sk, Olga → Olze, Jana → Janě cs / Jane sk,\n // Tereza → Tereze).\n { ending: \"ka\", gate: /ka$/u, forms: [\"ce\"] },\n { ending: \"ra\", gate: /ra$/u, forms: [\"ře\", \"re\"] },\n { ending: \"ha\", gate: /ha$/u, forms: [\"ze\"] },\n { ending: \"ga\", gate: /ga$/u, forms: [\"ze\"] },\n { ending: \"cha\", gate: /cha$/u, forms: [\"še\"] },\n { ending: \"a\", gate: /[bdfmnptv]a$/u, forms: [\"ě\", \"e\"] },\n { ending: \"a\", gate: /[szl]a$/u, forms: [\"e\"] },\n // -á final, adjectival feminine (Nováková, Veselá):\n // cs gen/dat/loc -é, acc/instr -ou; sk gen/dat/loc\n // -ej, acc -ú.\n { ending: \"á\", gate: /á$/u, forms: [\"é\", \"ou\", \"ej\", \"ú\"] },\n // -ý final, adjectival masculine (Černý, Veselý):\n // gen/acc -ého, dat -ému, loc -ém, instr -ým.\n { ending: \"ý\", gate: /ý$/u, forms: [\"ého\", \"ému\", \"ém\", \"ým\"] },\n // -í/-i/-y final (Jiří, Krejčí, Henry): pronominal\n // declension +ho (gen/acc), +mu (dat), +m (loc/instr).\n { ending: \"\", gate: /[íiy]$/u, forms: [\"ho\", \"mu\", \"m\"] },\n // -e/-ie final feminine (Alice, Marie, Lucie):\n // dat/acc/loc -i, instr -í.\n { ending: \"e\", gate: /[^aeouyáéěíóôúůý]e$/u, forms: [\"i\", \"í\"] },\n // -o final masculine (Ivo, Janko, Hugo): gen/acc -a,\n // dat/loc -ovi, instr -em (cs) / -om (sk).\n { ending: \"o\", gate: /o$/u, forms: [\"a\", \"ovi\", \"em\", \"om\"] },\n];\n\n/**\n * Generate declined Czech/Slovak variants of a\n * nominative name. Only variants licensed by the\n * ending-shape rules are produced, which bounds the\n * deny-list AC pattern growth. Returns an empty array\n * for names too short to decline safely.\n */\nexport const expandNameDeclensions = (name: string): string[] => {\n if (name.length < 3) {\n return [];\n }\n const lower = name.toLowerCase();\n const variants: string[] = [];\n for (const rule of DECLENSION_RULES) {\n if (!rule.gate.test(lower)) {\n continue;\n }\n const stem = name.slice(0, name.length - rule.ending.length);\n if (stem.length < 2) {\n continue;\n }\n for (const form of rule.forms) {\n variants.push(stem + form);\n }\n }\n return variants;\n};\n\n/**\n * Strip Czech/Slovak case endings from a token using\n * the inverse of DECLENSION_RULES. Returns candidate\n * nominative forms; each candidate is validated against\n * the rule's shape gate, so only bases the paradigm\n * could actually have declined are proposed. Callers\n * check candidates against the name corpus.\n */\nconst stripInflection = (token: string): string[] => {\n const candidates: string[] = [];\n for (const rule of DECLENSION_RULES) {\n for (const form of rule.forms) {\n if (token.length <= form.length + 2 || !token.endsWith(form)) {\n continue;\n }\n const base = token.slice(0, token.length - form.length) + rule.ending;\n if (!/^\\p{Lu}/u.test(base)) {\n continue;\n }\n if (!rule.gate.test(base.toLowerCase())) {\n continue;\n }\n candidates.push(base);\n }\n }\n return candidates;\n};\n\n// ── Token types ──────────────────────────────────────\n\nconst TOKEN_TYPE = {\n NAME: \"name\",\n SURNAME: \"surname\",\n TITLE: \"title\",\n ABBREVIATION: \"abbreviation\",\n JA_SUFFIX: \"ja_suffix\",\n ARABIC_CONNECTOR: \"arabic_connector\",\n CAPITALIZED: \"capitalized\",\n OTHER: \"other\",\n} as const;\n\ntype TokenType = (typeof TOKEN_TYPE)[keyof typeof TOKEN_TYPE];\n\ntype ClassifiedToken = {\n text: string;\n type: TokenType;\n start: number;\n end: number;\n /** True when matched via nonWesternNames corpus. */\n nonWestern?: boolean;\n /** True when this is an abbreviation-style title (Dr, Mr, Smt, etc.)\n * whose trailing dot is not a sentence boundary. */\n titleAbbreviation?: boolean;\n};\n\nconst PERSON_CHAIN_BREAK_RE = /[!?;:]/u;\n\ntype NameCorpusDetectionOptions = {\n mode?: \"full\" | \"supplemental\";\n};\n\nconst isInitialContinuationGap = (text: string, gap: string): boolean =>\n (/^\\p{Lu}$/u.test(text) && /^\\.[^\\S\\n]{1,2}$/u.test(gap)) ||\n /^[^\\S\\n]{1,2}(?:\\p{Lu}\\.[^\\S\\n]{1,2})+$/u.test(gap);\n\n// ── Helpers ──────────────────────────────────────────\n\n/**\n * Check if a token is in the first-name set, either\n * directly or after stripping Czech/Slovak inflection.\n */\nconst isFirstNameToken = (token: string, corpus: NameCorpusData): boolean => {\n if (corpus.firstNames.has(token)) {\n return true;\n }\n return stripInflection(token).some((b) => corpus.firstNames.has(b));\n};\n\n/**\n * Check if a token is in the surname set, either\n * directly or after stripping Czech/Slovak inflection.\n */\nconst isSurnameToken = (token: string, corpus: NameCorpusData): boolean => {\n if (corpus.surnames.has(token)) {\n return true;\n }\n return stripInflection(token).some((b) => corpus.surnames.has(b));\n};\n\n/**\n * Check if a token looks like a single-letter\n * abbreviation: \"J.\", \"M.\", etc.\n */\nconst isAbbreviation = (token: string): boolean =>\n token.length === 2 && /^\\p{Lu}$/u.test(token[0] ?? \"\") && token[1] === \".\";\n\n/**\n * Title-case a token, re-capitalizing after apostrophes\n * so D'Souza, O'Brien, Hon'ble match the corpus.\n */\nconst titleCaseWithApostrophe = (text: string): string =>\n (text[0]?.toUpperCase() ?? \"\") +\n text\n .slice(1)\n .toLowerCase()\n .replace(/'\\p{Ll}/gu, (m) => m.toUpperCase());\n\n/**\n * Check if a token is in the non-Western name corpus.\n * Tries both title-cased (with apostrophe re-cap) and\n * raw forms.\n */\nconst isNonWesternNameToken = (\n token: string,\n corpus: NameCorpusData,\n): boolean => {\n if (corpus.nonWesternNames.has(token)) return true;\n return corpus.nonWesternNames.has(titleCaseWithApostrophe(token));\n};\n\n// ── Non-Western pattern helpers ─────────────────────\n\n/** Japanese honorific suffixes attached via hyphen. */\nconst JA_SUFFIXES = new Set([\"san\", \"sama\", \"sensei\"]);\n\n/** Arabic patronymic connectors. */\nconst ARABIC_CONNECTORS = new Set([\"bin\", \"bint\", \"ibn\", \"al\", \"el\"]);\n\n/** Al-/El- prefixed name pattern (e.g., Al-Rashid, El-Amin). */\nconst AL_EL_PREFIX_RE = /^[Aa]l-[A-Z][a-z]+$|^[Ee]l-[A-Z][a-z]+$/u;\n\n/** CJK name detection: 2-4 Han chars not adjacent to others. */\nconst CJK_NAME_RE =\n /(?<!\\p{Script=Han})\\p{Script=Han}{2,4}(?!\\p{Script=Han})/gu;\n\n/** Han character threshold for CJK-majority documents. */\nconst CJK_HAN_RATIO = 0.15;\n\nconst CJK_NON_PERSON_TERMS = new Set([\n \"中国\",\n \"中國\",\n \"中文\",\n \"人民\",\n \"公司\",\n \"香港\",\n \"台湾\",\n \"臺灣\",\n \"日本\",\n \"韩国\",\n \"韓國\",\n]);\n\nconst CJK_SURNAME_CHARS = new Set(\n [\n \"王\",\n \"李\",\n \"张\",\n \"張\",\n \"刘\",\n \"劉\",\n \"陈\",\n \"陳\",\n \"杨\",\n \"楊\",\n \"黄\",\n \"黃\",\n \"赵\",\n \"趙\",\n \"吴\",\n \"吳\",\n \"周\",\n \"徐\",\n \"孙\",\n \"孫\",\n \"马\",\n \"馬\",\n \"朱\",\n \"胡\",\n \"郭\",\n \"何\",\n \"林\",\n \"高\",\n \"梁\",\n \"郑\",\n \"鄭\",\n \"罗\",\n \"羅\",\n \"宋\",\n \"谢\",\n \"謝\",\n \"唐\",\n \"韩\",\n \"韓\",\n \"曹\",\n \"许\",\n \"許\",\n \"邓\",\n \"鄧\",\n \"萧\",\n \"蕭\",\n \"田\",\n \"山\",\n \"佐\",\n \"鈴\",\n \"渡\",\n \"伊\",\n \"中\",\n \"小\",\n \"吉\",\n \"金\",\n \"朴\",\n \"박\",\n \"김\",\n \"이\",\n \"최\",\n \"정\",\n \"강\",\n \"조\",\n \"윤\",\n \"장\",\n \"임\",\n \"한\",\n ].join(\"\"),\n);\n\nconst isLikelyCjkPersonName = (text: string): boolean => {\n if (CJK_NON_PERSON_TERMS.has(text)) {\n return false;\n }\n const first = text.at(0);\n return first !== undefined && CJK_SURNAME_CHARS.has(first);\n};\n\n/** Organization keyword filter. */\nconst ORG_WORDS =\n /\\b(?:Group|Company|LLC|LLP|LP|Inc|Ltd|Corp|Corporation|Holdings|Partners|Association|University|Bank|Fund|Trust|Agency|Government|Ministry|Office|Department|Council|Board|Committee|Commission|Services|Solutions|Technologies|Systems|Analytics|Software)\\b/i;\n\nconst isOrganization = (text: string): boolean => ORG_WORDS.test(text);\n\n/** Deduplicate overlapping entity spans (first wins). */\nconst deduplicateSpans = (entities: Entity[]): Entity[] => {\n const sorted = [...entities].sort(\n (a, b) => a.start - b.start || b.end - a.end,\n );\n const result: Entity[] = [];\n for (const entity of sorted) {\n const last = result.at(-1);\n if (!last || entity.start >= last.end) {\n result.push(entity);\n }\n }\n return result;\n};\n\n// ── Word segmentation ────────────────────────────────\n\nconst segmenter = new Intl.Segmenter(undefined, {\n granularity: \"word\",\n});\n\ntype WordSegment = {\n text: string;\n start: number;\n end: number;\n};\n\n/**\n * Split text into word segments using Intl.Segmenter.\n * Only returns segments flagged as words.\n */\nconst segmentWords = (fullText: string): WordSegment[] => {\n const words: WordSegment[] = [];\n for (const seg of segmenter.segment(fullText)) {\n if (seg.isWordLike) {\n words.push({\n text: seg.segment,\n start: seg.index,\n end: seg.index + seg.segment.length,\n });\n }\n }\n return words;\n};\n\n// ── Helpers for chain scoring ────────────────────────\n\n/** NAME or SURNAME — both represent corpus-matched tokens */\nconst isCorpusMatch = (type: TokenType): boolean =>\n type === TOKEN_TYPE.NAME || type === TOKEN_TYPE.SURNAME;\n\n// ── Token classification ─────────────────────────────\n\n// True when the line containing `start` is itself\n// predominantly upper-case (signature block, title\n// block, party caption). Used so the acronym filter\n// below can still match all-caps tokens that match\n// the name corpus in title-case (\"ELON R. MUSK\") while\n// still rejecting acronyms in mixed-case prose.\nconst ALL_CAPS_NAME_LINE_RATIO = 0.9;\nconst ALL_CAPS_NAME_LINE_MIN_LETTERS = 3;\n// Name-shape filter for the all-caps recovery path:\n// real party-caption and signature lines contain only\n// a handful of letter tokens and no digits (\"ELON R.\n// MUSK\", \"X HOLDINGS I, INC.\"). Disclosure/heading\n// lines that happen to include a corpus first name\n// (\"SERVICE MARK LICENSE\", \"ANNUAL STATEMENT OF\n// COMPLIANCE\") fail this check and stay OTHER.\nconst ALL_CAPS_NAME_LINE_MAX_TOKENS = 6;\nconst isAllCapsLineNameShaped = (fullText: string, start: number): boolean => {\n const lineStart = fullText.lastIndexOf(\"\\n\", start - 1) + 1;\n const lineEndIdx = fullText.indexOf(\"\\n\", start);\n const line = fullText.slice(\n lineStart,\n lineEndIdx === -1 ? fullText.length : lineEndIdx,\n );\n if (/\\d/.test(line)) return false;\n const tokens = line.match(/\\p{L}[\\p{L}\\p{M}'-]*/gu) ?? [];\n return tokens.length > 0 && tokens.length <= ALL_CAPS_NAME_LINE_MAX_TOKENS;\n};\n\nconst isAllCapsContextLine = (fullText: string, start: number): boolean => {\n const lineStart = fullText.lastIndexOf(\"\\n\", start - 1) + 1;\n const lineEndIdx = fullText.indexOf(\"\\n\", start);\n const line = fullText.slice(\n lineStart,\n lineEndIdx === -1 ? fullText.length : lineEndIdx,\n );\n let letters = 0;\n let upper = 0;\n for (const ch of line) {\n if (/\\p{L}/u.test(ch)) {\n letters += 1;\n if (ch === ch.toUpperCase() && ch !== ch.toLowerCase()) {\n upper += 1;\n }\n }\n }\n if (letters < ALL_CAPS_NAME_LINE_MIN_LETTERS) {\n return false;\n }\n return upper / letters >= ALL_CAPS_NAME_LINE_RATIO;\n};\n\nconst classifyToken = (\n word: WordSegment,\n corpus: NameCorpusData,\n fullText: string,\n): ClassifiedToken => {\n const { text, start, end } = word;\n const lower = text.toLowerCase();\n\n // Strip trailing period for title check (e.g., \"Ing.\")\n const stripped = text.endsWith(\".\") ? text.slice(0, -1).toLowerCase() : lower;\n\n // 1. Title tokens (Western + non-Western honorifics)\n if (corpus.titleTokens.has(stripped)) {\n // Mark abbreviation-style titles so the chain\n // assembler knows their trailing dot is not a\n // sentence boundary (Dr., Mr., Smt. etc.).\n return {\n text,\n type: TOKEN_TYPE.TITLE,\n start,\n end,\n ...(corpus.titleAbbreviations.has(stripped)\n ? { titleAbbreviation: true }\n : {}),\n };\n }\n\n // 2. Japanese honorific suffixes (san, sama, sensei)\n if (JA_SUFFIXES.has(lower)) {\n return { text, type: TOKEN_TYPE.JA_SUFFIX, start, end };\n }\n\n // 3. Arabic patronymic connectors (bin, bint, ibn, al, el)\n if (ARABIC_CONNECTORS.has(lower)) {\n return { text, type: TOKEN_TYPE.ARABIC_CONNECTOR, start, end };\n }\n\n // 4. Al-/El- prefixed name pattern (Al-Rashid, El-Amin)\n if (AL_EL_PREFIX_RE.test(text)) {\n return { text, type: TOKEN_TYPE.NAME, start, end, nonWestern: true };\n }\n\n // 5. Abbreviation (initials like \"R.\", \"M.\")\n if (isAbbreviation(text)) {\n return {\n text,\n type: TOKEN_TYPE.ABBREVIATION,\n start,\n end,\n };\n }\n\n // 5b. Multi-dot abbreviation patterns (A.P.J, K.C.R)\n if (/^(?:\\p{Lu}\\.)+\\p{Lu}?$/u.test(text)) {\n return { text, type: TOKEN_TYPE.ABBREVIATION, start, end };\n }\n\n // `Intl.Segmenter` splits middle initials into a\n // letter word and a separate punctuation segment\n // (\"R.\" → word \"R\" + \".\"), so the standard\n // `isAbbreviation` check (which requires a length-2\n // \"X.\" token) misses them. Recognise a single\n // uppercase letter immediately followed by a \".\" in\n // the source text as an abbreviation too, so chains\n // like \"ADAM R. BARTOŠ\" don't break on the initial.\n //\n // Standalone enumerators (\"A. Definitions\",\n // \"Section R. Adam\") look identical to middle\n // initials at the token level. Distinguish by\n // structural context: a middle initial is preceded by\n // a name-corpus first name, OR followed by another\n // name token (covering \"R. K. Narayan\" where \"R\"\n // starts the name but is followed by \"K.\").\n if (text.length === 1 && UPPER_START_RE.test(text) && fullText[end] === \".\") {\n const lineStart = fullText.lastIndexOf(\"\\n\", start - 1) + 1;\n const before = fullText.slice(lineStart, start).trimEnd();\n const lastWord = /\\p{L}[\\p{L}\\p{M}'-]*$/u.exec(before)?.[0];\n const lookup = (token: string): boolean =>\n isFirstNameToken(token, corpus) ||\n isFirstNameToken(\n (token[0] ?? \"\") + token.slice(1).toLowerCase(),\n corpus,\n ) ||\n isNonWesternNameToken(token, corpus);\n // Check preceding context (middle initial after a name)\n if (lastWord && lookup(lastWord)) {\n return { text, type: TOKEN_TYPE.ABBREVIATION, start, end };\n }\n // Check following context (initial before a name or\n // another initial, e.g., \"R. K. Narayan\")\n const afterDot = fullText.slice(end + 1).trimStart();\n const nextWord = /^\\p{L}[\\p{L}\\p{M}'-]*/u.exec(afterDot)?.[0];\n if (nextWord) {\n const isNextName =\n lookup(nextWord) ||\n (nextWord.length === 1 && UPPER_START_RE.test(nextWord));\n if (isNextName) {\n return { text, type: TOKEN_TYPE.ABBREVIATION, start, end };\n }\n }\n return { text, type: TOKEN_TYPE.OTHER, start, end };\n }\n\n // Skip excluded words\n if (corpus.excludedWords.has(lower)) {\n return { text, type: TOKEN_TYPE.OTHER, start, end };\n }\n\n // Minimum length 3 for Western corpus matching, but\n // allow 2-char tokens that match the non-Western name\n // corpus (e.g., \"Yi\", \"Li\", \"Vo\"), are Arabic\n // connectors / Japanese suffixes already classified,\n // or are short all-caps post-nominals not in the\n // excluded-all-caps set (e.g., \"JP\", \"KC\", \"QC\").\n if (text.length < 2) {\n return { text, type: TOKEN_TYPE.OTHER, start, end };\n }\n if (\n text.length < 3 &&\n !isNonWesternNameToken(text, corpus) &&\n !JA_SUFFIXES.has(lower) &&\n !ARABIC_CONNECTORS.has(lower) &&\n !(ALL_UPPER_RE.test(text) && !corpus.excludedAllCaps.has(text))\n ) {\n return { text, type: TOKEN_TYPE.OTHER, start, end };\n }\n\n // All-uppercase tokens >= 3 chars are usually\n // acronyms, but there are two legitimate name contexts:\n //\n // 1. Signature/title blocks: \"ELON R. MUSK\", \"JAN\n // NOVÁK\" — allowed when the line is overwhelmingly\n // upper-case and looks name-shaped.\n // 2. Non-Western family-name-first convention: \"SATO\n // Kenji\", \"PARK Jihoon\" — the surname is written in\n // ALL CAPS with the given name in title case. This is\n // valid even in mixed-case text, so we check the\n // non-Western corpus without requiring an all-caps\n // line context.\n if (text.length >= 3 && ALL_UPPER_RE.test(text)) {\n // Exclude common all-caps acronyms\n if (corpus.excludedAllCaps.has(text)) {\n return { text, type: TOKEN_TYPE.OTHER, start, end };\n }\n const titleCased = (text[0] ?? \"\") + text.slice(1).toLowerCase();\n const nwMatch = isNonWesternNameToken(titleCased, corpus);\n\n // Non-Western all-caps surname pattern (SATO, PARK,\n // KIM, etc.) — valid without all-caps line context\n if (nwMatch && !isFirstNameToken(titleCased, corpus)) {\n return { text, type: TOKEN_TYPE.NAME, start, end, nonWestern: true };\n }\n\n // Signature/title block all-caps recovery path\n if (\n isAllCapsContextLine(fullText, start) &&\n isAllCapsLineNameShaped(fullText, start)\n ) {\n if (isFirstNameToken(titleCased, corpus)) {\n return {\n text,\n type: TOKEN_TYPE.NAME,\n start,\n end,\n ...(nwMatch ? { nonWestern: true } : {}),\n };\n }\n if (isSurnameToken(titleCased, corpus)) {\n return {\n text,\n type: TOKEN_TYPE.SURNAME,\n start,\n end,\n ...(nwMatch ? { nonWestern: true } : {}),\n };\n }\n // Non-Western corpus match only (not in Western\n // corpus) in all-caps name-shaped line\n if (nwMatch) {\n return { text, type: TOKEN_TYPE.NAME, start, end, nonWestern: true };\n }\n }\n return { text, type: TOKEN_TYPE.OTHER, start, end };\n }\n\n // Must start with uppercase\n if (!UPPER_START_RE.test(text)) {\n return { text, type: TOKEN_TYPE.OTHER, start, end };\n }\n\n if (isFirstNameToken(text, corpus)) {\n // Also flag as non-Western if matched by both corpora\n const nw = isNonWesternNameToken(text, corpus);\n return {\n text,\n type: TOKEN_TYPE.NAME,\n start,\n end,\n ...(nw ? { nonWestern: true } : {}),\n };\n }\n\n if (isSurnameToken(text, corpus)) {\n const nw = isNonWesternNameToken(text, corpus);\n return {\n text,\n type: TOKEN_TYPE.SURNAME,\n start,\n end,\n ...(nw ? { nonWestern: true } : {}),\n };\n }\n\n // Non-Western name corpus check (after Western\n // corpus, so a token in both gets type NAME/SURNAME\n // with nonWestern flag rather than type NAME alone).\n if (isNonWesternNameToken(text, corpus)) {\n return { text, type: TOKEN_TYPE.NAME, start, end, nonWestern: true };\n }\n\n // Capitalised word (not in corpus but starts uppercase)\n return {\n text,\n type: TOKEN_TYPE.CAPITALIZED,\n start,\n end,\n };\n};\n\n// ── Chain assembly ───────────────────────────────────\n\n/**\n * Detect person names by looking up tokens against the\n * name corpus, then chaining adjacent name-like tokens.\n * Handles both Western and non-Western name patterns.\n *\n * Requires initNameCorpus() to have been called first.\n * If not initialized, returns an empty array.\n *\n * Scoring (Western):\n * TITLE + NAME/SURNAME → 0.95\n * NAME + NAME/SURNAME → 0.9\n * SURNAME + NAME/SURNAME → 0.9\n * NAME + CAPITALIZED → 0.7\n * ABBREVIATION + NAME → 0.7\n * Standalone NAME → 0.5 (low confidence)\n * Standalone SURNAME → skip (too ambiguous)\n *\n * Scoring (non-Western, when chain contains nonWestern tokens):\n * TITLE + (nonWestern|CAPITALIZED) → 0.95\n * JA_SUFFIX + (CAPITALIZED|nonWestern) → 0.9\n * ARABIC_CONNECTOR + nonWestern → 0.9\n * 2+ nonWestern tokens → 0.9\n * nonWestern + (CAPITALIZED|ABBREVIATION) → 0.9\n * Standalone nonWestern mid-sentence → 0.5\n */\nexport const detectNameCorpus = (\n fullText: string,\n ctx: PipelineContext = defaultContext,\n options: NameCorpusDetectionOptions = {},\n): Entity[] => {\n const corpus = getCorpus(ctx);\n if (!corpus) {\n return [];\n }\n\n const supplementalMode = options.mode === \"supplemental\";\n const entities: Entity[] = [];\n\n // ── CJK pre-pass ─────────────────────────────────\n // Only detect CJK names in Latin-majority documents\n // (CJK-majority text has too many Han characters for\n // this heuristic to be useful).\n const threshold = Math.ceil(fullText.length * CJK_HAN_RATIO);\n let hanCount = 0;\n for (const _ of fullText.matchAll(/\\p{Script=Han}/gu)) {\n hanCount++;\n if (hanCount >= threshold) break;\n }\n const isLatinMajority = fullText.length < 100 || hanCount < threshold;\n if (isLatinMajority) {\n CJK_NAME_RE.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = CJK_NAME_RE.exec(fullText)) !== null) {\n const cjkText = match[0];\n if (isLikelyCjkPersonName(cjkText) && !isOrganization(cjkText)) {\n entities.push({\n start: match.index,\n end: match.index + cjkText.length,\n label: \"person\",\n text: cjkText,\n score: 0.95,\n source: DETECTION_SOURCES.REGEX,\n });\n }\n }\n }\n\n // ── Token-based detection ────────────────────────\n const words = segmentWords(fullText);\n\n // Convert word segments to classified tokens,\n // handling s/o, d/o, w/o, r/o relational connectors\n // as single ARABIC_CONNECTOR tokens.\n const tokens: ClassifiedToken[] = [];\n for (let idx = 0; idx < words.length; idx++) {\n const word = words[idx]!;\n const lower = word.text.toLowerCase();\n const nextWord = words[idx + 1];\n if (\n (lower === \"s\" || lower === \"d\" || lower === \"w\" || lower === \"r\") &&\n fullText[word.end] === \"/\" &&\n nextWord &&\n nextWord.start === word.end + 1 &&\n nextWord.text.toLowerCase() === \"o\"\n ) {\n tokens.push({\n text: fullText.slice(word.start, word.end + 2),\n type: TOKEN_TYPE.ARABIC_CONNECTOR,\n start: word.start,\n end: word.end + 2,\n });\n idx++;\n } else {\n tokens.push(classifyToken(word, corpus, fullText));\n }\n }\n\n const consumed = new Set<number>();\n\n for (let i = 0; i < tokens.length; i++) {\n if (consumed.has(i)) {\n continue;\n }\n\n const token = tokens[i];\n if (!token) {\n continue;\n }\n\n // Only start chains from TITLE, NAME, SURNAME,\n // ABBREVIATION, or ARABIC_CONNECTOR (JA_SUFFIX\n // alone is not a valid chain start)\n if (\n token.type !== TOKEN_TYPE.TITLE &&\n token.type !== TOKEN_TYPE.NAME &&\n token.type !== TOKEN_TYPE.SURNAME &&\n token.type !== TOKEN_TYPE.ABBREVIATION &&\n token.type !== TOKEN_TYPE.ARABIC_CONNECTOR\n ) {\n continue;\n }\n\n // Build a chain of adjacent relevant tokens.\n // Max 5 tokens to prevent merging independent names\n // (e.g., \"Jan Novák Pavel Moc\" should be two entities).\n const MAX_CHAIN = 5;\n const chain: ClassifiedToken[] = [token];\n let j = i + 1;\n\n while (j < tokens.length && chain.length < MAX_CHAIN) {\n const next = tokens[j];\n if (!next) {\n break;\n }\n\n // Break chain if there's a newline between tokens\n const prev = chain.at(-1);\n if (prev) {\n const gap = fullText.slice(prev.end, next.start);\n // A period in the gap breaks the chain, unless:\n // 1. The gap looks like an initial continuation\n // (e.g., \"R.\" between two initials).\n // 2. The previous token is an abbreviation-style\n // title (Dr., Mr., Smt.) whose trailing dot is\n // part of the abbreviation, not a sentence end.\n // 3. The previous token is an abbreviation (R., K.,\n // A.P.J.) whose trailing dot is part of the\n // abbreviation.\n const periodIsPartOfPrevToken =\n prev.type === TOKEN_TYPE.ABBREVIATION ||\n (prev.type === TOKEN_TYPE.TITLE && prev.titleAbbreviation === true);\n const breaksOnPeriod =\n gap.includes(\".\") &&\n !isInitialContinuationGap(prev.text, gap) &&\n !periodIsPartOfPrevToken;\n if (\n gap.includes(\"\\n\") ||\n PERSON_CHAIN_BREAK_RE.test(gap) ||\n breaksOnPeriod\n ) {\n break;\n }\n // Japanese suffixes only attach via hyphen or\n // whitespace (no other gap allowed)\n if (\n next.type === TOKEN_TYPE.JA_SUFFIX &&\n gap !== \"-\" &&\n gap.trim() !== \"\"\n ) {\n break;\n }\n }\n\n // Chain NAME, SURNAME, TITLE, ABBREVIATION,\n // CAPITALIZED, JA_SUFFIX, ARABIC_CONNECTOR\n if (next.type !== TOKEN_TYPE.OTHER) {\n chain.push(next);\n j++;\n } else {\n break;\n }\n }\n\n // Score the chain\n const hasTitle = chain.some((t) => t.type === TOKEN_TYPE.TITLE);\n const hasCorpusName = chain.some((t) => isCorpusMatch(t.type));\n const hasFirstName = chain.some((t) => t.type === TOKEN_TYPE.NAME);\n const hasAbbreviation = chain.some(\n (t) => t.type === TOKEN_TYPE.ABBREVIATION,\n );\n const hasNonWestern = chain.some((t) => t.nonWestern === true);\n const hasJaSuffix = chain.some((t) => t.type === TOKEN_TYPE.JA_SUFFIX);\n const hasArabicConnector = chain.some(\n (t) => t.type === TOKEN_TYPE.ARABIC_CONNECTOR,\n );\n const corpusCount = chain.filter((t) => isCorpusMatch(t.type)).length;\n const capitalizedCount = chain.filter(\n (t) => t.type === TOKEN_TYPE.CAPITALIZED,\n ).length;\n const nonWesternCount = chain.filter((t) => t.nonWestern).length;\n\n // Determine score based on chain composition\n let score: number;\n\n if (hasNonWestern) {\n // ── Non-Western scoring path ─────────────────\n if (hasTitle && (nonWesternCount > 0 || capitalizedCount > 0)) {\n score = 0.95;\n } else if (hasJaSuffix && (capitalizedCount > 0 || nonWesternCount > 0)) {\n score = 0.9;\n } else if (hasArabicConnector && nonWesternCount > 0) {\n score = 0.9;\n } else if (nonWesternCount >= 2) {\n score = 0.9;\n } else if (\n nonWesternCount > 0 &&\n (capitalizedCount > 0 || hasAbbreviation)\n ) {\n score = 0.9;\n } else if (\n nonWesternCount === 1 &&\n chain.length === 1 &&\n !isSentenceStart(fullText, token.start)\n ) {\n // Standalone non-Western token mid-sentence\n score = 0.5;\n } else {\n // Insufficient evidence for non-Western chain\n continue;\n }\n if (supplementalMode && score < 0.9) {\n continue;\n }\n } else if (supplementalMode) {\n continue;\n } else {\n // ── Western scoring path (unchanged) ─────────\n if (hasTitle && hasCorpusName) {\n // TITLE + NAME/SURNAME → high confidence\n score = 0.95;\n } else if (corpusCount >= 2) {\n // NAME + NAME, NAME + SURNAME, etc. → high confidence\n score = 0.9;\n } else if (hasCorpusName && capitalizedCount > 0) {\n // NAME/SURNAME + CAPITALIZED → medium confidence\n score = 0.7;\n } else if (hasAbbreviation && hasCorpusName) {\n // ABBREVIATION + NAME/SURNAME → medium confidence\n score = 0.7;\n } else if (hasFirstName && chain.length === 1) {\n // Standalone first NAME → low confidence\n // Skip if at sentence start (likely not a name)\n if (isSentenceStart(fullText, token.start)) {\n continue;\n }\n // Standalone all-caps corpus hits are too\n // ambiguous on their own to emit. \"MARK\" inside\n // \"SERVICE MARK LICENSE\" matches the corpus but\n // is plainly a common noun in context; we need\n // chain evidence (another name token, a title,\n // an abbreviation) before we trust an all-caps\n // first-name token as a person.\n const first = chain[0];\n if (first && ALL_UPPER_RE.test(first.text) && first.text.length >= 3) {\n continue;\n }\n score = 0.5;\n } else if (\n !hasFirstName &&\n chain.length === 1 &&\n chain[0]?.type === TOKEN_TYPE.SURNAME\n ) {\n // Standalone SURNAME → skip (too ambiguous alone)\n continue;\n } else if (hasTitle && chain.length === 1) {\n // Standalone TITLE → skip (not a name by itself)\n continue;\n } else if (hasJaSuffix || hasArabicConnector) {\n // JA_SUFFIX or ARABIC_CONNECTOR without a name\n // token is not a person by itself\n if (!hasCorpusName && !hasFirstName) {\n continue;\n }\n score = 0.5;\n } else {\n // No corpus match in chain → skip\n if (!hasCorpusName) {\n continue;\n }\n score = 0.5;\n }\n }\n\n // Build entity span from first to last token in chain\n const first = chain.at(0);\n const last = chain.at(-1);\n if (!first || !last) {\n continue;\n }\n\n const start = first.start;\n const end = last.end;\n const text = fullText.slice(start, end);\n\n // Reject organization-like spans\n if (isOrganization(text)) {\n continue;\n }\n\n // Mark all chain tokens as consumed\n for (let k = i; k < i + chain.length; k++) {\n consumed.add(k);\n }\n\n entities.push({\n start,\n end,\n label: \"person\",\n text,\n score,\n source: DETECTION_SOURCES.REGEX,\n });\n }\n\n return deduplicateSpans(entities);\n};\n","import type { Entity } from \"../types\";\nimport { DETECTION_SOURCES } from \"../types\";\nimport type { PipelineContext } from \"../context\";\nimport { defaultContext } from \"../context\";\n\n/*\n * Signature-block detector.\n *\n * Recognises the stereotyped shape of legal-document\n * signature blocks and emits high-confidence person\n * spans for the signatory name. Anchors we trust:\n *\n * • \"/s/\" — the canonical \"in lieu of physical\n * signature\" mark used across SEC and EDGAR\n * filings. Whatever follows on the same line (or,\n * when the mark is on its own line, the next\n * non-empty line) is the signer's printed name.\n *\n * • \"Name:\" / \"By:\" labels — explicitly-labelled\n * signature fields. When the value sits on the\n * same line we use it directly; when the label is\n * on its own line we walk forward to the next\n * non-empty non-image line.\n *\n * • \"IN WITNESS WHEREOF\" / \"In Witness Whereof\" —\n * the standard preamble. We walk past the preamble\n * sentence and try the next several short lines\n * until one validates as a name.\n *\n * Heuristics applied to every emission:\n * - Same-line tables (\"/s/ Jane Doe CEO 5/1/24\")\n * split on 3+ whitespace; only the first cell is\n * treated as the signer.\n * - Trailing post-nominal suffixes (\", Jr.\", \", M.D.\")\n * are stripped before validation.\n * - Lowercase name particles (\"van der\", \"de la\",\n * \"von\") are allowed between capitalised tokens.\n * - At least two tokens required; single capitalised\n * words (\"President\", \"Director\") are rejected.\n * - Lines containing legal-form suffixes (LLC, INC.,\n * GMBH, s.r.o., …) are rejected to stop party\n * captions sitting directly above a \"/s/\" mark\n * from being mis-tagged as persons.\n *\n * The detector emits with source TRIGGER (priority 4)\n * so signature-anchored detections outrank deny-list\n * heuristics that may otherwise mis-classify part of\n * the name (\"Sean D Reilly\" — deny-list previously\n * caught only \"Reilly\").\n */\n\nconst SLASH_S_RE = /\\/s\\/[ \\t]*/g;\n// Match `Name:` / `By:` anywhere on a line (anchored on\n// a word boundary), not just at line start. Capturing\n// is non-greedy and bounded by either the next column\n// boundary (3+ spaces / tab) or a newline, so two-column\n// signature blocks (\"Name: Priya Ramanathan Name:\n// Jonathan H. Whitaker\") emit a span per signer.\nconst LABELLED_NAME_RE =\n /\\b(?:By|Name)[ \\t]*:[ \\t]*(?:\\/s\\/[ \\t]+)?([^\\n]*?)(?=\\s{3,}|[\\t]|\\n|$)/gi;\nconst WITNESS_ANCHOR_RE = /\\bIN WITNESS WHEREOF\\b[^\\n]*\\n/gi;\n\n// Tokens allowed as lowercase name particles between\n// two cap tokens (\"Juan de la Cruz\", \"Hans van der\n// Meer\", \"Vincent van Gogh\", \"Jean d'Arc\"). Restricted\n// to a curated list of common particles so we don't\n// promote arbitrary lowercase prose into name shape.\n// Shared with the trigger detector's person name-run\n// boundary (see detectors/triggers.ts).\nexport const NAME_PARTICLE =\n \"(?:de|del|della|der|den|di|du|da|das|do|dos|el|la|le|van|von|y|zu|af|ben|bin|al|d'|d’)\";\nconst CAP_TOKEN = \"\\\\p{Lu}[\\\\p{L}\\\\p{M}.'\\\\-]{0,30}\";\n// A name: starts with a cap token, then 1-4 more\n// tokens which may be cap tokens or lowercase\n// particles. Requires ≥2 tokens total so role titles\n// like \"President\" don't qualify.\nconst NAME_SHAPE_RE = new RegExp(\n `^${CAP_TOKEN}(?:[ \\\\t]+(?:${NAME_PARTICLE}|${CAP_TOKEN})){1,4}$`,\n \"u\",\n);\nconst MAX_NAME_LEN = 60;\n\n// Trailing post-nominal suffixes (\"John Smith, Jr.\",\n// \"Jane Doe, M.D.\", \"Alex Park, Esq.\"). Stripped\n// before validation so the comma doesn't poison the\n// name-shape check.\nconst POST_NOMINAL_SUFFIX_RE =\n /,\\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;\n\n// 3+ whitespace or any tab marks a column boundary in\n// signature tables (\"/s/ Jane Doe CEO 5/1/24\").\nconst COLUMN_SEPARATOR_RE = /\\s{3,}|\\t+/;\n\n// Quick recognition of common legal-form suffixes so we\n// can refuse to emit a previous-line \"name\" when that\n// line is actually a party caption. Case-insensitive;\n// matches in any token position.\nconst ORG_SUFFIX_RE =\n /\\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;\n\n// Lines that look like image stubs or section markers\n// — skip them when walking forward from a witness\n// anchor.\nconst IMAGE_STUB_RE = /^(?:\\[?img|\\[image|\\[logo|\\(logo\\))/i;\n\nconst normaliseCandidate = (text: string): string => {\n let candidate = text.trim();\n candidate = candidate.replace(POST_NOMINAL_SUFFIX_RE, \"\").trim();\n const cells = candidate.split(COLUMN_SEPARATOR_RE);\n const firstCell = cells[0]?.trim() ?? candidate;\n return firstCell;\n};\n\nconst isNameShape = (text: string): boolean => {\n if (text.length === 0 || text.length > MAX_NAME_LEN) return false;\n if (text.length < 3) return false;\n if (!NAME_SHAPE_RE.test(text)) return false;\n return true;\n};\n\nconst findLineEnd = (text: string, pos: number): number => {\n const idx = text.indexOf(\"\\n\", pos);\n return idx === -1 ? text.length : idx;\n};\n\n// Try to extract a normalised name from a span and emit\n// it as a person entity. Returns true when an entity was\n// emitted. Skips the emission if the source span looks\n// like an organisation caption (contains a legal-form\n// suffix) — these should be claimed by the legal-form\n// detector, not the signature detector.\nconst tryEmit = (\n results: Entity[],\n fullText: string,\n start: number,\n end: number,\n score: number,\n): boolean => {\n const raw = fullText.slice(start, end);\n if (ORG_SUFFIX_RE.test(raw)) return false;\n const candidate = normaliseCandidate(raw);\n if (!isNameShape(candidate)) return false;\n // Re-locate the candidate inside the raw slice so the\n // entity coordinates are tight.\n const offset = raw.indexOf(candidate);\n if (offset < 0) return false;\n const absStart = start + offset;\n results.push({\n start: absStart,\n end: absStart + candidate.length,\n label: \"person\",\n text: candidate,\n score,\n source: DETECTION_SOURCES.TRIGGER,\n });\n return true;\n};\n\n// Walk forward up to `maxLines` non-empty non-image\n// lines and emit the first one that validates as a\n// name. Codex P2: emits previously stopped at the\n// first non-empty line even if validation failed,\n// which prevented witness-block scans from reaching\n// the printed signer through intervening \"By:\" /\n// \"COMPANY:\" lines.\nconst tryEmitForwardLines = (\n results: Entity[],\n fullText: string,\n fromPos: number,\n maxLines: number,\n score: number,\n): boolean => {\n let pos = fromPos;\n for (let i = 0; i < maxLines; i++) {\n if (pos >= fullText.length) return false;\n const lineEnd = findLineEnd(fullText, pos);\n const line = fullText.slice(pos, lineEnd).trim();\n if (line.length > 0 && !IMAGE_STUB_RE.test(line)) {\n if (tryEmit(results, fullText, pos, lineEnd, score)) return true;\n }\n pos = lineEnd + 1;\n }\n return false;\n};\n\nconst findPrevLine = (\n fullText: string,\n pos: number,\n): { lineStart: number; lineEnd: number } | null => {\n let cursor = pos - 1;\n while (cursor >= 0 && fullText.charAt(cursor) !== \"\\n\") cursor--;\n while (cursor >= 0) {\n let lineStart = cursor;\n while (lineStart > 0 && fullText.charAt(lineStart - 1) !== \"\\n\") {\n lineStart -= 1;\n }\n const lineEnd = cursor;\n const line = fullText.slice(lineStart, lineEnd).trim();\n if (line.length > 0 && !IMAGE_STUB_RE.test(line)) {\n return { lineStart, lineEnd };\n }\n cursor = lineStart - 1;\n }\n return null;\n};\n\nexport const detectSignatures = (\n fullText: string,\n _ctx: PipelineContext = defaultContext,\n): Entity[] => {\n const results: Entity[] = [];\n\n // Pass 1: `/s/` marks.\n SLASH_S_RE.lastIndex = 0;\n for (\n let m = SLASH_S_RE.exec(fullText);\n m !== null;\n m = SLASH_S_RE.exec(fullText)\n ) {\n const afterMark = m.index + m[0].length;\n const lineEnd = findLineEnd(fullText, afterMark);\n const sameLine = fullText.slice(afterMark, lineEnd).trim();\n if (sameLine.length > 0) {\n // Column-aware: in a tabular signature row\n // (\"/s/ Jane Doe Chief Executive Officer\n // 5/1/2024\"), only the first cell after \"/s/\" is\n // the signer; subsequent cells are title/date.\n const rawSlice = fullText.slice(afterMark, lineEnd);\n const cells = rawSlice.split(COLUMN_SEPARATOR_RE);\n const firstCell = cells[0] ?? \"\";\n if (firstCell.trim().length > 0) {\n const firstCellEnd = afterMark + firstCell.length;\n tryEmit(results, fullText, afterMark, firstCellEnd, 0.95);\n }\n } else {\n tryEmitForwardLines(results, fullText, lineEnd + 1, 4, 0.9);\n }\n\n // Previous-line lookback. EDGAR documents often\n // print the signatory's name in ALL CAPS on its\n // own line directly above the \"/s/\" mark\n // (\"ELON R. MUSK\\n/s/ Elon R. Musk\"). Skip the\n // lookback when the previous line is a party\n // caption (contains a legal-form suffix) so a\n // line like \"TWITTER, INC.\\n/s/ Jane Doe\" doesn't\n // mis-emit the company name as a person.\n const prev = findPrevLine(fullText, m.index);\n if (prev) {\n tryEmit(results, fullText, prev.lineStart, prev.lineEnd, 0.85);\n }\n }\n\n // Pass 2: \"Name:\" / \"By:\" labels.\n LABELLED_NAME_RE.lastIndex = 0;\n for (\n let m = LABELLED_NAME_RE.exec(fullText);\n m !== null;\n m = LABELLED_NAME_RE.exec(fullText)\n ) {\n const value = m[1];\n if (value === undefined) continue;\n const valueStart = m.index + m[0].length - value.length;\n const valueEnd = valueStart + value.length;\n const trimmedValue = value.trim();\n if (trimmedValue.length === 0) {\n // Label sits on its own line — walk forward to the\n // next non-empty line for the printed name.\n tryEmitForwardLines(results, fullText, valueEnd + 1, 3, 0.9);\n continue;\n }\n tryEmit(results, fullText, valueStart, valueEnd, 0.95);\n }\n\n // Pass 3: \"IN WITNESS WHEREOF\" preamble.\n WITNESS_ANCHOR_RE.lastIndex = 0;\n for (\n let m = WITNESS_ANCHOR_RE.exec(fullText);\n m !== null;\n m = WITNESS_ANCHOR_RE.exec(fullText)\n ) {\n const search = fullText.slice(m.index, m.index + 600);\n // Permissive sentence terminator — `.`, `:`, `;`,\n // or just a newline closes the preamble. Some\n // contracts run the preamble straight into the\n // signature block with only a paragraph break.\n const sentenceEnd = /[.:;]\\s*\\n|\\n\\s*\\n/.exec(search);\n if (!sentenceEnd) continue;\n const scanFrom = m.index + sentenceEnd.index + sentenceEnd[0].length;\n tryEmitForwardLines(results, fullText, scanFrom, 6, 0.85);\n }\n\n return results;\n};\n","","import type { Match } from \"@stll/text-search\";\nimport type { Validator } from \"@stll/stdnum\";\nimport {\n at,\n au,\n be,\n bg,\n br,\n ch,\n cn,\n cz,\n cy,\n de,\n dk,\n ee,\n es,\n fi,\n fr,\n gb,\n gr,\n hr,\n hu,\n ie,\n it,\n lt,\n lu,\n lv,\n mt,\n nl,\n no,\n pl,\n pt,\n ro,\n se,\n si,\n sk,\n us,\n} from \"@stll/stdnum\";\nimport { toRegex } from \"@stll/stdnum/patterns\";\n\nimport {\n HONORIFIC_ABBREVIATION,\n HONORIFIC_BOUNDARY,\n HONORIFICS,\n POST_NOMINALS,\n TITLE_PREFIXES,\n} from \"../config/titles\";\nimport amountWordsConfig from \"../data/amount-words.json\";\nimport { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\nimport { DASH, DASH_INNER } from \"../util/char-groups\";\n\nconst MIN_PHONE_LENGTH = 7;\nconst MIN_MONTH_NAME_LENGTH = 3;\n\n// ── Shared helpers ──────────────────────────────────\n\nconst escapeTitle = (title: string): string =>\n title\n // eslint-disable-next-line no-useless-escape\n .replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\s+/g, \"\\\\s*\");\n\n/** Escape for use inside a regex alternation. */\nconst escapeRegex = (s: string): string =>\n // eslint-disable-next-line no-useless-escape\n s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\nconst escapeRegexPhrase = (s: string): string =>\n escapeRegex(s.trim()).replace(/\\s+/g, \"[^\\\\S\\\\n\\\\t]+\");\n\n/** Escape for use inside a regex character class. */\nconst escapeCharClass = (s: string): string => s.replace(/[\\]\\\\^-]/g, \"\\\\$&\");\n\nconst toSortedAlternation = (values: readonly string[]): string =>\n [\n ...new Set(\n values.map(escapeRegexPhrase).filter((value) => value.length > 0),\n ),\n ]\n .toSorted((a, b) => b.length - a.length)\n .join(\"|\");\n\nconst TITLE_PREFIX = TITLE_PREFIXES.toSorted((a, b) => b.length - a.length)\n .map(escapeTitle)\n .join(\"|\");\n\nconst POST_NOMINAL = POST_NOMINALS.toSorted((a, b) => b.length - a.length)\n .map(escapeTitle)\n .join(\"|\");\n\n// Unicode property classes keep the name-word pattern\n// language-agnostic: any uppercase letter followed by\n// lowercase letters works for cs/de/fr/it/es/sk and any\n// future language with cased scripts. The Rust regex\n// engine downstream (@stll/text-search) supports \\p{Lu}\n// and \\p{Ll} natively.\nconst NAME_WORD = `\\\\p{Lu}\\\\p{Ll}+`;\n\nconst PARTICLE =\n `(?:van der|van den|de la|della|` +\n `von|van|dos|ibn|ben|bin|del|zum|zur|ten|ter|` +\n `da|de|di|al|el|le|la|zu|af|av)`;\n\n// Non-newline whitespace. Tabs are admitted because\n// DOCX exports routinely place a TAB between an\n// academic title and the following name (table-cell\n// layouts like \"Ing.\\tStanislav Braňka\"); newlines\n// are not, so spans cannot bleed across paragraphs.\nconst SP = \"[^\\\\S\\\\n]\";\n\n/** Honorific alternation built from titles.ts config. Sorted\n * longest-first so e.g. \"Sig.ra\" wins over \"Sig.\". */\nconst buildHonorificAlt = (entries: readonly string[]): string =>\n [...entries]\n .toSorted((a, b) => b.length - a.length)\n .map((h) => {\n const escaped = escapeRegex(h);\n return HONORIFIC_BOUNDARY.has(h) ? `\\\\b${escaped}` : escaped;\n })\n .join(\"|\");\n\n// Abbreviation honorifics (\"Mr\", \"Sr.\") may be followed by an\n// abbreviation dot; full-word honorifics (\"President\", \"Lord\")\n// may not, so a sentence-ending period after them is not consumed\n// and the person span stops at the sentence boundary.\nconst HONORIFIC_ABBREV_ALT = buildHonorificAlt(\n HONORIFICS.filter((h) => HONORIFIC_ABBREVIATION.has(h)),\n);\nconst HONORIFIC_FULLWORD_ALT = buildHonorificAlt(\n HONORIFICS.filter((h) => !HONORIFIC_ABBREVIATION.has(h)),\n);\n\n// ── Pattern definitions ─────────────────────────────\n\nexport type RegexMeta = {\n label: string;\n score: number;\n sourceDetail?: Entity[\"sourceDetail\"];\n /** Post-match stdnum validator for confirmation. */\n validator?: Validator;\n};\n\ntype RegexDef = {\n pattern: string;\n label: string;\n score: number;\n validator?: Validator;\n};\n\ntype AmountWordsConfig = {\n percentages?: Array<{\n lang: string;\n keywords: string[];\n ones: string[];\n teens: string[];\n tens: string[];\n standalone?: string[];\n allowSpaceCompoundSeparator?: boolean;\n }>;\n magnitudeSuffixes?: Array<{\n lang: string;\n words?: string[];\n abbreviationsCaseInsensitive?: string[];\n abbreviationsCaseSensitive?: string[];\n }>;\n shareQuantityTerms?: Array<{\n lang: string;\n modifiers?: string[];\n nouns: string[];\n }>;\n};\n\nconst AMOUNT_WORDS = amountWordsConfig as AmountWordsConfig;\n\n// ── stdnum validator entries ────────────────────────\n// Each entry pairs a @stll/stdnum validator with a\n// label and confidence score. The pattern derived via\n// toRegex(validator).source is used as the regex; the\n// validator itself is stored in META for post-match\n// confirmation (see processRegexMatches).\n\ntype StdnumEntry = {\n validator: Validator;\n label: string;\n score: number;\n pattern: string;\n};\n\nconst toEntry = (\n validator: Validator,\n label: string,\n score: number,\n): StdnumEntry | null => {\n const pattern = toRegex(validator).source;\n if (!pattern) return null;\n return {\n validator,\n label,\n score,\n pattern,\n };\n};\n\n/**\n * Stdnum validators for national/company IDs.\n *\n * Selection criteria: only patterns specific enough\n * to avoid excessive false positives (country-prefixed\n * VAT numbers, structured personal IDs). Generic\n * digit-only patterns (e.g. \\d{8}) are excluded unless\n * the validator's checksum is strong enough to filter.\n */\nconst STDNUM_ENTRIES: readonly StdnumEntry[] = [\n // ── Original PR #28 patterns (were 15-21) ────────\n toEntry(hu.vat, \"tax identification number\", 0.95),\n toEntry(it.codiceFiscale, \"national identification number\", 0.95),\n // es.dni / es.nie omitted: stdnum patterns are over-fit to\n // the spec letter (`Q` / `X`) and miss real-world prefixes;\n // covered by the format-level ES_DNI / ES_NIE regex below.\n toEntry(se.personnummer, \"national identification number\", 0.9),\n toEntry(ro.cnp, \"national identification number\", 0.95),\n toEntry(fr.nir, \"social security number\", 0.9),\n\n // ── CZ validators ────────────────────────────────\n toEntry(cz.dic, \"tax identification number\", 0.95),\n // cz.ico and cz.rc omitted: cz.ico is \\d{8} (too\n // generic), cz.rc is \\d{6}/\\d{3,4} (handled by\n // pattern 7: czech birth number)\n\n // ── DE validators ────────────────────────────────\n toEntry(de.vat, \"tax identification number\", 0.95),\n toEntry(de.idnr, \"tax identification number\", 0.9),\n toEntry(de.stnr, \"tax identification number\", 0.9),\n toEntry(de.svnr, \"social security number\", 0.9),\n\n // ── PL validators ────────────────────────────────\n toEntry(pl.nip, \"tax identification number\", 0.95),\n toEntry(pl.pesel, \"national identification number\", 0.9),\n // pl.regon omitted: \\d{9,14} too generic\n\n // ── GB validators ────────────────────────────────\n toEntry(gb.vat, \"tax identification number\", 0.95),\n toEntry(gb.nino, \"social security number\", 0.95),\n // gb.utr omitted: \\d{10} too generic\n\n // ── AT validators ────────────────────────────────\n toEntry(at.uid, \"tax identification number\", 0.95),\n toEntry(at.tin, \"tax identification number\", 0.9),\n toEntry(at.businessid, \"registration number\", 0.95),\n\n // ── CH validators ────────────────────────────────\n // Swiss UID: CHE + 9 digits with checksum. It is a\n // company identifier; VAT-specific usage reuses the\n // same base number with tax suffixes, so the bare\n // shape is labelled as registration number.\n toEntry(ch.uid, \"registration number\", 0.95),\n\n // ── AU validators ────────────────────────────────\n toEntry(au.abn, \"tax identification number\", 0.9),\n toEntry(au.acn, \"registration number\", 0.9),\n\n // ── BE validators ────────────────────────────────\n toEntry(be.vat, \"tax identification number\", 0.95),\n toEntry(be.nn, \"national identification number\", 0.9),\n\n // ── NL validators ────────────────────────────────\n toEntry(nl.vat, \"tax identification number\", 0.95),\n // nl.bsn omitted: \\d{9} too generic\n\n // ── NO validators ────────────────────────────────\n toEntry(no.orgnr, \"registration number\", 0.9),\n toEntry(no.mva, \"tax identification number\", 0.95),\n\n // ── DK validators ────────────────────────────────\n toEntry(dk.vat, \"tax identification number\", 0.95),\n toEntry(dk.cpr, \"national identification number\", 0.9),\n\n // ── FI validators ────────────────────────────────\n toEntry(fi.vat, \"tax identification number\", 0.95),\n toEntry(fi.hetu, \"national identification number\", 0.95),\n toEntry(fi.ytunnus, \"registration number\", 0.9),\n\n // ── BG validators ────────────────────────────────\n toEntry(bg.vat, \"tax identification number\", 0.95),\n\n // ── SK validators ────────────────────────────────\n toEntry(sk.dic, \"tax identification number\", 0.95),\n // sk.ico: \\d{8} too generic; sk.rc overlaps with\n // czech birth number pattern\n\n // ── ES additional validators ─────────────────────\n // es.cif omitted: stdnum's candidatePattern is over-fit\n // to the spec letter and misses real-world prefixes;\n // covered by the format-level ES_CIF regex below.\n toEntry(es.vat, \"tax identification number\", 0.95),\n toEntry(es.nss, \"social security number\", 0.9),\n\n // ── FR additional validators ─────────────────────\n toEntry(fr.tva, \"tax identification number\", 0.95),\n toEntry(fr.siren, \"registration number\", 0.9),\n toEntry(fr.siret, \"registration number\", 0.9),\n\n // ── IT additional validators ─────────────────────\n toEntry(it.iva, \"tax identification number\", 0.95),\n\n // ── IE validators ────────────────────────────────\n toEntry(ie.vat, \"tax identification number\", 0.95),\n toEntry(ie.pps, \"national identification number\", 0.9),\n\n // ── PT validators ────────────────────────────────\n toEntry(pt.vat, \"tax identification number\", 0.95),\n toEntry(pt.cc, \"national identification number\", 0.9),\n\n // ── RO additional validators ─────────────────────\n toEntry(ro.vat, \"tax identification number\", 0.95),\n\n // ── GR validators ────────────────────────────────\n toEntry(gr.vat, \"tax identification number\", 0.95),\n\n // ── HR validators ────────────────────────────────\n toEntry(hr.vat, \"tax identification number\", 0.95),\n\n // ── SI validators ────────────────────────────────\n toEntry(si.vat, \"tax identification number\", 0.95),\n\n // ── LT validators ────────────────────────────────\n toEntry(lt.vat, \"tax identification number\", 0.95),\n toEntry(lt.asmens, \"national identification number\", 0.9),\n\n // ── LV validators ────────────────────────────────\n toEntry(lv.vat, \"tax identification number\", 0.95),\n\n // ── EE validators ────────────────────────────────\n toEntry(ee.vat, \"tax identification number\", 0.95),\n toEntry(ee.ik, \"national identification number\", 0.9),\n\n // ── CY validators ────────────────────────────────\n toEntry(cy.vat, \"tax identification number\", 0.95),\n\n // ── MT validators ────────────────────────────────\n toEntry(mt.vat, \"tax identification number\", 0.95),\n\n // ── LU validators ────────────────────────────────\n toEntry(lu.vat, \"tax identification number\", 0.95),\n\n // ── US validators ────────────────────────────────\n toEntry(us.ein, \"tax identification number\", 0.9),\n\n // ── BR validators ────────────────────────────────\n // CPF (personal tax ID, 11 digits, checksum). Higher\n // score than the generic phone patterns so the\n // overlap resolver prefers the tax-ID label.\n toEntry(br.cpf, \"tax identification number\", 0.95),\n toEntry(br.cnpj, \"tax identification number\", 0.95),\n\n // ── CN validators ────────────────────────────────\n // RIC (Resident Identity Card, 18-digit modern form:\n // region + YYYYMMDD + sequence + MOD 11-2 check digit,\n // last position may be `X`). The pattern is tightened\n // to a digit-only `\\d{17}[\\dX]` shape so it can't\n // match alphanumeric blobs that share the length\n // bucket; the validator then enforces the embedded\n // birth date and the checksum.\n //\n // The legacy 15-digit form is NOT covered: its bare\n // `\\d{15}` shape is shadowed by the `fr.nir` pattern\n // (also 15 digits) in the unified text-search engine,\n // which returns only one match per position and picks\n // the earlier-registered pattern. The modern 18-digit\n // form has dominated CN issuance since 1999, so the\n // gap is theoretical for current corpora.\n // Lookbehind/lookahead use an ASCII identifier class rather than\n // `\\w`: the text-search regex backend treats `\\w` as Unicode word\n // chars, which would have CJK label prefixes (`身份证号120…`) satisfy\n // the negative boundary and block matches in native-language\n // contexts. Restricting the boundary to ASCII identifier chars\n // still rejects the cases the boundary exists for — order/account\n // numbers (`Order 1201…`, `ID-1201…`).\n //\n // The check-digit class accepts both `X` and `x`: real-world IDs\n // are commonly written with the lowercase variant, and the stdnum\n // validator's compact step normalises the case before checksum.\n {\n validator: cn.ric,\n label: \"national identification number\",\n score: 0.95,\n pattern: \"(?<![A-Za-z0-9_])\\\\d{17}[\\\\dXx](?![A-Za-z0-9_])\",\n },\n].filter((e): e is StdnumEntry => e !== null);\n\n// ── Named pattern definitions ────────────────────────\n\nconst TITLED_PERSON: RegexDef = {\n pattern:\n `(?:${TITLE_PREFIX})` +\n `(?:${SP}+(?:${TITLE_PREFIX}))*` +\n `${SP}+` +\n `(?:${NAME_WORD})` +\n `(?:${SP}{1,4}(?:${PARTICLE}${SP}+)?` +\n `${NAME_WORD}){1,3}` +\n `(?:,?${SP}+(?:${POST_NOMINAL})(?:,?${SP}+(?:${POST_NOMINAL}))*)?`,\n label: \"person\",\n score: 0.95,\n};\n\nconst HONORIFIC_PERSON: RegexDef = {\n pattern:\n `(?:(?:${HONORIFIC_ABBREV_ALT})\\\\.?|(?:${HONORIFIC_FULLWORD_ALT}))` +\n `${SP}+${NAME_WORD}` +\n `(?:(?:${SP}|-){1,2}(?:${PARTICLE}${SP}+)?` +\n `${NAME_WORD}){0,3}` +\n `(?:${SP}+(?:QC|KC|SC|LJ|AG))?`,\n label: \"person\",\n score: 0.95,\n};\n\n// Bare post-nominal anchor: a multi-word capitalised\n// name followed by a UK senior-barrister rank\n// (KC/QC). Picks up \"John Smith KC\" /\n// \"Jane Doe-Robinson QC\" without requiring a title\n// prefix or honorific. Other post-nominals (Ph.D.,\n// MBA, …) are intentionally NOT in this alternation\n// — they over-fire on non-name patterns. KC/QC are\n// safe because the two-letter rank only ever follows\n// a real person name in UK prose.\nconst POSTNOMINAL_PERSON: RegexDef = {\n pattern:\n `${NAME_WORD}` +\n `(?:(?:${SP}|-){1,2}(?:${PARTICLE}${SP}+)?` +\n `${NAME_WORD}){1,3}` +\n // Either `Name KC` or `Name, KC` — UK convention\n // varies between Bar Council style (no comma) and\n // older legal-citation style (with comma).\n `,?${SP}+(?:KC|QC)\\\\b`,\n label: \"person\",\n score: 0.95,\n};\n\nconst IBAN: RegexDef = {\n pattern:\n `\\\\b[A-Z]{2}\\\\d{2}\\\\s?[\\\\dA-Z]{4}\\\\s?[\\\\dA-Z]{4}` +\n `\\\\s?[\\\\dA-Z]{4}\\\\s?[\\\\dA-Z]{4}` +\n `\\\\s?[\\\\dA-Z]{0,14}\\\\b`,\n label: \"iban\",\n score: 1,\n};\n\nconst EMAIL: RegexDef = {\n pattern: `\\\\b[\\\\w.+\\\\-]+@[\\\\w\\\\-]+(?:\\\\.[\\\\w\\\\-]+)+\\\\b`,\n label: \"email address\",\n score: 1,\n};\n\n// [^\\S\\n] instead of \\s: separators must not\n// match newlines (prevents cross-line bleeding).\nconst INTL_PHONE: RegexDef = {\n pattern:\n `\\\\+\\\\d{1,3}(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\(?\\\\d{2,4}\\\\)?` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{2,4}` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{0,4}\\\\b`,\n label: \"phone number\",\n score: 1,\n};\n\n// Czech phone numbers: mobiles start with 6/7,\n// landlines with 2-5. Restrict to [2-7] but require\n// the full 9-digit pattern to avoid matching monetary\n// amounts. The negative lookahead prevents bank\n// account patterns (digits/digits).\nconst CZ_PHONE: RegexDef = {\n pattern:\n `\\\\b[2-7]\\\\d{2}(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}` +\n `(?!(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d*/\\\\d)` +\n `(?![^\\\\S\\\\n]*(?:Kč|,-|korun|EUR|USD|€|\\\\$))\\\\b`,\n label: \"phone number\",\n score: 0.85,\n};\n\n/**\n * Phone numbers prefixed with \"tel.:\" or \"telefon:\".\n * Captures the number after the prefix, including\n * optional international code (+420).\n */\nconst TEL_PREFIX_PHONE: RegexDef = {\n pattern:\n `(?:\\\\b[Tt]el(?:efon)?\\\\.?\\\\s*:?\\\\s*)` +\n `(?:\\\\+?\\\\d{1,3}[^\\\\S\\\\n]?)?` +\n `\\\\d{3}(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}\\\\b`,\n label: \"phone number\",\n score: 0.95,\n};\n\n/**\n * US phone numbers in the (NNN) NNN-NNNN form. Dominant\n * shape in US notice blocks (\"(212) 735-3000\"); not\n * covered by INTL_PHONE (which requires a leading `+`)\n * or TEL_PREFIX_PHONE (which requires a `tel.:` label).\n *\n * The parenthesised area code is the constraint that\n * keeps this from matching random digit clusters —\n * `(212) 555-1212` looks like nothing else in a contract.\n * Score below INTL_PHONE (1) and TEL_PREFIX_PHONE (0.95)\n * so labelled / fully-qualified forms still win the\n * overlap resolver when both fire on the same span.\n */\nconst US_PAREN_PHONE: RegexDef = {\n pattern:\n `\\\\(\\\\d{3}\\\\)(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}` + `(?:[^\\\\S\\\\n]|[.\\\\-])\\\\d{4}\\\\b`,\n label: \"phone number\",\n score: 0.9,\n};\n\nconst CREDIT_CARD: RegexDef = {\n pattern:\n `\\\\b(?:4\\\\d{3}|5[1-5]\\\\d{2}|3[47]\\\\d{2})` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{4}(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{4}` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{2,4}\\\\b`,\n label: \"credit card number\",\n score: 1,\n};\n\nconst CZ_BIRTH_NUMBER: RegexDef = {\n pattern: `\\\\b\\\\d{6}/\\\\d{3,4}\\\\b`,\n label: \"birth number\",\n score: 1,\n validator: cz.rc,\n};\n\n// Czech commercial-register reference. Every Czech\n// legal entity in the public registry is uniquely\n// identified by a registry section letter (\"oddíl X\")\n// plus an insert number (\"vložka NNN\"). The full phrase\n// uniquely identifies the company, so we emit it as a\n// single registration-number entity rather than only\n// capturing the trailing digits.\n//\n// Tolerances:\n// - case-insensitive \"oddíl\" / \"vložka\";\n// - optional whitespace around comma and after each\n// keyword (DOCX exports add NBSPs and double\n// spaces);\n// - section letter is a single A-Z; insert number is\n// a 1-6 digit integer.\nconst CZ_COMMERCIAL_REGISTER: RegexDef = {\n pattern:\n `(?i)\\\\boddíl[^\\\\S\\\\n]+[A-Z]` +\n `[^\\\\S\\\\n]*,[^\\\\S\\\\n]*` +\n `vložka[^\\\\S\\\\n]+\\\\d{1,6}\\\\b`,\n label: \"registration number\",\n score: 0.95,\n};\n\nconst DATE_NUMERIC: RegexDef = {\n pattern:\n `\\\\b(?:\\\\d{1,2}[./]\\\\d{1,2}[./]\\\\d{2,4}` +\n `|\\\\d{4}-\\\\d{2}-\\\\d{2}` +\n `|\\\\d{4}\\\\.\\\\d{2}\\\\.\\\\d{2})\\\\b`,\n label: \"date\",\n score: 1,\n};\n\nconst DATE_CZ_SPACED: RegexDef = {\n pattern: `\\\\b\\\\d{1,2}\\\\.[^\\\\S\\\\n]+\\\\d{1,2}\\\\.[^\\\\S\\\\n]+\\\\d{4}\\\\b`,\n label: \"date\",\n score: 1,\n};\n\nconst IP_ADDRESS: RegexDef = {\n pattern:\n `\\\\b(?:(?:25[0-5]|2[0-4]\\\\d|[01]?\\\\d\\\\d?)\\\\.){3}` +\n `(?:25[0-5]|2[0-4]\\\\d|[01]?\\\\d\\\\d?)\\\\b`,\n label: \"ip address\",\n score: 1,\n};\n\nconst CZ_BANK_ACCOUNT: RegexDef = {\n pattern: `\\\\b(?:\\\\d{1,6}-)?\\\\d{6,10}/\\\\d{4}(?!\\\\d)`,\n label: \"bank account number\",\n score: 0.95,\n};\n\n// Hungarian Budapest landline (+36 1 XXX XXXX).\n// 2+ digit area codes handled by INTL_PHONE.\nconst HU_LANDLINE: RegexDef = {\n pattern:\n `\\\\+36(?:[^\\\\S\\\\n]|[.\\\\-])?1(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{4}\\\\b`,\n label: \"phone number\",\n score: 0.9,\n};\n\n// Czech license plates (SPZ/RZ).\n// New format: 3SJ 0753 — digit, two letters, space?,\n// four digits. Old format: 1A2 3456 — digit, letter,\n// digit, space?, four digits.\n\n// Czech/Slovak postal code: \"110 00\", \"120 00\".\n// The distinctive XXX XX format with mandatory\n// space is specific enough to avoid most false\n// positives.\nconst CZ_POSTAL: RegexDef = {\n pattern: `\\\\b\\\\d{3}[^\\\\S\\\\n]\\\\d{2}\\\\b`,\n label: \"address\",\n score: 0.7,\n};\n\n// Spanish postal code (CP): 5 digits preceded by a\n// CP marker (\"C.P.\", \"CP\", \"código postal\"). The\n// marker is required to avoid matching arbitrary\n// 5-digit numbers (document IDs, ISBNs, etc.).\n// The pattern uses `includeTrigger`-style prefixing:\n// the marker is part of the match span (length\n// trimmed downstream if needed).\nconst ES_POSTAL: RegexDef = {\n pattern:\n `\\\\b(?:C\\\\.?P\\\\.?|[Cc][óo]digo[^\\\\S\\\\n]+postal)` +\n `[^\\\\S\\\\n]{0,3}:?[^\\\\S\\\\n]{0,3}\\\\d{5}\\\\b`,\n label: \"address\",\n score: 0.7,\n};\n\n// Spanish DNI: 8 digits + 1 letter. Letter is a\n// checksum; post-match validator confirms.\n// Pattern derived from stdnum (es.dni) candidatePattern,\n// constrained for DFA compatibility.\nconst ES_DNI: RegexDef = {\n pattern: `\\\\b\\\\d{8}-?[A-Za-z]\\\\b`,\n label: \"national identification number\",\n score: 0.9,\n validator: es.dni,\n};\n\n// Spanish NIE: X/Y/Z + 7 digits + check letter.\n// Pattern derived from stdnum (es.nie) candidatePattern.\nconst ES_NIE: RegexDef = {\n pattern: `\\\\b[XYZxyz]-?\\\\d{7}-?[A-Za-z]\\\\b`,\n label: \"national identification number\",\n score: 0.95,\n validator: es.nie,\n};\n\n// Spanish CIF: org-type letter (A-H, J, N, P-S, U-W),\n// then 7 digits, then a check character (digit or A-J).\n// Pattern derived from stdnum (es.cif) candidatePattern.\nconst ES_CIF: RegexDef = {\n pattern: `\\\\b[A-HJNP-SUVWa-hjnp-suvw]-?\\\\d{7}-?[0-9A-Ja-j]\\\\b`,\n label: \"registration number\",\n score: 0.95,\n validator: es.cif,\n};\n\nconst AU_ABN_FORMATTED: RegexDef = {\n pattern: `\\\\b\\\\d{2}[^\\\\S\\\\n]\\\\d{3}[^\\\\S\\\\n]\\\\d{3}[^\\\\S\\\\n]\\\\d{3}\\\\b`,\n label: \"tax identification number\",\n score: 0.95,\n validator: au.abn,\n};\n\nconst NO_ORGNR_FORMATTED: RegexDef = {\n pattern: `\\\\b\\\\d{3}[^\\\\S\\\\n]\\\\d{3}[^\\\\S\\\\n]\\\\d{3}\\\\b`,\n label: \"registration number\",\n score: 0.9,\n validator: no.orgnr,\n};\n\nconst NO_MVA_FORMATTED: RegexDef = {\n pattern:\n `\\\\bNO[^\\\\S\\\\n]?\\\\d{3}[^\\\\S\\\\n]?\\\\d{3}` +\n `[^\\\\S\\\\n]?\\\\d{3}[^\\\\S\\\\n]?MVA\\\\b`,\n label: \"tax identification number\",\n score: 0.95,\n validator: no.mva,\n};\n\nconst US_EIN_FORMATTED: RegexDef = {\n pattern: `\\\\b\\\\d{2}${DASH}\\\\d{7}\\\\b`,\n label: \"tax identification number\",\n score: 0.95,\n validator: us.ein,\n};\n\n// Brazilian CEP (Código de Endereçamento Postal):\n// NNNNN-NNN. Distinctive 5-digit + hyphen + 3-digit\n// shape, but the bare form is indistinguishable from\n// non-address order/ticket/reference numbers\n// (\"Order 12345-678\"), so it is not emitted as an\n// active address regex. Instead the shape is consumed\n// by `processAddressSeeds` as a postal-code seed, so\n// expansion only fires when other street/city signals\n// cluster around it (e.g.\n// \"Rua Augusta, 123, 01001-000 São Paulo\").\n//\n// Kept here as documentation only.\n\n// Brazilian CPF (personal tax ID), formatted form\n// only: NNN.NNN.NNN-NN. Dotted/dashed form is matched\n// by the distinctive separators; the br.cpf validator\n// rejects placeholder values such as \"000.000.000-00\"\n// or other invalid checksums.\n//\n// Score must beat the generic phone patterns so the\n// overlap resolver assigns the tax-ID label.\nconst BR_CPF_FORMATTED: RegexDef = {\n pattern: `\\\\b\\\\d{3}\\\\.\\\\d{3}\\\\.\\\\d{3}${DASH}\\\\d{2}\\\\b`,\n label: \"tax identification number\",\n score: 0.95,\n validator: br.cpf,\n};\n\n// Brazilian CNPJ (company tax ID), formatted:\n// NN.NNN.NNN/NNNN-NN. The slash is unique to CNPJ\n// for shape, and the br.cnpj validator filters\n// placeholder values such as \"12.345.678/0001-00\".\nconst BR_CNPJ_FORMATTED: RegexDef = {\n pattern: `\\\\b\\\\d{2}\\\\.\\\\d{3}\\\\.\\\\d{3}/\\\\d{4}${DASH}\\\\d{2}\\\\b`,\n label: \"tax identification number\",\n score: 0.95,\n validator: br.cnpj,\n};\n\n// Brazilian RG (Registro Geral, state-issued identity).\n// Format is non-uniform across states; the most reliable\n// anchor is the trailing \"SSP/UF\" issuer marker. Captures\n// the number and the SSP suffix.\n// 12.345.678 SSP/DF\n// 45.678.901-2 SSP/SP\n// 32.456.789-X SSP/SP\nconst BR_RG_WITH_SSP: RegexDef = {\n pattern:\n `\\\\b\\\\d{1,3}\\\\.?\\\\d{3}\\\\.?\\\\d{3}` +\n `(?:${DASH}[0-9A-Za-z])?` +\n `[^\\\\S\\\\n]+SSP(?:/[A-Z]{2})?\\\\b`,\n label: \"national identification number\",\n score: 0.95,\n};\n\n// Brazilian OAB (lawyer registration). Format:\n// \"OAB/UF NNNNNN\" or \"OAB/UF NNN.NNN\" — two-letter\n// state code, optional \"nº\" / \"n.\" marker, then 4–6\n// digits with optional thousand separator dot.\nconst BR_OAB: RegexDef = {\n pattern:\n `\\\\bOAB/[A-Z]{2}[^\\\\S\\\\n]+(?:n[º°.][^\\\\S\\\\n]*)?` +\n `(?:\\\\d{1,3}(?:\\\\.\\\\d{3})+|\\\\d{4,6})\\\\b`,\n label: \"registration number\",\n score: 0.95,\n};\n\n// URL: scheme + host + optional port + path + query +\n// fragment. Trailing prose punctuation excluded but\n// ? = & # kept for query strings.\n// Allow missing // after http:/https: — common OCR\n// artifact (\"http:example.cz\" instead of\n// \"http://example.cz\"). Lookahead ensures bare scheme\n// is not matched in isolation (e.g., \"http:\" at EOL).\nconst URL: RegexDef = {\n pattern:\n `(?:https?://|https?:(?=[^\\\\s])|www\\\\.)` +\n `[\\\\w\\\\-]+(?:\\\\.[\\\\w\\\\-]+)+` +\n `(?::\\\\d+)?` +\n `(?:[/?#][^\\\\s)\\\\]>]*[^\\\\s.,;:!?)\\\\]>])?`,\n label: \"url\",\n score: 1,\n};\n\n// Bare domain: no protocol/www prefix, ends with a\n// known TLD. Catches \"fondkinematografie.cz\" etc.\n// Uses [a-zA-Z0-9] (no underscores — invalid in\n// hostnames). Short ambiguous TLDs (de, at, no, se,\n// fi, dk, be, it, uk) require at least one subdomain\n// dot to reduce false positives in European legal\n// text; unambiguous TLDs allow bare second-level\n// domains (e.g., \"fondkinematografie.cz\").\nconst LONG_TLDS =\n \"com|org|net|eu|cz|sk|pl|hu|ro|fr|es\" + \"|co\\\\.uk|nl|ch|info|io|dev\";\nconst SHORT_TLDS = \"de|at|be|se|fi|dk|no|it|uk\";\n// RFC 1123: labels cannot start or end with hyphen.\nconst HOST_LABEL = `[a-zA-Z0-9](?:[a-zA-Z0-9\\\\-]*[a-zA-Z0-9])?`;\nconst BARE_HOST = `\\\\b[a-zA-Z0-9][a-zA-Z0-9\\\\-]+[a-zA-Z0-9]`;\nconst PATH_SUFFIX = `(?:[/?#][^\\\\s)\\\\]>]*[^\\\\s.,;:!?)\\\\]>])?`;\nconst BARE_DOMAIN: RegexDef = {\n pattern:\n // Unambiguous TLDs: bare SLDs ok (one dot)\n `${BARE_HOST}(?:\\\\.${HOST_LABEL})*` +\n `\\\\.(?:${LONG_TLDS})\\\\b${PATH_SUFFIX}` +\n `|` +\n // Short/ambiguous TLDs: require subdomain (two+ dots)\n `${BARE_HOST}(?:\\\\.${HOST_LABEL})+` +\n `\\\\.(?:${SHORT_TLDS})\\\\b${PATH_SUFFIX}`,\n label: \"url\",\n score: 0.9,\n};\n\n// Full RFC 5952 IPv6. :: compressed form replaces\n// 1+ zero groups. Right side: 1-7 hex groups.\nconst IPV6_ADDRESS: RegexDef = {\n pattern:\n `\\\\b(?:[0-9a-fA-F]{1,4}:){7}` +\n `[0-9a-fA-F]{1,4}\\\\b` +\n `|\\\\b(?:[0-9a-fA-F]{1,4}:){1,7}:\\\\b` +\n `|::(?:[0-9a-fA-F]{1,4}:){0,6}` +\n `[0-9a-fA-F]{1,4}`,\n label: \"ip address\",\n score: 1,\n};\n\n// MAC: colon-only OR hyphen-only (no mixed).\nconst MAC_ADDRESS: RegexDef = {\n pattern:\n `\\\\b(?:[0-9a-fA-F]{2}:){5}` +\n `[0-9a-fA-F]{2}\\\\b` +\n `|\\\\b(?:[0-9a-fA-F]{2}-){5}` +\n `[0-9a-fA-F]{2}\\\\b`,\n label: \"mac address\",\n score: 1,\n};\n\n// SWIFT/BIC moved to trigger-based detection\n// (triggers.global.json) for better composability.\n\n// UK postcode (standard outward + inward). Covers:\n// \"SW1A 1AA\", \"EC4A 1AB\", \"M1 1AE\", \"B33 8TH\",\n// \"CR2 6XH\", \"DN55 1PT\", \"GIR 0AA\" (Girobank).\n// Strict format: outward area letter(s) + digit/digit\n// (or digit+letter) + space + inward digit + 2 letters.\n// Space between outward and inward is optional in\n// freely-typed text.\nconst UK_POSTCODE: RegexDef = {\n pattern:\n `\\\\b(?:` +\n // GIR 0AA — historic Girobank postcode\n `GIR[^\\\\S\\\\n]?0AA` +\n `|` +\n // Standard outward (1-2 letters, 1-2 digits, opt. letter)\n `[A-PR-UWYZ](?:[A-HK-Y][0-9](?:[0-9]|[ABEHMNPRV-Y])?|[0-9](?:[0-9]|[A-HJKPS-UW])?)` +\n `[^\\\\S\\\\n]?[0-9][ABD-HJLNP-UW-Z]{2}` +\n `)\\\\b`,\n label: \"address\",\n score: 0.9,\n};\n\n// UK National Insurance Number. Two-letter prefix +\n// six digits + optional suffix letter A-D. Per HMRC:\n// first letter not D/F/I/Q/U/V; second letter not\n// D/F/I/O/Q/U/V; and several explicit prefix blocks\n// (BG, GB, KN, NK, NT, TN, ZZ). The character classes\n// here enforce the per-position letter rules; the\n// stdnum `gb.nino` validator handles the blocked\n// prefixes at post-match time. Negative lookaheads\n// are avoided because the Rust DFA upstream rejects\n// them. Optional spaces between segments cover the\n// common printed form `AB 12 34 56 C` — stdnum's own\n// candidatePattern (`[A-Z]{2}\\d{6}[A-Z]`) misses it.\nconst UK_NINO: RegexDef = {\n pattern:\n `\\\\b[A-CEGHJ-PR-TWXYZ][A-CEGHJ-NPR-TWXYZ]` +\n `[^\\\\S\\\\n]?\\\\d{2}[^\\\\S\\\\n]?\\\\d{2}[^\\\\S\\\\n]?\\\\d{2}` +\n `[^\\\\S\\\\n]?[A-D]?\\\\b`,\n label: \"social security number\",\n score: 0.95,\n validator: gb.nino,\n};\n\n// 12-hour time: \"5:00 p.m.\", \"12:30 AM\", \"5:00p.m.\",\n// \"11:00 a.m. Eastern Time\". Captures HH:MM and the\n// am/pm marker; optional timezone suffix is not\n// included (it's not PII). Case spelled out explicitly\n// (no (?i)) because DFA compilation fails with\n// Unicode + case-insensitive flag on this pattern.\nconst TIME_12H: RegexDef = {\n pattern:\n `\\\\b(?:1[0-2]|0?[1-9]):[0-5]\\\\d` +\n `[^\\\\S\\\\n]?(?:[aApP]\\\\.?[mM]\\\\.?)` +\n `(?=[\\\\s,;!?)]|$)`,\n label: \"date\",\n score: 0.9,\n};\n\nconst PERCENT_NUMBER_BODY = `(?:\\\\d{1,3}(?:[.,]\\\\d{3})+(?:[.,]\\\\d{1,4})?|\\\\d+(?:[.,]\\\\d{1,4})?)`;\nconst PERCENT_NUMBER = `(?:[+${DASH_INNER}])?${PERCENT_NUMBER_BODY}`;\nconst PERCENT_TOKEN = `${PERCENT_NUMBER}[^\\\\S\\\\n]{0,2}%`;\nconst PERCENT_RANGE_NUMBER = `\\\\d+(?:[.,]\\\\d{1,4})?`;\nconst PERCENT_RANGE =\n `${PERCENT_RANGE_NUMBER}[^\\\\S\\\\n]*${DASH}[^\\\\S\\\\n]*` +\n `${PERCENT_RANGE_NUMBER}[^\\\\S\\\\n]{0,2}%`;\n\nconst buildPercentWordPattern = (config: AmountWordsConfig): string => {\n const phrases: string[] = [];\n for (const entry of config.percentages ?? []) {\n const ones = entry.ones.map(escapeRegex);\n const standalone = (entry.standalone ?? []).map(escapeRegex);\n const baseWords = [...ones, ...entry.teens, ...entry.tens].map(escapeRegex);\n const compoundSeparator = entry.allowSpaceCompoundSeparator\n ? `(?:${DASH}|[^\\\\S\\\\n]+)`\n : DASH;\n const compound =\n `(?:${baseWords.join(\"|\")})` +\n (ones.length > 0 ? `(?:${compoundSeparator}(?:${ones.join(\"|\")}))?` : \"\");\n const word = `(?:${[...standalone, compound].join(\"|\")})`;\n const keyword = `(?:${entry.keywords.map(escapeRegex).join(\"|\")})`;\n phrases.push(`${word}[^\\\\S\\\\n]+${keyword}`);\n }\n return phrases.length > 0 ? `(?i:(?:${phrases.join(\"|\")}))` : \"(?!)\";\n};\n\nconst PERCENT_WORD = buildPercentWordPattern(AMOUNT_WORDS);\n\n// Percentages and financial rates. Captures signed numeric\n// values with dot or comma decimals, grouped thousands, and\n// locale-style spacing before `%`. Also widens written-out\n// legal thresholds paired with a numeric parenthetical\n// (`fifty percent (50%)`) so the text does not disclose the\n// exact value after only the parenthesized token is redacted.\n// Percentages are not classically personally identifying, but\n// in legal text they routinely fingerprint specific debt\n// instruments (`3.875% Senior Notes due 2027`) and tax\n// brackets; labelling them as `monetary amount` keeps the\n// operator-side handling consistent with how other quantitative\n// identifiers are redacted.\nconst PERCENT_RATE: RegexDef = {\n pattern:\n `(?<![\\\\p{L}\\\\p{N}_.,])(?:` +\n `${PERCENT_WORD}[^\\\\S\\\\n]*\\\\([^\\\\S\\\\n]*${PERCENT_TOKEN}[^\\\\S\\\\n]*\\\\)` +\n `|${PERCENT_RANGE}` +\n `|${PERCENT_TOKEN}` +\n `)(?![\\\\p{L}\\\\p{N}_])`,\n label: \"monetary amount\",\n score: 0.85,\n};\n\n// ── Collected definitions ────────────────────────────\n\n/**\n * All static PII regex definitions. Scanned in a\n * single pass by @stll/regex-set (Rust DFA).\n *\n * Hand-written patterns (0-17) followed by\n * stdnum-derived patterns (18+). Each stdnum entry\n * has a post-match validator for confirmation.\n *\n * Monetary amount patterns are built dynamically from\n * currencies.json via `getCurrencyPatterns()`.\n *\n * Date patterns using written month names are built\n * dynamically from date-months.json via\n * `getDatePatterns()`.\n */\nconst ALL_REGEX_DEFS: readonly RegexDef[] = [\n TITLED_PERSON,\n HONORIFIC_PERSON,\n POSTNOMINAL_PERSON,\n IBAN,\n EMAIL,\n INTL_PHONE,\n CZ_PHONE,\n TEL_PREFIX_PHONE,\n US_PAREN_PHONE,\n CREDIT_CARD,\n CZ_BIRTH_NUMBER,\n CZ_COMMERCIAL_REGISTER,\n DATE_NUMERIC,\n DATE_CZ_SPACED,\n IP_ADDRESS,\n CZ_BANK_ACCOUNT,\n HU_LANDLINE,\n CZ_POSTAL,\n ES_POSTAL,\n ES_DNI,\n ES_NIE,\n ES_CIF,\n AU_ABN_FORMATTED,\n NO_ORGNR_FORMATTED,\n NO_MVA_FORMATTED,\n US_EIN_FORMATTED,\n BR_CPF_FORMATTED,\n BR_CNPJ_FORMATTED,\n BR_RG_WITH_SSP,\n BR_OAB,\n URL,\n IPV6_ADDRESS,\n MAC_ADDRESS,\n BARE_DOMAIN,\n UK_POSTCODE,\n UK_NINO,\n TIME_12H,\n PERCENT_RATE,\n ...STDNUM_ENTRIES,\n];\n\n/** Flat pattern array for text-search. */\nexport const REGEX_PATTERNS: readonly string[] = ALL_REGEX_DEFS.map(\n (d) => d.pattern,\n);\n\n/** Parallel metadata. Index = pattern index. */\nexport const REGEX_META: readonly RegexMeta[] = ALL_REGEX_DEFS.map(\n (d): RegexMeta => {\n const meta: RegexMeta = {\n label: d.label,\n score: d.score,\n };\n if (d.validator) {\n meta.validator = d.validator;\n }\n return meta;\n },\n);\n\n// ── Dynamic date patterns (22 languages) ────────────\n\n/**\n * JSON shape: language codes map to string arrays;\n * metadata keys (prefixed `_`) map to strings.\n * The `_` keys are skipped by `buildMonthAlternation`.\n */\ntype DateMonths = Record<string, string[] | string>;\n\n/**\n * Build month-name alternation from date-months.json.\n * Deduplicates across all 22 languages, filters names\n * shorter than 3 chars (too many false positives), and\n * sorts longest-first so the regex engine prefers the\n * longest match.\n */\nconst buildMonthAlternation = (months: DateMonths): string => {\n const seen = new Set<string>();\n for (const [key, value] of Object.entries(months)) {\n if (key.startsWith(\"_\")) continue;\n const names = Array.isArray(value) ? value : [value];\n for (const name of names) {\n // Strip trailing dots for the regex; date patterns\n // use `\\\\.?` after the alternation to match optional\n // abbreviation dots.\n const clean = name.replace(/\\.$/, \"\").toLowerCase();\n if (clean.length >= MIN_MONTH_NAME_LENGTH) {\n seen.add(clean);\n }\n }\n }\n return [...seen]\n .toSorted((a, b) => b.length - a.length)\n .map(escapeRegex)\n .join(\"|\");\n};\n\n/**\n * Build date patterns from a month-name alternation.\n * Returns 6 patterns covering the major written-date\n * formats across all supported languages.\n */\nconst buildDatePatternsFromMonths = (alt: string): string[] => {\n if (!alt) {\n // No month names survived filtering — return nothing\n // rather than emitting patterns with (?:) that match\n // arbitrary whitespace.\n return [];\n }\n // Optional time suffix: \"19:45:50\" or \"19:45\"\n const TIME = `(?:\\\\s+\\\\d{1,2}:\\\\d{2}(?::\\\\d{2})?)`;\n return [\n // a. DD[.] Month[.] YYYY [HH:MM[:SS]]\n `(?i)\\\\b\\\\d{1,2}\\\\.?\\\\s+(?:${alt})\\\\.?\\\\s+\\\\d{4}${TIME}?\\\\b`,\n // b. Month[.] DD[,] YYYY — \"March 7, 2023\" (US format)\n `(?i)\\\\b(?:${alt})\\\\.?\\\\s+\\\\d{1,2},?\\\\s+\\\\d{4}\\\\b`,\n // g. Month[.] DD — \"December 31\" (no year, US format)\n `(?i)\\\\b(?:${alt})\\\\.?\\\\s+\\\\d{1,2}(?=\\\\s|[.,;!?)]|$)`,\n // c. DDst/nd/rd/th Month[.] [YYYY] — \"1st January 2025\"\n `(?i)\\\\b\\\\d{1,2}(?:st|nd|rd|th)\\\\s+(?:${alt})\\\\.?` +\n `(?:\\\\s+\\\\d{4})?(?=\\\\s|[.,;!?)]|$)`,\n // d. Month[.] YYYY — \"October 1983\"\n `(?i)\\\\b(?:${alt})\\\\.?\\\\s+\\\\d{4}\\\\b`,\n // e. YYYY. Month[.] DD. — Hungarian \"2025. január 7.\"\n `(?i)\\\\b\\\\d{4}\\\\.\\\\s+(?:${alt})\\\\.?\\\\s+\\\\d{1,2}\\\\.?(?=\\\\s|[.,;!?)]|$)`,\n // f. DD de Month[.] [de] YYYY — Spanish \"7 de enero de 2025\"\n `(?i)\\\\b\\\\d{1,2}\\\\s+de\\\\s+(?:${alt})\\\\.?` + `(?:\\\\s+de)?\\\\s+\\\\d{4}\\\\b`,\n ];\n};\n\n/** Cached promise for date patterns. Loaded once. */\nlet datePatternPromise: Promise<string[]> | null = null;\n\nconst loadDatePatterns = async (): Promise<string[]> => {\n const mod = await import(\"../data/date-months.json\");\n // Dynamic import of JSON returns { default, ...keys }.\n // Use `default` if present (ESM wrapper), else the\n // module itself.\n const months: DateMonths = mod.default ?? mod;\n const alt = buildMonthAlternation(months);\n return buildDatePatternsFromMonths(alt);\n};\n\n/**\n * Get dynamically built date patterns from\n * date-months.json. Returns a cached promise; the JSON\n * is loaded only once.\n */\nexport const getDatePatterns = (): Promise<string[]> => {\n if (!datePatternPromise) {\n datePatternPromise = loadDatePatterns().catch((err) => {\n datePatternPromise = null;\n throw err;\n });\n }\n return datePatternPromise;\n};\n\n/** Date pattern metadata (all are score 1 dates). */\nexport const DATE_PATTERN_META: Readonly<RegexMeta> = Object.freeze({\n label: \"date\",\n score: 1,\n});\n\n// ── Dynamic currency patterns ──────────────────────\n\n/**\n * JSON shape from currencies.json: ISO 4217 codes,\n * common currency symbols, and local currency names.\n */\ntype CurrenciesData = {\n codes: string[];\n symbols: string[];\n localNames?: string[];\n};\n\ntype FinancialLexicons = {\n magnitudeOptional: string;\n magnitudeRequired: string;\n magnitudePrefilterTerms: readonly string[];\n quantityFollowerGuard: string;\n};\n\ntype CurrencyPatternEntry = {\n pattern: string;\n literal?: false;\n lazy: true;\n prefilterAny: readonly string[];\n prefilterCaseInsensitive: boolean;\n prefilterRegex?: RegExp;\n};\n\ntype MagnitudePattern = {\n optional: string;\n required: string;\n prefilterTerms: readonly string[];\n};\n\nconst buildMagnitudePattern = (config: AmountWordsConfig): MagnitudePattern => {\n const words: string[] = [];\n const caseInsensitiveAbbreviations: string[] = [];\n const caseSensitiveAbbreviations: string[] = [];\n\n for (const entry of config.magnitudeSuffixes ?? []) {\n words.push(...(entry.words ?? []));\n caseInsensitiveAbbreviations.push(\n ...(entry.abbreviationsCaseInsensitive ?? []),\n );\n caseSensitiveAbbreviations.push(\n ...(entry.abbreviationsCaseSensitive ?? []),\n );\n }\n\n const branches: string[] = [];\n const wordsAlt = toSortedAlternation(words);\n const abbreviationCiAlt = toSortedAlternation(caseInsensitiveAbbreviations);\n const abbreviationCsAlt = toSortedAlternation(caseSensitiveAbbreviations);\n\n if (wordsAlt) {\n branches.push(`[^\\\\S\\\\n\\\\t]+(?i:(?:${wordsAlt}))\\\\b`);\n }\n if (abbreviationCiAlt) {\n branches.push(`[^\\\\S\\\\n\\\\t]?(?i:${abbreviationCiAlt})\\\\b`);\n }\n if (abbreviationCsAlt) {\n branches.push(`[^\\\\S\\\\n\\\\t]?(?:${abbreviationCsAlt})\\\\b`);\n }\n\n const required = branches.length > 0 ? `(?:${branches.join(\"|\")})` : \"\";\n return {\n optional: required ? `${required}?` : \"\",\n required,\n prefilterTerms: [\n ...words,\n ...caseInsensitiveAbbreviations,\n ...caseSensitiveAbbreviations,\n ],\n };\n};\n\nconst buildQuantityFollowerGuard = (config: AmountWordsConfig): string => {\n const modifiers: string[] = [];\n const nouns: string[] = [];\n\n for (const entry of config.shareQuantityTerms ?? []) {\n modifiers.push(...(entry.modifiers ?? []));\n nouns.push(...entry.nouns);\n }\n\n const modifierAlt = toSortedAlternation(modifiers);\n const nounAlt = toSortedAlternation(nouns);\n if (!nounAlt) return \"\";\n\n const modifierPrefix = modifierAlt\n ? `(?:(?:${modifierAlt})[^\\\\S\\\\n\\\\t]+){0,3}`\n : \"\";\n\n return `(?![^\\\\S\\\\n\\\\t]+(?i:` + `${modifierPrefix}(?:${nounAlt}))\\\\b)`;\n};\n\nconst buildFinancialLexicons = (\n config: AmountWordsConfig,\n): FinancialLexicons => {\n const magnitude = buildMagnitudePattern(config);\n return Object.freeze({\n magnitudeOptional: magnitude.optional,\n magnitudeRequired: magnitude.required,\n magnitudePrefilterTerms: magnitude.prefilterTerms,\n quantityFollowerGuard: buildQuantityFollowerGuard(config),\n });\n};\n\nconst FINANCIAL_LEXICONS = buildFinancialLexicons(AMOUNT_WORDS);\n\n/**\n * Build symbol character class, code alternation,\n * and local-name alternation from currencies.json,\n * then return two monetary amount patterns: leading\n * symbol and trailing code/name.\n *\n * The number sub-pattern accepts both grouped\n * thousands (1,000) and plain integers (100000)\n * via `\\d{1,9}` to catch unformatted amounts.\n */\nconst buildCurrencyPatternEntries = (\n data: CurrenciesData,\n): CurrencyPatternEntry[] => {\n const symbols = data.symbols.map(escapeCharClass).join(\"\");\n\n // Build trailing alternation: ISO codes (case-\n // sensitive, always uppercase) + local names.\n // Local names that contain only ASCII letters\n // are wrapped in (?i:...) for case-insensitive\n // matching; abbreviations with non-ASCII or\n // punctuation (Kč, zł, Fr.) stay case-sensitive.\n // Sorted longest-first to avoid partial matches.\n const isAsciiAlpha = /^[a-zA-Z\\s]+$/;\n\n type CurrencyTermPart = { term: string; len: number; alt: string };\n const codeParts: CurrencyTermPart[] = data.codes.map((code) => ({\n term: code,\n len: code.length,\n alt: escapeRegex(code),\n }));\n const localNameParts: CurrencyTermPart[] = [];\n\n // Minimum length for case-insensitive wrapping.\n // Short abbreviations like \"Ft\" (2 chars) stay\n // case-sensitive to avoid collisions (Ft vs ft/feet).\n const MIN_CI_LENGTH = 3;\n\n if (data.localNames) {\n for (const name of data.localNames) {\n const escaped = escapeRegex(name);\n const wrapCI = isAsciiAlpha.test(name) && name.length >= MIN_CI_LENGTH;\n if (wrapCI) {\n localNameParts.push({\n term: name,\n len: name.length,\n alt: `(?i:${escaped})`,\n });\n } else {\n localNameParts.push({\n term: name,\n len: name.length,\n alt: escaped,\n });\n }\n }\n }\n\n // Also include currency symbols as trailing\n // alternatives (e.g., \"126 €\", \"8 190 £\").\n // These are common in European notation.\n const toPartAlternation = (parts: readonly CurrencyTermPart[]): string =>\n parts\n .toSorted((a, b) => b.len - a.len)\n .map((p) => p.alt)\n .join(\"|\");\n const codeAlt = toPartAlternation(codeParts);\n const localNameAlt = toPartAlternation(localNameParts);\n const codeTerms = codeParts.map((part) => part.term);\n const localNameTerms = localNameParts.map((part) => part.term);\n const trailingAlt = [...codeParts, ...localNameParts]\n .toSorted((a, b) => b.len - a.len)\n .map((p) => p.alt)\n .join(\"|\");\n\n if (!symbols && !trailingAlt) return [];\n\n // Number sub-pattern: grouped thousands OR plain\n // integer up to 9 digits (covers unformatted\n // amounts like \"100000 CZK\").\n const NUM = `(?:\\\\d{1,3}(?:[,.'[^\\\\S\\\\n\\\\t]]\\\\d{3})+` + `|\\\\d{1,9})`;\n const PREFILTER_NUM = `(?:\\\\d{1,3}(?:[,.'\\\\s]\\\\d{3})+|\\\\d{1,9})`;\n const PREFILTER_DECIMAL =\n `(?:[.,](?=\\\\d|[${DASH_INNER}])` +\n `\\\\s?(?:\\\\d{1,2}${DASH}?|${DASH}{1,2}))?`;\n\n const patterns: CurrencyPatternEntry[] = [];\n const lazyCurrencyPattern = (\n pattern: string,\n prefilterAny: readonly string[],\n prefilterCaseInsensitive: boolean,\n prefilterRegex?: RegExp,\n ): CurrencyPatternEntry => ({\n pattern,\n lazy: true,\n prefilterAny,\n prefilterCaseInsensitive,\n ...(prefilterRegex ? { prefilterRegex } : {}),\n });\n const makeLeadingPrefilter = (\n terms: readonly string[],\n caseInsensitive: boolean,\n ): RegExp | undefined => {\n const termAlt = toSortedAlternation(terms);\n if (!termAlt) return undefined;\n return new RegExp(\n `(?:^|[^\\\\p{L}\\\\p{N}_])(?:${termAlt})[^\\\\S\\\\n\\\\t]{0,2}\\\\d`,\n caseInsensitive ? \"iu\" : \"u\",\n );\n };\n const makeTrailingPrefilter = (\n terms: readonly string[],\n caseInsensitive: boolean,\n ): RegExp | undefined => {\n const termAlt = toSortedAlternation(terms);\n if (!termAlt) return undefined;\n return new RegExp(\n `${PREFILTER_NUM}${PREFILTER_DECIMAL}[^\\\\S\\\\n\\\\t]{0,4}(?:${termAlt})`,\n caseInsensitive ? \"iu\" : \"u\",\n );\n };\n const makeLeadingMagnitudePrefilter = (\n terms: readonly string[],\n caseInsensitive: boolean,\n ): RegExp | undefined => {\n const termAlt = toSortedAlternation(terms);\n const magnitudeAlt = toSortedAlternation(\n FINANCIAL_LEXICONS.magnitudePrefilterTerms,\n );\n if (!termAlt || !magnitudeAlt) return undefined;\n return new RegExp(\n `(?:^|[^\\\\p{L}\\\\p{N}_])(?:${termAlt})` +\n `[^\\\\S\\\\n\\\\t]{0,2}${PREFILTER_NUM}${PREFILTER_DECIMAL}` +\n `[^\\\\S\\\\n\\\\t]{0,8}(?:${magnitudeAlt})(?:$|[^\\\\p{L}\\\\p{N}_])`,\n caseInsensitive ? \"iu\" : \"u\",\n );\n };\n const makeLeadingSymbolMagnitudePrefilter = (): RegExp | undefined => {\n const magnitudeAlt = toSortedAlternation(\n FINANCIAL_LEXICONS.magnitudePrefilterTerms,\n );\n if (!magnitudeAlt) return undefined;\n return new RegExp(\n `(?:^|[^\\\\p{L}\\\\p{N}_])(?:[${symbols}])` +\n `[^\\\\S\\\\n\\\\t]{0,2}${PREFILTER_NUM}${PREFILTER_DECIMAL}` +\n `[^\\\\S\\\\n\\\\t]{0,8}(?:${magnitudeAlt})(?:$|[^\\\\p{L}\\\\p{N}_])`,\n \"iu\",\n );\n };\n const makeTrailingMagnitudePrefilter = (\n terms: readonly string[],\n caseInsensitive: boolean,\n ): RegExp | undefined => {\n const termAlt = toSortedAlternation(terms);\n const magnitudeAlt = toSortedAlternation(\n FINANCIAL_LEXICONS.magnitudePrefilterTerms,\n );\n if (!termAlt || !magnitudeAlt) return undefined;\n return new RegExp(\n `${PREFILTER_NUM}${PREFILTER_DECIMAL}` +\n `[^\\\\S\\\\n\\\\t]{0,8}(?:${magnitudeAlt})(?:$|[^\\\\p{L}\\\\p{N}_])` +\n `[\\\\s\\\\S]{0,24}(?:${termAlt})`,\n caseInsensitive ? \"iu\" : \"u\",\n );\n };\n\n // Decimal part: dot/comma must be followed by at\n // least one digit or dash. Without this, a trailing\n // sentence period (\"$25,000,000.\") gets consumed by\n // the optional group, breaking the \\b anchor.\n // Use lookahead (?=\\d|DASH) after [.,] to ensure\n // the separator is actually a decimal marker.\n const DECIMAL =\n `(?:[.,](?=\\\\d|[${DASH_INNER}])` +\n `[^\\\\S\\\\n\\\\t]?` +\n `(?:\\\\d{1,2}${DASH}?|${DASH}{1,2}))?`;\n const END = `(?:\\\\b|(?=\\\\s|[.,;!?)]|$))`;\n\n const MAGNITUDE_OPTIONAL = FINANCIAL_LEXICONS.magnitudeOptional;\n const MAGNITUDE_REQUIRED = FINANCIAL_LEXICONS.magnitudeRequired;\n\n // Leading symbol: $100, €1,000.50, € 100000.\n // Magnitude-bearing forms ($25 million, $2bn)\n // are a separate pattern: making the magnitude\n // suffix optional in this very broad symbol pattern\n // forces the regex engine to do much more work on\n // EDGAR-style contracts with large numeric sections.\n if (symbols) {\n patterns.push(\n lazyCurrencyPattern(\n `(?:[${symbols}])` + `[^\\\\S\\\\n\\\\t]?` + `${NUM}${DECIMAL}${END}`,\n data.symbols,\n true,\n ),\n );\n if (MAGNITUDE_REQUIRED) {\n patterns.push(\n lazyCurrencyPattern(\n `(?:[${symbols}])` +\n `[^\\\\S\\\\n\\\\t]?` +\n `${NUM}${DECIMAL}${MAGNITUDE_REQUIRED}${END}`,\n data.symbols,\n true,\n makeLeadingSymbolMagnitudePrefilter(),\n ),\n );\n }\n }\n\n // Leading multi-char code: \"Kč 10,—\", \"Fr. 500\",\n // \"EUR 1.5 billion\".\n if (codeAlt) {\n patterns.push(\n lazyCurrencyPattern(\n `\\\\b(?:${codeAlt})` + `[^\\\\S\\\\n\\\\t]{0,2}` + `${NUM}${DECIMAL}${END}`,\n codeTerms,\n false,\n makeLeadingPrefilter(codeTerms, false),\n ),\n );\n if (MAGNITUDE_REQUIRED) {\n patterns.push(\n lazyCurrencyPattern(\n `\\\\b(?:${codeAlt})` +\n `[^\\\\S\\\\n\\\\t]{0,2}` +\n `${NUM}${DECIMAL}${MAGNITUDE_REQUIRED}${END}`,\n codeTerms,\n false,\n makeLeadingMagnitudePrefilter(codeTerms, false),\n ),\n );\n }\n }\n if (localNameAlt) {\n patterns.push(\n lazyCurrencyPattern(\n `\\\\b(?:${localNameAlt})` +\n `[^\\\\S\\\\n\\\\t]{0,2}` +\n `${NUM}${DECIMAL}${END}`,\n localNameTerms,\n true,\n makeLeadingPrefilter(localNameTerms, true),\n ),\n );\n if (MAGNITUDE_REQUIRED) {\n patterns.push(\n lazyCurrencyPattern(\n `\\\\b(?:${localNameAlt})` +\n `[^\\\\S\\\\n\\\\t]{0,2}` +\n `${NUM}${DECIMAL}${MAGNITUDE_REQUIRED}${END}`,\n localNameTerms,\n true,\n makeLeadingMagnitudePrefilter(localNameTerms, true),\n ),\n );\n }\n }\n\n // Trailing code/name: 100 USD, 1,000.50 CZK,\n // 100000 Kč, 500 korun, 100 Fr., 25 million USD,\n // $25 million USD.\n // Magnitude sits between the number and the code so\n // \"100 million USD\" parses naturally; the existing\n // 0-4 whitespace span absorbs the separator.\n const optionalLeadingSymbol = symbols\n ? `(?<![\\\\p{L}\\\\p{N}_])(?:[${symbols}][^\\\\S\\\\n\\\\t]?)?`\n : \"\\\\b\";\n if (codeAlt) {\n patterns.push(\n lazyCurrencyPattern(\n `${optionalLeadingSymbol}${NUM}${DECIMAL}` +\n `[^\\\\S\\\\n\\\\t]{0,4}` +\n `(?:${codeAlt})${END}`,\n codeTerms,\n false,\n makeTrailingPrefilter(codeTerms, false),\n ),\n );\n if (MAGNITUDE_REQUIRED) {\n patterns.push(\n lazyCurrencyPattern(\n `${optionalLeadingSymbol}${NUM}${DECIMAL}${MAGNITUDE_REQUIRED}` +\n `[^\\\\S\\\\n\\\\t]{0,4}` +\n `(?:${codeAlt})${FINANCIAL_LEXICONS.quantityFollowerGuard}${END}`,\n codeTerms,\n false,\n makeTrailingMagnitudePrefilter(codeTerms, false),\n ),\n );\n }\n }\n if (localNameAlt) {\n patterns.push(\n lazyCurrencyPattern(\n `${optionalLeadingSymbol}${NUM}${DECIMAL}` +\n `[^\\\\S\\\\n\\\\t]{0,4}` +\n `(?:${localNameAlt})${END}`,\n localNameTerms,\n true,\n makeTrailingPrefilter(localNameTerms, true),\n ),\n );\n if (MAGNITUDE_REQUIRED) {\n patterns.push(\n lazyCurrencyPattern(\n `${optionalLeadingSymbol}${NUM}${DECIMAL}${MAGNITUDE_REQUIRED}` +\n `[^\\\\S\\\\n\\\\t]{0,4}` +\n `(?:${localNameAlt})${FINANCIAL_LEXICONS.quantityFollowerGuard}${END}`,\n localNameTerms,\n true,\n makeTrailingMagnitudePrefilter(localNameTerms, true),\n ),\n );\n }\n }\n\n if (symbols) {\n const trailingSymbolPrefilter = new RegExp(\n `${PREFILTER_NUM}${PREFILTER_DECIMAL}[^\\\\S\\\\n\\\\t]{0,4}[${symbols}]`,\n \"u\",\n );\n patterns.push(\n lazyCurrencyPattern(\n `${NUM}${DECIMAL}${MAGNITUDE_OPTIONAL}` +\n `[^\\\\S\\\\n\\\\t]{0,4}` +\n `(?:[${symbols}])${END}`,\n data.symbols,\n true,\n trailingSymbolPrefilter,\n ),\n );\n }\n\n return patterns;\n};\n\n/** Cached promise for currency patterns. Loaded once. */\nlet currencyPatternPromise: Promise<string[]> | null = null;\nlet currencyPatternEntryPromise: Promise<CurrencyPatternEntry[]> | null = null;\n\nconst loadCurrencyPatternEntries = async (): Promise<\n CurrencyPatternEntry[]\n> => {\n const mod = await import(\"../data/currencies.json\");\n const data: CurrenciesData = mod.default ?? mod;\n return buildCurrencyPatternEntries(data);\n};\n\nconst loadCurrencyPatterns = async (): Promise<string[]> =>\n (await loadCurrencyPatternEntries()).map((entry) => entry.pattern);\n\n/**\n * Get dynamically built monetary amount patterns from\n * currencies.json. Returns a cached promise; the JSON\n * is loaded only once.\n */\nexport const getCurrencyPatterns = (): Promise<string[]> => {\n if (!currencyPatternPromise) {\n currencyPatternPromise = loadCurrencyPatterns().catch((err) => {\n currencyPatternPromise = null;\n throw err;\n });\n }\n return currencyPatternPromise;\n};\n\nexport const getCurrencyPatternEntries = (): Promise<\n CurrencyPatternEntry[]\n> => {\n if (!currencyPatternEntryPromise) {\n currencyPatternEntryPromise = loadCurrencyPatternEntries().catch((err) => {\n currencyPatternEntryPromise = null;\n throw err;\n });\n }\n return currencyPatternEntryPromise;\n};\n\n/** Currency pattern metadata (score 0.9). */\nexport const CURRENCY_PATTERN_META: Readonly<RegexMeta> = Object.freeze({\n label: \"monetary amount\",\n score: 0.9,\n});\n\n// ── Public API ──────────────────────────────────────\n\n/**\n * Process regex matches from the unified search.\n * Receives all matches; filters to the regex slice\n * via sliceStart/sliceEnd. Local index into META is\n * match.pattern - sliceStart.\n *\n * For stdnum-derived patterns (those with a validator\n * in META), the matched text is passed through the\n * validator's validate() method. If validation fails,\n * the match is discarded as a false positive.\n */\nexport const processRegexMatches = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n meta_: readonly RegexMeta[],\n): Entity[] => {\n const results: Entity[] = [];\n\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) {\n continue;\n }\n\n const localIdx = idx - sliceStart;\n const meta = meta_[localIdx];\n if (!meta) {\n continue;\n }\n if (\n meta.sourceDetail !== \"custom-regex\" &&\n meta.label === \"phone number\" &&\n match.text.length < MIN_PHONE_LENGTH\n ) {\n continue;\n }\n\n // Post-match validation: if the pattern came from\n // a stdnum validator, compact (strip separators)\n // then validate. The candidate regex may capture\n // spaced/dashed variants that validate() rejects\n // without compaction.\n if (meta.validator) {\n const compacted = meta.validator.compact(match.text);\n const result = meta.validator.validate(compacted);\n if (!result.valid) {\n continue;\n }\n }\n\n const entity: Entity = {\n start: match.start,\n end: match.end,\n label: meta.label,\n text: match.text,\n score: meta.score,\n source: DETECTION_SOURCES.REGEX,\n };\n if (meta.sourceDetail) {\n entity.sourceDetail = meta.sourceDetail;\n }\n results.push(entity);\n }\n\n return results;\n};\n\n// ── Dynamic signing clause patterns ────────────────\n\ntype SigningClauseConfig = {\n patterns: Array<{\n lang: string;\n prefix: string;\n suffix: string;\n prepositions: string[];\n }>;\n};\n\n/**\n * Build signing clause place-name patterns from\n * signing-clauses.json. Each pattern captures the\n * city/place name from contract signing locations.\n *\n * The place name sub-pattern:\n * \\p{Lu}\\p{Ll}+ (capitalized word)\n * optionally followed by preposition + capitalized\n * word (for \"nad Nisou\", \"am Main\", etc.)\n * optionally followed by more capitalized words\n * (for \"Hradec Králové\", \"New York\", etc.)\n */\nconst buildSigningClausePatterns = (data: SigningClauseConfig): string[] => {\n const patterns: string[] = [];\n\n for (const entry of data.patterns) {\n const prepAlt =\n entry.prepositions.length > 0 ? entry.prepositions.join(\"|\") : null;\n\n // Place name: Uppercase word, optionally with\n // preposition + uppercase, optionally more caps\n const place = prepAlt\n ? `(\\\\p{Lu}\\\\p{Ll}+` +\n `(?:\\\\s+(?:${prepAlt})\\\\s+\\\\p{Lu}\\\\p{Ll}+)*` +\n `(?:\\\\s+\\\\p{Lu}\\\\p{Ll}+)*)`\n : `(\\\\p{Lu}\\\\p{Ll}+(?:[- ]\\\\p{Lu}\\\\p{Ll}+)*)`;\n\n const full =\n `(?:^|\\\\n|[^\\\\S\\\\n])` +\n entry.prefix +\n place +\n (entry.suffix ? `(?:${entry.suffix})` : \"\");\n\n patterns.push(full);\n }\n\n return patterns;\n};\n\nexport const SIGNING_CLAUSE_META: Readonly<RegexMeta> = {\n label: \"address\",\n score: 0.9,\n};\n\nlet signingPatternPromise: Promise<string[]> | null = null;\n\nconst loadSigningPatterns = async (): Promise<string[]> => {\n const mod = await import(\"../data/signing-clauses.json\");\n const data: SigningClauseConfig = mod.default ?? mod;\n return buildSigningClausePatterns(data);\n};\n\nexport const getSigningClausePatterns = (): Promise<string[]> => {\n if (!signingPatternPromise) {\n signingPatternPromise = loadSigningPatterns().catch((err) => {\n signingPatternPromise = null;\n throw err;\n });\n }\n return signingPatternPromise;\n};\n","/**\n * Legal-form ORG detection — candidate + validator architecture.\n *\n * Replaces only the front-end of the v1 path: where v1 builds a\n * ~7 KB monolithic regex (greedy head + tail + ~600-suffix\n * alternation + nested Unicode-aware lookarounds) and feeds the\n * whole thing into `@stll/text-search`, v2 splits the work in two:\n *\n * 1. AC-flavoured literal lookup over the ~330-entry suffix\n * lexicon — small per-suffix patterns the regex backend\n * handles in microseconds, no DFA blowup against long\n * preambles with embedded parentheticals.\n * 2. A small TS-side backward walker that constructs the rough\n * span around each suffix occurrence (head words + connectors\n * + lowercase tail tokens), matching the shape v1's regex\n * would have produced.\n *\n * The rough span is then handed to v1's `processLegalFormMatches`\n * unchanged — that function already implements every validator\n * the v1 pipeline depends on (role-head sentence-verb trim, leading\n * clause trim, embedded list split, line-break / single-cap /\n * all-caps-line rejection, post-match accented-letter boundary,\n * etc.). The wins are:\n *\n * - 17–730× speedup vs the monolithic regex (see PR description).\n * - Sidesteps the upstream text-search DFA bug that drops every\n * match on long preambles with embedded `(this \"Agreement\")`.\n * - No carved-up validator logic — the 1671-line code-side\n * validation chain is reused as-is.\n */\n\nimport type { Match, TextSearch } from \"@stll/text-search\";\n\nimport { getTextSearch } from \"../search-engine\";\nimport type { Entity } from \"../types\";\nimport { normalizeForSearch } from \"../util/normalize\";\nimport {\n getClauseNounHeadsSync,\n getKnownLegalSuffixes,\n getLegalRoleHeadsSync,\n getSentenceVerbIndicatorsSync,\n processLegalFormMatches,\n warmLegalRoleHeads,\n} from \"./legal-forms\";\n\n// Normalised suffix set for the \"word before `and` is itself a\n// legal-form suffix\" boundary check — strip dots/spaces so \"LLC\",\n// \"Inc.\", and \"s.r.o.\" all reduce to a comparable token.\nconst normalizeSuffixToken = (s: string): string =>\n s.replace(/[.,\\s]/g, \"\").toLowerCase();\n\nlet cachedNormalizedSuffixes: ReadonlySet<string> | null = null;\nconst getNormalizedSuffixSet = (): ReadonlySet<string> => {\n if (cachedNormalizedSuffixes !== null) return cachedNormalizedSuffixes;\n const out = new Set<string>();\n for (const s of getKnownLegalSuffixes()) {\n const n = normalizeSuffixToken(s);\n if (n.length > 0) out.add(n);\n }\n cachedNormalizedSuffixes = out;\n return out;\n};\n\nconst isLegalFormSuffixWord = (word: string): boolean => {\n const n = normalizeSuffixToken(word);\n if (n.length === 0) return false;\n return getNormalizedSuffixSet().has(n);\n};\n\n// ── Suffix index (AC-flavoured literal pass) ────────────────────\n\nlet cachedSuffixSearch: { ts: TextSearch; suffixes: readonly string[] } | null =\n null;\n\nconst getSuffixSearch = (): {\n ts: TextSearch;\n suffixes: readonly string[];\n} => {\n const suffixes = getKnownLegalSuffixes();\n if (cachedSuffixSearch !== null && cachedSuffixSearch.suffixes === suffixes) {\n return cachedSuffixSearch;\n }\n // `getKnownLegalSuffixes` already returns the list sorted\n // longest-first, which gives the regex backend longest-match-\n // first behaviour for overlapping forms like `LLP` vs `LLLP` vs\n // `PLLC`.\n const patterns = suffixes.map((suffix) => ({\n pattern: suffix,\n literal: true as const,\n }));\n // Use the injected TextSearch constructor so the wasm build\n // (`@stll/anonymize-wasm`) gets its own indirection — a static\n // import from `@stll/text-search` here would bypass the\n // `initTextSearch`/`getTextSearch` wiring in `src/wasm.ts`.\n const Ctor = getTextSearch();\n const ts = new Ctor(patterns);\n cachedSuffixSearch = { ts, suffixes };\n return cachedSuffixSearch;\n};\n\n// ── Boundary helpers ────────────────────────────────────────────\n\nconst ANY_LETTER_RE = /\\p{L}/u;\nconst ANY_LETTER_OR_DIGIT_RE = /[\\p{L}\\p{N}]/u;\n\nconst isTrailingBoundary = (fullText: string, end: number): boolean => {\n if (end >= fullText.length) return true;\n const next = fullText.charAt(end);\n // Reject when the suffix bleeds into a real word — covers both\n // ASCII (`LLCx`) and accented Latin (`AGÊNCIA`). Digit\n // continuations (`LLC123`) are not org-name boundaries either.\n if (ANY_LETTER_RE.test(next)) return false;\n if (/\\d/.test(next)) return false;\n return true;\n};\n\nconst isLeadingSeparator = (fullText: string, suffixStart: number): boolean => {\n if (suffixStart === 0) return true;\n const prev = fullText.charAt(suffixStart - 1);\n // The separator between the head and the suffix is at most\n // a single space, comma, or run of those — anything else\n // means we'd be slicing into a real word.\n if (ANY_LETTER_OR_DIGIT_RE.test(prev)) return false;\n // A dot preceded by a letter is the inside of a longer dotted\n // abbreviation (`18 U.S.C.` → the `S.C.` hit is inside the citation,\n // not the start of a fresh suffix). The same shape is fine when a\n // CJK/space precedes the dot; only a Latin letter signals continuation.\n if (\n prev === \".\" &&\n suffixStart >= 2 &&\n ANY_LETTER_RE.test(fullText.charAt(suffixStart - 2))\n ) {\n return false;\n }\n return true;\n};\n\n// ── Greedy backward walker ──────────────────────────────────────\n//\n// Emits a rough span around each suffix occurrence, large enough\n// that v1's `processLegalFormMatches` validators can decide how\n// much (if any) to trim. The walk admits the same token shapes\n// v1's regex `(head + optional lowercase tail)` admits:\n//\n// head : CapWord | ALL-CAPS | single Cap | digit\n// tail : LowerWord | CapWord | ALL-CAPS | digit\n// connected by a SimpleSep (HSPACE, comma, dash, dot, &)\n// or LowerConnector (a / and / und / et / e / y / i)\n//\n// Up to 20 tokens total, matching the per-prefix cap in\n// `buildPatternString`. The walker never crosses a hard newline.\n\nconst HEAD_TOKEN_CAP = 20;\n\nconst TOKEN_RE = /[\\p{L}\\p{N}'’.&-]+/u;\n\n// Treat NBSP (U+00A0) and narrow NBSP (U+202F) as whitespace —\n// DOCX exports routinely use them between name tokens and the\n// trailing legal-form suffix (\"Acme s.r.o.\").\nconst isInterTokenWs = (ch: string): boolean =>\n ch === \" \" || ch === \"\\t\" || ch === \" \" || ch === \" \" || ch === \",\";\n\nconst findTokenBefore = (\n fullText: string,\n pos: number,\n): { start: number; end: number; text: string } | null => {\n let end = pos;\n // Skip horizontal whitespace / commas back. Refuse hard newlines.\n while (end > 0) {\n const ch = fullText.charAt(end - 1);\n if (ch === \"\\n\") return null;\n if (isInterTokenWs(ch) || ch === \";\") {\n end--;\n continue;\n }\n break;\n }\n if (end === 0) return null;\n let start = end;\n while (start > 0) {\n const ch = fullText.charAt(start - 1);\n if (ch === \"\\n\") break;\n if (!TOKEN_RE.test(ch)) break;\n start--;\n }\n if (start === end) return null;\n return { start, end, text: fullText.slice(start, end) };\n};\n\n// `Cena. KB poskytla úvěr.` — the AC suffix lookup catches `KB`,\n// the backward walker grabs `Cena.` because the `.` is admitted\n// by TOKEN_RE. Reject candidates whose backward span crosses a\n// sentence-ending period — `<Lu><Ll>+\\.<space>` immediately\n// before the suffix anchor is the structural marker.\nconst crossesSentenceEnd = (\n fullText: string,\n candidateStart: number,\n suffixStart: number,\n): boolean => {\n // Sentence-end shape immediately inside the candidate range\n // signals that the walker swept across a sentence boundary\n // (`...Cena. KB poskytla úvěr.` or `... Co.\\nLLC. Except for\n // Goldman Sachs & Co. LLC`). Two shapes count:\n // `<Cap><lowercase{2,}>.<whitespace>` — ordinary sentence\n // like `Cena.<space>`.\n // `<Cap>{2,}\\.<whitespace>` — all-caps acronym or\n // legal-form suffix used\n // sentence-finally like\n // `LLC.<space>Except`.\n const slice = fullText.slice(candidateStart, suffixStart);\n return /\\p{Lu}\\p{Ll}{2,}\\.\\s/u.test(slice) || /\\p{Lu}{2,}\\.\\s/u.test(slice);\n};\n\nconst UPPER_LETTER_RE = /^\\p{Lu}/u;\nconst LOWER_LETTER_RE = /^\\p{Ll}/u;\nconst DIGIT_RE = /^\\d/;\nconst CONNECTOR_RE = /^(?:a|and|und|et|e|y|i|&)$/i;\n\nconst isAcceptableToken = (tok: string): boolean => {\n if (tok.length === 0) return false;\n if (UPPER_LETTER_RE.test(tok)) return true;\n if (DIGIT_RE.test(tok)) return true;\n if (CONNECTOR_RE.test(tok)) return true;\n // Lowercase tail tokens (`pracovní`, `plošiny`, `s`, `r`, `o`)\n // are admitted — v1's tail allowed up to 10 of them. The\n // role-head trim downstream rips them out when the leading\n // chunk turns out to be clause prose.\n if (LOWER_LETTER_RE.test(tok)) return true;\n return false;\n};\n\n// Multi-char \"and\"-type connectors. v1's `extendBackward` refuses\n// to cross one when only one uppercase word precedes it (\"Paul\n// Newman and Apple, Inc.\" → \"Apple, Inc.\") because the leading\n// pattern looks like a person name. Three or more upper words\n// before the connector is a real multi-word org name and the\n// walker crosses (\"UniCredit Bank Czech Republic and Slovakia,\n// a.s.\"). v2 applies the same rule on the way back.\nconst AND_TYPE_CONNECTOR_RE = /^(?:and|und|et)$/i;\n\n// \"Elon R. Musk and X Corp.\" — the middle initial `R.` is a Cap\n// token but the whole shape is a personal name, not a multi-word\n// org. v1 looks for the `<Initial>.<HSPACE><Surname>` shape\n// immediately before the connector. The check here looks at the\n// 32 chars preceding the connector and asks whether they end in\n// `<Lu>.<space><Word><whitespace>` — i.e. an initial + dot, then\n// a final surname token right before the connector.\nconst MIDDLE_INITIAL_RE = /\\p{Lu}\\.[^\\S\\n]+\\p{L}[\\p{L}\\p{M}'’]*[^\\S\\n]*$/u;\nconst hasMiddleInitialBefore = (fullText: string, pos: number): boolean => {\n const slice = fullText.slice(Math.max(0, pos - 32), pos);\n return MIDDLE_INITIAL_RE.test(slice);\n};\n\n/**\n * Count consecutive uppercase-starting tokens immediately before\n * `pos`. Stops at the first non-upper token, a hard newline, or\n * text start.\n */\nconst countUpperBefore = (fullText: string, pos: number): number => {\n let scan = pos;\n let count = 0;\n while (true) {\n const tok = findTokenBefore(fullText, scan);\n if (!tok) break;\n if (!UPPER_LETTER_RE.test(tok.text)) break;\n count++;\n scan = tok.start;\n }\n return count;\n};\n\n// Once at least one capitalised token has been admitted, refuse to\n// bridge more than this many consecutive lowercase tokens. The bridge\n// cap protects against sentence-prose sweeps like\n// `Specifikace a přesná poloha místností v areálu Základní školy …`\n// where a sentence-start CapWord precedes a long prose run. Long\n// lowercase TAILS preceding the first CapWord stay admitted, so\n// Czech state-form names with many descriptor tokens\n// (`Národní agentura pro podporu rozvoje vzdělávání … republiky,\n// z.s.`) survive — the cap only fires after a CapWord is already in\n// the span.\nconst MAX_LOWER_BRIDGE = 4;\n\nconst walkBackward = (fullText: string, suffixStart: number): number => {\n let pos = suffixStart;\n let stepsLeft = HEAD_TOKEN_CAP;\n let leftmostCapPos = -1;\n let lowerBridgeRun = 0;\n\n while (stepsLeft > 0) {\n const tok = findTokenBefore(fullText, pos);\n if (!tok) break;\n if (!isAcceptableToken(tok.text)) break;\n\n // Clause-descriptor boundary. A lowercase token that is\n // ITSELF a known legal-form descriptor (`corporation`,\n // `company`, `s.r.o.`, …) followed by a comma marks a clause\n // break (`Delaware corporation, X Holdings I, Inc.` — the\n // comma sits between two separate entities). Without the\n // suffix-word gate this would also fire inside long Czech\n // names whose lowercase tail is part of the name\n // (`Krajská správa, příspěvková organizace`), so we require\n // both the suffix-word match AND a CapWord already accepted\n // to the right.\n if (LOWER_LETTER_RE.test(tok.text) && leftmostCapPos >= 0) {\n const afterTok = fullText.slice(tok.end, pos);\n if (/^[,;]/.test(afterTok) && isLegalFormSuffixWord(tok.text)) break;\n }\n\n // Connector boundary checks. We're considering crossing a\n // connector — three reasons not to:\n //\n // (a) The word immediately BEFORE the connector is itself\n // a legal-form suffix (`Morgan Securities LLC and Allen\n // & Company LLC`, `RELAKA s.r.o. a AGROBIOPLYN s.r.o.`\n // — the connector sits between two complete orgs, not\n // inside one). Applies to every connector.\n // (b) (`and`-type only) ≤2 uppercase tokens precede the\n // connector — `Paul Newman and X, Inc.` shape.\n // (c) (`and`-type only) the previous token is a middle\n // initial (`Elon R. Musk and X Corp.` — the personal\n // name extends through the initial dot).\n //\n // Three uppercase tokens or more before an `and`-type connector\n // signal a real multi-word org name (`UniCredit Bank Czech\n // Republic and Slovakia, a.s.`) and the walker crosses.\n if (CONNECTOR_RE.test(tok.text)) {\n const prevPeek = findTokenBefore(fullText, tok.start);\n if (prevPeek && isLegalFormSuffixWord(prevPeek.text)) break;\n if (AND_TYPE_CONNECTOR_RE.test(tok.text)) {\n const upperBefore = countUpperBefore(fullText, tok.start);\n if (upperBefore <= 2) break;\n if (hasMiddleInitialBefore(fullText, tok.start)) break;\n }\n }\n\n if (UPPER_LETTER_RE.test(tok.text)) {\n leftmostCapPos = tok.start;\n lowerBridgeRun = 0;\n } else if (LOWER_LETTER_RE.test(tok.text)) {\n if (leftmostCapPos >= 0) {\n lowerBridgeRun++;\n if (lowerBridgeRun > MAX_LOWER_BRIDGE) break;\n }\n } else {\n lowerBridgeRun = 0;\n }\n pos = tok.start;\n stepsLeft--;\n }\n\n return leftmostCapPos < 0 ? suffixStart : leftmostCapPos;\n};\n\nconst CAP_TOKEN_RE = /(?<![\\p{L}\\p{N}])\\p{Lu}[\\p{L}\\p{M}\\p{N}'’.&-]*/gu;\nconst LOWER_WORD_RE = /(?<![\\p{L}\\p{N}])\\p{Ll}[\\p{L}\\p{M}'’]*/gu;\n\n/**\n * Post-walker verb gate. The walker admits any lowercase token so\n * Czech and German extended state-form tails (`Krajská správa,\n * příspěvková organizace`, `Národní agentura pro komunikační a\n * informační technologie, s. p.`) keep their full vocabulary. The\n * trade-off is that prose sentences ending in a legal-form descriptor\n * (`...převádí na příspěvková organizace.`) get swept too. When the\n * candidate text contains a known lowercase sentence-verb token we\n * slide `candidateStart` forward to the first capitalised token after\n * the last verb; if no capitalised token follows the verb, the\n * candidate is prose and gets dropped.\n */\nconst trimToFirstCapAfterVerb = (\n fullText: string,\n candidateStart: number,\n suffixStart: number,\n): number => {\n if (candidateStart >= suffixStart) return candidateStart;\n const head = fullText.slice(candidateStart, suffixStart);\n const verbIndicators = getSentenceVerbIndicatorsSync();\n let lastVerbEnd = -1;\n for (const wordMatch of head.matchAll(LOWER_WORD_RE)) {\n if (\n wordMatch.index !== undefined &&\n verbIndicators.has(wordMatch[0].toLowerCase())\n ) {\n lastVerbEnd = wordMatch.index + wordMatch[0].length;\n }\n }\n if (lastVerbEnd < 0) return candidateStart;\n // Skip role-heads (`Licensee`, `Buyer`, `Prodávající`) and clause\n // nouns (`Agreement`, `Section`, `Schedule`) that appear between\n // the verb and the real organisation name — both behave like\n // appositive labels, not company names.\n const roleHeads = getLegalRoleHeadsSync();\n const clauseNouns = getClauseNounHeadsSync();\n CAP_TOKEN_RE.lastIndex = lastVerbEnd;\n for (\n let next = CAP_TOKEN_RE.exec(head);\n next !== null;\n next = CAP_TOKEN_RE.exec(head)\n ) {\n const word = next[0].toLowerCase();\n if (roleHeads.has(word) || clauseNouns.has(word)) continue;\n return candidateStart + next.index;\n }\n return suffixStart;\n};\n\n// ── Public API ──────────────────────────────────────────────────\n\nexport const warmLegalFormsV2 = warmLegalRoleHeads;\n\n/**\n * Synthesise the `Match` objects v1's `processLegalFormMatches`\n * expects. Pattern index 0 with `sliceStart=0, sliceEnd=1` lets\n * every synthesised match pass the slice gate without depending on\n * a real unified-search pattern table.\n */\ntype SynthMatch = Match & { text: string };\n\nconst synthMatch = (\n start: number,\n end: number,\n fullText: string,\n): SynthMatch => ({\n start,\n end,\n pattern: 0,\n text: fullText.slice(start, end),\n});\n\nexport const detectLegalFormsV2 = (fullText: string): Entity[] => {\n const { ts } = getSuffixSearch();\n const searchText = normalizeForSearch(fullText);\n const candidates: SynthMatch[] = [];\n const trimmedCandidates = new Set<SynthMatch>();\n\n for (const match of ts.findIter(searchText)) {\n const suffixStart = match.start;\n const suffixEnd = match.end;\n\n // SEC EDGAR line wrap: `Goldman Sachs & Co.\\nLLC` — terminal\n // suffix on its own line after a dotted business designator.\n // Allow crossing a single newline ONLY when the line above\n // ends in `<word>.` (a dotted abbreviation). Indentation on\n // the suffix line is preserved verbatim in EDGAR HTML, so the\n // newline check skips any leading horizontal whitespace first.\n let effectiveSuffixStart = suffixStart;\n {\n let scan = suffixStart;\n while (scan > 0) {\n const ch = fullText.charAt(scan - 1);\n if (ch === \" \" || ch === \"\\t\") {\n scan--;\n continue;\n }\n break;\n }\n if (scan > 0 && fullText.charAt(scan - 1) === \"\\n\") {\n let p = scan - 1;\n while (p > 0 && fullText.charAt(p - 1) === \" \") p--;\n if (p > 0 && fullText.charAt(p - 1) === \".\") {\n effectiveSuffixStart = p;\n }\n }\n }\n\n // The leading-separator check runs on the ORIGINAL suffix start\n // so the dotted-abbreviation rejection (`18 U.S.C.` → reject the\n // inner `S.C.` hit) does not also fire on legitimate line-wrap\n // candidates whose remapped anchor lands on a dotted designator\n // (`Goldman Sachs & Co.\\nLLC` → LLC's original prev char is a\n // space, not a dotted-citation continuation).\n if (!isLeadingSeparator(fullText, suffixStart)) continue;\n if (!isTrailingBoundary(fullText, suffixEnd)) continue;\n\n const walkerStart = walkBackward(fullText, effectiveSuffixStart);\n if (walkerStart >= effectiveSuffixStart) continue; // no head body\n if (crossesSentenceEnd(fullText, walkerStart, effectiveSuffixStart))\n continue;\n const candidateStart = trimToFirstCapAfterVerb(\n fullText,\n walkerStart,\n effectiveSuffixStart,\n );\n if (candidateStart >= effectiveSuffixStart) continue;\n const candidate = synthMatch(candidateStart, suffixEnd, fullText);\n if (candidateStart !== walkerStart) trimmedCandidates.add(candidate);\n candidates.push(candidate);\n }\n\n if (candidates.length === 0) return [];\n\n // When the AC pass finds both a nested suffix and the outer\n // suffix on the same span (`Goldman Sachs & Co.` + `Goldman\n // Sachs & Co.\\nLLC` for the line-wrap case), prefer the\n // longer span — the validator pipeline emits one entity per\n // candidate, so otherwise we'd ship both. Pure overlap; the\n // legitimate sibling-org split is already done by\n // splitEmbeddedLegalFormList inside processLegalFormMatches.\n const dedupedCandidates = dropOverlapping(candidates);\n\n // Hand the rough spans to v1's full validator pipeline —\n // role-head trim, leading-clause trim, embedded-list split,\n // line-break check, all-caps-line rejection, post-match\n // accented-letter boundary — all already implemented there.\n //\n // Split by whether the verb trim moved the start. Trimmed\n // candidates skip v1's `extendBackward` because re-extending\n // would walk back across the prose the trim just removed\n // (`Vendor grants Licensee Acme Inc.` → trim to `Acme Inc.`\n // → extend back through `Licensee`). Un-trimmed candidates\n // still benefit from `extendBackward` for in-name preposition\n // crossings the v2 walker doesn't perform (`The Bank of\n // America and Trust Company, Inc.`).\n const trimmed: SynthMatch[] = [];\n const untrimmed: SynthMatch[] = [];\n for (const c of dedupedCandidates) {\n (trimmedCandidates.has(c) ? trimmed : untrimmed).push(c);\n }\n return [\n ...processLegalFormMatches(trimmed, 0, 1, fullText, {\n suppressExtendBackward: true,\n }),\n ...processLegalFormMatches(untrimmed, 0, 1, fullText),\n ];\n};\n\nconst dropOverlapping = (candidates: SynthMatch[]): SynthMatch[] => {\n const sorted = [...candidates].sort(\n (a, b) => a.start - b.start || b.end - a.end,\n );\n const out: SynthMatch[] = [];\n for (const c of sorted) {\n const last = out.at(-1);\n if (last && c.start >= last.start && c.end <= last.end) continue;\n out.push(c);\n }\n return out;\n};\n","import type { Match } from \"@stll/text-search\";\nimport { br, us } from \"@stll/stdnum\";\nimport type { Validator } from \"@stll/stdnum\";\n\nimport { DETECTION_SOURCES } from \"../types\";\nimport type {\n CompiledValidation,\n Entity,\n TriggerGroupConfig,\n TriggerRule,\n TriggerValidation,\n ValidIdValidator,\n} from \"../types\";\nimport { POST_NOMINALS } from \"../config/titles\";\nimport { getKnownLegalSuffixes } from \"./legal-forms\";\nimport { NAME_PARTICLE } from \"./signatures\";\nimport { loadLanguageConfigs } from \"../util/lang-loader\";\nimport { DASH, DASH_INNER } from \"../util/char-groups\";\n\nconst VALID_ID_VALIDATORS: Record<ValidIdValidator, Validator> = {\n \"br.cpf\": br.cpf,\n \"br.cnpj\": br.cnpj,\n \"us.rtn\": us.rtn,\n};\n\nconst TRIGGER_SCORE = 0.95;\nconst WHITESPACE_RE = /\\s+/;\nconst LETTER_RE = /\\p{L}/u;\n/**\n * Decimal-comma pattern: comma followed by digit or\n * dash notation (\"0,05%\", \"1.529,50 Kč\", \"98.000,- Kč\").\n */\nconst DECIMAL_COMMA_RE = new RegExp(`^,(?:\\\\d|${DASH}{1,2})`);\n\n/**\n * Post-nominal degree regex. When a comma-stop is\n * followed by a known post-nominal (Ph.D., CSc., MBA\n * etc.), skip the comma and degree, then continue.\n */\nconst POST_NOMINAL_RE = new RegExp(\n `^,\\\\s*(?:${POST_NOMINALS.toSorted((a, b) => b.length - a.length)\n .map((d) =>\n d.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\").replace(/\\\\\\./g, \"\\\\.\\\\s*\"),\n )\n .join(\"|\")})\\\\.?`,\n \"i\",\n);\n\n// ── Person name-run boundary ────────────────────────\n//\n// A person value captured by a trigger must look like a\n// name: a run of capitalized tokens (any cased script,\n// so diacritics work: Novák, Müller, Łukasz). A token\n// may contain name-internal hyphens, apostrophes and\n// periods (initials \"J.\", degrees \"Ph.D.\", \"MUDr.\").\n// Short lowercase onomastic particles (von, van, de,\n// della, …) are allowed between capitalized tokens —\n// shape rule via the curated NAME_PARTICLE set shared\n// with the signature detector, never a per-name list.\n// A comma is only crossed when the delimiter-scan in\n// `extractValue` already vetted it (post-nominal degree\n// or decimal), so the optional `,` here cannot bridge\n// independent clauses. The run ends at the first token\n// that does not fit the shape: a lowercase prose word,\n// a digit-bearing token, or clause punctuation.\n//\n// Compiled once at module load; no per-token regexes.\nconst PERSON_NAME_TOKEN = \"\\\\p{Lu}[\\\\p{L}\\\\p{M}.'’\\\\-]*\";\n// A particle is space-separated (\"van der Meer\") or\n// apostrophe-attached (\"Jean d'Arc\"). Runs opening with\n// a lowercase particle (\"von Bülow\" directly after the\n// trigger) are rejected upstream by the starts-uppercase\n// validation, so the run anchors on a capitalized token.\nconst PERSON_NAME_PARTICLE_SEP = `(?:${NAME_PARTICLE}[ \\\\t]+|d['’])`;\nconst PERSON_NAME_RUN_RE = new RegExp(\n `^${PERSON_NAME_TOKEN}` +\n `(?:,?[ \\\\t]+(?:${PERSON_NAME_PARTICLE_SEP}){0,2}${PERSON_NAME_TOKEN})*`,\n \"u\",\n);\n\n// Definitive legal form suffixes (case-sensitive).\n// When a person-labeled trigger captures text containing\n// one of these, the entity is reclassified as\n// \"organization\". No \"i\" flag: short uppercase-only\n// forms (SE, SA, AG) must NOT match Czech/Slovak\n// reflexive pronouns \"se\", \"sa\" which appear in\n// person-trigger captures like\n// \"Ing. Jan Novák, se sídlem...\".\n//\nlet cachedLegalFormCheckSource: readonly string[] | null = null;\nlet cachedLegalFormCheckForms: readonly string[] = [];\nconst LEGAL_FORM_ALNUM_RE = /[\\p{L}\\p{N}]/u;\nconst LETTER_ONLY_LEGAL_FORM_RE = /^[\\p{L}\\p{M}]+$/u;\n\nconst getLegalFormCheckForms = (): readonly string[] => {\n const source = getKnownLegalSuffixes();\n if (cachedLegalFormCheckSource !== source) {\n cachedLegalFormCheckSource = source;\n cachedLegalFormCheckForms = source;\n }\n return cachedLegalFormCheckForms;\n};\n\nconst hasKnownLegalFormSuffix = (text: string): boolean => {\n for (const form of getLegalFormCheckForms()) {\n let fromIndex = 0;\n while (fromIndex < text.length) {\n const start = text.indexOf(form, fromIndex);\n if (start === -1) {\n break;\n }\n const end = start + form.length;\n fromIndex = start + 1;\n\n // Dot-free letter-only forms get Unicode-aware word\n // boundaries so short uppercase forms (\"SE\", \"AG\") and\n // longer single-word forms (\"Branch\", \"Limited\") don't\n // match as substrings of unrelated tokens. Forms with\n // punctuation already terminate naturally.\n if (!LETTER_ONLY_LEGAL_FORM_RE.test(form)) {\n return true;\n }\n if (\n !LEGAL_FORM_ALNUM_RE.test(text[start - 1] ?? \"\") &&\n !LEGAL_FORM_ALNUM_RE.test(text[end] ?? \"\")\n ) {\n return true;\n }\n }\n }\n return false;\n};\n\n// ── Validation compilation ─────────────────────────\n\nconst compileValidations = (\n validations: TriggerValidation[],\n): CompiledValidation[] =>\n validations.map((v): CompiledValidation => {\n switch (v.type) {\n case \"starts-uppercase\":\n return {\n type: \"starts-uppercase\",\n re: /^\\p{Lu}/u,\n };\n case \"min-length\":\n return { type: \"min-length\", min: v.min };\n case \"max-length\":\n return { type: \"max-length\", max: v.max };\n case \"no-digits\":\n return { type: \"no-digits\", re: /\\d/ };\n case \"has-digits\":\n return { type: \"has-digits\", re: /\\d/ };\n case \"matches-pattern\":\n return {\n type: \"matches-pattern\",\n // Strip g/y flags: the compiled regex is shared\n // across all rules in the group and must be\n // stateless (no lastIndex advancement).\n re: new RegExp(v.pattern, (v.flags ?? \"\").replace(/[gy]/g, \"\")),\n };\n case \"valid-id\": {\n const validator = VALID_ID_VALIDATORS[v.validator];\n if (!validator) {\n throw new Error(\n `Unknown valid-id validator: ${JSON.stringify(v.validator)}`,\n );\n }\n return {\n type: \"valid-id\",\n check: (value) => {\n // stdnum validators expect compact digits only;\n // strip formatting (spaces, dots, dashes,\n // slashes) so dotted/spaced IDs still validate.\n const compact = value.replace(/[\\s.\\-/]/g, \"\");\n return validator.validate(compact).valid;\n },\n };\n }\n default: {\n // TriggerValidation is a public export; custom\n // configs may bypass the build-time validator.\n const _exhaustive: never = v;\n throw new Error(\n `Unknown validation type: ${JSON.stringify(_exhaustive)}`,\n );\n }\n }\n });\n\nconst applyValidations = (\n text: string,\n validations: CompiledValidation[],\n): boolean => {\n for (const v of validations) {\n switch (v.type) {\n case \"starts-uppercase\":\n if (!v.re.test(text)) return false;\n break;\n case \"min-length\":\n if (text.length < v.min) return false;\n break;\n case \"max-length\":\n if (text.length > v.max) return false;\n break;\n case \"no-digits\":\n if (v.re.test(text)) return false;\n break;\n case \"has-digits\":\n if (!v.re.test(text)) return false;\n break;\n case \"matches-pattern\":\n if (!v.re.test(text)) return false;\n break;\n case \"valid-id\":\n if (!v.check(text)) return false;\n break;\n default: {\n const _exhaustive: never = v;\n throw new Error(\n `Unknown compiled validation type: ${JSON.stringify(_exhaustive)}`,\n );\n }\n }\n }\n return true;\n};\n\n// ── Trigger expansion ──────────────────────────────\n\nconst expandTriggerGroups = (groups: TriggerGroupConfig[]): TriggerRule[] => {\n const rules: TriggerRule[] = [];\n for (const group of groups) {\n const extensions = group.extensions ?? [];\n const compiled = compileValidations(group.validations ?? []);\n\n // Generate trigger variants from the original\n // trigger strings. Extensions are applied once to\n // the base set only (not combinatorially); e.g.,\n // [\"add-colon\", \"normalize-spaces\"] produces\n // \"trigger:\", \"trigger\\u00A0\", but NOT\n // \"trigger\\u00A0:\". This is intentional to avoid\n // exponential variant growth.\n const allTriggers = new Set(group.triggers);\n for (const trigger of group.triggers) {\n if (extensions.includes(\"add-colon\") && !trigger.endsWith(\":\"))\n allTriggers.add(`${trigger}:`);\n if (extensions.includes(\"add-trailing-space\") && !trigger.endsWith(\" \"))\n allTriggers.add(`${trigger} `);\n if (\n extensions.includes(\"add-colon-space\") &&\n !trigger.endsWith(\": \") &&\n !trigger.endsWith(\":\")\n )\n allTriggers.add(`${trigger}: `);\n if (extensions.includes(\"normalize-spaces\")) {\n if (trigger.includes(\" \")) {\n allTriggers.add(trigger.replace(/ /g, \"\\u00A0\"));\n }\n }\n }\n\n const includeTrigger = group.includeTrigger ?? false;\n\n for (const trigger of allTriggers) {\n rules.push({\n trigger,\n label: group.label,\n strategy: group.strategy,\n validations: compiled,\n includeTrigger,\n });\n }\n }\n return rules;\n};\n\n// ── Pattern builder for unified search ──────────────\n\ntype TriggerPatterns = {\n patterns: string[];\n rules: TriggerRule[];\n};\n\nlet triggerPatternsPromise: Promise<TriggerPatterns> | null = null;\n\n/**\n * Build trigger patterns and rules from data configs.\n * Returns string[] for the unified TextSearch\n * builder and the parallel rules array.\n */\nconst loadTriggerPatterns = async (): Promise<TriggerPatterns> => {\n const rules: TriggerRule[] = [];\n\n const allGroups = await loadLanguageConfigs<readonly TriggerGroupConfig[]>(\n \"triggers\",\n (mod) => {\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config\n const m = mod as {\n default?: readonly TriggerGroupConfig[];\n };\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config\n return (m.default ?? mod) as readonly TriggerGroupConfig[];\n },\n );\n for (const groups of allGroups) {\n if (!Array.isArray(groups)) {\n console.warn(\n \"[anonymize] triggers: unexpected \" + \"config shape, skipping\",\n );\n continue;\n }\n rules.push(...expandTriggerGroups(groups as TriggerGroupConfig[]));\n }\n\n // Load global triggers (language-agnostic)\n try {\n const globalMod = await import(\"../data/triggers.global.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config\n const globalGroups = ((globalMod as { default?: unknown }).default ??\n globalMod) as TriggerGroupConfig[];\n if (Array.isArray(globalGroups)) {\n rules.push(...expandTriggerGroups(globalGroups));\n }\n } catch (err) {\n // Only suppress \"module not found\"; re-throw\n // other errors (JSON parse, etc.).\n if (\n !(err instanceof Error) ||\n !err.message.includes(\"Cannot find module\")\n ) {\n throw err;\n }\n }\n\n // Load year-words from JSON dictionary and create\n // date triggers: \"rok 2022\" → date entity.\n try {\n const yearMod = await import(\"../data/year-words.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON\n const data = ((yearMod as { default?: unknown }).default ??\n yearMod) as Record<string, string | string[]>;\n const seen = new Set<string>();\n const yearValidation = compileValidations([\n {\n type: \"matches-pattern\",\n pattern: \"^(?:19|20)\\\\d{2}\\\\.?$\",\n },\n ]);\n for (const [key, words] of Object.entries(data)) {\n if (key.startsWith(\"_\") || !Array.isArray(words)) {\n continue;\n }\n for (const word of words) {\n const lc = word.toLowerCase();\n if (seen.has(lc)) continue;\n seen.add(lc);\n rules.push({\n trigger: word,\n label: \"date\",\n strategy: { type: \"n-words\", count: 1 },\n validations: yearValidation,\n includeTrigger: false,\n });\n }\n }\n } catch {\n // year-words.json not available — skip\n }\n\n // Warn about cross-group trigger duplicates.\n // Duplicates cause redundant AC matches but are\n // not fatal (mergeAndDedup handles overlap).\n const seen = new Map<string, { label: string; strategy: string }>();\n for (const rule of rules) {\n const key = rule.trigger.toLowerCase();\n const prev = seen.get(key);\n if (prev !== undefined) {\n const labelDiff = prev.label !== rule.label;\n const stratDiff = prev.strategy !== rule.strategy.type;\n if (labelDiff || stratDiff) {\n console.warn(\n `[anonymize] duplicate trigger` +\n ` \"${rule.trigger}\":` +\n (labelDiff\n ? ` labels \"${prev.label}\" vs` + ` \"${rule.label}\"`\n : \"\") +\n (stratDiff\n ? ` strategies \"${prev.strategy}\" vs` + ` \"${rule.strategy.type}\"`\n : \"\"),\n );\n }\n }\n seen.set(key, {\n label: rule.label,\n strategy: rule.strategy.type,\n });\n }\n\n // Build patterns from lowercased trigger strings.\n // rules[i] corresponds to patterns[i].\n // Plain lowercased strings — the unified builder\n // sets caseInsensitive globally on the AC.\n const patterns: string[] = rules.map((r) => r.trigger.toLowerCase());\n\n // Warm the address stop-keywords cache so the\n // synchronous `extractValue` (address strategy) can\n // read it without an async hop.\n await loadAddressStopKeywords();\n\n return { patterns, rules };\n};\n\nexport const buildTriggerPatterns = async (): Promise<TriggerPatterns> => {\n triggerPatternsPromise ??= loadTriggerPatterns();\n return triggerPatternsPromise;\n};\n\n// ── Value extraction ────────────────────────────────\n\nconst LEADING_PUNCT = /^[„\"\"»«'\"()\\s]+/;\nconst TRAILING_PUNCT = /[\"\"»«'\"()\\s]+$/;\n\nconst stripQuotes = (value: {\n start: number;\n end: number;\n text: string;\n}): {\n start: number;\n end: number;\n text: string;\n} | null => {\n const leadingMatch = LEADING_PUNCT.exec(value.text);\n const leadingLen = leadingMatch ? leadingMatch[0].length : 0;\n const stripped = value.text.slice(leadingLen).replace(TRAILING_PUNCT, \"\");\n if (stripped.length === 0) {\n return null;\n }\n return {\n start: value.start + leadingLen,\n end: value.start + leadingLen + stripped.length,\n text: stripped,\n };\n};\n\n/**\n * Hard stop characters for to-next-comma scanning. A closing\n * parenthesis or closing bracket terminates a clause just like\n * an opening one: `State of New York or any other jurisdiction)`\n * is the tail of a parenthesised insertion, not the start of a\n * larger phrase that should be absorbed into a jurisdiction span.\n */\nconst COMMA_STOP_CHARS = new Set([\"\\n\", \"(\", \")\", \"[\", \"]\", \"\\t\", \";\"]);\n\n/**\n * Sentence-terminator detection: a period that genuinely ends\n * one clause and starts another. Used by `to-next-comma` so\n * governing-law clauses (\"…State of New York. SECTION 2…\")\n * don't sweep across the period.\n *\n * Three positive signals (any one terminates):\n * 1. Long lowercase tail before the dot (>= 5 letters) —\n * catches \"…construction. SECTION 2…\", \"…vykonává.\".\n * 2. Currency/amount tail (zł, Kč, USD, €) — catches\n * \"…w kwocie 1000 zł. Termin płatności…\".\n * 3. Proper-noun head: a capitalized word of >= 4 letters\n * ending in lowercase, with no internal uppercase, AND\n * a substantial next clause (Capital + >= 2 lowercase).\n * Catches short city names that the lowercase-tail rule\n * misses: \"…z siedzibą w Łódź. Kapitał zakładowy…\",\n * \"…seat in Brno. Section 2…\".\n *\n * The rule must NOT fire on:\n * - title abbreviations: \"Mr.\", \"Mrs.\", \"Dr.\", \"Hon.\",\n * \"Sr.\", \"Jr.\" (head <= 3 chars or insufficient Ll tail)\n * - degree abbreviations: \"Ph.D.\", \"RNDr.\", \"MUDr.\",\n * \"Ing.\" (internal periods or internal uppercase block\n * the proper-noun pattern)\n * - street-type abbreviations: \"Ste.\", \"Ave.\", \"Inc.\",\n * \"ul.\", \"al.\", \"nábř.\" (lowercase initials or\n * insufficient Ll tail)\n * - small-word lowercase abbreviations: \"prof.\", \"inż.\",\n * \"hab.\" (no leading uppercase, so the proper-noun rule\n * can't fire)\n */\nconst NEXT_IS_SENTENCE_START_RE = /^\\.(?:\\s+\\p{Lu}|\\s*$)/u;\nconst SENTENCE_TAIL_RE = /\\p{Ll}{5,}$/u;\n/**\n * Proper-noun tail: capital letter + >= 3 lowercase letters,\n * preceded by a non-letter and non-period (so we don't slice\n * into the middle of an acronym or a multi-dot abbreviation).\n * The 4-character minimum excludes 2–3-char titles (\"Mr\",\n * \"Mrs\", \"Dr\", \"Inc\", \"Ste\"); the all-lowercase tail\n * excludes mixed-case degrees (\"RNDr\", \"MUDr\").\n */\nconst PROPER_NOUN_HEAD_RE = /(?:^|[^\\p{L}.])\\p{Lu}\\p{Ll}{3,}$/u;\n/**\n * Next clause begins with a real word: capital + >= 2\n * lowercase letters. Filters cases where a capitalized\n * abbreviation (e.g., \"Smith Inc.\") follows a proper noun,\n * which would otherwise look sentence-like.\n */\nconst NEXT_IS_REAL_SENTENCE_RE = /^\\.\\s+\\p{Lu}\\p{Ll}{2,}/u;\n/**\n * Short currency-abbreviation tail (zł, Kč, gr, Ft, kr,\n * лв, USD, PLN, EUR, …). When a period follows one of\n * these and the next token starts uppercase, treat it as\n * a sentence boundary even though the tail is too short\n * to satisfy `SENTENCE_TAIL_RE`. Without this, amount\n * triggers using `to-next-comma` swallow the following\n * clause: `\"w kwocie 1000 zł. Termin płatności…\"`.\n *\n * Currency codes are typically uppercase (`USD`, `PLN`)\n * but appear lowercased in informal writing (`pln`, `eur`);\n * local names mix case (`zł`, `Kč`, `Ft`); symbols\n * (`€`, `$`, `£`) appear after the amount as well. The\n * negative lookbehind on a letter ensures the abbreviation\n * is matched only as a standalone token; symbols are\n * matched unconditionally since they are not letters.\n */\nconst CURRENCY_TAIL_RE =\n /(?:(?<![\\p{L}])(?:zł|Kč|gr|Ft|kr|лв|USD|PLN|EUR|CZK|GBP|CHF|HUF|RON|SEK|NOK|DKK)|[€$£])$/iu;\n// Numeric sentence tail: a digit immediately before the\n// period (e.g. monetary amounts \"R$ 1.000,00.\" or\n// \"EUR 5.000.\") signals a sentence end too, so amount\n// triggers do not consume the next clause.\nconst NUMERIC_SENTENCE_TAIL_RE = /\\d$/;\n\nconst isSentenceTerminator = (text: string, periodIndex: number): boolean => {\n const tail = text.slice(periodIndex);\n if (!NEXT_IS_SENTENCE_START_RE.test(tail)) {\n return false;\n }\n const head = text.slice(0, periodIndex);\n if (\n SENTENCE_TAIL_RE.test(head) ||\n CURRENCY_TAIL_RE.test(head) ||\n NUMERIC_SENTENCE_TAIL_RE.test(head)\n ) {\n return true;\n }\n // Proper-noun head (short city names like \"Łódź.\",\n // \"Brno.\", \"York.\") gated by a real-word next clause\n // to avoid breaking on title chains (\"Mrs. Smith Inc.\").\n return PROPER_NOUN_HEAD_RE.test(head) && NEXT_IS_REAL_SENTENCE_RE.test(tail);\n};\n\n/**\n * Field-label keywords that terminate address scanning.\n * When a comma in the address strategy is followed by\n * one of these, the address stops before the keyword.\n *\n * The list is sourced from\n * `data/address-stop-keywords.json` (per-language so new\n * languages can drop in their own labels without\n * touching this file). `loadAddressStopKeywords` unions\n * every language into a single longest-first array;\n * `getAddressStopKeywordsSync` returns the cached union\n * and falls back to a seed list so the strategy keeps\n * working before the warmup promise resolves.\n *\n * The address strategy doesn't know the document\n * language, so a flat union is intentional: any\n * language's labels can appear in any address.\n */\nconst ADDRESS_STOP_KEYWORDS_SEED: readonly string[] = [\n \"číslo účtu\",\n \"registrační\",\n \"zastoupen\",\n \"bankovní\",\n \"e-mail\",\n \"telefon\",\n \"jednatel\",\n \"ředitel\",\n \"datová\",\n \"vložka\",\n \"sp.zn.\",\n \"oddíl\",\n \"swift\",\n \"email\",\n \"iban\",\n \"dič\",\n \"ičo\",\n \"tel\",\n \"č.ú.\",\n \"bic\",\n \"ič\",\n];\n\nlet addressStopKeywordsCache: readonly string[] | null = null;\nlet addressStopKeywordsPromise: Promise<readonly string[]> | null = null;\n\nconst loadAddressStopKeywords = async (): Promise<readonly string[]> => {\n if (addressStopKeywordsCache) return addressStopKeywordsCache;\n if (addressStopKeywordsPromise) return addressStopKeywordsPromise;\n addressStopKeywordsPromise = (async () => {\n let data: Record<string, unknown> = {};\n try {\n const mod = await import(\"../data/address-stop-keywords.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON module shape\n const parsed =\n (mod as { default?: Record<string, unknown> }).default ?? mod;\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON module shape\n data = parsed as Record<string, unknown>;\n } catch (err) {\n console.warn(\n \"[anonymize] triggers: failed to load \" +\n \"address-stop-keywords.json, falling back to \" +\n \"seed list:\",\n err,\n );\n }\n const seen = new Set<string>();\n const out: string[] = [];\n const addAll = (list: readonly string[]): void => {\n for (const kw of list) {\n if (typeof kw !== \"string\" || kw.length === 0) continue;\n const lower = kw.toLowerCase();\n if (seen.has(lower)) continue;\n seen.add(lower);\n out.push(lower);\n }\n };\n for (const [key, value] of Object.entries(data)) {\n if (key.startsWith(\"_\")) continue;\n if (!Array.isArray(value)) continue;\n addAll(value as readonly string[]);\n }\n addAll(ADDRESS_STOP_KEYWORDS_SEED);\n // Sort longest-first so multi-word labels\n // (\"bankové spojenie\", \"číslo účtu\") match\n // before nested shorter ones (\"č.\").\n out.sort((a, b) => b.length - a.length);\n addressStopKeywordsCache = out;\n return out;\n })();\n return addressStopKeywordsPromise;\n};\n\nconst getAddressStopKeywordsSync = (): readonly string[] =>\n addressStopKeywordsCache ?? ADDRESS_STOP_KEYWORDS_SEED;\n\n/**\n * Warm the address-stop-keywords cache. Pipeline callers\n * await this before invoking trigger detection so the\n * synchronous `extractValue` path uses the merged list\n * instead of the seed fallback.\n */\nexport const warmAddressStopKeywords = async (): Promise<void> => {\n await loadAddressStopKeywords();\n};\n\n// Hard cap for unterminated trigger values. Applies to\n// strategies that scan forward until a delimiter\n// (`to-next-comma`, `to-end-of-line`). Prevents a missing\n// delimiter — common in HTML-flattened or single-paragraph\n// PDFs where a whole signature block lives on one line —\n// from turning a trigger into a multi-hundred-character\n// entity. 100 chars covers normal full-name + address\n// lines while bounding pathological inputs.\nconst MAX_TRIGGER_VALUE_LEN = 100;\nconst MIN_TRIGGER_PHONE_DIGITS = 5;\nconst TRIGGER_LOOKAHEAD_MARGIN = 128;\nconst LINE_TRIGGER_LOOKAHEAD = 2_048;\nconst MATCH_PATTERN_LOOKAHEAD = 512;\nconst PHONE_VALUE_START_RE = /^[+(\\d]/;\n/**\n * Phone-shaped run: an opening \"+\", \"(\", or digit, then\n * any mix of digits, whitespace, parens, dots, slashes,\n * and dash variants. The dash class includes every\n * typographic dash (U+2011 non-breaking hyphen, U+2013\n * en-dash, etc.) via `DASH_INNER` so a Unicode separator\n * (\"+1 555‑123‑4567\") does not break the run and leave the\n * number unredacted.\n */\nconst PHONE_SHAPE_PREFIX_RE = new RegExp(`^[+(\\\\d][\\\\d\\\\s()./${DASH_INNER}]*`);\n/**\n * Optional phone-extension suffix following the numeric\n * run: \"ext\", \"ext.\", \"extension\", or \"x\" (case-\n * insensitive) plus 1-6 digits, with optional leading\n * whitespace. Matches \"+1 555 123 4567 ext. 89\" and\n * \"555-1234 x42\" so the shape bound keeps the extension.\n */\nconst PHONE_EXTENSION_SUFFIX_RE = /^\\s*(?:ext\\.?|extension|x)\\s*\\d{1,6}/i;\nconst ISO_DATE_PREFIX_RE = /^\\d{4}-\\d{2}-\\d{2}\\b/;\nconst INLINE_FIELD_LABEL_RE = /\\b[\\p{L}][\\p{L}\\p{M} /-]{1,32}:/u;\nconst INLINE_FIELD_LABEL_STOP_RE =\n /(?:^|[^\\S\\n\\t])[\\p{L}][\\p{L}\\p{M} /-]{1,32}:/u;\n\nconst capAtWordBoundary = (valueText: string, cap: number): number => {\n let capped = cap;\n const isWordChar = (i: number): boolean =>\n /[\\p{L}\\p{N}]/u.test(valueText[i] ?? \"\");\n while (capped > 0 && isWordChar(capped - 1) && isWordChar(capped)) {\n capped--;\n }\n return capped;\n};\n\nconst isPlausiblePhoneTriggerValue = (value: string): boolean => {\n const trimmed = value.trimStart();\n if (!PHONE_VALUE_START_RE.test(trimmed)) {\n return false;\n }\n if (ISO_DATE_PREFIX_RE.test(trimmed)) {\n return false;\n }\n if (INLINE_FIELD_LABEL_RE.test(trimmed)) {\n return false;\n }\n let digits = 0;\n for (const ch of trimmed) {\n if (/\\d/.test(ch)) {\n digits++;\n }\n }\n return digits >= MIN_TRIGGER_PHONE_DIGITS;\n};\n\nconst getTriggerLookahead = (strategy: TriggerRule[\"strategy\"]): number => {\n switch (strategy.type) {\n case \"to-next-comma\":\n return (strategy.maxLength ?? 100) + TRIGGER_LOOKAHEAD_MARGIN;\n case \"to-end-of-line\":\n return LINE_TRIGGER_LOOKAHEAD;\n case \"n-words\":\n return strategy.count * 64 + TRIGGER_LOOKAHEAD_MARGIN;\n case \"company-id-value\":\n return 256;\n case \"address\":\n return (strategy.maxChars ?? 120) + TRIGGER_LOOKAHEAD_MARGIN;\n case \"match-pattern\":\n return MATCH_PATTERN_LOOKAHEAD;\n default: {\n const _exhaustive: never = strategy;\n throw new Error(\n `Unknown trigger strategy: ${JSON.stringify(_exhaustive)}`,\n );\n }\n }\n};\n\nconst extractValue = (\n text: string,\n triggerEnd: number,\n strategy: TriggerRule[\"strategy\"],\n label?: string,\n): {\n start: number;\n end: number;\n text: string;\n} | null => {\n const lookaheadEnd = Math.min(\n text.length,\n triggerEnd + getTriggerLookahead(strategy),\n );\n const remaining = text.slice(triggerEnd, lookaheadEnd);\n // Strip leading whitespace, colons, semicolons —\n // triggers are often followed by \": \\t\\t\\t\" in\n // formatted documents.\n const stripped = remaining.replace(/^[\\s:;]+/, \"\");\n const trimmedOffset = remaining.length - stripped.length;\n const valueStart = triggerEnd + trimmedOffset;\n const valueText = stripped;\n\n if (valueText.length === 0) {\n return null;\n }\n\n switch (strategy.type) {\n case \"to-next-comma\": {\n // Stop at comma, newline, or opening parenthesis.\n // Parens mark defined-term clauses in legal text\n // (e.g., \"(dále jen ...)\") and should not be\n // captured as part of a name/address.\n // When a comma is followed by a known post-nominal\n // degree (Ph.D., CSc., MBA), skip it and continue\n // so \"RNDr. Filipem Hartvichem, Ph.D., CSc.\"\n // captures the full name with degrees.\n // Also stop at any of the trigger's configured\n // `stopWords` so e.g. a court trigger (\"Městským\n // soudem v Praze\") doesn't sweep into the following\n // clause (\"dne 1. 1. 2020\") when the comma is\n // missing.\n const stopWords = strategy.stopWords ?? [];\n const stopWordRe =\n stopWords.length > 0\n ? new RegExp(\n `^(?:${stopWords\n .map((w) => w.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"))\n .join(\"|\")})(?![\\\\p{L}\\\\p{N}])`,\n \"iu\",\n )\n : null;\n let end = 0;\n\n while (end < valueText.length) {\n const ch = valueText[end];\n // Hard stops: newline, paren, tab, semicolon\n if (ch !== undefined && COMMA_STOP_CHARS.has(ch)) {\n break;\n }\n // Sentence terminator: a period that genuinely ends\n // a clause (\"…State of Louisiana shall govern its\n // construction. SECTION 3.05…\"). The helper rejects\n // abbreviation dots (Ste./RNDr./Mr.) by requiring a\n // real word before the period.\n if (ch === \".\" && isSentenceTerminator(valueText, end)) {\n break;\n }\n // Trigger-supplied stop words (e.g. \"dne\", \"vložka\"\n // for court triggers). Only check at word starts so\n // a stopword that happens to appear inside a longer\n // token doesn't terminate prematurely.\n if (\n stopWordRe !== null &&\n (end === 0 || !/[\\p{L}\\p{N}]/u.test(valueText[end - 1] ?? \"\")) &&\n stopWordRe.test(valueText.slice(end))\n ) {\n break;\n }\n // Comma: for person triggers, check if followed\n // by a post-nominal degree (Ph.D., CSc.) and\n // skip past it. Non-person triggers stop here.\n if (ch === \",\") {\n const afterComma = valueText.slice(end);\n // Decimal separator: comma followed by digit or\n // dash notation (\"0,05%\", \"1.529,50 Kč\",\n // \"98.000,- Kč\", \"98.000,-- Kč\")\n if (DECIMAL_COMMA_RE.test(afterComma)) {\n end++;\n continue;\n }\n const degreeMatch =\n label === \"person\" ? POST_NOMINAL_RE.exec(afterComma) : null;\n if (degreeMatch) {\n // Skip the comma + degree, continue scan\n end += degreeMatch[0].length;\n continue;\n }\n // Regular comma — stop here\n break;\n }\n end++;\n }\n\n // Per-trigger hard length cap. Applies even when a\n // stop char was found, so jurisdiction triggers\n // (\"State of Delaware\") cannot absorb forum-selection\n // clauses that legitimately end at a comma sentences\n // away (\"…in the event any dispute arises out of…\").\n // When no stop char was found, the cap also acts as\n // the unterminated-value fallback; default to 100\n // when the strategy does not configure one. When\n // capping, retreat to the previous word boundary so\n // the returned span never ends mid-word.\n const lengthCap = strategy.maxLength ?? 100;\n if (end > lengthCap) {\n end = capAtWordBoundary(valueText, lengthCap);\n }\n\n const rawSlice = valueText.slice(0, end);\n const extracted = rawSlice.trim();\n if (extracted.length === 0) {\n return null;\n }\n const trailingSpaces = rawSlice.length - rawSlice.trimEnd().length;\n return {\n start: valueStart,\n end: valueStart + end - trailingSpaces,\n text: extracted,\n };\n }\n\n case \"to-end-of-line\": {\n // Only capture on the SAME line as the trigger. The\n // generic leading-whitespace strip above swallows\n // newlines, which would let a header-style trigger\n // (\"Bankovní spojení\\nKupní cena bude uhrazena na ...\")\n // consume the next line wholesale. If the strip\n // crossed a newline, the trigger has no inline value\n // and the strategy emits nothing.\n const consumed = remaining.length - valueText.length;\n if (consumed > 0 && remaining.slice(0, consumed).includes(\"\\n\")) {\n return null;\n }\n // Stop at newline or tab (tab separates cells\n // in DOCX table rows).\n const LINE_STOPS = [\"\\n\", \"\\t\"];\n let end = valueText.length;\n let foundLineStop = false;\n for (const ch of LINE_STOPS) {\n const idx = valueText.indexOf(ch);\n if (idx !== -1 && idx < end) {\n end = idx;\n foundLineStop = true;\n }\n }\n if (label === \"phone number\") {\n const inlineLabel = INLINE_FIELD_LABEL_STOP_RE.exec(\n valueText.slice(0, end),\n );\n if (inlineLabel) {\n end = inlineLabel.index;\n foundLineStop = true;\n }\n // A phone value is digits plus separators and ends\n // on a digit. Cut the capture at the end of the\n // phone-shaped run so a mid-sentence trigger\n // (\"... phone +420 777 123 456. Signed on ...\")\n // cannot swallow the rest of a single-line\n // paragraph through the line delimiter.\n //\n // A dot followed by whitespace is a sentence\n // boundary, not a phone separator (\"555.123.4567\"\n // has no whitespace after its dots), so bound the\n // shape scan before the first \". \" — otherwise the\n // shape class (dot, space, digits) would run into a\n // following numbered clause (\"…123 456. 1. Defs…\").\n const region = valueText.slice(0, end);\n const sentenceBreak = /\\.\\s/.exec(region);\n const shapeRegion =\n sentenceBreak !== null\n ? region.slice(0, sentenceBreak.index)\n : region;\n const shape = PHONE_SHAPE_PREFIX_RE.exec(shapeRegion);\n if (shape) {\n // Trim trailing separators/whitespace so the run\n // ends on a digit, then re-admit an optional\n // extension suffix (\"ext. 89\", \"x42\") that sits\n // just past the numeric run.\n let shapeEnd = shape[0].replace(/\\D+$/, \"\").length;\n const extension = PHONE_EXTENSION_SUFFIX_RE.exec(\n region.slice(shapeEnd),\n );\n if (extension !== null) {\n shapeEnd += extension[0].length;\n }\n // Only bind when the phone shape stops before the\n // current `end` — i.e. the shape, not a genuine\n // line delimiter, is what terminates the value. A\n // shape that runs all the way to a real newline/tab\n // is a legitimate long labelled line and is left\n // untouched (no cap), mirroring how newline-\n // terminated bank-account values survive the cap.\n if (shapeEnd > 0 && shapeEnd < end) {\n // Apply the 100-char cap only to this shape-\n // derived stop so a pathological all-digits run\n // (\"1 1 1 …\" for hundreds of chars, with a later\n // shape-breaking \"Signed by …\") cannot produce an\n // over-long phone entity. Mirror the fallback's\n // word-boundary retreat.\n const cappedShapeEnd =\n shapeEnd > MAX_TRIGGER_VALUE_LEN\n ? Math.min(\n capAtWordBoundary(valueText, MAX_TRIGGER_VALUE_LEN),\n MAX_TRIGGER_VALUE_LEN,\n )\n : shapeEnd;\n end = cappedShapeEnd;\n foundLineStop = true;\n }\n }\n }\n // Cap only when no real line delimiter was found. HTML-\n // flattened text and signature blocks routinely pack\n // hundreds of chars (a chain of \"Phone:\" / \"Name:\"\n // pseudo-fields) onto one logical line; newline-terminated\n // values should still capture through the delimiter.\n if (!foundLineStop) {\n end = capAtWordBoundary(\n valueText,\n Math.min(end, MAX_TRIGGER_VALUE_LEN),\n );\n }\n const rawSlice = valueText.slice(0, end);\n const extracted = rawSlice.trim();\n if (extracted.length === 0) {\n return null;\n }\n const trailingSpaces = rawSlice.length - rawSlice.trimEnd().length;\n return {\n start: valueStart,\n end: valueStart + end - trailingSpaces,\n text: extracted,\n };\n }\n\n case \"n-words\": {\n // Respect tab as a cell boundary (DOCX table\n // rows use tabs between columns).\n const tabIdx = valueText.indexOf(\"\\t\");\n const cellText = tabIdx !== -1 ? valueText.slice(0, tabIdx) : valueText;\n // Skip punctuation-only tokens (colons, dashes)\n // so \"datová schránka : hsaxra8\" captures\n // \"hsaxra8\" not \":\"\n const PUNCT_ONLY = /^[\\p{P}\\p{S}]+$/u;\n // Skip generic \"number\" markers (PT: \"nº/n°/n.\",\n // FR/IT/ES use \"n°\", general \"№\") so triggers\n // like \"OAB/SP nº 123456\" capture the actual\n // identifier, not the marker.\n const NUMBER_MARKER = /^(?:n[ºo°.]|№)$/i;\n const allTokens = cellText.split(WHITESPACE_RE);\n const words = allTokens\n .filter((w) => !PUNCT_ONLY.test(w) && !NUMBER_MARKER.test(w))\n .slice(0, strategy.count);\n if (words.length === 0) {\n return null;\n }\n // Find span of the first real word\n const firstWord = words[0];\n if (firstWord === undefined) {\n return null;\n }\n const firstIdx = cellText.indexOf(firstWord);\n let actualEnd = firstIdx + firstWord.length;\n // If multiple words, extend to last one\n let searchPos = actualEnd;\n for (let wi = 1; wi < words.length; wi++) {\n const w = words[wi];\n if (w === undefined) {\n break;\n }\n const wIdx = cellText.indexOf(w, searchPos);\n if (wIdx === -1) break;\n actualEnd = wIdx + w.length;\n searchPos = actualEnd;\n }\n return {\n start: valueStart + firstIdx,\n end: valueStart + actualEnd,\n text: cellText.slice(firstIdx, actualEnd),\n };\n }\n\n case \"company-id-value\": {\n // Work from the raw remaining text (before the\n // upstream trimStart) so we can require at least\n // a colon or whitespace separator between the\n // keyword and value (e.g., \"IČO: 12345678\" or\n // \"IČO 12345678\", but not \"IČO12345678\").\n //\n // Exception: when the trigger itself ends in a\n // number-marker character (\"n°\", \"№\", \"#\"), the\n // value can follow the marker without any\n // intervening separator (\"SIREN n°123456789\"). In\n // that case we accept an empty separator too.\n const raw = text.slice(triggerEnd);\n const triggerLastChar = text[triggerEnd - 1] ?? \"\";\n const allowEmptySep =\n triggerLastChar === \"°\" ||\n triggerLastChar === \"º\" ||\n triggerLastChar === \"№\" ||\n triggerLastChar === \"#\";\n const sepRe = allowEmptySep ? /^(?:\\s*:\\s*|\\s+|)/ : /^(?:\\s*:\\s*|\\s+)/;\n const sepMatch = sepRe.exec(raw);\n if (!sepMatch) {\n return null;\n }\n let afterSep = raw.slice(sepMatch[0].length);\n // Skip a leading number-label word (e.g.\n // \"PESEL nr 44051401458\", \"NIP numer 1234567890\",\n // \"REGON № 123456789\", \"RG nº 12.345.678-9\",\n // \"CPF n° 123.456.789-00\") that may appear between\n // the trigger and the value. The label is consumed\n // along with its trailing separator so the\n // case-insensitive prefix regex below cannot mistake\n // it for a VAT country prefix like \"cz\"/\"pl\".\n const labelMatch =\n /^(?:nr\\.?|numer|n[ºo°.]|№|no\\.?)(?:\\s*:\\s*|\\s+)/i.exec(afterSep);\n let labelOffset = 0;\n if (labelMatch) {\n labelOffset = labelMatch[0].length;\n afterSep = afterSep.slice(labelOffset);\n }\n // Value shape: optional country prefix (e.g. \"CZ\",\n // \"PL\", \"FR\"), optional whitespace, an optional\n // 2-char alphanumeric key with optional trailing\n // space (covers spaced French VAT keys whose first\n // char is a letter, like \"FR A1 123456789\" or\n // \"FR AB 123456789\"), then a digit, then 4+ value\n // chars. The trailing class permits letters so\n // alphanumeric VAT keys like \"FR1A123456789\" and\n // French NIR Corsican department codes like\n // \"1 84 12 2A 075 …\" are captured. A letter is only\n // admitted when it is glued directly to a preceding\n // digit (`(?<=\\d)[A-Z]`), so the regex cannot grow\n // past whitespace into a one-letter prose word\n // (\"SIREN 123456789 a son siège\" stops at \"9\").\n // Case-insensitive so lowercase variants\n // (\"DIČ cz12345678\", \"VAT number pl1234567890\")\n // still validate via stdnum downstream. An optional\n // 2-char alphanumeric key with optional trailing\n // space covers spaced French VAT keys whose first\n // char is a letter, like \"FR A1 123456789\" or\n // \"FR AB 123456789\". Dots are permitted inside the\n // value so dotted IDs such as Brazilian RG\n // (\"12.345.678-9\") and dotted CPF/CNPJ values\n // introduced by triggers are captured. Require at\n // least two leading digits before the dot-permitting\n // tail so single-digit dotted dates (\"6.11.2025\")\n // after triggers like \"DNI\" or \"RG\" do not slide\n // in. Stricter checksum validation for CPF/CNPJ runs\n // in the regex detector. A letter is only admitted\n // when it is glued directly to a preceding digit\n // (`(?<=\\d)[A-Z]`), so the regex cannot grow past\n // whitespace into a one-letter prose word\n // (\"SIREN 123456789 a son siège\" stops at \"9\").\n // This still captures alphanumeric VAT keys like\n // \"FR1A123456789\" and French NIR Corsican department\n // codes like \"1 84 12 2A 075 …\".\n const idMatch =\n /^[A-Z]{0,6}\\s?(?:[A-Z0-9]{2}\\s?)?\\d{2}(?:(?<=\\d)[A-Z]|[\\d\\s.\\-/]){3,}/i.exec(\n afterSep,\n );\n if (!idMatch) {\n return null;\n }\n // Optional trailing check letter must sit flush against\n // the last digit of the base match (no intervening\n // whitespace). This captures Spanish DNI / NIE / CIF /\n // NIF check letters (e.g. `DNI 12345678Z`,\n // `NIF A12345678J`) without sliding into the next word.\n const baseEndsOnDigit = /\\d$/.test(idMatch[0]);\n const trailingLetterMatch = baseEndsOnDigit\n ? /^[A-Z]/i.exec(afterSep.slice(idMatch[0].length))\n : null;\n const idRaw = trailingLetterMatch\n ? idMatch[0] + trailingLetterMatch[0]\n : idMatch[0];\n const idText = idRaw.trim();\n const leadingSpaces = idMatch[0].length - idMatch[0].trimStart().length;\n const idStart =\n triggerEnd +\n sepMatch[0].length +\n labelOffset +\n // idMatch.index is always 0 (anchored ^ regex)\n leadingSpaces;\n return {\n start: idStart,\n end: idStart + idText.length,\n text: idText,\n };\n }\n\n case \"address\": {\n // Walk through comma-separated segments.\n // Continue through a comma if the next segment\n // starts with a digit (PSČ like \"110 00\") or\n // an uppercase letter (city like \"Praha\").\n // Stop at: newline, opening paren \"(\", tab,\n // or a period that is NOT an abbreviation\n // (abbreviation = period followed by a space\n // and a lowercase letter, e.g. \"nábř. \").\n const maxLen = strategy.maxChars ?? 120;\n const UPPER_RE = /\\p{Lu}/u;\n const stopKeywords = getAddressStopKeywordsSync();\n let end = 0;\n\n while (end < valueText.length && end < maxLen) {\n const ch = valueText[end];\n\n // Hard stops: newline, opening paren.\n // Tabs are NOT hard stops — they appear as\n // formatting in structured documents between\n // trigger and value.\n if (ch === \"\\n\" || ch === \"(\") {\n break;\n }\n\n // Whitespace boundary: when an address has no\n // commas (e.g. headline-style triggers like\n // \"Adresse : 10 rue de la Paix Email : a@b.fr\"),\n // the comma-scoped stop-keyword check below\n // never fires. Re-check the stop list at every\n // whitespace boundary so a following field\n // label terminates the address before its value.\n if (ch === \" \" || ch === \"\\t\") {\n const afterWs = valueText.slice(end).trimStart().toLowerCase();\n const hitsKeyword = stopKeywords.some((kw) => {\n if (!afterWs.startsWith(kw)) return false;\n const next = afterWs[kw.length];\n return next === undefined || /[\\s:;.,!?()\\d]/.test(next);\n });\n if (hitsKeyword) {\n break;\n }\n }\n\n // Period: stop unless it's an abbreviation.\n // Abbreviation patterns in Czech addresses:\n // \"nábř. Kpt.\" — period + space + letter\n // \"č.p.\" — period + letter (no space)\n // \"1000/7.\" — period at end of address\n if (ch === \".\") {\n const next = valueText[end + 1];\n const afterNext = valueText[end + 2];\n // Field-label check: if the text after the\n // period (with optional space) begins with a\n // known stop-keyword (e.g. \"C.F.\", \"P.IVA\"),\n // treat the period as a clause boundary even\n // when followed by a letter/digit. Without\n // this, \"Via Roma 1. C.F. 12345678901\" would\n // absorb the tax-id label into the address.\n const afterPeriod = valueText\n .slice(end + 1)\n .replace(/^\\s+/, \"\")\n .toLowerCase();\n if (afterPeriod.length > 0) {\n const hitsKeywordAfterPeriod = getAddressStopKeywordsSync().some(\n (kw) => {\n if (!afterPeriod.startsWith(kw)) return false;\n const afterKw = afterPeriod[kw.length];\n return afterKw === undefined || /[\\s:;.,!?()\\d]/.test(afterKw);\n },\n );\n if (hitsKeywordAfterPeriod) {\n break;\n }\n }\n // \"č.p.\" or \"Kpt.J\" — period immediately\n // followed by letter or digit\n if (next !== undefined && (/\\p{L}/u.test(next) || /\\d/.test(next))) {\n end++;\n continue;\n }\n // \"nábř. Kpt.\" or \"ul. nová\" — period +\n // space + any letter or digit. Treat as an\n // abbreviation unless this is a genuine\n // sentence boundary (real word before, then\n // \". \" + uppercase start) — that case\n // happens when the address has already ended\n // and the next clause begins, e.g.\n // \"z siedzibą w Warszawie. Kapitał…\" /\n // \"Adresse : 10 rue de la Paix. Jean Dupont…\".\n if (\n next === \" \" &&\n afterNext !== undefined &&\n (/\\p{L}/u.test(afterNext) || /\\d/.test(afterNext))\n ) {\n if (isSentenceTerminator(valueText, end)) {\n break;\n }\n end++;\n continue;\n }\n // Period at end of address — stop but\n // don't include the period\n break;\n }\n\n // Comma: look ahead to see if address continues\n if (ch === \",\") {\n // Skip comma + whitespace, check next char\n let peek = end + 1;\n while (\n peek < valueText.length &&\n (valueText[peek] === \" \" || valueText[peek] === \"\\t\")\n ) {\n peek++;\n }\n const peekCh = valueText[peek];\n if (peekCh === undefined) {\n break;\n }\n // Check for field-label keywords after\n // comma before continuing. Keywords like\n // \"IČ\", \"DIČ\" start new fields. Loaded\n // from per-language JSON config so every\n // enabled language's stop words apply.\n const afterComma = valueText\n .slice(end + 1)\n .trimStart()\n .toLowerCase();\n const hitsKeyword = getAddressStopKeywordsSync().some((kw) => {\n if (!afterComma.startsWith(kw)) return false;\n // Guard: next char must be a delimiter\n // or digit to avoid truncating city\n // names like \"Telč\" on \"tel\". Digits\n // are included so \"IČ25672541\" (no\n // space) still triggers a stop.\n const next = afterComma[kw.length];\n return next === undefined || /[\\s:;.,!?()\\d]/.test(next);\n });\n if (hitsKeyword) {\n break;\n }\n // Continue if next segment starts with\n // digit (PSČ) or uppercase (city name)\n if (/\\d/.test(peekCh) || UPPER_RE.test(peekCh)) {\n end++;\n continue;\n }\n // Otherwise stop at this comma\n break;\n }\n\n end++;\n }\n\n // When the loop stopped at maxLen, trim back to\n // the last word boundary so fixPartialWords does\n // not extend the entity beyond the configured max.\n if (end >= maxLen) {\n const lastSpace = valueText.lastIndexOf(\" \", end - 1);\n if (lastSpace > 0) {\n end = lastSpace;\n }\n }\n\n const rawSlice = valueText.slice(0, end);\n const extracted = rawSlice.trim();\n if (extracted.length === 0) {\n return null;\n }\n const trailingSpaces = rawSlice.length - rawSlice.trimEnd().length;\n return {\n start: valueStart,\n end: valueStart + end - trailingSpaces,\n text: extracted,\n };\n }\n\n case \"match-pattern\": {\n // Anchor the configured pattern to the start of the\n // value text (after the generic leading-whitespace/\n // colon strip above). This prevents a missing or\n // placeholder value from stealing the next numeric\n // field on the same line: e.g. with a phone trigger\n // applied to `Téléphone : non communiqué SIREN :\n // 123456789`, an unanchored search would pull the\n // SIREN digits into the phone entity. Stops at the\n // first newline so a header-style trigger cannot\n // pull a value from a following line. The compiled\n // regex strips `g`/`y` flags so it stays stateless\n // across calls.\n const nlIdx = valueText.indexOf(\"\\n\");\n const searchText = nlIdx === -1 ? valueText : valueText.slice(0, nlIdx);\n if (searchText.length === 0) {\n return null;\n }\n const flags = (strategy.flags ?? \"\").replace(/[gy]/g, \"\");\n // Wrap the pattern in a non-capturing group so a\n // leading anchor authored in the config (e.g. `^`)\n // and authored alternation precedence still work\n // when the engine prepends its own start anchor.\n const anchoredSource = `^(?:${strategy.pattern})`;\n let re: RegExp;\n try {\n re = new RegExp(anchoredSource, flags);\n } catch {\n return null;\n }\n const m = re.exec(searchText);\n if (!m || m[0].length === 0) {\n return null;\n }\n const matchStart = m.index;\n const matchEnd = matchStart + m[0].length;\n return {\n start: valueStart + matchStart,\n end: valueStart + matchEnd,\n text: m[0],\n };\n }\n\n default:\n return null;\n }\n};\n\n// ── Match processor ─────────────────────────────────\n\n/**\n * Process trigger matches from the unified search.\n * Receives all matches; filters to the trigger slice\n * via sliceStart/sliceEnd. Uses fullText for value\n * extraction (the unified search runs on lowercased\n * text, but extraction needs original casing).\n */\nexport const processTriggerMatches = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText: string,\n rules: readonly TriggerRule[],\n): Entity[] => {\n const results: Entity[] = [];\n\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) {\n continue;\n }\n\n const localIdx = idx - sliceStart;\n\n // Left word-boundary: reject if preceded by a\n // letter (prevents partial keyword matches).\n if (match.start > 0 && LETTER_RE.test(fullText[match.start - 1] ?? \"\")) {\n continue;\n }\n\n const rule = rules[localIdx];\n if (!rule) {\n continue;\n }\n\n // Right word-boundary: reject only when the trigger\n // ends with a letter AND is followed by another\n // letter (which would mean the keyword bleeds into\n // a longer word, e.g. \"pan\" inside \"pana\"). Triggers\n // ending in punctuation (`:`, `'`, `’`, `°`, `.`, …)\n // or whitespace are already self-bounded: whatever\n // follows cannot extend the keyword token, so any\n // following character (letter, digit, etc.) is fine.\n const triggerLastChar = rule.trigger.at(-1) ?? \"\";\n if (\n LETTER_RE.test(triggerLastChar) &&\n LETTER_RE.test(fullText[match.end] ?? \"\")\n ) {\n continue;\n }\n\n const triggerEnd = match.end;\n const rawValue = extractValue(\n fullText,\n triggerEnd,\n rule.strategy,\n rule.label,\n );\n let value = rawValue ? stripQuotes(rawValue) : null;\n\n if (value) {\n // Apply declarative validations to the captured\n // value text (not the full entity including\n // trigger). This is intentional: validations\n // like min-length should test the extracted\n // value, not the trigger keyword itself.\n if (!applyValidations(value.text, rule.validations)) {\n continue;\n }\n\n // Label-shape invariant: a phone-number entity\n // must start like a phone value and contain enough\n // digits. Triggers like\n // a multilingual \"Phone:\" / \"PHONE:\" / \"Tel.:\"\n // can fire on signature blocks where the digit\n // value is blank (\"Phone: Date: 2026-05-15 ...\");\n // without this check the strategy emits a long\n // high-priority phone entity that can overlap and\n // suppress the later real phone number.\n if (\n rule.label === \"phone number\" &&\n !isPlausiblePhoneTriggerValue(value.text)\n ) {\n continue;\n }\n if (\n rule.label === \"phone number\" &&\n value.text.length > MAX_TRIGGER_VALUE_LEN &&\n fullText[value.end] !== \"\\n\" &&\n fullText[value.end] !== \"\\t\"\n ) {\n const cappedEnd = Math.min(\n capAtWordBoundary(value.text, MAX_TRIGGER_VALUE_LEN),\n MAX_TRIGGER_VALUE_LEN,\n );\n const cappedText = value.text.slice(0, cappedEnd).trimEnd();\n if (cappedText.length === 0) {\n continue;\n }\n value = {\n ...value,\n end: value.start + cappedText.length,\n text: cappedText,\n };\n }\n\n // When includeTrigger is set, the entity span\n // starts at the trigger match, not the value.\n const entityStart = rule.includeTrigger ? match.start : value.start;\n let entityEnd = value.end;\n let entityText = fullText.slice(entityStart, entityEnd);\n\n // Legal form reclassification: any person-labeled\n // trigger whose captured text contains a definitive\n // legal form suffix is reclassified as organization.\n // This is universal — every person with a legal form\n // is an organisation, no per-group config needed.\n const effectiveLabel =\n rule.label === \"person\" && hasKnownLegalFormSuffix(entityText)\n ? \"organization\"\n : rule.label;\n\n // Person name-run boundary: bound the value to its\n // leading run of name-shaped tokens so a delimiter\n // scan that found no comma cannot sweep following\n // prose into the span (\"Pan Novák bydlí v Praze.\"\n // must capture \"Novák\", not the whole clause).\n // Applies only to values still labeled person (a\n // legal-form capture stays greedy: trimming would\n // drop the suffix the reclassification relies on).\n // Values that do not start with a name token are\n // left as-is for the declarative validations.\n if (effectiveLabel === \"person\") {\n const run = PERSON_NAME_RUN_RE.exec(value.text);\n if (run !== null && run[0].length < value.text.length) {\n entityEnd = value.start + run[0].length;\n entityText = fullText.slice(entityStart, entityEnd);\n }\n }\n\n results.push({\n start: entityStart,\n end: entityEnd,\n label: effectiveLabel,\n text: entityText,\n score: TRIGGER_SCORE,\n source: DETECTION_SOURCES.TRIGGER,\n });\n }\n }\n\n return results;\n};\n\n// Re-export for testing\nexport { expandTriggerGroups, compileValidations, applyValidations };\n","/**\n * Geographic regions and country code mappings for\n * scoping deny list dictionaries.\n */\n\nexport const REGIONS = {\n Global: null,\n International: null,\n Europe: [\n \"AL\",\n \"AD\",\n \"AT\",\n \"BE\",\n \"BA\",\n \"BG\",\n \"HR\",\n \"CY\",\n \"CZ\",\n \"DK\",\n \"EE\",\n \"FI\",\n \"FR\",\n \"DE\",\n \"GR\",\n \"HU\",\n \"IS\",\n \"IE\",\n \"IT\",\n \"XK\",\n \"LV\",\n \"LI\",\n \"LT\",\n \"LU\",\n \"MD\",\n \"ME\",\n \"MK\",\n \"MT\",\n \"MC\",\n \"NL\",\n \"NO\",\n \"PL\",\n \"PT\",\n \"RO\",\n \"RS\",\n \"SK\",\n \"SI\",\n \"ES\",\n \"SE\",\n \"CH\",\n \"UA\",\n \"GB\",\n ],\n Americas: [\n \"US\",\n \"CA\",\n \"MX\",\n \"BR\",\n \"AR\",\n \"CL\",\n \"CO\",\n \"PE\",\n \"EC\",\n \"VE\",\n \"UY\",\n \"PY\",\n \"BO\",\n \"CR\",\n \"PA\",\n \"DO\",\n \"GT\",\n \"HN\",\n \"SV\",\n \"NI\",\n \"CU\",\n ],\n AsiaPacific: [\n \"AU\",\n \"NZ\",\n \"JP\",\n \"KR\",\n \"CN\",\n \"TW\",\n \"SG\",\n \"MY\",\n \"TH\",\n \"VN\",\n \"PH\",\n \"ID\",\n \"IN\",\n \"PK\",\n \"BD\",\n \"LK\",\n \"NP\",\n \"HK\",\n \"MO\",\n ],\n MENA: [\n \"AE\",\n \"SA\",\n \"IL\",\n \"TR\",\n \"EG\",\n \"JO\",\n \"LB\",\n \"IQ\",\n \"IR\",\n \"QA\",\n \"KW\",\n \"BH\",\n \"OM\",\n \"MA\",\n \"TN\",\n \"DZ\",\n \"LY\",\n \"SY\",\n \"YE\",\n \"PS\",\n ],\n SubSaharanAfrica: [\n \"ZA\",\n \"NG\",\n \"KE\",\n \"GH\",\n \"TZ\",\n \"ET\",\n \"SN\",\n \"CI\",\n \"CM\",\n \"UG\",\n \"RW\",\n \"MZ\",\n \"AO\",\n \"ZW\",\n \"BW\",\n \"NA\",\n \"MU\",\n ],\n EU: [\n \"AT\",\n \"BE\",\n \"BG\",\n \"HR\",\n \"CY\",\n \"CZ\",\n \"DK\",\n \"EE\",\n \"FI\",\n \"FR\",\n \"DE\",\n \"GR\",\n \"HU\",\n \"IE\",\n \"IT\",\n \"LV\",\n \"LT\",\n \"LU\",\n \"MT\",\n \"NL\",\n \"PL\",\n \"PT\",\n \"RO\",\n \"SK\",\n \"SI\",\n \"ES\",\n \"SE\",\n ],\n DACH: [\"DE\", \"AT\", \"CH\"],\n Nordics: [\"DK\", \"SE\", \"NO\", \"FI\", \"IS\"],\n CEE: [\"CZ\", \"SK\", \"PL\", \"HU\", \"RO\", \"BG\", \"HR\", \"SI\", \"LT\", \"LV\", \"EE\"],\n Anglosphere: [\"GB\", \"US\", \"CA\", \"AU\", \"NZ\", \"IE\"],\n Benelux: [\"BE\", \"NL\", \"LU\"],\n GulfStates: [\"AE\", \"SA\", \"QA\", \"KW\", \"BH\", \"OM\"],\n SouthAsia: [\"IN\", \"PK\", \"BD\", \"LK\", \"NP\"],\n EastAsia: [\"CN\", \"JP\", \"KR\", \"TW\"],\n SoutheastAsia: [\"SG\", \"MY\", \"TH\", \"VN\", \"PH\", \"ID\"],\n Oceania: [\"AU\", \"NZ\"],\n} as const;\n\nexport type RegionId = keyof typeof REGIONS;\n\ntype RegionArrays = {\n [K in RegionId]: (typeof REGIONS)[K];\n};\n\ntype NonNullRegion = {\n [K in RegionId as RegionArrays[K] extends null ? never : K]: RegionArrays[K];\n};\n\nexport type CountryCode = NonNullRegion[keyof NonNullRegion][number];\n\n/**\n * Expand region names to country codes and merge with\n * explicit country codes. Returns null when both inputs\n * are empty/undefined (meaning \"match all countries\").\n */\nexport const resolveCountries = (\n regions?: string[],\n countries?: string[],\n): Set<string> | null => {\n const hasRegions = regions && regions.length > 0;\n const hasCountries = countries && countries.length > 0;\n\n if (!hasRegions && !hasCountries) {\n return null;\n }\n\n const result = new Set<string>();\n\n const isRegion = (name: string): name is RegionId => name in REGIONS;\n\n if (hasRegions) {\n for (const name of regions) {\n if (!isRegion(name)) {\n continue;\n }\n const codes = REGIONS[name];\n if (codes === null) {\n // Global/International: return null (all)\n return null;\n }\n for (const code of codes) {\n result.add(code);\n }\n }\n }\n\n if (hasCountries) {\n for (const code of countries) {\n result.add(code);\n }\n }\n\n return result;\n};\n","/**\n * Fold Cyrillic and Greek lookalike characters to their\n * Latin equivalents for stop-list lookups.\n *\n * Adversarial inputs and OCR artifacts can produce\n * mixed-script tokens like \"mаnager\" (with a Cyrillic\n * `а`) that visually read as Latin but bypass a Latin-\n * keyed stop-list. Folding before lookup closes that\n * gap.\n *\n * Only used inside stop-list / false-positive checks —\n * never on stored entity text. Genuine Cyrillic words\n * never pass through this path. Replacements are same-\n * length (single code unit → single code unit), so\n * offsets remain valid if a caller needs them.\n *\n * Mappings are restricted to letters that are visually\n * indistinguishable from Latin in common fonts. Glyphs\n * whose lowercase form does not resemble Latin (e.g.\n * Cyrillic lowercase `в`, which looks nothing like\n * lowercase `b`) are omitted from the lowercase\n * section even when their uppercase form is included.\n */\n\nconst REPLACEMENTS: readonly [number, number][] = [\n // ── Cyrillic uppercase ───────────────────────────────\n [0x0410, 0x0041], // А → A\n [0x0412, 0x0042], // В → B\n [0x0415, 0x0045], // Е → E\n [0x041a, 0x004b], // К → K\n [0x041c, 0x004d], // М → M\n [0x041d, 0x0048], // Н → H\n [0x041e, 0x004f], // О → O\n [0x0420, 0x0050], // Р → P\n [0x0421, 0x0043], // С → C\n [0x0422, 0x0054], // Т → T\n [0x0425, 0x0058], // Х → X\n // ── Cyrillic lowercase ───────────────────────────────\n [0x0430, 0x0061], // а → a\n [0x0435, 0x0065], // е → e\n [0x043e, 0x006f], // о → o\n [0x0440, 0x0070], // р → p\n [0x0441, 0x0063], // с → c\n [0x0443, 0x0079], // у → y\n [0x0445, 0x0078], // х → x\n // ── Other Cyrillic (Ukrainian/Belarusian/Serbian) ────\n [0x0456, 0x0069], // і → i\n [0x0458, 0x006a], // ј → j\n // ── Greek uppercase ──────────────────────────────────\n [0x0391, 0x0041], // Α → A\n [0x0392, 0x0042], // Β → B\n [0x0395, 0x0045], // Ε → E\n [0x0397, 0x0048], // Η → H\n [0x0399, 0x0049], // Ι → I\n [0x039a, 0x004b], // Κ → K\n [0x039c, 0x004d], // Μ → M\n [0x039d, 0x004e], // Ν → N\n [0x039f, 0x004f], // Ο → O\n [0x03a1, 0x0050], // Ρ → P\n [0x03a4, 0x0054], // Τ → T\n [0x03a5, 0x0059], // Υ → Y\n [0x03a7, 0x0058], // Χ → X\n [0x0396, 0x005a], // Ζ → Z\n // ── Greek lowercase ──────────────────────────────────\n [0x03b1, 0x0061], // α → a\n [0x03ba, 0x006b], // κ → k\n [0x03bf, 0x006f], // ο → o\n [0x03c1, 0x0070], // ρ → p\n];\n\nconst CHAR_MAP = new Map<number, number>(REPLACEMENTS);\n\n/** Chunk size for `String.fromCharCode` to avoid hitting\n * the call-stack limit on very large strings. */\nconst CHUNK_SIZE = 8192;\n\nexport const normalizeHomoglyphs = (text: string): string => {\n let hasSpecial = false;\n for (let i = 0; i < text.length; i++) {\n if (CHAR_MAP.has(text.charCodeAt(i))) {\n hasSpecial = true;\n break;\n }\n }\n if (!hasSpecial) return text;\n\n const len = text.length;\n const codes = new Uint16Array(len);\n for (let i = 0; i < len; i++) {\n const code = text.charCodeAt(i);\n codes[i] = CHAR_MAP.get(code) ?? code;\n }\n\n if (len <= CHUNK_SIZE) {\n return String.fromCharCode(...codes);\n }\n\n let result = \"\";\n for (let offset = 0; offset < len; offset += CHUNK_SIZE) {\n const end = Math.min(offset + CHUNK_SIZE, len);\n result += String.fromCharCode(...codes.subarray(offset, end));\n }\n return result;\n};\n","import type { Entity } from \"../types\";\nimport type { PipelineContext } from \"../context\";\nimport { defaultContext } from \"../context\";\nimport { getPersonStopwords } from \"../detectors/deny-list\";\nimport { normalizeHomoglyphs } from \"../util/homoglyphs\";\n\nconst TEMPLATE_PLACEHOLDER_RE = /^(?:\\.{3,}|_{3,}|\\[[\\w\\s]+\\]|\\{[\\w\\s]+\\})$/;\n\n// Patterns that indicate a genuine address (not prose).\nconst POSTAL_CODE_RE = /\\d{3}\\s?\\d{2}/;\nconst HAS_DIGIT_RE = /\\d/;\n// Address-component anchors not derived from per-language\n// street-type vocabulary: Czech house/parcel number forms\n// (č.p., č.ev., č.) and \"sídliště\" (housing estate) which\n// is a settlement type rather than a street type. Polish,\n// pt-BR, English, etc. street vocabulary is loaded from\n// address-street-types.json via initAddressComponents.\nconst ADDRESS_COMPONENT_EXTRA_RE =\n /(?:^|\\s)(?:č\\.p\\.|č\\.ev\\.|č\\.|sídliště)(?=[\\s,./]|$)/i;\n\n// Bare French \"cours\" is ambiguous: \"Cours Mirabeau\" is a\n// genuine street, but \"au cours du contrat\" is prose. Real\n// addresses either include digits (caught by the prior\n// rule) or a capitalized proper-name token right after\n// `cours`. Returns true when the only address-component\n// token in `text` is bare lowercase `cours` AND it is not\n// followed by a capitalized name token, i.e. the entity is\n// almost certainly prose and should be rejected.\nconst BARE_COURS_PROSE_RE = /(?:^|\\s)cours(?!\\s+\\p{Lu})(?=[\\s,./]|$)/u;\nconst isOnlyAmbiguousCours = (text: string): boolean => {\n if (!BARE_COURS_PROSE_RE.test(text)) return false;\n // Strip every bare \"cours\" occurrence; if anything else\n // still matches a street-type word, the entity has\n // other street-type evidence and is not \"only cours\".\n const stripped = text.replace(/(?:^|\\s)cours(?=[\\s,./]|$)/gi, \" \");\n return !hasAddressComponent(stripped);\n};\n\n// Jurisdiction patterns: \"State of X\", \"Commonwealth of X\",\n// \"District of X\", \"Territory of X\"\n// — valid address entities without digits or street words.\nconst JURISDICTION_RE = /^(?:state|commonwealth|district|territory)\\s+of\\s+/i;\n\n// Max entity text length by label. Prevents runaway\n// trigger extractions (e.g., \"město Dobříš i okolních\n// obcí...\") from producing absurdly long entities.\nconst MAX_ENTITY_LENGTH: Partial<Record<string, number>> = {\n organization: 80,\n person: 60,\n};\n\n// Per-label upper bound on word count. Real-world\n// organisation names cap out well below this even for\n// long firm names (\"European Bank for Reconstruction\n// and Development\" — 6). A trigger or coreference span\n// running past this is almost certainly absorbing prose.\n// Only open-ended sources (trigger, coreference) are\n// subject to this cap: gazetteer/NER/regex detections\n// are bounded by their dictionary, model, or pattern\n// and may legitimately span longer names like \"The\n// University of Texas Health Science Center at Houston\".\nconst MAX_ENTITY_WORDS: Partial<Record<string, number>> = {\n organization: 8,\n};\nconst OPEN_ENDED_SOURCES: ReadonlySet<string> = new Set([\n \"trigger\",\n \"coreference\",\n]);\nconst WORD_TOKEN_RE = /\\p{L}[\\p{L}\\p{M}\\p{N}'’\\-./]*/gu;\nconst countWordTokens = (text: string): number => {\n let count = 0;\n WORD_TOKEN_RE.lastIndex = 0;\n while (WORD_TOKEN_RE.exec(text) !== null) count += 1;\n return count;\n};\n\n// Lines whose visible letters are overwhelmingly\n// uppercase are EITHER:\n// - heading/boilerplate prose (SEC securities legends,\n// \"THIS INSTRUMENT AND ANY SECURITIES ...\" clauses,\n// numbered section headings such as \"17.NO\n// ASSIGNMENT.\"),\n// - or genuine party captions where a real org name\n// happens to be rendered in caps on its own line\n// (\"TWITTER, INC.\", \"X HOLDINGS I, INC.\").\n// The old guard rejected both cases. We now keep\n// captions: the deciding signal is whether the line\n// contains substantial *prose* outside the candidate\n// span. SEC legends sprawl across the line; captions\n// occupy nearly all of it. A numbered section heading\n// (\"17.NO ASSIGNMENT.\") is also rejected outright\n// because its all-caps span is part of the section\n// title, not a party name.\nconst ALL_CAPS_LINE_LETTER_THRESHOLD = 5;\nconst ALL_CAPS_LINE_RATIO = 0.95;\nconst ALL_CAPS_LINE_PROSE_EXTRA_LETTERS = 20;\n// A genuine party caption has the shape \"Name[, ]Suffix\"\n// — short, with a comma marking the suffix break (or no\n// suffix-style separator at all). Heading lines such as\n// \"REGISTRATION STATEMENT\" or Czech \"RÁMCOVÁ DOHODA NA\n// POSKYTOVÁNÍ PRÁVNÍCH SLUŽEB\" are longer prose-style\n// sequences without a comma. The word-count cutoff is\n// deliberately permissive; legitimate firm names like\n// \"European Bank for Reconstruction and Development\"\n// (6 words) keep their comma + legal-form suffix, so\n// they retain the suffix break test above.\nconst ALL_CAPS_LINE_HEADING_WORD_LIMIT = 5;\n// Section-number prefix at the start of a line, with\n// optional leading whitespace so indented headings\n// (\" 17. NO ASSIGNMENT.\", \"§ 3\") still match. The\n// trailing uppercase character anchors the title token.\nconst SECTION_HEADING_PREFIX_RE =\n /^\\s*(?:§\\s*)?\\d{1,3}(?:\\.\\d{1,3}){0,4}\\.?\\s*\\p{Lu}/u;\nconst LINE_LETTER_RE = /\\p{L}/gu;\nconst ENTITY_WORD_TOKEN_RE = /\\p{L}[\\p{L}\\p{M}\\p{N}'’-]*/gu;\nconst isAllCapsBoilerplateLine = (\n fullText: string,\n start: number,\n length: number,\n): boolean => {\n const lineStart = fullText.lastIndexOf(\"\\n\", start) + 1;\n const lineEndIdx = fullText.indexOf(\"\\n\", start + length);\n const line = fullText.slice(\n lineStart,\n lineEndIdx === -1 ? fullText.length : lineEndIdx,\n );\n let letterCount = 0;\n let upperCount = 0;\n let outsideEntityLetters = 0;\n const entityRelStart = start - lineStart;\n const entityRelEnd = entityRelStart + length;\n LINE_LETTER_RE.lastIndex = 0;\n for (\n let m = LINE_LETTER_RE.exec(line);\n m !== null;\n m = LINE_LETTER_RE.exec(line)\n ) {\n const ch = m[0];\n letterCount += 1;\n if (ch === ch.toUpperCase() && ch !== ch.toLowerCase()) {\n upperCount += 1;\n }\n if (m.index < entityRelStart || m.index >= entityRelEnd) {\n outsideEntityLetters += 1;\n }\n }\n if (letterCount <= ALL_CAPS_LINE_LETTER_THRESHOLD) return false;\n if (upperCount / letterCount < ALL_CAPS_LINE_RATIO) return false;\n // Numbered section headings are always boilerplate.\n if (SECTION_HEADING_PREFIX_RE.test(line)) return true;\n // SEC legend / paragraph: substantial all-caps prose\n // outside the entity span.\n if (outsideEntityLetters >= ALL_CAPS_LINE_PROSE_EXTRA_LETTERS) return true;\n // Heading-shape inside the entity itself: many words\n // and no comma to mark a Name+Suffix break. Czech\n // section titles (\"RÁMCOVÁ DOHODA NA POSKYTOVÁNÍ\n // PRÁVNÍCH SLUŽEB\") match here. Real captions like\n // \"ACME CORPORATION\" (2 words, no comma) and\n // \"TWITTER, INC.\" (comma) survive.\n const entityText = fullText.slice(start, start + length);\n const entityWordCount = (entityText.match(ENTITY_WORD_TOKEN_RE) ?? []).length;\n if (\n entityWordCount > ALL_CAPS_LINE_HEADING_WORD_LIMIT &&\n !entityText.includes(\",\")\n ) {\n return true;\n }\n return false;\n};\nconst isAllCapsCandidate = (text: string): boolean =>\n text === text.toUpperCase() && /\\p{Lu}/u.test(text);\n// Section/clause numbers: \"§ 3\", \"3.2.1\", \"12.\" but NOT\n// dates like \"4.3.2026\" or long digit strings like IČO.\n// A section number has 1-3 digit groups of 1-3 digits each,\n// never ending with a 4-digit group (that's a year).\nconst SECTION_NUMBER_RE = /^(?:§\\s*)?\\d{1,3}(?:\\.\\d{1,3}){0,4}\\.?$/;\nconst STANDALONE_YEAR_RE = /^(?:19|20)\\d{2}$/;\n\n// Number-abbreviation prefixes: \"č.\", \"Nr.\", \"No.\", \"nr.\",\n// \"no.\", \"n.\", \"čís.\" — when a numeric entity is preceded\n// by one of these, it's a reference number, not PII.\nconst NUMBER_ABBREV_RE = /(?:^|[\\s(])(?:č|čís|nr|no|n)\\.\\s*$/i;\nconst SIGNING_CLAUSE_ADDRESS_RE = /^(?:v|ve)\\s+[^\\d,\\n]{1,40},?\\s+dne$/iu;\nconst PERSON_TRAILING_NOUNS: ReadonlySet<string> = new Set([\n \"association\",\n \"period\",\n \"reform\",\n]);\nconst LEGAL_FORM_HEADING_RE = /\\b(?:agreement|amendment|contract|exhibit)\\b/iu;\nconst LEADING_ARTIFACT_RE = /^(?:\\.\\s)+/u;\nconst ADDRESS_ROLE_PREFIX_RE =\n /^(?:prodávajícího|kupujícího|objednatele|zhotovitele|pronajímatele|dodavatele|odběratele|zaměstnance|zaměstnavatele|nájemce)\\s+(?=(?:\\p{Lu}|\\d|ul\\.?|ulice|nám\\.?|náměstí|tř\\.?|třída|nábř\\.?|nábřeží|č\\.p\\.?|č\\.ev\\.?|sídliště))/iu;\nconst ADDRESS_INLINE_ABBREV_AFTER_RE =\n /^(?:\\p{Lu}[\\p{L}\\p{M}]{0,3}\\.|ul\\.?|nám\\.?|tř\\.?|nábř\\.?|č\\.p\\.?|č\\.ev\\.?)/u;\nconst ADDRESS_INLINE_ABBREV_BEFORE_RE =\n /(?:^|[\\s,])(?:st|ave|rd|dr|blvd|ln|hwy|pkwy|cir|ct|pl|sq|ter|trl|ste|apt|bldg|fl|ul|nám|tř|nábř|č\\.p|č\\.ev)$/iu;\nconst ADDRESS_CONTINUATION_WORD_RE =\n /^(?:suite|building|floor|unit|apartment|room|tower|wing|block|bldg|ste|apt|fl)\\b/iu;\n\nconst trimTrailingAddressProse = (text: string): string => {\n for (const match of text.matchAll(/\\.(?=\\s+\\p{Lu})/gu)) {\n const cutoff = match.index;\n if (cutoff === undefined) {\n continue;\n }\n const before = text.slice(0, cutoff);\n if (!HAS_DIGIT_RE.test(before)) {\n continue;\n }\n const after = text.slice(cutoff + 1).trimStart();\n if (\n after.length < 5 ||\n ADDRESS_INLINE_ABBREV_AFTER_RE.test(after) ||\n ADDRESS_INLINE_ABBREV_BEFORE_RE.test(before.trimEnd()) ||\n ADDRESS_CONTINUATION_WORD_RE.test(after)\n ) {\n continue;\n }\n return before.trimEnd();\n }\n\n return text;\n};\n\nconst normalizeEntity = (entity: Entity): Entity | null => {\n let start = entity.start;\n let text = entity.text;\n\n const trimLeading = (re: RegExp) => {\n const match = re.exec(text);\n if (!match) {\n return;\n }\n start += match[0].length;\n text = text.slice(match[0].length);\n };\n\n trimLeading(LEADING_ARTIFACT_RE);\n trimLeading(/^\\s+/u);\n\n if (entity.label === \"address\") {\n trimLeading(ADDRESS_ROLE_PREFIX_RE);\n text = trimTrailingAddressProse(text);\n }\n\n const trailingMatch = /[,\\s]+$/u.exec(text);\n if (trailingMatch) {\n text = text.slice(0, text.length - trailingMatch[0].length);\n }\n\n if (text.length === 0) {\n return null;\n }\n\n return {\n ...entity,\n start,\n end: start + text.length,\n text,\n };\n};\n\n// ── Document-structure headings (lazy-loaded from JSON) ──\n//\n// Per-language list of heading words (`příloha`, `anlage`,\n// `schedule`, …) that the trigger detector emits as organisations\n// when they precede an ordinal-abbreviation+digit shape\n// (`č. 2`, `Nr. 3`, `No. 4`). The set is loaded once and cached on\n// the module — the heading vocabulary is language-data, not state.\n\nlet cachedHeadingRe: RegExp | null = null;\nlet cachedHeadingPromise: Promise<RegExp> | null = null;\nconst ORDINAL_MARKER = \"(?:č|no|nr|n)\\\\.?\";\n\nconst buildHeadingRegex = (words: readonly string[]): RegExp => {\n if (words.length === 0) {\n return /[\\s\\S](?!)/u;\n }\n const sorted = [...words].sort((a, b) => b.length - a.length);\n const escaped = sorted\n .map((w) => w.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"))\n .join(\"|\");\n return new RegExp(\n `^(?:${escaped})[\\\\s\\\\u00a0]+(?:${ORDINAL_MARKER}|#)[\\\\s\\\\u00a0]*\\\\d`,\n \"iu\",\n );\n};\n\nconst loadHeadingWords = async (): Promise<readonly string[]> => {\n try {\n const mod = await import(\"../data/document-structure-headings.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON shape\n const data = (mod as { default?: Record<string, unknown> }).default ?? mod;\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON shape\n const entries = Object.entries(data as Record<string, unknown>);\n const out: string[] = [];\n const seen = new Set<string>();\n for (const [key, value] of entries) {\n if (key.startsWith(\"_\")) continue;\n if (!Array.isArray(value)) continue;\n for (const word of value) {\n if (typeof word !== \"string\" || word.length === 0) continue;\n const lc = word.toLowerCase();\n if (seen.has(lc)) continue;\n seen.add(lc);\n out.push(lc);\n }\n }\n return out;\n } catch {\n return [];\n }\n};\n\nexport const loadDocumentStructureHeadings = async (): Promise<void> => {\n if (cachedHeadingRe) return;\n cachedHeadingPromise ??= loadHeadingWords().then(buildHeadingRegex);\n cachedHeadingRe = await cachedHeadingPromise;\n};\n\nconst isDocumentStructureHeading = (text: string): boolean => {\n const re = cachedHeadingRe;\n if (!re) return false;\n return re.test(text);\n};\n\n// ── Generic roles (lazy-loaded from JSON) ────────────\n\nconst EMPTY_GENERIC_ROLES: ReadonlySet<string> = new Set();\n\n/**\n * Load generic-roles.json and cache the result on the\n * given context. Must be awaited during pipeline init\n * so the sync accessor is populated before\n * filterFalsePositives runs.\n */\nexport const loadGenericRoles = (\n ctx: PipelineContext = defaultContext,\n): Promise<ReadonlySet<string>> => {\n if (ctx.genericRolesPromise) {\n return ctx.genericRolesPromise;\n }\n ctx.genericRolesPromise = (async () => {\n try {\n const mod: {\n default?: { roles?: string[] };\n } = await import(\"../data/generic-roles.json\");\n const set: ReadonlySet<string> = new Set(mod.default?.roles ?? []);\n ctx.genericRoles = set;\n return set;\n } catch {\n const empty: ReadonlySet<string> = new Set();\n ctx.genericRoles = empty;\n return empty;\n }\n })();\n return ctx.genericRolesPromise;\n};\n\n/** Sync accessor — returns empty set before init. */\nconst getGenericRoles = (ctx: PipelineContext): ReadonlySet<string> =>\n ctx.genericRoles ?? EMPTY_GENERIC_ROLES;\n\n// ── Street-type vocabulary (lazy-loaded from JSON) ───\n//\n// Builds a single regex from address-street-types.json\n// that recognises any per-language street word (Polish\n// \"aleja\", \"ulicy\"; English \"Street\", \"Avenue\"; etc.) as\n// a genuine address component. Falls back to a permissive\n// match-nothing regex before init.\n\n// Baseline regex used when address-street-types.json\n// has not been loaded yet (e.g. callers using\n// `filterFalsePositives` directly without going through\n// `runPipeline`). Mirrors the previous hardcoded Czech\n// street-word anchors so trigger-sourced digitless\n// Czech addresses (\"Národní třída\") still survive the\n// digit gate before initAddressComponents() runs.\nconst STREET_TYPES_SEED_RE =\n /(?:^|\\s)(?:ul\\.|ulice|nám\\.|náměstí|tř\\.|třída|nábř\\.|nábřeží|bulvár)(?=[\\s,./]|$)/i;\nlet _streetTypesRe: RegExp = STREET_TYPES_SEED_RE;\nlet _streetTypesPromise: Promise<void> | null = null;\n\nconst escapeRegex = (s: string): string =>\n s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\nconst loadStreetTypeRegex = async (): Promise<void> => {\n try {\n const mod: { default?: Record<string, string[] | string> } =\n await import(\"../data/address-street-types.json\");\n const data = mod.default ?? {};\n const words = new Set<string>();\n for (const [key, val] of Object.entries(data)) {\n if (key.startsWith(\"_\")) continue;\n if (!Array.isArray(val)) continue;\n for (const w of val) {\n if (typeof w === \"string\" && w.length > 0) {\n words.add(w.toLowerCase());\n }\n }\n }\n if (words.size === 0) {\n _streetTypesRe = STREET_TYPES_SEED_RE;\n return;\n }\n // Longest first so \"aleja\" doesn't shadow \"al.\"\n const alternation = [...words]\n .sort((a, b) => b.length - a.length)\n .map(escapeRegex)\n .join(\"|\");\n _streetTypesRe = new RegExp(\n `(?:^|\\\\s)(?:${alternation})(?=[\\\\s,./]|$)`,\n \"iu\",\n );\n } catch {\n _streetTypesRe = STREET_TYPES_SEED_RE;\n }\n};\n\n/** Ensure street-type vocabulary is loaded. */\nexport const initAddressComponents = (): Promise<void> => {\n if (!_streetTypesPromise) {\n _streetTypesPromise = loadStreetTypeRegex();\n }\n return _streetTypesPromise;\n};\n\n/**\n * True when `text` contains a recognised address\n * component: either a per-language street word (loaded\n * from JSON) or a language-agnostic anchor such as a\n * Czech house number form.\n */\nconst hasAddressComponent = (text: string): boolean =>\n _streetTypesRe.test(text) || ADDRESS_COMPONENT_EXTRA_RE.test(text);\n\nconst isCallerOwnedEntity = (entity: Entity): boolean =>\n entity.sourceDetail === \"custom-deny-list\" ||\n entity.sourceDetail === \"custom-regex\";\n\n/**\n * Filter out entities that are likely false positives:\n * template placeholders, clause/section numbers,\n * standalone years, and generic legal role terms.\n *\n * Runs as a post-processing step after all detection\n * layers have merged.\n */\nexport const filterFalsePositives = (\n entities: Entity[],\n ctx: PipelineContext = defaultContext,\n fullText?: string,\n): Entity[] => {\n const filtered: Entity[] = [];\n const roles = getGenericRoles(ctx);\n\n for (const entity of entities) {\n if (isCallerOwnedEntity(entity)) {\n filtered.push(entity);\n continue;\n }\n\n const normalized = normalizeEntity(entity);\n if (!normalized) {\n continue;\n }\n\n // Strip leading \". \" artifacts from trigger extraction\n // after abbreviations (\"dat. nar.\", \"č.p.\").\n const trimmed = normalized.text;\n\n if (isCallerOwnedEntity(normalized)) {\n filtered.push(normalized);\n continue;\n }\n\n if (TEMPLATE_PLACEHOLDER_RE.test(trimmed)) {\n continue;\n }\n // Reject entities exceeding max length for their\n // label (prevents runaway trigger extractions).\n // Exempt legal-form entities: their span is already\n // bounded by the regex pattern, not open-ended.\n const maxLen = MAX_ENTITY_LENGTH[normalized.label];\n if (\n maxLen &&\n trimmed.length > maxLen &&\n normalized.source !== \"legal-form\"\n ) {\n continue;\n }\n // Word-count cap. The byte-length cap above only\n // catches truly runaway spans; a 70-char trigger\n // capture full of short jargon words still slips\n // through despite being clearly prose. Org names in\n // practice cap out at 6 words even for verbose firm\n // names; 8 leaves headroom without admitting a full\n // boilerplate clause.\n const maxWords = MAX_ENTITY_WORDS[normalized.label];\n if (\n maxWords &&\n OPEN_ENDED_SOURCES.has(normalized.source) &&\n countWordTokens(trimmed) > maxWords\n ) {\n continue;\n }\n // SEC-style legends, numbered section headings\n // (\"17.NO ASSIGNMENT.\"), and other boilerplate\n // disclosure blocks render as all-uppercase lines.\n // Detectors anchored to uppercase tokens otherwise\n // emit bigrams like \"SECURITIES ACT\" or\n // \"REGISTRATION STATEMENT\" as organization spans,\n // and the legal-form regex matches headings such as\n // \"NO ASSIGNMENT\". Real party captions almost always\n // carry a legal-form suffix (\"ACME CORPORATION\")\n // and survive via the legal-forms detector's own\n // 3-word-on-mixed-case pathway, so we gate every\n // all-caps organization candidate whose surrounding\n // line is itself all-caps regardless of source.\n if (\n fullText &&\n normalized.label === \"organization\" &&\n isAllCapsCandidate(trimmed) &&\n isAllCapsBoilerplateLine(fullText, normalized.start, trimmed.length)\n ) {\n continue;\n }\n // Section numbers (§ 3, 3.2.1, 12.) are false\n // positives unless they were captured by a trigger\n // phrase (e.g., \"č.p. 92\" is an address, not a\n // section number).\n if (SECTION_NUMBER_RE.test(trimmed) && normalized.source !== \"trigger\") {\n continue;\n }\n // Standalone years (2022, 1995) without a trigger\n // context are noise. Trigger-sourced years are\n // valid (\"rok 2022\", \"year 2019\").\n if (STANDALONE_YEAR_RE.test(trimmed) && normalized.source !== \"trigger\") {\n continue;\n }\n\n // Numeric entities preceded by a number abbreviation\n // (\"č.\", \"Nr.\", \"No.\") are usually reference\n // numbers, not PII. Apply this only to non-trigger\n // entities: trigger-based detections intentionally\n // use these abbreviations as their semantic anchor\n // (e.g. \"parc. č. 852/2\", \"LV č. 154\",\n // \"Flurstück Nr. 1234\").\n if (\n fullText &&\n normalized.source !== \"trigger\" &&\n /^\\d/.test(trimmed) &&\n NUMBER_ABBREV_RE.test(\n fullText.slice(Math.max(0, normalized.start - 10), normalized.start),\n )\n ) {\n continue;\n }\n\n if (\n normalized.label === \"registration number\" &&\n /^[\\p{L}]{1,2}$/u.test(trimmed)\n ) {\n continue;\n }\n\n // Document-structure headings (Czech `Příloha č.2`, German\n // `Anlage Nr. 3`, English `Schedule No. 4`) get captured by the\n // trigger detector as organizations. They are scaffolding, not\n // party references. The heading vocabulary lives in\n // `data/document-structure-headings.json`; the regex composes the\n // current word set with the cross-language ordinal abbreviations.\n if (\n normalized.label === \"organization\" &&\n isDocumentStructureHeading(trimmed)\n ) {\n continue;\n }\n\n // Person names never contain digits.\n // \"Solution Pack ABL90 Flex\" → reject.\n if (normalized.label === \"person\" && HAS_DIGIT_RE.test(trimmed)) {\n continue;\n }\n\n // Demonstrative pronouns and other words that collide\n // with rare given names in the corpus (e.g. Czech\n // \"Tato\" — both a sentence-opening demonstrative and\n // an Italian/Iberian first name). The deny-list path\n // applies this filter itself; this catches the same\n // tokens when they leak out of NER or any other\n // single-token person source.\n if (normalized.label === \"person\") {\n const trimmedToken = trimmed.replace(/[.,;:!?]+$/u, \"\").trim();\n if (\n !/\\s/.test(trimmedToken) &&\n getPersonStopwords(ctx).has(trimmedToken.toLowerCase())\n ) {\n continue;\n }\n }\n\n if (normalized.label === \"person\") {\n const tokens = trimmed.split(/\\s+/u);\n // Fold homoglyphs *before* lowercasing: uppercase\n // Greek lookalikes (Α, Ε, …) lowercase to Greek\n // lowercase code points that aren't in the\n // homoglyph map, so the order matters.\n const last = tokens.at(-1)?.replace(/[.,;:!?]+$/u, \"\");\n const lastFolded = last\n ? normalizeHomoglyphs(last).toLowerCase()\n : undefined;\n if (\n tokens.length > 1 &&\n lastFolded &&\n PERSON_TRAILING_NOUNS.has(lastFolded)\n ) {\n continue;\n }\n }\n\n if (\n (normalized.label === \"person\" || normalized.label === \"organization\") &&\n roles.has(normalizeHomoglyphs(trimmed).toLowerCase())\n ) {\n continue;\n }\n\n if (\n normalized.label === \"organization\" &&\n normalized.source === \"legal-form\" &&\n trimmed === trimmed.toUpperCase() &&\n LEGAL_FORM_HEADING_RE.test(trimmed)\n ) {\n continue;\n }\n\n // Reject long address entities that look like prose:\n // no digits, no postal code, no known address\n // component (street abbreviations, etc.).\n if (\n normalized.label === \"address\" &&\n trimmed.length > 40 &&\n !POSTAL_CODE_RE.test(trimmed) &&\n !HAS_DIGIT_RE.test(trimmed) &&\n !hasAddressComponent(trimmed) &&\n !JURISDICTION_RE.test(trimmed)\n ) {\n continue;\n }\n\n // Reject ANY trigger-sourced address without digits\n // and without a known street-type word. Catches\n // non-address text like \"Nejsme plátci DPH !\".\n // Exempt jurisdiction patterns (\"State of ...\",\n // \"Commonwealth of ...\") which are valid addresses\n // without digits. The street-type fallback covers\n // non-Czech vocabulary loaded from JSON, so e.g.\n // Italian `Via Roma` is preserved.\n //\n // Special case: French \"cours\" is highly ambiguous\n // (\"au cours du contrat\" vs. \"Cours Mirabeau\"). When\n // bare `cours` is the only street-type token and the\n // entity has no digits, require it to look like a\n // proper-name address (capitalized token following).\n if (\n normalized.label === \"address\" &&\n normalized.source === \"trigger\" &&\n !HAS_DIGIT_RE.test(trimmed) &&\n !hasAddressComponent(trimmed) &&\n !JURISDICTION_RE.test(trimmed)\n ) {\n continue;\n }\n if (\n normalized.label === \"address\" &&\n normalized.source === \"trigger\" &&\n !HAS_DIGIT_RE.test(trimmed) &&\n isOnlyAmbiguousCours(trimmed)\n ) {\n continue;\n }\n\n if (\n normalized.label === \"address\" &&\n SIGNING_CLAUSE_ADDRESS_RE.test(trimmed)\n ) {\n continue;\n }\n\n filtered.push(normalized);\n }\n\n return filtered;\n};\n","import type { Match } from \"@stll/text-search\";\n\nimport {\n expandNameDeclensions,\n getNameCorpusFirstNames,\n getNameCorpusSurnames,\n getNameCorpusTitles,\n initNameCorpus,\n} from \"./names\";\nimport { resolveCountries } from \"../regions\";\nimport { DETECTION_SOURCES } from \"../types\";\nimport type {\n Dictionaries,\n DictionaryMeta,\n Entity,\n PipelineConfig,\n} from \"../types\";\nimport type { PipelineContext } from \"../context\";\nimport { defaultContext } from \"../context\";\nimport { loadGenericRoles } from \"../filters/false-positives\";\nimport { normalizeForSearch } from \"../util/normalize\";\nimport { ALL_UPPER_RE, UPPER_START_RE } from \"../util/text\";\nimport { DASH } from \"../util/char-groups\";\n\nexport type DenyListConfig = Pick<\n PipelineConfig,\n | \"enableDenyList\"\n | \"enableNameCorpus\"\n | \"nameCorpusLanguages\"\n | \"denyListCountries\"\n | \"denyListRegions\"\n | \"denyListExcludeCategories\"\n | \"customDenyList\"\n | \"dictionaries\"\n | \"enableCountries\"\n>;\n\n// ── Allow list (lazy-loaded from JSON) ───────────────\n\nconst loadAllowList = (ctx: PipelineContext): Promise<ReadonlySet<string>> => {\n if (ctx.allowListPromise) return ctx.allowListPromise;\n ctx.allowListPromise = (async () => {\n try {\n const mod: {\n default?: { words?: string[] };\n } = await import(\"../data/allow-list.json\");\n const set: ReadonlySet<string> = new Set(mod.default?.words ?? []);\n ctx.allowList = set;\n return set;\n } catch {\n const empty: ReadonlySet<string> = new Set();\n ctx.allowList = empty;\n return empty;\n }\n })();\n return ctx.allowListPromise;\n};\n\nconst EMPTY_ALLOW_LIST: ReadonlySet<string> = new Set();\n\n/** Sync accessor — returns empty set before init. */\nconst getAllowList = (ctx: PipelineContext): ReadonlySet<string> =>\n ctx.allowList ?? EMPTY_ALLOW_LIST;\n\nlet commonWordsPromise: Promise<ReadonlySet<string>> | null = null;\nlet commonWordsCache: ReadonlySet<string> | null = null;\n\nconst EMPTY_COMMON_WORDS: ReadonlySet<string> = new Set();\n\n/** Sync accessor — returns empty set before init. */\nconst getCommonWords = (): ReadonlySet<string> =>\n commonWordsCache ?? EMPTY_COMMON_WORDS;\n\nconst loadCommonWords = (): Promise<ReadonlySet<string>> => {\n if (commonWordsCache) return Promise.resolve(commonWordsCache);\n if (commonWordsPromise) return commonWordsPromise;\n commonWordsPromise = (async () => {\n try {\n const mod: { default?: { words?: string[] } } =\n await import(\"../data/common-words-en.json\");\n const set: ReadonlySet<string> = new Set(\n (mod.default?.words ?? []).map((word) => word.toLowerCase()),\n );\n commonWordsCache = set;\n return set;\n } catch {\n const empty: ReadonlySet<string> = new Set();\n commonWordsCache = empty;\n return empty;\n }\n })();\n return commonWordsPromise;\n};\n\n/**\n * Curated dictionary entries that are pure dotted\n * single-letter acronyms (e.g. `S.C.`, `D.N.J.`, `C.E.C.`)\n * need targeted suffix guards. The AC search matches\n * case-insensitively on token boundaries where `.` is not\n * a word character, so `S.C.` can match inside `U.S.C.`.\n * Two-segment non-address aliases are too noisy and are\n * dropped at build time; longer official aliases stay\n * searchable and are only suppressed when the source text\n * shows they are the tail of a longer dotted token.\n * Caller-supplied custom entries are exempted.\n */\nconst DOTTED_ACRONYM_RE = /^(?=.{3,}$)\\p{L}(?:\\.\\p{L}){0,3}\\.?$/u;\n\nconst isCuratedNoiseAcronym = (normalized: string): boolean =>\n DOTTED_ACRONYM_RE.test(normalized);\n\nconst dottedAcronymSegmentCount = (normalized: string): number =>\n normalized.split(\".\").filter(Boolean).length;\n\nconst isShortCuratedNoiseAcronym = (normalized: string): boolean =>\n isCuratedNoiseAcronym(normalized) &&\n dottedAcronymSegmentCount(normalized) <= 2;\n\nconst isDottedAcronymSuffixCollision = (\n fullText: string,\n start: number,\n matchText: string,\n): boolean =>\n isCuratedNoiseAcronym(matchText) &&\n /[\\p{L}]\\.$/u.test(fullText.slice(Math.max(0, start - 2), start));\n\n/**\n * Common EU given names present in the stopwords-iso dataset\n * but absent from the first-name corpus. Without this\n * supplementary set, these names would pass through the\n * corpus-based filter and remain in the stopwords, silently\n * suppressing person detection.\n *\n * Sourced from EU member state birth registries (top-100\n * names) cross-referenced with stopwords.json.\n */\nconst SUPPLEMENTARY_NAME_EXCLUSIONS: ReadonlySet<string> = new Set([\n \"ana\",\n \"ben\",\n \"dan\",\n \"eden\",\n \"ella\",\n \"ina\",\n \"jo\",\n \"kai\",\n \"lena\",\n \"may\",\n \"mia\",\n \"sam\",\n \"sara\",\n \"sue\",\n \"tim\",\n \"tom\",\n]);\n\n/**\n * Names from the first-name corpus (lowercased) that also\n * appear in the stopwords-iso dataset, plus supplementary\n * common EU given names not in the corpus. These must be\n * kept out of global STOPWORDS so that person detection is\n * not silently suppressed for real given names.\n *\n * Computed lazily after initNameCorpus() has populated\n * the first-name corpus. Re-builds if corpus size changes.\n */\nconst getFirstNameExclusions = (ctx: PipelineContext): ReadonlySet<string> => {\n const corpus = getNameCorpusFirstNames(ctx);\n // Re-build if corpus has been populated since last call\n if (\n ctx.firstNameExclusions &&\n corpus.length === ctx.firstNameExclusionCorpusLen\n ) {\n return ctx.firstNameExclusions;\n }\n ctx.firstNameExclusionCorpusLen = corpus.length;\n const set: ReadonlySet<string> = new Set([\n ...corpus.map((n) => n.toLowerCase()),\n ...SUPPLEMENTARY_NAME_EXCLUSIONS,\n ]);\n ctx.firstNameExclusions = set;\n return set;\n};\n\n/**\n * Global stopwords: common words across 23 EU languages\n * sourced from the stopwords-iso dataset (MIT license).\n * Checked case-insensitively against matches.\n *\n * Entries that collide with the first-name corpus are\n * excluded so they can still be detected as person names.\n *\n * Regenerate: bun packages/data/scripts/generate-stopwords.ts\n */\n\n// INVARIANT: must be called after initNameCorpus() has\n// resolved, so getFirstNameExclusions() sees the full\n// corpus. buildDenyList() enforces this ordering.\nconst loadStopwords = (ctx: PipelineContext): Promise<ReadonlySet<string>> => {\n if (ctx.stopwordsPromise) return ctx.stopwordsPromise;\n ctx.stopwordsPromise = (async () => {\n try {\n const mod: { default?: string[] } =\n await import(\"../data/stopwords.json\");\n const list = (mod.default ?? []).filter(\n (w: string) => !getFirstNameExclusions(ctx).has(w),\n );\n const set: ReadonlySet<string> = new Set(list);\n ctx.stopwords = set;\n return set;\n } catch (err) {\n console.warn(\n \"[anonymize] Failed to load stopwords.json\" +\n \" — stopword filtering disabled:\",\n err,\n );\n const empty: ReadonlySet<string> = new Set();\n ctx.stopwords = empty;\n return empty;\n }\n })();\n return ctx.stopwordsPromise;\n};\n\nconst EMPTY_STOPWORDS: ReadonlySet<string> = new Set();\n\n/** Sync accessor — returns empty set before init. */\nconst getStopwords = (ctx: PipelineContext): ReadonlySet<string> =>\n ctx.stopwords ?? EMPTY_STOPWORDS;\n\n// ── Person stopwords (lazy-loaded from JSON) ─────────\n\nconst loadPersonStopwords = (\n ctx: PipelineContext,\n): Promise<ReadonlySet<string>> => {\n if (ctx.personStopwordsPromise) {\n return ctx.personStopwordsPromise;\n }\n ctx.personStopwordsPromise = (async () => {\n try {\n const mod: {\n default?: { words?: string[] };\n } = await import(\"../data/person-stopwords.json\");\n const set: ReadonlySet<string> = new Set(mod.default?.words ?? []);\n ctx.personStopwords = set;\n return set;\n } catch {\n const empty: ReadonlySet<string> = new Set();\n ctx.personStopwords = empty;\n return empty;\n }\n })();\n return ctx.personStopwordsPromise;\n};\n\nconst EMPTY_PERSON_STOPWORDS: ReadonlySet<string> = new Set();\n\n/** Sync accessor — returns empty set before init. */\nexport const getPersonStopwords = (ctx: PipelineContext): ReadonlySet<string> =>\n ctx.personStopwords ?? EMPTY_PERSON_STOPWORDS;\n\n// ── Address stopwords (single-token city collisions) ──\n\nconst loadAddressStopwords = (\n ctx: PipelineContext,\n): Promise<ReadonlySet<string>> => {\n if (ctx.addressStopwordsPromise) {\n return ctx.addressStopwordsPromise;\n }\n ctx.addressStopwordsPromise = (async () => {\n try {\n const mod: { default?: { words?: string[] } } =\n await import(\"../data/address-stopwords.json\");\n const set: ReadonlySet<string> = new Set(mod.default?.words ?? []);\n ctx.addressStopwords = set;\n return set;\n } catch {\n const empty: ReadonlySet<string> = new Set();\n ctx.addressStopwords = empty;\n return empty;\n }\n })();\n return ctx.addressStopwordsPromise;\n};\n\nconst EMPTY_ADDRESS_STOPWORDS: ReadonlySet<string> = new Set();\n\nconst getAddressStopwords = (ctx: PipelineContext): ReadonlySet<string> =>\n ctx.addressStopwords ?? EMPTY_ADDRESS_STOPWORDS;\n\n/**\n * Word characters in unicode property notation. The check is\n * \"single-token\" — no internal whitespace — and we keep dashes\n * tokens like \"K-12\" out by requiring the surface to be a single\n * uninterrupted run of letters or marks.\n */\nconst SINGLE_WORD_RE = /^\\p{L}+$/u;\n\n/**\n * Format-level address signals — structurally numeric or\n * 2-letter-state patterns, language-agnostic. The street-type\n * vocabulary is loaded from `address-street-types.json` so new\n * languages contribute via data, not code.\n * - `,\\s*[A-Z]{2}\\b` → US state abbreviation after a comma\n * - `\\b\\d{5}(?:-\\d{4})?\\b` → US ZIP / ZIP+4\n * - `\\b\\d{3}\\s\\d{2}\\b` → Czech/Slovak postal block (140 00)\n * - `\\b\\d{2}-\\d{3}\\b` → Polish postal code (00-950)\n */\nconst ADDRESS_FORMAT_RE =\n /,\\s*\\p{Lu}{2}\\b|\\b\\d{5}(?:-\\d{4})?\\b|\\b\\d{3}\\s\\d{2}\\b|\\b\\d{2}-\\d{3}\\b/u;\n\nlet cachedStreetTypeRe: RegExp | null = null;\nlet streetTypeReLoaded = false;\n\nconst loadStreetTypeRe = async (): Promise<RegExp | null> => {\n if (streetTypeReLoaded) return cachedStreetTypeRe;\n try {\n const mod: { default?: Record<string, unknown> } =\n await import(\"../data/address-street-types.json\");\n const config = mod.default ?? {};\n const words: string[] = [];\n for (const value of Object.values(config)) {\n if (!Array.isArray(value)) continue;\n for (const word of value) {\n if (typeof word === \"string\" && word.length > 0) words.push(word);\n }\n }\n if (words.length === 0) {\n cachedStreetTypeRe = null;\n } else {\n words.sort((a, b) => b.length - a.length);\n const isLetterDigit = (c: string): boolean => /[\\p{L}\\p{N}]/u.test(c);\n const wordLikeTail: string[] = [];\n const punctTail: string[] = [];\n for (const w of words) {\n const escaped = w.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const last = w.at(-1) ?? \"\";\n // Entries ending in a letter/digit need the trailing\n // negative lookahead to enforce a word boundary\n // (`Avenue` should not match inside `Avenues`). Entries\n // ending in punctuation like `Av.` or `C/` cannot\n // continue into another letter/digit and a trailing\n // lookahead would actually exclude valid forms such as\n // `C/Mayor` (no space).\n if (isLetterDigit(last)) {\n wordLikeTail.push(escaped);\n } else {\n punctTail.push(escaped);\n }\n }\n const branches: string[] = [];\n if (wordLikeTail.length > 0) {\n branches.push(`(?:${wordLikeTail.join(\"|\")})(?![\\\\p{L}\\\\p{N}])`);\n }\n if (punctTail.length > 0) {\n branches.push(`(?:${punctTail.join(\"|\")})`);\n }\n // Unicode-class lookarounds rather than `\\b` so entries\n // ending in `.` or `/` (Spanish `Av.`, `C/`) still match\n // when followed by whitespace OR a letter (`C/Mayor`);\n // `\\b` does not fire after non-word characters and a\n // uniform trailing lookahead would block the no-space\n // form.\n cachedStreetTypeRe = new RegExp(\n `(?<![\\\\p{L}\\\\p{N}])(?:${branches.join(\"|\")})`,\n \"iu\",\n );\n }\n } catch {\n cachedStreetTypeRe = null;\n }\n streetTypeReLoaded = true;\n return cachedStreetTypeRe;\n};\n\nconst getStreetTypeRe = (): RegExp | null =>\n streetTypeReLoaded ? cachedStreetTypeRe : null;\n\nconst hasAdjacentAddressEvidence = (\n fullText: string,\n start: number,\n end: number,\n): boolean => {\n const window = fullText.slice(\n Math.max(0, start - 40),\n Math.min(fullText.length, end + 40),\n );\n if (ADDRESS_FORMAT_RE.test(window)) return true;\n const streetRe = getStreetTypeRe();\n return streetRe !== null && streetRe.test(window);\n};\n\n/**\n * Capitalised words that almost never start a person name. When a\n * single-token surname candidate is immediately followed by one of\n * these, the \"next-word is uppercase\" promotion heuristic would\n * otherwise turn section headings (\"Purchase Price↵The Purchaser\n * undertakes…\") into spurious person hits. Kept narrow on purpose;\n * the surrounding pipeline still chains real names via the deny-list\n * cascade when both halves are surnames.\n */\nconst SENTENCE_STARTER_WORDS: ReadonlySet<string> = new Set([\n \"The\",\n \"This\",\n \"These\",\n \"Those\",\n \"An\",\n \"Any\",\n \"All\",\n \"Each\",\n \"Every\",\n \"No\",\n \"Now\",\n \"Whereas\",\n \"Whereby\",\n \"Wherein\",\n \"Whereof\",\n \"Notwithstanding\",\n \"Subject\",\n \"In\",\n \"On\",\n \"At\",\n \"By\",\n \"For\",\n \"If\",\n \"Upon\",\n \"Unless\",\n \"Until\",\n \"Provided\",\n \"Pursuant\",\n \"Such\",\n]);\n\nconst PERSON_CHAIN_BREAK_RE = /[!?;:]|,/u;\nconst WORD_CHAR_RE = /[\\p{L}\\p{N}]/u;\nconst CURATED_PATTERN_SYNTAX_RE = /[|\\\\]/g;\n\nconst stripCuratedPatternSyntax = (value: string): string =>\n value.includes(\"|\") || value.includes(\"\\\\\")\n ? value.replace(CURATED_PATTERN_SYNTAX_RE, \"\")\n : value;\n\nconst isInitialContinuationGap = (text: string, gap: string): boolean =>\n (/^\\p{Lu}$/u.test(text) && /^\\.[^\\S\\n]{1,2}$/u.test(gap)) ||\n /^[^\\S\\n]{1,2}(?:\\p{Lu}\\.[^\\S\\n]{1,2})+$/u.test(gap);\n\n/**\n * Source tag for each pattern in the automaton.\n * \"deny-list\" = standard deny list entry\n * \"city\" = city dictionary entry\n * \"custom-deny-list\" = caller-owned exact term\n * \"first-name\" = name corpus first name\n * \"surname\" = name corpus surname\n * \"title\" = academic/professional title\n */\ntype PatternSource =\n | \"deny-list\"\n | \"city\"\n | \"custom-deny-list\"\n | \"first-name\"\n | \"surname\"\n | \"title\";\n\ntype PatternLabels = string | string[];\ntype PatternSources = PatternSource | PatternSource[];\n\nconst EMPTY_PATTERN_LABELS: readonly string[] = [];\nconst EMPTY_PATTERN_SOURCES: readonly PatternSource[] = [];\n\nconst patternLabels = (\n labels: PatternLabels | undefined,\n): readonly string[] => {\n if (labels === undefined) {\n return EMPTY_PATTERN_LABELS;\n }\n return Array.isArray(labels) ? labels : [labels];\n};\n\nconst patternSources = (\n sources: PatternSources | undefined,\n): readonly PatternSource[] => {\n if (sources === undefined) {\n return EMPTY_PATTERN_SOURCES;\n }\n return Array.isArray(sources) ? sources : [sources];\n};\n\nconst hasPersonNameSource = (match: RawMatch): boolean =>\n match.sources.includes(\"first-name\") || match.sources.includes(\"surname\");\n\nconst addPatternLabel = (\n list: PatternLabels[],\n index: number,\n label: string,\n): void => {\n const existing = list[index];\n if (existing === undefined) {\n list[index] = label;\n return;\n }\n if (Array.isArray(existing)) {\n if (!existing.includes(label)) {\n existing.push(label);\n }\n return;\n }\n if (existing !== label) {\n list[index] = [existing, label];\n }\n};\n\nconst addPatternSource = (\n list: PatternSources[],\n index: number,\n source: PatternSource,\n): void => {\n const existing = list[index];\n if (existing === undefined) {\n list[index] = source;\n return;\n }\n if (Array.isArray(existing)) {\n if (!existing.includes(source)) {\n existing.push(source);\n }\n return;\n }\n if (existing !== source) {\n list[index] = [existing, source];\n }\n};\n\nconst addOptionalPatternLabel = (\n list: (PatternLabels | undefined)[],\n index: number,\n label: string,\n): void => {\n const existing = list[index];\n if (existing === undefined) {\n list[index] = label;\n return;\n }\n if (Array.isArray(existing)) {\n if (!existing.includes(label)) {\n existing.push(label);\n }\n return;\n }\n if (existing !== label) {\n list[index] = [existing, label];\n }\n};\n\n/**\n * Pre-built deny list data. Constructed once by\n * `buildDenyList`, reused across `processDenyListMatches`\n * calls. Contains PatternEntry[] for the unified builder\n * plus parallel label/source arrays for post-processing.\n */\nexport type DenyListData = {\n /**\n * Maps pattern index → entity labels (plural).\n * Same pattern can have multiple labels when it\n * appears in multiple dictionaries (e.g., \"Denver\"\n * is both a person name and a city name).\n */\n labels: PatternLabels[];\n /** Maps pattern index → labels contributed by custom entries. */\n customLabels: (PatternLabels | undefined)[];\n /** Maps pattern index → original pattern text. */\n originals: string[];\n /** Maps pattern index → source types (plural). */\n sources: PatternSources[];\n};\n\nconst getCityEntries = (\n dictionaries: Dictionaries | undefined,\n allowedCountries: ReadonlySet<string> | null,\n): readonly string[] => {\n const byCountry = dictionaries?.citiesByCountry;\n if (!byCountry) {\n return dictionaries?.cities ?? [];\n }\n\n const result: string[] = [];\n const append = (entries: readonly string[] | undefined) => {\n if (!entries) {\n return;\n }\n for (const entry of entries) {\n result.push(entry);\n }\n };\n\n if (allowedCountries === null) {\n for (const entries of Object.values(byCountry)) {\n append(entries);\n }\n return result;\n }\n\n for (const country of allowedCountries) {\n append(byCountry[country.toUpperCase()]);\n }\n\n return result;\n};\n\n/**\n * Resolve which dictionaries to load based on country\n * and category filters, then build the deny list data.\n * The returned data provides PatternEntry[] for the\n * unified builder and parallel arrays for\n * post-processing.\n *\n * Dictionary data is injected via `config.dictionaries`.\n * Returns null if no dictionaries are provided.\n */\nexport const buildDenyList = async (\n config: DenyListConfig,\n ctx: PipelineContext = defaultContext,\n): Promise<DenyListData | null> => {\n // Pre-load name corpus so getNameCorpus*() accessors\n // and getFirstNameExclusions() are populated before\n // stopwords filtering runs.\n await initNameCorpus(ctx, config.dictionaries, config.nameCorpusLanguages);\n // Pre-load all JSON data so sync accessors are\n // populated before processDenyListMatches runs.\n await Promise.all([\n loadStopwords(ctx),\n loadAllowList(ctx),\n loadPersonStopwords(ctx),\n loadAddressStopwords(ctx),\n loadCommonWords(),\n loadStreetTypeRe(),\n loadGenericRoles(ctx),\n ]);\n const commonWords = await loadCommonWords();\n\n const dictionaries = config.dictionaries;\n const hasDenyList = dictionaries?.denyList && dictionaries?.denyListMeta;\n const hasCustomDenyList =\n config.customDenyList !== undefined && config.customDenyList.length > 0;\n const allowedCountries = resolveCountries(\n config.denyListRegions,\n config.denyListCountries,\n );\n const cityEntries = getCityEntries(dictionaries, allowedCountries);\n const hasCities = cityEntries.length > 0;\n\n // No dictionary data available — skip deny-list building\n if (!hasDenyList && !hasCities && !hasCustomDenyList) {\n // Still build name corpus entries if available\n return buildNameCorpusOnly(config, ctx);\n }\n\n const excluded = config.denyListExcludeCategories;\n const excludeCategories = excluded ? new Set(excluded) : new Set<string>();\n\n const patternList: string[] = [];\n const labelList: PatternLabels[] = [];\n const customLabelList: (PatternLabels | undefined)[] = [];\n const sourceList: PatternSources[] = [];\n // Maps lowercased pattern → index in patternList\n // for accumulating labels from multiple dictionaries\n const patternIndex = new Map<string, number>();\n\n const addDenyListEntry = (\n entry: string,\n label: string,\n source: PatternSource = \"deny-list\",\n ) => {\n // Strip | and \\ only for curated data — these caused the 12K FP\n // bug (| creates empty regex alternation, \\ is\n // an escape prefix). Caller-owned custom terms stay exact.\n const normalized =\n source === \"custom-deny-list\"\n ? normalizeForSearch(entry)\n : stripCuratedPatternSyntax(normalizeForSearch(entry));\n if (normalized.length === 0) {\n return;\n }\n const lower = normalized.toLowerCase();\n if (source !== \"custom-deny-list\" && label !== \"address\") {\n if (SINGLE_WORD_RE.test(normalized) && commonWords.has(lower)) {\n return;\n }\n if (isShortCuratedNoiseAcronym(normalized)) {\n return;\n }\n }\n const existing = patternIndex.get(lower);\n if (existing !== undefined) {\n addPatternLabel(labelList, existing, label);\n addPatternSource(sourceList, existing, source);\n if (\n source === \"custom-deny-list\" &&\n !patternLabels(customLabelList[existing]).includes(label)\n ) {\n addOptionalPatternLabel(customLabelList, existing, label);\n }\n } else {\n patternIndex.set(lower, patternList.length);\n patternList.push(normalized);\n labelList.push(label);\n if (source === \"custom-deny-list\") {\n customLabelList[patternList.length - 1] = label;\n }\n sourceList.push(source);\n }\n };\n\n // Load dictionaries from injected data\n if (hasDenyList) {\n const denyListData = dictionaries.denyList!;\n const metaData = dictionaries.denyListMeta!;\n const useScopedNameCorpus = config.nameCorpusLanguages !== undefined;\n\n for (const [id, entries] of Object.entries(denyListData)) {\n const meta: DictionaryMeta | undefined = metaData[id];\n if (!meta) {\n continue;\n }\n\n if (!config.enableNameCorpus && meta.category === \"Names\") {\n continue;\n }\n\n if (useScopedNameCorpus && meta.category === \"Names\") {\n continue;\n }\n\n if (excludeCategories.has(meta.category)) {\n continue;\n }\n\n // enableCountries: false must zero-out country redaction\n // across both the new country detector and the legacy\n // `countries/translations` dictionary, which ships Czech\n // declensions (\"České republiky\", \"Slovenskou republikou\",\n // …) the CLDR canonicals don't carry. Without this gate,\n // toggling the flag would silently keep the legacy path\n // active.\n if (meta.label === \"country\" && config.enableCountries === false) {\n continue;\n }\n\n if (allowedCountries !== null && meta.country !== null) {\n if (!allowedCountries.has(meta.country)) {\n continue;\n }\n }\n\n for (const entry of entries) {\n addDenyListEntry(entry, meta.label);\n }\n }\n }\n\n // Add pre-loaded city entries\n if (hasCities && !excludeCategories.has(\"Places\")) {\n for (const entry of cityEntries) {\n addDenyListEntry(entry, \"address\", \"city\");\n }\n }\n\n if (hasCustomDenyList) {\n for (const entry of config.customDenyList!) {\n addDenyListEntry(entry.value, entry.label, \"custom-deny-list\");\n for (const variant of entry.variants ?? []) {\n addDenyListEntry(variant, entry.label, \"custom-deny-list\");\n }\n }\n }\n\n // Add name corpus entries — accumulate labels\n // for entries that already exist from deny-list.\n appendNameCorpusEntries(\n config,\n ctx,\n patternList,\n labelList,\n sourceList,\n patternIndex,\n );\n\n if (patternList.length === 0) {\n return null;\n }\n\n return {\n labels: labelList,\n customLabels: customLabelList,\n originals: patternList,\n sources: sourceList,\n };\n};\n\n/**\n * Build deny-list data containing only name corpus\n * entries (no external dictionaries). Used when no\n * dictionary data is injected but name corpus is\n * enabled.\n */\nconst buildNameCorpusOnly = (\n config: DenyListConfig,\n ctx: PipelineContext,\n): DenyListData | null => {\n if (!config.enableNameCorpus) {\n return null;\n }\n\n const excluded = config.denyListExcludeCategories;\n const excludeCategories = excluded ? new Set(excluded) : new Set<string>();\n if (excludeCategories.has(\"Names\")) {\n return null;\n }\n\n const patternList: string[] = [];\n const labelList: PatternLabels[] = [];\n const customLabelList: (PatternLabels | undefined)[] = [];\n const sourceList: PatternSources[] = [];\n const patternIndex = new Map<string, number>();\n\n appendNameCorpusEntries(\n config,\n ctx,\n patternList,\n labelList,\n sourceList,\n patternIndex,\n );\n\n if (patternList.length === 0) {\n return null;\n }\n\n return {\n labels: labelList,\n customLabels: customLabelList,\n originals: patternList,\n sources: sourceList,\n };\n};\n\n/**\n * Append name corpus entries (first names, surnames,\n * titles) to the pattern arrays. Shared between\n * buildDenyList and buildNameCorpusOnly.\n */\nconst appendNameCorpusEntries = (\n config: DenyListConfig,\n ctx: PipelineContext,\n patternList: string[],\n labelList: PatternLabels[],\n sourceList: PatternSources[],\n patternIndex: Map<string, number>,\n): void => {\n const excluded = config.denyListExcludeCategories;\n const excludeCategories = excluded ? new Set(excluded) : new Set<string>();\n\n if (!config.enableNameCorpus || excludeCategories.has(\"Names\")) {\n return;\n }\n\n const addNameEntry = (name: string, source: PatternSource) => {\n // Normalize same as deny-list entries so name\n // patterns match against normalizeForSearch(text).\n const normalized = stripCuratedPatternSyntax(normalizeForSearch(name));\n if (normalized.length === 0) {\n return;\n }\n if (isCuratedNoiseAcronym(normalized)) {\n return;\n }\n const lower = normalized.toLowerCase();\n const existing = patternIndex.get(lower);\n if (existing !== undefined) {\n addPatternLabel(labelList, existing, \"person\");\n addPatternSource(sourceList, existing, source);\n } else {\n patternIndex.set(lower, patternList.length);\n patternList.push(normalized);\n labelList.push(\"person\");\n sourceList.push(source);\n }\n };\n\n // Inject declined Czech/Slovak variants alongside the\n // nominative so the whole-word AC search matches names\n // in running declined text (\"s Janem Novákem\"). Variant\n // generation is gated by ending shape, which bounds the\n // pattern-count growth. Variants that collide with\n // common English words are dropped, mirroring the\n // surname curation in initNameCorpus.\n const commonWords = getCommonWords();\n const addDeclinedVariants = (name: string, source: PatternSource) => {\n for (const variant of expandNameDeclensions(name)) {\n if (commonWords.has(variant.toLowerCase())) {\n continue;\n }\n addNameEntry(variant, source);\n }\n };\n for (const name of getNameCorpusFirstNames(ctx)) {\n addNameEntry(name, \"first-name\");\n addDeclinedVariants(name, \"first-name\");\n }\n for (const name of getNameCorpusSurnames(ctx)) {\n addNameEntry(name, \"surname\");\n addDeclinedVariants(name, \"surname\");\n }\n for (const title of getNameCorpusTitles(ctx)) {\n const norm = stripCuratedPatternSyntax(normalizeForSearch(title));\n if (norm.length === 0) continue;\n const lower = norm.toLowerCase();\n const existing = patternIndex.get(lower);\n if (existing !== undefined) {\n addPatternSource(sourceList, existing, \"title\");\n } else {\n patternIndex.set(lower, patternList.length);\n patternList.push(norm);\n labelList.push(\"person\");\n sourceList.push(\"title\");\n }\n }\n};\n\ntype RawMatch = {\n start: number;\n end: number;\n /** All labels for this pattern (e.g., [\"person\", \"address\"]). */\n labels: readonly string[];\n customLabels: readonly string[];\n sources: readonly PatternSource[];\n text: string;\n patternIdx: number;\n};\n\nconst customMatchHasValidEdges = (\n fullText: string,\n start: number,\n end: number,\n pattern: string,\n): boolean => {\n if (!WORD_CHAR_RE.test(pattern)) {\n return true;\n }\n const prev = fullText[start - 1] ?? \"\";\n const next = fullText[end] ?? \"\";\n if (WORD_CHAR_RE.test(prev)) {\n return false;\n }\n if (WORD_CHAR_RE.test(next)) {\n return false;\n }\n return true;\n};\n\n/**\n * Ensure all deny-list support data (stopwords, allow\n * list, person stopwords, generic roles) is loaded on\n * the given context. Call this before\n * processDenyListMatches / filterFalsePositives when\n * the search instance was built on a different context\n * (e.g. cachedSearch).\n */\nexport const ensureDenyListData = async (\n ctx: PipelineContext = defaultContext,\n dictionaries?: Dictionaries,\n nameCorpusLanguages?: readonly string[],\n): Promise<void> => {\n // INVARIANT: initNameCorpus must resolve before\n // loadStopwords so first-name exclusions are\n // available when computing the stopword set.\n await initNameCorpus(ctx, dictionaries, nameCorpusLanguages);\n await Promise.all([\n loadStopwords(ctx),\n loadAllowList(ctx),\n loadPersonStopwords(ctx),\n loadAddressStopwords(ctx),\n loadStreetTypeRe(),\n loadGenericRoles(ctx),\n ]);\n};\n\n// ── Match processor ─────────────────────────────────\n\n/**\n * Process deny list matches from the unified search.\n * Receives all matches; filters to the deny list slice\n * via sliceStart/sliceEnd. Local index into data.labels,\n * data.originals, data.sources is match.pattern - sliceStart.\n *\n * Two-pass approach to reduce false positives:\n * 1. Collect all matches (case-insensitive,\n * whole-word via Rust automaton)\n * 2. Require uppercase start in source text\n * 3. For person names, require at least one\n * mid-sentence occurrence to prove proper noun\n * 4. Return all occurrences of validated terms\n */\nexport const processDenyListMatches = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText: string,\n data: DenyListData,\n ctx: PipelineContext = defaultContext,\n): Entity[] => {\n // Pass 1: collect valid matches grouped by pattern\n const matchesByPattern = new Map<number, RawMatch[]>();\n\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) {\n continue;\n }\n\n const localIdx = idx - sliceStart;\n const sources = patternSources(data.sources[localIdx]);\n\n // Use original text for display; normalized was\n // only for the AC search.\n const matchText = fullText.slice(match.start, match.end);\n const sourceChar = fullText[match.start] ?? \"\";\n const keyword = matchText.toLowerCase();\n\n const labels = patternLabels(data.labels[localIdx]);\n const pattern = data.originals[localIdx] ?? \"\";\n const customPatternLabels = patternLabels(data.customLabels[localIdx]);\n const customEdgesAreValid = customMatchHasValidEdges(\n fullText,\n match.start,\n match.end,\n pattern,\n );\n const customLabels = customEdgesAreValid ? customPatternLabels : [];\n if (labels.length === 0 && customLabels.length === 0) {\n continue;\n }\n\n // All-uppercase acronym patterns (\"OIL\", \"OP\", \"BIS\")\n // case-fold to common English words under the AC's\n // caseInsensitive flag and match mixed-case occurrences\n // (\"Oil\", \"Op\"). Require all-uppercase patterns to\n // match all-uppercase source text so acronym dictionary\n // entries cannot collide with everyday prose.\n const patternIsAcronym =\n pattern.length > 0 && pattern.length <= 5 && ALL_UPPER_RE.test(pattern);\n const acronymMatchesAcronym =\n !patternIsAcronym || ALL_UPPER_RE.test(matchText);\n\n const passesCuratedFilters =\n UPPER_START_RE.test(sourceChar) &&\n !getStopwords(ctx).has(keyword) &&\n !getAllowList(ctx).has(keyword) &&\n acronymMatchesAcronym &&\n !ALL_UPPER_RE.test(matchText);\n const curatedLabels = passesCuratedFilters\n ? labels.filter(\n (label) =>\n !customPatternLabels.includes(label) && customEdgesAreValid,\n )\n : [];\n const suffixCollision = isDottedAcronymSuffixCollision(\n fullText,\n match.start,\n matchText,\n );\n const filteredCuratedLabels = suffixCollision ? [] : curatedLabels;\n\n if (filteredCuratedLabels.length === 0 && customLabels.length === 0) {\n continue;\n }\n\n const entry: RawMatch = {\n start: match.start,\n end: match.end,\n labels: filteredCuratedLabels,\n customLabels,\n sources,\n text: matchText,\n patternIdx: localIdx,\n };\n\n const existing = matchesByPattern.get(localIdx);\n if (existing) {\n existing.push(entry);\n } else {\n matchesByPattern.set(localIdx, [entry]);\n }\n }\n\n // Pass 2: process all matches\n const results: Entity[] = [];\n const nameHits: RawMatch[] = [];\n\n for (const [, matches] of Array.from(matchesByPattern)) {\n const first = matches[0];\n if (!first) {\n continue;\n }\n\n for (const m of matches) {\n for (const label of m.customLabels) {\n results.push({\n start: m.start,\n end: m.end,\n label,\n text: m.text,\n score: 0.9,\n source: DETECTION_SOURCES.DENY_LIST,\n sourceDetail: \"custom-deny-list\",\n });\n }\n }\n\n // Curated labels are evaluated per match because\n // custom-only matches can share a pattern with\n // curated matches while failing curated FP filters.\n for (const m of matches) {\n if (m.labels.includes(\"person\")) {\n const keyword = m.text.toLowerCase();\n if (!getPersonStopwords(ctx).has(keyword)) {\n nameHits.push(m);\n }\n }\n\n const nonPersonLabels = m.labels.filter((l) => l !== \"person\");\n // Single-token city-dictionary collisions (Price, Union,\n // Brent, Time, …) are common English words that GeoNames\n // also knows as tiny villages. Drop them so \"Purchase\n // Price\" / \"European Union\" prose stops getting tagged\n // as an address — but only when there's no surrounding\n // address context. \"Union, WA 98592\" or \"Price, UT\" stay,\n // because a state abbreviation or ZIP nearby confirms the\n // city interpretation.\n const isStopwordSingleAddress =\n SINGLE_WORD_RE.test(m.text) &&\n getAddressStopwords(ctx).has(m.text.toLowerCase());\n const suppressAddress =\n isStopwordSingleAddress &&\n !hasAdjacentAddressEvidence(fullText, m.start, m.end);\n for (const label of nonPersonLabels) {\n if (label === \"address\" && suppressAddress) {\n continue;\n }\n results.push({\n start: m.start,\n end: m.end,\n label,\n text: m.text,\n score: 0.9,\n source: DETECTION_SOURCES.DENY_LIST,\n });\n }\n }\n }\n\n // Pass 2b: person hits — chain adjacent hits and\n // extend to following capitalised words.\n nameHits.sort((a, b) => a.start - b.start);\n\n const nameConsumed = new Set<number>();\n for (let i = 0; i < nameHits.length; i++) {\n if (nameConsumed.has(i)) {\n continue;\n }\n const hit = nameHits[i];\n if (!hit) {\n continue;\n }\n\n // Build chain of adjacent person hits\n const chain: RawMatch[] = [hit];\n let j = i + 1;\n\n while (j < nameHits.length && chain.length < 5) {\n const next = nameHits[j];\n if (!next) {\n break;\n }\n const prev = chain.at(-1);\n if (!prev) {\n break;\n }\n\n const gap = fullText.slice(prev.end, next.start);\n const breaksOnPeriod =\n gap.includes(\".\") && !isInitialContinuationGap(prev.text, gap);\n if (\n gap.length > 4 ||\n gap.length === 0 ||\n gap.includes(\"\\n\") ||\n gap.includes(\"\\t\") ||\n PERSON_CHAIN_BREAK_RE.test(gap) ||\n breaksOnPeriod\n ) {\n break;\n }\n\n chain.push(next);\n j++;\n }\n\n // Mark chain members consumed\n for (let k = i; k < i + chain.length; k++) {\n nameConsumed.add(k);\n }\n\n // Extend to following capitalised word (for\n // unknown surnames not in the corpus)\n const first = chain.at(0);\n const last = chain.at(-1);\n if (!first || !last) {\n continue;\n }\n if (!chain.some(hasPersonNameSource)) {\n continue;\n }\n\n // Skip the trailing-capitalised-word extension when the\n // chain sits inside a defined-term quote\n // (`\"Bond Hedge Transactions\"`, `\"Blue Sky Laws\"`).\n // Legal prose uses curly or straight quotes to introduce\n // capitalised noun phrases that are not personal names;\n // chaining beyond the name corpus inside that bracketed\n // context produces unstable spans like\n // `\"Bond Hedge Transactions\"`-as-person.\n const insideDefinedTermQuote = isSuppressibleDefinedTermQuote(\n fullText,\n first.start,\n ctx,\n );\n\n if (insideDefinedTermQuote) {\n continue;\n }\n\n const extended = extendPersonName(fullText, first.start, last.end, ctx);\n\n // Score: chained names get 0.9, single names 0.5\n const score = chain.length >= 2 ? 0.9 : 0.5;\n\n // Single-word deny-list matches are too noisy:\n // \"Rate\", \"Server\", \"Code\" etc. are surnames but\n // also common English words. Only accept single-\n // word matches when the next word is also uppercase\n // (likely a full name: \"Alena Zemanová\"). Skip\n // sentence-starter articles (\"The Purchaser…\")\n // which otherwise turn section headings like\n // \"Purchase Price↵The Purchaser…\" into person hits.\n if (chain.length === 1) {\n const afterEnd = last.end;\n const rest = fullText.slice(afterEnd).trimStart();\n // Require Cap + lowercase: filters out acronyms like\n // \"EU\", \"USA\" so \"Rady EU\" doesn't read as a name.\n const nextIsUpper = rest.length > 1 && /^\\p{Lu}\\p{Ll}/u.test(rest);\n if (!nextIsUpper) {\n continue;\n }\n // Reject sentence-starter articles (\"The Purchaser…\")\n // so section headings followed by a sentence don't\n // get promoted to person hits.\n const nextWord = /^\\p{L}+/u.exec(rest)?.[0] ?? \"\";\n if (SENTENCE_STARTER_WORDS.has(nextWord)) {\n continue;\n }\n }\n\n results.push({\n start: first.start,\n end: extended.end,\n label: \"person\",\n text: extended.text,\n score,\n source: DETECTION_SOURCES.DENY_LIST,\n });\n }\n\n // Post-process: extend city/address matches to\n // include adjacent trailing district numbers (e.g.,\n // \"Praha 1\", \"Brno 2\"). Czech and Slovak cities\n // commonly have numbered districts that are part of\n // the address.\n extendCityDistricts(results, fullText);\n\n return results;\n};\n\n/**\n * Extend address-label entities to absorb adjacent\n * integers: trailing district numbers (\"Praha 1\") and\n * leading postal codes (\"80336 München\").\n * Mutates the entities in place.\n */\n// District suffixes: digits (\"Praha 1\") or Roman\n// numerals (\"Příbram II\", \"Brno III\")\n// Valid Roman numerals only (I-XXX range, no invalid\n// combos like IC, LC, VC). Covers district suffixes\n// up to Praha XXX which is more than enough.\n// Roman numeral district suffixes II-XXX. Standalone\n// \"I\" and \"V\" excluded: \"V\" clashes with Czech\n// preposition \"in\"; \"I\" is too ambiguous.\nconst ROMAN_DISTRICT =\n \"XXX|XXIX|XXVIII|XXVII|XXVI|XXV|XXIV|XXIII\" +\n \"|XXII|XXI|XX|XIX|XVIII|XVII|XVI|XV|XIV|XIII\" +\n \"|XII|XI|X|IX|VIII|VII|VI|IV|III|II\";\nconst DISTRICT_SUFFIX_RE = new RegExp(\n `^ (\\\\d{1,2}(?!\\\\d)|(?:${ROMAN_DISTRICT}))` + `(?=[\\\\s,;.)\"\\\\n]|$)`,\n);\n// Postal code before city: \"163 00 \", \"16300 \",\n// \"16300 - \" (with dash separator).\nconst POSTAL_PREFIX_RE = new RegExp(\n `(?:\\\\d{5}|\\\\d{3}\\\\s\\\\d{2})\\\\s*${DASH}?\\\\s*$`,\n);\n\n// Words that must NOT be absorbed into an address span\n// when they follow a postal-code + city pattern. Party\n// roles, organizational nouns, and common legal terms.\nconst TRAILING_WORD_EXCLUSIONS: ReadonlySet<string> = new Set([\n // CZ/SK party roles\n \"nájemce\",\n \"pronajímatel\",\n \"kupující\",\n \"prodávající\",\n \"objednatel\",\n \"zhotovitel\",\n \"dodavatel\",\n \"odběratel\",\n \"věřitel\",\n \"dlužník\",\n \"zadavatel\",\n \"uchazeč\",\n \"příjemce\",\n \"plátce\",\n // Organizational nouns\n \"správa\",\n \"sekretariát\",\n \"kancelář\",\n \"odbor\",\n \"oddělení\",\n \"úřad\",\n \"inspekce\",\n \"agentura\",\n // Legal clause starters\n \"článek\",\n \"smlouva\",\n \"dodatek\",\n \"příloha\",\n \"předmět\",\n \"podmínky\",\n \"ustanovení\",\n]);\n\nconst extendCityDistricts = (entities: Entity[], fullText: string): void => {\n for (const entity of entities) {\n if (entity.label !== \"address\") {\n continue;\n }\n if (entity.sourceDetail === \"custom-deny-list\") {\n continue;\n }\n\n // Trailing: \"Praha\" + \" 1\" → \"Praha 1\"\n // Trailing: \"Praha\" + \" 1\" → \"Praha 1\"\n const afterMatch = fullText.slice(entity.end);\n const suffixM = DISTRICT_SUFFIX_RE.exec(afterMatch);\n if (suffixM) {\n entity.end += suffixM[0].length;\n entity.text = fullText.slice(entity.start, entity.end);\n }\n\n // Dash-separated district name:\n // \"Praha 10 - Strašnice\", \"Havířov – Město\"\n const afterDistrict = fullText.slice(entity.end);\n const dashDistrictM = /^[ \\t]{1,4}[-–][ \\t]*(\\p{Lu}\\p{Ll}+)/u.exec(\n afterDistrict,\n );\n if (dashDistrictM && !dashDistrictM[0].includes(\"\\n\")) {\n entity.end += dashDistrictM[0].length;\n entity.text = fullText.slice(entity.start, entity.end);\n }\n\n // Leading: \"80336 \" + \"München\" → \"80336 München\"\n // Absorbs 3-5 digit postal codes before the city.\n const beforeMatch = fullText.slice(\n Math.max(0, entity.start - 10),\n entity.start,\n );\n const prefixM = POSTAL_PREFIX_RE.exec(beforeMatch);\n if (prefixM) {\n entity.start -= prefixM[0].length;\n entity.text = fullText.slice(entity.start, entity.end);\n }\n\n // Trailing uppercase word: \"434 01\" + \" Most\" →\n // \"434 01 Most\". Absorb if the next word starts\n // with uppercase and is on the same line.\n // Guard: skip party-role or organizational terms.\n const afterExt = fullText.slice(entity.end);\n // Max 4 spaces gap — more means different column\n const trailingWordM = /^[\\s]{1,4}(\\p{Lu}\\p{Ll}+)/u.exec(afterExt);\n if (trailingWordM && !trailingWordM[0].includes(\"\\n\")) {\n const candidate = (trailingWordM[1] ?? \"\").toLowerCase();\n if (!TRAILING_WORD_EXCLUSIONS.has(candidate)) {\n entity.end += trailingWordM[0].length;\n entity.text = fullText.slice(entity.start, entity.end);\n }\n }\n }\n};\n\n/**\n * Extend a person name match to include subsequent\n * capitalized words. \"Pavel\" + \" Heřmánek\" → \"Pavel\n * Heřmánek\". Stops at lowercase words, punctuation,\n * or end of text. Also extends backward if preceded\n * by a capitalized word (for \"Miroslav Braňka\" when\n * only \"Braňka\" matched).\n */\n/**\n * Defined-term marker: an opening typographic or straight\n * quote enclosing the chain start, AND a closing quote\n * within a short window followed by a\n * definitional cue (`means`, `shall mean`, `shall have\n * the meaning(s)`, `refers to`). Legal documents reserve\n * this construction for defined terms; the contents are\n * not personal names even when individual tokens collide\n * with the name corpus.\n *\n * Plain quotations like `\"John Unknown\" said ...` do NOT\n * count: there is no definitional cue, so the trailing\n * surname extension is still allowed to absorb `Unknown`.\n */\nconst OPENING_QUOTES = new Set(['\"', \"'\", \"“\", \"„\", \"‟\", \"‘\", \"‛\", \"«\"]);\nconst CLOSING_QUOTES = new Set(['\"', \"'\", \"”\", \"’\", \"»\", \"“\"]);\nconst DEFINED_TERM_CUE_RE =\n /^[\\s,]*(?:means?|shall\\s+means?|shall\\s+have\\s+the\\s+meanings?|refers?\\s+to|has\\s+the\\s+meanings?|is\\s+defined)\\b/iu;\nconst DEFINED_TERM_LOOKAHEAD = 120;\nconst DEFINED_TERM_LOOKBEHIND = 80;\nconst EMPTY_GENERIC_ROLES: ReadonlySet<string> = new Set();\n\ntype DefinedTermQuote = {\n content: string;\n afterClosingQuote: string;\n};\n\nconst isLetter = (ch: string | undefined): boolean =>\n ch !== undefined && /^\\p{L}$/u.test(ch);\n\nconst isApostropheInsideWord = (text: string, index: number): boolean =>\n isLetter(text[index - 1]) && isLetter(text[index + 1]);\n\nconst isQuoteBoundary = (text: string, index: number): boolean => {\n const ch = text[index];\n if (ch !== \"'\" && ch !== \"’\") {\n return true;\n }\n return !isApostropheInsideWord(text, index);\n};\n\nconst findDefinedTermQuoteContent = (\n text: string,\n start: number,\n): DefinedTermQuote | null => {\n const min = Math.max(0, start - DEFINED_TERM_LOOKBEHIND);\n let quoteStart = -1;\n for (let i = start - 1; i >= min; i--) {\n const ch = text[i];\n if (ch === \"\\n\") {\n break;\n }\n if (ch && OPENING_QUOTES.has(ch) && isQuoteBoundary(text, i)) {\n quoteStart = i;\n break;\n }\n if (ch && CLOSING_QUOTES.has(ch) && isQuoteBoundary(text, i)) {\n break;\n }\n }\n if (quoteStart === -1) {\n return null;\n }\n\n const max = Math.min(text.length, quoteStart + 1 + DEFINED_TERM_LOOKAHEAD);\n for (let i = start; i < max; i++) {\n const ch = text[i];\n if (!ch || !CLOSING_QUOTES.has(ch) || !isQuoteBoundary(text, i)) {\n continue;\n }\n const after = text.slice(i + 1, max);\n if (!DEFINED_TERM_CUE_RE.test(after)) {\n return null;\n }\n return {\n content: text.slice(quoteStart + 1, i),\n afterClosingQuote: after,\n };\n }\n\n return null;\n};\n\nconst FIRST_WORD_RE = /^\\p{L}+/u;\nconst WORD_RE = /\\p{L}+/gu;\n\nconst startsWithKnownFirstName = (\n quoteContent: string,\n ctx: PipelineContext,\n): boolean => {\n const firstWord = FIRST_WORD_RE.exec(quoteContent.trim())?.[0];\n if (!firstWord) {\n return false;\n }\n const firstNames = new Set(\n getNameCorpusFirstNames(ctx).map((name) => name.toLowerCase()),\n );\n return firstNames.has(firstWord.toLowerCase());\n};\n\nconst hasPersonRoleDefinition = (\n afterClosingQuote: string,\n ctx: PipelineContext,\n): boolean => {\n const roleWords =\n afterClosingQuote\n .replace(DEFINED_TERM_CUE_RE, \"\")\n .match(WORD_RE)\n ?.slice(0, 8) ?? [];\n if (roleWords.length === 0) {\n return false;\n }\n\n const genericRoles = ctx.genericRoles ?? EMPTY_GENERIC_ROLES;\n return roleWords.some((word) => genericRoles.has(word.toLowerCase()));\n};\n\nconst isSuppressibleDefinedTermQuote = (\n text: string,\n start: number,\n ctx: PipelineContext,\n): boolean => {\n const definedTermQuote = findDefinedTermQuoteContent(text, start);\n if (definedTermQuote === null) {\n return false;\n }\n\n const words = definedTermQuote.content.match(WORD_RE) ?? [];\n\n // A quoted defined term can itself be a real person:\n // `\"John Smith\" shall mean the employee...`. Preserve those\n // when the definition itself points at a legal/business role\n // from dictionary data. Legal terms such as `\"Bond Hedge\"`\n // stay suppressible even if their first token collides with\n // a given-name corpus entry.\n if (\n words.length >= 2 &&\n startsWithKnownFirstName(definedTermQuote.content, ctx) &&\n hasPersonRoleDefinition(definedTermQuote.afterClosingQuote, ctx)\n ) {\n return false;\n }\n\n return words.length >= 2;\n};\n\nconst extendPersonName = (\n text: string,\n start: number,\n end: number,\n ctx: PipelineContext,\n): { end: number; text: string } => {\n let newEnd = end;\n\n // Extend forward: skip whitespace, check if next\n // word starts with uppercase\n let pos = newEnd;\n while (pos < text.length) {\n // Skip single whitespace\n if (pos < text.length && text[pos] === \" \") {\n const wordStart = pos + 1;\n if (wordStart >= text.length) {\n break;\n }\n\n const char = text[wordStart] ?? \"\";\n if (!UPPER_START_RE.test(char)) {\n break;\n }\n\n // Find end of this word\n let wordEnd = wordStart;\n while (wordEnd < text.length && !/\\s/.test(text[wordEnd] ?? \"\")) {\n wordEnd++;\n }\n\n // Skip trailing punctuation (commas, periods,\n // typographic closing quotes). Curly quotes survive\n // normalisation because they often appear inside\n // defined-term clauses (`\"Blue Sky Laws\"`); strip\n // them so the allow-list / stopword check sees the\n // bare word.\n const word = text.slice(wordStart, wordEnd);\n const stripped = word.replace(/[,;.”\"’'“»]+$/, \"\");\n if (stripped.length < 2) {\n break;\n }\n\n // Don't extend into stopwords or person stopwords.\n // The global allow list is intentionally NOT consulted\n // here: real surnames such as `Law`, `Tesla`, or\n // `Vote` are common English words and live on the\n // allow list to suppress single-token noise, but they\n // are legitimate name extensions when preceded by a\n // first name in plain prose (`John Law`, `Elon\n // Tesla`). Defined-term contexts (`\"Blue Sky Laws\"`,\n // `\"Bond Hedge Transactions\"`) are filtered earlier by\n // `isInsideDefinedTermQuote`, so by the time\n // `extendPersonName` runs we are in ordinary prose and\n // the allow-list block would only swallow real\n // surnames.\n const lower = stripped.toLowerCase();\n if (getStopwords(ctx).has(lower) || getPersonStopwords(ctx).has(lower)) {\n break;\n }\n\n newEnd = wordStart + stripped.length;\n pos = newEnd;\n } else {\n break;\n }\n }\n\n return {\n end: newEnd,\n text: text.slice(start, newEnd),\n };\n};\n","/**\n * Address detection via seed expansion.\n *\n * 1. Find \"address seeds\" — high-confidence address\n * components (postal codes, cities, street types)\n * 2. Cluster nearby seeds (within ~150 chars)\n * 3. Expand each cluster to the full address span\n * 4. Score by seed diversity (more types = higher)\n *\n * Language-agnostic: street type words and boundary\n * words are loaded from data dictionaries, not\n * hardcoded per language.\n */\n\nimport type { Match } from \"@stll/text-search\";\n\nimport { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\nimport {\n DASH,\n DASH_INNER,\n OPENING_BRACKETS_INNER,\n QUOTE_DOUBLE_INNER,\n QUOTE_SINGLE_INNER,\n} from \"../util/char-groups\";\n\n/**\n * Trailing chars that should never end an address span. Combines\n * structural separators, opening brackets, and typographic quote\n * variants so a span like `… GA 30326, USA (the \"Premises\")`\n * does not pull the parenthetical opener into the address.\n * The prime (`′`) catches measurement tails (`5′`) that the\n * cluster occasionally absorbs.\n */\nconst ADDRESS_TRAILING_TRIM_RE = new RegExp(\n `[,;:\\\\s${OPENING_BRACKETS_INNER}${QUOTE_DOUBLE_INNER}${QUOTE_SINGLE_INNER}′]`,\n \"u\",\n);\nconst POSTAL_ADJACENT = `\\\\p{L}\\\\p{N}_${DASH_INNER}`;\nconst POSTAL_CODE_RE = new RegExp(\n `(?<![${POSTAL_ADJACENT}])` +\n `(?:\\\\d{3}\\\\s\\\\d{2}|\\\\d{2}${DASH}\\\\d{3}|\\\\d{5}${DASH}\\\\d{3}|\\\\d{5}${DASH}\\\\d{4})` +\n `(?![${POSTAL_ADJACENT}])`,\n \"gu\",\n);\nconst BR_CEP_SHAPE_RE = new RegExp(`^\\\\d{5}${DASH}\\\\d{3}$`, \"u\");\nconst US_ZIP_PLUS_FOUR_SHAPE_RE = new RegExp(`^\\\\d{5}${DASH}\\\\d{4}$`, \"u\");\nconst US_STATE_ABBREV =\n \"A[KLRZ]|C[AOT]|D[CE]|F[LM]|G[AU]|HI|I[ADLN]|K[SY]|LA|\" +\n \"M[ADEHINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|\" +\n \"UT|V[AIT]|W[AIVY]\";\nconst US_STATE_ABBREV_BEFORE_ZIP_RE = new RegExp(\n `(?:^|[^A-Za-z0-9])(${US_STATE_ABBREV})\\\\s*,?\\\\s*$`,\n \"u\",\n);\nconst US_ZIP_CONTEXT_WINDOW = 120;\nconst US_CITY_ZIP_GAP_RE = /^[\\s,]+$/u;\nconst HOUSE_NUMBER_BEFORE_STREET_RE =\n /\\b\\d{1,6}(?:[-/]\\d{1,6})?\\s+(?:\\p{Lu}\\p{L}+[^\\S\\n\\t]+){0,4}$/u;\nconst HOUSE_NUMBER_AFTER_STREET_RE = /^[^\\S\\n\\t]+\\d{1,6}(?:[-/]\\d{1,6})?\\b/u;\n\n// ── Seed types ──────────────────────────────────────\n\ntype SeedType =\n | \"street-word\"\n | \"house-number\"\n | \"postal-code\"\n | \"city\"\n | \"state\"\n | \"address-trigger\";\n\ntype Seed = {\n type: SeedType;\n start: number;\n end: number;\n text: string;\n};\n\n// ── Dictionary loading ──────────────────────────────\n\ntype DictionaryConfig = Record<string, string[] | string>;\n\nlet cachedBoundaryRe: RegExp | null = null;\n\nconst loadBoundaryWords = async (): Promise<DictionaryConfig> => {\n try {\n const mod = await import(\"../data/address-boundaries.json\");\n return mod.default as DictionaryConfig;\n } catch {\n return {};\n }\n};\n\n// ── pt-BR CEP context gating ────────────────────────\n//\n// The bare `\\d{5}-\\d{3}` CEP shape collides with non-\n// address identifiers (\"Order 12345-678\"), so the seed\n// is only accepted when a pt-BR cue word appears within\n// the cluster window around it. The cue regex is built\n// once from the pt-BR entries of `address-street-types`\n// and `address-boundaries` (no hardcoded language strings\n// in TS). The window matches the seed-cluster gap so a\n// CEP that would otherwise cluster with a non-BR `city`\n// seed is filtered out before clustering.\n\nconst BR_CEP_CONTEXT_WINDOW = 200;\n\nlet cachedBrCepContextRe: RegExp | null = null;\nlet cachedBrCepContextPromise: Promise<RegExp | null> | null = null;\n\nconst loadBrCueWords = async (): Promise<readonly string[]> => {\n const sources = await Promise.all([\n (async () => {\n try {\n const mod = await import(\"../data/address-street-types.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON shape\n return (mod.default as DictionaryConfig)[\"pt-br\"];\n } catch {\n return undefined;\n }\n })(),\n (async () => {\n try {\n const mod = await import(\"../data/address-boundaries.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON shape\n return (mod.default as DictionaryConfig)[\"pt-br\"];\n } catch {\n return undefined;\n }\n })(),\n ]);\n\n const out: string[] = [];\n const seen = new Set<string>();\n for (const entry of sources) {\n if (!Array.isArray(entry)) continue;\n for (const word of entry) {\n if (typeof word !== \"string\" || word.length === 0) continue;\n const key = word.toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(word);\n }\n }\n return out;\n};\n\nconst getBrCepContextRe = async (): Promise<RegExp | null> => {\n if (cachedBrCepContextRe !== null) {\n return cachedBrCepContextRe;\n }\n if (cachedBrCepContextPromise) {\n return cachedBrCepContextPromise;\n }\n cachedBrCepContextPromise = (async () => {\n const words = await loadBrCueWords();\n if (words.length === 0) return null;\n const escaped = words\n .toSorted((a, b) => b.length - a.length)\n .map((w) => w.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"));\n const re = new RegExp(\n `(?<![\\\\p{L}\\\\p{N}])(?:${escaped.join(\"|\")})(?![\\\\p{L}\\\\p{N}])`,\n \"iu\",\n );\n cachedBrCepContextRe = re;\n return re;\n })();\n return cachedBrCepContextPromise;\n};\n\nconst hasBrCueNearby = (\n fullText: string,\n start: number,\n end: number,\n re: RegExp,\n): boolean => {\n const windowStart = Math.max(0, start - BR_CEP_CONTEXT_WINDOW);\n const windowEnd = Math.min(fullText.length, end + BR_CEP_CONTEXT_WINDOW);\n const window = fullText.slice(windowStart, windowEnd);\n // Build a fresh non-global RegExp per call — sharing\n // would carry lastIndex across calls.\n const probe = new RegExp(re.source, re.flags.replace(\"g\", \"\"));\n return probe.test(window);\n};\n\ntype UsZipPlusFourContext = {\n stateSeed: Seed | null;\n hasContext: boolean;\n};\n\nconst getUsStateSeedBeforeZip = (\n fullText: string,\n start: number,\n): Seed | null => {\n const stateWindowStart = Math.max(0, start - 24);\n const stateWindow = fullText.slice(stateWindowStart, start);\n const match = US_STATE_ABBREV_BEFORE_ZIP_RE.exec(stateWindow);\n const state = match?.[1];\n if (!match || !state) {\n return null;\n }\n\n const stateOffset = match[0].indexOf(state);\n const stateStart = stateWindowStart + match.index + stateOffset;\n return {\n type: \"state\",\n start: stateStart,\n end: stateStart + state.length,\n text: state,\n };\n};\n\nconst hasHouseNumberNearStreetWord = (\n fullText: string,\n seed: Seed,\n): boolean => {\n if (/\\d/.test(seed.text)) {\n return true;\n }\n\n const before = fullText.slice(Math.max(0, seed.start - 50), seed.start);\n if (HOUSE_NUMBER_BEFORE_STREET_RE.test(before)) {\n return true;\n }\n\n const after = fullText.slice(\n seed.end,\n Math.min(fullText.length, seed.end + 24),\n );\n return HOUSE_NUMBER_AFTER_STREET_RE.test(after);\n};\n\nconst isLowercaseStreetWordInProse = (fullText: string, seed: Seed): boolean =>\n /^\\p{Ll}/u.test(seed.text) &&\n /^\\s+\\p{Ll}/u.test(fullText.slice(seed.end, seed.end + 16)) &&\n !hasHouseNumberNearStreetWord(fullText, seed);\n\nconst getUsZipPlusFourContext = (\n fullText: string,\n start: number,\n seeds: readonly Seed[],\n): UsZipPlusFourContext => {\n const stateSeed = getUsStateSeedBeforeZip(fullText, start);\n if (stateSeed !== null) {\n return { stateSeed, hasContext: true };\n }\n\n const hasContext = seeds.some((seed) => {\n if (Math.abs(seed.start - start) > US_ZIP_CONTEXT_WINDOW) {\n return false;\n }\n if (seed.type === \"address-trigger\") {\n return true;\n }\n if (seed.type === \"city\" && seed.end <= start) {\n const gap = fullText.slice(seed.end, start);\n return US_CITY_ZIP_GAP_RE.test(gap);\n }\n if (seed.type === \"street-word\") {\n return hasHouseNumberNearStreetWord(fullText, seed);\n }\n return false;\n });\n return { stateSeed: null, hasContext };\n};\n\n/**\n * Build regex for boundary words. Matches any\n * boundary word preceded by a word boundary.\n */\nconst getBoundaryRe = async (): Promise<RegExp> => {\n if (cachedBoundaryRe) {\n return cachedBoundaryRe;\n }\n const config = await loadBoundaryWords();\n const words: string[] = [];\n for (const entries of Object.values(config)) {\n if (!Array.isArray(entries)) {\n continue;\n }\n for (const word of entries) {\n // Escape regex special chars\n words.push(word.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"));\n }\n }\n // Sort longest first so longer phrases match first\n words.sort((a, b) => b.length - a.length);\n // Word-boundary lookarounds via unicode letter/number\n // classes — `\\b` doesn't fire after non-word chars, so a\n // phrase ending in `.` (e.g. `sp. zn.`, `r.č.`, Italian\n // `C.F.`, Spanish `con C.I.F.`) would never match with\n // `\\b...\\b`. The lookarounds anchor on the absence of a\n // letter/digit on either side, which works regardless of\n // the phrase's last character.\n cachedBoundaryRe =\n words.length > 0\n ? new RegExp(\n `(?<![\\\\p{L}\\\\p{N}])(?:${words.join(\"|\")})(?![\\\\p{L}\\\\p{N}])`,\n \"iu\",\n )\n : /(?!)/;\n return cachedBoundaryRe;\n};\n\n// ── Pattern builder for unified search ──────────────\n\n/**\n * Build street type patterns for the unified search.\n * Returns string[] for the unified TextSearch\n * builder. Empty if data package is not installed.\n */\nlet streetTypePatternsPromise: Promise<string[]> | null = null;\n\nconst loadStreetTypePatterns = async (): Promise<string[]> => {\n let config: DictionaryConfig;\n try {\n const mod = await import(\"../data/address-street-types.json\");\n config = mod.default as DictionaryConfig;\n } catch {\n return [];\n }\n\n // Plain strings — the unified builder sets\n // caseInsensitive + wholeWords globally.\n const words: string[] = [];\n for (const values of Object.values(config)) {\n if (!Array.isArray(values)) {\n continue;\n }\n for (const word of values) {\n words.push(word);\n }\n }\n return words;\n};\n\nexport const buildStreetTypePatterns = async (): Promise<string[]> => {\n streetTypePatternsPromise ??= loadStreetTypePatterns();\n return streetTypePatternsPromise;\n};\n\n// ── Seed collection ─────────────────────────────────\n\nconst collectSeeds = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText: string,\n existingEntities: Entity[],\n brCepContextRe: RegExp | null,\n): Seed[] => {\n const seeds: Seed[] = [];\n\n // 1. Street type words from unified search matches\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) {\n continue;\n }\n const seed = {\n type: \"street-word\",\n start: match.start,\n end: match.end,\n text: match.text,\n } satisfies Seed;\n if (isLowercaseStreetWordInProse(fullText, seed)) {\n continue;\n }\n seeds.push(seed);\n }\n\n // 2. Cities and postal codes from existing entities\n for (const e of existingEntities) {\n if (e.label !== \"address\") {\n continue;\n }\n if (e.sourceDetail === \"custom-deny-list\") {\n continue;\n }\n if (e.source === \"deny-list\") {\n seeds.push({\n type: \"city\",\n start: e.start,\n end: e.end,\n text: e.text,\n });\n } else if (e.source === \"trigger\" && /^\\d/.test(e.text)) {\n seeds.push({\n type: \"postal-code\",\n start: e.start,\n end: e.end,\n text: e.text,\n });\n } else if (e.source === \"trigger\") {\n seeds.push({\n type: \"address-trigger\",\n start: e.start,\n end: e.end,\n text: e.text,\n });\n }\n }\n\n // 3. Standalone postal codes (multiple formats):\n // Czech/Slovak: NNN NN (e.g., 140 00)\n // Polish: NN-NNN (e.g., 00-950)\n // Brazilian CEP: NNNNN-NNN (e.g., 01001-000)\n // US ZIP+4: NNNNN-NNNN (e.g., 94304-1050)\n // The CEP shape collides with bare order/ticket numbers\n // (\"Order 12345-678\"), and the cluster's other seed\n // could be a non-BR deny-list city. To prevent that, the\n // CEP-shaped seed is only kept when a pt-BR cue word\n // (rua/avenida/CNPJ/CPF/RG/…) appears within the cluster\n // window around it. The Czech/Slovak and Polish shapes\n // are distinctive enough not to need a similar gate. US\n // ZIP+4 seeds are only kept with nearby address evidence\n // because business/order IDs often share the same shape.\n const postalRe = POSTAL_CODE_RE;\n postalRe.lastIndex = 0;\n let postalMatch;\n while ((postalMatch = postalRe.exec(fullText)) !== null) {\n const start = postalMatch.index;\n const end = start + postalMatch[0].length;\n const alreadyCovered = seeds.some((s) => s.start <= start && s.end >= end);\n if (alreadyCovered) {\n continue;\n }\n const isCepShape = BR_CEP_SHAPE_RE.test(postalMatch[0]);\n if (\n isCepShape &&\n (brCepContextRe === null ||\n !hasBrCueNearby(fullText, start, end, brCepContextRe))\n ) {\n continue;\n }\n const isUsZipPlusFourShape = US_ZIP_PLUS_FOUR_SHAPE_RE.test(postalMatch[0]);\n if (isUsZipPlusFourShape) {\n const usContext = getUsZipPlusFourContext(fullText, start, seeds);\n if (!usContext.hasContext) {\n continue;\n }\n const stateSeed = usContext.stateSeed;\n if (stateSeed !== null) {\n const hasStateSeed = seeds.some(\n (seed) =>\n seed.start === stateSeed.start && seed.end === stateSeed.end,\n );\n if (!hasStateSeed) {\n seeds.push(stateSeed);\n }\n }\n }\n seeds.push({\n type: \"postal-code\",\n start,\n end,\n text: postalMatch[0],\n });\n }\n\n // 3b. Italian CAP: 5 consecutive digits followed by a\n // capitalised word (\"41012 Carpi\", \"41012 MODENA\"). The\n // capitalised word requirement keeps random 5-digit IDs\n // (years, order numbers) out, and the explicit\n // address-evidence-within-80-chars gate keeps a stray\n // \"12345 Paris\" reference from accidentally clustering\n // into an address in non-Italian text. A bare \"via\"\n // street-word does not count on its own — it is also a\n // common English preposition matched case-insensitively\n // (\"sent via form 12345 Paris\"), so require either an\n // address trigger / city / postal seed nearby, or a\n // street-word other than a standalone lowercase \"via\".\n const itCapRe = /\\b\\d{5}(?=\\s+\\p{Lu}\\p{L}+)/gu;\n let itCapMatch;\n while ((itCapMatch = itCapRe.exec(fullText)) !== null) {\n const start = itCapMatch.index;\n const end = start + itCapMatch[0].length;\n const alreadyCovered = seeds.some((s) => s.start <= start && s.end >= end);\n if (alreadyCovered) continue;\n const hasNearbyAddressEvidence = seeds.some((s) => {\n if (Math.abs(s.start - start) > 80) return false;\n if (s.type === \"address-trigger\") return true;\n if (s.type === \"city\") return true;\n if (s.type === \"postal-code\") return true;\n if (s.type === \"street-word\") {\n // Reject the bare English preposition \"via\" as the\n // sole signal; any longer or non-\"via\" street word\n // (Piazza, Viale, Corso, Via Roma) still qualifies.\n return s.text.toLowerCase() !== \"via\";\n }\n return false;\n });\n if (!hasNearbyAddressEvidence) continue;\n seeds.push({\n type: \"postal-code\",\n start,\n end,\n text: itCapMatch[0],\n });\n }\n\n // 4. Street name + house number pattern:\n // [CapitalizedWord] [number](/[number])?,\n // e.g., \"Olbrachtova 1929/62,\" or \"Kamínky 5,\"\n const streetNumRe =\n /\\b(\\p{Lu}\\p{Ll}{2,})\\s+(\\d{1,5}(?:\\/\\d{1,5})?)\\s*[,\\n]/gu;\n let streetMatch;\n while ((streetMatch = streetNumRe.exec(fullText)) !== null) {\n const matchedStreet = streetMatch[1];\n const matchedNum = streetMatch[2];\n if (!matchedStreet || !matchedNum) {\n continue;\n }\n const start = streetMatch.index;\n const end = start + matchedStreet.length + 1 + matchedNum.length;\n seeds.push({\n type: \"street-word\",\n start,\n end,\n text: fullText.slice(start, end),\n });\n }\n\n return seeds.sort((a, b) => a.start - b.start);\n};\n\n// ── Cluster nearby seeds ────────────────────────────\n\ntype SeedCluster = {\n seeds: Seed[];\n start: number;\n end: number;\n};\n\nconst clusterSeeds = (seeds: Seed[], maxGap: number): SeedCluster[] => {\n const first = seeds[0];\n if (!first) {\n return [];\n }\n\n const clusters: SeedCluster[] = [];\n let current: SeedCluster = {\n seeds: [first],\n start: first.start,\n end: first.end,\n };\n\n for (let i = 1; i < seeds.length; i++) {\n const seed = seeds.at(i);\n if (!seed) {\n continue;\n }\n if (seed.start - current.end <= maxGap) {\n current.seeds.push(seed);\n current.end = Math.max(current.end, seed.end);\n } else {\n clusters.push(current);\n current = {\n seeds: [seed],\n start: seed.start,\n end: seed.end,\n };\n }\n }\n clusters.push(current);\n\n return clusters;\n};\n\n// ── Score a cluster ─────────────────────────────────\n\nconst scoreCluster = (cluster: SeedCluster): number => {\n const types = new Set(cluster.seeds.map((s) => s.type));\n\n // Need at least 2 different seed types for an\n // address (e.g., city + postal code, or street\n // word + house number)\n if (types.size < 2) {\n return 0;\n }\n\n let score = 0.5;\n\n if (types.has(\"postal-code\")) score += 0.15;\n if (types.has(\"city\")) score += 0.15;\n if (types.has(\"state\")) score += 0.15;\n if (types.has(\"street-word\")) score += 0.15;\n if (types.has(\"address-trigger\")) score += 0.1;\n\n return Math.min(score, 0.95);\n};\n\n// ── Expand cluster to full address span ─────────────\n\nconst NON_ADDRESS_LABELS = new Set([\n \"registration number\",\n \"tax identification number\",\n \"national identification number\",\n \"social security number\",\n \"birth number\",\n \"identity card number\",\n \"person\",\n \"bank account number\",\n \"email address\",\n \"phone number\",\n \"organization\",\n \"iban\",\n]);\n\nconst expandCluster = async (\n fullText: string,\n cluster: SeedCluster,\n existingEntities: Entity[],\n): Promise<{ start: number; end: number }> => {\n const { start, end } = cluster;\n const seedTypes = new Set(cluster.seeds.map((seed) => seed.type));\n\n // Find the nearest non-address entity to the LEFT\n let leftBound = 0;\n for (const e of existingEntities) {\n if (\n NON_ADDRESS_LABELS.has(e.label) &&\n e.end <= start &&\n e.end > leftBound\n ) {\n leftBound = e.end;\n }\n }\n\n // Expand left: include preceding capitalized words\n // and numbers (street name before the street type)\n let leftPos = start;\n while (leftPos > leftBound) {\n let p = leftPos - 1;\n while (p >= 0 && (fullText[p] === \" \" || fullText[p] === \",\")) {\n p--;\n }\n if (p < 0) break;\n\n let wordEnd = p + 1;\n while (p >= 0 && /\\S/.test(fullText[p] ?? \"\")) {\n p--;\n }\n const word = fullText.slice(p + 1, wordEnd);\n\n if (word.length < 2 || (!/^\\p{Lu}/u.test(word) && !/^\\d/.test(word))) {\n break;\n }\n\n if (fullText.slice(p + 1, leftPos).includes(\"\\n\")) {\n break;\n }\n\n leftPos = p + 1;\n }\n\n const hasExpandableAddressContext =\n seedTypes.has(\"street-word\") ||\n seedTypes.has(\"house-number\") ||\n seedTypes.has(\"postal-code\") ||\n seedTypes.has(\"address-trigger\");\n\n if (!hasExpandableAddressContext) {\n return {\n start: Math.min(leftPos, start),\n end,\n };\n }\n\n // Expand right: include following text until we\n // hit a boundary word, non-address entity,\n // double newline, or 200 char cap.\n let rightPos = end;\n const remaining = fullText.slice(rightPos);\n let nearestBoundary = Math.min(remaining.length, 200);\n\n // Stop at dictionary-defined boundary words\n const boundaryRe = await getBoundaryRe();\n const boundaryMatch = boundaryRe.exec(remaining);\n if (boundaryMatch && boundaryMatch.index < nearestBoundary) {\n nearestBoundary = boundaryMatch.index;\n }\n\n // Stop at non-address entities\n for (const e of existingEntities) {\n if (!NON_ADDRESS_LABELS.has(e.label)) {\n continue;\n }\n const offset = e.start - rightPos;\n if (offset > 0 && offset < nearestBoundary) {\n nearestBoundary = offset;\n }\n }\n\n // Stop at double newline (paragraph break)\n const doubleNewline = remaining.indexOf(\"\\n\\n\");\n if (doubleNewline !== -1 && doubleNewline < nearestBoundary) {\n nearestBoundary = doubleNewline;\n }\n\n const expanded = remaining.slice(0, nearestBoundary).trimEnd();\n rightPos = end + expanded.length;\n\n while (\n rightPos > end &&\n ADDRESS_TRAILING_TRIM_RE.test(fullText[rightPos - 1] ?? \"\")\n ) {\n rightPos--;\n }\n\n return {\n start: Math.min(leftPos, start),\n end: Math.max(rightPos, end),\n };\n};\n\n// Allow a span containing exactly one line break only when the cluster\n// carries independent address evidence on both sides of the break: a\n// \"street\" seed (street-word or house-number) above the newline AND a\n// \"destination\" seed (postal-code or city) below (or vice-versa).\n// This admits the dominant US notice-block shape\n//\n// One American Road\n// Cleveland, Ohio 44144-2398\n//\n// while rejecting both the single-line case that got right-expanded\n// across a newline (all seeds above the break, prose below) and the\n// structurally over-reaching case with two or more internal newlines.\nconst STREET_SEED_TYPES: ReadonlySet<SeedType> = new Set([\n \"street-word\",\n \"house-number\",\n]);\nconst DESTINATION_SEED_TYPES: ReadonlySet<SeedType> = new Set([\n \"postal-code\",\n \"city\",\n]);\n\ntype NewlineBoundaryResolution =\n | { kind: \"keep\" }\n | { kind: \"drop\" }\n | { kind: \"trim\"; relativeEnd: number };\n\nconst resolveNewlineBoundary = (\n spanStart: number,\n text: string,\n cluster: SeedCluster,\n): NewlineBoundaryResolution => {\n const newlines = (text.match(/\\n/gu) ?? []).length;\n if (newlines === 0) return { kind: \"keep\" };\n if (newlines > 1) return { kind: \"drop\" };\n\n const relativeNewline = text.indexOf(\"\\n\");\n const newlineAbs = spanStart + relativeNewline;\n\n let streetAbove = false;\n let streetBelow = false;\n let destAbove = false;\n let destBelow = false;\n for (const seed of cluster.seeds) {\n const isAbove = seed.end <= newlineAbs;\n const isStreet = STREET_SEED_TYPES.has(seed.type);\n const isDest = DESTINATION_SEED_TYPES.has(seed.type);\n if (isStreet && isAbove) streetAbove = true;\n if (isStreet && !isAbove) streetBelow = true;\n if (isDest && isAbove) destAbove = true;\n if (isDest && !isAbove) destBelow = true;\n }\n\n // True multi-line notice block: street and destination evidence\n // straddle the newline (`One American Road\\nCleveland, Ohio 44144`).\n if ((streetAbove && destBelow) || (streetBelow && destAbove)) {\n return { kind: \"keep\" };\n }\n\n // Inline address followed by unrelated prose on the next line\n // (`650 Page Mill Road, Palo Alto, CA 94304-1050.\\nPlease review…`).\n // The expansion walked through the newline up to its 200-char cap,\n // but the complete address sits entirely above the break. Trim back\n // to the line above instead of dropping the whole span.\n if (streetAbove && destAbove) {\n return { kind: \"trim\", relativeEnd: relativeNewline };\n }\n\n return { kind: \"drop\" };\n};\n\n// Normalise CRLF paragraph breaks to LF for the address-seed\n// newline-boundary check. Windows/PDF-extracted contracts commonly use\n// `\\r\\n`, which leaves the expanded text with paragraph breaks the\n// LF-only `\\n\\n` stop in `expandCluster` doesn't catch and inflates the\n// internal-newline count past the single-newline cap.\nconst normaliseLineBreaks = (text: string): string =>\n text.replace(/\\r\\n?/gu, \"\\n\");\n\n// ── Public API ──────────────────────────────────────\n\n/**\n * Process address seeds from the unified search.\n * Receives all matches; filters to the street types\n * slice via sliceStart/sliceEnd. Uses fullText and\n * existingEntities for seed collection, clustering,\n * expansion, and scoring.\n *\n * Runs as a post-processor after all other detectors,\n * using their output as seed sources.\n */\nexport const processAddressSeeds = async (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText: string,\n existingEntities: Entity[],\n): Promise<Entity[]> => {\n const brCepContextRe = await getBrCepContextRe();\n const seeds = collectSeeds(\n allMatches,\n sliceStart,\n sliceEnd,\n fullText,\n existingEntities,\n brCepContextRe,\n );\n const clusters = clusterSeeds(seeds, 150);\n\n const results: Entity[] = [];\n\n for (const cluster of clusters) {\n const score = scoreCluster(cluster);\n if (score < 0.6) {\n continue;\n }\n\n const { start, end } = await expandCluster(\n fullText,\n cluster,\n existingEntities,\n );\n const rawText = fullText.slice(start, end);\n const resolution = resolveNewlineBoundary(\n start,\n normaliseLineBreaks(rawText),\n cluster,\n );\n if (resolution.kind === \"drop\") continue;\n const effectiveText =\n resolution.kind === \"trim\"\n ? rawText.slice(0, resolution.relativeEnd).trim()\n : rawText.trim();\n if (effectiveText.length < 5 || effectiveText.length > 300) continue;\n\n results.push({\n start,\n end: start + effectiveText.length,\n label: \"address\",\n text: effectiveText,\n score,\n source: DETECTION_SOURCES.REGEX,\n });\n }\n\n return results;\n};\n","import { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\nimport { LEGAL_SUFFIXES } from \"../config/legal-forms\";\n\nconst TRAILING_SEP = /[,\\s]+$/;\nconst WORD_CHAR_RE = /[\\p{L}\\p{N}]/u;\nconst ORG_PROPAGATION_SCORE = 0.9;\n// Common determiners that prefix a defined-term shorthand for\n// the originating company in mid-document references. After\n// declaring `Acme s.r.o.` (or `Acme Corp.`) we treat\n// \"Společnost Acme\", \"Společnosti Acme\" (Czech declensions),\n// \"the Company Acme\", or \"die Gesellschaft Acme\" as the same\n// organisation and extend the highlighted span to cover them.\n// Match is case-insensitive and word-bounded.\nconst ORG_DETERMINER_RE =\n /(?<![\\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;\n\nconst isCallerOwnedEntity = (entity: Entity): boolean =>\n entity.sourceDetail === \"custom-deny-list\" ||\n entity.sourceDetail === \"custom-regex\";\n\ntype Seed = {\n baseName: string;\n label: string;\n /** Full entity text the propagated mentions link to. */\n sourceText: string;\n};\n\n/**\n * After the main detection pass, collect organization\n * entities with a legal form suffix, strip the suffix\n * to get the base name, and re-scan the full text for\n * bare mentions of that base name. Returns new entities\n * for occurrences not already covered.\n *\n * Propagated mentions are coref aliases: each carries\n * `corefSourceText` linking it to the full seed entity\n * text, so placeholder numbering assigns the bare\n * mention the same placeholder as its source (\"Acme\"\n * and \"Acme Corp.\" both become [ORGANIZATION_1]).\n */\nexport const propagateOrgNames = (\n entities: Entity[],\n fullText: string,\n): Entity[] => {\n const seedByBase = new Map<string, Seed>();\n\n for (const e of entities) {\n if (e.label !== \"organization\") continue;\n if (isCallerOwnedEntity(e)) continue;\n for (const suffix of LEGAL_SUFFIXES) {\n if (e.text.endsWith(suffix)) {\n const base = e.text\n .slice(0, -suffix.length)\n .replace(TRAILING_SEP, \"\")\n .trim();\n if (base.length >= 3) {\n const existing = seedByBase.get(base);\n if (existing === undefined) {\n seedByBase.set(base, {\n baseName: base,\n label: e.label,\n sourceText: e.text,\n });\n } else if (existing.sourceText !== e.text) {\n // Distinct full forms share this base\n // (\"Acme LLC\" vs \"Acme Corporation\").\n // Linking bare mentions to either one would\n // corrupt the redaction key, so link them to\n // the base name itself: all bare mentions\n // still share one placeholder, distinct from\n // both full forms.\n existing.sourceText = base;\n }\n }\n break;\n }\n }\n }\n\n const seeds = [...seedByBase.values()];\n if (seeds.length === 0) return [];\n\n // Build a mutable array of already-covered spans\n // for overlap checks. Updated as new entities are\n // emitted to prevent duplicate propagation.\n const covered: [number, number][] = entities.map((e) => [e.start, e.end]);\n const isOverlapping = (start: number, end: number): boolean =>\n covered.some(([cs, ce]) => start < ce && end > cs);\n\n const results: Entity[] = [];\n\n for (const seed of seeds) {\n const { baseName, label } = seed;\n let searchFrom = 0;\n while (searchFrom < fullText.length) {\n const idx = fullText.indexOf(baseName, searchFrom);\n if (idx === -1) break;\n\n const matchEnd = idx + baseName.length;\n\n // Word boundary: reject if preceded or followed\n // by a letter or digit (prevents substring\n // matches like \"ACME\" inside \"ACME2\").\n const prevCh = fullText[idx - 1] ?? \"\";\n const nextCh = fullText[matchEnd] ?? \"\";\n if (WORD_CHAR_RE.test(prevCh) || WORD_CHAR_RE.test(nextCh)) {\n searchFrom = idx + 1;\n continue;\n }\n\n // Extend the span backward to include a Czech / English\n // \"Společnost\"/\"the Company\"-style determiner if present.\n // Without this, after `(\"Společnost Acme\")` the\n // propagator would only highlight the bare \"Acme\" in\n // later mentions like \"Společnost Acme\" — losing the\n // determiner that's part of the referring phrase.\n let spanStart = idx;\n const lookbackStart = Math.max(0, idx - 40);\n const lookback = fullText.slice(lookbackStart, idx);\n const determinerMatch = ORG_DETERMINER_RE.exec(lookback);\n if (determinerMatch !== null) {\n // The match string may include a leading separator\n // char (the alternation accepts `^` or `[\\s ]`) and\n // trailing whitespace; the capture group is the\n // determiner itself, so locate it inside the wider\n // match to skip whatever separators were consumed.\n const determiner = determinerMatch[1] ?? \"\";\n const offsetInMatch = determinerMatch[0].indexOf(determiner);\n spanStart = lookbackStart + determinerMatch.index + offsetInMatch;\n }\n\n // Skip if already covered by an existing entity\n // or a previously propagated result.\n if (!isOverlapping(spanStart, matchEnd)) {\n results.push({\n start: spanStart,\n end: matchEnd,\n label,\n text: fullText.slice(spanStart, matchEnd),\n score: ORG_PROPAGATION_SCORE,\n source: DETECTION_SOURCES.COREFERENCE,\n corefSourceText: seed.sourceText,\n });\n covered.push([spanStart, matchEnd]);\n }\n\n searchFrom = matchEnd;\n }\n }\n\n return results;\n};\n","","import addressStreetTypesJson from \"../data/address-street-types.json\";\nimport type { Entity } from \"../types\";\n\n// Capitalised words that look like the start of an\n// `[Uppercase] [number]` address (Czech: \"Vinohradská 12\")\n// but in contract prose introduce a section, clause, or\n// document reference instead (\"Section 6\", \"Article 9\").\n// Listed here so the `bareHouseRe` near-address scan does\n// not promote them to address spans. Module-level to avoid\n// allocation in a hot loop.\nconst BARE_STOPWORDS = new Set([\n // ── Czech ────────────────────────────────────────\n \"Příloha\",\n \"Smlouva\",\n \"Článek\",\n \"Dodatek\",\n \"Celkem\",\n \"Strana\",\n \"Faktura\",\n \"Částka\",\n \"Položka\",\n \"Kapitola\",\n \"Zákon\",\n \"Vyhláška\",\n \"Nařízení\",\n \"Usnesení\",\n \"Rozsudek\",\n \"Bod\",\n \"Odstavec\",\n \"Záloha\",\n \"Zbývá\",\n \"Dne\",\n \"Platba\",\n \"Datum\",\n \"Splatnost\",\n \"Variabilní\",\n \"Konstantní\",\n \"Specifický\",\n // ── English ──────────────────────────────────────\n \"Section\",\n \"Sections\",\n \"Article\",\n \"Articles\",\n \"Schedule\",\n \"Schedules\",\n \"Exhibit\",\n \"Exhibits\",\n \"Annex\",\n \"Annexes\",\n \"Appendix\",\n \"Appendices\",\n \"Clause\",\n \"Clauses\",\n \"Chapter\",\n \"Chapters\",\n \"Paragraph\",\n \"Paragraphs\",\n \"Subsection\",\n \"Subsections\",\n \"Form\",\n \"Page\",\n \"Pages\",\n \"Item\",\n \"Items\",\n \"Note\",\n \"Notes\",\n \"Rule\",\n \"Rules\",\n \"Attachment\",\n \"Attachments\",\n \"Volume\",\n \"Volumes\",\n \"Book\",\n \"Books\",\n \"Part\",\n \"Parts\",\n]);\n\nconst NEAR_MISS_BAND = 0.15;\nconst BOOST_PER_NEIGHBOUR = 0.05;\nconst CONTEXT_WINDOW_CHARS = 150;\nconst HIGH_CONFIDENCE_FLOOR = 0.9;\n\nconst isCallerOwnedEntity = (entity: Entity): boolean =>\n entity.sourceDetail === \"custom-deny-list\" ||\n entity.sourceDetail === \"custom-regex\";\n\n/**\n * Boost confidence of near-miss NER entities that appear\n * near high-confidence detections (regex, trigger phrase).\n *\n * If an NER entity scored between (threshold - 0.15) and\n * threshold, count how many confirmed entities exist within\n * a 150-char window. Add +0.05 per co-located entity.\n * If the boosted score crosses the threshold, include it.\n *\n * Only mutates score on near-miss entities; high-confidence\n * entities pass through unchanged.\n */\nexport const boostNearMissEntities = (\n entities: Entity[],\n threshold: number,\n): Entity[] => {\n const nearMissBand = Math.max(0, threshold - NEAR_MISS_BAND);\n const confirmed = entities.filter((e) => e.score >= HIGH_CONFIDENCE_FLOOR);\n\n const boosted: Entity[] = [];\n\n for (const entity of entities) {\n if (entity.score >= threshold) {\n boosted.push(entity);\n continue;\n }\n\n if (entity.score < nearMissBand) {\n continue;\n }\n\n const midpoint = (entity.start + entity.end) / 2;\n let neighbourCount = 0;\n\n for (const anchor of confirmed) {\n const anchorMid = (anchor.start + anchor.end) / 2;\n if (Math.abs(midpoint - anchorMid) <= CONTEXT_WINDOW_CHARS) {\n neighbourCount++;\n }\n }\n\n const boostedScore = entity.score + neighbourCount * BOOST_PER_NEIGHBOUR;\n\n if (boostedScore >= threshold) {\n boosted.push({ ...entity, score: boostedScore });\n }\n }\n\n return boosted;\n};\n\n// ── Address backward scan ────────────────────────────\n\nconst UPPER_WORD_RE = /\\p{Lu}/u;\n\n/** Header zone: top 15% of document */\nconst HEADER_ZONE_FRACTION = 0.15;\n\n/** Context window for address adjacency */\nconst STREET_CONTEXT_WINDOW = 200;\n\n// ── Preposition data (lazy-loaded from JSON) ────────\n\ntype PrepositionData = {\n address: Record<string, string[] | string>;\n temporal: Record<string, string[] | string>;\n};\n\nlet _addressPreps: ReadonlySet<string> | null = null;\nlet _temporalPreps: ReadonlySet<string> | null = null;\nlet _prepsPromise: Promise<void> | null = null;\n\nconst loadPrepositions = async (): Promise<void> => {\n try {\n const mod = await import(\"../data/address-prepositions.json\");\n const data: PrepositionData = mod.default ?? mod;\n // Merge all languages into flat sets\n const addr = new Set<string>();\n const temp = new Set<string>();\n for (const words of Object.values(data.address)) {\n if (Array.isArray(words)) {\n for (const w of words) addr.add(w.toLowerCase());\n }\n }\n for (const words of Object.values(data.temporal)) {\n if (Array.isArray(words)) {\n for (const w of words) temp.add(w.toLowerCase());\n }\n }\n _addressPreps = addr;\n _temporalPreps = temp;\n } catch {\n _addressPreps = new Set();\n _temporalPreps = new Set();\n }\n};\n\n/** Ensure preposition data is loaded. */\nexport const initPrepositions = (): Promise<void> => {\n if (!_prepsPromise) {\n _prepsPromise = loadPrepositions();\n }\n return _prepsPromise;\n};\n\nconst getAddressPreps = (): ReadonlySet<string> => _addressPreps ?? new Set();\n\nconst getTemporalPreps = (): ReadonlySet<string> => _temporalPreps ?? new Set();\n\n// ── Street type abbreviations (lazy-loaded) ─────────\n\nconst buildStreetAbbrevs = (\n data: Record<string, unknown>,\n): ReadonlySet<string> => {\n const abbrevs = new Set<string>();\n for (const [key, words] of Object.entries(data)) {\n if (key.startsWith(\"_\")) continue;\n if (!Array.isArray(words)) continue;\n for (const word of words) {\n if (typeof word === \"string\" && word.includes(\".\")) {\n abbrevs.add(word.toLowerCase());\n }\n }\n }\n return abbrevs;\n};\n\nlet _streetAbbrevs: ReadonlySet<string> | null = buildStreetAbbrevs(\n addressStreetTypesJson,\n);\nlet _streetAbbrevsPromise: Promise<void> | null = null;\n\nconst loadStreetAbbrevs = async (): Promise<void> => {\n try {\n const mod = await import(\"../data/address-street-types.json\");\n const data: Record<string, string[] | string> = mod.default ?? mod;\n _streetAbbrevs = buildStreetAbbrevs(data);\n } catch {\n _streetAbbrevs ??= new Set();\n }\n};\n\n/** Ensure street abbreviation data is loaded. */\nexport const initStreetAbbrevs = (): Promise<void> => {\n if (!_streetAbbrevsPromise) {\n _streetAbbrevsPromise = loadStreetAbbrevs();\n }\n return _streetAbbrevsPromise;\n};\n\nexport const getStreetAbbrevs = (): ReadonlySet<string> =>\n _streetAbbrevs ?? new Set();\n\n/**\n * Scan backwards from known address entities and\n * house number patterns to find street names.\n *\n * Strategy (a): from a house number like \"2512/2a\",\n * walk left to find the first uppercase word — that's\n * the street name start. \"Mezi úvozy 2512/2a\" →\n * captures \"Mezi úvozy 2512/2a\".\n *\n * Strategy (b): if a colon \":\" appears within 3 chars\n * before the street start, boost confidence. Colons\n * signal \"label: value\" pairs universal in contracts.\n *\n * Strategy (c): in the header zone (top 15% of doc),\n * be more aggressive — detect street patterns even\n * without nearby address entities.\n */\nexport const detectStreetPatternsNearAddresses = (\n fullText: string,\n existingEntities: Entity[],\n): Entity[] => {\n const results: Entity[] = [];\n const contextEntities = existingEntities.filter(\n (entity) => !(entity.label === \"address\" && isCallerOwnedEntity(entity)),\n );\n const addressEntities = contextEntities.filter((e) => e.label === \"address\");\n const headerEnd = Math.floor(fullText.length * HEADER_ZONE_FRACTION);\n\n // Find all house number positions in the text.\n // Slash-style house numbers only: either with a letter\n // suffix (\"2512/2a\") or primary >= 100 (\"853/12\").\n // This excludes date-like patterns (\"31/12\", \"1/1\").\n // Require either a letter suffix (2512/2a) or a\n // primary part > 31 to avoid matching Czech slash\n // dates like \"31/12\" or \"1/1\". Slash house numbers\n // in Czech addresses almost always have primary > 99\n // or carry a letter suffix.\n const houseNumRe =\n /\\b(?:\\d{1,4}\\/\\d+[a-zA-Z]\\b|\\d{3,4}\\/\\d+\\b|(?:1[3-9]|[2-9]\\d)\\/\\d{3,}\\b)/g;\n houseNumRe.lastIndex = 0;\n\n for (\n let m = houseNumRe.exec(fullText);\n m !== null;\n m = houseNumRe.exec(fullText)\n ) {\n const numStart = m.index;\n const numEnd = numStart + m[0].length;\n\n // Skip if already covered by an existing entity\n if (existingEntities.some((e) => e.start <= numStart && e.end >= numEnd)) {\n continue;\n }\n\n // Is this near a known address entity OR in header?\n const inHeader = numStart < headerEnd;\n const nearAddress = addressEntities.some(\n (e) =>\n Math.abs(e.start - numEnd) < STREET_CONTEXT_WINDOW ||\n Math.abs(e.end - numStart) < STREET_CONTEXT_WINDOW,\n );\n\n if (!inHeader && !nearAddress) {\n continue;\n }\n\n // Backward scan: find street name before the\n // house number. Walk left over whitespace, then\n // collect words that start with uppercase.\n let scanPos = numStart - 1;\n\n // Skip whitespace before the number (including\n // non-breaking spaces from PDF extraction)\n while (scanPos >= 0 && /[\\s\\u00A0]/.test(fullText[scanPos] ?? \"\")) {\n scanPos--;\n }\n\n if (scanPos < 0) {\n continue;\n }\n\n // Track if any temporal preposition was passed\n // through during the scan (e.g., \"do\", \"od\").\n // If so, the result is likely a date expression\n // even if wordCount > 1 (\"Praha do 225/1\").\n let hasTemporalPrep = false;\n\n // Collect words backwards until we hit:\n // - a non-letter character (except space/dot)\n // - a newline\n // - start of text\n // - a lowercase-only word (not a street name)\n let streetStart = scanPos + 1;\n let wordCount = 0;\n const MAX_WORDS = 5;\n\n while (scanPos >= 0 && wordCount < MAX_WORDS) {\n // Find end of current word. If we're on a dot,\n // include it (street abbreviations: \"ul.\", \"nám.\")\n let wordEnd = scanPos + 1;\n const hasDot = fullText[scanPos] === \".\";\n if (hasDot) {\n scanPos--;\n }\n\n // Walk back through word chars (letters, marks,\n // and digits for tokens like \"28\" in \"28. října\").\n while (scanPos >= 0 && /[\\p{L}\\p{M}\\d]/u.test(fullText[scanPos] ?? \"\")) {\n scanPos--;\n }\n const wordStart = scanPos + 1;\n const rawWord = fullText.slice(wordStart, wordEnd);\n // Strip trailing dot for word checks\n const word = hasDot ? rawWord.slice(0, -1) : rawWord;\n\n if (word.length === 0) {\n break;\n }\n\n // Check if this is a known street abbreviation\n // (e.g., \"ul.\", \"nám.\", \"tř.\", \"nábř.\")\n const isStreetAbbrev =\n hasDot && getStreetAbbrevs().has(rawWord.toLowerCase());\n\n // Word must start with uppercase (street name),\n // be a known preposition, a street abbreviation,\n // or a digit-starting token (e.g., \"28.\" in\n // \"28. října 1168/102\").\n const isUpper = UPPER_WORD_RE.test(word[0] ?? \"\");\n const isPrep = getAddressPreps().has(word.toLowerCase());\n const isDigitToken = /^\\d{1,2}$/.test(word);\n\n if (!isUpper && !isPrep && !isStreetAbbrev && !isDigitToken) {\n break;\n }\n\n // Track temporal prepositions passed through\n if (isPrep && getTemporalPreps().has(word.toLowerCase())) {\n hasTemporalPrep = true;\n }\n\n streetStart = wordStart;\n wordCount++;\n\n // Skip whitespace before this word (inc. NBSP)\n while (scanPos >= 0 && /[\\s\\u00A0]/.test(fullText[scanPos] ?? \"\")) {\n scanPos--;\n }\n\n // Stop at newline, tab, comma, semicolon\n const prevCh = fullText[scanPos];\n if (\n prevCh === \"\\n\" ||\n prevCh === \"\\t\" ||\n prevCh === \";\" ||\n prevCh === undefined\n ) {\n break;\n }\n\n // Comma: stop (it separates address from\n // previous clause)\n if (prevCh === \",\") {\n break;\n }\n }\n\n if (wordCount === 0) {\n continue;\n }\n\n const streetText = fullText.slice(streetStart, numEnd);\n\n // Skip if too short (single digit without name)\n if (streetText.length < 4) {\n continue;\n }\n\n // Guard: reject if any temporal preposition was\n // encountered during the backward scan. Catches\n // both \"do 225/1\" (wordCount=1) and \"Praha do\n // 225/1\" (wordCount=2).\n if (hasTemporalPrep) {\n continue;\n }\n\n // Skip if already covered\n if (\n existingEntities.some((e) => e.start <= streetStart && e.end >= numEnd)\n ) {\n continue;\n }\n\n // Colon boost: if \":\" appears within 5 chars\n // before street start, this is a \"label: value\"\n // pair — very high confidence.\n const beforeStreet = fullText.slice(\n Math.max(0, streetStart - 5),\n streetStart,\n );\n const hasColon = beforeStreet.includes(\":\");\n let score = 0.8;\n if (hasColon) {\n score = 0.95;\n } else if (inHeader) {\n score = 0.85;\n }\n\n results.push({\n start: streetStart,\n end: numEnd,\n label: \"address\",\n text: streetText,\n score,\n source: \"regex\",\n });\n }\n\n // ── Second scan: bare house numbers near addresses ─\n // \"Vinohradská 46\" near \"Praha 2\" → address.\n // Uppercase word + space + 1-3 digit number, no slash.\n // Capped at 3 digits to exclude year numbers (2024).\n const bareHouseRe = /(?<=\\s|^)(\\p{Lu}\\p{Ll}[\\p{Ll}\\p{Lu}]+\\s+\\d{1,3})\\b/gu;\n bareHouseRe.lastIndex = 0;\n\n // Merge existing + newly found entities for proximity\n const allAddr = [...addressEntities, ...results];\n\n for (\n let m = bareHouseRe.exec(fullText);\n m !== null;\n m = bareHouseRe.exec(fullText)\n ) {\n const captured = m[1];\n if (captured === undefined) continue;\n\n const start = m.index;\n const end = start + captured.length;\n\n // Must be on the same line as a confirmed address\n // entity and within 50 chars.\n const nearAddr = allAddr.some((e) => {\n const dist = Math.min(Math.abs(e.start - end), Math.abs(e.end - start));\n if (dist > 50) return false;\n // Ensure same line: no newline between\n const lo = Math.min(e.start, start);\n const hi = Math.max(e.end, end);\n const between = fullText.slice(lo, hi);\n return !between.includes(\"\\n\");\n });\n\n if (!nearAddr) continue;\n\n // Extract the uppercase word to check stopwords\n const spaceIdx = captured.search(/\\s+\\d/);\n const word = spaceIdx > 0 ? captured.slice(0, spaceIdx) : captured;\n\n if (BARE_STOPWORDS.has(word)) continue;\n\n // Skip if overlapping an existing entity\n const allEntities = [...existingEntities, ...results];\n const overlaps = allEntities.some((e) => e.start < end && e.end > start);\n if (overlaps) continue;\n\n results.push({\n start,\n end,\n label: \"address\",\n text: captured,\n score: 0.75,\n source: \"regex\",\n });\n }\n\n return results;\n};\n\n// ── Orphan street lines in header zone ──────────────\n\n// Orphan street: first word uppercase, subsequent words\n// can be lowercase (Czech: \"Karlínské náměstí 7\",\n// \"Pražská ulice 12\") or uppercase (\"Národní třída 1\").\n// House number requires 2+ digits to avoid matching\n// contract headings like \"Příloha 1\" or \"Smlouva 3\".\n// Inner whitespace is constrained to non-newline runs so a\n// table-of-contents block like\n// `Litigation\\n \\n \\n26\\n` does not accidentally satisfy\n// the pattern: a real street + number sits on a single line.\nconst ORPHAN_STREET_RE =\n /^[^\\S\\n]*(\\p{Lu}[\\p{Ll}\\p{Lu}]+(?:[^\\S\\n]+[\\p{Lu}\\p{Ll}][\\p{Ll}]+)*[^\\S\\n]+\\d{2,4}[a-zA-Z]?)[^\\S\\n]*$/gmu;\n\n/**\n * In the header zone (top 15%), find standalone lines\n * matching \"[Uppercase word(s)] [number]\" that sit\n * between other detected entities. These are almost\n * certainly street addresses in party definitions.\n *\n * Example: \"Evropská 710\" on its own line between\n * an organization entity and a postal code entity.\n */\nexport const detectOrphanStreetLines = (\n fullText: string,\n existingEntities: Entity[],\n): Entity[] => {\n const headerEnd = Math.floor(fullText.length * HEADER_ZONE_FRACTION);\n const contextEntities = existingEntities.filter(\n (entity) => !(entity.label === \"address\" && isCallerOwnedEntity(entity)),\n );\n const results: Entity[] = [];\n ORPHAN_STREET_RE.lastIndex = 0;\n\n for (\n let m = ORPHAN_STREET_RE.exec(fullText);\n m !== null;\n m = ORPHAN_STREET_RE.exec(fullText)\n ) {\n const captured = m[1];\n if (captured === undefined) {\n continue;\n }\n const start = m.index + m[0].indexOf(captured);\n const end = start + captured.length;\n\n // Only in header zone\n if (start >= headerEnd) {\n continue;\n }\n\n // Skip if already covered\n if (existingEntities.some((e) => e.start <= start && e.end >= end)) {\n continue;\n }\n\n // Must have a nearby entity (within 200 chars)\n const hasContext = contextEntities.some(\n (e) => Math.abs(e.start - end) < 200 || Math.abs(e.end - start) < 200,\n );\n if (!hasContext) {\n continue;\n }\n\n results.push({\n start,\n end,\n label: \"address\",\n text: captured,\n score: 0.85,\n source: \"regex\",\n });\n }\n\n return results;\n};\n","import type { Entity } from \"../types\";\nimport type { PipelineContext } from \"../context\";\nimport { defaultContext } from \"../context\";\n\n// ── Zone types ───────────────────────────────────\n\nexport type DocumentZone = \"header\" | \"signature\" | \"body\" | \"table\";\n\nexport type ZoneSpan = {\n zone: DocumentZone;\n start: number;\n end: number;\n};\n\n// ── Score adjustments per zone ───────────────────\n\n/**\n * Additive score adjustments per document zone.\n * Header and signature blocks are dense with PII;\n * tables often contain structured identifying data.\n */\nexport const ZONE_SCORE_ADJUSTMENTS = {\n header: 0.1,\n signature: 0.15,\n body: 0,\n table: 0.05,\n} as const satisfies Record<DocumentZone, number>;\n\n// ── Lazy-loaded config ───────────────────────────\n\ntype SectionHeadingsConfig = {\n patterns: Array<{ re: string; flags: string }>;\n};\n\ntype SigningClauseConfig = {\n patterns: Array<{\n lang: string;\n prefix: string;\n suffix: string;\n prepositions: string[];\n }>;\n};\n\nconst loadSectionHeadings = async (): Promise<RegExp[]> => {\n const mod = await import(\"../data/section-headings.json\");\n const data: SectionHeadingsConfig = mod.default ?? mod;\n return data.patterns.map((p) => new RegExp(p.re, p.flags));\n};\n\nconst loadSigningClauses = async (): Promise<RegExp[]> => {\n const mod = await import(\"../data/signing-clauses.json\");\n const data: SigningClauseConfig = mod.default ?? mod;\n return data.patterns.map((p) => {\n // Build a pattern that matches the signing\n // clause prefix at the start of a line.\n // Note: prefix/suffix are regex fragments by\n // design (e.g. `(?:V|Ve)\\\\s+`), not literals.\n const prefix = p.prefix || \"\";\n const suffix = p.suffix || \"\";\n // Include prepositions so multi-word place\n // names like \"Ústí nad Labem\" are matched,\n // consistent with buildSigningClausePatterns\n // in detectors/regex.ts.\n const prepAlt = p.prepositions.length > 0 ? p.prepositions.join(\"|\") : null;\n const place = prepAlt\n ? `\\\\p{Lu}\\\\p{Ll}+` +\n `(?:\\\\s+(?:${prepAlt})` +\n `\\\\s+\\\\p{Lu}\\\\p{Ll}+)*` +\n `(?:\\\\s+\\\\p{Lu}\\\\p{Ll}+)*`\n : `\\\\p{Lu}\\\\p{Ll}+` + `(?:[- ]\\\\p{Lu}\\\\p{Ll}+)*`;\n // Anchor to the start of the line. Each line is\n // already split on \\n, so ^ suffices without m.\n const combined = `^\\\\s*(?:${prefix}${place}${suffix})`;\n return new RegExp(combined, \"u\");\n });\n};\n\n/**\n * Ensure config data is loaded. Call once before\n * classifyZones. Safe to call multiple times.\n */\nexport const initZoneClassifier = (\n ctx: PipelineContext = defaultContext,\n): Promise<void> => {\n if (ctx.zoneInitPromise) return ctx.zoneInitPromise;\n ctx.zoneInitPromise = Promise.all([\n loadSectionHeadings(),\n loadSigningClauses(),\n ])\n .then(([headings, clauses]) => {\n ctx.zoneHeadingPatterns = headings;\n ctx.zoneSigningPatterns = clauses;\n })\n .catch((err: unknown) => {\n // Clear cached promise so a subsequent call\n // can retry after a transient failure.\n ctx.zoneInitPromise = null;\n throw err;\n });\n return ctx.zoneInitPromise;\n};\n\n// ── Table detection ──────────────────────────────\n\nconst MIN_TABS_FOR_TABLE = 2;\n\nconst isTableLine = (line: string): boolean => {\n let tabCount = 0;\n for (const ch of line) {\n if (ch === \"\\t\") tabCount++;\n if (tabCount >= MIN_TABS_FOR_TABLE) return true;\n }\n return false;\n};\n\n// ── Zone classification ──────────────────────────\n\n/**\n * Classify a document into zones based on\n * structural heuristics. Zones are non-overlapping\n * and cover the entire text.\n *\n * Must call `initZoneClassifier()` first.\n */\nexport const classifyZones = (\n fullText: string,\n ctx: PipelineContext = defaultContext,\n): ZoneSpan[] => {\n if (fullText.length === 0) return [];\n\n const headingRes = ctx.zoneHeadingPatterns;\n const signingRes = ctx.zoneSigningPatterns;\n\n if (!headingRes || !signingRes) {\n console.warn(\n \"[anonymize] classifyZones called before \" +\n \"initZoneClassifier(); returning body-only\",\n );\n return [{ zone: \"body\", start: 0, end: fullText.length }];\n }\n\n const lines = fullText.split(\"\\n\");\n const zones: ZoneSpan[] = [];\n\n // Find header end: first section heading line\n let headerEndLine = -1;\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n if (line === undefined) continue;\n for (const re of headingRes) {\n if (re.test(line)) {\n headerEndLine = i;\n break;\n }\n }\n if (headerEndLine !== -1) break;\n }\n\n // Find signature start: last signing clause line\n let signatureStartLine = -1;\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i];\n if (line === undefined) continue;\n for (const re of signingRes) {\n if (re.test(line)) {\n signatureStartLine = i;\n break;\n }\n }\n if (signatureStartLine !== -1) break;\n }\n\n // Build offset map: line index -> char offset\n const lineOffsets: number[] = [];\n let offset = 0;\n for (const line of lines) {\n lineOffsets.push(offset);\n // +1 for the newline character\n offset += line.length + 1;\n }\n\n // Determine zone boundaries\n let headerEndOffset =\n headerEndLine >= 0 ? (lineOffsets[headerEndLine] ?? 0) : 0;\n\n const signatureStartOffset =\n signatureStartLine >= 0\n ? (lineOffsets[signatureStartLine] ?? fullText.length)\n : fullText.length;\n\n // Guard: if the signing clause appears before\n // the first section heading, the zones would\n // overlap. Treat as degenerate layout: drop the\n // header so the signature zone takes priority.\n if (\n headerEndLine > 0 &&\n signatureStartLine >= 0 &&\n headerEndOffset > signatureStartOffset\n ) {\n headerEndLine = -1;\n headerEndOffset = 0;\n }\n\n // Add header zone if detected. Using > 0 (not\n // >= 0) intentionally: when the section heading is\n // on line 0, there is no preamble to classify as a\n // header — everything starts as body.\n if (headerEndLine > 0) {\n zones.push({\n zone: \"header\",\n start: 0,\n end: headerEndOffset,\n });\n }\n\n // Scan body region for table zones\n const bodyStart = headerEndLine > 0 ? headerEndOffset : 0;\n const bodyEnd =\n signatureStartLine >= 0 ? signatureStartOffset : fullText.length;\n\n let tableStart = -1;\n for (\n let i = Math.max(headerEndLine, 0);\n i < (signatureStartLine >= 0 ? signatureStartLine : lines.length);\n i++\n ) {\n const line = lines[i];\n if (line === undefined) continue;\n const lineStart = lineOffsets[i] ?? 0;\n const lineEnd = lineStart + line.length;\n\n if (isTableLine(line)) {\n if (tableStart === -1) {\n tableStart = lineStart;\n }\n } else if (tableStart !== -1) {\n zones.push({\n zone: \"table\",\n start: tableStart,\n end: lineStart,\n });\n tableStart = -1;\n }\n\n // Close table at the end of body range\n if (\n i ===\n (signatureStartLine >= 0 ? signatureStartLine - 1 : lines.length - 1) &&\n tableStart !== -1\n ) {\n zones.push({\n zone: \"table\",\n start: tableStart,\n end: Math.min(lineEnd + 1, bodyEnd),\n });\n tableStart = -1;\n }\n }\n\n // Fill body gaps between header/table/signature\n const sortedSpecial = zones.toSorted((a, b) => a.start - b.start);\n\n let cursor = bodyStart;\n const bodyZones: ZoneSpan[] = [];\n for (const span of sortedSpecial) {\n if (span.zone === \"header\") continue;\n if (span.start > cursor) {\n bodyZones.push({\n zone: \"body\",\n start: cursor,\n end: span.start,\n });\n }\n cursor = Math.max(cursor, span.end);\n }\n if (cursor < bodyEnd) {\n bodyZones.push({\n zone: \"body\",\n start: cursor,\n end: bodyEnd,\n });\n }\n\n for (const z of bodyZones) zones.push(z);\n\n // Add signature zone if detected\n if (signatureStartLine >= 0) {\n zones.push({\n zone: \"signature\",\n start: signatureStartOffset,\n end: fullText.length,\n });\n }\n\n return zones.toSorted((a, b) => a.start - b.start);\n};\n\n// ── Entity zone lookup ───────────────────────────\n\n/**\n * Find which zone an entity's midpoint falls in.\n * Returns \"body\" if no zone matches (defensive).\n */\nconst findZone = (midpoint: number, zones: ZoneSpan[]): DocumentZone => {\n for (const span of zones) {\n if (midpoint >= span.start && midpoint < span.end) {\n return span.zone;\n }\n }\n return \"body\";\n};\n\n/**\n * Apply zone-based score adjustments to entities.\n * Entities in header/signature/table zones get a\n * small additive boost reflecting the higher PII\n * density in those regions.\n *\n * Returns a new array; does not mutate inputs.\n */\nexport const applyZoneAdjustments = (\n entities: Entity[],\n zones: ZoneSpan[],\n): Entity[] => {\n if (zones.length === 0) {\n return entities.map((e) => ({ ...e }));\n }\n\n const result: Entity[] = [];\n for (const entity of entities) {\n const midpoint = (entity.start + entity.end) / 2;\n const zone = findZone(midpoint, zones);\n const adjustment = ZONE_SCORE_ADJUSTMENTS[zone];\n\n if (adjustment > 0) {\n result.push({\n ...entity,\n score: Math.min(1, entity.score + adjustment),\n });\n } else {\n result.push({ ...entity });\n }\n }\n return result;\n};\n","import type { Match, PatternEntry } from \"@stll/text-search\";\n\nimport { getTextSearch } from \"../search-engine\";\nimport type { Entity } from \"../types\";\n\n// ── Types ───────────────────────────────────────────\n\nexport type HotwordRule = {\n hotwords: string[];\n targetLabels: string[];\n scoreAdjustment: number;\n reclassifyTo?: string;\n proximityBefore: number;\n proximityAfter: number;\n};\n\ntype HotwordRulesConfig = {\n rules: HotwordRule[];\n};\n\n// ── Lazy-loaded state ───────────────────────────────\n\nlet rules: HotwordRule[] | null = null;\nlet search: { findIter: (text: string) => Match[] } | null = null;\n/**\n * Maps each TextSearch pattern index back to the\n * rule index that owns it, so a single AC scan\n * resolves all hotword hits to their rule.\n */\nlet patternToRule: number[] | null = null;\nlet initPromise: Promise<void> | null = null;\n\n// ── Init ────────────────────────────────────────────\n\nconst loadRules = async (): Promise<void> => {\n const mod = await import(\"../data/hotword-rules.json\");\n const data: HotwordRulesConfig = mod.default ?? mod;\n const loaded = data.rules;\n\n // Build a flat pattern list and the reverse map.\n const patterns: PatternEntry[] = [];\n const mapping: number[] = [];\n\n for (let ruleIdx = 0; ruleIdx < loaded.length; ruleIdx++) {\n const rule = loaded[ruleIdx];\n if (!rule) continue;\n for (const hw of rule.hotwords) {\n patterns.push({\n pattern: hw,\n literal: true,\n caseInsensitive: true,\n });\n mapping.push(ruleIdx);\n }\n }\n\n const builtSearch =\n patterns.length > 0\n ? new (getTextSearch())(patterns, {\n overlapStrategy: \"all\",\n caseInsensitive: true,\n wholeWords: true,\n })\n : null;\n\n // Assign all module state atomically after all\n // failable operations succeed, so a mid-flight\n // error cannot leave a half-initialized state.\n patternToRule = mapping;\n search = builtSearch;\n rules = loaded;\n};\n\n/**\n * Load hotword rules from the data package.\n * Safe to call multiple times; subsequent calls\n * are no-ops.\n */\nexport const initHotwordRules = async (): Promise<void> => {\n if (rules !== null) return;\n if (initPromise !== null) return initPromise;\n initPromise = loadRules().catch((err) => {\n // Reset so callers can retry on transient failure.\n initPromise = null;\n throw err;\n });\n return initPromise;\n};\n\n/**\n * Expand requested output labels with any source labels\n * that hotword rules may reclassify into them.\n *\n * Example: requesting only \"date of birth\" still needs\n * \"date\" candidates to survive until the hotword pass.\n * If rules are not initialized, or if no labels were\n * requested, returns the input labels unchanged.\n */\nexport const expandLabelsForHotwordRules = (\n requestedLabels: readonly string[],\n): readonly string[] => {\n if (rules === null || requestedLabels.length === 0) {\n return requestedLabels;\n }\n\n const requested = new Set(requestedLabels);\n const expanded = new Set(requestedLabels);\n\n for (const rule of rules) {\n if (rule.reclassifyTo === undefined || !requested.has(rule.reclassifyTo)) {\n continue;\n }\n for (const label of rule.targetLabels) {\n expanded.add(label);\n }\n }\n\n return [...expanded];\n};\n\n// ── Application ─────────────────────────────────────\n\n/**\n * Apply hotword context rules to detected entities.\n *\n * Scans `fullText` once with a single AC automaton\n * for all hotwords across all rules, then checks\n * proximity to each entity. Distance-decayed\n * adjustment: closer hotwords give a stronger boost.\n *\n * Returns a new array; input entities are not mutated.\n */\nexport const applyHotwordRules = (\n entities: Entity[],\n fullText: string,\n): Entity[] => {\n if (\n rules === null ||\n rules.length === 0 ||\n search === null ||\n patternToRule === null\n ) {\n return entities;\n }\n\n // Single scan for all hotword positions.\n const hits = search.findIter(fullText);\n if (hits.length === 0) return entities;\n\n // Group hits by rule index for fast lookup.\n const hitsByRule = new Map<number, Match[]>();\n for (const hit of hits) {\n const ruleIdx = patternToRule[hit.pattern];\n if (ruleIdx === undefined) continue;\n let bucket = hitsByRule.get(ruleIdx);\n if (bucket === undefined) {\n bucket = [];\n hitsByRule.set(ruleIdx, bucket);\n }\n bucket.push(hit);\n }\n\n const result: Entity[] = [];\n\n for (const entity of entities) {\n let bestAdjustment = 0;\n let bestReclassify: string | undefined;\n\n for (let ruleIdx = 0; ruleIdx < rules.length; ruleIdx++) {\n const rule = rules[ruleIdx];\n if (!rule) continue;\n\n // Check if entity label matches this rule.\n if (!rule.targetLabels.includes(entity.label)) {\n continue;\n }\n\n const ruleHits = hitsByRule.get(ruleIdx);\n if (ruleHits === undefined) continue;\n\n for (const hit of ruleHits) {\n // Asymmetric proximity check.\n // Hotword BEFORE entity: hotword end <=\n // entity start, distance = entity.start -\n // hit.end\n // Hotword AFTER entity: hotword start >=\n // entity end, distance = hit.start -\n // entity.end\n let distance: number;\n let maxDistance: number;\n\n if (hit.end <= entity.start) {\n // Hotword is before the entity.\n distance = entity.start - hit.end;\n maxDistance = rule.proximityBefore;\n } else if (hit.start >= entity.end) {\n // Hotword is after the entity.\n distance = hit.start - entity.end;\n maxDistance = rule.proximityAfter;\n } else {\n // Hotword overlaps the entity: distance 0,\n // use larger window for max.\n distance = 0;\n maxDistance = Math.max(rule.proximityBefore, rule.proximityAfter);\n }\n\n if (distance > maxDistance) continue;\n\n // Distance-decayed adjustment.\n const decay = maxDistance === 0 ? 1 : 1 - distance / maxDistance;\n const adj = rule.scoreAdjustment * decay;\n\n if (Math.abs(adj) > Math.abs(bestAdjustment)) {\n bestAdjustment = adj;\n // Only carry reclassification from boost\n // rules; a penalty winner must not silently\n // overwrite a label set by a closer positive\n // rule.\n if (adj > 0) {\n bestReclassify = rule.reclassifyTo;\n } else {\n bestReclassify = undefined;\n }\n }\n }\n }\n\n if (bestAdjustment === 0) {\n result.push(entity);\n continue;\n }\n\n const newScore = Math.min(1, Math.max(0, entity.score + bestAdjustment));\n const newLabel =\n bestReclassify !== undefined ? bestReclassify : entity.label;\n\n result.push({\n ...entity,\n score: newScore,\n label: newLabel,\n });\n }\n\n return result;\n};\n","import type { Entity } from \"../types\";\nimport { DETECTION_SOURCES } from \"../types\";\n\n/** Max gap (in chars) between entities to merge. */\nconst MAX_GAP = 3;\n\nconst hasLockedBoundary = (entity: Entity): boolean =>\n entity.sourceDetail === \"custom-deny-list\" ||\n entity.sourceDetail === \"custom-regex\";\n\nconst hasDetectorLockedBoundary = (entity: Entity): boolean =>\n entity.label === \"phone number\" &&\n entity.source === DETECTION_SOURCES.TRIGGER;\n\n/**\n * Characters allowed in the gap between two adjacent\n * same-label entities that should be merged: spaces,\n * tabs, commas, and hyphens. Uses `[ \\t,-]` instead\n * of `\\s` to avoid merging entities across newlines.\n */\nconst GAP_PATTERN = /^[ \\t,-]+$/;\n\nconst isLegalFormOrganization = (entity: Entity): boolean =>\n entity.label === \"organization\" &&\n entity.source === DETECTION_SOURCES.LEGAL_FORM;\n\n/**\n * Build a set of word boundary offsets for the full\n * text using `Intl.Segmenter`. Returns a sorted array\n * of offsets where words start and end.\n */\nconst buildWordBoundaries = (text: string): Set<number> => {\n // Use \"und\" (undetermined) locale for consistent\n // word-boundary results across environments.\n const segmenter = new Intl.Segmenter(\"und\", {\n granularity: \"word\",\n });\n const boundaries = new Set<number>();\n for (const seg of segmenter.segment(text)) {\n if (!seg.isWordLike) continue;\n boundaries.add(seg.index);\n boundaries.add(seg.index + seg.segment.length);\n }\n return boundaries;\n};\n\n/**\n * Characters that act as hard stops when scanning\n * backward for a word boundary. Entity boundaries\n * should never extend past these.\n */\nconst WORD_START_STOPS = new Set([\n \"\\n\",\n \"\\r\",\n \",\",\n \";\",\n \"(\",\n \")\",\n \"[\",\n \"]\",\n \"&\",\n]);\n\n/**\n * Find the word-start offset at or before `pos`.\n * Scans left until a word boundary is found.\n */\nconst wordStartAt = (\n pos: number,\n boundaries: Set<number>,\n text: string,\n): number => {\n let p = pos;\n while (p > 0 && !boundaries.has(p)) {\n const prev = text[p - 1];\n if (prev !== undefined && WORD_START_STOPS.has(prev)) {\n return p;\n }\n p--;\n }\n return p;\n};\n\n/**\n * Characters that act as hard stops when scanning\n * forward for a word boundary. Entity boundaries\n * should never extend past these.\n */\nconst WORD_END_STOPS = new Set([\n \"\\n\",\n \"\\r\",\n \",\",\n \";\",\n \".\",\n \"(\",\n \")\",\n \"[\",\n \"]\",\n \"&\",\n]);\n\n/**\n * Find the word-end offset at or after `pos`.\n * Scans right until a word boundary is found.\n */\nconst wordEndAt = (\n pos: number,\n boundaries: Set<number>,\n text: string,\n): number => {\n let p = pos;\n while (p < text.length && !boundaries.has(p)) {\n const ch = text[p];\n if (ch !== undefined && WORD_END_STOPS.has(ch)) {\n return p;\n }\n p++;\n }\n return p;\n};\n\n/**\n * Binary search: find the leftmost index in `arr`\n * where `arr[index].start >= value`.\n */\nconst lowerBound = (arr: Entity[], value: number): number => {\n let lo = 0;\n let hi = arr.length;\n while (lo < hi) {\n const mid = (lo + hi) >>> 1;\n const el = arr[mid];\n if (el && el.start < value) {\n lo = mid + 1;\n } else {\n hi = mid;\n }\n }\n return lo;\n};\n\n/**\n * Merge adjacent same-label entities separated only by\n * whitespace, comma, or hyphen (max 3 chars). Also\n * merges same-label entities that partially overlap\n * (which can happen after word-boundary expansion).\n *\n * Looks for the last same-label entity in the result\n * (not just the very last entity) so that an\n * intervening different-label entity does not prevent\n * merging.\n *\n * Uses binary search for the `gapOccupied` check and\n * a Map for O(1) same-label prev lookup. O(n log n).\n */\nconst mergeAdjacent = (entities: Entity[], fullText: string): Entity[] => {\n const sorted = entities.toSorted((a, b) => a.start - b.start);\n const result: Entity[] = [];\n // O(1) lookup for the last same-label entity in\n // result, replacing the O(n) backward scan.\n const lastByLabel = new Map<string, Entity>();\n\n for (const entity of sorted) {\n if (hasLockedBoundary(entity)) {\n const copy = { ...entity };\n result.push(copy);\n continue;\n }\n\n const prev = lastByLabel.get(entity.label);\n\n if (!prev) {\n const copy = { ...entity };\n result.push(copy);\n lastByLabel.set(entity.label, copy);\n continue;\n }\n\n // Handle overlap created by fixPartialWords:\n // two same-label entities may now partially overlap\n // after word-boundary expansion.\n if (!hasLockedBoundary(prev) && entity.start < prev.end) {\n prev.end = Math.max(prev.end, entity.end);\n prev.text = fullText.slice(prev.start, prev.end);\n prev.score = Math.max(prev.score, entity.score);\n continue;\n }\n\n const gap = fullText.slice(prev.end, entity.start);\n // GAP_PATTERN uses `+` quantifier, so empty gaps\n // (zero-gap / touching entities) won't match.\n // Also reject merging when a different-label entity\n // occupies the gap range (would create cross-label\n // overlap). Use binary search to find candidates\n // in the gap range instead of scanning all entities.\n const gapStart = prev.end;\n const gapEnd = entity.start;\n const searchIdx = lowerBound(sorted, gapStart);\n let gapOccupied = false;\n for (let k = searchIdx; k < sorted.length; k++) {\n const other = sorted[k];\n if (!other || other.start >= gapEnd) break;\n if (other.label !== entity.label && other.end > gapStart) {\n gapOccupied = true;\n break;\n }\n }\n\n // Touching entities (entity.start === prev.end) leave a zero-\n // length gap that GAP_PATTERN's `+` quantifier refuses to match;\n // the two would stay split even though they sit flush against\n // each other. `cs-address-psc` emits a leading-space variant of\n // the postal code that ends up flush with the preceding street\n // address (`Kamínky 302/16, Brno` + ` 634 00`); treat them as\n // mergeable.\n const isMergeableGap =\n gap.length === 0 || (gap.length <= MAX_GAP && GAP_PATTERN.test(gap));\n if (\n !hasLockedBoundary(prev) &&\n !(\n // A legal-form match has a precise regex-bounded\n // extent; never extend it across a comma into a\n // sibling org span, even if that sibling came\n // from a probabilistic source. This stops list\n // items like \"Morgan Stanley & Co. LLC, Bank of\n // America Merrill Lynch\" from collapsing into a\n // single span just because the deny-list detected\n // \"Bank of America\" next door.\n (\n (isLegalFormOrganization(prev) || isLegalFormOrganization(entity)) &&\n gap.includes(\",\")\n )\n ) &&\n // Country entities are atomic; never merge two\n // countries across a separator. \"USA, Czechia, and\n // Mexico\" must stay three distinct spans, not one.\n entity.label !== \"country\" &&\n !gapOccupied &&\n isMergeableGap\n ) {\n // Merge into prev\n prev.end = entity.end;\n prev.text = fullText.slice(prev.start, prev.end);\n prev.score = Math.max(prev.score, entity.score);\n } else {\n const copy = { ...entity };\n result.push(copy);\n lastByLabel.set(entity.label, copy);\n }\n }\n\n return result;\n};\n\n/**\n * Fix partial-word boundaries by extending entity\n * start/end to the nearest word boundary. Does not\n * extend across newlines or into spans occupied by\n * different-label entities.\n *\n * Uses binary search to skip irrelevant entries when\n * clamping at cross-label neighbors. O(n log n) in\n * the common case; O(n^2) worst case when many\n * same-label entities precede a cross-label boundary.\n */\nconst fixPartialWords = (entities: Entity[], fullText: string): Entity[] => {\n const boundaries = buildWordBoundaries(fullText);\n const sorted = entities.toSorted((a, b) => a.start - b.start);\n\n // Build a secondary array sorted by end position\n // for efficient \"nearest entity ending before me\"\n // lookups. Each entry tracks the original entity.\n const byEnd = sorted\n .map((e, idx) => ({ entity: e, idx }))\n .sort((a, b) => a.entity.end - b.entity.end);\n const endPositions = byEnd.map((x) => x.entity.end);\n\n return sorted.map((e, eIdx) => {\n if (hasLockedBoundary(e) || hasDetectorLockedBoundary(e)) {\n return e;\n }\n // The entity's stored `text` is its source-of-truth display;\n // an upstream collapse pass (e.g. monetary `273,- Kč` →\n // `273,- Kč`) intentionally divorces `text` from\n // `fullText.slice(start, end)`. Letting `wordStartAt`/`wordEndAt`\n // recompute the span here would overwrite the entity text with\n // the raw slice and lose the collapsed-out characters. Treat\n // such entities as having locked boundaries — their detector\n // already chose the right span.\n if (e.text !== fullText.slice(e.start, e.end)) {\n return e;\n }\n\n let newStart = wordStartAt(e.start, boundaries, fullText);\n let newEnd = wordEndAt(e.end, boundaries, fullText);\n\n // Clamp start: find different-label entities whose\n // end is in (newStart, e.start]. We search byEnd\n // for entities with end > newStart and end <= e.start.\n // Binary search for the first entry with\n // end > newStart.\n let lo = 0;\n let hi = endPositions.length;\n while (lo < hi) {\n const mid = (lo + hi) >>> 1;\n if ((endPositions[mid] ?? Number.POSITIVE_INFINITY) <= newStart) {\n lo = mid + 1;\n } else {\n hi = mid;\n }\n }\n // Scan forward from lo; all entries have end >\n // newStart. Stop when end > e.start.\n for (let k = lo; k < byEnd.length; k++) {\n const entry = byEnd[k];\n if (!entry || entry.entity.end > e.start) break;\n if (entry.idx === eIdx) continue;\n if (entry.entity.label === e.label) continue;\n // This entity's end is in (newStart, e.start]\n // and has a different label: clamp.\n newStart = Math.max(newStart, entry.entity.end);\n }\n\n // Clamp end: find different-label entities whose\n // start is in [e.end, newEnd). Use the start-sorted\n // array with binary search.\n const startIdx = lowerBound(sorted, e.end);\n for (let k = startIdx; k < sorted.length; k++) {\n const other = sorted[k];\n if (!other || other.start >= newEnd) break;\n if (other === e) continue;\n if (other.label === e.label) continue;\n // This entity's start is in [e.end, newEnd)\n // and has a different label: clamp.\n newEnd = Math.min(newEnd, other.start);\n }\n\n if (newStart === e.start && newEnd === e.end) {\n return e;\n }\n return {\n ...e,\n start: newStart,\n end: newEnd,\n text: fullText.slice(newStart, newEnd),\n };\n });\n};\n\n/**\n * Deduplicate entities with identical [start, end, label].\n * Keeps the entry with the highest score.\n */\nconst deduplicateSpans = (entities: Entity[]): Entity[] => {\n const seen = new Map<string, Entity>();\n for (const entity of entities) {\n const key = `${entity.start}:${entity.end}:${entity.label}`;\n const existing = seen.get(key);\n if (!existing || entity.score > existing.score) {\n seen.set(key, entity);\n }\n }\n return [...seen.values()];\n};\n\n/**\n * Remove nested same-label entities. If a shorter\n * entity is fully contained within a longer entity\n * of the same label, drop the shorter one.\n *\n * Uses a \"max end seen\" sweep per label. O(n) after\n * the initial sort.\n */\nconst removeNestedSameLabel = (entities: Entity[]): Entity[] => {\n // Sort by start asc, then by length desc so the\n // longer entity comes first.\n const sorted = entities.toSorted((a, b) => {\n if (a.start !== b.start) return a.start - b.start;\n return b.end - a.end;\n });\n\n const result: Entity[] = [];\n // Track the furthest end seen per label. Any entity\n // whose end <= maxEnd for its label is nested inside\n // a previously seen entity of the same label.\n const maxEndByLabel = new Map<string, number>();\n\n for (const entity of sorted) {\n const maxEnd = maxEndByLabel.get(entity.label);\n if (maxEnd !== undefined && entity.end <= maxEnd) {\n // Nested inside a same-label entity: skip.\n continue;\n }\n maxEndByLabel.set(entity.label, entity.end);\n result.push(entity);\n }\n\n return result;\n};\n\n/**\n * Resolve cross-label overlaps that can arise when\n * `fixPartialWords` independently expands two\n * different-label entities toward the same word\n * boundary. The entity with the higher score (or\n * longer span on tie) keeps its boundary; the other\n * is trimmed so the overlap disappears.\n *\n * Preserved existing structure: sorted + early break\n * already gives good amortized behavior. O(n^2)\n * worst case but rare in practice.\n */\nconst resolveCrossLabelOverlaps = (\n entities: Entity[],\n fullText: string,\n): Entity[] => {\n const sorted = entities\n .map((e) => ({ ...e }))\n .sort((a, b) => a.start - b.start);\n\n for (let i = 0; i < sorted.length; i++) {\n for (let j = i + 1; j < sorted.length; j++) {\n const a = sorted[i];\n const b = sorted[j];\n if (!a || !b) continue;\n if (b.start >= a.end) break; // no overlap\n if (a.label === b.label) continue;\n\n // Skip full containment (one entity fully\n // inside another). Cross-label nesting is\n // valid and should be preserved.\n const aContainsB = a.start <= b.start && a.end >= b.end;\n const bContainsA = b.start <= a.start && b.end >= a.end;\n if (aContainsB || bContainsA) continue;\n\n // Partial overlap. Higher score wins; on tie\n // the longer span wins.\n const aLen = a.end - a.start;\n const bLen = b.end - b.start;\n const aLocked = hasLockedBoundary(a);\n const bLocked = hasLockedBoundary(b);\n const aWins =\n aLocked !== bLocked\n ? aLocked\n : a.score > b.score || (a.score === b.score && aLen >= bLen);\n\n if (aWins) {\n // Trim b's start to a's end\n b.start = a.end;\n b.text = fullText.slice(b.start, b.end);\n } else {\n // Trim a's end to b's start. Because the\n // array is sorted by start and a.end can\n // only decrease, all remaining j will have\n // b.start >= a.end so the break fires\n // immediately.\n a.end = b.start;\n a.text = fullText.slice(a.start, a.end);\n }\n }\n }\n\n // Drop any entity that was trimmed to zero width.\n return sorted.filter((e) => e.start < e.end);\n};\n\n/**\n * Post-processing pass for entity boundary consistency.\n * Runs after mergeAndDedup, before false-positive\n * filtering.\n *\n * 1. Fix partial-word boundaries (respects cross-label\n * neighbors to avoid introducing new overlaps)\n * 2. Resolve any remaining cross-label overlaps\n * 3. Deduplicate identical [start, end, label] spans\n * 4. Merge adjacent same-label entities (catches any\n * new adjacency/overlap from step 1)\n * 5. Remove nested same-label entities\n */\nexport const enforceBoundaryConsistency = (\n entities: Entity[],\n fullText: string,\n): Entity[] => {\n const fixed = fixPartialWords(entities, fullText);\n const resolved = resolveCrossLabelOverlaps(fixed, fullText);\n const deduped = deduplicateSpans(resolved);\n const merged = mergeAdjacent(deduped, fullText);\n return removeNestedSameLabel(merged);\n};\n","/**\n * Build the unified search instances from all\n * detector pattern sources.\n *\n * Two TextSearch instances (not one) to avoid\n * 200K per-pattern object allocations:\n * 1. regex + triggers + legal-forms (mixed, ~140\n * patterns, caseInsensitive for trigger AC)\n * 2. deny-list + street-types + gazetteer\n * (caseInsensitive, overlap \"all\";\n * deny-list/street-type use per-pattern\n * wholeWords: true; gazetteer exact use\n * wholeWords: false; gazetteer fuzzy use\n * distance: 2 via @stll/fuzzy-search)\n *\n * All patterns are PatternEntry objects with\n * per-pattern literal/wholeWords settings.\n */\n\nimport type { PatternEntry, TextSearch } from \"@stll/text-search\";\n\nimport { getTextSearch } from \"./search-engine\";\n\nimport {\n isLegalFormsEnabled,\n type GazetteerEntry,\n type PipelineConfig,\n} from \"./types\";\nimport type { RegexMeta } from \"./detectors/regex\";\nimport type { TriggerRule } from \"./types\";\nimport type { DenyListData } from \"./detectors/deny-list\";\nimport type { PipelineContext } from \"./context\";\nimport { defaultContext } from \"./context\";\n\nimport {\n REGEX_PATTERNS,\n REGEX_META,\n getCurrencyPatternEntries,\n CURRENCY_PATTERN_META,\n getDatePatterns,\n DATE_PATTERN_META,\n getSigningClausePatterns,\n SIGNING_CLAUSE_META,\n} from \"./detectors/regex\";\nimport { buildTriggerPatterns } from \"./detectors/triggers\";\nimport { buildDenyList } from \"./detectors/deny-list\";\nimport { buildStreetTypePatterns } from \"./detectors/address-seeds\";\nimport { buildGazetteerPatterns } from \"./detectors/gazetteer\";\nimport { buildCountryPatterns, type CountryData } from \"./detectors/countries\";\nimport { expandLabelsForHotwordRules } from \"./filters/hotword-rules\";\n\nconst DEFAULT_CUSTOM_REGEX_SCORE = 0.9;\nconst ALNUM_RE = /[\\p{L}\\p{N}]/u;\n\ntype PatternSlice = {\n start: number;\n end: number;\n};\n\nconst createAllowedLabelSet = (\n labels: readonly string[],\n): ReadonlySet<string> | null => (labels.length > 0 ? new Set(labels) : null);\n\nconst labelIsAllowed = (\n label: string,\n allowedLabels: ReadonlySet<string> | null,\n): boolean => allowedLabels === null || allowedLabels.has(label);\n\nexport type GazetteerData = {\n /** Maps local pattern index to entry label. */\n labels: string[];\n /**\n * Whether each pattern is fuzzy (distance > 0).\n * Used by the post-processor to assign scores.\n */\n isFuzzy: boolean[];\n};\n\nexport type UnifiedSearchInstance = {\n /** Regex + triggers + legal-forms. */\n tsRegex: TextSearch;\n /** Caller-owned custom regexes, isolated for overlap preservation. */\n tsCustomRegex: TextSearch;\n /** Deny-list + street-types + gazetteer. */\n tsLiterals: TextSearch;\n slices: {\n regex: PatternSlice;\n customRegex: PatternSlice;\n legalForms: PatternSlice;\n triggers: PatternSlice;\n denyList: PatternSlice;\n streetTypes: PatternSlice;\n gazetteer: PatternSlice;\n countries: PatternSlice;\n };\n regexMeta: readonly RegexMeta[];\n customRegexMeta: readonly RegexMeta[];\n triggerRules: readonly TriggerRule[];\n denyListData: DenyListData | null;\n gazetteerData: GazetteerData | null;\n countryData: CountryData | null;\n};\n\nexport const buildUnifiedSearch = async (\n config: PipelineConfig,\n gazetteerEntries: GazetteerEntry[] = [],\n ctx: PipelineContext = defaultContext,\n): Promise<UnifiedSearchInstance> => {\n const legalFormsEnabled = isLegalFormsEnabled(config);\n const searchLabels =\n config.enableHotwordRules === true\n ? expandLabelsForHotwordRules(config.labels)\n : config.labels;\n const allowedLabels = createAllowedLabelSet(searchLabels);\n const customRegexes = config.enableRegex\n ? (config.customRegexes ?? []).filter((entry) =>\n labelIsAllowed(entry.label, allowedLabels),\n )\n : [];\n // Legal-form detection lives in `detectors/legal-forms-v2.ts`\n // as an AC suffix pass + TS-side validator; the unified search\n // no longer carries legal-form regex patterns. `legalFormsEnabled`\n // still gates whether the v2 detector runs in the pipeline, but\n // its pattern slice is always empty.\n const [\n triggers,\n denyListData,\n streetTypes,\n currencyPatterns,\n datePatterns,\n signingPatterns,\n ] = await Promise.all([\n config.enableTriggerPhrases\n ? buildTriggerPatterns()\n : Promise.resolve({\n patterns: [] as string[],\n rules: [] as TriggerRule[],\n }),\n config.enableDenyList ? buildDenyList(config, ctx) : Promise.resolve(null),\n buildStreetTypePatterns(),\n config.enableRegex && labelIsAllowed(\"monetary amount\", allowedLabels)\n ? getCurrencyPatternEntries()\n : Promise.resolve([] as PatternEntry[]),\n config.enableRegex && labelIsAllowed(\"date\", allowedLabels)\n ? getDatePatterns()\n : Promise.resolve([] as string[]),\n config.enableRegex && labelIsAllowed(\"address\", allowedLabels)\n ? getSigningClausePatterns()\n : Promise.resolve([] as string[]),\n ]);\n // Read but never populated: the legal-form slice in the unified\n // search is permanently empty after the v2 rewrite. Tracking it\n // here as a 0-length slice keeps the downstream slice math\n // (start/end offsets for the regex meta) compatible with code\n // that hasn't migrated to v2-aware indexing yet.\n const legalForms: readonly string[] = [];\n void legalFormsEnabled;\n\n // ── Instance 1: regex + triggers + legal-forms ──\n // Trigger patterns are lowercased strings with\n // caseInsensitive on the AC. Regex patterns have\n // their own (?i) flags (caseInsensitive on AC\n // is ignored for regex since they route to\n // RegexSet). Legal-form patterns are regex too.\n //\n // Currency patterns (from currencies.json) are\n // appended after the static regex patterns; their\n // meta is spliced into regexMeta at the same offset.\n const allRegex: PatternEntry[] = [];\n const regexMeta: RegexMeta[] = [];\n if (config.enableRegex) {\n for (const [index, pattern] of REGEX_PATTERNS.entries()) {\n const meta = REGEX_META[index];\n if (!meta || !labelIsAllowed(meta.label, allowedLabels)) {\n continue;\n }\n allRegex.push(pattern);\n regexMeta.push(meta);\n }\n }\n for (const pattern of currencyPatterns) {\n allRegex.push(pattern);\n regexMeta.push(CURRENCY_PATTERN_META);\n }\n for (const pattern of datePatterns) {\n allRegex.push(pattern);\n regexMeta.push(DATE_PATTERN_META);\n }\n for (const pattern of signingPatterns) {\n allRegex.push(pattern);\n regexMeta.push(SIGNING_CLAUSE_META);\n }\n const customRegexMeta: RegexMeta[] = customRegexes.map((entry) => ({\n label: entry.label,\n score: entry.score ?? DEFAULT_CUSTOM_REGEX_SCORE,\n sourceDetail: \"custom-regex\" as const,\n }));\n\n let offset = 0;\n\n const regexSlice = {\n start: offset,\n end: offset + allRegex.length,\n };\n offset = regexSlice.end;\n\n const customRegexSlice = {\n start: 0,\n end: customRegexes.length,\n };\n\n const legalFormsSlice = {\n start: offset,\n end: offset + legalForms.length,\n };\n offset = legalFormsSlice.end;\n\n const triggersSlice = {\n start: offset,\n end: offset + triggers.patterns.length,\n };\n\n // Trigger patterns need caseInsensitive on AC\n // (only ~120 objects, not 200K). Regex/legal-form\n // patterns are bare strings (auto-classified).\n const triggerEntries = triggers.patterns.map((p) => ({\n pattern: p,\n literal: true as const,\n caseInsensitive: true,\n }));\n\n const regexAllPatterns = [...allRegex, ...legalForms, ...triggerEntries];\n\n // TextSearch uses static complexity routing for\n // regex patterns: common regexes share bounded\n // chunks, while high-risk patterns are isolated.\n const tsRegex = new (getTextSearch())(regexAllPatterns);\n const tsCustomRegex = new (getTextSearch())(\n customRegexes.map((entry) => entry.pattern),\n {\n overlapStrategy: \"all\",\n },\n );\n\n // ── Instance 2: deny-list + street-types + gaz ──\n // Deny-list and street-type patterns are plain\n // strings (allLiteral). Gazetteer adds exact\n // literals plus fuzzy PatternEntry objects for\n // terms >= 4 chars.\n offset = 0;\n\n const denyListOriginals = denyListData?.originals ?? [];\n const denyListSlice = {\n start: offset,\n end: offset + denyListOriginals.length,\n };\n offset = denyListSlice.end;\n\n const streetTypesSlice = {\n start: offset,\n end: offset + streetTypes.length,\n };\n offset = streetTypesSlice.end;\n\n // Gazetteer patterns (exact + fuzzy)\n const gazResult =\n config.enableGazetteer && gazetteerEntries.length > 0\n ? buildGazetteerPatterns(gazetteerEntries)\n : null;\n\n const gazetteerSlice = {\n start: offset,\n end: offset + (gazResult?.patterns.length ?? 0),\n };\n offset = gazetteerSlice.end;\n\n // Country patterns: ISO 3166-1 names, curated aliases,\n // alpha-3 codes. Literal + case-insensitive + whole-word.\n const countryResult =\n config.enableCountries === false ||\n !labelIsAllowed(\"country\", allowedLabels)\n ? null\n : buildCountryPatterns();\n\n const countriesSlice = {\n start: offset,\n end: offset + (countryResult?.patterns.length ?? 0),\n };\n\n // Build the combined pattern array.\n // Deny-list and street-type patterns use\n // per-pattern wholeWords: true (they are\n // known tokens). Gazetteer exact patterns\n // already set wholeWords: false in\n // buildGazetteerPatterns. The global\n // wholeWords is false so fuzzy patterns\n // (which don't support per-pattern override)\n // match without word-boundary constraints.\n const wrapWholeWord = (s: string, wholeWords: boolean): PatternEntry => ({\n pattern: s,\n literal: true as const,\n wholeWords,\n });\n const customDenyListNeedsWholeWords = (pattern: string): boolean => {\n const first = pattern.at(0) ?? \"\";\n const last = pattern.at(-1) ?? \"\";\n return ALNUM_RE.test(first) && ALNUM_RE.test(last);\n };\n const literalPatternText = (entry: PatternEntry): string => {\n if (typeof entry === \"string\") return entry;\n if (entry instanceof RegExp) {\n throw new Error(\"Expected literal country pattern, got RegExp\");\n }\n if (entry.pattern instanceof RegExp) {\n throw new Error(\"Expected literal country pattern, got RegExp entry\");\n }\n return entry.pattern;\n };\n const hasCustomDenyListPatterns =\n denyListData?.sources.some((sources) =>\n sources.includes(\"custom-deny-list\"),\n ) ?? false;\n const canUseGlobalWholeWordLiterals =\n !hasCustomDenyListPatterns && gazResult === null;\n const literalAllPatterns: PatternEntry[] | string[] =\n canUseGlobalWholeWordLiterals\n ? [\n ...denyListOriginals,\n ...streetTypes,\n ...(countryResult?.patterns.map(literalPatternText) ?? []),\n ]\n : [\n ...denyListOriginals.map((pattern, index) =>\n wrapWholeWord(\n pattern,\n (denyListData?.sources[index] ?? []).includes(\"custom-deny-list\")\n ? customDenyListNeedsWholeWords(pattern)\n : true,\n ),\n ),\n ...streetTypes.map((pattern) => wrapWholeWord(pattern, true)),\n ...(gazResult?.patterns ?? []),\n ...(countryResult?.patterns ?? []),\n ];\n\n const tsLiterals =\n literalAllPatterns.length > 0\n ? new (getTextSearch())(literalAllPatterns, {\n ...(canUseGlobalWholeWordLiterals\n ? { allLiteral: true, wholeWords: true }\n : {}),\n caseInsensitive: true,\n overlapStrategy: \"all\",\n })\n : new (getTextSearch())([]);\n\n return {\n tsRegex,\n tsCustomRegex,\n tsLiterals,\n slices: {\n regex: regexSlice,\n customRegex: customRegexSlice,\n legalForms: legalFormsSlice,\n triggers: triggersSlice,\n denyList: denyListSlice,\n streetTypes: streetTypesSlice,\n gazetteer: gazetteerSlice,\n countries: countriesSlice,\n },\n regexMeta,\n customRegexMeta,\n triggerRules: triggers.rules,\n denyListData,\n gazetteerData: gazResult?.data ?? null,\n countryData: countryResult?.data ?? null,\n };\n};\n","/**\n * Run the two-instance unified search and return\n * raw matches. Two passes instead of six:\n * 1. regex + triggers + legal-forms\n * 2. deny-list + street-types (normalized text)\n */\n\nimport type { Match } from \"@stll/text-search\";\nimport type { UnifiedSearchInstance } from \"./build-unified-search\";\nimport { normalizeForSearch } from \"./util/normalize\";\n\nexport type UnifiedResult = {\n /** All matches from both instances combined. */\n regexMatches: Match[];\n customRegexMatches: Match[];\n literalMatches: Match[];\n};\n\nexport const runUnifiedSearch = (\n instance: UnifiedSearchInstance,\n fullText: string,\n): UnifiedResult => {\n // Pass 1: regex + triggers + legal-forms\n // on original text (regex patterns encode\n // their own case flags)\n const regexMatches = instance.tsRegex.findIter(fullText);\n const customRegexMatches = instance.tsCustomRegex.findIter(fullText);\n\n // Pass 2: deny-list + street-types on\n // normalized text (NBSP, smart quotes folded)\n const normalized = normalizeForSearch(fullText);\n const literalMatches = instance.tsLiterals.findIter(normalized);\n\n return { regexMatches, customRegexMatches, literalMatches };\n};\n","import type { Entity } from \"../types\";\n\nconst MASK_TOKEN = \"[MASKED]\";\nconst MASK_LEN = MASK_TOKEN.length;\n\ntype OffsetSegment = {\n /** Start of this mask token in masked text */\n maskedStart: number;\n /** End of this mask token in masked text */\n maskedEnd: number;\n /** Cumulative shift: original - masked */\n shift: number;\n /** Original start of the masked span */\n origStart: number;\n /** Original end of the masked span */\n origEnd: number;\n};\n\nexport type MaskResult = {\n maskedText: string;\n /**\n * Maps masked-text offsets back to original offsets.\n * Returns null if the span overlaps a masked region.\n */\n offsetMap: (\n maskedStart: number,\n maskedEnd: number,\n ) => { start: number; end: number } | null;\n};\n\n/**\n * Replace detected entity spans with placeholder tokens.\n * Each entity span is replaced with \"[MASKED]\" (fixed\n * length). Returns the masked text and an offset mapping\n * function.\n */\nexport const maskDetectedSpans = (\n fullText: string,\n entities: Entity[],\n): MaskResult => {\n if (entities.length === 0) {\n return {\n maskedText: fullText,\n offsetMap: (s, e) => ({ start: s, end: e }),\n };\n }\n\n // Sort by start, then longest span first\n const sorted = entities.toSorted(\n (a, b) => a.start - b.start || b.end - a.end,\n );\n\n // Merge overlapping spans so we don't double-mask.\n // Adjacent (touching) spans are kept separate so each\n // produces its own [MASKED] token, preserving token\n // boundaries for the NER model.\n const spans: { start: number; end: number }[] = [];\n const first = sorted[0];\n if (!first) {\n return {\n maskedText: fullText,\n offsetMap: (s, e) => ({ start: s, end: e }),\n };\n }\n let cur = {\n start: first.start,\n end: first.end,\n };\n for (let i = 1; i < sorted.length; i++) {\n const s = sorted[i];\n if (!s) continue;\n if (s.start < cur.end) {\n cur.end = Math.max(cur.end, s.end);\n } else {\n spans.push(cur);\n cur = { start: s.start, end: s.end };\n }\n }\n spans.push(cur);\n\n // Build masked text and offset segments\n const segments: OffsetSegment[] = [];\n const parts: string[] = [];\n let prev = 0;\n let cumulativeShift = 0;\n\n for (const span of spans) {\n parts.push(fullText.slice(prev, span.start));\n parts.push(MASK_TOKEN);\n\n const origLen = span.end - span.start;\n const delta = origLen - MASK_LEN;\n cumulativeShift += delta;\n\n // Position of mask token in masked text\n const maskedStart = span.start - (cumulativeShift - delta);\n const maskedEnd = maskedStart + MASK_LEN;\n\n segments.push({\n maskedStart,\n maskedEnd,\n shift: cumulativeShift,\n origStart: span.start,\n origEnd: span.end,\n });\n\n prev = span.end;\n }\n parts.push(fullText.slice(prev));\n\n const maskedText = parts.join(\"\");\n\n const offsetMap = (\n maskedStart: number,\n maskedEnd: number,\n ): { start: number; end: number } | null => {\n // Check if span overlaps any masked region\n for (const seg of segments) {\n if (maskedStart < seg.maskedEnd && maskedEnd > seg.maskedStart) {\n return null;\n }\n }\n\n // Find cumulative shift for this position.\n // Since the overlap check above guarantees both\n // endpoints fall within the same gap zone, a\n // single shift value applies to both.\n let shift = 0;\n for (const seg of segments) {\n if (maskedStart >= seg.maskedEnd) {\n shift = seg.shift;\n } else {\n break;\n }\n }\n\n return {\n start: maskedStart + shift,\n end: maskedEnd + shift,\n };\n };\n\n return { maskedText, offsetMap };\n};\n\n/**\n * Map NER entities from masked-text offsets back to\n * original-text offsets. Discards any NER entity whose\n * mapped span overlaps a masked (rule-detected) region.\n */\nexport const unmaskNerEntities = (\n nerEntities: Entity[],\n maskResult: MaskResult,\n fullText: string,\n): Entity[] => {\n const result: Entity[] = [];\n\n for (const ner of nerEntities) {\n // offsetMap returns null when the mapped span\n // overlaps any merged rule-entity region, so no\n // secondary overlap check is needed.\n const mapped = maskResult.offsetMap(ner.start, ner.end);\n if (mapped === null) continue;\n\n result.push({\n ...ner,\n start: mapped.start,\n end: mapped.end,\n text: fullText.slice(mapped.start, mapped.end),\n });\n }\n\n return result;\n};\n","import {\n extractDefinedTerms,\n findCoreferenceSpans,\n} from \"./detectors/coreference\";\nimport { processGazetteerMatches } from \"./detectors/gazetteer\";\nimport { processCountryMatches } from \"./detectors/countries\";\nimport { detectNameCorpus, initNameCorpus } from \"./detectors/names\";\nimport { detectSignatures } from \"./detectors/signatures\";\nimport { processRegexMatches } from \"./detectors/regex\";\nimport {\n getKnownLegalSuffixes,\n warmLegalRoleHeads,\n} from \"./detectors/legal-forms\";\nimport { detectLegalFormsV2 } from \"./detectors/legal-forms-v2\";\nimport {\n processTriggerMatches,\n warmAddressStopKeywords,\n} from \"./detectors/triggers\";\nimport {\n ensureDenyListData,\n processDenyListMatches,\n} from \"./detectors/deny-list\";\nimport { processAddressSeeds } from \"./detectors/address-seeds\";\nimport { propagateOrgNames } from \"./detectors/org-propagation\";\nimport {\n boostNearMissEntities,\n detectStreetPatternsNearAddresses,\n getStreetAbbrevs,\n detectOrphanStreetLines,\n initPrepositions,\n initStreetAbbrevs,\n} from \"./filters/confidence-boost\";\nimport {\n filterFalsePositives,\n initAddressComponents,\n loadDocumentStructureHeadings,\n loadGenericRoles,\n} from \"./filters/false-positives\";\nimport {\n applyZoneAdjustments,\n classifyZones,\n initZoneClassifier,\n type ZoneSpan,\n} from \"./filters/zone-classifier\";\nimport {\n applyHotwordRules,\n expandLabelsForHotwordRules,\n initHotwordRules,\n} from \"./filters/hotword-rules\";\nimport { enforceBoundaryConsistency } from \"./filters/boundary-consistency\";\nimport type {\n Dictionaries,\n Entity,\n GazetteerEntry,\n PipelineConfig,\n} from \"./types\";\nimport {\n DEFAULT_ENTITY_LABELS,\n DETECTION_SOURCES,\n DETECTOR_PRIORITY,\n isLegalFormsEnabled,\n} from \"./types\";\nimport {\n buildUnifiedSearch,\n type UnifiedSearchInstance,\n} from \"./build-unified-search\";\nimport { runUnifiedSearch } from \"./unified-search\";\nimport { maskDetectedSpans, unmaskNerEntities } from \"./util/entity-masking\";\nimport type { PipelineContext } from \"./context\";\nimport { defaultContext } from \"./context\";\n\n/**\n * Sources backed by curated literal dictionaries.\n * Longer matches from these sources are more specific,\n * so the containment rule trusts their length.\n */\nconst LITERAL_SOURCES: ReadonlySet<string> = new Set([\n \"deny-list\",\n \"gazetteer\",\n]);\n\nconst isCallerOwnedEntity = (entity: Entity): boolean =>\n entity.sourceDetail === \"custom-deny-list\" ||\n entity.sourceDetail === \"custom-regex\";\n\nconst hasLockedBoundary = (entity: Entity): boolean =>\n isCallerOwnedEntity(entity);\n\nconst isCoveredByDenyListEntity = (\n entity: Entity,\n denyListEntity: Entity,\n): boolean =>\n entity.label === denyListEntity.label &&\n denyListEntity.start <= entity.start &&\n denyListEntity.end >= entity.end;\n\nconst filterNameCorpusCoveredByDenyList = (\n nameCorpusEntities: readonly Entity[],\n denyListEntities: readonly Entity[],\n): Entity[] =>\n nameCorpusEntities.filter(\n (entity) =>\n !denyListEntities.some((denyListEntity) =>\n isCoveredByDenyListEntity(entity, denyListEntity),\n ),\n );\n\nconst LITERAL_BOUNDARY_PUNCT_RE = /^[\"“„‟‘‛'«]|[\"”’'»!.]$/u;\n\n// Bare postal-code shapes used by the address-containment rule.\n// Covers Czech `160 00`, German/EU `12345`, US ZIP / ZIP+4\n// (`94304`, `94304-1050`), and the standard `\\d{3} \\d{2}` /\n// `\\d{2}-\\d{3}` continental variants. Surrounding whitespace\n// is allowed so the trigger detector's trimmed span still matches.\nconst BARE_POSTAL_CODE_RE =\n /^\\s*(?:\\d{3}\\s?\\d{2}|\\d{2}[-–]\\d{3}|\\d{5}(?:[-–]\\d{3,4})?)\\s*$/u;\n\nconst hasCuratedLiteralBoundary = (entity: Entity): boolean =>\n LITERAL_SOURCES.has(entity.source) &&\n entity.label !== \"person\" &&\n entity.sourceDetail !== \"gazetteer-extension\" &&\n LITERAL_BOUNDARY_PUNCT_RE.test(entity.text);\n\nconst shouldReplace = (a: Entity, b: Entity): boolean => {\n const aLen = a.end - a.start;\n const bLen = b.end - b.start;\n const aCallerOwned = isCallerOwnedEntity(a);\n const bCallerOwned = isCallerOwnedEntity(b);\n if (aCallerOwned !== bCallerOwned) {\n return aCallerOwned;\n }\n\n // Containment: when a literal-match entity (deny-list\n // or gazetteer) fully contains a shorter entity with\n // the same label, prefer the longer one. Curated\n // dictionary entries are more specific when longer:\n // \"656 91 Brno\" (deny-list) should beat \"656 91\"\n // (regex) even though regex has higher priority.\n // Non-literal sources (trigger, regex, NER) are\n // excluded because their length does not reliably\n // indicate accuracy.\n if (\n a.label === b.label &&\n LITERAL_SOURCES.has(a.source) &&\n a.start <= b.start &&\n a.end >= b.end &&\n aLen > bLen\n ) {\n return true;\n }\n if (\n a.label === b.label &&\n LITERAL_SOURCES.has(b.source) &&\n b.start <= a.start &&\n b.end >= a.end &&\n bLen > aLen\n ) {\n return false;\n }\n\n // Postal-code inside a fuller address: when a longer same-label\n // address span fully contains a shorter span whose text is just\n // a bare postal code (Czech `160 00`, German `12345`, US `94304`\n // or `94304-1050`), the shorter span is a fragment of the same\n // data point. The longer span wins regardless of source priority.\n // Bare postal-code text without surrounding street/city evidence\n // is the only narrow case where containment beats the priority\n // comparison; field-label trims (\\`… IČ\\`, \\`… oddíl\\`) and other\n // address-vs-address comparisons stay on the standard path.\n if (\n a.label === \"address\" &&\n b.label === \"address\" &&\n a.start <= b.start &&\n a.end >= b.end &&\n aLen > bLen &&\n BARE_POSTAL_CODE_RE.test(b.text)\n ) {\n return true;\n }\n if (\n a.label === \"address\" &&\n b.label === \"address\" &&\n b.start <= a.start &&\n b.end >= a.end &&\n bLen > aLen &&\n BARE_POSTAL_CODE_RE.test(a.text)\n ) {\n return false;\n }\n\n // Legal-form containment: a v2 legal-form entity span anchors on\n // a suffix and grows back through CapWords + connectors, so its\n // length DOES reliably indicate accuracy. When such a span fully\n // contains a shorter same-label entity from a higher-priority\n // detector (typically a trigger reclassifying a city name like\n // `Prahy` inside `Technologie hlavního města Prahy, a. s.`), the\n // legal-form span wins regardless of source priority.\n if (\n a.label === b.label &&\n a.source === DETECTION_SOURCES.LEGAL_FORM &&\n a.start <= b.start &&\n a.end >= b.end &&\n aLen > bLen\n ) {\n return true;\n }\n if (\n a.label === b.label &&\n b.source === DETECTION_SOURCES.LEGAL_FORM &&\n b.start <= a.start &&\n b.end >= a.end &&\n bLen > aLen\n ) {\n return false;\n }\n\n // Same-start same-label longest-wins rule for shape-extending\n // detectors. For labels where the entity naturally extends to\n // include trailing context (a date that grows from `21.` to\n // `21. März 1968`, a monetary amount that grows from `273,-`\n // to `273,- Kč`), the longer span at the same offset is the\n // correct boundary regardless of which detector emitted it.\n // Without this rule the priority comparison below picks the\n // shorter-but-higher-priority trigger and discards the full\n // entity. The list is intentionally narrow — `address`,\n // `organization`, `person` keep the priority semantics\n // because their detectors don't have the same \"trigger\n // captures a prefix, regex captures the whole shape\"\n // relationship.\n const LONGEST_WINS_LABELS: ReadonlySet<string> = new Set([\n \"date\",\n \"date of birth\",\n \"monetary amount\",\n \"phone number\",\n \"email address\",\n \"url\",\n ]);\n if (\n a.label === b.label &&\n a.start === b.start &&\n aLen !== bLen &&\n LONGEST_WINS_LABELS.has(a.label)\n ) {\n return aLen > bLen;\n }\n\n // Cross-label containment for country: a country token\n // contained inside a longer person or organization span\n // is almost always a first-name collision (\"Chad Smith\",\n // \"Georgia Smith\", \"Jordan Williams\"). The longer span\n // carries more evidence — keep it and drop the country.\n if (\n a.label === \"country\" &&\n (b.label === \"person\" || b.label === \"organization\") &&\n b.start <= a.start &&\n b.end >= a.end &&\n bLen > aLen\n ) {\n return false;\n }\n if (\n b.label === \"country\" &&\n (a.label === \"person\" || a.label === \"organization\") &&\n a.start <= b.start &&\n a.end >= b.end &&\n aLen > bLen\n ) {\n return true;\n }\n\n const aPri = DETECTOR_PRIORITY[a.source] ?? 0;\n const bPri = DETECTOR_PRIORITY[b.source] ?? 0;\n if (aPri !== bPri) return aPri > bPri;\n return a.score > b.score || (a.score === b.score && aLen > bLen);\n};\n\n/** Labels where colons are structurally significant. */\nconst COLON_LABELS = new Set([\"ip address\", \"mac address\"]);\n\n/**\n * Labels whose entities should have a trailing sentence\n * `.` stripped during sanitisation. Restricted to\n * proper-noun-style labels where a final period is\n * almost always the sentence terminator that ran into\n * the capture, not a structural part of the value.\n * Numeric labels (`date`, `date of birth`, `phone\n * number`, `monetary amount`, `time`) and `person`\n * stay out — German writes `21. März`, post-nominal\n * degrees write `M.Sc.`, times write `5:00 p.m.`, and\n * stripping the dot would corrupt those spans.\n */\nconst PERIOD_STRIPPED_LABELS: ReadonlySet<string> = new Set([\n \"organization\",\n \"location\",\n \"address\",\n]);\nconst ADDRESS_FINAL_TOKEN_RE = /(?:^|[\\s,])([\\p{L}\\p{M}.]+\\.)$/u;\nconst LOCATION_FINAL_DOTTED_ABBREV_RE = /(?:^|[\\s,])(?:\\p{Lu}\\.){2,}$/u;\n\nconst hasKnownAddressFinalAbbrev = (text: string): boolean => {\n const finalToken = ADDRESS_FINAL_TOKEN_RE.exec(text)?.[1];\n if (!finalToken) {\n return false;\n }\n return getStreetAbbrevs().has(finalToken.toLowerCase());\n};\n\nconst hasLocationFinalAbbrev = (text: string): boolean =>\n LOCATION_FINAL_DOTTED_ABBREV_RE.test(text);\n\n/**\n * Labels whose detectors emit precise, evidence-backed spans. When\n * one of these fires at the exact same offsets as a fuzzier\n * `address` hit (city dictionary lookup, address-seed cluster), the\n * `address` label is almost always a dictionary collision — \"March\n * 15\" the date getting tagged as an address because \"March\" appears\n * in a city corpus, or \"Brent Phillips\" emitted as both `person`\n * and `address` because \"Brent\" is a UK city. The precise detector\n * wins.\n */\nconst PRECISE_OVER_ADDRESS: ReadonlySet<string> = new Set([\n \"person\",\n \"date\",\n \"date of birth\",\n \"phone number\",\n \"email address\",\n \"monetary amount\",\n \"iban\",\n \"bank account number\",\n \"tax identification number\",\n \"registration number\",\n \"identity card number\",\n \"national identification number\",\n \"passport number\",\n \"credit card number\",\n]);\n\n/**\n * Labels the `person` chain wins against at identical offsets. The\n * chain carries adjacent-name evidence (deny-list surname plus a\n * capitalised follow-up) a single-token dictionary collision does\n * not. Kept narrow: organizations are NOT here — \"Morgan Stanley\"\n * legitimately appears in both the org and name dictionaries, and\n * the existing detector priority is the right tie-breaker there.\n *\n * `country` is included because the country detector runs at the\n * same offsets as deny-list person matches for names like `Chad`,\n * `Georgia`, `Jordan` (all valid first names AND country names).\n * Letting a higher-priority country span win there would mark\n * `Chad Smith` as country + leave `Smith` unredacted.\n */\nconst PERSON_PREFERRED_OVER: ReadonlySet<string> = new Set([\n \"address\",\n \"country\",\n \"land parcel\",\n]);\n\nconst resolveSameSpanLabelConflicts = (entities: Entity[]): Entity[] => {\n if (entities.length < 2) return entities;\n const byOffsets = new Map<string, Entity[]>();\n for (const entity of entities) {\n const key = `${entity.start}:${entity.end}`;\n const list = byOffsets.get(key);\n if (list) {\n list.push(entity);\n } else {\n byOffsets.set(key, [entity]);\n }\n }\n const dropped = new Set<Entity>();\n for (const [, group] of byOffsets) {\n if (group.length < 2) continue;\n const labels = new Set(group.map((e) => e.label));\n if (labels.size < 2) continue;\n\n const hasPerson = labels.has(\"person\");\n const hasPreciseNonAddress = [...labels].some(\n (l) => l !== \"address\" && PRECISE_OVER_ADDRESS.has(l),\n );\n\n // Person-preferred drops are decided up front so the\n // priority-based pass below doesn't keep a higher-pri country\n // span (e.g., `Chad` from the country detector at priority 3)\n // while also dropping the lower-pri person hit (`Chad` from\n // the deny-list at priority 2) for being below the same group's\n // max. Without this, person tokens that happen to be country\n // names would be flipped to `country` and the surrounding\n // surname left exposed.\n const yieldingToPerson = new Set<Entity>();\n if (hasPerson) {\n for (const e of group) {\n if (hasLockedBoundary(e)) continue;\n if (PERSON_PREFERRED_OVER.has(e.label)) {\n yieldingToPerson.add(e);\n }\n }\n }\n\n // When entities at the same offsets have different labels,\n // also let detector priority break ties: a `legal-form`\n // organization hit (priority 3) should keep its label over a\n // coincident `deny-list` person hit (priority 2). Compute the\n // max priority over entities NOT already yielding to person,\n // so the person hit isn't accidentally crowded out by the\n // priority of the very entity it's beating.\n let maxPriority = -1;\n for (const e of group) {\n if (hasLockedBoundary(e)) continue;\n if (yieldingToPerson.has(e)) continue;\n const pri = DETECTOR_PRIORITY[e.source] ?? 0;\n if (pri > maxPriority) maxPriority = pri;\n }\n\n for (const e of group) {\n // Caller-owned spans (custom deny-list / custom regex) carry\n // explicit user intent; never drop them in favour of a\n // detector-generated label.\n if (hasLockedBoundary(e)) continue;\n if (yieldingToPerson.has(e)) {\n dropped.add(e);\n continue;\n }\n const pri = DETECTOR_PRIORITY[e.source] ?? 0;\n if (pri < maxPriority) {\n dropped.add(e);\n continue;\n }\n if (hasPreciseNonAddress && e.label === \"address\") {\n dropped.add(e);\n }\n }\n }\n if (dropped.size === 0) return entities;\n return entities.filter((e) => !dropped.has(e));\n};\n\n/**\n * Trailing typographic punctuation that detectors\n * occasionally swallow when a capture runs to the end\n * of a sentence or quoted phrase. Stripped from every\n * non-literal, non-locked entity. Curated dictionary and\n * gazetteer entries with punctuation that is clearly part of\n * the literal (`Hello bank!`, `\"Juez y parte\"`) keep their\n * own boundaries. Generated/extended spans from the same\n * sources still pass through cleanup so dangling punctuation\n * does not become part of the redaction\n * (e.g. `Bond Hedge Documentation\"` →\n * `Bond Hedge Documentation`).\n *\n * `)` is deliberately omitted — monetary amounts are\n * extended to include trailing \"(slovy ...)\" / \"(in\n * words ...)\" parentheticals where the closing paren\n * is structural, and stripping it would leave the open\n * paren dangling. `.` is also omitted because the\n * trailing-period rule below has label-aware handling\n * (legal-form abbreviations keep their dot).\n */\nconst TRAILING_PUNCT_CLASS = `[\"“”‘’'»!?]`;\n\n/**\n * Leading typographic punctuation that detectors\n * occasionally swallow when a capture starts at an\n * opening quote. `(` is deliberately omitted — it is\n * almost always the opening of a structural\n * parenthetical (registration number group, monetary\n * \"(slovy ...)\" extension) that the detector\n * intentionally captured.\n */\nconst LEADING_PUNCT_CLASS = `[\"“”‘’'«¿¡]`;\nconst LEADING_ELLIPSIS_RE = /^(?:\\.{2,}|…+)/u;\nconst ELLIPSIS_PREFIX_LABELS: ReadonlySet<string> = new Set([\n \"date\",\n \"date of birth\",\n \"monetary amount\",\n \"phone number\",\n \"email address\",\n \"url\",\n \"time\",\n]);\nconst STRIP_BY_LABEL = {\n colon: /[\\s,;]+/,\n default: /[\\s:,;]+/,\n} as const;\nconst LEADING_TRIM_BY_LABEL = {\n colon: new RegExp(\n `^(?:\\\\.\\\\s|${STRIP_BY_LABEL.colon.source}|${LEADING_PUNCT_CLASS})+`,\n ),\n default: new RegExp(\n `^(?:\\\\.\\\\s|${STRIP_BY_LABEL.default.source}|${LEADING_PUNCT_CLASS})+`,\n ),\n} as const;\nconst TRAILING_TRIM_BY_LABEL = {\n colon: new RegExp(\n `(?:${STRIP_BY_LABEL.colon.source}|${TRAILING_PUNCT_CLASS})+$`,\n ),\n default: new RegExp(\n `(?:${STRIP_BY_LABEL.default.source}|${TRAILING_PUNCT_CLASS})+$`,\n ),\n} as const;\n\n/** Strip leading/trailing whitespace and punctuation. */\nexport const sanitizeEntities = (entities: Entity[]): Entity[] =>\n entities.flatMap((e) => {\n if (hasLockedBoundary(e) || hasCuratedLiteralBoundary(e)) {\n return [e];\n }\n\n const stripKind = COLON_LABELS.has(e.label) ? \"colon\" : \"default\";\n // Also strip leading dots followed by whitespace —\n // artifact from trigger extraction after abbreviations\n // like \"dat. nar.\" or \"č.p.\" where the extraction\n // starts at the trailing dot of the abbreviation.\n // The typographic-punctuation passes run in a loop\n // alongside the whitespace strip so combinations like\n // `\"Some Org\",` or ` \"Name\" ` collapse cleanly.\n const leadRe = LEADING_TRIM_BY_LABEL[stripKind];\n const trailRe = TRAILING_TRIM_BY_LABEL[stripKind];\n // Sentence-tail ellipsis: a date or other shape-extending\n // entity that sits at the end of a clause sometimes gets\n // captured with the preceding `...` / `…` run still attached\n // (`V Praze, dne ...2. 2. 2026` → `...2. 2. 2026`). The\n // generic punctuation strip below doesn't touch them because\n // the trailing dots aren't followed by whitespace. Strip the\n // run explicitly for the labels that have this shape — numeric\n // values where leading punctuation is never structurally part\n // of the entity.\n const ellipsisStripped = ELLIPSIS_PREFIX_LABELS.has(e.label)\n ? e.text.replace(LEADING_ELLIPSIS_RE, \"\")\n : e.text;\n const leadTrimmed = ellipsisStripped.replace(leadRe, \"\");\n const lead = e.text.length - leadTrimmed.length;\n let cleaned = leadTrimmed.replace(trailRe, \"\");\n // Trailing-period strip for proper-noun labels that\n // don't end in a legal-form abbreviation. Trigger\n // and NER captures often include the sentence\n // terminator\n // (\"Krajského soudu v Praze.\" → \"Krajského soudu v Praze\",\n // \"State of Delaware.\" → \"State of Delaware\").\n // Numeric labels (`date`, `phone number`, `monetary\n // amount`, `time`, etc.) and the `person` label keep\n // their trailing period — German dates write `21.`,\n // post-nominals write `M.Sc.`, times write `p.m.`,\n // all of which are structurally significant.\n // Literal deny-list and gazetteer spans whose\n // punctuation is part of the dictionary entry are\n // skipped above. For everything else, keep the period\n // when it follows the FULL detector vocabulary\n // (data/legal-forms.json plus `LEGAL_SUFFIXES`), not\n // only the small propagation list, so detected forms\n // like \"Acme Kft.\" or \"Bank of America, N.A.\" retain\n // their final dot.\n if (\n PERIOD_STRIPPED_LABELS.has(e.label) &&\n cleaned.endsWith(\".\") &&\n !LITERAL_SOURCES.has(e.source)\n ) {\n const known = getKnownLegalSuffixes();\n const keepsPeriod =\n known.some((suffix) => cleaned.endsWith(suffix)) ||\n (e.label === \"address\" && hasKnownAddressFinalAbbrev(cleaned)) ||\n (e.label === \"location\" && hasLocationFinalAbbrev(cleaned));\n if (!keepsPeriod) {\n cleaned = cleaned.slice(0, -1).trimEnd();\n }\n }\n // After the period strip, re-run the trailing-punctuation\n // pass in case the period sat between the entity and\n // already-stripped quotes/parens (e.g., `Foo.\"` →\n // `Foo.` → `Foo`).\n cleaned = cleaned.replace(trailRe, \"\");\n if (cleaned.length === 0) return [];\n // Reject entities with no alphanumeric content\n if (!/[\\p{L}\\p{N}]/u.test(cleaned)) return [];\n // Collapse internal whitespace runs (address entities\n // spanning multiple lines in structured documents)\n const collapsed = cleaned.replace(/\\s*\\n\\s*/g, \" \").replace(/\\s{2,}/g, \" \");\n if (collapsed === e.text) return [e];\n return [\n {\n ...e,\n start: e.start + lead,\n end: e.start + lead + collapsed.length,\n text: collapsed,\n },\n ];\n });\n\nexport const mergeAndDedup = (...layers: Entity[][]): Entity[] => {\n const all: Entity[] = [];\n for (const layer of layers) {\n for (const entity of layer) {\n all.push(entity);\n }\n }\n if (all.length === 0) return [];\n\n const sorted = all.toSorted((a, b) => a.start - b.start);\n\n // Single-pass sweep-line: since entities are sorted\n // by start, a new entity can only overlap the tail\n // of merged[] (all earlier entries end before it).\n const first = sorted[0];\n if (!first) return [];\n const merged: Entity[] = [{ ...first }];\n\n for (let i = 1; i < sorted.length; i++) {\n const entity = sorted[i];\n const last = merged.at(-1);\n if (!entity || !last) continue;\n\n if (last.end <= entity.start) {\n // No overlap: append.\n merged.push({ ...entity });\n continue;\n }\n\n const overlapStart = (() => {\n for (let j = merged.length - 1; j >= 0; j--) {\n const existing = merged[j];\n if (!existing || existing.end <= entity.start) {\n return j + 1;\n }\n }\n return 0;\n })();\n const overlaps = merged.slice(overlapStart);\n const hasPartialOverlap = overlaps.some(\n (existing) =>\n existing.start !== entity.start || existing.end !== entity.end,\n );\n\n if (!hasPartialOverlap) {\n const sameLabelIndex = overlaps.findIndex(\n (existing) => existing.label === entity.label,\n );\n if (sameLabelIndex === -1) {\n merged.push({ ...entity });\n continue;\n }\n\n const actualIndex = overlapStart + sameLabelIndex;\n const sameLabel = merged[actualIndex];\n if (sameLabel && shouldReplace(entity, sameLabel)) {\n merged[actualIndex] = { ...entity };\n }\n continue;\n }\n\n if (overlaps.every((existing) => shouldReplace(entity, existing))) {\n merged.splice(overlapStart, overlaps.length, { ...entity });\n }\n }\n\n return resolveSameSpanLabelConflicts(sanitizeEntities(merged));\n};\n\nexport type NerInferenceFn = (\n fullText: string,\n labels: string[],\n threshold: number,\n signal?: AbortSignal,\n) => Promise<Entity[]>;\n\n/**\n * Extend monetary amount entities to include a trailing\n * \"(slovy ...)\" or \"(slovně ...)\" parenthetical that\n * spells out the amount in words. Common in Czech legal\n * documents, e.g. \"1 529,-Kč (slovy jeden-tisíc)\".\n *\n * Keywords are loaded from amount-words.json config so\n * new languages can be added without code changes.\n */\ntype AmountWordsConfig = {\n patterns: { lang: string; keywords: string[] }[];\n};\n\nconst escapeRegex = (s: string): string =>\n s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\nlet amountWordsRe: RegExp | null = null;\nlet amountWordsLoaded = false;\n\nconst getAmountWordsRe = async (): Promise<RegExp> => {\n if (amountWordsLoaded && amountWordsRe) {\n return amountWordsRe;\n }\n try {\n const mod = await import(\"./data/amount-words.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON module shape\n const data = (mod as { default: AmountWordsConfig }).default;\n const keywords = data.patterns.flatMap((p) => p.keywords);\n const alt = keywords.map(escapeRegex).join(\"|\");\n amountWordsRe = new RegExp(\n `^[,;]?[^\\\\S\\\\n]*(\\\\((?:${alt})[:\\\\s][^)\\\\n]{1,120}\\\\))`,\n \"i\",\n );\n } catch {\n // Fallback: original Czech-only pattern\n amountWordsRe = /^[,;]?[^\\S\\n]*(\\((?:slovy|slovně)[:\\s][^)\\n]{1,120}\\))/i;\n }\n amountWordsLoaded = true;\n return amountWordsRe;\n};\n\nconst extendMonetaryAmountWords = (\n entities: Entity[],\n fullText: string,\n re: RegExp,\n): Entity[] =>\n entities.map((e) => {\n if (e.label !== \"monetary amount\" || isCallerOwnedEntity(e)) {\n return e;\n }\n const after = fullText.slice(e.end);\n const m = re.exec(after);\n if (!m) return e;\n const newEnd = e.end + m[0].length;\n return {\n ...e,\n end: newEnd,\n text: fullText.slice(e.start, newEnd),\n };\n });\n\nlet monetaryTrailingCurrencyRe: RegExp | null = null;\nlet monetaryTrailingCurrencyLoaded = false;\n\ntype CurrenciesData = {\n codes?: string[];\n symbols?: string[];\n localNames?: string[];\n};\n\nconst getMonetaryTrailingCurrencyRe = async (): Promise<RegExp | null> => {\n if (monetaryTrailingCurrencyLoaded) return monetaryTrailingCurrencyRe;\n try {\n const mod = await import(\"./data/currencies.json\");\n const data: CurrenciesData = mod.default ?? mod;\n const codes = (data.codes ?? []).filter((c) => /^[A-Z]{2,4}$/.test(c));\n const names = (data.localNames ?? []).filter((n) => n.length > 0);\n const parts: string[] = [];\n if (names.length > 0) {\n parts.push(names.map(escapeRegex).join(\"|\"));\n }\n if (codes.length > 0) {\n parts.push(codes.map(escapeRegex).join(\"|\"));\n }\n if (parts.length === 0) {\n monetaryTrailingCurrencyRe = null;\n } else {\n const alt = parts.join(\"|\");\n monetaryTrailingCurrencyRe = new RegExp(\n `^([^\\\\S\\\\n\\\\t]{0,4})(${alt})(?![\\\\p{L}\\\\p{N}])`,\n \"u\",\n );\n }\n } catch {\n monetaryTrailingCurrencyRe = null;\n }\n monetaryTrailingCurrencyLoaded = true;\n return monetaryTrailingCurrencyRe;\n};\n\n// Extend a monetary-amount entity to include a trailing currency\n// code/name when one sits within a short whitespace gap after the\n// captured span (`273,-` followed by ` Kč`, `1 000` followed by\n// ` CZK`). The unified regex backend occasionally drops the longer\n// currency pattern in favour of a shorter NUM-only match depending\n// on Rust regex DFA construction order; this post-process pass\n// re-attaches the suffix from \\`currencies.json\\` so the boundary\n// is the same regardless of which match resolved.\nconst extendMonetaryTrailingCurrency = (\n entities: Entity[],\n fullText: string,\n re: RegExp | null,\n): Entity[] => {\n if (!re) return entities;\n return entities.map((e) => {\n if (e.label !== \"monetary amount\" || isCallerOwnedEntity(e)) return e;\n if (/\\p{L}/u.test(e.text.slice(-1) ?? \"\")) return e;\n const after = fullText.slice(e.end);\n const m = re.exec(after);\n if (!m) return e;\n const newEnd = e.end + m[0].length;\n return {\n ...e,\n end: newEnd,\n text: fullText.slice(e.start, newEnd),\n };\n });\n};\n\ntype AllowedLabelSet = ReadonlySet<string> | null;\n\nconst createAllowedLabelSetFromLabels = (\n labels: readonly string[],\n): AllowedLabelSet => (labels.length > 0 ? new Set(labels) : null);\n\nconst createAllowedLabelSet = (config: PipelineConfig): AllowedLabelSet =>\n createAllowedLabelSetFromLabels(config.labels);\n\nconst DEFAULT_CUSTOM_REGEX_SCORE = 0.9;\n\nconst filterAllowedLabels = (\n entities: Entity[],\n allowedLabels: AllowedLabelSet,\n): Entity[] => {\n if (!allowedLabels) {\n return entities;\n }\n return entities.filter((e) => allowedLabels.has(e.label));\n};\n\nconst labelIsAllowed = (\n label: string,\n allowedLabels: AllowedLabelSet,\n): boolean => !allowedLabels || allowedLabels.has(label);\n\n// MISC is intentionally a label without detection — only the\n// custom deny-list path produces it. Asking the NER schema for\n// MISC would invite zero-shot guesses that contradict that\n// contract and cause over-redaction.\nconst NON_NER_LABELS: ReadonlySet<string> = new Set([\"misc\"]);\n\nconst getRequestedNerLabels = (\n config: PipelineConfig,\n expandForHotwords = false,\n): readonly string[] => {\n const labels =\n config.labels.length > 0 ? config.labels : DEFAULT_ENTITY_LABELS;\n const expanded = expandForHotwords\n ? expandLabelsForHotwordRules(labels)\n : labels;\n return expanded.filter((label) => !NON_NER_LABELS.has(label));\n};\n\nconst checkAbort = (signal?: AbortSignal): void => {\n if (signal?.aborted) {\n throw new DOMException(\"Pipeline aborted\", \"AbortError\");\n }\n};\n\nconst configKey = (\n config: PipelineConfig,\n gazetteerEntries: GazetteerEntry[],\n): string => {\n const legalFormsEnabled = isLegalFormsEnabled(config);\n const customDenyFingerprint =\n config.enableDenyList && config.customDenyList\n ? config.customDenyList\n .map((entry) =>\n JSON.stringify({\n label: entry.label,\n value: entry.value,\n variants: [...(entry.variants ?? [])].sort(),\n }),\n )\n .sort()\n .join(\"\\n\")\n : \"\";\n const customRegexFingerprint =\n config.enableRegex && config.customRegexes\n ? config.customRegexes\n .map((entry) =>\n JSON.stringify({\n label: entry.label,\n pattern: entry.pattern,\n score: entry.score ?? DEFAULT_CUSTOM_REGEX_SCORE,\n }),\n )\n .sort()\n .join(\"\\n\")\n : \"\";\n // Gazetteer fingerprint: sorted entry IDs,\n // canonical forms, labels, and variants.\n // Skip when gazetteer is disabled to avoid\n // unnecessary cache misses.\n const gazFingerprint =\n config.enableGazetteer && gazetteerEntries.length > 0\n ? gazetteerEntries\n .map(\n (e) =>\n `${e.id}:${e.canonical}:${e.label}:${[...e.variants].sort().join(\",\")}`,\n )\n .toSorted()\n .join(\";\")\n : \"\";\n return (\n `${config.enableDenyList}:` +\n `${config.enableTriggerPhrases}:` +\n `${legalFormsEnabled}:` +\n `${config.enableNameCorpus}:` +\n `${config.nameCorpusLanguages?.toSorted().join(\",\") ?? \"\"}:` +\n `${config.enableRegex}:` +\n `${config.labels.toSorted().join(\",\")}:` +\n `${config.denyListCountries?.toSorted().join(\",\") ?? \"\"}:` +\n `${config.denyListRegions?.toSorted().join(\",\") ?? \"\"}:` +\n `${config.denyListExcludeCategories?.toSorted().join(\",\") ?? \"\"}:` +\n `${customDenyFingerprint}:` +\n `${customRegexFingerprint}:` +\n `${config.enableGazetteer}:${gazFingerprint}:` +\n `${config.enableCountries !== false}`\n );\n};\n\ntype SharedSearchCacheValue =\n | Promise<UnifiedSearchInstance>\n | UnifiedSearchInstance;\n\n// Prepared search instances are immutable after construction\n// except for lazy regex slots, which only memoize their own\n// RegexSet. Scope the shared cache by dictionary object identity\n// so callers with different workspace dictionaries do not share\n// deny-list automata accidentally.\nconst sharedSearchByDictionaries = new WeakMap<\n Dictionaries,\n Map<string, SharedSearchCacheValue>\n>();\nconst sharedSearchWithoutDictionaries = new Map<\n string,\n SharedSearchCacheValue\n>();\n\nconst sharedSearchCacheFor = (\n dictionaries: Dictionaries | undefined,\n): Map<string, SharedSearchCacheValue> => {\n if (dictionaries === undefined) {\n return sharedSearchWithoutDictionaries;\n }\n const cached = sharedSearchByDictionaries.get(dictionaries);\n if (cached !== undefined) {\n return cached;\n }\n const created = new Map<string, SharedSearchCacheValue>();\n sharedSearchByDictionaries.set(dictionaries, created);\n return created;\n};\n\nconst ensureSearchSupportData = async (\n config: PipelineConfig,\n ctx: PipelineContext,\n): Promise<void> => {\n if (!config.enableDenyList) {\n return;\n }\n await ensureDenyListData(\n ctx,\n config.dictionaries,\n config.nameCorpusLanguages,\n );\n};\n\n/**\n * Get or build a cached search instance. Cache state\n * lives on the provided PipelineContext first. A\n * dictionary-scoped process cache prevents fresh\n * contexts with the same immutable dictionary bundle\n * from rebuilding identical automata.\n */\nconst getCachedSearch = async (\n config: PipelineConfig,\n gazetteerEntries: GazetteerEntry[],\n ctx: PipelineContext,\n): Promise<UnifiedSearchInstance> => {\n const key = configKey(config, gazetteerEntries);\n if (ctx.search && ctx.searchKey === key) {\n return ctx.search;\n }\n if (ctx.searchPromise && ctx.searchKey === key) {\n return ctx.searchPromise;\n }\n\n const sharedCache = sharedSearchCacheFor(config.dictionaries);\n const shared = sharedCache.get(key);\n if (shared !== undefined) {\n const result = await shared;\n await ensureSearchSupportData(config, ctx);\n ctx.search = result;\n ctx.searchKey = key;\n ctx.searchPromise = null;\n return result;\n }\n\n // Build new search. Null the cached instance first\n // so concurrent callers don't use stale data while\n // the new build is in flight.\n ctx.search = null;\n ctx.searchKey = key;\n const promise = buildUnifiedSearch(config, gazetteerEntries, ctx);\n ctx.searchPromise = promise;\n sharedCache.set(key, promise);\n let result: UnifiedSearchInstance;\n try {\n result = await promise;\n } catch (err) {\n if (sharedCache.get(key) === promise) {\n sharedCache.delete(key);\n }\n throw err;\n }\n if (sharedCache.get(key) === promise) {\n sharedCache.set(key, result);\n }\n // Guard: another call may have replaced the key\n // while we were awaiting. Only cache if still ours.\n if (ctx.searchKey === key) {\n ctx.search = result;\n }\n return result;\n};\n\nexport type PipelineSearchOptions = {\n config: PipelineConfig;\n gazetteerEntries?: GazetteerEntry[];\n context?: PipelineContext;\n};\n\n/**\n * Pre-build and cache the unified search instance for a\n * pipeline configuration. Use the same context in\n * `runPipeline` to reuse the prepared automata without\n * passing `cachedSearch` around manually.\n */\nexport const preparePipelineSearch = ({\n config,\n gazetteerEntries = [],\n context,\n}: PipelineSearchOptions): Promise<UnifiedSearchInstance> =>\n getCachedSearch(config, gazetteerEntries, context ?? defaultContext);\n\n/**\n * Options for {@link runPipeline}.\n *\n * @property cachedSearch Pre-built search instance.\n * When provided, `config` and `gazetteerEntries`\n * are not used for building; the caller must\n * ensure the instance matches both parameters.\n */\nexport type PipelineOptions = {\n fullText: string;\n config: PipelineConfig;\n gazetteerEntries: GazetteerEntry[];\n nerInference?: NerInferenceFn | null;\n onProgress?: (step: string, detail: string) => void;\n cachedSearch?: UnifiedSearchInstance;\n signal?: AbortSignal;\n context?: PipelineContext;\n};\n\n/**\n * Run the full detection pipeline.\n *\n * Two TextSearch instances scan the text (regex +\n * literals). Results are dispatched to each\n * detector's post-processor by pattern index range.\n *\n * Pass an AbortSignal to cancel the pipeline between\n * stages. Throws a DOMException with name \"AbortError\"\n * when cancelled.\n *\n * Pass an optional `context` to isolate cached state\n * from other pipeline runs. If omitted, a module-level\n * default context is used (backward compatible).\n */\nexport const runPipeline = async (\n options: PipelineOptions,\n): Promise<Entity[]> => {\n const {\n fullText,\n config,\n gazetteerEntries,\n nerInference = null,\n onProgress,\n cachedSearch,\n signal,\n context,\n } = options;\n const ctx = context ?? defaultContext;\n const allowedLabels = createAllowedLabelSet(config);\n const legalFormsEnabled = isLegalFormsEnabled(config);\n\n const log = (step: string, detail: string) => {\n onProgress?.(step, detail);\n };\n\n checkAbort(signal);\n\n // Ensure generic-roles data, zone config, hotword\n // rules, and prepositions are loaded before the\n // pipeline runs. All are no-ops after the first call.\n let zoneInitOk = false;\n const enableHotwords = config.enableHotwordRules === true;\n let hotwordInitOk = false;\n const hotwordInit = enableHotwords\n ? initHotwordRules()\n .then(() => {\n hotwordInitOk = true;\n })\n .catch((err: unknown) => {\n log(\"hotwords\", \"init failed; skipping\");\n console.warn(\"[anonymize] hotword rules init failed\", err);\n })\n : Promise.resolve();\n if (config.enableZoneClassification) {\n const zoneInit = initZoneClassifier(ctx)\n .then(() => {\n zoneInitOk = true;\n })\n .catch((err: unknown) => {\n log(\"zones\", \"init failed; skipping\");\n console.warn(\"[anonymize] zone classifier init failed\", err);\n });\n await Promise.all([\n loadGenericRoles(ctx),\n loadDocumentStructureHeadings(),\n initPrepositions(),\n initStreetAbbrevs(),\n initAddressComponents(),\n warmAddressStopKeywords(),\n zoneInit,\n hotwordInit,\n ]);\n } else {\n await Promise.all([\n loadGenericRoles(ctx),\n loadDocumentStructureHeadings(),\n initPrepositions(),\n initStreetAbbrevs(),\n initAddressComponents(),\n warmAddressStopKeywords(),\n hotwordInit,\n ]);\n }\n\n // When a pre-built search is provided, buildDenyList\n // was skipped for this context. Ensure stopwords,\n // allow list, and person stopwords are loaded so\n // processDenyListMatches filters correctly.\n if (cachedSearch && config.enableDenyList) {\n await ensureDenyListData(\n ctx,\n config.dictionaries,\n config.nameCorpusLanguages,\n );\n }\n\n // Classify document zones once up front\n let zones: ZoneSpan[] = [];\n if (config.enableZoneClassification && zoneInitOk) {\n zones = classifyZones(fullText, ctx);\n if (zones.length > 0) {\n const zoneNames = [...new Set(zones.map((z) => z.zone))];\n log(\"zones\", zoneNames.join(\", \"));\n }\n }\n\n checkAbort(signal);\n\n const hotwordsActive = enableHotwords && hotwordInitOk;\n const preHotwordAllowedLabels = hotwordsActive\n ? createAllowedLabelSetFromLabels(\n expandLabelsForHotwordRules(config.labels),\n )\n : allowedLabels;\n const searchConfig = hotwordsActive\n ? {\n ...config,\n labels: [...expandLabelsForHotwordRules(config.labels)],\n }\n : config;\n\n const search =\n cachedSearch ??\n (await getCachedSearch(searchConfig, gazetteerEntries, ctx));\n\n checkAbort(signal);\n\n // Two-pass scan (regex + literals)\n const { regexMatches, customRegexMatches, literalMatches } = runUnifiedSearch(\n search,\n fullText,\n );\n const { slices } = search;\n\n const rawRegexEntities = config.enableRegex\n ? processRegexMatches(\n regexMatches,\n slices.regex.start,\n slices.regex.end,\n search.regexMeta,\n )\n : [];\n const rawCustomRegexEntities = config.enableRegex\n ? processRegexMatches(\n customRegexMatches,\n slices.customRegex.start,\n slices.customRegex.end,\n search.customRegexMeta,\n )\n : [];\n const customRegexEntities = filterAllowedLabels(\n rawCustomRegexEntities,\n preHotwordAllowedLabels,\n );\n const regexEntities = filterAllowedLabels(\n [...rawRegexEntities, ...customRegexEntities],\n preHotwordAllowedLabels,\n );\n if (regexEntities.length > 0) log(\"regex\", `${regexEntities.length} matches`);\n\n if (legalFormsEnabled || config.enableTriggerPhrases) {\n // Populate the per-language legal-role-head cache so the\n // synchronous match processor below can read it. Cheap and\n // idempotent — only the first call kicks the loads.\n // Triggers also need this: the trigger reclassification step\n // (person → organization when the captured text contains a\n // legal-form suffix) reads `getKnownLegalSuffixes()`, which\n // falls back to the seed list until the cache is warmed.\n await warmLegalRoleHeads();\n }\n const rawLegalFormEntities = legalFormsEnabled\n ? detectLegalFormsV2(fullText)\n : [];\n const legalFormEntities = filterAllowedLabels(\n rawLegalFormEntities,\n preHotwordAllowedLabels,\n );\n if (legalFormEntities.length > 0)\n log(\"legal-forms\", `${legalFormEntities.length} matches`);\n\n if (config.enableTriggerPhrases) {\n // Populate the address-stop-keywords cache so the\n // synchronous address strategy uses the merged\n // per-language list instead of the seed fallback.\n await warmAddressStopKeywords();\n }\n const rawTriggerEntities = config.enableTriggerPhrases\n ? processTriggerMatches(\n regexMatches,\n slices.triggers.start,\n slices.triggers.end,\n fullText,\n search.triggerRules,\n )\n : [];\n const triggerEntities = filterAllowedLabels(\n rawTriggerEntities,\n preHotwordAllowedLabels,\n );\n if (triggerEntities.length > 0)\n log(\"trigger-phrases\", `${triggerEntities.length} matches`);\n\n // Signature-block recognition. Always on: the\n // anchors (\"/s/\", \"Name:\", \"By:\", \"IN WITNESS\n // WHEREOF\") are unambiguous legal-document markers\n // and produce no matches in unrelated prose.\n const rawSignatureEntities = detectSignatures(fullText, ctx);\n const signatureEntities = filterAllowedLabels(\n rawSignatureEntities,\n preHotwordAllowedLabels,\n );\n if (signatureEntities.length > 0)\n log(\"signatures\", `${signatureEntities.length} matches`);\n\n checkAbort(signal);\n\n let rawNameCorpusEntities: Entity[] = [];\n let nameCorpusEntities: Entity[] = [];\n if (config.enableNameCorpus && !config.enableDenyList) {\n await initNameCorpus(ctx, config.dictionaries, config.nameCorpusLanguages);\n checkAbort(signal);\n rawNameCorpusEntities = detectNameCorpus(fullText, ctx);\n nameCorpusEntities = filterAllowedLabels(\n rawNameCorpusEntities,\n preHotwordAllowedLabels,\n );\n log(\"name-corpus\", `${nameCorpusEntities.length} matches`);\n }\n\n const rawDenyListEntities =\n config.enableDenyList && search.denyListData\n ? processDenyListMatches(\n literalMatches,\n slices.denyList.start,\n slices.denyList.end,\n fullText,\n search.denyListData,\n ctx,\n )\n : [];\n const denyListEntities = filterAllowedLabels(\n rawDenyListEntities,\n preHotwordAllowedLabels,\n );\n if (denyListEntities.length > 0)\n log(\"deny-list\", `${denyListEntities.length} matches`);\n\n if (config.enableNameCorpus && config.enableDenyList) {\n await initNameCorpus(ctx, config.dictionaries, config.nameCorpusLanguages);\n checkAbort(signal);\n rawNameCorpusEntities = filterNameCorpusCoveredByDenyList(\n detectNameCorpus(fullText, ctx, { mode: \"supplemental\" }),\n rawDenyListEntities,\n );\n nameCorpusEntities = filterAllowedLabels(\n rawNameCorpusEntities,\n preHotwordAllowedLabels,\n );\n log(\"name-corpus\", `${nameCorpusEntities.length} supplemental matches`);\n }\n\n // Gazetteer: unified into tsLiterals\n const rawGazetteerEntities =\n config.enableGazetteer && search.gazetteerData\n ? processGazetteerMatches(\n literalMatches,\n slices.gazetteer.start,\n slices.gazetteer.end,\n fullText,\n search.gazetteerData,\n )\n : [];\n const gazetteerEntities = filterAllowedLabels(\n rawGazetteerEntities,\n preHotwordAllowedLabels,\n );\n if (gazetteerEntities.length > 0)\n log(\"gazetteer\", `${gazetteerEntities.length} matches`);\n\n const rawCountryEntities = search.countryData\n ? processCountryMatches(\n literalMatches,\n slices.countries.start,\n slices.countries.end,\n fullText,\n search.countryData,\n )\n : [];\n const countryEntities = filterAllowedLabels(\n rawCountryEntities,\n preHotwordAllowedLabels,\n );\n if (countryEntities.length > 0)\n log(\"countries\", `${countryEntities.length} matches`);\n\n checkAbort(signal);\n\n const rawCustomDenyListEntities = rawDenyListEntities.filter((entity) =>\n isCallerOwnedEntity(entity),\n );\n const rawCuratedDenyListEntities = rawDenyListEntities.filter(\n (entity) => !isCallerOwnedEntity(entity),\n );\n const customDenyListEntities = filterAllowedLabels(\n rawCustomDenyListEntities,\n preHotwordAllowedLabels,\n );\n\n const ruleContextEntities = [\n ...rawTriggerEntities,\n ...rawRegexEntities,\n ...customRegexEntities,\n ...rawLegalFormEntities,\n ...rawNameCorpusEntities,\n ...rawCuratedDenyListEntities,\n ...customDenyListEntities,\n ...rawGazetteerEntities,\n ];\n const nerMaskEntities = [\n ...rawTriggerEntities,\n ...rawRegexEntities,\n ...customRegexEntities,\n ...rawLegalFormEntities,\n ...rawNameCorpusEntities,\n ...rawCuratedDenyListEntities,\n ...customDenyListEntities,\n ...rawGazetteerEntities,\n ];\n\n // NER (mask rule-detected spans so the model doesn't\n // produce contradictory boundaries for known entities)\n let rawNerEntities: Entity[] = [];\n let nerEntities: Entity[] = [];\n const requestedNerLabels = getRequestedNerLabels(config, hotwordsActive);\n if (config.enableNer && nerInference && requestedNerLabels.length > 0) {\n const maskResult = maskDetectedSpans(fullText, nerMaskEntities);\n log(\"ner\", \"running inference...\");\n const rawNer = await nerInference(\n maskResult.maskedText,\n [...requestedNerLabels],\n config.threshold,\n signal,\n );\n rawNerEntities = unmaskNerEntities(rawNer, maskResult, fullText);\n nerEntities = filterAllowedLabels(rawNerEntities, preHotwordAllowedLabels);\n const masked = rawNer.length - rawNerEntities.length;\n const labelFiltered = rawNerEntities.length - nerEntities.length;\n log(\n \"ner\",\n `${nerEntities.length} entities` +\n (masked > 0 ? ` (${masked} masked)` : \"\") +\n (labelFiltered > 0 ? ` (${labelFiltered} label-filtered)` : \"\"),\n );\n }\n\n checkAbort(signal);\n\n // Address seed expansion\n const preAddressEntities = [\n ...triggerEntities,\n ...signatureEntities,\n ...regexEntities,\n ...legalFormEntities,\n ...nameCorpusEntities,\n ...denyListEntities,\n ...gazetteerEntities,\n ...countryEntities,\n ...nerEntities,\n ];\n const addressSeedEntities = labelIsAllowed(\"address\", allowedLabels)\n ? await processAddressSeeds(\n literalMatches,\n slices.streetTypes.start,\n slices.streetTypes.end,\n fullText,\n [...ruleContextEntities, ...rawNerEntities],\n )\n : [];\n if (addressSeedEntities.length > 0)\n log(\"address-seeds\", `${addressSeedEntities.length} expanded`);\n\n checkAbort(signal);\n\n // Zone-based score adjustment: apply before\n // threshold filtering so entities in PII-dense zones\n // can cross the threshold.\n const zoneAdjusted = applyZoneAdjustments(\n [...preAddressEntities, ...addressSeedEntities],\n zones,\n );\n\n // Hotword context rules: boost or reclassify\n // entities near relevant keywords. Applied after\n // zone adjustments so both effects stack.\n const preBoostEntities = (() => {\n if (!hotwordsActive) {\n return zoneAdjusted;\n }\n const hotwordCandidates = zoneAdjusted.filter(\n (entity) => !isCallerOwnedEntity(entity),\n );\n const callerOwnedEntities = zoneAdjusted.filter(isCallerOwnedEntity);\n return [\n ...filterAllowedLabels(\n applyHotwordRules(hotwordCandidates, fullText),\n allowedLabels,\n ),\n ...callerOwnedEntities,\n ];\n })();\n\n // Confidence boost + threshold filter\n let allEntities: Entity[];\n if (config.enableConfidenceBoost) {\n allEntities = boostNearMissEntities(preBoostEntities, config.threshold);\n const boosted =\n allEntities.length -\n preBoostEntities.filter((e) => e.score >= config.threshold).length;\n if (boosted > 0) log(\"confidence-boost\", `${boosted} near-miss promoted`);\n } else {\n allEntities = preBoostEntities.filter((e) => e.score >= config.threshold);\n }\n\n // Street patterns near existing addresses\n // (e.g. \"Ostrovni 225/1\" near \"110 00 Praha 1\")\n const streetPatterns = labelIsAllowed(\"address\", allowedLabels)\n ? detectStreetPatternsNearAddresses(fullText, allEntities)\n : [];\n if (streetPatterns.length > 0) {\n allEntities = [...allEntities, ...streetPatterns];\n log(\n \"street-context\",\n `${streetPatterns.length} street patterns near addresses`,\n );\n }\n\n // Orphan street lines in header zone\n const orphanStreets = labelIsAllowed(\"address\", allowedLabels)\n ? detectOrphanStreetLines(fullText, allEntities)\n : [];\n if (orphanStreets.length > 0) {\n allEntities = [...allEntities, ...orphanStreets];\n log(\"orphan-streets\", `${orphanStreets.length} header street lines`);\n }\n\n // Merge + dedup\n const rawMerged = mergeAndDedup(allEntities);\n log(\"merge\", `${rawMerged.length} after dedup`);\n\n // Extend monetary amounts to include trailing\n // \"(slovy ...)\" or \"(slovně ...)\" parentheticals.\n // Runs after dedup so each monetary span is unique,\n // preventing duplicate extensions from clobbering\n // unrelated entities between e.end and newEnd.\n const monetaryAmountWordsRe = await getAmountWordsRe();\n const monetaryTrailingRe = await getMonetaryTrailingCurrencyRe();\n const mergedWithCurrency = extendMonetaryTrailingCurrency(\n rawMerged,\n fullText,\n monetaryTrailingRe,\n );\n const mergedExtended = extendMonetaryAmountWords(\n mergedWithCurrency,\n fullText,\n monetaryAmountWordsRe,\n );\n\n // Boundary consistency (merge adjacent, fix partial\n // words, remove nested same-label)\n const consistent = enforceBoundaryConsistency(mergedExtended, fullText);\n if (consistent.length < mergedExtended.length)\n log(\n \"boundary\",\n `${mergedExtended.length - consistent.length} consolidated`,\n );\n\n // Organization name propagation: strip legal form\n // suffixes from detected orgs and re-scan for bare\n // mentions of the base name. Gated by enableCoreference\n // since this is a coreference-like pass. Propagated\n // entities are filtered by the configured threshold to\n // ensure they respect the caller's confidence floor.\n let postOrgEntities = consistent;\n if (\n config.enableCoreference &&\n labelIsAllowed(\"organization\", allowedLabels)\n ) {\n const orgPropagated = propagateOrgNames(consistent, fullText);\n const thresholded = orgPropagated.filter(\n (e) => e.score >= config.threshold,\n );\n if (thresholded.length > 0) {\n postOrgEntities = mergeAndDedup(consistent, thresholded);\n log(\"org-propagation\", `${thresholded.length} base names`);\n }\n }\n\n // False-positive filtering\n const merged = filterFalsePositives(postOrgEntities, ctx, fullText);\n if (merged.length < postOrgEntities.length)\n log(\"filter\", `removed ${postOrgEntities.length - merged.length} FPs`);\n\n checkAbort(signal);\n\n // Coreference\n if (config.enableCoreference) {\n // Coreference's alias filter rejects parenthetical\n // captures that are nothing but a legal-form suffix\n // (\"(« SAS »)\"), which needs the full vocabulary from\n // `data/legal-forms.json`. Warm it here so configs\n // that enable coreference without legal-form detection\n // still see the complete suffix set.\n if (!legalFormsEnabled) {\n await warmLegalRoleHeads();\n }\n const coreferenceSeeds = merged.filter(\n (entity) => !isCallerOwnedEntity(entity),\n );\n const terms = await extractDefinedTerms(fullText, coreferenceSeeds, ctx);\n if (terms.length > 0) {\n log(\"coreference\", `${terms.length} defined terms`);\n const corefSpans = findCoreferenceSpans(fullText, terms, ctx);\n if (corefSpans.length > 0) {\n log(\"coreference-rescan\", `${corefSpans.length} aliases`);\n const corefMerged = mergeAndDedup(merged, corefSpans);\n const corefConsistent = enforceBoundaryConsistency(\n corefMerged,\n fullText,\n );\n return sanitizeEntities(\n filterAllowedLabels(\n filterFalsePositives(corefConsistent, ctx, fullText),\n allowedLabels,\n ),\n );\n }\n }\n }\n\n // Re-sanitize: enforceBoundaryConsistency may adjust\n // entity boundaries after mergeAndDedup's sanitization,\n // potentially re-introducing whitespace or punctuation.\n return sanitizeEntities(filterAllowedLabels(merged, allowedLabels));\n};\n","import type {\n AnonymisationOperator,\n OperatorConfig,\n OperatorType,\n} from \"./types\";\n\n// ── Operator registry ──────────────────────────────────\n\nconst replaceOperator: AnonymisationOperator = {\n type: \"replace\",\n reversibility: \"reversible\",\n apply: (_text, _label, placeholder) => placeholder,\n};\n\nconst redactOperator: AnonymisationOperator = {\n type: \"redact\",\n reversibility: \"irreversible\",\n apply: (_text, _label, _placeholder, redactString) => redactString,\n};\n\nexport const OPERATOR_REGISTRY = {\n replace: replaceOperator,\n redact: redactOperator,\n} as const satisfies Record<OperatorType, AnonymisationOperator>;\n\nconst DEFAULT_REDACT_STRING = \"[REDACTED]\";\n\n/**\n * Default operator config: replace for all labels.\n * Preserves existing pipeline behaviour.\n */\nexport const DEFAULT_OPERATOR_CONFIG: OperatorConfig = {\n operators: {},\n redactString: DEFAULT_REDACT_STRING,\n};\n\n/**\n * Resolve the operator for a label, falling back to \"replace\".\n */\nexport const resolveOperator = (\n config: OperatorConfig,\n label: string,\n): OperatorType => config.operators[label] ?? \"replace\";\n","import {\n DEFAULT_OPERATOR_CONFIG,\n OPERATOR_REGISTRY,\n resolveOperator,\n} from \"./operators\";\nimport type {\n Entity,\n OperatorConfig,\n OperatorType,\n RedactionResult,\n} from \"./types\";\nimport type { PipelineContext } from \"./context\";\nimport { defaultContext } from \"./context\";\n\nconst WHITESPACE_RE = /\\s+/g;\nconst PHONE_NOISE_RE = /[()\\s-]/g;\n// Strip all separators the ID detectors accept so the\n// same real-world value canonicalises to one placeholder:\n// - whitespace and `-` for IBAN, NIP, REGON, etc.\n// - `/` for birth numbers (\"900101/1234\") and Czech\n// bank accounts (\"123-4567/0100\").\n// - `.` for credit cards (\"4111.1111.1111.1111\") and\n// other dotted IDs.\nconst ID_SEPARATOR_RE = /[\\s\\-/.]/g;\n\n/**\n * Normalize entity text so that surface-form variations\n * of the same real-world value map to a single canonical\n * key. Lowercased emails, stripped phone formatting, etc.\n */\nconst normalizeEntityText = (label: string, text: string): string => {\n const upper = label.toUpperCase().replace(WHITESPACE_RE, \"_\");\n\n if (upper === \"EMAIL_ADDRESS\" || upper === \"EMAIL\") {\n return text.toLowerCase().trim();\n }\n if (upper === \"PHONE_NUMBER\" || upper === \"PHONE\") {\n return text.replace(PHONE_NOISE_RE, \"\");\n }\n if (\n upper === \"IBAN\" ||\n upper === \"BANK_ACCOUNT_NUMBER\" ||\n upper === \"TAX_IDENTIFICATION_NUMBER\" ||\n upper === \"REGISTRATION_NUMBER\" ||\n upper === \"NATIONAL_IDENTIFICATION_NUMBER\" ||\n upper === \"SOCIAL_SECURITY_NUMBER\" ||\n upper === \"BIRTH_NUMBER\" ||\n upper === \"IDENTITY_CARD_NUMBER\" ||\n upper === \"PASSPORT_NUMBER\" ||\n upper === \"CREDIT_CARD_NUMBER\"\n ) {\n return text.replace(ID_SEPARATOR_RE, \"\").toUpperCase();\n }\n if (\n upper === \"PERSON\" ||\n upper === \"ORGANIZATION\" ||\n upper === \"ADDRESS\" ||\n upper === \"LAND_PARCEL\" ||\n upper === \"MISC\"\n ) {\n return text.replace(WHITESPACE_RE, \" \").toLowerCase().trim();\n }\n return text.trim();\n};\n\n/**\n * Build a stable mapping from entity text to numbered\n * placeholders. Same real-world value always maps to the\n * same placeholder (e.g., \"Dr. Muller\" and \"Dr. Muller\"\n * both become [PERSON_1]).\n *\n * Placeholder format: [LABEL_N] where LABEL is uppercase\n * and N is a 1-based counter per label.\n *\n * @param _ctx Unused. Kept for signature compatibility;\n * coref alias links now travel on the entities\n * themselves (`corefSourceText`).\n */\nexport const buildPlaceholderMap = (\n entities: Entity[],\n _ctx: PipelineContext = defaultContext,\n): Map<string, string> => {\n const counters = new Map<string, number>();\n const textLabelToPlaceholder = new Map<string, string>();\n const normalizedToPlaceholder = new Map<string, string>();\n\n const sorted = entities.toSorted((a, b) => a.start - b.start);\n\n for (const entity of sorted) {\n const compositeKey = `${entity.label}\\0${entity.text}`;\n if (textLabelToPlaceholder.has(compositeKey)) {\n continue;\n }\n\n const labelKey = entity.label.toUpperCase().replace(WHITESPACE_RE, \"_\");\n\n // If this entity is a coref alias, unify its key\n // with the source entity's key so both get the same\n // number — in either direction: a backward alias\n // joins the source's existing placeholder, and a\n // forward alias (bare mention before the full form)\n // reserves its placeholder under the source key so\n // the source joins it when numbered later. The link\n // is carried on the entity itself, so it cannot be\n // lost between detection and redaction.\n const sourceText =\n entity.source === \"coreference\" ? entity.corefSourceText : undefined;\n const sourceNormalizedKey =\n sourceText === undefined\n ? undefined\n : `${labelKey}\\0${normalizeEntityText(entity.label, sourceText)}`;\n if (sourceNormalizedKey !== undefined) {\n const sourceExisting = normalizedToPlaceholder.get(sourceNormalizedKey);\n if (sourceExisting) {\n textLabelToPlaceholder.set(compositeKey, sourceExisting);\n continue;\n }\n }\n\n const normalized = normalizeEntityText(entity.label, entity.text);\n const normalizedKey = `${labelKey}\\0${normalized}`;\n const existing = normalizedToPlaceholder.get(normalizedKey);\n if (existing) {\n textLabelToPlaceholder.set(compositeKey, existing);\n if (sourceNormalizedKey !== undefined) {\n normalizedToPlaceholder.set(sourceNormalizedKey, existing);\n }\n continue;\n }\n\n const count = (counters.get(labelKey) ?? 0) + 1;\n counters.set(labelKey, count);\n\n const placeholder = `[${labelKey}_${count}]`;\n textLabelToPlaceholder.set(compositeKey, placeholder);\n normalizedToPlaceholder.set(normalizedKey, placeholder);\n if (sourceNormalizedKey !== undefined) {\n normalizedToPlaceholder.set(sourceNormalizedKey, placeholder);\n }\n }\n\n return textLabelToPlaceholder;\n};\n\n/**\n * Apply redactions to the source text, replacing each\n * confirmed entity span using the configured operator.\n *\n * Co-references are consistent: if the same text appears\n * multiple times, all occurrences get the same placeholder.\n *\n * @param ctx Pipeline context. Must be the same instance\n * passed to `runPipeline` (or `findCoreferenceSpans`)\n * so coreference placeholder links are preserved.\n * Defaults to `defaultContext` for single-tenant usage.\n */\nexport const redactText = (\n fullText: string,\n entities: Entity[],\n config: OperatorConfig = DEFAULT_OPERATOR_CONFIG,\n ctx: PipelineContext = defaultContext,\n): RedactionResult => {\n if (entities.length === 0) {\n return {\n redactedText: fullText,\n redactionMap: new Map(),\n operatorMap: new Map(),\n entityCount: 0,\n };\n }\n\n const placeholderMap = buildPlaceholderMap(entities, ctx);\n\n const sorted = entities.toSorted((a, b) => a.start - b.start);\n\n // Remove overlapping spans (keep first occurrence)\n const nonOverlapping: Entity[] = [];\n let lastEnd = 0;\n for (const entity of sorted) {\n if (entity.start >= lastEnd) {\n nonOverlapping.push(entity);\n lastEnd = entity.end;\n }\n }\n\n const parts: string[] = [];\n const redactionMap = new Map<string, string>();\n const operatorMap = new Map<string, OperatorType>();\n let cursor = 0;\n\n for (const entity of nonOverlapping) {\n if (entity.start > cursor) {\n parts.push(fullText.slice(cursor, entity.start));\n }\n\n const placeholder =\n placeholderMap.get(`${entity.label}\\0${entity.text}`) ??\n `[${entity.label.toUpperCase().replace(/\\s+/g, \"_\")}]`;\n\n const opType = resolveOperator(config, entity.label);\n const operator = OPERATOR_REGISTRY[opType];\n\n const replacement = operator.apply(\n entity.text,\n entity.label,\n placeholder,\n config.redactString,\n );\n\n parts.push(replacement);\n // operatorMap is keyed by the conceptual placeholder\n // ([LABEL_N]), not by the replacement text. For \"redact\"\n // operators the placeholder never appears in the output;\n // the map is only consulted via exportRedactionKey which\n // iterates redactionMap (replace entries only).\n operatorMap.set(placeholder, opType);\n\n // Only populate redactionMap for reversible operators.\n // A coref alias contributes its source's full text, so\n // a forward alias (\"Acme\" before \"Acme Corporation\")\n // cannot pin the shortened surface form as the key's\n // canonical value for the shared placeholder.\n if (\n operator.reversibility === \"reversible\" &&\n !redactionMap.has(placeholder)\n ) {\n redactionMap.set(\n placeholder,\n entity.source === \"coreference\" ? entity.corefSourceText : entity.text,\n );\n }\n\n cursor = entity.end;\n }\n\n if (cursor < fullText.length) {\n parts.push(fullText.slice(cursor));\n }\n\n return {\n redactedText: parts.join(\"\"),\n redactionMap,\n operatorMap,\n entityCount: nonOverlapping.length,\n };\n};\n\n/**\n * Serialize the redaction key to JSON for export.\n * Includes operator metadata so the export is self-describing.\n */\nexport const exportRedactionKey = (\n redactionMap: Map<string, string>,\n operatorMap: Map<string, OperatorType>,\n): string => {\n const entries: Record<string, { original: string; operator: OperatorType }> =\n {};\n\n for (const [placeholder, value] of redactionMap) {\n entries[placeholder] = {\n original: value,\n operator: operatorMap.get(placeholder) ?? \"replace\",\n };\n }\n\n return JSON.stringify({ entries }, null, 2);\n};\n\n/**\n * De-anonymise text using a redaction key.\n * Replaces placeholders back with original values.\n * Only works for reversible operators (replace).\n */\nexport const deanonymise = (\n redactedText: string,\n redactionMap: Map<string, string>,\n): string => {\n let result = redactedText;\n\n for (const [placeholder, original] of redactionMap) {\n result = result.replaceAll(placeholder, original);\n }\n\n return result;\n};\n","/**\n * Decode ONNX model output into entity spans.\n *\n * The span-level model outputs a logits tensor of shape\n * [batch, inputLength, maxWidth, numEntities]. This module\n * applies sigmoid, thresholds, and greedy non-overlapping\n * selection to produce the final entity list.\n */\nimport type { RawInferenceResult } from \"./types\";\n\ntype Span = [string, number, number, string, number];\n\nconst sigmoid = (x: number): number => 1 / (1 + Math.exp(-x));\n\n/** Check if two spans overlap (optionally allowing multi-label). */\nconst hasOverlapping = (\n a: number[],\n b: number[],\n multiLabel: boolean,\n): boolean => {\n const a0 = a[0] ?? 0;\n const a1 = a[1] ?? 0;\n const b0 = b[0] ?? 0;\n const b1 = b[1] ?? 0;\n if (a0 === b0 && a1 === b1) {\n return !multiLabel;\n }\n return !(a0 > b1 || b0 > a1);\n};\n\n/**\n * Greedy non-overlapping span selection.\n * Sorts by score descending, keeps each span only if it\n * doesn't overlap with already-selected spans.\n */\nconst greedySearch = (\n spans: Span[],\n flatNer: boolean,\n multiLabel: boolean,\n): Span[] => {\n const sorted = spans.toSorted((a, b) => b[4] - a[4]);\n const selected: Span[] = [];\n\n for (const span of sorted) {\n const overlaps = selected.some((s) => {\n if (flatNer) {\n return hasOverlapping([span[1], span[2]], [s[1], s[2]], multiLabel);\n }\n // Non-flat: also allow nested spans\n const isNested =\n (span[1] <= s[1] && span[2] >= s[2]) ||\n (s[1] <= span[1] && s[2] >= span[2]);\n if (isNested) {\n return false;\n }\n return hasOverlapping([span[1], span[2]], [s[1], s[2]], multiLabel);\n });\n\n if (!overlaps) {\n selected.push(span);\n }\n }\n\n return selected.toSorted((a, b) => a[1] - b[1]);\n};\n\n/**\n * Decode span-level model logits into entity results.\n */\nexport const decodeSpans = (\n batchSize: number,\n inputLength: number,\n maxWidth: number,\n numEntities: number,\n texts: string[],\n batchIds: number[],\n batchWordsStartIdx: number[][],\n batchWordsEndIdx: number[][],\n idToClass: Record<number, string>,\n modelOutput: ArrayLike<number>,\n flatNer: boolean,\n threshold: number,\n multiLabel: boolean,\n): RawInferenceResult => {\n const spans: Span[][] = Array.from({ length: batchSize }, () => []);\n\n const batchPadding = inputLength * maxWidth * numEntities;\n const startTokenPadding = maxWidth * numEntities;\n const endTokenPadding = numEntities;\n\n for (let id = 0; id < modelOutput.length; id++) {\n const batch = Math.floor(id / batchPadding);\n const startToken = Math.floor(id / startTokenPadding) % inputLength;\n const endToken = startToken + (Math.floor(id / endTokenPadding) % maxWidth);\n const entity = id % numEntities;\n\n const prob = sigmoid(modelOutput[id] ?? 0);\n\n const batchStarts = batchWordsStartIdx[batch];\n const batchEnds = batchWordsEndIdx[batch];\n const batchSpans = spans[batch];\n if (!batchStarts || !batchEnds || !batchSpans) {\n continue;\n }\n\n if (\n prob >= threshold &&\n startToken < batchStarts.length &&\n endToken < batchEnds.length\n ) {\n const globalBatch = batchIds[batch] ?? 0;\n const startIdx = batchStarts[startToken] ?? 0;\n const endIdx = batchEnds[endToken] ?? 0;\n const spanText = (texts[globalBatch] ?? \"\").slice(startIdx, endIdx);\n batchSpans.push([\n spanText,\n startIdx,\n endIdx,\n idToClass[entity + 1] ?? \"\",\n prob,\n ]);\n }\n }\n\n return spans.map((batchSpans) =>\n greedySearch(batchSpans, flatNer, multiLabel),\n );\n};\n","/**\n * Token-level BIO decoder for GLiNER TokenGLiNER models\n * (e.g., gliner-pii-edge-v1.0).\n *\n * These models output logits of shape [B, L, C, 3] where:\n * B = batch size\n * L = number of words (text_lengths)\n * C = number of entity classes\n * 3 = BIO tags: [B(egin), I(nside), O(utside)]\n *\n * This decoder converts BIO-tagged logits into entity spans\n * with character offsets.\n */\nimport type { RawInferenceResult } from \"./types\";\n\ntype Span = [string, number, number, string, number];\n\nconst sigmoid = (x: number): number => 1 / (1 + Math.exp(-x));\n\nconst B_TAG = 0;\nconst I_TAG = 1;\n\n/**\n * Decode token-level BIO logits into entity spans.\n *\n * For each word, checks if the B(egin) logit for any class\n * exceeds the threshold. If so, extends the span by consuming\n * subsequent I(nside) tokens of the same class.\n */\nexport const decodeTokenSpans = (\n batchSize: number,\n numWords: number,\n numEntities: number,\n texts: string[],\n batchIds: number[],\n batchWordsStartIdx: number[][],\n batchWordsEndIdx: number[][],\n idToClass: Record<number, string>,\n modelOutput: ArrayLike<number>,\n threshold: number,\n): RawInferenceResult => {\n const results: Span[][] = Array.from({ length: batchSize }, () => []);\n\n const wordStride = numEntities * 3;\n const batchStride = numWords * wordStride;\n\n for (let b = 0; b < batchSize; b++) {\n const batchOffset = b * batchStride;\n const starts = batchWordsStartIdx[b];\n const ends = batchWordsEndIdx[b];\n const globalBatch = batchIds[b] ?? 0;\n const text = texts[globalBatch] ?? \"\";\n const batchSpans = results[b];\n\n if (!starts || !ends || !batchSpans) {\n continue;\n }\n\n const actualWords = starts.length;\n\n for (let e = 0; e < numEntities; e++) {\n let w = 0;\n\n while (w < actualWords) {\n const bLogitIdx = batchOffset + w * wordStride + e * 3 + B_TAG;\n const bScore = sigmoid(modelOutput[bLogitIdx] ?? 0);\n\n if (bScore < threshold) {\n w++;\n continue;\n }\n\n const spanStart = w;\n let spanEnd = w;\n let maxScore = bScore;\n\n while (spanEnd + 1 < actualWords) {\n const iLogitIdx =\n batchOffset + (spanEnd + 1) * wordStride + e * 3 + I_TAG;\n const iScore = sigmoid(modelOutput[iLogitIdx] ?? 0);\n\n if (iScore < threshold) {\n break;\n }\n\n spanEnd++;\n maxScore = Math.max(maxScore, iScore);\n }\n\n const charStart = starts[spanStart] ?? 0;\n const charEnd = ends[spanEnd] ?? 0;\n const spanText = text.slice(charStart, charEnd);\n const label = idToClass[e + 1] ?? \"\";\n\n if (spanText.trim().length > 0 && label) {\n batchSpans.push([spanText, charStart, charEnd, label, maxScore]);\n }\n\n w = spanEnd + 1;\n }\n }\n\n const selected: Span[] = [];\n for (const span of batchSpans.toSorted((x, y) => y[4] - x[4])) {\n const overlaps = selected.some((s) => span[1] < s[2] && span[2] > s[1]);\n if (!overlaps) {\n selected.push(span);\n }\n }\n\n results[b] = selected.toSorted((x, y) => x[1] - y[1]);\n }\n\n return results;\n};\n","/**\n * Tokenization and input preparation for GLiNER span model.\n *\n * Splits text into word tokens, encodes via HuggingFace\n * tokenizer, and builds the span index grid that the ONNX\n * model expects as input.\n */\nimport type { Encoding, Tokenizer } from \"@huggingface/tokenizers\";\n\nconst segmenter = new Intl.Segmenter(undefined, {\n granularity: \"word\",\n});\n\n/** Tokenize text into words with character offsets. */\nexport const tokenizeText = (\n text: string,\n): [words: string[], starts: number[], ends: number[]] => {\n const words: string[] = [];\n const starts: number[] = [];\n const ends: number[] = [];\n\n for (const { segment, index, isWordLike } of segmenter.segment(text)) {\n // Include words and numeric segments (Intl.Segmenter\n // marks pure digit sequences as non-word-like, but the\n // NER model needs them for entity detection).\n if (!isWordLike && !/\\d/u.test(segment)) {\n continue;\n }\n words.push(segment);\n starts.push(index);\n ends.push(index + segment.length);\n }\n\n return [words, starts, ends];\n};\n\n/** Build entity label <-> id mappings. */\nconst createMappings = (\n labels: string[],\n): {\n classToId: Record<string, number>;\n idToClass: Record<number, string>;\n} => {\n const classToId: Record<string, number> = {};\n const idToClass: Record<number, string> = {};\n for (let i = 0; i < labels.length; i++) {\n const label = labels[i];\n if (label === undefined) {\n continue;\n }\n const id = i + 1;\n classToId[label] = id;\n idToClass[id] = label;\n }\n return { classToId, idToClass };\n};\n\n/** Prepend entity prompt tokens to word tokens. */\nconst prepareTextInputs = (\n batchTokens: string[][],\n entities: string[],\n): [inputTexts: string[][], textLengths: number[], promptLengths: number[]] => {\n const inputTexts: string[][] = [];\n const promptLengths: number[] = [];\n const textLengths: number[] = [];\n\n for (const tokens of batchTokens) {\n textLengths.push(tokens.length);\n\n const prompt: string[] = [];\n for (const ent of entities) {\n prompt.push(\"<<ENT>>\");\n prompt.push(ent);\n }\n prompt.push(\"<<SEP>>\");\n\n promptLengths.push(prompt.length);\n inputTexts.push([...prompt, ...tokens]);\n }\n\n return [inputTexts, textLengths, promptLengths];\n};\n\n/** Encode word sequences into token IDs with masks. */\nconst encodeInputs = (\n tokenizer: Tokenizer,\n texts: string[][],\n promptLengths: number[],\n): [\n inputIds: number[][],\n attentionMasks: number[][],\n wordsMasks: number[][],\n] => {\n // Resolve special token IDs dynamically instead of\n // hardcoding DeBERTa-v3 values. Fallbacks (1, 2) match\n // the current model but future models may differ.\n const clsTokenId = tokenizer.token_to_id(\"[CLS]\") ?? 1;\n const sepTokenId = tokenizer.token_to_id(\"[SEP]\") ?? 2;\n\n const allInputIds: number[][] = [];\n const allAttentionMasks: number[][] = [];\n const allWordsMasks: number[][] = [];\n\n for (let idx = 0; idx < texts.length; idx++) {\n const promptLength = promptLengths[idx] ?? 0;\n const words = texts[idx];\n if (!words) {\n continue;\n }\n const wordsMask: number[] = [0];\n const inputIds: number[] = [clsTokenId];\n const attentionMask: number[] = [1];\n\n let wordCounter = 1;\n for (let wordId = 0; wordId < words.length; wordId++) {\n const word = words[wordId];\n if (word === undefined) {\n continue;\n }\n // encode() returns { ids, tokens, attention_mask }\n // with BOS/EOS; strip them with slice(1, -1)\n const encoded: Encoding = tokenizer.encode(word);\n const wordTokens: number[] = encoded.ids.slice(1, -1);\n\n for (let tokenId = 0; tokenId < wordTokens.length; tokenId++) {\n attentionMask.push(1);\n if (wordId < promptLength) {\n wordsMask.push(0);\n } else if (tokenId === 0) {\n wordsMask.push(wordCounter);\n wordCounter++;\n } else {\n wordsMask.push(0);\n }\n inputIds.push(wordTokens[tokenId] ?? 0);\n }\n }\n\n inputIds.push(sepTokenId);\n wordsMask.push(0);\n attentionMask.push(1);\n\n allInputIds.push(inputIds);\n allAttentionMasks.push(attentionMask);\n allWordsMasks.push(wordsMask);\n }\n\n return [allInputIds, allAttentionMasks, allWordsMasks];\n};\n\n/** Build span index pairs and masks for the model. */\nconst prepareSpans = (\n batchTokens: string[][],\n maxWidth: number,\n): { spanIdxs: number[][][]; spanMasks: boolean[][] } => {\n const spanIdxs: number[][][] = [];\n const spanMasks: boolean[][] = [];\n\n for (const tokens of batchTokens) {\n const len = tokens.length;\n const idx: number[][] = [];\n const mask: boolean[] = [];\n\n for (let i = 0; i < len; i++) {\n for (let j = 0; j < maxWidth; j++) {\n const endIdx = Math.min(i + j, len - 1);\n idx.push([i, endIdx]);\n mask.push(endIdx < len);\n }\n }\n\n spanIdxs.push(idx);\n spanMasks.push(mask);\n }\n\n return { spanIdxs, spanMasks };\n};\n\n/** Pad a 2D or 3D array to uniform inner length. */\nexport const padArray = <T>(arr: T[][], dimensions: number = 2): T[][] => {\n if (arr.length === 0) {\n return [];\n }\n const maxLength = Math.max(...arr.map((sub) => sub.length));\n // For 3D arrays, infer inner dimension from the first\n // non-empty element (arr[0] may be empty when a batch\n // element had zero word tokens).\n let finalDim = 0;\n if (dimensions === 3) {\n for (const sub of arr) {\n if (sub.length > 0) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- 3D arrays have number[] inner elements\n const inner = sub[0] as unknown as number[];\n finalDim = inner.length;\n break;\n }\n }\n }\n\n return arr.map((sub) => {\n const padCount = maxLength - sub.length;\n const fill =\n dimensions === 3\n ? Array.from({ length: padCount }, () =>\n Array.from<number>({ length: finalDim }).fill(0),\n )\n : Array.from<number>({ length: padCount }).fill(0);\n // SAFETY: fill values (zero arrays) match the shape of T\n // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- generic padding for ONNX tensor arrays\n return [...sub, ...(fill as T[])] as T[];\n });\n};\n\n/** Prepare a complete batch for ONNX inference. */\nexport const prepareBatch = (\n tokenizer: Tokenizer,\n texts: string[],\n entities: string[],\n maxWidth: number,\n): {\n inputsIds: number[][];\n attentionMasks: number[][];\n wordsMasks: number[][];\n textLengths: number[];\n spanIdxs: number[][][];\n spanMasks: boolean[][];\n idToClass: Record<number, string>;\n batchTokens: string[][];\n batchWordsStartIdx: number[][];\n batchWordsEndIdx: number[][];\n} => {\n const batchTokens: string[][] = [];\n const batchWordsStartIdx: number[][] = [];\n const batchWordsEndIdx: number[][] = [];\n\n for (const text of texts) {\n const [words, starts, ends] = tokenizeText(text);\n batchTokens.push(words);\n batchWordsStartIdx.push(starts);\n batchWordsEndIdx.push(ends);\n }\n\n const { idToClass } = createMappings(entities);\n\n const [inputTokens, textLengths, promptLengths] = prepareTextInputs(\n batchTokens,\n entities,\n );\n\n let [inputsIds, attentionMasks, wordsMasks] = encodeInputs(\n tokenizer,\n inputTokens,\n promptLengths,\n );\n\n inputsIds = padArray(inputsIds);\n attentionMasks = padArray(attentionMasks);\n wordsMasks = padArray(wordsMasks);\n\n let { spanIdxs, spanMasks } = prepareSpans(batchTokens, maxWidth);\n\n spanIdxs = padArray(spanIdxs, 3);\n spanMasks = padArray(spanMasks);\n\n return {\n inputsIds,\n attentionMasks,\n wordsMasks,\n textLengths,\n spanIdxs,\n spanMasks,\n idToClass,\n batchTokens,\n batchWordsStartIdx,\n batchWordsEndIdx,\n };\n};\n","import type { Entity } from \"../types\";\n\nconst MAX_CHUNK_CHARS = 1500;\nconst OVERLAP_CHARS = 50;\nconst MIN_CHUNK_LENGTH = 10;\n\n/**\n * Split text into overlapping chunks for GLiNER's\n * ~512 token context window. Character-based splitting\n * (rough approximation of token limits).\n *\n * Tries to break at sentence boundaries when possible.\n */\nexport const chunkText = (text: string): string[] => {\n const chunks: string[] = [];\n let offset = 0;\n\n while (offset < text.length) {\n let end = Math.min(offset + MAX_CHUNK_CHARS, text.length);\n\n if (end < text.length) {\n const slice = text.slice(offset, end);\n const lastPeriod = slice.lastIndexOf(\". \");\n if (lastPeriod > MAX_CHUNK_CHARS * 0.5) {\n end = offset + lastPeriod + 2;\n }\n }\n\n const chunk = text.slice(offset, end);\n if (chunk.trim().length > MIN_CHUNK_LENGTH) {\n chunks.push(chunk);\n }\n offset = Math.max(offset + 1, end - OVERLAP_CHARS);\n }\n\n return chunks;\n};\n\n/**\n * Compute the byte offset of each chunk within the\n * original document text.\n */\nexport const computeChunkOffsets = (\n fullText: string,\n chunks: string[],\n): number[] => {\n const offsets: number[] = [];\n let searchFrom = 0;\n\n for (const chunk of chunks) {\n const idx = fullText.indexOf(chunk, searchFrom);\n offsets.push(idx !== -1 ? idx : searchFrom);\n searchFrom =\n idx !== -1 ? idx + Math.max(1, chunk.length - OVERLAP_CHARS) : searchFrom;\n }\n\n return offsets;\n};\n\nconst POSITION_THRESHOLD = 5;\n\n/**\n * Merge entities from overlapping chunks back to\n * document-level offsets. Deduplicates entities that\n * appear in overlap regions (keeps highest score).\n *\n * Dedup invariant: each incoming entity is compared\n * against the highest-scored same-label near-dup in\n * its proximity window. If it loses, it is dropped.\n * This does NOT guarantee that all pairwise near-dup\n * relationships in the output are resolved; a lower-\n * scored entity can survive if the bridging entity\n * that would have replaced it was itself dropped by\n * a higher-scored match.\n *\n * Uses a reverse-scan over the sorted merged array\n * so each entity only compares against nearby\n * predecessors — O(n * w) average where w is the max\n * entities per POSITION_THRESHOLD window, O(n²) worst\n * case when replacements dominate (splice is O(n)).\n */\nexport const mergeChunkEntities = (\n chunkOffsets: number[],\n chunkResults: Entity[][],\n): Entity[] => {\n const allEntities: Entity[] = [];\n\n for (let i = 0; i < chunkResults.length; i++) {\n const offset = chunkOffsets[i] ?? 0;\n const entities = chunkResults[i];\n if (!entities) {\n continue;\n }\n for (const entity of entities) {\n allEntities.push({\n ...entity,\n start: entity.start + offset,\n end: entity.end + offset,\n });\n }\n }\n\n const sorted = allEntities.toSorted((a, b) => a.start - b.start);\n const merged: Entity[] = [];\n\n for (const entity of sorted) {\n let bestDupIndex = -1;\n let bestDupScore = -1;\n\n // Reverse-scan the full proximity window. Collect\n // the highest-scored same-label match so we dedup\n // against the strongest existing entity, not just\n // the nearest one.\n for (let j = merged.length - 1; j >= 0; j--) {\n const existing = merged[j];\n if (existing === undefined) {\n continue;\n }\n // merged is kept sorted by start (splice+push\n // maintains this); elements further back have\n // even smaller starts, so we can break early.\n if (entity.start - existing.start >= POSITION_THRESHOLD) {\n break;\n }\n if (\n existing.label === entity.label &&\n Math.abs(existing.end - entity.end) < POSITION_THRESHOLD &&\n existing.score > bestDupScore\n ) {\n bestDupIndex = j;\n bestDupScore = existing.score;\n }\n }\n\n if (bestDupIndex !== -1) {\n const existing = merged[bestDupIndex];\n if (existing !== undefined && entity.score > existing.score) {\n // Replace with winner. Splice out the old entry\n // and re-insert at the end to maintain sorted\n // order (entity.start >= all prior starts).\n merged.splice(bestDupIndex, 1);\n merged.push({ ...entity });\n }\n // Entity loses to the best match and is dropped.\n // Other lower-scored near-dups of this entity are\n // not revisited (see docstring dedup invariant).\n } else {\n merged.push({ ...entity });\n }\n }\n\n return merged;\n};\n","/**\n * Compute the Levenshtein edit distance between two\n * strings. O(n*m) time, O(min(n,m)) space using a\n * single-row DP approach.\n */\nexport const levenshtein = (rawA: string, rawB: string): number => {\n if (rawA === rawB) {\n return 0;\n }\n if (rawA.length === 0) {\n return rawB.length;\n }\n if (rawB.length === 0) {\n return rawA.length;\n }\n\n const [shorter, longer] =\n rawA.length <= rawB.length ? [rawA, rawB] : [rawB, rawA];\n\n const aLen = shorter.length;\n const bLen = longer.length;\n const row = new Uint16Array(aLen + 1);\n\n for (let i = 0; i <= aLen; i++) {\n row[i] = i;\n }\n\n for (let j = 1; j <= bLen; j++) {\n let prev = row[0] ?? 0;\n row[0] = j;\n\n for (let i = 1; i <= aLen; i++) {\n const cost = shorter[i - 1] === longer[j - 1] ? 0 : 1;\n const temp = row[i] ?? 0;\n row[i] = Math.min((row[i] ?? 0) + 1, (row[i - 1] ?? 0) + 1, prev + cost);\n prev = temp;\n }\n }\n\n return row[aLen] ?? 0;\n};\n","/* Browser/WASM entry point — loads TextSearch\n * from @stll/text-search-wasm and re-exports\n * the full anonymize API. */\n\nimport { TextSearch } from \"@stll/text-search-wasm\";\n\nimport { initTextSearch } from \"./search-engine\";\n\ninitTextSearch(TextSearch);\n\nexport * from \"./index-shared\";\n"],"mappings":"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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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,KAAK,MAAM,SAAS,YAAY;EAC9B,MAAM,MAAM,MAAM;EAClB,IAAI,MAAM,cAAc,OAAO,UAAU;EAEzC,MAAM,WAAW,MAAM;EACvB,MAAM,QAAQ,KAAK,OAAO;EAC1B,IAAI,CAAC,OAAO;EAEZ,IAAI,CAAC,mBAAmB,UAAU,MAAM,KAAK,GAAG;EAEhD,QAAQ,KAAK;GACX,OAAO,MAAM;GACb,KAAK,MAAM;GACX;GACA,MAAM,SAAS,MAAM,MAAM,OAAO,MAAM,GAAG;GAC3C,OAAO;GACP,QAAQ,kBAAkB;EAC5B,CAAC;CACH;CAEA,OAAO;AACT;;;;;;;;ACzPA,MAAa,iBAAiB;CAE5B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;AACF;;;;;;AAOA,MAAa,aAAa;CACxB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;AAMA,MAAa,qBAAqB,IAAI,IAAI;CACxC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;;;;;;AAaD,MAAa,yBAAyB,IAAI,IAAI;CAC5C;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;AAMD,MAAa,gBAAgB;CAC3B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAKA;CACA;AACF;AAEA,MAAa,wBAAwB;CACnC,IAAI;EACF;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,IAAI;EACF;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,WAAW;EACT;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,WAAW;EAAC;EAAQ;EAAS;EAAW;EAAO;CAAK;CACpD,IAAI,CAAC,YAAY,OAAO;CACxB,IAAI;EACF;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,IAAI;EAAC;EAAO;EAAO;EAAM;CAAI;CAC7B,KAAK;EACH;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,IAAI;EACF;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF;;;;;;;;;;AC9SA,MAAa,iBAAiB;;AAG9B,MAAa,eAAe;AAE5B,MAAM,kBAAkB;;;;;;;AAQxB,MAAa,mBAAmB,MAAc,QAAyB;CACrE,IAAI,QAAQ,GACV,OAAO;CAET,IAAI,IAAI,MAAM;CACd,OAAO,KAAK,KAAK,KAAK,KAAK,KAAK,MAAM,EAAE,GACtC;CAEF,IAAI,IAAI,GACN,OAAO;CAET,OAAO,gBAAgB,KAAK,KAAK,MAAM,EAAE;AAC3C;;;ACdA,MAAM,aAAa,QACjB,IAAI;AAGN,MAAa,2BACX,MAAuB,mBACD,IAAI,YAAY,kBAAkB,CAAC;AAC3D,MAAa,yBACX,MAAuB,mBACD,IAAI,YAAY,gBAAgB,CAAC;AACzD,MAAa,uBACX,MAAuB,mBACD,IAAI,YAAY,cAAc,CAAC;AAIvD,MAAa,gCACX,MAAuB,mBACD,IAAI,YAAY,uBAAuB,CAAC;AAEhE,MAAM,yBAAyB;CA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ACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,UAAU;EACR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,aAAa;EACX;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,MAAM;EACJ;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,kBAAkB;EAChB;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,IAAI;EACF;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,MAAM;EAAC;EAAM;EAAM;CAAI;CACvB,SAAS;EAAC;EAAM;EAAM;EAAM;EAAM;CAAI;CACtC,KAAK;EAAC;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;CAAI;CACtE,aAAa;EAAC;EAAM;EAAM;EAAM;EAAM;EAAM;CAAI;CAChD,SAAS;EAAC;EAAM;EAAM;CAAI;CAC1B,YAAY;EAAC;EAAM;EAAM;EAAM;EAAM;EAAM;CAAI;CAC/C,WAAW;EAAC;EAAM;EAAM;EAAM;EAAM;CAAI;CACxC,UAAU;EAAC;EAAM;EAAM;EAAM;CAAI;CACjC,eAAe;EAAC;EAAM;EAAM;EAAM;EAAM;EAAM;CAAI;CAClD,SAAS,CAAC,MAAM,IAAI;AACtB;;;;;;AAmBA,MAAa,oBACX,SACA,cACuB;CACvB,MAAM,aAAa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"}