@stll/anonymize 1.5.0 → 2.0.0-alpha.1

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.
Files changed (41) hide show
  1. package/ATTRIBUTION.md +70 -0
  2. package/README.md +86 -48
  3. package/dist/address-boundaries.mjs +2 -0
  4. package/dist/address-jurisdiction-prefixes.mjs +16 -0
  5. package/dist/address-jurisdiction-prefixes.mjs.map +1 -0
  6. package/dist/address-stop-keywords.mjs +12 -1
  7. package/dist/address-unit-abbreviations.mjs +15 -0
  8. package/dist/address-unit-abbreviations.mjs.map +1 -0
  9. package/dist/clause-noun-heads.mjs +5 -1
  10. package/dist/coreference-org-determiners.mjs +19 -0
  11. package/dist/coreference-org-determiners.mjs.map +1 -0
  12. package/dist/defined-term-heads.mjs +15 -0
  13. package/dist/defined-term-heads.mjs.map +1 -0
  14. package/dist/false-positive-shapes.mjs +36 -0
  15. package/dist/false-positive-shapes.mjs.map +1 -0
  16. package/dist/index.d.mts +3 -1201
  17. package/dist/index.mjs +3 -16267
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/legal-role-heads.cs.mjs +6 -0
  20. package/dist/native-node.d.mts +124 -0
  21. package/dist/native-node.mjs +3 -0
  22. package/dist/native-node2.d.mts +3 -0
  23. package/dist/native-node2.mjs +13353 -0
  24. package/dist/native-node2.mjs.map +1 -0
  25. package/dist/native.d.mts +1158 -0
  26. package/dist/native.mjs +230 -0
  27. package/dist/native.mjs.map +1 -0
  28. package/dist/native2.d.mts +2 -0
  29. package/dist/organization-unit-heads.mjs +20 -0
  30. package/dist/organization-unit-heads.mjs.map +1 -0
  31. package/dist/person-stopwords.mjs +205 -199
  32. package/dist/signing-clauses.mjs +33 -9
  33. package/index.cjs +3 -0
  34. package/native-pipeline.cs.stlanonpkg +0 -0
  35. package/native-pipeline.de.stlanonpkg +0 -0
  36. package/native-pipeline.en.stlanonpkg +0 -0
  37. package/native-pipeline.stlanonpkg +0 -0
  38. package/package.json +40 -6
  39. package/scripts/build-native-pipeline-package.mjs +225 -0
  40. package/dist/address-prepositions.mjs +0 -182
  41. package/dist/address-prepositions.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-node2.mjs","names":["languageScopes","charGroupsJson","amountWordsConfig","URL","m","_exhaustive","m","denyListFiltersByLanguage","countriesData","addressContextJson","addressStreetTypesJson","addressPrepositionsJson","DEFAULT_CUSTOM_REGEX_SCORE","nameCorpusCjk","nameCorpusParticles","organizationIndicators","triggerSupport","signatureDetection","legalFormRuleWords.connectorWords","legalFormRuleWords.andConnectorWords","legalFormRuleWords.inNamePrepositions","legalFormRuleWords.companySuffixWords","legalFormRuleWords.commaGatedDirectPrefixes","_exhaustive","moduleValue","nativePackageVersionWithBinding","normalizeForSearchWithBinding","prepareSearchPackageWithBinding","loadPreparedPackageWithBinding","redactTextWithBinding","redactTextJsonWithBinding","redactTextStreamJsonWithBinding","diagnosticsJsonWithBinding","diagnosticsStreamJsonWithBinding","summaryDiagnosticsJsonWithBinding"],"sources":["../src/data/legal-form-rule-words.json","../src/data/name-corpus-cjk.json","../src/data/name-corpus-particles.json","../src/data/organization-indicators.json","../src/data/signature-detection.json","../src/data/trigger-support.json","../src/types.ts","../src/data/language-scopes.json","../src/language-scope.ts","../src/context.ts","../src/config/titles.ts","../src/config/legal-forms.ts","../src/util/language-selection.ts","../src/util/lang-loader.ts","../src/data/amount-words.json","../src/data/char-groups.json","../src/util/char-groups.ts","../src/detectors/regex.ts","../src/detectors/legal-forms.ts","../src/detectors/signatures.ts","../src/detectors/triggers.ts","../src/detectors/names.ts","../src/regions.ts","../src/filters/false-positives.ts","../src/detectors/address-seeds.ts","../src/util/normalize.ts","../src/data/deny-list-filters.json","../src/detectors/deny-list.ts","../src/detectors/gazetteer.ts","../src/data/ambiguous-country-surfaces.json","../src/data/countries.json","../src/detectors/countries.ts","../src/filters/hotword-rules.ts","../src/data/address-context.json","../src/data/address-prepositions.json","../src/data/address-street-types.json","../src/filters/confidence-boost.ts","../src/build-unified-search.ts","../src/pipeline-cache-key.ts","../src/native-pipeline.ts","../src/native-default-config.ts","../src/native-node.ts"],"sourcesContent":["","","","","","","// 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 | {\n type: \"valid-id\";\n validator: ValidIdValidator;\n check: (value: string) => boolean;\n };\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 preparedArtifactPolicy?: \"include\" | \"omit\";\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 * Expected content language codes. When present, these\n * derive default dictionary scopes for name corpus and\n * deny-list matching unless the lower-level scope fields\n * below are set explicitly.\n */\n languages?: string[];\n /**\n * Convenience form for single-language documents. Ignored\n * when `languages` is also provided.\n */\n language?: string;\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 languageScopes from \"./data/language-scopes.json\";\n\nimport type { PipelineConfig } from \"./types\";\n\ntype LanguageScope = {\n nameCorpusLanguages?: readonly string[];\n denyListCountries?: readonly string[];\n};\n\ntype LanguageScopeData = {\n languages: Record<string, LanguageScope>;\n};\n\nconst scopeData = languageScopes as LanguageScopeData;\n\nconst normalizeLanguage = (language: string): string =>\n language.trim().toLowerCase();\n\nconst fallbackLanguage = (language: string): string | null => {\n const index = language.indexOf(\"-\");\n return index === -1 ? null : language.slice(0, index);\n};\n\nconst uniquePush = (target: string[], values: readonly string[]): void => {\n const seen = new Set(target);\n for (const value of values) {\n if (seen.has(value)) {\n continue;\n }\n seen.add(value);\n target.push(value);\n }\n};\n\nconst resolveLanguageScope = (language: string): LanguageScope | null => {\n const normalized = normalizeLanguage(language);\n if (normalized.length === 0) {\n return null;\n }\n const exact = scopeData.languages[normalized];\n if (exact !== undefined) {\n return exact;\n }\n const fallback = fallbackLanguage(normalized);\n return fallback === null ? null : (scopeData.languages[fallback] ?? null);\n};\n\nconst configuredLanguages = (config: PipelineConfig): readonly string[] => {\n if (config.languages !== undefined) {\n return config.languages;\n }\n return config.language === undefined ? [] : [config.language];\n};\n\nexport const configuredContentLanguages = (\n config: Pick<PipelineConfig, \"language\" | \"languages\">,\n): readonly string[] | undefined => {\n if (config.languages !== undefined) {\n return config.languages;\n }\n return config.language === undefined ? undefined : [config.language];\n};\n\nexport const applyPipelineLanguageScope = (\n config: PipelineConfig,\n): PipelineConfig => {\n const languages = configuredLanguages(config);\n if (languages.length === 0) {\n return config;\n }\n\n const nameCorpusLanguages: string[] = [];\n const denyListCountries: string[] = [];\n for (const language of languages) {\n const scope = resolveLanguageScope(language);\n if (scope === null) {\n continue;\n }\n uniquePush(nameCorpusLanguages, scope.nameCorpusLanguages ?? []);\n uniquePush(denyListCountries, scope.denyListCountries ?? []);\n }\n\n const next: Partial<PipelineConfig> = {};\n if (\n config.nameCorpusLanguages === undefined &&\n nameCorpusLanguages.length > 0\n ) {\n next.nameCorpusLanguages = nameCorpusLanguages;\n }\n if (config.denyListCountries === undefined && denyListCountries.length > 0) {\n next.denyListCountries = denyListCountries;\n }\n\n return Object.keys(next).length === 0 ? config : { ...config, ...next };\n};\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 /** Lowercased common English words. A name chain whose\n * every token is a common word (e.g. \"Loan Documents\",\n * where \"Loan\" coincides with a Vietnamese given name)\n * is treated as a common-word phrase, not a person. */\n commonWords: 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 nativePipelinePackage: Uint8Array | null;\n nativePipelinePackageKey: string;\n nativePipelinePackagePromise: Promise<Uint8Array> | 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 definedTermHeads: ReadonlySet<string> | null;\n definedTermHeadsPromise: 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 corefPatternsKey: string;\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 nativePipelinePackage: null,\n nativePipelinePackageKey: \"\",\n nativePipelinePackagePromise: 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 definedTermHeads: null,\n definedTermHeadsPromise: 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 corefPatternsKey: \"\",\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 * 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 * 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","const normalizeLanguageCode = (language: string): string =>\n language.trim().toLowerCase();\n\nconst normalizeLanguageSelection = (\n languages: readonly string[] | undefined,\n): string[] =>\n languages === undefined\n ? []\n : languages\n .map(normalizeLanguageCode)\n .filter((language) => language.length > 0);\n\nexport const languageSelectionKey = (\n languages: readonly string[] | undefined,\n): string => {\n const normalized = normalizeLanguageSelection(languages).toSorted();\n return normalized.length === 0 ? \"*\" : normalized.join(\",\");\n};\n\nconst baseLanguage = (language: string): string => {\n const index = language.indexOf(\"-\");\n return index === -1 ? language : language.slice(0, index);\n};\n\nexport const languageConfigMatches = (\n configLanguage: string,\n selectedLanguages: readonly string[] | undefined,\n): boolean => {\n if (selectedLanguages === undefined || selectedLanguages.length === 0) {\n return true;\n }\n const normalizedSelectedLanguages =\n normalizeLanguageSelection(selectedLanguages);\n if (normalizedSelectedLanguages.length === 0) {\n return true;\n }\n\n const normalizedConfigLanguage = normalizeLanguageCode(configLanguage);\n if (normalizedConfigLanguage.length === 0) {\n return false;\n }\n\n const genericConfig =\n baseLanguage(normalizedConfigLanguage) === normalizedConfigLanguage;\n for (const normalizedLanguage of normalizedSelectedLanguages) {\n if (normalizedLanguage === normalizedConfigLanguage) {\n return true;\n }\n if (\n genericConfig &&\n baseLanguage(normalizedLanguage) === normalizedConfigLanguage\n ) {\n return true;\n }\n }\n\n return false;\n};\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\nimport { languageConfigMatches } from \"./language-selection\";\n\nexport { languageConfigMatches } from \"./language-selection\";\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\ntype LoadLanguageConfigOptions = {\n languages?: readonly string[];\n};\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 options: LoadLanguageConfigOptions = {},\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 configuredCodes = 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 const codes = configuredCodes.filter((code) =>\n languageConfigMatches(code, options.languages),\n );\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 * 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","import type { Match, PatternEntry } 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 crypto,\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;\nconst CONTEXT_REGEX_PREFILTER_WINDOW_BYTES = 160;\nconst US_STATE_CODE =\n \"(?:(?i:A[KLZR]|C[AOT]|D[CE]|FL|GA|HI|I[ADL]|K[SY]|LA|M[ADEHINOPST]|\" +\n \"N[CDEHJMVY]|O[HK]|PA|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])|I[Nn]|iN|O[Rr]|oR)\";\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 utf8ByteLength = (text: string): number => {\n let length = 0;\n for (const char of text) {\n const codePoint = char.codePointAt(0);\n if (codePoint === undefined) {\n continue;\n }\n if (codePoint <= 0x7f) {\n length += 1;\n } else if (codePoint <= 0x7ff) {\n length += 2;\n } else if (codePoint <= 0xffff) {\n length += 3;\n } else {\n length += 4;\n }\n }\n return length;\n};\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 minByteLength?: number;\n /** Post-match stdnum validator for confirmation. */\n validator?: Validator;\n validatorId?: string;\n /** Extract the identifier portion when context is part of the regex span. */\n validatorInput?: (text: string) => string;\n validatorInputKind?: \"digits-only\" | \"crypto-wallet-candidate\";\n};\n\ntype RegexDef = {\n pattern: string;\n label: string;\n score: number;\n minByteLength?: number;\n lazy?: true;\n prefilterAny?: readonly string[];\n prefilterCaseInsensitive?: boolean;\n prefilterRegex?: RegExp;\n prefilterWindowBytes?: number;\n preparedArtifactPolicy?: \"include\" | \"omit\";\n validator?: Validator;\n validatorId?: string;\n validatorInput?: (text: string) => string;\n validatorInputKind?: \"digits-only\" | \"crypto-wallet-candidate\";\n};\n\ntype RegexPatternEntry = {\n pattern: string;\n literal?: false;\n lazy?: true;\n prefilterAny?: readonly string[];\n prefilterCaseInsensitive?: boolean;\n prefilterRegex?: RegExp;\n prefilterWindowBytes?: number;\n preparedArtifactPolicy?: \"include\" | \"omit\";\n};\n\ntype AmountWordsConfig = {\n patterns?: Array<{\n lang: string;\n keywords: string[];\n }>;\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\nconst DIGITS_ONLY_VALIDATOR_INPUT = (text: string): string =>\n text.replace(/\\D/g, \"\");\n\nconst VALIDATOR_IDS = new Map<Validator, string>([\n [at.businessid, \"at.businessid\"],\n [at.tin, \"at.tin\"],\n [at.uid, \"at.uid\"],\n [au.abn, \"au.abn\"],\n [au.acn, \"au.acn\"],\n [be.nn, \"be.nn\"],\n [be.vat, \"be.vat\"],\n [bg.vat, \"bg.vat\"],\n [br.cnpj, \"br.cnpj\"],\n [br.cpf, \"br.cpf\"],\n [ch.uid, \"ch.uid\"],\n [cn.ric, \"cn.ric\"],\n [crypto.wallet, \"crypto.wallet\"],\n [cy.vat, \"cy.vat\"],\n [cz.dic, \"cz.dic\"],\n [cz.rc, \"cz.rc\"],\n [de.idnr, \"de.idnr\"],\n [de.stnr, \"de.stnr\"],\n [de.svnr, \"de.svnr\"],\n [de.vat, \"de.vat\"],\n [dk.cpr, \"dk.cpr\"],\n [dk.vat, \"dk.vat\"],\n [ee.ik, \"ee.ik\"],\n [ee.vat, \"ee.vat\"],\n [es.cif, \"es.cif\"],\n [es.dni, \"es.dni\"],\n [es.nie, \"es.nie\"],\n [es.nss, \"es.nss\"],\n [es.vat, \"es.vat\"],\n [fi.hetu, \"fi.hetu\"],\n [fi.vat, \"fi.vat\"],\n [fi.ytunnus, \"fi.ytunnus\"],\n [fr.nir, \"fr.nir\"],\n [fr.siren, \"fr.siren\"],\n [fr.siret, \"fr.siret\"],\n [fr.tva, \"fr.tva\"],\n [gb.nhs, \"gb.nhs\"],\n [gb.nino, \"gb.nino\"],\n [gb.vat, \"gb.vat\"],\n [gr.vat, \"gr.vat\"],\n [hr.vat, \"hr.vat\"],\n [hu.vat, \"hu.vat\"],\n [ie.pps, \"ie.pps\"],\n [ie.vat, \"ie.vat\"],\n [it.codiceFiscale, \"it.codiceFiscale\"],\n [it.iva, \"it.iva\"],\n [lt.asmens, \"lt.asmens\"],\n [lt.vat, \"lt.vat\"],\n [lu.vat, \"lu.vat\"],\n [lv.vat, \"lv.vat\"],\n [mt.vat, \"mt.vat\"],\n [nl.vat, \"nl.vat\"],\n [no.mva, \"no.mva\"],\n [no.orgnr, \"no.orgnr\"],\n [pl.nip, \"pl.nip\"],\n [pl.pesel, \"pl.pesel\"],\n [pt.cc, \"pt.cc\"],\n [pt.vat, \"pt.vat\"],\n [ro.cnp, \"ro.cnp\"],\n [ro.vat, \"ro.vat\"],\n [se.personnummer, \"se.personnummer\"],\n [si.vat, \"si.vat\"],\n [sk.dic, \"sk.dic\"],\n [us.ein, \"us.ein\"],\n]);\n\nexport const NATIVE_REGEX_VALIDATOR_IDS: ReadonlySet<string> = new Set([\n \"au.abn\",\n \"au.acn\",\n \"at.businessid\",\n \"at.tin\",\n \"at.uid\",\n \"be.nn\",\n \"be.vat\",\n \"bg.vat\",\n \"br.cnpj\",\n \"br.cpf\",\n \"ch.uid\",\n \"cn.ric\",\n \"crypto.wallet\",\n \"cy.vat\",\n \"cz.dic\",\n \"cz.rc\",\n \"de.idnr\",\n \"de.stnr\",\n \"de.svnr\",\n \"de.vat\",\n \"dk.cpr\",\n \"dk.vat\",\n \"ee.ik\",\n \"ee.vat\",\n \"es.cif\",\n \"es.dni\",\n \"es.nie\",\n \"es.nss\",\n \"es.vat\",\n \"fi.hetu\",\n \"fi.vat\",\n \"fi.ytunnus\",\n \"fr.nir\",\n \"fr.siren\",\n \"fr.siret\",\n \"fr.tva\",\n \"gb.nhs\",\n \"gb.nino\",\n \"gb.vat\",\n \"gr.vat\",\n \"hr.vat\",\n \"hu.vat\",\n \"ie.pps\",\n \"ie.vat\",\n \"it.codiceFiscale\",\n \"it.iva\",\n \"lt.asmens\",\n \"lt.vat\",\n \"lu.vat\",\n \"lv.vat\",\n \"mt.vat\",\n \"nl.vat\",\n \"no.mva\",\n \"no.orgnr\",\n \"pl.nip\",\n \"pl.pesel\",\n \"pt.cc\",\n \"pt.vat\",\n \"ro.cnp\",\n \"ro.vat\",\n \"se.personnummer\",\n \"si.vat\",\n \"sk.dic\",\n \"us.ein\",\n \"us.rtn\",\n]);\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 lazy?: true;\n prefilterAny?: readonly string[];\n prefilterCaseInsensitive?: boolean;\n preparedArtifactPolicy?: \"include\" | \"omit\";\n};\n\nconst PREFIXED_STDNUM_PATTERN = /^\\(\\?<!\\\\w\\)([A-Z]{1,4})(?:\\[|\\\\|\\()/u;\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 const prefilterAny = stdnumPrefixPrefilter(pattern);\n if (prefilterAny === undefined) {\n return {\n validator,\n label,\n score,\n pattern,\n };\n }\n return {\n validator,\n label,\n score,\n pattern,\n lazy: true,\n preparedArtifactPolicy: \"omit\",\n prefilterAny,\n prefilterCaseInsensitive: false,\n };\n};\n\nconst stdnumPrefixPrefilter = (\n pattern: string,\n): readonly string[] | undefined => {\n const prefix = PREFIXED_STDNUM_PATTERN.exec(pattern)?.at(1);\n return prefix === undefined ? undefined : [prefix];\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 lazy: true,\n preparedArtifactPolicy: \"omit\",\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 lazy: true,\n prefilterAny: TITLE_PREFIXES,\n prefilterCaseInsensitive: false,\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 lazy: true,\n prefilterAny: HONORIFICS,\n prefilterCaseInsensitive: false,\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 lazy: true,\n prefilterAny: [\"KC\", \"QC\"],\n prefilterCaseInsensitive: false,\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 lazy: true,\n prefilterAny: [\"@\"],\n prefilterCaseInsensitive: false,\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 minByteLength: MIN_PHONE_LENGTH,\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 minByteLength: MIN_PHONE_LENGTH,\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 minByteLength: MIN_PHONE_LENGTH,\n lazy: true,\n prefilterAny: [\"tel\", \"telefon\"],\n prefilterCaseInsensitive: true,\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 minByteLength: MIN_PHONE_LENGTH,\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 `\\\\b[Oo][Dd][Dd][Íí][Ll][^\\\\S\\\\n]+[A-Za-z]` +\n `[^\\\\S\\\\n]*,[^\\\\S\\\\n]*` +\n `[Vv][Ll][Oo][Žž][Kk][Aa][^\\\\S\\\\n]+\\\\d{1,6}\\\\b`,\n label: \"registration number\",\n score: 0.95,\n lazy: true,\n prefilterAny: [\"oddíl\", \"vložka\"],\n prefilterCaseInsensitive: true,\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 minByteLength: MIN_PHONE_LENGTH,\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 lazy: true,\n prefilterAny: [\"C.P\", \"CP\", \"código postal\", \"codigo postal\"],\n prefilterCaseInsensitive: true,\n prefilterWindowBytes: CONTEXT_REGEX_PREFILTER_WINDOW_BYTES,\n preparedArtifactPolicy: \"omit\",\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 NHS_NUMBER_CONTEXT: RegexDef = {\n pattern:\n \"\\\\b(?:(?i:NHS)(?:[^\\\\S\\\\n]+(?:(?i:number)|(?i:no\\\\.?)|#))?|\" +\n \"(?i:National)[^\\\\S\\\\n]+(?i:Health)[^\\\\S\\\\n]+(?i:Service)\" +\n \"[^\\\\S\\\\n]+(?:(?i:number)|(?i:no\\\\.?)|#))\" +\n \"[^\\\\S\\\\n]{0,4}:?[^\\\\S\\\\n]{0,4}\\\\d{3}[^\\\\S\\\\n]?\\\\d{3}[^\\\\S\\\\n]?\\\\d{4}\\\\b\",\n label: \"national identification number\",\n score: 0.95,\n validator: gb.nhs,\n validatorInput: DIGITS_ONLY_VALIDATOR_INPUT,\n validatorInputKind: \"digits-only\",\n lazy: true,\n prefilterAny: [\"NHS\", \"National Health Service\"],\n prefilterCaseInsensitive: true,\n prefilterWindowBytes: CONTEXT_REGEX_PREFILTER_WINDOW_BYTES,\n preparedArtifactPolicy: \"omit\",\n};\n\nconst PASSPORT_CONTEXT: RegexDef = {\n pattern:\n `\\\\b(?:(?:(?i:U\\\\.?S\\\\.?)|(?i:USA)|(?i:United)[^\\\\S\\\\n]+(?i:States)|` +\n `(?i:UK)|(?i:U\\\\.?K\\\\.?)|(?i:GBR)|(?i:British)|(?i:EU)|` +\n `(?i:European)|(?i:France)|(?i:French))[^\\\\S\\\\n]{1,4})?` +\n `(?i:passports?)` +\n `(?:[^\\\\S\\\\n]+(?:(?i:number)|(?i:no\\\\.?)|#))?` +\n `[^\\\\S\\\\n]{0,4}:?[^\\\\S\\\\n]{0,4}` +\n `(?:[A-Za-z]{1,2}\\\\d{6,8}|\\\\d{2}[A-Za-z]{2}\\\\d{5}|\\\\d{7,9})\\\\b`,\n label: \"passport number\",\n score: 0.96,\n lazy: true,\n prefilterAny: [\"passport\"],\n prefilterCaseInsensitive: true,\n prefilterWindowBytes: CONTEXT_REGEX_PREFILTER_WINDOW_BYTES,\n preparedArtifactPolicy: \"omit\",\n};\n\nconst FR_CNI_CONTEXT: RegexDef = {\n pattern:\n `\\\\b(?:(?i:CNI)|(?i:carte)[^\\\\S\\\\n]+(?i:nationale)[^\\\\S\\\\n]+(?i:d)[’'](?i:identit[ée])|` +\n `(?i:French)[^\\\\S\\\\n]+(?i:national)[^\\\\S\\\\n]+(?i:identity)[^\\\\S\\\\n]+(?i:card))` +\n `(?:` +\n `[^\\\\S\\\\n]+(?:(?i:number)|(?i:no\\\\.?)|(?i:n[°ºo]\\\\.?))` +\n `[^\\\\S\\\\n]{0,4}:?[^\\\\S\\\\n]{0,4}[A-Za-z0-9]{9,12}` +\n `|[^\\\\S\\\\n]{0,4}:?[^\\\\S\\\\n]{0,4}` +\n `(?=[A-Za-z0-9]{0,11}\\\\d)[A-Za-z0-9]{9,12}` +\n `)\\\\b`,\n label: \"identity card number\",\n score: 0.96,\n lazy: true,\n prefilterAny: [\"CNI\", \"carte nationale\", \"French national identity card\"],\n prefilterCaseInsensitive: true,\n prefilterWindowBytes: CONTEXT_REGEX_PREFILTER_WINDOW_BYTES,\n preparedArtifactPolicy: \"omit\",\n};\n\nconst CY_TIC_CONTEXT: RegexDef = {\n pattern:\n `\\\\b(?:(?:(?i:Cyprus)|(?i:Cypriot))[^\\\\S\\\\n]{1,4})?` +\n `(?:(?i:TIC)|(?i:tax)[^\\\\S\\\\n]+(?i:identification)[^\\\\S\\\\n]+(?i:code))` +\n `(?:[^\\\\S\\\\n]+(?:(?i:number)|(?i:no\\\\.?)|#))?` +\n `[^\\\\S\\\\n]{0,4}:?[^\\\\S\\\\n]{0,4}` +\n `\\\\d{8}[A-Za-z]\\\\b`,\n label: \"tax identification number\",\n score: 0.96,\n lazy: true,\n prefilterAny: [\"TIC\", \"tax identification code\"],\n prefilterCaseInsensitive: true,\n prefilterWindowBytes: CONTEXT_REGEX_PREFILTER_WINDOW_BYTES,\n preparedArtifactPolicy: \"omit\",\n};\n\nconst CY_ID_CARD_CONTEXT: RegexDef = {\n pattern:\n `\\\\b(?:(?i:Cyprus)|(?i:Cypriot))[^\\\\S\\\\n]+` +\n `(?:(?i:identity)[^\\\\S\\\\n]+(?i:card)|(?i:ID)[^\\\\S\\\\n]+(?i:card))` +\n `(?:[^\\\\S\\\\n]+(?:(?i:number)|(?i:no\\\\.?)|#))?` +\n `[^\\\\S\\\\n]{0,4}:?[^\\\\S\\\\n]{0,4}` +\n `\\\\d{6,8}\\\\b`,\n label: \"identity card number\",\n score: 0.96,\n lazy: true,\n prefilterAny: [\"Cyprus\", \"Cypriot\", \"identity card\", \"ID card\"],\n prefilterCaseInsensitive: true,\n prefilterWindowBytes: CONTEXT_REGEX_PREFILTER_WINDOW_BYTES,\n preparedArtifactPolicy: \"omit\",\n};\n\nconst UK_DRIVING_LICENCE_CONTEXT: RegexDef = {\n pattern:\n `\\\\b(?:(?:(?i:UK)|(?i:U\\\\.?K\\\\.?)|(?i:British))[^\\\\S\\\\n]{1,4})?` +\n `(?i:driving)[^\\\\S\\\\n]+(?i:licen[cs]e)` +\n `(?:[^\\\\S\\\\n]+(?:(?i:number)|(?i:no\\\\.?)|#))?` +\n `[^\\\\S\\\\n]{0,4}:?[^\\\\S\\\\n]{0,4}` +\n `[A-Za-z9]{5}\\\\d{6}[A-Za-z0-9]{2}\\\\d[A-Za-z]{2}\\\\b`,\n label: \"identity card number\",\n score: 0.96,\n lazy: true,\n prefilterAny: [\"driving licence\", \"driving license\"],\n prefilterCaseInsensitive: true,\n prefilterWindowBytes: CONTEXT_REGEX_PREFILTER_WINDOW_BYTES,\n preparedArtifactPolicy: \"omit\",\n};\n\nconst US_DRIVER_LICENSE_CONTEXT: RegexDef = {\n pattern:\n `\\\\b(?:(?:(?i:U\\\\.?S\\\\.?)|(?i:USA)|(?i:United)[^\\\\S\\\\n]+(?i:States)|${US_STATE_CODE})` +\n `[^\\\\S\\\\n]{1,4})?(?:(?i:driver['’]?s?)|(?i:driving))[^\\\\S\\\\n]+(?i:licen[cs]e)` +\n `(?:` +\n `[^\\\\S\\\\n]+(?:(?i:number)|(?i:no\\\\.?)|#)` +\n `[^\\\\S\\\\n]{0,4}:?[^\\\\S\\\\n]{0,4}[A-Za-z0-9]{5,15}` +\n `|[^\\\\S\\\\n]{0,4}:?[^\\\\S\\\\n]{0,4}` +\n `(?=[A-Za-z0-9]{0,14}\\\\d)[A-Za-z0-9]{5,15}` +\n `)\\\\b`,\n label: \"identity card number\",\n score: 0.8,\n lazy: true,\n prefilterAny: [\n \"driver license\",\n \"driver licence\",\n \"drivers license\",\n \"drivers licence\",\n \"driver's license\",\n \"driver's licence\",\n \"driver’s license\",\n \"driver’s licence\",\n \"driving license\",\n \"driving licence\",\n ],\n prefilterCaseInsensitive: true,\n prefilterWindowBytes: CONTEXT_REGEX_PREFILTER_WINDOW_BYTES,\n preparedArtifactPolicy: \"omit\",\n};\n\nconst MEDICAL_LICENSE_CONTEXT: RegexDef = {\n pattern:\n `\\\\b(?:` +\n `(?:(?i:GMC)|(?i:NMC))` +\n `(?:[^\\\\S\\\\n]+(?:(?i:licen[cs]e)|(?i:registration)|(?i:reg\\\\.?)|(?i:pin)|(?i:number)|(?i:no\\\\.?)))*` +\n `|(?:(?i:medical)|(?i:physician)|(?i:doctor)|(?i:surgeon)|(?i:nursing)|(?i:nurse))` +\n `(?:[^\\\\S\\\\n]+(?:(?i:licen[cs]e)|(?i:registration)|(?i:reg\\\\.?)|(?i:pin)|(?i:number)|(?i:no\\\\.?)))+` +\n `)` +\n `[^\\\\S\\\\n]{0,4}:?[^\\\\S\\\\n]{0,4}` +\n `(?:[A-Za-z]{0,3}\\\\d{5,8}|\\\\d{2}[A-Za-z]\\\\d{4}[A-Za-z])\\\\b`,\n label: \"registration number\",\n score: 0.85,\n lazy: true,\n prefilterAny: [\n \"GMC\",\n \"NMC\",\n \"medical\",\n \"physician\",\n \"doctor\",\n \"surgeon\",\n \"nursing\",\n \"nurse\",\n ],\n prefilterCaseInsensitive: true,\n prefilterWindowBytes: CONTEXT_REGEX_PREFILTER_WINDOW_BYTES,\n preparedArtifactPolicy: \"omit\",\n};\n\nconst CRYPTO_WALLET_CANDIDATE = crypto.wallet.candidatePattern ?? \"(?!)\";\nconst CRYPTO_WALLET_CANDIDATE_REGEX = new RegExp(CRYPTO_WALLET_CANDIDATE);\nconst getCryptoWalletCandidate = (text: string): string =>\n CRYPTO_WALLET_CANDIDATE_REGEX.exec(text)?.[0] ?? text;\n\nconst CRYPTO_WALLET_ADDRESS: RegexDef = {\n pattern:\n `\\\\b(?:0x[0-9A-Fa-f]{40}` +\n `|bc1[ac-hj-np-z02-9]{11,71}` +\n `|BC1[AC-HJ-NP-Z02-9]{11,71})\\\\b` +\n `|\\\\b(?:(?i:BTC|Bitcoin|crypto|wallet|address)[^\\\\S\\\\n]{0,4}:?[^\\\\S\\\\n]{1,8}){1,4}` +\n `[13][a-km-zA-HJ-NP-Z1-9]{25,34}\\\\b`,\n label: \"crypto\",\n score: 0.85,\n validator: crypto.wallet,\n validatorInput: getCryptoWalletCandidate,\n validatorInputKind: \"crypto-wallet-candidate\",\n lazy: true,\n prefilterAny: [\"0x\", \"bc1\", \"BTC\", \"Bitcoin\", \"crypto\", \"wallet\", \"address\"],\n prefilterCaseInsensitive: true,\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 lazy: true,\n prefilterAny: [\"MVA\"],\n prefilterCaseInsensitive: false,\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 lazy: true,\n prefilterAny: [\"SSP\"],\n prefilterCaseInsensitive: false,\n preparedArtifactPolicy: \"omit\",\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 lazy: true,\n prefilterAny: [\"OAB/\"],\n prefilterCaseInsensitive: false,\n preparedArtifactPolicy: \"omit\",\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 lazy: true,\n prefilterAny: [\"http://\", \"https://\", \"http:\", \"https:\", \"www.\"],\n prefilterCaseInsensitive: false,\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 lazy: true,\n prefilterAny: [\"am\", \"pm\", \"a.m\", \"p.m\"],\n prefilterCaseInsensitive: true,\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}%`;\nconst PERCENT_LEFT_BOUNDARY = \"(?<![\\\\p{L}\\\\p{N}_.,])\";\nconst PERCENT_RIGHT_BOUNDARY = \"(?![\\\\p{L}\\\\p{N}_])\";\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);\nconst PERCENT_WORD_PREFILTERS = [\n ...new Set(\n (AMOUNT_WORDS.percentages ?? []).flatMap((entry) => entry.keywords),\n ),\n];\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_WORD_RATE: RegexDef = {\n pattern:\n `${PERCENT_LEFT_BOUNDARY}` +\n `${PERCENT_WORD}[^\\\\S\\\\n]*\\\\([^\\\\S\\\\n]*${PERCENT_TOKEN}[^\\\\S\\\\n]*\\\\)` +\n PERCENT_RIGHT_BOUNDARY,\n label: \"monetary amount\",\n score: 0.85,\n lazy: true,\n prefilterAny: PERCENT_WORD_PREFILTERS,\n prefilterCaseInsensitive: true,\n prefilterWindowBytes: CONTEXT_REGEX_PREFILTER_WINDOW_BYTES,\n};\n\nconst PERCENT_NUMERIC_RATE: RegexDef = {\n pattern:\n `${PERCENT_LEFT_BOUNDARY}(?:${PERCENT_RANGE}|${PERCENT_TOKEN})` +\n PERCENT_RIGHT_BOUNDARY,\n label: \"monetary amount\",\n score: 0.85,\n lazy: true,\n prefilterAny: [\"%\"],\n prefilterCaseInsensitive: false,\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 NHS_NUMBER_CONTEXT,\n PASSPORT_CONTEXT,\n FR_CNI_CONTEXT,\n CY_TIC_CONTEXT,\n CY_ID_CARD_CONTEXT,\n UK_DRIVING_LICENCE_CONTEXT,\n US_DRIVER_LICENSE_CONTEXT,\n MEDICAL_LICENSE_CONTEXT,\n CRYPTO_WALLET_ADDRESS,\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_WORD_RATE,\n PERCENT_NUMERIC_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\nconst toRegexPatternEntry = (definition: RegexDef): PatternEntry => {\n if (\n definition.lazy === undefined &&\n definition.prefilterAny === undefined &&\n definition.prefilterCaseInsensitive === undefined &&\n definition.prefilterRegex === undefined &&\n definition.prefilterWindowBytes === undefined &&\n definition.preparedArtifactPolicy === undefined\n ) {\n return definition.pattern;\n }\n\n const entry: RegexPatternEntry = { pattern: definition.pattern };\n if (definition.lazy !== undefined) {\n entry.lazy = definition.lazy;\n }\n if (definition.prefilterAny !== undefined) {\n entry.prefilterAny = definition.prefilterAny;\n }\n if (definition.prefilterCaseInsensitive !== undefined) {\n entry.prefilterCaseInsensitive = definition.prefilterCaseInsensitive;\n }\n if (definition.prefilterRegex !== undefined) {\n entry.prefilterRegex = definition.prefilterRegex;\n }\n if (definition.prefilterWindowBytes !== undefined) {\n entry.prefilterWindowBytes = definition.prefilterWindowBytes;\n }\n if (definition.preparedArtifactPolicy !== undefined) {\n entry.preparedArtifactPolicy = definition.preparedArtifactPolicy;\n }\n return entry;\n};\n\n/** Static regex entries with compile-time prefilter hints. */\nexport const REGEX_PATTERN_ENTRIES: readonly PatternEntry[] =\n ALL_REGEX_DEFS.map(toRegexPatternEntry);\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 const validatorId = d.validatorId ?? VALIDATOR_IDS.get(d.validator);\n if (!validatorId) {\n throw new Error(`Missing regex validator id for ${d.label}`);\n }\n meta.validatorId = validatorId;\n }\n if (d.minByteLength) {\n meta.minByteLength = d.minByteLength;\n }\n if (d.validatorInput) {\n meta.validatorInput = d.validatorInput;\n if (!d.validatorInputKind) {\n throw new Error(`Missing regex validator input kind for ${d.label}`);\n }\n meta.validatorInputKind = d.validatorInputKind;\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\nexport type DateMonthData = Record<string, string[]>;\nexport type YearWordData = Record<string, string[]>;\n\nconst languageCacheKey = (languages: readonly string[] | undefined): string => {\n if (languages === undefined || languages.length === 0) {\n return \"*\";\n }\n return [...new Set(languages.map(normalizeLanguageKey).filter(Boolean))]\n .toSorted()\n .join(\",\");\n};\n\nconst normalizeLanguageKey = (language: string): string =>\n language.trim().toLowerCase();\n\nconst selectedLanguageKeys = (\n languages: readonly string[] | undefined,\n): ReadonlySet<string> | null => {\n if (languages === undefined || languages.length === 0) {\n return null;\n }\n const selected = new Set<string>();\n for (const language of languages) {\n const normalized = normalizeLanguageKey(language);\n if (normalized.length === 0) {\n continue;\n }\n selected.add(normalized);\n const separator = normalized.indexOf(\"-\");\n if (separator !== -1) {\n selected.add(normalized.slice(0, separator));\n }\n }\n return selected.size === 0 ? null : selected;\n};\n\nconst filterDateMonthsByLanguage = (\n months: DateMonths,\n languages: readonly string[] | undefined,\n): DateMonths => {\n const selected = selectedLanguageKeys(languages);\n if (selected === null) {\n return months;\n }\n\n const filtered: DateMonths = {};\n for (const [key, value] of Object.entries(months)) {\n if (key.startsWith(\"_\") || !selected.has(normalizeLanguageKey(key))) {\n continue;\n }\n filtered[key] = value;\n }\n return Object.keys(filtered).length === 0 ? months : filtered;\n};\n\nconst filterYearWordsByLanguage = (\n data: Record<string, unknown>,\n languages: readonly string[] | undefined,\n): Record<string, unknown> => {\n const selected = selectedLanguageKeys(languages);\n if (selected === null) {\n return data;\n }\n\n const filtered: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(data)) {\n if (key.startsWith(\"_\") || !selected.has(normalizeLanguageKey(key))) {\n continue;\n }\n filtered[key] = value;\n }\n return Object.keys(filtered).length === 0 ? data : filtered;\n};\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\nconst buildDateMonthData = (months: DateMonths): DateMonthData => {\n const result: DateMonthData = {};\n for (const [key, value] of Object.entries(months)) {\n if (key.startsWith(\"_\")) continue;\n const names = Array.isArray(value) ? value : [value];\n result[key] = names.filter(\n (name) => name.replace(/\\.$/, \"\").length >= MIN_MONTH_NAME_LENGTH,\n );\n }\n return result;\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. */\nconst datePatternPromises = new Map<string, Promise<string[]>>();\nconst dateMonthDataPromises = new Map<string, Promise<DateMonthData>>();\nconst yearWordDataPromises = new Map<string, Promise<YearWordData>>();\n\nconst loadDateMonths = async (): Promise<DateMonths> => {\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 return mod.default ?? mod;\n};\n\nconst loadDatePatterns = async (\n languages?: readonly string[],\n): Promise<string[]> => {\n const months = await loadDateMonths();\n const alt = buildMonthAlternation(\n filterDateMonthsByLanguage(months, languages),\n );\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 = (\n languages?: readonly string[],\n): Promise<string[]> => {\n const key = languageCacheKey(languages);\n let promise = datePatternPromises.get(key);\n if (promise === undefined) {\n promise = loadDatePatterns(languages).catch((err) => {\n datePatternPromises.delete(key);\n throw err;\n });\n datePatternPromises.set(key, promise);\n }\n return promise;\n};\n\nexport const getDateMonthData = (\n languages?: readonly string[],\n): Promise<DateMonthData> => {\n const key = languageCacheKey(languages);\n let promise = dateMonthDataPromises.get(key);\n if (promise === undefined) {\n promise = loadDateMonths()\n .then((months) =>\n buildDateMonthData(filterDateMonthsByLanguage(months, languages)),\n )\n .catch((err) => {\n dateMonthDataPromises.delete(key);\n throw err;\n });\n dateMonthDataPromises.set(key, promise);\n }\n return promise;\n};\n\nexport const getYearWordData = (\n languages?: readonly string[],\n): Promise<YearWordData> => {\n const key = languageCacheKey(languages);\n let promise = yearWordDataPromises.get(key);\n if (promise !== undefined) {\n return promise;\n }\n promise = import(\"../data/year-words.json\")\n .then((mod) => {\n const data = (mod.default ?? mod) as Record<string, unknown>;\n const scopedData = filterYearWordsByLanguage(data, languages);\n const result: YearWordData = {};\n for (const [language, words] of Object.entries(scopedData)) {\n if (language.startsWith(\"_\") || !Array.isArray(words)) {\n continue;\n }\n result[language] = words.filter(\n (word): word is string => typeof word === \"string\" && word.length > 0,\n );\n }\n return result;\n })\n .catch((err) => {\n yearWordDataPromises.delete(key);\n throw err;\n });\n yearWordDataPromises.set(key, promise);\n return promise;\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\nexport type MonetaryData = {\n currencies: {\n codes: string[];\n symbols: string[];\n local_names: string[];\n };\n amount_words: {\n written_amount_patterns: Array<{\n keywords: string[];\n }>;\n magnitude_suffixes: Array<{\n words: string[];\n abbreviations_case_insensitive: string[];\n abbreviations_case_sensitive: string[];\n }>;\n share_quantity_terms: Array<{\n modifiers: string[];\n nouns: string[];\n }>;\n };\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;\nlet monetaryDataPromise: Promise<MonetaryData> | 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\nconst loadMonetaryData = async (): Promise<MonetaryData> => {\n const mod = await import(\"../data/currencies.json\");\n const currencies: CurrenciesData = mod.default ?? mod;\n return {\n currencies: {\n codes: currencies.codes,\n symbols: currencies.symbols,\n local_names: currencies.localNames ?? [],\n },\n amount_words: {\n written_amount_patterns: (AMOUNT_WORDS.patterns ?? []).map((entry) => ({\n keywords: entry.keywords,\n })),\n magnitude_suffixes: (AMOUNT_WORDS.magnitudeSuffixes ?? []).map(\n (entry) => ({\n words: entry.words ?? [],\n abbreviations_case_insensitive:\n entry.abbreviationsCaseInsensitive ?? [],\n abbreviations_case_sensitive: entry.abbreviationsCaseSensitive ?? [],\n }),\n ),\n share_quantity_terms: (AMOUNT_WORDS.shareQuantityTerms ?? []).map(\n (entry) => ({\n modifiers: entry.modifiers ?? [],\n nouns: entry.nouns,\n }),\n ),\n },\n };\n};\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\nexport const getMonetaryData = (): Promise<MonetaryData> => {\n if (!monetaryDataPromise) {\n monetaryDataPromise = loadMonetaryData().catch((err) => {\n monetaryDataPromise = null;\n throw err;\n });\n }\n return monetaryDataPromise;\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.minByteLength !== undefined &&\n utf8ByteLength(match.text) < meta.minByteLength\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 validatorText = meta.validatorInput\n ? meta.validatorInput(match.text)\n : match.text;\n const compacted = meta.validator.compact(validatorText);\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 prefilterPhrases?: string[];\n guardPrefixPhrases?: string[];\n guardSuffixPhrases?: 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 languageMatches = (\n entryLanguage: string,\n selectedLanguages: readonly string[] | undefined,\n): boolean => {\n if (selectedLanguages === undefined || selectedLanguages.length === 0) {\n return true;\n }\n const normalizedEntry = entryLanguage.toLowerCase();\n return selectedLanguages.some((language) => {\n const normalized = language.trim().toLowerCase();\n return (\n normalized === normalizedEntry ||\n normalized.split(\"-\").at(0) === normalizedEntry\n );\n });\n};\n\nconst buildSigningClausePatterns = (\n data: SigningClauseConfig,\n selectedLanguages?: readonly string[],\n): string[] => {\n const patterns: string[] = [];\n\n for (const entry of data.patterns) {\n if (!languageMatches(entry.lang, selectedLanguages)) {\n continue;\n }\n\n patterns.push(buildSigningClausePattern(entry));\n }\n\n return patterns;\n};\n\nconst buildNativeSigningClausePatterns = (\n data: SigningClauseConfig,\n selectedLanguages?: readonly string[],\n): PatternEntry[] => {\n const patterns: PatternEntry[] = [];\n\n for (const entry of data.patterns) {\n if (!languageMatches(entry.lang, selectedLanguages)) {\n continue;\n }\n\n const pattern: RegexPatternEntry = {\n pattern: buildSigningClausePattern(entry),\n lazy: true,\n prefilterAny: entry.prefilterPhrases ?? [],\n prefilterCaseInsensitive: false,\n prefilterWindowBytes: CONTEXT_REGEX_PREFILTER_WINDOW_BYTES,\n preparedArtifactPolicy: \"omit\",\n };\n patterns.push(pattern);\n }\n\n return patterns;\n};\n\nconst buildSigningClausePattern = (\n entry: SigningClauseConfig[\"patterns\"][number],\n): string => {\n const prepAlt =\n entry.prepositions.length > 0 ? entry.prepositions.join(\"|\") : null;\n\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 return (\n `(?:^|\\\\n|[^\\\\S\\\\n])` +\n entry.prefix +\n place +\n (entry.suffix ? `(?:${entry.suffix})` : \"\")\n );\n};\n\nexport const SIGNING_CLAUSE_META: Readonly<RegexMeta> = {\n label: \"address\",\n score: 0.9,\n};\n\nlet signingPatternPromise: Promise<string[]> | null = null;\nlet nativeSigningPatternPromise: Promise<PatternEntry[]> | null = null;\n\nconst loadSigningPatterns = async (\n selectedLanguages?: readonly string[],\n): Promise<string[]> => {\n const mod = await import(\"../data/signing-clauses.json\");\n const data: SigningClauseConfig = mod.default ?? mod;\n return buildSigningClausePatterns(data, selectedLanguages);\n};\n\nconst loadNativeSigningPatterns = async (\n selectedLanguages?: readonly string[],\n): Promise<PatternEntry[]> => {\n const mod = await import(\"../data/signing-clauses.json\");\n const data: SigningClauseConfig = mod.default ?? mod;\n return buildNativeSigningClausePatterns(data, selectedLanguages);\n};\n\nexport const getSigningClausePatterns = (\n selectedLanguages?: readonly string[],\n): Promise<string[]> => {\n if (selectedLanguages !== undefined) {\n return loadSigningPatterns(selectedLanguages);\n }\n if (!signingPatternPromise) {\n signingPatternPromise = loadSigningPatterns().catch((err) => {\n signingPatternPromise = null;\n throw err;\n });\n }\n return signingPatternPromise;\n};\n\nexport const getNativeSigningClausePatterns = (\n selectedLanguages?: readonly string[],\n): Promise<PatternEntry[]> => {\n if (selectedLanguages !== undefined) {\n return loadNativeSigningPatterns(selectedLanguages);\n }\n if (!nativeSigningPatternPromise) {\n nativeSigningPatternPromise = loadNativeSigningPatterns().catch((err) => {\n nativeSigningPatternPromise = null;\n throw err;\n });\n }\n return nativeSigningPatternPromise;\n};\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\nexport const 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\nexport const getNormalizedLegalBoundarySuffixesSync = (): ReadonlySet<string> =>\n normalizedLegalBoundarySuffixesCache ??\n new Set(\n LEGAL_SUFFIXES.map(normalizeLegalSuffixToken).filter(\n (suffix) => suffix.length > 0,\n ),\n );\n\nexport const 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\nexport const 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\nexport const 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 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 { 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 {\n languageConfigMatches,\n languageSelectionKey,\n} from \"../util/language-selection\";\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 validator: v.validator,\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\nconst triggerPatternsPromises = new Map<string, Promise<TriggerPatterns>>();\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 (\n languages: readonly string[] | undefined,\n): Promise<TriggerPatterns> => {\n const rules: TriggerRule[] = [];\n const languageOptions = languages === undefined ? {} : { languages };\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 languageOptions,\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 if (!languageConfigMatches(key, languages)) {\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 (\n languages?: readonly string[],\n): Promise<TriggerPatterns> => {\n const key = languageSelectionKey(languages);\n let promise = triggerPatternsPromises.get(key);\n if (promise === undefined) {\n promise = loadTriggerPatterns(languages);\n triggerPatternsPromises.set(key, promise);\n }\n return promise;\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\nexport const getAddressStopKeywordsSync = (): readonly string[] =>\n addressStopKeywordsCache ?? ADDRESS_STOP_KEYWORDS_SEED;\n\n/**\n * Warm address support data. Pipeline callers\n * await this before invoking trigger detection so the\n * synchronous `extractValue` path uses merged data.\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().replace(/[.,;:!?]+$/u, \"\");\n if (idText.length === 0) {\n return null;\n }\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","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\ntype NonWesternNamesModule = Promise<{ default: { names: string[] } }>;\n\n// Literal import specifiers so the bundler resolves each corpus\n// file into the build output; a template-literal specifier survives\n// bundling as a runtime-relative path that does not exist in dist.\nconst NONWESTERN_NAME_IMPORTS: Record<\n (typeof NONWESTERN_LOCALE_KEYS)[number],\n () => NonWesternNamesModule\n> = {\n in: () => import(\"../data/names-nw-in.json\"),\n ar: () => import(\"../data/names-nw-ar.json\"),\n \"ja-latn\": () => import(\"../data/names-nw-ja-latn.json\"),\n ko: () => import(\"../data/names-nw-ko.json\"),\n \"zh-latn\": () => import(\"../data/names-nw-zh-latn.json\"),\n th: () => import(\"../data/names-nw-th.json\"),\n vi: () => import(\"../data/names-nw-vi.json\"),\n fil: () => import(\"../data/names-nw-fil.json\"),\n id: () => import(\"../data/names-nw-id.json\"),\n};\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((locale) => NONWESTERN_NAME_IMPORTS[locale]()),\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 commonWords: Object.freeze(commonWords),\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 // A chain whose every token is a common English word is a\n // common-word phrase, not a person — even when one token\n // happens to coincide with a non-Western given name (e.g.\n // \"Loan Documents\"/\"Loan Amount\", where \"Loan\" is also a\n // Vietnamese name). Only used to veto the weakest\n // single-non-Western-token heuristic below; chains with a\n // second corpus name, title, or connector are unaffected.\n const chainAllCommonWords = chain.every((t) =>\n corpus.commonWords.has(t.text.toLowerCase()),\n );\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 !chainAllCommonWords\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","/**\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","import type { Entity } from \"../types\";\nimport type { PipelineContext } from \"../context\";\nimport { defaultContext } from \"../context\";\nimport { isCallerOwnedEntity } from \"../util/entity-source\";\nimport {\n getDefinedTermHeads,\n getPersonStopwords,\n} 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 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 collapseEntityWhitespace = (text: string): string =>\n text.replace(/\\s*\\n\\s*/g, \" \").replace(/\\s{2,}/g, \" \");\n\nconst normalizeEntityFromRaw = (\n entity: Entity,\n fullText: string,\n): Entity | null => {\n let start = entity.start;\n let end = entity.end;\n let text = fullText.slice(start, end);\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 const beforeTrim = text;\n text = trimTrailingAddressProse(text);\n end -= beforeTrim.length - text.length;\n }\n\n const trailingMatch = /[,\\s]+$/u.exec(text);\n if (trailingMatch) {\n end -= trailingMatch[0].length;\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,\n text: collapseEntityWhitespace(text),\n };\n};\n\nconst normalizeEntity = (entity: Entity, fullText?: string): Entity | null => {\n if (fullText !== undefined) {\n return normalizeEntityFromRaw(entity, fullText);\n }\n\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 const removedFromFront = start - entity.start;\n const removedFromBack = entity.text.length - removedFromFront - text.length;\n const end = entity.end - removedFromBack;\n\n return {\n ...entity,\n start,\n end,\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\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 const definedTermHeads = getDefinedTermHeads(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, fullText);\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 (tokens.length > 1 && lastFolded && definedTermHeads.has(lastFolded)) {\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","/**\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\nexport type AddressSeedData = {\n boundary_words: string[];\n br_cep_cue_words: string[];\n unit_abbreviations: string[];\n};\n\nlet cachedBoundaryRe: RegExp | null = null;\nlet addressSeedDataPromise: Promise<AddressSeedData> | 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\nconst loadFieldStopWords = async (): Promise<DictionaryConfig> => {\n try {\n const mod = await import(\"../data/address-stop-keywords.json\");\n return mod.default as DictionaryConfig;\n } catch {\n return {};\n }\n};\n\nconst loadUnitAbbreviations = async (): Promise<DictionaryConfig> => {\n try {\n const mod = await import(\"../data/address-unit-abbreviations.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\nexport const getAddressSeedData = async (): Promise<AddressSeedData> => {\n addressSeedDataPromise ??= (async () => {\n const [boundaryWords, fieldStopWords, unitAbbreviations, brCueWords] =\n await Promise.all([\n loadBoundaryWords(),\n loadFieldStopWords(),\n loadUnitAbbreviations(),\n loadBrCueWords(),\n ]);\n return {\n boundary_words: flattenDictionaries([boundaryWords, fieldStopWords]),\n br_cep_cue_words: [...brCueWords],\n unit_abbreviations: flattenDictionaries([unitAbbreviations]),\n };\n })();\n return addressSeedDataPromise;\n};\n\nconst flattenDictionaries = (\n configs: readonly DictionaryConfig[],\n): string[] => {\n const words: string[] = [];\n const seen = new Set<string>();\n for (const config of configs) {\n for (const word of flattenDictionary(config)) {\n const key = word.toLowerCase();\n if (seen.has(key)) {\n continue;\n }\n seen.add(key);\n words.push(word);\n }\n }\n return words;\n};\n\nconst flattenDictionary = (config: DictionaryConfig): string[] => {\n const words: string[] = [];\n const seen = new Set<string>();\n for (const values of Object.values(config)) {\n if (!Array.isArray(values)) {\n continue;\n }\n for (const word of values) {\n if (typeof word !== \"string\" || word.length === 0) {\n continue;\n }\n const key = word.toLowerCase();\n if (seen.has(key)) {\n continue;\n }\n seen.add(key);\n words.push(word);\n }\n }\n return words;\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","/**\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 } 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 { buildStreetTypePatterns } from \"./address-seeds\";\nimport {\n getClauseNounHeadsSync,\n getLegalRoleHeadsSync,\n warmLegalRoleHeads,\n} from \"./legal-forms\";\nimport { normalizeForSearch } from \"../util/normalize\";\nimport { ALL_UPPER_RE, UPPER_START_RE } from \"../util/text\";\nimport { DASH } from \"../util/char-groups\";\nimport denyListFiltersByLanguage from \"../data/deny-list-filters.json\";\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\nconst lowerSortedUnique = (values: Iterable<string>): string[] =>\n [...new Set([...values].map((value) => value.toLowerCase()))].toSorted();\n\nconst collectLanguageWordValues = (data: Record<string, unknown>): string[] => {\n const words: string[] = [];\n const append = (value: unknown): void => {\n if (!Array.isArray(value)) {\n return;\n }\n for (const word of value) {\n if (typeof word === \"string\" && word.length > 0) {\n words.push(word);\n }\n }\n };\n\n append(data[\"words\"]);\n for (const [key, value] of Object.entries(data)) {\n if (key === \"words\" || key.startsWith(\"_\")) {\n continue;\n }\n append(value);\n }\n\n return lowerSortedUnique(words);\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\nlet monthNamesPromise: Promise<ReadonlySet<string>> | null = null;\nlet monthNamesCache: ReadonlySet<string> | null = null;\n\n/**\n * English month names, lowercased. City gazetteers carry a\n * handful of entries that collide with month words (e.g. a\n * place named \"August\"); in prose these surface as address\n * false positives where the token is plainly a date, so they\n * are dropped from address patterns at build time.\n */\nconst loadMonthNames = (): Promise<ReadonlySet<string>> => {\n if (monthNamesCache) return Promise.resolve(monthNamesCache);\n if (monthNamesPromise) return monthNamesPromise;\n monthNamesPromise = (async () => {\n try {\n const mod: { default?: { en?: string[] } } =\n await import(\"../data/date-months.json\");\n const set: ReadonlySet<string> = new Set(\n (mod.default?.en ?? []).map((month) => month.toLowerCase()),\n );\n monthNamesCache = set;\n return set;\n } catch {\n const empty: ReadonlySet<string> = new Set();\n monthNamesCache = empty;\n return empty;\n }\n })();\n return monthNamesPromise;\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 = await import(\"../data/person-stopwords.json\");\n const parsed =\n (mod as { default?: Record<string, unknown> }).default ?? mod;\n const set: ReadonlySet<string> = new Set(\n collectLanguageWordValues(parsed as Record<string, unknown>),\n );\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\nexport const loadDefinedTermHeads = (\n ctx: PipelineContext,\n): Promise<ReadonlySet<string>> => {\n if (ctx.definedTermHeadsPromise) {\n return ctx.definedTermHeadsPromise;\n }\n ctx.definedTermHeadsPromise = (async () => {\n try {\n const mod = await import(\"../data/defined-term-heads.json\");\n const parsed =\n (mod as { default?: Record<string, unknown> }).default ?? mod;\n const set: ReadonlySet<string> = new Set(\n collectLanguageWordValues(parsed as Record<string, unknown>),\n );\n ctx.definedTermHeads = set;\n return set;\n } catch {\n const empty: ReadonlySet<string> = new Set();\n ctx.definedTermHeads = empty;\n return empty;\n }\n })();\n return ctx.definedTermHeadsPromise;\n};\n\nconst EMPTY_DEFINED_TERM_HEADS: ReadonlySet<string> = new Set();\n\nexport const getDefinedTermHeads = (\n ctx: PipelineContext,\n): ReadonlySet<string> => ctx.definedTermHeads ?? EMPTY_DEFINED_TERM_HEADS;\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\ntype DenyListLanguageFilters = {\n sentenceStarters?: readonly string[];\n definedTermCues?: readonly string[];\n};\n\ntype FalsePositiveShapeFilters = {\n addressComponentTerms: string[];\n ambiguousStreetTypeTerms: string[];\n numberAbbrevPrefixes: string[];\n documentHeadingOrdinalMarkers: string[];\n};\n\ntype SigningClauseData = {\n patterns: readonly {\n guardPrefixPhrases?: readonly string[];\n guardSuffixPhrases?: readonly string[];\n }[];\n};\n\nexport type DenyListFilterData = {\n stopwords: string[];\n allowList: string[];\n personStopwords: string[];\n personTrailingNouns: string[];\n addressStopwords: string[];\n addressJurisdictionPrefixes: string[];\n streetTypes: string[];\n addressComponentTerms: string[];\n ambiguousStreetTypeTerms: string[];\n firstNames: string[];\n genericRoles: string[];\n numberAbbrevPrefixes: string[];\n sentenceStarters: string[];\n trailingAddressWordExclusions: string[];\n documentHeadingWords: string[];\n documentHeadingOrdinalMarkers: string[];\n definedTermCues: string[];\n signingPlaceGuards: DenyListSigningPlaceGuardData[];\n};\n\nexport type DenyListSigningPlaceGuardData = {\n prefixPhrases: string[];\n suffixPhrases: string[];\n};\n\nconst DENY_LIST_FILTER_GROUPS: readonly DenyListLanguageFilters[] =\n Object.values(denyListFiltersByLanguage);\n\nconst escapeRegExp = (value: string): string =>\n value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\nconst collectLanguageFilterValues = (\n selector: (filters: DenyListLanguageFilters) => readonly string[] | undefined,\n): string[] =>\n lowerSortedUnique(\n DENY_LIST_FILTER_GROUPS.flatMap((filters) => selector(filters) ?? []),\n );\n\nconst DENY_LIST_STATIC_FILTERS = {\n definedTermCues: collectLanguageFilterValues(\n (filters) => filters.definedTermCues,\n ),\n sentenceStarters: collectLanguageFilterValues(\n (filters) => filters.sentenceStarters,\n ),\n};\n\nconst SENTENCE_STARTER_WORDS: ReadonlySet<string> = new Set(\n DENY_LIST_STATIC_FILTERS.sentenceStarters,\n);\n\nconst buildDefinedTermCueRe = (): RegExp => {\n const cues = DENY_LIST_STATIC_FILTERS.definedTermCues.toSorted(\n (left, right) => right.length - left.length,\n );\n if (cues.length === 0) {\n return /$(?!)/;\n }\n const pattern = cues\n .map((cue) => escapeRegExp(cue).replace(/\\s+/g, \"\\\\s+\"))\n .join(\"|\");\n return new RegExp(`^[\\\\s,]*(?:${pattern})\\\\b`, \"iu\");\n};\nconst DEFINED_TERM_CUE_RE = buildDefinedTermCueRe();\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 filters: DenyListFilterData;\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 loadDefinedTermHeads(ctx),\n loadAddressStopwords(ctx),\n loadCommonWords(),\n loadMonthNames(),\n loadStreetTypeRe(),\n loadGenericRoles(ctx),\n warmLegalRoleHeads(),\n loadTrailingAddressWordExclusions(),\n ]);\n const commonWords = await loadCommonWords();\n const monthNames = await loadMonthNames();\n const filters = await buildDenyListFilterData(ctx);\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, filters);\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\") {\n if (label !== \"address\") {\n if (SINGLE_WORD_RE.test(normalized) && commonWords.has(lower)) {\n return;\n }\n if (isShortCuratedNoiseAcronym(normalized)) {\n return;\n }\n } else if (monthNames.has(lower)) {\n // Some city gazetteer entries collide with month words\n // (e.g. a place named \"August\"); in prose these are\n // overwhelmingly dates, so they surface as address false\n // positives. The common-word filter is deliberately not\n // applied to addresses (most cities are ordinary words),\n // so month words are dropped explicitly instead.\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 filters,\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 filters: DenyListFilterData,\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 filters,\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 buildStreetTypeFilterValues = async (): Promise<string[]> =>\n lowerSortedUnique(await buildStreetTypePatterns());\n\ntype SigningPlaceFilters = {\n guards: DenyListSigningPlaceGuardData[];\n};\n\nlet signingPlaceFiltersPromise: Promise<SigningPlaceFilters> | null = null;\n\nconst loadSigningPlaceFilters = (): Promise<SigningPlaceFilters> => {\n if (signingPlaceFiltersPromise) {\n return signingPlaceFiltersPromise;\n }\n\n signingPlaceFiltersPromise = (async () => {\n const mod = await import(\"../data/signing-clauses.json\");\n const data: SigningClauseData = mod.default ?? mod;\n return {\n guards: data.patterns\n .map((entry) => ({\n prefixPhrases: lowerSortedUnique(entry.guardPrefixPhrases ?? []),\n suffixPhrases: lowerSortedUnique(entry.guardSuffixPhrases ?? []),\n }))\n .filter(\n (entry) =>\n entry.prefixPhrases.length > 0 && entry.suffixPhrases.length > 0,\n ),\n };\n })().catch((error) => {\n signingPlaceFiltersPromise = null;\n throw error;\n });\n\n return signingPlaceFiltersPromise;\n};\n\nlet trailingAddressWordExclusionsPromise: Promise<ReadonlySet<string>> | null =\n null;\nlet documentHeadingWordsPromise: Promise<string[]> | null = null;\nlet addressJurisdictionPrefixesPromise: Promise<string[]> | null = null;\n\nconst loadLanguageWordFile = async (\n importer: () => Promise<unknown>,\n): Promise<string[]> => {\n const mod = await importer();\n const parsed = (mod as { default?: Record<string, unknown> }).default ?? mod;\n return collectLanguageWordValues(parsed as Record<string, unknown>);\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst languageWordValues = (value: unknown): string[] =>\n isRecord(value) ? collectLanguageWordValues(value) : [];\n\nlet falsePositiveShapeFiltersPromise: Promise<FalsePositiveShapeFilters> | null =\n null;\n\nconst loadFalsePositiveShapeFilters =\n (): Promise<FalsePositiveShapeFilters> => {\n if (falsePositiveShapeFiltersPromise) {\n return falsePositiveShapeFiltersPromise;\n }\n\n falsePositiveShapeFiltersPromise = (async () => {\n const mod = await import(\"../data/false-positive-shapes.json\");\n const defaultValue = isRecord(mod) ? mod.default : undefined;\n let data: Record<string, unknown> = {};\n if (isRecord(defaultValue)) {\n data = defaultValue;\n } else if (isRecord(mod)) {\n data = mod;\n }\n return {\n addressComponentTerms: languageWordValues(\n data[\"addressComponentTerms\"],\n ),\n ambiguousStreetTypeTerms: languageWordValues(\n data[\"ambiguousStreetTypeTerms\"],\n ),\n numberAbbrevPrefixes: languageWordValues(data[\"numberAbbrevPrefixes\"]),\n documentHeadingOrdinalMarkers: languageWordValues(\n data[\"documentHeadingOrdinalMarkers\"],\n ),\n };\n })().catch((error) => {\n falsePositiveShapeFiltersPromise = null;\n throw error;\n });\n\n return falsePositiveShapeFiltersPromise;\n };\n\nconst loadDocumentHeadingWords = (): Promise<string[]> => {\n if (documentHeadingWordsPromise) {\n return documentHeadingWordsPromise;\n }\n\n documentHeadingWordsPromise = loadLanguageWordFile(\n () => import(\"../data/document-structure-headings.json\"),\n ).catch((error) => {\n documentHeadingWordsPromise = null;\n throw error;\n });\n\n return documentHeadingWordsPromise;\n};\n\nconst loadTrailingAddressWordExclusions = async (): Promise<\n ReadonlySet<string>\n> => {\n if (trailingAddressWordExclusionsPromise) {\n return trailingAddressWordExclusionsPromise;\n }\n\n trailingAddressWordExclusionsPromise = (async () => {\n await warmLegalRoleHeads();\n const [organizationUnits, documentHeadings] = await Promise.all([\n loadLanguageWordFile(\n () => import(\"../data/organization-unit-heads.json\"),\n ),\n loadDocumentHeadingWords(),\n ]);\n return new Set(\n lowerSortedUnique([\n ...getLegalRoleHeadsSync(),\n ...getClauseNounHeadsSync(),\n ...organizationUnits,\n ...documentHeadings,\n ]),\n );\n })().catch((error) => {\n trailingAddressWordExclusionsPromise = null;\n throw error;\n });\n\n return trailingAddressWordExclusionsPromise;\n};\n\nconst loadAddressJurisdictionPrefixes = (): Promise<string[]> => {\n if (addressJurisdictionPrefixesPromise) {\n return addressJurisdictionPrefixesPromise;\n }\n\n addressJurisdictionPrefixesPromise = loadLanguageWordFile(\n () => import(\"../data/address-jurisdiction-prefixes.json\"),\n ).catch((error) => {\n addressJurisdictionPrefixesPromise = null;\n throw error;\n });\n\n return addressJurisdictionPrefixesPromise;\n};\n\nexport const buildDenyListFilterData = async (\n ctx: PipelineContext,\n): Promise<DenyListFilterData> => {\n const [\n signingPlaceFilters,\n trailingAddressWordExclusions,\n addressJurisdictionPrefixes,\n falsePositiveShapeFilters,\n documentHeadingWords,\n ] = await Promise.all([\n loadSigningPlaceFilters(),\n loadTrailingAddressWordExclusions(),\n loadAddressJurisdictionPrefixes(),\n loadFalsePositiveShapeFilters(),\n loadDocumentHeadingWords(),\n ]);\n\n return {\n stopwords: [...getStopwords(ctx)],\n allowList: [...getAllowList(ctx)],\n personStopwords: [...getPersonStopwords(ctx)],\n personTrailingNouns: [...getDefinedTermHeads(ctx)],\n addressStopwords: [...getAddressStopwords(ctx)],\n addressJurisdictionPrefixes,\n streetTypes: await buildStreetTypeFilterValues(),\n addressComponentTerms: falsePositiveShapeFilters.addressComponentTerms,\n ambiguousStreetTypeTerms:\n falsePositiveShapeFilters.ambiguousStreetTypeTerms,\n firstNames: [...getNameCorpusFirstNames(ctx)],\n genericRoles: [\n ...(ctx.genericRoles ?? EMPTY_GENERIC_ROLES),\n ...getLegalRoleHeadsSync(),\n ],\n numberAbbrevPrefixes: falsePositiveShapeFilters.numberAbbrevPrefixes,\n sentenceStarters: [...DENY_LIST_STATIC_FILTERS.sentenceStarters],\n trailingAddressWordExclusions: [...trailingAddressWordExclusions],\n documentHeadingWords,\n documentHeadingOrdinalMarkers:\n falsePositiveShapeFilters.documentHeadingOrdinalMarkers,\n definedTermCues: [...DENY_LIST_STATIC_FILTERS.definedTermCues],\n signingPlaceGuards: signingPlaceFilters.guards.map((entry) => ({\n prefixPhrases: [...entry.prefixPhrases],\n suffixPhrases: [...entry.suffixPhrases],\n })),\n };\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 loadDefinedTermHeads(ctx),\n loadAddressStopwords(ctx),\n loadStreetTypeRe(),\n loadGenericRoles(ctx),\n warmLegalRoleHeads(),\n loadTrailingAddressWordExclusions(),\n loadAddressJurisdictionPrefixes(),\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 extension inside quoted defined-term contexts:\n // legal prose often uses quoted capitalised noun phrases\n // that are not personal names.\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 noisy. Only accept\n // them when the next token has the shape of a name word,\n // while excluding language-data sentence starters.\n if (chain.length === 1) {\n const afterEnd = last.end;\n const rest = fullText.slice(afterEnd).trimStart();\n // Require Cap + lowercase so acronym-shaped tokens\n // do not promote a single-token hit.\n const nextIsUpper = rest.length > 1 && /^\\p{Lu}\\p{Ll}/u.test(rest);\n if (!nextIsUpper) {\n continue;\n }\n // Reject sentence starters so headings followed by\n // prose do not get promoted to person hits.\n const nextWord = /^\\p{L}+/u.exec(rest)?.[0] ?? \"\";\n if (SENTENCE_STARTER_WORDS.has(nextWord.toLowerCase())) {\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(\n results,\n fullText,\n new Set(data.filters.trailingAddressWordExclusions),\n );\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\nconst extendCityDistricts = (\n entities: Entity[],\n fullText: string,\n trailingAddressWordExclusions: ReadonlySet<string>,\n): 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 (!trailingAddressWordExclusions.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. Stops at lowercase words,\n * punctuation, or end of text.\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 language-data\n * definitional cue. Legal documents reserve this\n * construction for defined terms; the contents are not\n * personal names even when individual tokens collide with\n * the name corpus.\n */\nconst OPENING_QUOTES = new Set(['\"', \"'\", \"“\", \"„\", \"‟\", \"‘\", \"‛\", \"«\"]);\nconst CLOSING_QUOTES = new Set(['\"', \"'\", \"”\", \"’\", \"»\", \"“\"]);\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 // Preserve those when the definition points at a role from\n // dictionary data.\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 and typographic closing\n // quotes so stopword checks see the bare word.\n const word = text.slice(wordStart, wordEnd);\n const stripped = word.replace(/[,;.”\"’'“»]+$/, \"\");\n if (stripped.length < 2) {\n break;\n }\n\n // Do not consult the global allow list here: common\n // words can be legitimate name extensions once a first\n // name has established person context.\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","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","","","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 ambiguousCountrySurfaces from \"../data/ambiguous-country-surfaces.json\" with { type: \"json\" };\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 ambiguousCountrySurfaces.words.map((surface) => surface.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","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: readonly 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 ruleSetPromise: Promise<readonly HotwordRule[]> | null = null;\nlet initPromise: Promise<void> | null = null;\n\n// ── Init ────────────────────────────────────────────\n\nexport const loadHotwordRuleSet = (): Promise<readonly HotwordRule[]> => {\n if (ruleSetPromise !== null) return ruleSetPromise;\n ruleSetPromise = import(\"../data/hotword-rules.json\")\n .then((mod) => {\n const data: HotwordRulesConfig = mod.default ?? mod;\n return data.rules;\n })\n .catch((err) => {\n ruleSetPromise = null;\n throw err;\n });\n return ruleSetPromise;\n};\n\nconst loadRules = async (): Promise<void> => {\n const loaded = await loadHotwordRuleSet();\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 return expandLabelsForHotwordRuleSet(requestedLabels, rules);\n};\n\nexport const expandLabelsForHotwordRuleSet = (\n requestedLabels: readonly string[],\n ruleSet: readonly HotwordRule[],\n): readonly string[] => {\n if (requestedLabels.length === 0) {\n return requestedLabels;\n }\n const requested = new Set(requestedLabels);\n const expanded = new Set(requestedLabels);\n\n for (const rule of ruleSet) {\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 addressContextJson from \"../data/address-context.json\";\nimport addressPrepositionsJson from \"../data/address-prepositions.json\";\nimport addressStreetTypesJson from \"../data/address-street-types.json\";\nimport type { Entity } from \"../types\";\nimport { isCallerOwnedEntity } from \"../util/entity-source\";\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\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 // Clamp to the 0..1 score invariant. Several confirmed\n // neighbours can push a near-miss past 1.0; downstream\n // sorting, thresholding, and display all assume scores\n // never exceed 1, matching the clamps in zone-classifier\n // and hotword-rules.\n boosted.push({ ...entity, score: Math.min(1, 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\ntype AddressContextJson = {\n bareHouseStopwords: Record<string, string[] | string>;\n};\n\nexport type AddressContextData = {\n address_prepositions: string[];\n temporal_prepositions: string[];\n street_abbreviations: string[];\n bare_house_stopwords: string[];\n};\n\nconst languageRecordValues = (\n record: Record<string, string[] | string>,\n transform: (value: string) => string = (value) => value,\n): string[] => {\n const values: string[] = [];\n for (const [language, words] of Object.entries(record)) {\n if (language.startsWith(\"_\") || !Array.isArray(words)) {\n continue;\n }\n for (const word of words) {\n values.push(transform(word));\n }\n }\n return values;\n};\n\nconst buildPrepositionSets = (\n data: PrepositionData,\n): {\n address: ReadonlySet<string>;\n temporal: ReadonlySet<string>;\n} => ({\n address: new Set(\n languageRecordValues(data.address, (word) => word.toLowerCase()),\n ),\n temporal: new Set(\n languageRecordValues(data.temporal, (word) => word.toLowerCase()),\n ),\n});\n\nconst buildBareStopwords = (data: AddressContextJson): ReadonlySet<string> =>\n new Set(languageRecordValues(data.bareHouseStopwords));\n\nconst BARE_STOPWORDS = buildBareStopwords(addressContextJson);\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 prepositions = buildPrepositionSets(addressPrepositionsJson);\n _addressPreps = prepositions.address;\n _temporalPreps = prepositions.temporal;\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\nexport const getAddressContextData = (): AddressContextData => {\n const prepositions = buildPrepositionSets(addressPrepositionsJson);\n return {\n address_prepositions: [...prepositions.address],\n temporal_prepositions: [...prepositions.temporal],\n street_abbreviations: [...buildStreetAbbrevs(addressStreetTypesJson)],\n bare_house_stopwords: [...buildBareStopwords(addressContextJson)],\n };\n};\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","/**\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\";\nimport legalFormRuleWords from \"./data/legal-form-rule-words.json\";\nimport nameCorpusCjk from \"./data/name-corpus-cjk.json\";\nimport nameCorpusParticles from \"./data/name-corpus-particles.json\";\nimport organizationIndicators from \"./data/organization-indicators.json\";\nimport signatureDetection from \"./data/signature-detection.json\";\nimport triggerSupport from \"./data/trigger-support.json\";\n\nimport { getTextSearch } from \"./search-engine\";\n\nimport {\n isLegalFormsEnabled,\n type CustomRegexPattern,\n type GazetteerEntry,\n type PipelineConfig,\n} from \"./types\";\nimport {\n applyPipelineLanguageScope,\n configuredContentLanguages,\n} from \"./language-scope\";\nimport type { RegexMeta } from \"./detectors/regex\";\nimport type { TriggerRule } from \"./types\";\nimport type { DenyListData, DenyListFilterData } from \"./detectors/deny-list\";\nimport type { PipelineContext } from \"./context\";\nimport { defaultContext } from \"./context\";\nimport { POST_NOMINALS } from \"./config/titles\";\nimport { LEGAL_SUFFIXES } from \"./config/legal-forms\";\nimport { loadLanguageConfigs } from \"./util/lang-loader\";\nimport { languageConfigMatches } from \"./util/language-selection\";\n\nimport {\n REGEX_PATTERN_ENTRIES,\n REGEX_META,\n NATIVE_REGEX_VALIDATOR_IDS,\n getCurrencyPatternEntries,\n CURRENCY_PATTERN_META,\n getDateMonthData,\n getDatePatterns,\n getYearWordData,\n getMonetaryData,\n DATE_PATTERN_META,\n getSigningClausePatterns,\n getNativeSigningClausePatterns,\n SIGNING_CLAUSE_META,\n type DateMonthData,\n type YearWordData,\n type MonetaryData,\n} from \"./detectors/regex\";\nimport {\n buildTriggerPatterns,\n getAddressStopKeywordsSync,\n} from \"./detectors/triggers\";\nimport {\n buildDenyList,\n buildDenyListFilterData,\n ensureDenyListData,\n} from \"./detectors/deny-list\";\nimport {\n buildStreetTypePatterns,\n getAddressSeedData,\n type AddressSeedData,\n} from \"./detectors/address-seeds\";\nimport { buildGazetteerPatterns } from \"./detectors/gazetteer\";\nimport { buildCountryPatterns, type CountryData } from \"./detectors/countries\";\nimport {\n expandLabelsForHotwordRuleSet,\n loadHotwordRuleSet,\n type HotwordRule,\n} from \"./filters/hotword-rules\";\nimport {\n getAddressContextData,\n type AddressContextData,\n} from \"./filters/confidence-boost\";\nimport {\n getClauseNounHeadsSync,\n getConnectorProseHeadsSync,\n getKnownLegalSuffixes,\n getLeadingClauseTrimsSync,\n getLegalRoleHeadsSync,\n getNormalizedInNameLegalFormWordsSync,\n getNormalizedLegalBoundarySuffixesSync,\n getSentenceVerbIndicatorsSync,\n getStructuralSingleCapPrefixesSync,\n warmLegalRoleHeads,\n} from \"./detectors/legal-forms\";\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\ntype NativeSearchPatternKind =\n | \"literal\"\n | \"literal-with-options\"\n | \"regex\"\n | \"fuzzy\";\n\nexport type NativeSearchPattern = {\n kind: NativeSearchPatternKind;\n pattern: string;\n distance?: number;\n case_insensitive?: boolean;\n whole_words?: boolean;\n lazy?: boolean;\n prefilter_any?: string[];\n prefilter_case_insensitive?: boolean;\n prefilter_regex?: string;\n prefilter_window_bytes?: number;\n prepared_artifact_policy?: \"include\" | \"omit\";\n};\n\nexport type NativeSearchOptions = {\n literal_case_insensitive?: boolean;\n literal_whole_words?: boolean;\n regex_whole_words?: boolean;\n regex_overlap_all?: boolean;\n regex_artifact_policy?: \"include\" | \"omit\";\n fuzzy_case_insensitive?: boolean;\n fuzzy_whole_words?: boolean;\n fuzzy_normalize_diacritics?: boolean;\n};\n\nexport type NativeRegexMatchMeta = {\n label: string;\n score: number;\n source_detail?: string;\n requires_validation?: boolean;\n validator_id?: string;\n validator_input?: string;\n min_byte_length?: number;\n};\n\nexport type NativeDenyListFilterData = {\n stopwords: string[];\n allow_list: string[];\n person_stopwords: string[];\n person_trailing_nouns: string[];\n address_stopwords: string[];\n address_jurisdiction_prefixes: string[];\n street_types: string[];\n address_component_terms: string[];\n ambiguous_street_type_terms: string[];\n first_names: string[];\n generic_roles: string[];\n number_abbrev_prefixes: string[];\n sentence_starters: string[];\n trailing_address_word_exclusions: string[];\n document_heading_words: string[];\n document_heading_ordinal_markers: string[];\n defined_term_cues: string[];\n signing_place_guards: NativeSigningPlaceGuardData[];\n};\n\nexport type NativeSigningPlaceGuardData = {\n prefix_phrases: string[];\n suffix_phrases: string[];\n};\n\nexport type NativeDenyListMatchData = {\n labels?: string[][];\n label_table?: string[];\n label_indices?: number[][];\n custom_labels?: string[][];\n custom_label_indices?: number[][];\n originals: string[];\n sources?: string[][];\n source_table?: string[];\n source_indices?: number[][];\n filters?: NativeDenyListFilterData;\n};\n\nexport type NativeTriggerStrategy =\n | { type: \"to-next-comma\"; stop_words?: string[]; max_length?: number }\n | { type: \"to-end-of-line\" }\n | { type: \"n-words\"; count: number }\n | { type: \"company-id-value\" }\n | { type: \"address\"; max_chars?: number }\n | { type: \"match-pattern\"; pattern: string; flags?: string };\n\nexport type NativeTriggerValidation =\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 | { type: \"matches-pattern\"; pattern: string; flags?: string }\n | { type: \"valid-id\"; validator: string };\n\nexport type NativeTriggerRule = {\n trigger: string;\n label: string;\n strategy: NativeTriggerStrategy;\n validations: NativeTriggerValidation[];\n include_trigger: boolean;\n};\n\nexport type NativeTriggerData = {\n rules: NativeTriggerRule[];\n address_stop_keywords: string[];\n party_position_terms: string[];\n post_nominals: string[];\n sentence_terminal_currency_terms: string[];\n phone_extension_labels: string[];\n number_markers: string[];\n number_labels: string[];\n};\n\nexport type NativeLegalFormData = {\n suffixes: string[];\n normalized_boundary_suffixes: string[];\n normalized_in_name_words: string[];\n normalized_suffix_words: string[];\n role_heads: string[];\n sentence_verb_indicators: string[];\n clause_noun_heads: string[];\n connector_prose_heads: string[];\n structural_single_cap_prefixes: string[];\n leading_clause_phrases: string[];\n leading_clause_direct_prefixes: string[];\n connector_words: string[];\n and_connector_words: string[];\n in_name_prepositions: string[];\n company_suffix_words: string[];\n comma_gated_direct_prefixes: string[];\n};\n\nexport type NativeDateData = {\n month_names_by_language: DateMonthData;\n year_words_by_language: YearWordData;\n};\n\nexport type NativeMonetaryData = MonetaryData;\nexport type NativeAddressSeedData = AddressSeedData;\nexport type NativeAddressContextData = AddressContextData;\nexport type NativeCoreferencePatternData = {\n pattern: string;\n flags: string;\n};\nexport type NativeCoreferenceData = {\n definition_patterns: NativeCoreferencePatternData[];\n role_stop_terms: string[];\n legal_form_aliases: string[];\n organization_suffixes: string[];\n organization_determiners: string[];\n};\nexport type NativeNameCorpusData = {\n first_names: string[];\n surnames: string[];\n title_tokens: string[];\n title_abbreviations: string[];\n excluded_words: string[];\n common_words: string[];\n non_western_names: string[];\n excluded_all_caps: string[];\n ja_suffixes: string[];\n arabic_connectors: string[];\n relation_connectors: string[];\n hyphenated_prefixes: string[];\n cjk_non_person_terms: string[];\n cjk_surname_starters: string[];\n organization_terms: string[];\n};\nexport type NativeNameCorpusMode = \"full\" | \"supplemental\";\nexport type NativeZonePatternData = {\n pattern: string;\n flags: string;\n};\nexport type NativeZoneSigningClauseData = {\n prefix: string;\n suffix: string;\n prepositions: string[];\n};\nexport type NativeZoneData = {\n section_heading_patterns: NativeZonePatternData[];\n signing_clauses: NativeZoneSigningClauseData[];\n};\ntype GenericRolesData = {\n roles: string[];\n};\ntype CoreferenceDeterminersData = Record<string, readonly string[] | string>;\nexport type NativeGazetteerData = {\n labels: string[];\n is_fuzzy: boolean[];\n};\n\nexport type NativeHotwordRule = {\n hotwords: string[];\n target_labels: string[];\n score_adjustment: number;\n reclassify_to?: string;\n proximity_before: number;\n proximity_after: number;\n};\n\nexport type NativeHotwordRuleData = {\n rules: NativeHotwordRule[];\n pattern_rule_indices: number[];\n};\n\nexport type NativeSignatureData = {\n labels: string[];\n witness_phrases: string[];\n name_particles: string[];\n post_nominal_suffixes: string[];\n organization_suffixes: string[];\n image_stub_prefixes: string[];\n};\n\nexport type NativePreparedSearchConfig = {\n regex_patterns: NativeSearchPattern[];\n custom_regex_patterns: NativeSearchPattern[];\n literal_patterns: NativeSearchPattern[];\n regex_options: NativeSearchOptions;\n custom_regex_options: NativeSearchOptions;\n literal_options: NativeSearchOptions;\n literal_patterns_from_deny_list_data?: boolean;\n allowed_labels: string[];\n threshold: number;\n confidence_boost: boolean;\n slices: {\n regex: PatternSlice;\n custom_regex: PatternSlice;\n legal_forms?: PatternSlice;\n triggers?: PatternSlice;\n deny_list: PatternSlice;\n street_types?: PatternSlice;\n gazetteer: PatternSlice;\n countries: PatternSlice;\n hotwords?: PatternSlice;\n };\n regex_meta: NativeRegexMatchMeta[];\n custom_regex_meta: NativeRegexMatchMeta[];\n deny_list_data?: NativeDenyListMatchData;\n false_positive_filters?: NativeDenyListFilterData;\n gazetteer_data?: NativeGazetteerData;\n country_data?: CountryData;\n hotword_data?: NativeHotwordRuleData;\n trigger_data?: NativeTriggerData;\n legal_form_data?: NativeLegalFormData;\n address_seed_data?: NativeAddressSeedData;\n zone_data?: NativeZoneData;\n address_context_data?: NativeAddressContextData;\n coreference_data?: NativeCoreferenceData;\n name_corpus_data?: NativeNameCorpusData;\n signature_data?: NativeSignatureData;\n name_corpus_mode?: NativeNameCorpusMode;\n date_data?: NativeDateData;\n monetary_data?: NativeMonetaryData;\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\nconst sliceContains = (slice: PatternSlice, index: number): boolean =>\n index >= slice.start && index < slice.end;\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 nativeStaticConfig: NativePreparedSearchConfig;\n};\n\ntype GazetteerPatternResult = {\n patterns: PatternEntry[];\n data: GazetteerData;\n};\n\ntype CountryPatternResult = {\n patterns: PatternEntry[];\n data: CountryData;\n};\n\ntype CoreferenceConfigRow = {\n pattern: string;\n flags: string;\n};\n\ntype NameCorpusCjkLanguageData = {\n nonPersonTerms: string[];\n surnameStarters: string[];\n};\n\ntype NameCorpusCjkData = Record<\n string,\n NameCorpusCjkLanguageData | string | undefined\n>;\n\ntype NameCorpusParticleLanguageData = {\n connectors?: string[];\n relationConnectors?: string[];\n suffixes?: string[];\n hyphenatedPrefixes?: string[];\n};\n\ntype NameCorpusParticleData = Record<\n string,\n NameCorpusParticleLanguageData | string | undefined\n>;\n\ntype OrganizationIndicatorData = Record<string, string[] | string | undefined>;\n\ntype LanguageKeyedTerms = Record<\n string,\n readonly string[] | string | undefined\n>;\n\ntype TriggerSupportData = {\n phoneExtensionLabels: LanguageKeyedTerms;\n numberMarkers: LanguageKeyedTerms;\n numberLabels: LanguageKeyedTerms;\n};\n\ntype SignatureDetectionData = {\n labels: LanguageKeyedTerms;\n witnessPhrases: LanguageKeyedTerms;\n nameParticles: LanguageKeyedTerms;\n postNominalSuffixes: LanguageKeyedTerms;\n organizationSuffixes: LanguageKeyedTerms;\n imageStubPrefixes: LanguageKeyedTerms;\n};\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\ntype UnifiedSearchSources = {\n contentLanguages: readonly string[] | undefined;\n allRegex: PatternEntry[];\n regexMeta: RegexMeta[];\n customRegexes: CustomRegexPattern[];\n customRegexMeta: RegexMeta[];\n legalForms: readonly string[];\n triggers: {\n patterns: string[];\n rules: TriggerRule[];\n };\n denyListData: DenyListData | null;\n falsePositiveFilters: DenyListFilterData;\n streetTypes: string[];\n gazResult: GazetteerPatternResult | null;\n countryResult: CountryPatternResult | null;\n nativeLegalFormPatterns: string[];\n nativeLegalFormData: NativeLegalFormData | null;\n nativeDateData: NativeDateData | null;\n nativeMonetaryData: NativeMonetaryData | null;\n nativeSentenceTerminalCurrencyTerms: string[];\n nativeAddressSeedData: NativeAddressSeedData | null;\n nativeZoneData: NativeZoneData | null;\n nativeAddressContextData: NativeAddressContextData | null;\n nativeCoreferenceData: NativeCoreferenceData | null;\n nativeNameCorpusData: NativeNameCorpusData | null;\n nativeNameCorpusMode: NativeNameCorpusMode | null;\n nativeSigningPatterns: readonly PatternEntry[];\n partyPositionTerms: string[];\n hotwordRules: readonly HotwordRule[];\n nativeCurrencyPatternRange: PatternSlice;\n nativeDatePatternRange: PatternSlice;\n nativeSigningPatternRange: PatternSlice;\n nativeAllowedLabels: readonly string[];\n threshold: number;\n confidenceBoost: boolean;\n slices: UnifiedSearchInstance[\"slices\"];\n literalAllPatterns: PatternEntry[] | string[];\n canUseGlobalWholeWordLiterals: boolean;\n customDenyListNeedsWholeWords: (pattern: string) => boolean;\n};\n\nexport type NativeStaticSearchBundle = {\n nativeStaticConfig: NativePreparedSearchConfig;\n slices: UnifiedSearchInstance[\"slices\"];\n regexMeta: readonly RegexMeta[];\n customRegexMeta: readonly RegexMeta[];\n denyListData: DenyListData | null;\n falsePositiveFilters: DenyListFilterData;\n};\n\n// eslint-disable-next-line no-unsafe-type-assertion -- JSON config module shape.\nconst NAME_CORPUS_CJK = nameCorpusCjk as NameCorpusCjkData;\n// eslint-disable-next-line no-unsafe-type-assertion -- JSON config module shape.\nconst NAME_CORPUS_PARTICLES = nameCorpusParticles as NameCorpusParticleData;\n// eslint-disable-next-line no-unsafe-type-assertion -- JSON config module shape.\nconst ORGANIZATION_INDICATORS =\n organizationIndicators as OrganizationIndicatorData;\n\nconst CJK_LANGUAGE_ALIASES: Record<string, readonly string[]> = {\n zh: [\"zh\", \"zh-latn\", \"zh-hans\", \"zh-hant\"],\n ja: [\"ja\", \"ja-latn\"],\n ko: [\"ko\", \"ko-latn\"],\n};\n\nconst buildNativeNameCorpusData = (\n config: PipelineConfig,\n ctx: PipelineContext,\n): NativeNameCorpusData | null => {\n if (!config.enableNameCorpus || !ctx.nameCorpus) {\n return null;\n }\n\n const languages = config.nameCorpusLanguages?.map((language) =>\n language.toLowerCase(),\n );\n const cjkNonPersonTerms: string[] = [];\n const cjkSurnameStarters: string[] = [];\n for (const [language, value] of Object.entries(NAME_CORPUS_CJK)) {\n if (!isNameCorpusCjkLanguageData(value)) continue;\n if (!languageIsSelected(language, languages, CJK_LANGUAGE_ALIASES)) {\n continue;\n }\n cjkNonPersonTerms.push(...value.nonPersonTerms);\n cjkSurnameStarters.push(...value.surnameStarters);\n }\n\n const jaSuffixes: string[] = [];\n const arabicConnectors: string[] = [];\n const relationConnectors: string[] = [];\n const hyphenatedPrefixes: string[] = [];\n for (const [language, value] of Object.entries(NAME_CORPUS_PARTICLES)) {\n if (!isNameCorpusParticleLanguageData(value)) continue;\n if (!languageIsSelected(language, languages)) continue;\n jaSuffixes.push(...(value.suffixes ?? []));\n arabicConnectors.push(...(value.connectors ?? []));\n relationConnectors.push(...(value.relationConnectors ?? []));\n hyphenatedPrefixes.push(...(value.hyphenatedPrefixes ?? []));\n }\n\n const organizationTerms: string[] = [];\n for (const value of Object.values(ORGANIZATION_INDICATORS)) {\n if (Array.isArray(value)) {\n organizationTerms.push(...value);\n }\n }\n\n return {\n first_names: [...ctx.nameCorpus.firstNamesList],\n surnames: [...ctx.nameCorpus.surnamesList],\n title_tokens: [...ctx.nameCorpus.titlesList],\n title_abbreviations: [...ctx.nameCorpus.titleAbbreviations],\n excluded_words: [...ctx.nameCorpus.excludedList],\n common_words: [...ctx.nameCorpus.commonWords],\n non_western_names: [...ctx.nameCorpus.nonWesternNamesList],\n excluded_all_caps: [...ctx.nameCorpus.excludedAllCapsList],\n ja_suffixes: uniqueStrings(jaSuffixes),\n arabic_connectors: uniqueStrings(arabicConnectors),\n relation_connectors: uniqueStrings(relationConnectors),\n hyphenated_prefixes: uniqueStrings(hyphenatedPrefixes),\n cjk_non_person_terms: uniqueStrings(cjkNonPersonTerms),\n cjk_surname_starters: uniqueStrings(cjkSurnameStarters),\n organization_terms: uniqueStrings(organizationTerms),\n };\n};\n\nconst isNameCorpusCjkLanguageData = (\n value: NameCorpusCjkData[string],\n): value is NameCorpusCjkLanguageData =>\n typeof value === \"object\" &&\n value !== null &&\n Array.isArray(value.nonPersonTerms) &&\n Array.isArray(value.surnameStarters);\n\nconst isNameCorpusParticleLanguageData = (\n value: NameCorpusParticleData[string],\n): value is NameCorpusParticleLanguageData =>\n typeof value === \"object\" && value !== null;\n\nconst languageIsSelected = (\n language: string,\n selectedLanguages: readonly string[] | undefined,\n aliases: Record<string, readonly string[]> = {},\n): boolean => {\n if (selectedLanguages === undefined) {\n return true;\n }\n const normalized = language.toLowerCase();\n const accepted = aliases[normalized] ?? [normalized];\n return accepted.some((entry) => selectedLanguages.includes(entry));\n};\n\nconst uniqueStrings = (values: readonly string[]): string[] => {\n const seen = new Set<string>();\n const result: string[] = [];\n for (const value of values) {\n if (seen.has(value)) continue;\n seen.add(value);\n result.push(value);\n }\n return result;\n};\n\nconst TRIGGER_SUPPORT = triggerSupport as TriggerSupportData;\nconst SIGNATURE_DETECTION = signatureDetection as SignatureDetectionData;\n\nconst languageKeyedTerms = (\n values: LanguageKeyedTerms,\n selectedLanguages: readonly string[] | undefined,\n): string[] => {\n const result: string[] = [];\n for (const [language, terms] of Object.entries(values)) {\n if (!Array.isArray(terms)) {\n continue;\n }\n if (\n language !== \"und\" &&\n !languageConfigMatches(language, selectedLanguages)\n ) {\n continue;\n }\n result.push(...terms);\n }\n return uniqueStrings(result);\n};\n\nconst buildNativeSignatureData = (): NativeSignatureData => ({\n labels: languageKeyedTerms(SIGNATURE_DETECTION.labels, undefined),\n witness_phrases: languageKeyedTerms(\n SIGNATURE_DETECTION.witnessPhrases,\n undefined,\n ),\n name_particles: languageKeyedTerms(\n SIGNATURE_DETECTION.nameParticles,\n undefined,\n ),\n post_nominal_suffixes: languageKeyedTerms(\n SIGNATURE_DETECTION.postNominalSuffixes,\n undefined,\n ),\n organization_suffixes: languageKeyedTerms(\n SIGNATURE_DETECTION.organizationSuffixes,\n undefined,\n ),\n image_stub_prefixes: languageKeyedTerms(\n SIGNATURE_DETECTION.imageStubPrefixes,\n undefined,\n ),\n});\n\nconst buildUnifiedSearchSources = async (\n config: PipelineConfig,\n gazetteerEntries: GazetteerEntry[] = [],\n ctx: PipelineContext = defaultContext,\n): Promise<UnifiedSearchSources> => {\n config = applyPipelineLanguageScope(config);\n const contentLanguages = configuredContentLanguages(config);\n const legalFormsEnabled = isLegalFormsEnabled(config);\n const hotwordRules =\n config.enableHotwordRules === true ? await loadHotwordRuleSet() : [];\n const searchLabels =\n config.enableHotwordRules === true\n ? expandLabelsForHotwordRuleSet(config.labels, hotwordRules)\n : config.labels;\n const allowedLabels = createAllowedLabelSet(searchLabels);\n const regexMonetaryEnabled =\n config.enableRegex && labelIsAllowed(\"monetary amount\", allowedLabels);\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 _legalFormWarmup,\n triggers,\n denyListData,\n falsePositiveFilters,\n streetTypes,\n currencyPatterns,\n datePatterns,\n signingPatterns,\n nativeSigningPatterns,\n dateMonthData,\n yearWordData,\n monetaryData,\n addressSeedData,\n zoneData,\n addressContextData,\n coreferenceData,\n ] = await Promise.all([\n legalFormsEnabled || config.enableTriggerPhrases || config.enableCoreference\n ? warmLegalRoleHeads()\n : Promise.resolve(),\n config.enableTriggerPhrases\n ? buildTriggerPatterns(contentLanguages)\n : Promise.resolve({\n patterns: [] as string[],\n rules: [] as TriggerRule[],\n }),\n config.enableDenyList ? buildDenyList(config, ctx) : Promise.resolve(null),\n (async () => {\n await ensureDenyListData(\n ctx,\n config.dictionaries,\n config.nameCorpusLanguages,\n );\n return buildDenyListFilterData(ctx);\n })(),\n buildStreetTypePatterns(),\n regexMonetaryEnabled\n ? getCurrencyPatternEntries()\n : Promise.resolve([] as PatternEntry[]),\n config.enableRegex && labelIsAllowed(\"date\", allowedLabels)\n ? getDatePatterns(contentLanguages)\n : Promise.resolve([] as string[]),\n config.enableRegex && labelIsAllowed(\"address\", allowedLabels)\n ? getSigningClausePatterns(contentLanguages)\n : Promise.resolve([] as string[]),\n config.enableRegex && labelIsAllowed(\"address\", allowedLabels)\n ? getNativeSigningClausePatterns(contentLanguages)\n : Promise.resolve([] as PatternEntry[]),\n config.enableRegex && labelIsAllowed(\"date\", allowedLabels)\n ? getDateMonthData(contentLanguages)\n : Promise.resolve(null),\n config.enableRegex && labelIsAllowed(\"date\", allowedLabels)\n ? getYearWordData(contentLanguages)\n : Promise.resolve(null),\n config.enableTriggerPhrases || regexMonetaryEnabled\n ? getMonetaryData()\n : Promise.resolve(null),\n labelIsAllowed(\"address\", allowedLabels)\n ? getAddressSeedData()\n : Promise.resolve(null),\n config.enableZoneClassification\n ? buildNativeZoneData(contentLanguages)\n : Promise.resolve(null),\n labelIsAllowed(\"address\", allowedLabels)\n ? Promise.resolve(getAddressContextData())\n : Promise.resolve(null),\n config.enableCoreference\n ? buildNativeCoreferenceData(contentLanguages)\n : Promise.resolve(null),\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 const nativeLegalFormPatterns = legalFormsEnabled\n ? [...getKnownLegalSuffixes()]\n : [];\n const nativeLegalFormSuffixes =\n legalFormsEnabled || config.enableTriggerPhrases || config.enableCoreference\n ? [...getKnownLegalSuffixes()]\n : [];\n const nativeOrganizationSuffixes = config.enableCoreference\n ? [...LEGAL_SUFFIXES]\n : [];\n const nativeLegalFormData =\n nativeLegalFormSuffixes.length > 0\n ? {\n suffixes: nativeLegalFormSuffixes,\n normalized_boundary_suffixes: [\n ...getNormalizedLegalBoundarySuffixesSync(),\n ],\n normalized_in_name_words: [\n ...getNormalizedInNameLegalFormWordsSync(),\n ],\n normalized_suffix_words: nativeLegalFormSuffixes\n .map((suffix) => suffix.replaceAll(/[.,\\s]/g, \"\").toLowerCase())\n .filter((suffix) => suffix.length > 0),\n role_heads: [...getLegalRoleHeadsSync()],\n sentence_verb_indicators: [...getSentenceVerbIndicatorsSync()],\n clause_noun_heads: [...getClauseNounHeadsSync()],\n connector_prose_heads: [...getConnectorProseHeadsSync()],\n structural_single_cap_prefixes: [\n ...getStructuralSingleCapPrefixesSync(),\n ],\n leading_clause_phrases: [...getLeadingClauseTrimsSync().phrases],\n leading_clause_direct_prefixes: [\n ...getLeadingClauseTrimsSync().directPrefixes,\n ],\n connector_words: legalFormRuleWords.connectorWords,\n and_connector_words: legalFormRuleWords.andConnectorWords,\n in_name_prepositions: legalFormRuleWords.inNamePrepositions,\n company_suffix_words: legalFormRuleWords.companySuffixWords,\n comma_gated_direct_prefixes:\n legalFormRuleWords.commaGatedDirectPrefixes,\n }\n : null;\n const partyPositionTerms = config.enableTriggerPhrases\n ? [...getLegalRoleHeadsSync()]\n : [];\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_PATTERN_ENTRIES.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 const nativeCurrencyPatternRange = {\n start: allRegex.length,\n end: allRegex.length + currencyPatterns.length,\n };\n for (const pattern of currencyPatterns) {\n allRegex.push(pattern);\n regexMeta.push(CURRENCY_PATTERN_META);\n }\n const nativeDatePatternRange = {\n start: allRegex.length,\n end: allRegex.length + datePatterns.length,\n };\n for (const pattern of datePatterns) {\n allRegex.push(pattern);\n regexMeta.push(DATE_PATTERN_META);\n }\n const nativeSigningPatternRange = {\n start: allRegex.length,\n end: allRegex.length + signingPatterns.length,\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 const nativeDateData =\n dateMonthData === null\n ? null\n : {\n month_names_by_language: dateMonthData,\n year_words_by_language:\n config.enableTriggerPhrases === true ? (yearWordData ?? {}) : {},\n };\n const nativeMonetaryData =\n config.enableTriggerPhrases || regexMonetaryEnabled ? monetaryData : null;\n const nativeSentenceTerminalCurrencyTerms =\n sentenceTerminalCurrencyTerms(monetaryData);\n const nativeNameCorpusData = buildNativeNameCorpusData(config, ctx);\n const nativeNameCorpusMode: NativeNameCorpusMode = config.enableDenyList\n ? \"supplemental\"\n : \"full\";\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 // ── 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 hasCustomLiteralBoundaryOverride =\n denyListData?.originals.some(\n (pattern, index) =>\n (denyListData.sources[index] ?? []).includes(\"custom-deny-list\") &&\n !customDenyListNeedsWholeWords(pattern),\n ) ?? false;\n const canUseGlobalWholeWordLiterals =\n !hasCustomLiteralBoundaryOverride && 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 return {\n allRegex,\n regexMeta,\n customRegexes,\n customRegexMeta,\n legalForms,\n triggers,\n denyListData,\n falsePositiveFilters,\n streetTypes,\n gazResult,\n countryResult,\n contentLanguages,\n nativeLegalFormPatterns,\n nativeLegalFormData,\n nativeDateData,\n nativeMonetaryData,\n nativeSentenceTerminalCurrencyTerms,\n nativeAddressSeedData: addressSeedData,\n nativeZoneData: zoneData,\n nativeAddressContextData: addressContextData,\n nativeCoreferenceData:\n coreferenceData === null\n ? null\n : {\n ...coreferenceData,\n legal_form_aliases: nativeLegalFormSuffixes,\n organization_suffixes: nativeOrganizationSuffixes,\n },\n nativeNameCorpusData,\n nativeNameCorpusMode:\n nativeNameCorpusData === null ? null : nativeNameCorpusMode,\n nativeSigningPatterns,\n partyPositionTerms,\n hotwordRules,\n nativeCurrencyPatternRange,\n nativeDatePatternRange,\n nativeSigningPatternRange,\n nativeAllowedLabels: config.labels,\n threshold: config.threshold,\n confidenceBoost: config.enableConfidenceBoost,\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 literalAllPatterns,\n canUseGlobalWholeWordLiterals,\n customDenyListNeedsWholeWords,\n };\n};\n\nexport const buildNativeStaticSearchBundle = async (\n config: PipelineConfig,\n gazetteerEntries: GazetteerEntry[] = [],\n ctx: PipelineContext = defaultContext,\n): Promise<NativeStaticSearchBundle> => {\n const sources = await buildUnifiedSearchSources(\n config,\n gazetteerEntries,\n ctx,\n );\n return {\n nativeStaticConfig: buildNativeStaticConfig({\n regexPatterns: sources.allRegex,\n regexMeta: sources.regexMeta,\n customRegexes: sources.customRegexes,\n customRegexMeta: sources.customRegexMeta,\n denyListData: sources.denyListData,\n falsePositiveFilters: sources.falsePositiveFilters,\n triggerPatterns: sources.triggers.patterns,\n triggerRules: sources.triggers.rules,\n contentLanguages: sources.contentLanguages,\n legalFormPatterns: sources.nativeLegalFormPatterns,\n legalFormData: sources.nativeLegalFormData,\n dateData: sources.nativeDateData,\n monetaryData: sources.nativeMonetaryData,\n sentenceTerminalCurrencyTerms:\n sources.nativeSentenceTerminalCurrencyTerms,\n addressSeedData: sources.nativeAddressSeedData,\n zoneData: sources.nativeZoneData,\n addressContextData: sources.nativeAddressContextData,\n coreferenceData: sources.nativeCoreferenceData,\n nameCorpusData: sources.nativeNameCorpusData,\n nameCorpusMode: sources.nativeNameCorpusMode,\n nativeSigningPatterns: sources.nativeSigningPatterns,\n partyPositionTerms: sources.partyPositionTerms,\n hotwordRules: sources.hotwordRules,\n streetTypes: sources.streetTypes,\n omitRegexRanges: [\n sources.nativeCurrencyPatternRange,\n sources.nativeDatePatternRange,\n sources.nativeSigningPatternRange,\n ],\n gazetteerPatterns: sources.gazResult?.patterns ?? [],\n gazetteerData: sources.gazResult?.data ?? null,\n countryPatterns: sources.countryResult?.patterns ?? [],\n countryData: sources.countryResult?.data ?? null,\n canUseGlobalWholeWordLiterals: sources.canUseGlobalWholeWordLiterals,\n customDenyListNeedsWholeWords: sources.customDenyListNeedsWholeWords,\n allowedLabels: sources.nativeAllowedLabels,\n threshold: sources.threshold,\n confidenceBoost: sources.confidenceBoost,\n }),\n slices: sources.slices,\n regexMeta: sources.regexMeta,\n customRegexMeta: sources.customRegexMeta,\n denyListData: sources.denyListData,\n falsePositiveFilters: sources.falsePositiveFilters,\n };\n};\n\nexport const buildUnifiedSearch = async (\n config: PipelineConfig,\n gazetteerEntries: GazetteerEntry[] = [],\n ctx: PipelineContext = defaultContext,\n): Promise<UnifiedSearchInstance> => {\n const sources = await buildUnifiedSearchSources(\n config,\n gazetteerEntries,\n ctx,\n );\n const triggerEntries = sources.triggers.patterns.map((p) => ({\n pattern: p,\n literal: true as const,\n caseInsensitive: true,\n }));\n\n const regexAllPatterns = [\n ...sources.allRegex,\n ...sources.legalForms,\n ...triggerEntries,\n ];\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 sources.customRegexes.map((entry) => entry.pattern),\n {\n overlapStrategy: \"all\",\n },\n );\n\n const tsLiterals =\n sources.literalAllPatterns.length > 0\n ? new (getTextSearch())(sources.literalAllPatterns, {\n ...(sources.canUseGlobalWholeWordLiterals\n ? { allLiteral: true, wholeWords: true }\n : {}),\n caseInsensitive: true,\n overlapStrategy: \"all\",\n })\n : new (getTextSearch())([]);\n\n const nativeStaticConfig = buildNativeStaticConfig({\n regexPatterns: sources.allRegex,\n regexMeta: sources.regexMeta,\n customRegexes: sources.customRegexes,\n customRegexMeta: sources.customRegexMeta,\n denyListData: sources.denyListData,\n falsePositiveFilters: sources.falsePositiveFilters,\n triggerPatterns: sources.triggers.patterns,\n triggerRules: sources.triggers.rules,\n contentLanguages: sources.contentLanguages,\n legalFormPatterns: sources.nativeLegalFormPatterns,\n legalFormData: sources.nativeLegalFormData,\n dateData: sources.nativeDateData,\n monetaryData: sources.nativeMonetaryData,\n sentenceTerminalCurrencyTerms: sources.nativeSentenceTerminalCurrencyTerms,\n addressSeedData: sources.nativeAddressSeedData,\n zoneData: sources.nativeZoneData,\n addressContextData: sources.nativeAddressContextData,\n coreferenceData: sources.nativeCoreferenceData,\n nameCorpusData: sources.nativeNameCorpusData,\n nameCorpusMode: sources.nativeNameCorpusMode,\n nativeSigningPatterns: sources.nativeSigningPatterns,\n partyPositionTerms: sources.partyPositionTerms,\n hotwordRules: sources.hotwordRules,\n streetTypes: sources.streetTypes,\n omitRegexRanges: [\n sources.nativeCurrencyPatternRange,\n sources.nativeDatePatternRange,\n sources.nativeSigningPatternRange,\n ],\n gazetteerPatterns: sources.gazResult?.patterns ?? [],\n gazetteerData: sources.gazResult?.data ?? null,\n countryPatterns: sources.countryResult?.patterns ?? [],\n countryData: sources.countryResult?.data ?? null,\n canUseGlobalWholeWordLiterals: sources.canUseGlobalWholeWordLiterals,\n customDenyListNeedsWholeWords: sources.customDenyListNeedsWholeWords,\n allowedLabels: sources.nativeAllowedLabels,\n threshold: sources.threshold,\n confidenceBoost: sources.confidenceBoost,\n });\n\n return {\n tsRegex,\n tsCustomRegex,\n tsLiterals,\n slices: sources.slices,\n regexMeta: sources.regexMeta,\n customRegexMeta: sources.customRegexMeta,\n triggerRules: sources.triggers.rules,\n denyListData: sources.denyListData,\n gazetteerData: sources.gazResult?.data ?? null,\n countryData: sources.countryResult?.data ?? null,\n nativeStaticConfig,\n };\n};\n\ntype BuildNativeStaticConfigArgs = {\n regexPatterns: readonly PatternEntry[];\n regexMeta: readonly RegexMeta[];\n customRegexes: readonly CustomRegexPattern[];\n customRegexMeta: readonly RegexMeta[];\n denyListData: DenyListData | null;\n falsePositiveFilters: DenyListFilterData;\n triggerPatterns: readonly string[];\n triggerRules: readonly TriggerRule[];\n contentLanguages: readonly string[] | undefined;\n legalFormPatterns: readonly string[];\n legalFormData: NativeLegalFormData | null;\n dateData: NativeDateData | null;\n monetaryData: NativeMonetaryData | null;\n sentenceTerminalCurrencyTerms: readonly string[];\n addressSeedData: NativeAddressSeedData | null;\n zoneData: NativeZoneData | null;\n addressContextData: NativeAddressContextData | null;\n coreferenceData: NativeCoreferenceData | null;\n nameCorpusData: NativeNameCorpusData | null;\n nameCorpusMode: NativeNameCorpusMode | null;\n nativeSigningPatterns: readonly PatternEntry[];\n partyPositionTerms: readonly string[];\n hotwordRules: readonly HotwordRule[];\n omitRegexRanges?: readonly PatternSlice[];\n streetTypes: readonly string[];\n gazetteerPatterns: readonly PatternEntry[];\n gazetteerData: GazetteerData | null;\n countryPatterns: readonly PatternEntry[];\n countryData: CountryData | null;\n canUseGlobalWholeWordLiterals: boolean;\n customDenyListNeedsWholeWords: (pattern: string) => boolean;\n allowedLabels: readonly string[];\n threshold: number;\n confidenceBoost: boolean;\n};\n\nconst buildNativeStaticConfig = ({\n regexPatterns,\n regexMeta,\n customRegexes,\n customRegexMeta,\n denyListData,\n falsePositiveFilters,\n triggerPatterns,\n triggerRules,\n contentLanguages,\n legalFormPatterns,\n legalFormData,\n dateData,\n monetaryData,\n sentenceTerminalCurrencyTerms,\n addressSeedData,\n zoneData,\n addressContextData,\n coreferenceData,\n nameCorpusData,\n nameCorpusMode,\n nativeSigningPatterns,\n partyPositionTerms,\n hotwordRules,\n omitRegexRanges,\n streetTypes,\n gazetteerPatterns,\n gazetteerData,\n countryPatterns,\n countryData,\n canUseGlobalWholeWordLiterals,\n customDenyListNeedsWholeWords,\n allowedLabels,\n threshold,\n confidenceBoost,\n}: BuildNativeStaticConfigArgs): NativePreparedSearchConfig => {\n const nativeRegexPatterns: NativeSearchPattern[] = [];\n const nativeRegexMeta: NativeRegexMatchMeta[] = [];\n for (const [index, pattern] of regexPatterns.entries()) {\n if (omitRegexRanges?.some((range) => sliceContains(range, index))) {\n continue;\n }\n const meta = regexMeta[index];\n if (!meta) {\n continue;\n }\n nativeRegexPatterns.push(toNativeRegexPattern(pattern));\n nativeRegexMeta.push(toNativeRegexMeta(meta));\n }\n for (const pattern of nativeSigningPatterns) {\n nativeRegexPatterns.push(toNativeRegexPattern(pattern));\n nativeRegexMeta.push(toNativeRegexMeta(SIGNING_CLAUSE_META));\n }\n\n const nativeCustomRegexPatterns = customRegexes.map((entry) => ({\n kind: \"regex\" as const,\n pattern: entry.pattern,\n ...(entry.preparedArtifactPolicy === undefined\n ? {}\n : { prepared_artifact_policy: entry.preparedArtifactPolicy }),\n }));\n const nativeCustomRegexMeta = customRegexMeta.map(toNativeRegexMeta);\n const legalFormNativePatterns = legalFormPatterns.map(\n toNativeLegalFormPattern,\n );\n const triggerNativePatterns = triggerPatterns.map(toNativeTriggerPattern);\n const streetTypeNativePatterns = addressSeedData\n ? streetTypes.map((pattern) =>\n canUseGlobalWholeWordLiterals\n ? toNativeGlobalLiteralPattern(pattern)\n : toNativeDenyListPattern(pattern, true),\n )\n : [];\n const denyListPatternsFromData =\n canUseGlobalWholeWordLiterals && denyListData !== null;\n\n const denyPatterns =\n denyListData?.originals\n .map((pattern, index) => {\n if (denyListPatternsFromData) {\n return null;\n }\n return toNativeDenyListPattern(\n pattern,\n stringArrayValue(denyListData.sources[index]).includes(\n \"custom-deny-list\",\n )\n ? customDenyListNeedsWholeWords(pattern)\n : true,\n );\n })\n .filter((pattern): pattern is NativeSearchPattern => pattern !== null) ??\n [];\n const gazetteerNativePatterns = gazetteerPatterns.map(toNativeLiteralPattern);\n const countryNativePatterns = countryPatterns.map((pattern) =>\n canUseGlobalWholeWordLiterals\n ? toNativeGlobalLiteralPattern(patternEntryText(pattern))\n : toNativeLiteralPattern(pattern),\n );\n let literalOffset = 0;\n const denyListPatternCount = denyListPatternsFromData\n ? (denyListData?.originals.length ?? 0)\n : denyPatterns.length;\n const denyListSlice = {\n start: literalOffset,\n end: literalOffset + denyListPatternCount,\n };\n literalOffset = denyListSlice.end;\n const streetTypesSlice = {\n start: literalOffset,\n end: literalOffset + streetTypeNativePatterns.length,\n };\n literalOffset = streetTypesSlice.end;\n const gazetteerSlice = {\n start: literalOffset,\n end: literalOffset + gazetteerNativePatterns.length,\n };\n literalOffset = gazetteerSlice.end;\n const countriesSlice = {\n start: literalOffset,\n end: literalOffset + countryNativePatterns.length,\n };\n literalOffset = countriesSlice.end;\n const hotwordsSlice = {\n start: literalOffset,\n end: literalOffset,\n };\n const hasGazetteerFuzzyPatterns =\n gazetteerData?.isFuzzy.some((isFuzzy) => isFuzzy) ?? false;\n\n const nativeConfig: NativePreparedSearchConfig = {\n regex_patterns: nativeRegexPatterns,\n custom_regex_patterns: nativeCustomRegexPatterns,\n literal_patterns: [\n ...denyPatterns,\n ...streetTypeNativePatterns,\n ...gazetteerNativePatterns,\n ...countryNativePatterns,\n ],\n regex_options: {\n literal_case_insensitive: true,\n literal_whole_words: false,\n regex_whole_words: false,\n regex_artifact_policy: \"omit\",\n },\n custom_regex_options: {\n regex_whole_words: false,\n regex_overlap_all: true,\n regex_artifact_policy: \"omit\",\n },\n literal_options: {\n literal_case_insensitive: true,\n literal_whole_words: canUseGlobalWholeWordLiterals,\n fuzzy_case_insensitive: true,\n fuzzy_whole_words: !hasGazetteerFuzzyPatterns,\n fuzzy_normalize_diacritics: true,\n },\n literal_patterns_from_deny_list_data: denyListPatternsFromData,\n allowed_labels: [...allowedLabels],\n threshold,\n confidence_boost: confidenceBoost,\n slices: {\n regex: { start: 0, end: nativeRegexPatterns.length },\n custom_regex: { start: 0, end: nativeCustomRegexPatterns.length },\n legal_forms: {\n start: nativeRegexPatterns.length,\n end: nativeRegexPatterns.length + legalFormNativePatterns.length,\n },\n triggers: {\n start: nativeRegexPatterns.length + legalFormNativePatterns.length,\n end:\n nativeRegexPatterns.length +\n legalFormNativePatterns.length +\n triggerNativePatterns.length,\n },\n deny_list: denyListSlice,\n street_types: streetTypesSlice,\n gazetteer: gazetteerSlice,\n countries: countriesSlice,\n hotwords: hotwordsSlice,\n },\n regex_meta: nativeRegexMeta,\n custom_regex_meta: nativeCustomRegexMeta,\n signature_data: buildNativeSignatureData(),\n };\n nativeConfig.regex_patterns.push(\n ...legalFormNativePatterns,\n ...triggerNativePatterns,\n );\n if (denyListData) {\n nativeConfig.deny_list_data = toNativeDenyListData(denyListData);\n }\n nativeConfig.false_positive_filters =\n toNativeDenyListFilters(falsePositiveFilters);\n if (gazetteerData) {\n nativeConfig.gazetteer_data = toNativeGazetteerData(gazetteerData);\n }\n if (countryData) {\n nativeConfig.country_data = countryData;\n }\n if (hotwordRules.length > 0) {\n nativeConfig.hotword_data = {\n rules: hotwordRules.map(toNativeHotwordRule),\n pattern_rule_indices: [],\n };\n }\n if (triggerRules.length > 0) {\n nativeConfig.trigger_data = {\n rules: triggerRules.map(toNativeTriggerRule),\n address_stop_keywords: [...getAddressStopKeywordsSync()],\n party_position_terms: [...partyPositionTerms],\n post_nominals: [...POST_NOMINALS],\n sentence_terminal_currency_terms: [...sentenceTerminalCurrencyTerms],\n phone_extension_labels: languageKeyedTerms(\n TRIGGER_SUPPORT.phoneExtensionLabels,\n contentLanguages,\n ),\n number_markers: languageKeyedTerms(\n TRIGGER_SUPPORT.numberMarkers,\n contentLanguages,\n ),\n number_labels: languageKeyedTerms(\n TRIGGER_SUPPORT.numberLabels,\n contentLanguages,\n ),\n };\n }\n if (legalFormData) {\n nativeConfig.legal_form_data = legalFormData;\n }\n if (addressSeedData) {\n nativeConfig.address_seed_data = addressSeedData;\n }\n if (zoneData) {\n nativeConfig.zone_data = zoneData;\n }\n if (addressContextData) {\n nativeConfig.address_context_data = addressContextData;\n }\n if (coreferenceData) {\n nativeConfig.coreference_data = coreferenceData;\n }\n if (nameCorpusData) {\n nativeConfig.name_corpus_data = nameCorpusData;\n if (nameCorpusMode !== null) {\n nativeConfig.name_corpus_mode = nameCorpusMode;\n }\n }\n if (dateData) {\n nativeConfig.date_data = dateData;\n }\n if (monetaryData) {\n nativeConfig.monetary_data = monetaryData;\n }\n return nativeConfig;\n};\n\nconst toNativeLegalFormPattern = (pattern: string): NativeSearchPattern => ({\n kind: \"literal\",\n pattern,\n});\n\nconst toNativeGazetteerData = (data: GazetteerData): NativeGazetteerData => ({\n labels: [...data.labels],\n is_fuzzy: [...data.isFuzzy],\n});\n\nconst toNativeTriggerPattern = (pattern: string): NativeSearchPattern => ({\n kind: \"literal-with-options\",\n pattern,\n case_insensitive: true,\n});\n\nconst toNativeHotwordRule = (rule: HotwordRule): NativeHotwordRule => {\n const result: NativeHotwordRule = {\n hotwords: [...rule.hotwords],\n target_labels: [...rule.targetLabels],\n score_adjustment: rule.scoreAdjustment,\n proximity_before: rule.proximityBefore,\n proximity_after: rule.proximityAfter,\n };\n if (rule.reclassifyTo !== undefined) {\n result.reclassify_to = rule.reclassifyTo;\n }\n return result;\n};\n\nconst toNativeTriggerRule = (rule: TriggerRule): NativeTriggerRule => ({\n trigger: rule.trigger,\n label: rule.label,\n strategy: toNativeTriggerStrategy(rule.strategy),\n validations: rule.validations.map(toNativeTriggerValidation),\n include_trigger: rule.includeTrigger,\n});\n\nconst toNativeTriggerStrategy = (\n strategy: TriggerRule[\"strategy\"],\n): NativeTriggerStrategy => {\n switch (strategy.type) {\n case \"to-next-comma\": {\n const result: NativeTriggerStrategy = { type: \"to-next-comma\" };\n if (strategy.stopWords !== undefined) {\n result.stop_words = [...strategy.stopWords];\n }\n if (strategy.maxLength !== undefined) {\n result.max_length = strategy.maxLength;\n }\n return result;\n }\n case \"to-end-of-line\":\n return { type: \"to-end-of-line\" };\n case \"n-words\":\n return { type: \"n-words\", count: strategy.count };\n case \"company-id-value\":\n return { type: \"company-id-value\" };\n case \"address\": {\n const result: NativeTriggerStrategy = { type: \"address\" };\n if (strategy.maxChars !== undefined) {\n result.max_chars = strategy.maxChars;\n }\n return result;\n }\n case \"match-pattern\": {\n const result: NativeTriggerStrategy = {\n type: \"match-pattern\",\n pattern: strategy.pattern,\n };\n if (strategy.flags !== undefined) {\n result.flags = strategy.flags;\n }\n return result;\n }\n default: {\n const _exhaustive: never = strategy;\n throw new Error(`Unknown trigger strategy: ${String(_exhaustive)}`);\n }\n }\n};\n\nconst toNativeTriggerValidation = (\n validation: TriggerRule[\"validations\"][number],\n): NativeTriggerValidation => {\n switch (validation.type) {\n case \"starts-uppercase\":\n return { type: \"starts-uppercase\" };\n case \"min-length\":\n return { type: \"min-length\", min: validation.min };\n case \"max-length\":\n return { type: \"max-length\", max: validation.max };\n case \"no-digits\":\n return { type: \"no-digits\" };\n case \"has-digits\":\n return { type: \"has-digits\" };\n case \"matches-pattern\": {\n const result: NativeTriggerValidation = {\n type: \"matches-pattern\",\n pattern: validation.re.source,\n };\n if (validation.re.flags.length > 0) {\n result.flags = validation.re.flags;\n }\n return result;\n }\n case \"valid-id\":\n return {\n type: \"valid-id\",\n validator: validation.validator,\n };\n default: {\n const _exhaustive: never = validation;\n throw new Error(`Unknown trigger validation: ${String(_exhaustive)}`);\n }\n }\n};\n\nconst toNativeDenyListPattern = (\n pattern: string,\n wholeWords: boolean,\n): NativeSearchPattern => ({\n kind: \"literal-with-options\",\n pattern,\n case_insensitive: true,\n whole_words: wholeWords,\n});\n\nconst toNativeGlobalLiteralPattern = (\n pattern: string,\n): NativeSearchPattern => ({\n kind: \"literal\",\n pattern,\n});\n\nconst toNativeRegexPattern = (entry: PatternEntry): NativeSearchPattern => {\n const pattern: NativeSearchPattern = {\n kind: \"regex\",\n pattern: patternEntryText(entry),\n };\n if (\n typeof entry === \"string\" ||\n entry instanceof RegExp ||\n entry.pattern instanceof RegExp\n ) {\n return pattern;\n }\n\n const regexEntry = entry as {\n lazy?: boolean;\n prefilterAny?: readonly string[];\n prefilterCaseInsensitive?: boolean;\n prefilterRegex?: RegExp;\n prefilterWindowBytes?: number;\n preparedArtifactPolicy?: \"include\" | \"omit\";\n };\n if (regexEntry.lazy !== undefined) {\n pattern.lazy = regexEntry.lazy;\n }\n if (regexEntry.prefilterAny !== undefined) {\n pattern.prefilter_any = [...regexEntry.prefilterAny];\n }\n if (regexEntry.prefilterCaseInsensitive !== undefined) {\n pattern.prefilter_case_insensitive = regexEntry.prefilterCaseInsensitive;\n }\n if (regexEntry.prefilterRegex !== undefined) {\n pattern.prefilter_regex = toNativeRegexSource(regexEntry.prefilterRegex);\n }\n if (regexEntry.prefilterWindowBytes !== undefined) {\n pattern.prefilter_window_bytes = regexEntry.prefilterWindowBytes;\n }\n if (regexEntry.preparedArtifactPolicy !== undefined) {\n pattern.prepared_artifact_policy = regexEntry.preparedArtifactPolicy;\n }\n return pattern;\n};\n\nconst toNativeRegexSource = (regex: RegExp): string =>\n regex.ignoreCase ? `(?i:${regex.source})` : regex.source;\n\nconst toNativeLiteralPattern = (entry: PatternEntry): NativeSearchPattern => {\n if (typeof entry === \"string\") {\n return { kind: \"literal\", pattern: entry };\n }\n if (entry instanceof RegExp) {\n throw new Error(\"Native static config does not accept RegExp objects\");\n }\n if (entry.pattern instanceof RegExp) {\n throw new Error(\"Native static config does not accept RegExp entries\");\n }\n if (\"distance\" in entry) {\n const pattern: NativeSearchPattern = {\n kind: \"fuzzy\",\n pattern: entry.pattern,\n };\n if (entry.distance !== \"auto\") {\n pattern.distance = entry.distance;\n }\n return pattern;\n }\n if (entry.literal === true) {\n const pattern: NativeSearchPattern = {\n kind: \"literal-with-options\",\n pattern: entry.pattern,\n };\n if (entry.caseInsensitive !== undefined) {\n pattern.case_insensitive = entry.caseInsensitive;\n }\n if (entry.wholeWords !== undefined) {\n pattern.whole_words = entry.wholeWords;\n }\n return pattern;\n }\n return { kind: \"regex\", pattern: entry.pattern };\n};\n\nconst patternEntryText = (entry: PatternEntry): string => {\n if (typeof entry === \"string\") {\n return entry;\n }\n if (entry instanceof RegExp) {\n return entry.source;\n }\n if (entry.pattern instanceof RegExp) {\n return entry.pattern.source;\n }\n return entry.pattern;\n};\n\nconst toNativeRegexMeta = (meta: RegexMeta): NativeRegexMatchMeta => {\n const result: NativeRegexMatchMeta = {\n label: meta.label,\n score: meta.score,\n };\n if (meta.sourceDetail) {\n result.source_detail = meta.sourceDetail;\n }\n if (meta.validator) {\n const isSupportedValidator = nativeSupportsRegexMeta(meta);\n if (!isSupportedValidator || !meta.validatorId) {\n throw new Error(\n `Native static config does not support regex validator ${meta.validatorId ?? \"unknown\"}`,\n );\n }\n result.requires_validation = true;\n result.validator_id = meta.validatorId;\n if (meta.validatorInputKind) {\n result.validator_input = meta.validatorInputKind;\n }\n }\n if (meta.minByteLength !== undefined) {\n result.min_byte_length = meta.minByteLength;\n }\n return result;\n};\n\nconst nativeSupportsRegexMeta = (meta: RegexMeta): boolean => {\n if (!meta.validator) {\n return true;\n }\n return (\n meta.validatorId !== undefined &&\n NATIVE_REGEX_VALIDATOR_IDS.has(meta.validatorId) &&\n (meta.validatorInputKind === undefined ||\n meta.validatorInputKind === \"digits-only\" ||\n meta.validatorInputKind === \"crypto-wallet-candidate\")\n );\n};\n\nconst toNativeDenyListData = (data: DenyListData): NativeDenyListMatchData => {\n const labelEncoder = createStringGroupEncoder();\n const sourceEncoder = createStringGroupEncoder();\n const result: NativeDenyListMatchData = {\n label_table: labelEncoder.table,\n label_indices: data.labels.map(labelEncoder.encode),\n originals: data.originals,\n source_table: sourceEncoder.table,\n source_indices: data.sources.map(sourceEncoder.encode),\n filters: toNativeDenyListFilters(data.filters),\n };\n if (data.customLabels.length > 0) {\n const customLabelIndices = data.originals.map((_, index) =>\n labelEncoder.encode(data.customLabels[index]),\n );\n if (customLabelIndices.some((indices) => indices.length > 0)) {\n result.custom_label_indices = customLabelIndices;\n }\n }\n return result;\n};\n\nconst sentenceTerminalCurrencyTerms = (\n monetaryData: NativeMonetaryData | null,\n): string[] => {\n if (monetaryData === null) {\n return [];\n }\n return [\n ...new Set(\n [\n ...monetaryData.currencies.codes,\n ...monetaryData.currencies.symbols,\n ...monetaryData.currencies.local_names,\n ].filter((term) => term.length > 0),\n ),\n ].toSorted();\n};\n\nconst buildNativeCoreferenceData = async (\n selectedLanguages?: readonly string[],\n): Promise<NativeCoreferenceData> => {\n const [roleModule, determinerModule] = await Promise.all([\n import(\"./data/generic-roles.json\"),\n import(\"./data/coreference-org-determiners.json\"),\n ]);\n const roleData = (roleModule.default ?? roleModule) as GenericRolesData;\n const determinerData = (determinerModule.default ??\n determinerModule) as CoreferenceDeterminersData;\n const configs = await loadLanguageConfigs<readonly CoreferenceConfigRow[]>(\n \"coreference\",\n (mod) => {\n const moduleValue = mod as {\n default?: readonly CoreferenceConfigRow[];\n };\n return moduleValue.default ?? (mod as readonly CoreferenceConfigRow[]);\n },\n selectedLanguages === undefined ? {} : { languages: selectedLanguages },\n );\n const definitionPatterns: NativeCoreferencePatternData[] = [];\n for (const rows of configs) {\n for (const row of rows) {\n definitionPatterns.push({\n pattern: row.pattern,\n flags: row.flags,\n });\n }\n }\n\n return {\n definition_patterns: definitionPatterns,\n role_stop_terms: roleData.roles,\n legal_form_aliases: [],\n organization_suffixes: [],\n organization_determiners: Object.entries(determinerData)\n .flatMap(([language, values]) => {\n if (language === \"_comment\" || !Array.isArray(values)) {\n return [];\n }\n if (!languageConfigMatches(language, selectedLanguages)) {\n return [];\n }\n return values;\n })\n .toSorted((left, right) => left.localeCompare(right)),\n };\n};\n\nconst signingClauseLanguageMatches = (\n entryLanguage: string,\n selectedLanguages: readonly string[] | undefined,\n): boolean => {\n if (selectedLanguages === undefined || selectedLanguages.length === 0) {\n return true;\n }\n const normalizedEntry = entryLanguage.toLowerCase();\n return selectedLanguages.some((language) => {\n const normalized = language.trim().toLowerCase();\n return (\n normalized === normalizedEntry ||\n normalized.split(\"-\").at(0) === normalizedEntry\n );\n });\n};\n\nconst buildNativeZoneData = async (\n selectedLanguages?: readonly string[],\n): Promise<NativeZoneData> => {\n const [headingModule, signingModule] = await Promise.all([\n import(\"./data/section-headings.json\"),\n import(\"./data/signing-clauses.json\"),\n ]);\n const headingData = (headingModule.default ??\n headingModule) as SectionHeadingsConfig;\n const signingData = (signingModule.default ??\n signingModule) as SigningClauseConfig;\n\n return {\n section_heading_patterns: headingData.patterns.map((pattern) => ({\n pattern: pattern.re,\n flags: pattern.flags,\n })),\n signing_clauses: signingData.patterns\n .filter((pattern) =>\n signingClauseLanguageMatches(pattern.lang, selectedLanguages),\n )\n .map((pattern) => ({\n prefix: pattern.prefix ?? \"\",\n suffix: pattern.suffix ?? \"\",\n prepositions: pattern.prepositions ?? [],\n })),\n };\n};\n\nconst createStringGroupEncoder = (): {\n table: string[];\n encode: (values: string | readonly string[] | undefined) => number[];\n} => {\n const table: string[] = [];\n const indexes = new Map<string, number>();\n const encodeValue = (value: string): number => {\n const existing = indexes.get(value);\n if (existing !== undefined) {\n return existing;\n }\n const index = table.length;\n table.push(value);\n indexes.set(value, index);\n return index;\n };\n return {\n table,\n encode: (values) => {\n if (values === undefined) {\n return [];\n }\n if (typeof values === \"string\") {\n return [encodeValue(values)];\n }\n const encoded: number[] = [];\n for (const value of values) {\n encoded.push(encodeValue(value));\n }\n return encoded;\n },\n };\n};\n\nconst toNativeDenyListFilters = (\n filters: DenyListData[\"filters\"],\n): NativeDenyListFilterData => ({\n stopwords: filters.stopwords,\n allow_list: filters.allowList,\n person_stopwords: filters.personStopwords,\n person_trailing_nouns: filters.personTrailingNouns,\n address_stopwords: filters.addressStopwords,\n address_jurisdiction_prefixes: filters.addressJurisdictionPrefixes,\n street_types: filters.streetTypes,\n address_component_terms: filters.addressComponentTerms,\n ambiguous_street_type_terms: filters.ambiguousStreetTypeTerms,\n first_names: filters.firstNames,\n generic_roles: filters.genericRoles,\n number_abbrev_prefixes: filters.numberAbbrevPrefixes,\n sentence_starters: filters.sentenceStarters,\n trailing_address_word_exclusions: filters.trailingAddressWordExclusions,\n document_heading_words: filters.documentHeadingWords,\n document_heading_ordinal_markers: filters.documentHeadingOrdinalMarkers,\n defined_term_cues: filters.definedTermCues,\n signing_place_guards: filters.signingPlaceGuards.map((entry) => ({\n prefix_phrases: entry.prefixPhrases,\n suffix_phrases: entry.suffixPhrases,\n })),\n});\n\nconst stringArrayValue = (\n value: string | readonly string[] | undefined,\n): string[] => {\n if (value === undefined) {\n return [];\n }\n if (typeof value === \"string\") {\n return [value];\n }\n return [...value];\n};\n","import {\n isLegalFormsEnabled,\n type GazetteerEntry,\n type PipelineConfig,\n} from \"./types\";\nimport { languageSelectionKey } from \"./util/language-selection\";\n\nconst DEFAULT_CUSTOM_REGEX_SCORE = 0.9;\n\nconst contentLanguageFingerprint = (\n config: Pick<PipelineConfig, \"language\" | \"languages\">,\n): string => {\n const languages =\n config.languages ??\n (config.language === undefined ? [] : [config.language]);\n return languageSelectionKey(languages);\n};\n\nexport const pipelineConfigKey = (\n config: PipelineConfig,\n gazetteerEntries: readonly 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 preparedArtifactPolicy: entry.preparedArtifactPolicy ?? null,\n score: entry.score ?? DEFAULT_CUSTOM_REGEX_SCORE,\n }),\n )\n .sort()\n .join(\"\\n\")\n : \"\";\n const gazFingerprint =\n config.enableGazetteer && gazetteerEntries.length > 0\n ? gazetteerEntries\n .map(\n (entry) =>\n `${entry.id}:${entry.canonical}:${entry.label}:${[\n ...entry.variants,\n ]\n .sort()\n .join(\",\")}`,\n )\n .toSorted()\n .join(\";\")\n : \"\";\n\n return (\n `${config.enableDenyList}:` +\n `${config.enableTriggerPhrases}:` +\n `${legalFormsEnabled}:` +\n `${config.enableNameCorpus}:` +\n `${contentLanguageFingerprint(config)}:` +\n `${config.nameCorpusLanguages?.toSorted().join(\",\") ?? \"\"}:` +\n `${config.enableRegex}:` +\n `${config.threshold}:` +\n `${config.enableConfidenceBoost}:` +\n `${config.enableHotwordRules === true}:` +\n `${config.enableCoreference === true}:` +\n `${config.enableZoneClassification === true}:` +\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","import {\n buildNativeStaticSearchBundle,\n type NativePreparedSearchConfig,\n} from \"./build-unified-search\";\nimport type { PipelineContext } from \"./context\";\nimport { defaultContext } from \"./context\";\nimport { applyPipelineLanguageScope } from \"./language-scope\";\nimport { pipelineConfigKey } from \"./pipeline-cache-key\";\nimport type { Dictionaries, GazetteerEntry, PipelineConfig } from \"./types\";\nimport {\n createNativePipelineFromPackage,\n prepareNativeSearchPackage,\n PreparedNativePipeline,\n type NativeAnonymizeBinding,\n} from \"./native\";\n\nexport {\n PreparedNativePipeline,\n createNativePipelineFromPackage,\n} from \"./native\";\n\nexport type NativePipelineUnsupportedFeature = \"enableNer\";\n\nexport type NativePipelineCompatibility =\n | { status: \"supported\" }\n | {\n status: \"unsupported\";\n unsupportedFeatures: NativePipelineUnsupportedFeature[];\n };\n\nexport type NativePipelineBuildOptions = {\n binding: NativeAnonymizeBinding;\n config: PipelineConfig;\n gazetteerEntries?: GazetteerEntry[];\n context?: PipelineContext;\n};\n\nexport type NativePipelinePackageOptions = NativePipelineBuildOptions & {\n compressed?: boolean;\n};\n\nexport type { NativePipelineFromPackageOptions } from \"./native\";\n\ntype NativePipelinePackageCacheValue = Promise<Uint8Array> | Uint8Array;\n\nconst sharedPackageByDictionaries = new WeakMap<\n Dictionaries,\n Map<string, NativePipelinePackageCacheValue>\n>();\nconst sharedPackageWithoutDictionaries = new Map<\n string,\n NativePipelinePackageCacheValue\n>();\nconst dictionaryCacheIds = new WeakMap<Dictionaries, number>();\nlet nextDictionaryCacheId = 0;\n\nconst dictionaryCacheKey = (dictionaries: Dictionaries | undefined): string => {\n if (dictionaries === undefined) {\n return \"none\";\n }\n const existing = dictionaryCacheIds.get(dictionaries);\n if (existing !== undefined) {\n return `dict:${existing}`;\n }\n nextDictionaryCacheId += 1;\n dictionaryCacheIds.set(dictionaries, nextDictionaryCacheId);\n return `dict:${nextDictionaryCacheId}`;\n};\n\nconst sharedPackageCacheFor = (\n dictionaries: Dictionaries | undefined,\n): Map<string, NativePipelinePackageCacheValue> => {\n if (dictionaries === undefined) {\n return sharedPackageWithoutDictionaries;\n }\n const cached = sharedPackageByDictionaries.get(dictionaries);\n if (cached !== undefined) {\n return cached;\n }\n const created = new Map<string, NativePipelinePackageCacheValue>();\n sharedPackageByDictionaries.set(dictionaries, created);\n return created;\n};\n\nexport const getNativePipelineCompatibility = (\n config: PipelineConfig,\n): NativePipelineCompatibility => {\n const unsupportedFeatures: NativePipelineUnsupportedFeature[] = [];\n\n if (config.enableNer) unsupportedFeatures.push(\"enableNer\");\n if (unsupportedFeatures.length === 0) {\n return { status: \"supported\" };\n }\n return { status: \"unsupported\", unsupportedFeatures };\n};\n\nexport const assertNativePipelineSupported = (config: PipelineConfig): void => {\n const compatibility = getNativePipelineCompatibility(config);\n if (compatibility.status === \"supported\") {\n return;\n }\n throw new Error(\n `Native pipeline does not yet support: ${compatibility.unsupportedFeatures.join(\", \")}`,\n );\n};\n\nexport const prepareNativePipelineConfig = async ({\n config,\n gazetteerEntries = [],\n context,\n}: Omit<\n NativePipelineBuildOptions,\n \"binding\"\n>): Promise<NativePreparedSearchConfig> => {\n assertNativePipelineSupported(config);\n const bundle = await buildNativeStaticSearchBundle(\n config,\n gazetteerEntries,\n context ?? defaultContext,\n );\n return bundle.nativeStaticConfig;\n};\n\nexport const prepareNativePipelinePackage = async ({\n binding,\n config,\n gazetteerEntries = [],\n context,\n compressed = false,\n}: NativePipelinePackageOptions): Promise<Uint8Array> => {\n const packageBytes = await getCachedNativePipelinePackage({\n config,\n binding,\n gazetteerEntries,\n ...(context ? { context } : {}),\n compressed,\n });\n // Return a genuine copy: with the real NAPI binding packageBytes is a Node\n // Buffer, and Buffer.prototype.slice() yields a memory-sharing view, so a\n // caller mutating it would corrupt the shared cache and ctx.nativePipelinePackage.\n return new Uint8Array(packageBytes);\n};\n\nexport const createNativePipelineFromConfig = async ({\n binding,\n config,\n gazetteerEntries = [],\n context,\n}: NativePipelineBuildOptions): Promise<PreparedNativePipeline> => {\n const packageBytes = await getCachedNativePipelinePackage({\n binding,\n config,\n gazetteerEntries,\n ...(context ? { context } : {}),\n });\n return createNativePipelineFromPackage({ binding, packageBytes });\n};\n\nconst getCachedNativePipelinePackage = async ({\n binding,\n config,\n gazetteerEntries = [],\n context,\n compressed = false,\n}: NativePipelinePackageOptions): Promise<Uint8Array> => {\n const scopedConfig = applyPipelineLanguageScope(config);\n assertNativePipelineSupported(scopedConfig);\n const ctx = context ?? defaultContext;\n const key = nativePackageCacheKey({\n binding,\n config: scopedConfig,\n gazetteerEntries,\n compressed,\n });\n if (ctx.nativePipelinePackage && ctx.nativePipelinePackageKey === key) {\n return ctx.nativePipelinePackage;\n }\n if (\n ctx.nativePipelinePackagePromise &&\n ctx.nativePipelinePackageKey === key\n ) {\n return ctx.nativePipelinePackagePromise;\n }\n\n const sharedCache = sharedPackageCacheFor(scopedConfig.dictionaries);\n const shared = sharedCache.get(key);\n if (shared !== undefined) {\n const packageBytes = await shared;\n ctx.nativePipelinePackage = packageBytes;\n ctx.nativePipelinePackageKey = key;\n ctx.nativePipelinePackagePromise = null;\n return packageBytes;\n }\n\n ctx.nativePipelinePackage = null;\n ctx.nativePipelinePackageKey = key;\n const promise = buildNativePipelinePackage({\n binding,\n config: scopedConfig,\n gazetteerEntries,\n context: ctx,\n compressed,\n });\n ctx.nativePipelinePackagePromise = promise;\n sharedCache.set(key, promise);\n let packageBytes: Uint8Array;\n try {\n packageBytes = await promise;\n } catch (error) {\n if (sharedCache.get(key) === promise) {\n sharedCache.delete(key);\n }\n if (\n ctx.nativePipelinePackageKey === key &&\n ctx.nativePipelinePackagePromise === promise\n ) {\n ctx.nativePipelinePackage = null;\n ctx.nativePipelinePackagePromise = null;\n }\n throw error;\n }\n if (sharedCache.get(key) === promise) {\n sharedCache.set(key, packageBytes);\n }\n if (ctx.nativePipelinePackageKey === key) {\n ctx.nativePipelinePackage = packageBytes;\n ctx.nativePipelinePackagePromise = null;\n }\n return packageBytes;\n};\n\nconst buildNativePipelinePackage = async ({\n binding,\n config,\n gazetteerEntries,\n context,\n compressed,\n}: Required<NativePipelinePackageOptions>): Promise<Uint8Array> => {\n const bundle = await buildNativeStaticSearchBundle(\n config,\n gazetteerEntries,\n context,\n );\n return prepareNativeSearchPackage({\n binding,\n config: bundle.nativeStaticConfig,\n compressed,\n });\n};\n\ntype NativePackageCacheKeyOptions = {\n binding: NativeAnonymizeBinding;\n config: PipelineConfig;\n gazetteerEntries: readonly GazetteerEntry[];\n compressed: boolean;\n};\n\nconst nativePackageCacheKey = ({\n binding,\n config,\n gazetteerEntries,\n compressed,\n}: NativePackageCacheKeyOptions): string =>\n [\n binding.nativePackageVersion(),\n compressed ? \"compressed\" : \"raw\",\n dictionaryCacheKey(config.dictionaries),\n pipelineConfigKey(config, gazetteerEntries),\n ].join(\":\");\n","import { DEFAULT_ENTITY_LABELS } from \"./constants\";\nimport type { PipelineConfig } from \"./types\";\n\nexport const DEFAULT_NATIVE_PIPELINE_CONFIG: PipelineConfig = {\n threshold: 0.3,\n enableTriggerPhrases: true,\n enableRegex: true,\n enableLegalForms: true,\n enableNameCorpus: true,\n enableDenyList: true,\n enableGazetteer: false,\n enableCountries: true,\n enableNer: false,\n enableConfidenceBoost: true,\n enableCoreference: true,\n enableHotwordRules: true,\n enableZoneClassification: true,\n labels: [...DEFAULT_ENTITY_LABELS],\n workspaceId: \"native-pipeline-default\",\n};\n","import { createRequire } from \"node:module\";\nimport { existsSync, readFileSync, readdirSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport process from \"node:process\";\n\nimport {\n assertNativeBindingVersion,\n createNativePipelineFromPackage,\n type NativeOperatorConfig,\n type NativeAnonymizeBinding,\n type NativeNormalizeOptions,\n type NativeSearchPackageInput,\n PreparedNativeAnonymizer,\n PreparedNativePipeline,\n type NativeStaticRedactionResult,\n diagnostics_json as diagnosticsJsonWithBinding,\n diagnostics_stream_json as diagnosticsStreamJsonWithBinding,\n load_prepared_package as loadPreparedPackageWithBinding,\n native_package_version as nativePackageVersionWithBinding,\n normalize_for_search as normalizeForSearchWithBinding,\n prepare_search_package as prepareSearchPackageWithBinding,\n redact_text as redactTextWithBinding,\n redact_text_json as redactTextJsonWithBinding,\n redact_text_stream_json as redactTextStreamJsonWithBinding,\n summary_diagnostics_json as summaryDiagnosticsJsonWithBinding,\n} from \"./native\";\n\nexport * from \"./native\";\nexport {\n assertNativePipelineSupported,\n createNativePipelineFromConfig,\n getNativePipelineCompatibility,\n prepareNativePipelineConfig,\n prepareNativePipelinePackage,\n} from \"./native-pipeline\";\nexport type {\n NativePipelineBuildOptions,\n NativePipelineCompatibility,\n NativePipelinePackageOptions,\n NativePipelineUnsupportedFeature,\n} from \"./native-pipeline\";\n\nexport type NativeRequire = (specifier: string) => unknown;\n\nexport type NativeLibc = \"gnu\" | \"musl\";\n\nexport type LoadNativeBindingOptions = {\n expectedVersion?: string;\n platform?: string;\n arch?: string;\n libc?: NativeLibc;\n env?: Record<string, string | undefined>;\n requireModule?: NativeRequire;\n};\n\nexport type NativePipelinePackageFileOptions = LoadNativeBindingOptions & {\n binding?: NativeAnonymizeBinding;\n packagePath: string;\n};\n\nexport type NativeSdkOptions = LoadNativeBindingOptions & {\n binding?: NativeAnonymizeBinding;\n};\n\nexport type NativeSdkPackageOptions = NativeSdkOptions & {\n compressed?: boolean;\n};\n\nexport type DefaultNativePipelinePackageOptions = LoadNativeBindingOptions & {\n binding?: NativeAnonymizeBinding;\n language?: string;\n packagePath?: string;\n warmup?: DefaultNativePipelineWarmup;\n};\n\ntype ResolvedDefaultNativePipelineOptions = {\n binding: NativeAnonymizeBinding;\n language?: string;\n packagePath?: string;\n warmup: DefaultNativePipelineWarmup;\n};\n\nexport const DEFAULT_NATIVE_PIPELINE_WARMUPS = {\n lazyRegex: \"lazy-regex\",\n none: \"none\",\n} as const;\n\nexport type DefaultNativePipelineWarmup =\n (typeof DEFAULT_NATIVE_PIPELINE_WARMUPS)[keyof typeof DEFAULT_NATIVE_PIPELINE_WARMUPS];\n\nexport type DefaultNativePipelinePackageFileOptions = {\n language?: string;\n};\n\nconst LOCAL_NATIVE_LOADER = \"../index.cjs\";\nconst PACKAGE_SPECIFIC_NATIVE_PATH = \"STELLA_ANONYMIZE_NATIVE_LIBRARY_PATH\";\nconst DEFAULT_NATIVE_PIPELINE_PACKAGE_URL = new URL(\n \"../native-pipeline.stlanonpkg\",\n import.meta.url,\n);\nconst DEFAULT_NATIVE_PIPELINE_PACKAGE_DIR_URL = new URL(\"../\", import.meta.url);\nconst DEFAULT_NATIVE_PIPELINE_LANGUAGE_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;\nconst DEFAULT_NATIVE_PIPELINE_LANGUAGE_PACKAGE_PATTERN =\n /^native-pipeline\\.([a-z0-9]+(?:-[a-z0-9]+)*)\\.stlanonpkg$/u;\nconst DEFAULT_NATIVE_PIPELINE_PACKAGE_CACHE_KEY = \"<default>\";\nconst defaultNativePipelineCache = new WeakMap<\n NativeAnonymizeBinding,\n Map<string, PreparedNativePipeline>\n>();\nconst warmedDefaultNativePipelines = new WeakSet<PreparedNativePipeline>();\nconst defaultNativePipelineInflightCache = new WeakMap<\n NativeAnonymizeBinding,\n Map<string, Promise<PreparedNativePipeline>>\n>();\n\nexport { DEFAULT_NATIVE_PIPELINE_CONFIG } from \"./native-default-config\";\n\nexport const loadNativeAnonymizeBinding = (\n options: LoadNativeBindingOptions = {},\n): NativeAnonymizeBinding => {\n const requireModule = options.requireModule ?? createRequire(import.meta.url);\n const platform = options.platform ?? process.platform;\n const arch = options.arch ?? process.arch;\n const libc = options.libc ?? detectNativeLibc(platform);\n const env = options.env ?? process.env;\n const specifiers = nativeBindingSpecifiers({ arch, env, libc, platform });\n const errors: string[] = [];\n\n for (const specifier of specifiers) {\n const binding = tryLoadNativeBinding({\n specifier,\n requireModule,\n errors,\n });\n if (!binding) {\n continue;\n }\n if (options.expectedVersion !== undefined) {\n assertNativeBindingVersion({\n binding,\n expectedVersion: options.expectedVersion,\n });\n }\n return binding;\n }\n\n if (nativeBindingPackageName({ arch, libc, platform }) === null) {\n throw unsupportedNativeTargetError({ arch, errors, libc, platform });\n }\n throw new Error(\n `Unable to load native anonymize binding for ${platform}/${arch}:\\n${errors.join(\"\\n\")}`,\n );\n};\n\nexport const readNativePipelinePackageFile = (\n packagePath: string,\n): Uint8Array => readFileSync(packagePath);\n\nexport const readNativePipelinePackageFileAsync = async (\n packagePath: string,\n): Promise<Uint8Array> => readFile(packagePath);\n\nexport const native_package_version = (\n options: NativeSdkOptions = {},\n): string => nativePackageVersionWithBinding(resolveNativeSdkBinding(options));\n\nexport const normalize_for_search = (\n text: string,\n options: NativeSdkOptions = {},\n): string => {\n const args: NativeNormalizeOptions = {\n binding: resolveNativeSdkBinding(options),\n text,\n };\n return normalizeForSearchWithBinding(args);\n};\n\nexport const prepare_search_package = (\n config: NativeSearchPackageInput,\n { compressed = false, ...options }: NativeSdkPackageOptions = {},\n): Uint8Array =>\n prepareSearchPackageWithBinding({\n binding: resolveNativeSdkBinding(options),\n config,\n compressed,\n });\n\nexport const load_prepared_package = (\n packageBytes: Uint8Array,\n options: NativeSdkOptions = {},\n) =>\n loadPreparedPackageWithBinding({\n binding: resolveNativeSdkBinding(options),\n packageBytes,\n });\n\nexport const load_prepared_package_file = (\n packagePath: string,\n options: NativeSdkOptions = {},\n) => load_prepared_package(readNativePipelinePackageFile(packagePath), options);\n\nexport const redact_text = (\n config: NativeSearchPackageInput,\n fullText: string,\n operators?: NativeOperatorConfig,\n options: NativeSdkOptions = {},\n): NativeStaticRedactionResult =>\n redactTextWithBinding({\n binding: resolveNativeSdkBinding(options),\n config,\n fullText,\n ...(operators !== undefined ? { operators } : {}),\n });\n\nexport const redact_text_json = (\n config: NativeSearchPackageInput,\n fullText: string,\n operators?: NativeOperatorConfig,\n options: NativeSdkOptions = {},\n): string =>\n redactTextJsonWithBinding({\n binding: resolveNativeSdkBinding(options),\n config,\n fullText,\n ...(operators !== undefined ? { operators } : {}),\n });\n\nexport const redact_text_stream_json = (\n config: NativeSearchPackageInput,\n fullText: string,\n onEvent: (eventJson: string) => void,\n operators?: NativeOperatorConfig,\n options: NativeSdkOptions = {},\n): string | null =>\n redactTextStreamJsonWithBinding({\n binding: resolveNativeSdkBinding(options),\n config,\n fullText,\n onEvent,\n ...(operators !== undefined ? { operators } : {}),\n });\n\nexport const diagnostics_json = (\n config: NativeSearchPackageInput,\n fullText: string,\n operators?: NativeOperatorConfig,\n options: NativeSdkOptions = {},\n): string | null =>\n diagnosticsJsonWithBinding({\n binding: resolveNativeSdkBinding(options),\n config,\n fullText,\n ...(operators !== undefined ? { operators } : {}),\n });\n\nexport const diagnostics_stream_json = (\n config: NativeSearchPackageInput,\n fullText: string,\n onBatch: (diagnosticsJson: string) => void,\n operators?: NativeOperatorConfig,\n options: NativeSdkOptions = {},\n): string | null =>\n diagnosticsStreamJsonWithBinding({\n binding: resolveNativeSdkBinding(options),\n config,\n fullText,\n onBatch,\n ...(operators !== undefined ? { operators } : {}),\n });\n\nexport const summary_diagnostics_json = (\n config: NativeSearchPackageInput,\n fullText: string,\n operators?: NativeOperatorConfig,\n options: NativeSdkOptions = {},\n): string | null =>\n summaryDiagnosticsJsonWithBinding({\n binding: resolveNativeSdkBinding(options),\n config,\n fullText,\n ...(operators !== undefined ? { operators } : {}),\n });\n\nexport const readDefaultNativePipelinePackageFile = ({\n language,\n}: DefaultNativePipelinePackageFileOptions = {}): Uint8Array => {\n const packageUrl = defaultNativePipelinePackageUrl(language);\n try {\n return readFileSync(packageUrl);\n } catch (error) {\n throw new Error(\n `${defaultNativePipelinePackageDescription(language)} is unavailable: ${formatLoadError(error)}`,\n );\n }\n};\n\nexport const read_default_native_pipeline_package_file = (\n options: DefaultNativePipelinePackageFileOptions = {},\n): Uint8Array => readDefaultNativePipelinePackageFile(options);\n\nexport const availableDefaultNativePipelineLanguages = (): string[] => {\n const languages = new Set<string>();\n try {\n for (const fileName of readdirSync(\n DEFAULT_NATIVE_PIPELINE_PACKAGE_DIR_URL,\n )) {\n const match = fileName.match(\n DEFAULT_NATIVE_PIPELINE_LANGUAGE_PACKAGE_PATTERN,\n );\n if (match?.[1] !== undefined) {\n languages.add(match[1]);\n }\n }\n } catch (error) {\n throw new Error(\n `Default native pipeline package directory is unavailable: ${formatLoadError(error)}`,\n );\n }\n return [...languages].toSorted();\n};\n\nexport const available_default_native_pipeline_languages =\n availableDefaultNativePipelineLanguages;\n\nexport const readDefaultNativePipelinePackageFileAsync = async ({\n language,\n}: DefaultNativePipelinePackageFileOptions = {}): Promise<Uint8Array> => {\n const packageUrl = defaultNativePipelinePackageUrl(language);\n try {\n return await readFile(packageUrl);\n } catch (error) {\n throw new Error(\n `${defaultNativePipelinePackageDescription(language)} is unavailable: ${formatLoadError(error)}`,\n );\n }\n};\n\nexport const createNativePipelineFromPackageFile = ({\n binding,\n packagePath,\n expectedVersion,\n ...loadOptions\n}: NativePipelinePackageFileOptions): PreparedNativePipeline => {\n const resolvedBinding =\n binding ??\n loadNativeAnonymizeBinding({\n ...loadOptions,\n ...(expectedVersion !== undefined ? { expectedVersion } : {}),\n });\n if (binding && expectedVersion !== undefined) {\n assertNativeBindingVersion({ binding, expectedVersion });\n }\n return createNativePipelineFromPackage({\n binding: resolvedBinding,\n packageBytes: readNativePipelinePackageFile(packagePath),\n });\n};\n\nexport const createNativePipelineFromDefaultPackage = (\n options: DefaultNativePipelinePackageOptions = {},\n): PreparedNativePipeline => {\n const resolvedOptions = resolveDefaultNativePipelineOptions(options);\n return applyDefaultNativePipelineWarmup(\n createNativePipelineFromResolvedDefaultPackage(resolvedOptions),\n resolvedOptions.warmup,\n );\n};\n\nexport const create_native_pipeline_from_default_package = (\n options: DefaultNativePipelinePackageOptions = {},\n): PreparedNativePipeline => createNativePipelineFromDefaultPackage(options);\n\nexport const getDefaultNativePipeline = (\n options: DefaultNativePipelinePackageOptions = {},\n): PreparedNativePipeline => {\n const resolvedOptions = resolveDefaultNativePipelineOptions(options);\n const cache = defaultPipelineCacheFor(resolvedOptions.binding);\n const key = defaultPipelineCacheKey(resolvedOptions);\n const cached = cache.get(key);\n if (cached !== undefined) {\n return applyDefaultNativePipelineWarmup(cached, resolvedOptions.warmup);\n }\n const pipeline =\n createNativePipelineFromResolvedDefaultPackage(resolvedOptions);\n cache.set(key, pipeline);\n return applyDefaultNativePipelineWarmup(pipeline, resolvedOptions.warmup);\n};\n\nexport const get_default_native_pipeline = (\n options: DefaultNativePipelinePackageOptions = {},\n): PreparedNativePipeline => getDefaultNativePipeline(options);\n\nexport const preloadDefaultNativePipeline = (\n options: DefaultNativePipelinePackageOptions = {},\n): PreparedNativePipeline => {\n const pipeline = getDefaultNativePipeline(options);\n return applyDefaultNativePipelineWarmup(\n pipeline,\n DEFAULT_NATIVE_PIPELINE_WARMUPS.lazyRegex,\n );\n};\n\nexport const preload_default_native_pipeline = (\n options: DefaultNativePipelinePackageOptions = {},\n): PreparedNativePipeline => preloadDefaultNativePipeline(options);\n\nexport const redactDefaultText = (\n fullText: string,\n operators?: NativeOperatorConfig,\n options: DefaultNativePipelinePackageOptions = {},\n): NativeStaticRedactionResult =>\n getDefaultNativePipeline(options).redactText(fullText, operators);\n\nexport const redact_default_text = (\n fullText: string,\n operators?: NativeOperatorConfig,\n options: DefaultNativePipelinePackageOptions = {},\n): NativeStaticRedactionResult =>\n redactDefaultText(fullText, operators, options);\n\nexport const redactDefaultTextJson = (\n fullText: string,\n operators?: NativeOperatorConfig,\n options: DefaultNativePipelinePackageOptions = {},\n): string =>\n getDefaultNativePipeline(options).redact_text_json(fullText, operators);\n\nexport const redact_default_text_json = (\n fullText: string,\n operators?: NativeOperatorConfig,\n options: DefaultNativePipelinePackageOptions = {},\n): string => redactDefaultTextJson(fullText, operators, options);\n\nexport const preloadDefaultNativePipelineAsync = (\n options: DefaultNativePipelinePackageOptions = {},\n): Promise<PreparedNativePipeline> => {\n const resolvedOptions = {\n ...resolveDefaultNativePipelineOptions(options),\n warmup: DEFAULT_NATIVE_PIPELINE_WARMUPS.lazyRegex,\n };\n const cache = defaultPipelineCacheFor(resolvedOptions.binding);\n const key = defaultPipelineCacheKey(resolvedOptions);\n const cached = cache.get(key);\n if (cached !== undefined) {\n return Promise.resolve(\n applyDefaultNativePipelineWarmup(cached, resolvedOptions.warmup),\n );\n }\n\n const inflightCache = defaultPipelineInflightCacheFor(\n resolvedOptions.binding,\n );\n const inflight = inflightCache.get(key);\n if (inflight !== undefined) {\n return inflight;\n }\n\n const promise = createNativePipelineFromResolvedDefaultPackageAsync(\n resolvedOptions,\n )\n .then((pipeline) => {\n cache.set(key, pipeline);\n return applyDefaultNativePipelineWarmup(pipeline, resolvedOptions.warmup);\n })\n .finally(() => {\n inflightCache.delete(key);\n });\n inflightCache.set(key, promise);\n return promise;\n};\n\nconst resolveDefaultNativePipelineOptions = ({\n binding,\n language,\n packagePath,\n warmup,\n expectedVersion,\n ...loadOptions\n}: DefaultNativePipelinePackageOptions = {}): ResolvedDefaultNativePipelineOptions => {\n if (language !== undefined && packagePath !== undefined) {\n throw new Error(\"Use either language or packagePath, not both\");\n }\n const resolvedBinding =\n binding ??\n loadNativeAnonymizeBinding({\n ...loadOptions,\n ...(expectedVersion !== undefined ? { expectedVersion } : {}),\n });\n if (binding && expectedVersion !== undefined) {\n assertNativeBindingVersion({ binding, expectedVersion });\n }\n return {\n binding: resolvedBinding,\n warmup: normalizeDefaultNativePipelineWarmup(warmup),\n ...(language !== undefined\n ? { language: resolveDefaultNativePipelineLanguage(language) }\n : {}),\n ...(packagePath !== undefined ? { packagePath } : {}),\n };\n};\n\nconst applyDefaultNativePipelineWarmup = (\n pipeline: PreparedNativePipeline,\n warmup: DefaultNativePipelineWarmup,\n): PreparedNativePipeline => {\n if (warmup !== DEFAULT_NATIVE_PIPELINE_WARMUPS.lazyRegex) {\n return pipeline;\n }\n if (!warmedDefaultNativePipelines.has(pipeline)) {\n pipeline.warmLazyRegex();\n warmedDefaultNativePipelines.add(pipeline);\n }\n return pipeline;\n};\n\nconst createNativePipelineFromResolvedDefaultPackage = ({\n binding,\n language,\n packagePath,\n}: ResolvedDefaultNativePipelineOptions): PreparedNativePipeline => {\n const packageBytes =\n packagePath === undefined\n ? readDefaultNativePipelinePackageFile(\n defaultPackageFileOptions(language),\n )\n : readNativePipelinePackageFile(packagePath);\n return createNativePipelineFromTrustedDefaultPackage(binding, packageBytes);\n};\n\nconst createNativePipelineFromResolvedDefaultPackageAsync = async ({\n binding,\n language,\n packagePath,\n}: ResolvedDefaultNativePipelineOptions): Promise<PreparedNativePipeline> => {\n const packageBytes =\n packagePath === undefined\n ? await readDefaultNativePipelinePackageFileAsync(\n defaultPackageFileOptions(language),\n )\n : await readNativePipelinePackageFileAsync(packagePath);\n return createNativePipelineFromTrustedDefaultPackage(binding, packageBytes);\n};\n\nconst createNativePipelineFromTrustedDefaultPackage = (\n binding: NativeAnonymizeBinding,\n packageBytes: Uint8Array,\n): PreparedNativePipeline =>\n new PreparedNativePipeline(\n new PreparedNativeAnonymizer(\n binding.NativePreparedSearch.fromTrustedPreparedPackageBytesWithoutCache?.(\n packageBytes,\n ) ??\n binding.NativePreparedSearch.fromTrustedPreparedPackageBytes?.(\n packageBytes,\n ) ??\n binding.NativePreparedSearch.fromPreparedPackageBytesWithoutCache?.(\n packageBytes,\n ) ??\n binding.NativePreparedSearch.fromPreparedPackageBytes(packageBytes),\n ),\n );\n\nconst defaultPackageFileOptions = (\n language: string | undefined,\n): DefaultNativePipelinePackageFileOptions =>\n language === undefined ? {} : { language };\n\nconst normalizeDefaultNativePipelineWarmup = (\n warmup: DefaultNativePipelineWarmup | undefined,\n): DefaultNativePipelineWarmup => {\n if (warmup === undefined) {\n return DEFAULT_NATIVE_PIPELINE_WARMUPS.none;\n }\n switch (warmup) {\n case DEFAULT_NATIVE_PIPELINE_WARMUPS.lazyRegex:\n case DEFAULT_NATIVE_PIPELINE_WARMUPS.none:\n return warmup;\n }\n throw new Error(\n 'Default native pipeline warmup must be \"lazy-regex\" or \"none\"',\n );\n};\n\nconst resolveNativeSdkBinding = ({\n binding,\n expectedVersion,\n ...loadOptions\n}: NativeSdkOptions): NativeAnonymizeBinding => {\n const resolvedBinding =\n binding ??\n loadNativeAnonymizeBinding({\n ...loadOptions,\n ...(expectedVersion !== undefined ? { expectedVersion } : {}),\n });\n if (binding && expectedVersion !== undefined) {\n assertNativeBindingVersion({ binding, expectedVersion });\n }\n return resolvedBinding;\n};\n\nconst defaultPipelineCacheFor = (\n binding: NativeAnonymizeBinding,\n): Map<string, PreparedNativePipeline> => {\n const cached = defaultNativePipelineCache.get(binding);\n if (cached !== undefined) {\n return cached;\n }\n const created = new Map<string, PreparedNativePipeline>();\n defaultNativePipelineCache.set(binding, created);\n return created;\n};\n\nconst defaultPipelineInflightCacheFor = (\n binding: NativeAnonymizeBinding,\n): Map<string, Promise<PreparedNativePipeline>> => {\n const cached = defaultNativePipelineInflightCache.get(binding);\n if (cached !== undefined) {\n return cached;\n }\n const created = new Map<string, Promise<PreparedNativePipeline>>();\n defaultNativePipelineInflightCache.set(binding, created);\n return created;\n};\n\nconst defaultPipelineCacheKey = ({\n binding,\n language,\n packagePath,\n}: ResolvedDefaultNativePipelineOptions): string =>\n [\n binding.nativePackageVersion(),\n packagePath ??\n (language === undefined\n ? DEFAULT_NATIVE_PIPELINE_PACKAGE_CACHE_KEY\n : `language:${language}`),\n ].join(\"\\0\");\n\nconst defaultNativePipelinePackageUrl = (language: string | undefined): URL => {\n if (language === undefined) {\n return DEFAULT_NATIVE_PIPELINE_PACKAGE_URL;\n }\n const normalized = resolveDefaultNativePipelineLanguage(language);\n return defaultNativePipelineLanguagePackageUrl(normalized);\n};\n\nconst defaultNativePipelineLanguagePackageUrl = (language: string): URL =>\n new URL(`../native-pipeline.${language}.stlanonpkg`, import.meta.url);\n\nconst resolveDefaultNativePipelineLanguage = (language: string): string => {\n const normalized = normalizeDefaultNativePipelineLanguage(language);\n const exactUrl = defaultNativePipelineLanguagePackageUrl(normalized);\n if (existsSync(exactUrl)) {\n return normalized;\n }\n const baseLanguage = normalized.split(\"-\").at(0);\n if (baseLanguage === undefined || baseLanguage === normalized) {\n return normalized;\n }\n const baseUrl = defaultNativePipelineLanguagePackageUrl(baseLanguage);\n if (existsSync(baseUrl)) {\n return baseLanguage;\n }\n return normalized;\n};\n\nconst defaultNativePipelinePackageDescription = (\n language: string | undefined,\n): string =>\n language === undefined\n ? \"Default native pipeline package\"\n : `Default native pipeline package for language \"${resolveDefaultNativePipelineLanguage(language)}\"`;\n\nconst normalizeDefaultNativePipelineLanguage = (language: string): string => {\n const normalized = language.trim().toLowerCase();\n if (!DEFAULT_NATIVE_PIPELINE_LANGUAGE_PATTERN.test(normalized)) {\n throw new Error(\n `Default native pipeline language must match ${DEFAULT_NATIVE_PIPELINE_LANGUAGE_PATTERN.source}`,\n );\n }\n return normalized;\n};\n\ntype NativeBindingSpecifiersOptions = {\n arch: string;\n env: Record<string, string | undefined>;\n libc: NativeLibc | undefined;\n platform: string;\n};\n\nconst nativeBindingSpecifiers = ({\n arch,\n env,\n libc,\n platform,\n}: NativeBindingSpecifiersOptions): string[] => {\n const specifiers: string[] = [];\n const overridePath = env[PACKAGE_SPECIFIC_NATIVE_PATH];\n if (overridePath) {\n specifiers.push(overridePath);\n }\n specifiers.push(LOCAL_NATIVE_LOADER);\n const platformPackage = nativeBindingPackageName({ arch, libc, platform });\n if (platformPackage !== null) {\n specifiers.push(platformPackage);\n }\n return specifiers;\n};\n\ntype NativeBindingTarget = {\n platform: string;\n arch: string;\n libc?: NativeLibc;\n package: string;\n};\n\n// Single source of truth for published native sidecars. Both the runtime\n// package lookup and the \"unsupported target\" error message derive from this\n// table, so a target is never advertised as supported without a package (and\n// vice versa). musl Linux is intentionally absent: no musl sidecar is shipped.\nconst NATIVE_BINDING_TARGETS: readonly NativeBindingTarget[] = [\n {\n platform: \"darwin\",\n arch: \"arm64\",\n package: \"@stll/anonymize-darwin-arm64\",\n },\n { platform: \"darwin\", arch: \"x64\", package: \"@stll/anonymize-darwin-x64\" },\n {\n platform: \"linux\",\n arch: \"arm64\",\n libc: \"gnu\",\n package: \"@stll/anonymize-linux-arm64-gnu\",\n },\n {\n platform: \"linux\",\n arch: \"x64\",\n libc: \"gnu\",\n package: \"@stll/anonymize-linux-x64-gnu\",\n },\n { platform: \"win32\", arch: \"x64\", package: \"@stll/anonymize-win32-x64-msvc\" },\n];\n\ntype NativeBindingPackageNameOptions = {\n arch: string;\n libc: NativeLibc | undefined;\n platform: string;\n};\n\ntype DescribeNativeTargetOptions = {\n arch: string;\n libc?: NativeLibc | undefined;\n platform: string;\n};\n\nconst describeNativeTarget = ({\n arch,\n libc,\n platform,\n}: DescribeNativeTargetOptions): string =>\n libc === undefined ? `${platform}-${arch}` : `${platform}-${arch}-${libc}`;\n\nconst SUPPORTED_NATIVE_TARGETS: readonly string[] = NATIVE_BINDING_TARGETS.map(\n (target) => describeNativeTarget(target),\n);\n\nconst nativeBindingPackageName = ({\n arch,\n libc,\n platform,\n}: NativeBindingPackageNameOptions): string | null => {\n const match = NATIVE_BINDING_TARGETS.find(\n (target) =>\n target.platform === platform &&\n target.arch === arch &&\n (target.libc === undefined || target.libc === libc),\n );\n return match?.package ?? null;\n};\n\nconst unsupportedNativeTargetError = ({\n arch,\n errors,\n libc,\n platform,\n}: NativeBindingPackageNameOptions & { errors: string[] }): Error => {\n const target = describeNativeTarget({ arch, libc, platform });\n const supported = SUPPORTED_NATIVE_TARGETS.join(\", \");\n const attempts = errors.length > 0 ? `\\n${errors.join(\"\\n\")}` : \"\";\n return new Error(\n `No native anonymize binding is published for ${target}; supported targets: ${supported}. Set ${PACKAGE_SPECIFIC_NATIVE_PATH} to a locally built binding to run on this platform.${attempts}`,\n );\n};\n\nconst detectNativeLibc = (platform: string): NativeLibc | undefined => {\n if (platform !== \"linux\") {\n return undefined;\n }\n const report = process.report?.getReport();\n const header =\n isPropertyBag(report) && isPropertyBag(report[\"header\"])\n ? report[\"header\"]\n : null;\n return typeof header?.[\"glibcVersionRuntime\"] === \"string\" ? \"gnu\" : \"musl\";\n};\n\ntype TryLoadNativeBindingOptions = {\n specifier: string;\n requireModule: NativeRequire;\n errors: string[];\n};\n\nconst tryLoadNativeBinding = ({\n specifier,\n requireModule,\n errors,\n}: TryLoadNativeBindingOptions): NativeAnonymizeBinding | null => {\n try {\n const loaded = requireModule(specifier);\n const binding = toNativeAnonymizeBinding(loaded);\n if (binding) {\n return binding;\n }\n errors.push(`${specifier}: module does not match native binding shape`);\n } catch (error) {\n errors.push(`${specifier}: ${formatLoadError(error)}`);\n }\n return null;\n};\n\nconst toNativeAnonymizeBinding = (\n value: unknown,\n): NativeAnonymizeBinding | null => {\n const candidate =\n isPropertyBag(value) && isPropertyBag(value[\"default\"])\n ? value[\"default\"]\n : value;\n return isNativeAnonymizeBinding(candidate) ? candidate : null;\n};\n\nconst isNativeAnonymizeBinding = (\n candidate: unknown,\n): candidate is NativeAnonymizeBinding => {\n if (!isPropertyBag(candidate)) {\n return false;\n }\n if (typeof candidate[\"nativePackageVersion\"] !== \"function\") {\n return false;\n }\n if (typeof candidate[\"normalizeForSearch\"] !== \"function\") {\n return false;\n }\n if (typeof candidate[\"prepareStaticSearchPackageBytes\"] !== \"function\") {\n return false;\n }\n if (\n typeof candidate[\"prepareStaticSearchCompressedPackageBytes\"] !== \"function\"\n ) {\n return false;\n }\n const preparedSearch = candidate[\"NativePreparedSearch\"];\n if (!isPropertyBag(preparedSearch)) {\n return false;\n }\n if (typeof preparedSearch[\"fromConfigJsonBytes\"] !== \"function\") {\n return false;\n }\n if (typeof preparedSearch[\"fromPreparedPackageBytes\"] !== \"function\") {\n return false;\n }\n return true;\n};\n\nconst isPropertyBag = (value: unknown): value is Record<string, unknown> =>\n (typeof value === \"object\" && value !== null) || typeof value === \"function\";\n\nconst formatLoadError = (error: unknown): string => {\n if (error instanceof Error) {\n return error.message;\n }\n return String(error);\n};\n"],"mappings":"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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,qCAAqB,IAAI,IAAI;CACxC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;;;;;;AAaD,MAAa,yCAAyB,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;ACvNA,MAAa,iBAAoC,CAAC,GAAG;CAlFnD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CAMA;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;CAEA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CAKA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AAGmD,CAAkB,CAAC,CAAC,MACtE,GAAG,MAAM,EAAE,SAAS,EAAE,MACzB;;;ACjGA,MAAM,yBAAyB,aAC7B,SAAS,KAAK,CAAC,CAAC,YAAY;AAE9B,MAAM,8BACJ,cAEA,cAAc,KAAA,IACV,CAAC,IACD,UACG,IAAI,qBAAqB,CAAC,CAC1B,QAAQ,aAAa,SAAS,SAAS,CAAC;AAEjD,MAAa,wBACX,cACW;CACX,MAAM,aAAa,2BAA2B,SAAS,CAAC,CAAC,SAAS;CAClE,OAAO,WAAW,WAAW,IAAI,MAAM,WAAW,KAAK,GAAG;AAC5D;AAEA,MAAM,gBAAgB,aAA6B;CACjD,MAAM,QAAQ,SAAS,QAAQ,GAAG;CAClC,OAAO,UAAU,KAAK,WAAW,SAAS,MAAM,GAAG,KAAK;AAC1D;AAEA,MAAa,yBACX,gBACA,sBACY;CACZ,IAAI,sBAAsB,KAAA,KAAa,kBAAkB,WAAW,GAClE,OAAO;CAET,MAAM,8BACJ,2BAA2B,iBAAiB;CAC9C,IAAI,4BAA4B,WAAW,GACzC,OAAO;C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Q,KAAK,YAAY;CAC/B,MAAM,WAAqB,CAAC;CAC5B,KAAK,MAAM,QAAQ,kBAAkB;EACnC,IAAI,CAAC,KAAK,KAAK,KAAK,KAAK,GACvB;EAEF,MAAM,OAAO,KAAK,MAAM,GAAG,KAAK,SAAS,KAAK,OAAO,MAAM;EAC3D,IAAI,KAAK,SAAS,GAChB;EAEF,KAAK,MAAM,QAAQ,KAAK,OACtB,SAAS,KAAK,OAAO,IAAI;CAE7B;CACA,OAAO;AACT;AA0J0B,IAAI,IAC5B;CACE;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;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,CAAC,CAAC,KAAK,EAAE,CACX;AAiCkB,IAAI,KAAK,UAAU,KAAA,GAAW,EAC9C,aAAa,OACf,CAAC;;;;;;;ACvrBD,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;CA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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}