@stll/anonymize-wasm 1.0.8 → 1.0.9

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 (257) hide show
  1. package/dist/AT.mjs +282 -0
  2. package/dist/AT.mjs.map +1 -0
  3. package/dist/AT2.mjs +343 -0
  4. package/dist/AT2.mjs.map +1 -0
  5. package/dist/AU.mjs +122 -0
  6. package/dist/AU.mjs.map +1 -0
  7. package/dist/AU2.mjs +90 -0
  8. package/dist/AU2.mjs.map +1 -0
  9. package/dist/BE.mjs +144 -0
  10. package/dist/BE.mjs.map +1 -0
  11. package/dist/BE2.mjs +107 -0
  12. package/dist/BE2.mjs.map +1 -0
  13. package/dist/BG.mjs +73 -0
  14. package/dist/BG.mjs.map +1 -0
  15. package/dist/BG2.mjs +42 -0
  16. package/dist/BG2.mjs.map +1 -0
  17. package/dist/BR.mjs +226 -0
  18. package/dist/BR.mjs.map +1 -0
  19. package/dist/BR2.mjs +339 -0
  20. package/dist/BR2.mjs.map +1 -0
  21. package/dist/CA.mjs +275 -0
  22. package/dist/CA.mjs.map +1 -0
  23. package/dist/CA2.mjs +222 -0
  24. package/dist/CA2.mjs.map +1 -0
  25. package/dist/CZ.mjs +212 -0
  26. package/dist/CZ.mjs.map +1 -0
  27. package/dist/CZ2.mjs +619 -0
  28. package/dist/CZ2.mjs.map +1 -0
  29. package/dist/CZ3.mjs +127 -0
  30. package/dist/CZ3.mjs.map +1 -0
  31. package/dist/DE.mjs +1652 -0
  32. package/dist/DE.mjs.map +1 -0
  33. package/dist/DE2.mjs +3885 -0
  34. package/dist/DE2.mjs.map +1 -0
  35. package/dist/DE3.mjs +669 -0
  36. package/dist/DE3.mjs.map +1 -0
  37. package/dist/DK.mjs +218 -0
  38. package/dist/DK.mjs.map +1 -0
  39. package/dist/DK2.mjs +162 -0
  40. package/dist/DK2.mjs.map +1 -0
  41. package/dist/ES.mjs +522 -0
  42. package/dist/ES.mjs.map +1 -0
  43. package/dist/ES2.mjs +352 -0
  44. package/dist/ES2.mjs.map +1 -0
  45. package/dist/FI.mjs +267 -0
  46. package/dist/FI.mjs.map +1 -0
  47. package/dist/FI2.mjs +107 -0
  48. package/dist/FI2.mjs.map +1 -0
  49. package/dist/FR.mjs +224 -0
  50. package/dist/FR.mjs.map +1 -0
  51. package/dist/FR2.mjs +1823 -0
  52. package/dist/FR2.mjs.map +1 -0
  53. package/dist/GB.mjs +298 -0
  54. package/dist/GB.mjs.map +1 -0
  55. package/dist/GB2.mjs +208 -0
  56. package/dist/GB2.mjs.map +1 -0
  57. package/dist/HR.mjs +37 -0
  58. package/dist/HR.mjs.map +1 -0
  59. package/dist/HR2.mjs +16 -0
  60. package/dist/HR2.mjs.map +1 -0
  61. package/dist/HU.mjs +59 -0
  62. package/dist/HU.mjs.map +1 -0
  63. package/dist/HU2.mjs +29 -0
  64. package/dist/HU2.mjs.map +1 -0
  65. package/dist/IE.mjs +35 -0
  66. package/dist/IE.mjs.map +1 -0
  67. package/dist/IE2.mjs +19 -0
  68. package/dist/IE2.mjs.map +1 -0
  69. package/dist/IT.mjs +341 -0
  70. package/dist/IT.mjs.map +1 -0
  71. package/dist/IT2.mjs +46 -0
  72. package/dist/IT2.mjs.map +1 -0
  73. package/dist/NL.mjs +298 -0
  74. package/dist/NL.mjs.map +1 -0
  75. package/dist/NL2.mjs +305 -0
  76. package/dist/NL2.mjs.map +1 -0
  77. package/dist/NO.mjs +178 -0
  78. package/dist/NO.mjs.map +1 -0
  79. package/dist/NO2.mjs +346 -0
  80. package/dist/NO2.mjs.map +1 -0
  81. package/dist/PL.mjs +175 -0
  82. package/dist/PL.mjs.map +1 -0
  83. package/dist/PL2.mjs +107 -0
  84. package/dist/PL2.mjs.map +1 -0
  85. package/dist/PT.mjs +78 -0
  86. package/dist/PT.mjs.map +1 -0
  87. package/dist/PT2.mjs +657 -0
  88. package/dist/PT2.mjs.map +1 -0
  89. package/dist/RO.mjs +181 -0
  90. package/dist/RO.mjs.map +1 -0
  91. package/dist/RO2.mjs +46 -0
  92. package/dist/RO2.mjs.map +1 -0
  93. package/dist/SE.mjs +270 -0
  94. package/dist/SE.mjs.map +1 -0
  95. package/dist/SE2.mjs +369 -0
  96. package/dist/SE2.mjs.map +1 -0
  97. package/dist/SI.mjs +27 -0
  98. package/dist/SI.mjs.map +1 -0
  99. package/dist/SI2.mjs +88 -0
  100. package/dist/SI2.mjs.map +1 -0
  101. package/dist/SK.mjs +61 -0
  102. package/dist/SK.mjs.map +1 -0
  103. package/dist/SK2.mjs +26 -0
  104. package/dist/SK2.mjs.map +1 -0
  105. package/dist/US.mjs +1061 -0
  106. package/dist/US.mjs.map +1 -0
  107. package/dist/US2.mjs +518 -0
  108. package/dist/US2.mjs.map +1 -0
  109. package/dist/address-boundaries.mjs +85 -0
  110. package/dist/address-boundaries.mjs.map +1 -0
  111. package/dist/address-prepositions.mjs +182 -0
  112. package/dist/address-prepositions.mjs.map +1 -0
  113. package/dist/address-street-types.mjs +162 -0
  114. package/dist/address-street-types.mjs.map +1 -0
  115. package/dist/allow-list.mjs +183 -0
  116. package/dist/allow-list.mjs.map +1 -0
  117. package/dist/amount-words.mjs +42 -0
  118. package/dist/amount-words.mjs.map +1 -0
  119. package/dist/coreference.cs.mjs +14 -0
  120. package/dist/coreference.cs.mjs.map +1 -0
  121. package/dist/coreference.de.mjs +14 -0
  122. package/dist/coreference.de.mjs.map +1 -0
  123. package/dist/coreference.en.mjs +14 -0
  124. package/dist/coreference.en.mjs.map +1 -0
  125. package/dist/coreference.sk.mjs +10 -0
  126. package/dist/coreference.sk.mjs.map +1 -0
  127. package/dist/cs.mjs +5080 -0
  128. package/dist/cs.mjs.map +1 -0
  129. package/dist/cs2.mjs +17609 -0
  130. package/dist/cs2.mjs.map +1 -0
  131. package/dist/currencies.mjs +231 -0
  132. package/dist/currencies.mjs.map +1 -0
  133. package/dist/date-months.mjs +618 -0
  134. package/dist/date-months.mjs.map +1 -0
  135. package/dist/de4.mjs +8214 -0
  136. package/dist/de4.mjs.map +1 -0
  137. package/dist/de5.mjs +8197 -0
  138. package/dist/de5.mjs.map +1 -0
  139. package/dist/dist.mjs +619 -0
  140. package/dist/dist.mjs.map +1 -0
  141. package/dist/en.mjs +9977 -0
  142. package/dist/en.mjs.map +1 -0
  143. package/dist/en2.mjs +5014 -0
  144. package/dist/en2.mjs.map +1 -0
  145. package/dist/es3.mjs +9059 -0
  146. package/dist/es3.mjs.map +1 -0
  147. package/dist/es4.mjs +8254 -0
  148. package/dist/es4.mjs.map +1 -0
  149. package/dist/eu-institutions.mjs +518 -0
  150. package/dist/eu-institutions.mjs.map +1 -0
  151. package/dist/fr3.mjs +9999 -0
  152. package/dist/fr3.mjs.map +1 -0
  153. package/dist/fr4.mjs +9975 -0
  154. package/dist/fr4.mjs.map +1 -0
  155. package/dist/generic-roles.mjs +65 -0
  156. package/dist/generic-roles.mjs.map +1 -0
  157. package/dist/global.mjs +195368 -0
  158. package/dist/global.mjs.map +1 -0
  159. package/dist/hospitals-CZ.mjs +747 -0
  160. package/dist/hospitals-CZ.mjs.map +1 -0
  161. package/dist/hospitals-DE.mjs +3356 -0
  162. package/dist/hospitals-DE.mjs.map +1 -0
  163. package/dist/hotword-rules.mjs +145 -0
  164. package/dist/hotword-rules.mjs.map +1 -0
  165. package/dist/hu3.mjs +9964 -0
  166. package/dist/hu3.mjs.map +1 -0
  167. package/dist/hu4.mjs +5031 -0
  168. package/dist/hu4.mjs.map +1 -0
  169. package/dist/it3.mjs +5037 -0
  170. package/dist/it3.mjs.map +1 -0
  171. package/dist/it4.mjs +9989 -0
  172. package/dist/it4.mjs.map +1 -0
  173. package/dist/legal-forms.mjs +2112 -0
  174. package/dist/legal-forms.mjs.map +1 -0
  175. package/dist/manifest.mjs +56 -0
  176. package/dist/manifest.mjs.map +1 -0
  177. package/dist/ministries-AT.mjs +180 -0
  178. package/dist/ministries-AT.mjs.map +1 -0
  179. package/dist/ministries-CZ.mjs +201 -0
  180. package/dist/ministries-CZ.mjs.map +1 -0
  181. package/dist/ministries-DE.mjs +859 -0
  182. package/dist/ministries-DE.mjs.map +1 -0
  183. package/dist/ministries-SK.mjs +174 -0
  184. package/dist/ministries-SK.mjs.map +1 -0
  185. package/dist/names-exclusions.mjs +191 -0
  186. package/dist/names-exclusions.mjs.map +1 -0
  187. package/dist/names-first.mjs +418 -0
  188. package/dist/names-first.mjs.map +1 -0
  189. package/dist/names-surnames.mjs +113 -0
  190. package/dist/names-surnames.mjs.map +1 -0
  191. package/dist/names-title-tokens.mjs +40 -0
  192. package/dist/names-title-tokens.mjs.map +1 -0
  193. package/dist/person-stopwords.mjs +69 -0
  194. package/dist/person-stopwords.mjs.map +1 -0
  195. package/dist/pl3.mjs +5059 -0
  196. package/dist/pl3.mjs.map +1 -0
  197. package/dist/pl4.mjs +9991 -0
  198. package/dist/pl4.mjs.map +1 -0
  199. package/dist/ro3.mjs +10000 -0
  200. package/dist/ro3.mjs.map +1 -0
  201. package/dist/ro4.mjs +5037 -0
  202. package/dist/ro4.mjs.map +1 -0
  203. package/dist/section-headings.mjs +52 -0
  204. package/dist/section-headings.mjs.map +1 -0
  205. package/dist/signing-clauses.mjs +78 -0
  206. package/dist/signing-clauses.mjs.map +1 -0
  207. package/dist/sk3.mjs +9205 -0
  208. package/dist/sk3.mjs.map +1 -0
  209. package/dist/sk4.mjs +15242 -0
  210. package/dist/sk4.mjs.map +1 -0
  211. package/dist/stopwords.mjs +9915 -0
  212. package/dist/stopwords.mjs.map +1 -0
  213. package/dist/sv.mjs +8312 -0
  214. package/dist/sv.mjs.map +1 -0
  215. package/dist/sv2.mjs +8439 -0
  216. package/dist/sv2.mjs.map +1 -0
  217. package/dist/translations.mjs +603 -0
  218. package/dist/translations.mjs.map +1 -0
  219. package/dist/triggers.cs.mjs +465 -0
  220. package/dist/triggers.cs.mjs.map +1 -0
  221. package/dist/triggers.de.mjs +139 -0
  222. package/dist/triggers.de.mjs.map +1 -0
  223. package/dist/triggers.en.mjs +72 -0
  224. package/dist/triggers.en.mjs.map +1 -0
  225. package/dist/triggers.es.mjs +57 -0
  226. package/dist/triggers.es.mjs.map +1 -0
  227. package/dist/triggers.fr.mjs +76 -0
  228. package/dist/triggers.fr.mjs.map +1 -0
  229. package/dist/triggers.global.mjs +34 -0
  230. package/dist/triggers.global.mjs.map +1 -0
  231. package/dist/triggers.hu.mjs +41 -0
  232. package/dist/triggers.hu.mjs.map +1 -0
  233. package/dist/triggers.it.mjs +52 -0
  234. package/dist/triggers.it.mjs.map +1 -0
  235. package/dist/triggers.pl.mjs +84 -0
  236. package/dist/triggers.pl.mjs.map +1 -0
  237. package/dist/triggers.ro.mjs +59 -0
  238. package/dist/triggers.ro.mjs.map +1 -0
  239. package/dist/triggers.sk.mjs +62 -0
  240. package/dist/triggers.sk.mjs.map +1 -0
  241. package/dist/triggers.sv.mjs +58 -0
  242. package/dist/triggers.sv.mjs.map +1 -0
  243. package/dist/universities-AT.mjs +410 -0
  244. package/dist/universities-AT.mjs.map +1 -0
  245. package/dist/universities-CZ.mjs +427 -0
  246. package/dist/universities-CZ.mjs.map +1 -0
  247. package/dist/universities-DE.mjs +3393 -0
  248. package/dist/universities-DE.mjs.map +1 -0
  249. package/dist/universities-SK.mjs +215 -0
  250. package/dist/universities-SK.mjs.map +1 -0
  251. package/dist/vite.mjs +4 -2
  252. package/dist/vite.mjs.map +1 -1
  253. package/dist/wasm.mjs +548 -46
  254. package/dist/wasm.mjs.map +1 -1
  255. package/dist/year-words.mjs +62 -0
  256. package/dist/year-words.mjs.map +1 -0
  257. package/package.json +1 -5
package/dist/wasm.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"wasm.mjs","names":["PERSON_CHAIN_BREAK_RE","isInitialContinuationGap","segmenter","escapeRegex","WHITESPACE_RE","sigmoid"],"sources":["../../src/search-engine.ts","../../src/types.ts","../../src/context.ts","../../src/util/lang-loader.ts","../../src/detectors/coreference.ts","../../src/detectors/gazetteer.ts","../../src/util/text.ts","../../src/detectors/names.ts","../../src/config/titles.ts","../../src/util/char-groups.ts","../../src/detectors/regex.ts","../../src/detectors/legal-forms.ts","../../src/config/legal-forms.ts","../../src/detectors/triggers.ts","../../src/regions.ts","../../src/filters/false-positives.ts","../../src/util/normalize.ts","../../src/detectors/deny-list.ts","../../src/detectors/address-seeds.ts","../../src/detectors/org-propagation.ts","../../src/filters/confidence-boost.ts","../../src/filters/zone-classifier.ts","../../src/filters/hotword-rules.ts","../../src/filters/boundary-consistency.ts","../../src/build-unified-search.ts","../../src/unified-search.ts","../../src/util/entity-masking.ts","../../src/pipeline.ts","../../src/operators.ts","../../src/redact.ts","../../src/gliner/decoder.ts","../../src/gliner/token-decoder.ts","../../src/gliner/processor.ts","../../src/util/chunker.ts","../../src/util/levenshtein.ts","../../src/wasm.ts"],"sourcesContent":["/**\n * Late-bound TextSearch constructor.\n *\n * Native entry injects @stll/text-search,\n * WASM entry injects @stll/text-search-wasm.\n * Both expose the same API.\n */\n\n/* eslint-disable-next-line @typescript-eslint/no-explicit-any */\ntype TextSearchCtor = new (...args: any[]) => any;\n\nlet _TextSearch: TextSearchCtor | undefined;\n\nexport const initTextSearch = (ctor: TextSearchCtor): void => {\n _TextSearch = ctor;\n};\n\nexport const getTextSearch = (): TextSearchCtor => {\n if (!_TextSearch) {\n throw new Error(\n \"TextSearch not initialized. Import from \" +\n \"@stll/anonymize or @stll/anonymize-wasm, \" +\n \"not from internal modules.\",\n );\n }\n return _TextSearch;\n};\n","/**\n * Source of a detected entity span.\n * Ordered by detection layer in the pipeline.\n */\nexport const DETECTION_SOURCES = {\n TRIGGER: \"trigger\",\n REGEX: \"regex\",\n DENY_LIST: \"deny-list\",\n LEGAL_FORM: \"legal-form\",\n GAZETTEER: \"gazetteer\",\n NER: \"ner\",\n COREFERENCE: \"coreference\",\n} as const;\n\nexport type DetectionSource =\n (typeof DETECTION_SOURCES)[keyof typeof DETECTION_SOURCES];\n\n/**\n * Priority levels for detection sources.\n * Higher = more structurally reliable. Used during\n * overlap resolution so deterministic detectors beat\n * probabilistic ones regardless of raw score.\n */\nexport const DETECTOR_PRIORITY: Record<DetectionSource, number> = {\n gazetteer: 5,\n trigger: 4,\n \"legal-form\": 3,\n regex: 3,\n \"deny-list\": 2,\n coreference: 2,\n ner: 1,\n};\n\n/**\n * A detected PII entity span in the source text.\n * Every detection layer produces these.\n */\nexport type Entity = {\n start: number;\n end: number;\n label: string;\n text: string;\n score: number;\n source: DetectionSource;\n};\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 | { type: \"to-next-comma\" }\n | { type: \"to-end-of-line\" }\n | { type: \"n-words\"; count: number }\n | { type: \"company-id-value\" }\n | { type: \"address\"; maxChars?: number };\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/** 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/**\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\n/**\n * Anonymisation operator types. Each operator defines\n * how a confirmed entity is replaced in the output.\n */\nexport const OPERATOR_TYPES = [\"replace\", \"redact\"] as const;\n\nexport type OperatorType = (typeof OPERATOR_TYPES)[number];\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\nexport type PipelineConfig = {\n threshold: number;\n enableTriggerPhrases: boolean;\n enableRegex: boolean;\n /**\n * Enables legal-form organization detection.\n * Required for typed callers; legacy untyped\n * callers that omit this field are treated as\n * enabled at runtime for backward compatibility.\n */\n enableLegalForms: boolean;\n /**\n * Enables first-name/surname/title corpus matching.\n * When deny-list mode is enabled, this also controls\n * whether name-corpus entries are injected into the\n * deny-list search automaton.\n */\n enableNameCorpus: boolean;\n enableDenyList: boolean;\n denyListCountries?: string[];\n denyListRegions?: string[];\n denyListExcludeCategories?: string[];\n enableGazetteer: 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\n/**\n * Canonical entity labels used across the pipeline.\n * NER models may use different native labels; the bench\n * NER wrapper maps model output to these canonical names.\n *\n * These labels are ephemeral: entities are regenerated on\n * every pipeline run and never persisted to the database.\n * Renaming a label here requires no migration.\n */\nexport const DEFAULT_ENTITY_LABELS = [\n \"person\",\n \"organization\",\n \"phone number\",\n \"address\",\n \"email address\",\n \"date\",\n \"date of birth\",\n \"bank account number\",\n \"iban\",\n \"tax identification number\",\n \"identity card number\",\n \"registration number\",\n \"credit card number\",\n \"passport number\",\n \"monetary amount\",\n \"land parcel\",\n] as const;\n\nexport const isLegalFormsEnabled = (\n config: Pick<PipelineConfig, \"enableLegalForms\">,\n): boolean => config.enableLegalForms !== false;\n","import type { UnifiedSearchInstance } from \"./build-unified-search\";\nimport type { Entity } from \"./types\";\n\n/**\n * Build a stable cache key for an entity that survives\n * shallow copies (spread). Uses position + label so the\n * key is identical for the original object and any\n * `{ ...entity }` copy produced by mergeAndDedup.\n */\nexport const corefKey = (e: Entity): string => `${e.start}:${e.end}:${e.label}`;\n\n/**\n * Compiled RegExp pattern used for coreference\n * definition extraction.\n */\nexport type DefinitionPattern = {\n pattern: RegExp;\n};\n\n/**\n * Cached data for the name corpus detector.\n * Populated by initNameCorpus; consumed by\n * detectNameCorpus and deny-list AC integration.\n */\nexport type NameCorpusData = {\n firstNames: ReadonlySet<string>;\n surnames: ReadonlySet<string>;\n titleTokens: ReadonlySet<string>;\n excludedWords: ReadonlySet<string>;\n /** Raw arrays exposed for deny-list AC integration. */\n firstNamesList: readonly string[];\n surnamesList: readonly string[];\n titlesList: readonly string[];\n excludedList: readonly string[];\n};\n\n/**\n * All cached state for a single pipeline run (or\n * sequence of runs sharing the same config). Replacing\n * module-level singletons with this object enables\n * concurrent pipelines with different configs and\n * simplifies testing.\n *\n * Each field starts null and is populated lazily on\n * first use by the corresponding loader function.\n */\nexport type PipelineContext = {\n // ── Unified search cache ──────────────────────\n search: UnifiedSearchInstance | null;\n searchKey: string;\n searchPromise: Promise<UnifiedSearchInstance> | null;\n\n // ── Name corpus ───────────────────────────────\n nameCorpus: NameCorpusData | null;\n 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 /** First-name exclusions for stopword filtering. */\n firstNameExclusions: ReadonlySet<string> | null;\n firstNameExclusionCorpusLen: number;\n\n // ── Generic roles (false-positive filter) ─────\n genericRoles: ReadonlySet<string> | null;\n genericRolesPromise: Promise<ReadonlySet<string>> | null;\n\n // ── Coreference ───────────────────────────────\n corefPatterns: DefinitionPattern[] | null;\n corefPatternsPromise: Promise<DefinitionPattern[]> | null;\n corefLoadAttempted: boolean;\n roleStopSet: ReadonlySet<string> | null;\n roleStopSetPromise: Promise<ReadonlySet<string>> | null;\n\n // ── Zone classifier ───────────────────────────\n zoneHeadingPatterns: RegExp[] | null;\n zoneSigningPatterns: RegExp[] | null;\n zoneInitPromise: Promise<void> | null;\n\n // ── Coreference source map ────────────────────\n /**\n * Maps coreference entities to their source entity\n * text. Populated by findCoreferenceSpans, consumed\n * by buildPlaceholderMap for consistent placeholder\n * numbering across aliases and source entities.\n *\n * Keyed by `start:end:label` composite string so\n * lookups survive shallow copies (e.g. from\n * mergeAndDedup's spread operator).\n */\n corefSourceMap: Map<string, string>;\n};\n\n/** Create a fresh, empty pipeline context. */\nexport const createPipelineContext = (): PipelineContext => ({\n search: null,\n searchKey: \"\",\n searchPromise: null,\n\n nameCorpus: null,\n nameCorpusPromise: null,\n\n stopwords: null,\n stopwordsPromise: null,\n allowList: null,\n allowListPromise: null,\n personStopwords: null,\n personStopwordsPromise: null,\n firstNameExclusions: null,\n firstNameExclusionCorpusLen: 0,\n\n genericRoles: null,\n genericRolesPromise: null,\n\n corefPatterns: null,\n corefPatternsPromise: null,\n corefLoadAttempted: false,\n roleStopSet: null,\n roleStopSetPromise: null,\n\n zoneHeadingPatterns: null,\n zoneSigningPatterns: null,\n zoneInitPromise: null,\n\n corefSourceMap: new Map(),\n});\n\n/**\n * Module-level default context. Used when callers\n * don't provide an explicit context, preserving full\n * backward compatibility with the existing API.\n */\nexport const defaultContext: PipelineContext = createPipelineContext();\n","/**\n * Language manifest loader and config auto-discovery.\n *\n * Reads `manifest.json` from @stll/anonymize-data to\n * determine which languages have which config types,\n * then loads them via a static registry of import()\n * calls (string literals for bundler compatibility).\n */\n\n// ── Types ────────────────────────────────────────────\n\ntype ManifestLanguage = {\n triggers?: boolean;\n coreference?: boolean;\n};\n\ntype Manifest = {\n languages: Record<string, ManifestLanguage>;\n};\n\ntype ConfigType = \"triggers\" | \"coreference\";\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(\"@stll/anonymize-data/config/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(\"@stll/anonymize-data/config/triggers.cs.json\"),\n de: () => import(\"@stll/anonymize-data/config/triggers.de.json\"),\n en: () => import(\"@stll/anonymize-data/config/triggers.en.json\"),\n es: () => import(\"@stll/anonymize-data/config/triggers.es.json\"),\n fr: () => import(\"@stll/anonymize-data/config/triggers.fr.json\"),\n hu: () => import(\"@stll/anonymize-data/config/triggers.hu.json\"),\n it: () => import(\"@stll/anonymize-data/config/triggers.it.json\"),\n pl: () => import(\"@stll/anonymize-data/config/triggers.pl.json\"),\n ro: () => import(\"@stll/anonymize-data/config/triggers.ro.json\"),\n sk: () => import(\"@stll/anonymize-data/config/triggers.sk.json\"),\n sv: () => import(\"@stll/anonymize-data/config/triggers.sv.json\"),\n};\n\nconst COREFERENCE_LOADERS: Record<string, () => Promise<unknown>> = {\n cs: () => import(\"@stll/anonymize-data/config/coreference.cs.json\"),\n de: () => import(\"@stll/anonymize-data/config/coreference.de.json\"),\n en: () => import(\"@stll/anonymize-data/config/coreference.en.json\"),\n sk: () => import(\"@stll/anonymize-data/config/coreference.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};\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: [\"cs\", \"de\", \"en\", \"es\", \"fr\", \"hu\", \"it\", \"pl\", \"ro\", \"sk\", \"sv\"],\n coreference: [\"cs\", \"de\", \"en\", \"sk\"],\n};\n\n// ── Public API ───────────────────────────────────────\n\n/**\n * Load all config files of a given type for all\n * languages enabled in the manifest.\n *\n * Falls back to the hardcoded language list when the\n * manifest is unavailable (backward compatibility).\n */\nexport const loadLanguageConfigs = async <T extends NonNullable<unknown>>(\n configType: ConfigType,\n mapFn: (mod: unknown) => T,\n): Promise<T[]> => {\n const manifest = await loadManifest();\n const registry = LOADER_REGISTRIES[configType];\n\n // Determine which language codes to load\n const hasManifestLanguages = Object.keys(manifest.languages).length > 0;\n\n const codes = hasManifestLanguages\n ? Object.entries(manifest.languages)\n .filter(([code, lang]) => {\n if (!lang || typeof lang !== \"object\") {\n console.warn(\n `[anonymize] lang-loader: manifest ` +\n `entry for \"${code}\" is not an ` +\n `object, skipping`,\n );\n return false;\n }\n return lang[configType] === true;\n })\n .map(([code]) => code)\n : [...FALLBACK_LANGUAGES[configType]];\n\n // Use indexed assignment so results preserve\n // manifest declaration order regardless of import\n // resolution timing.\n const results: (T | undefined)[] = new Array(codes.length);\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","import { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\nimport type { DefinitionPattern, PipelineContext } from \"../context\";\nimport { corefKey, defaultContext } from \"../context\";\nimport { loadLanguageConfigs } from \"../util/lang-loader\";\n\ntype CoreferenceConfigRow = {\n pattern: string;\n flags: string;\n label: string;\n};\n\n/**\n * Load coreference definition patterns from per-language\n * JSON configs in @stll/anonymize-data. Uses the\n * language manifest for auto-discovery.\n */\nconst loadDefinitionPatterns = async (): Promise<DefinitionPattern[]> => {\n const patterns: DefinitionPattern[] = [];\n\n const allRows = await loadLanguageConfigs<readonly CoreferenceConfigRow[]>(\n \"coreference\",\n (mod) => {\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config\n const m = mod as {\n default?: readonly CoreferenceConfigRow[];\n };\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config\n return (m.default ?? mod) as readonly CoreferenceConfigRow[];\n },\n );\n\n for (const rows of allRows) {\n if (!Array.isArray(rows)) {\n console.warn(\n \"[anonymize] coreference: unexpected \" + \"config shape, skipping\",\n );\n continue;\n }\n for (const row of rows) {\n try {\n patterns.push({\n pattern: new RegExp(row.pattern, row.flags),\n });\n } catch (err) {\n console.warn(\n `[anonymize] coreference: invalid ` + `regex \"${row.pattern}\":`,\n err,\n );\n }\n }\n }\n\n return patterns;\n};\n\n/**\n * Load generic role terms that should NOT be treated\n * as PII coreferences. \"Prodávající\" (Seller),\n * \"Kupující\" (Buyer), etc. are legal roles, not\n * identifying information.\n */\nconst getRoleStopSet = async (\n ctx: PipelineContext,\n): Promise<ReadonlySet<string>> => {\n if (ctx.roleStopSet) return ctx.roleStopSet;\n if (ctx.roleStopSetPromise) return ctx.roleStopSetPromise;\n const promise = (async () => {\n let result: ReadonlySet<string>;\n try {\n const mod =\n await import(\"@stll/anonymize-data/config/generic-roles.json\");\n const data = (mod.default ?? mod) as {\n roles: string[];\n };\n result = new Set(data.roles.map((r: string) => r.toLowerCase()));\n } catch {\n result = new Set();\n }\n ctx.roleStopSet = result;\n return result;\n })();\n ctx.roleStopSetPromise = promise;\n return promise;\n};\n\nconst getDefinitionPatterns = async (\n ctx: PipelineContext,\n): Promise<DefinitionPattern[]> => {\n if (ctx.corefPatterns) {\n return ctx.corefPatterns;\n }\n if (ctx.corefPatternsPromise) {\n return ctx.corefPatternsPromise;\n }\n ctx.corefPatternsPromise = loadDefinitionPatterns();\n const patterns = await ctx.corefPatternsPromise;\n if (patterns.length === 0) {\n // All loads failed; cache empty array permanently\n // to avoid retrying dynamic imports and flooding\n // logs on every call in high-volume pipelines.\n ctx.corefPatterns = patterns;\n if (!ctx.corefLoadAttempted) {\n ctx.corefLoadAttempted = true;\n console.warn(\n \"[anonymize] coreference: no definition \" +\n \"patterns loaded; coreference detection \" +\n \"will be inactive\",\n );\n }\n return patterns;\n }\n ctx.corefPatterns = patterns;\n return patterns;\n};\n\nconst SEARCH_WINDOW = 200;\n\n/**\n * Check whether an alias has textual similarity to\n * the source entity. Prevents roles and structural\n * terms from being treated as name aliases.\n *\n * Three checks (any passes → similar):\n * 1. Word overlap: a word in the alias appears in the\n * entity (case-insensitive, min 2 chars)\n * 2. Initials: alias letters match first letters of\n * entity words (\"TB\" ↔ \"Tomas Bata\")\n * 3. Substring: alias is a substring of the entity\n * or vice versa (min 3 chars)\n */\nconst hasEntitySimilarity = (alias: string, entityText: string): boolean => {\n const aliasLower = alias.toLowerCase();\n const entityLower = entityText.toLowerCase();\n\n // Substring check (min 3 chars to avoid noise)\n if (aliasLower.length >= 3 && entityLower.includes(aliasLower)) {\n return true;\n }\n if (entityLower.length >= 3 && aliasLower.includes(entityLower)) {\n return true;\n }\n\n // Word overlap: split both into words, check for\n // any shared word of 2+ characters\n const aliasWords = aliasLower\n .split(/[\\s.,;:'\"()/-]+/)\n .filter((w) => w.length >= 2);\n const entityWords = entityLower\n .split(/[\\s.,;:'\"()/-]+/)\n .filter((w) => w.length >= 2);\n const entityWordSet = new Set(entityWords);\n for (const word of aliasWords) {\n if (entityWordSet.has(word)) {\n return true;\n }\n }\n\n // Initials: alias is all uppercase and each letter\n // matches the first letter of a consecutive run of\n // entity words. \"TP\" ↔ \"Ing. Tomáš Procházka\"\n // (skips \"Ing.\" and matches T+P). Check all\n // starting positions to handle title prefixes.\n if (\n /^[\\p{Lu}]+$/u.test(alias) &&\n alias.length >= 2 &&\n alias.length <= entityWords.length\n ) {\n for (let start = 0; start <= entityWords.length - alias.length; start++) {\n const initials = entityWords\n .slice(start, start + alias.length)\n .map((w) => w.charAt(0))\n .join(\"\");\n if (initials === aliasLower) {\n return true;\n }\n }\n }\n\n return false;\n};\n\ntype DefinedTerm = {\n alias: string;\n label: string;\n /** Position of the definition in the source text */\n definitionStart: number;\n /** Original entity text the alias refers to */\n sourceText: string;\n};\n\n/**\n * Scan for defined-term patterns near known entities.\n *\n * Legal documents universally follow:\n * \"Dr. Heinrich Muller (hereinafter 'the Seller')...\"\n *\n * After NER detects the entity, this function scans for\n * definitional patterns within +/-200 chars and extracts\n * the alias. Returns alias + label pairs that can be added\n * to the gazetteer for a full-text re-scan.\n */\n/**\n * Labels that can be the source of a coreference alias.\n * Only parties (person, organization) have defined-term\n * aliases in legal text. Dates, addresses, IDs do not.\n */\nconst COREF_SOURCE_LABELS = new Set([\"person\", \"organization\"]);\n\nexport const extractDefinedTerms = async (\n fullText: string,\n entities: Entity[],\n ctx: PipelineContext = defaultContext,\n): Promise<DefinedTerm[]> => {\n const [definitionPatterns, roleStops] = await Promise.all([\n getDefinitionPatterns(ctx),\n getRoleStopSet(ctx),\n ]);\n const terms: DefinedTerm[] = [];\n const seen = new Set<string>();\n\n // Sort entities by position for nearest-preceding lookup\n const sorted = [...entities].sort((a, b) => a.start - b.start);\n\n // Strategy: find all \"dále jen\" definitions in the\n // text, then for each one, find the nearest PRECEDING\n // person/organization entity. This is more accurate\n // than \"any entity within 200 chars\" because:\n // - The alias always refers to a party defined BEFORE\n // the parenthetical, not after\n // - Multiple entities (name, IČO, address) may appear\n // between the party name and the \"dále jen\"; only\n // the party name is the referent\n\n for (const { pattern } of definitionPatterns) {\n pattern.lastIndex = 0;\n\n for (\n let match = pattern.exec(fullText);\n match !== null;\n match = pattern.exec(fullText)\n ) {\n const alias = match[1]?.trim();\n if (!alias || alias.length < 2) {\n continue;\n }\n\n // Skip generic legal roles — \"Prodávající\",\n // \"Kupující\", \"Seller\", etc. are NOT PII.\n // Only track aliases that are themselves\n // identifying (initials, name fragments, etc.)\n if (roleStops.has(alias.toLowerCase())) {\n continue;\n }\n\n const defPos = match.index;\n\n // Find the nearest preceding person/org entity\n // within SEARCH_WINDOW chars before this definition\n let bestEntity: Entity | null = null;\n for (let i = sorted.length - 1; i >= 0; i--) {\n const e = sorted[i];\n if (e === undefined) {\n continue;\n }\n // Must be before the definition\n if (e.end > defPos) {\n continue;\n }\n // Must be within the search window\n if (defPos - e.end > SEARCH_WINDOW) {\n break;\n }\n // Must be a party label\n if (!COREF_SOURCE_LABELS.has(e.label)) {\n continue;\n }\n bestEntity = e;\n break;\n }\n\n if (bestEntity === null) {\n continue;\n }\n\n // Clause-boundary gate: reject if a semicolon or\n // sentence-ending period sits between the source\n // entity and the definition. Periods inside\n // abbreviations like \"r.č.\" should not block an\n // otherwise valid definition.\n const gapText = fullText.slice(bestEntity.end, defPos);\n if (/(?:;|\\.(?=\\s*(?:[\"'„‚(]*\\p{Lu}|$)))/u.test(gapText)) {\n continue;\n }\n\n // Similarity gate: the alias must have textual\n // overlap with the source entity. Prevents roles\n // (\"Executive\", \"Seller\") and structural terms\n // (\"Agreement\", \"Effective Date\") from being\n // treated as name aliases.\n if (!hasEntitySimilarity(alias, bestEntity.text)) {\n continue;\n }\n\n const key = `${alias.toLowerCase()}::${bestEntity.label}`;\n if (seen.has(key)) {\n continue;\n }\n seen.add(key);\n\n terms.push({\n alias,\n label: bestEntity.label,\n definitionStart: defPos,\n sourceText: bestEntity.text,\n });\n }\n }\n\n return terms;\n};\n\n/**\n * Check if a character is a Unicode word character\n * (letter, digit, or combining mark). Used for word\n * boundary checks in coreference matching.\n */\nconst isWordChar = (ch: string | undefined): boolean => {\n if (ch === undefined) {\n return false;\n }\n return /[\\p{L}\\p{M}\\p{N}]/u.test(ch);\n};\n\n/**\n * Find all occurrences of defined-term aliases in the\n * full text. Returns Entity spans for each match.\n *\n * Respects word boundaries: \"Kupující\" must not match\n * inside \"Kupujícímu\". A match is valid only if the\n * character before the start and after the end are NOT\n * word characters (letter/digit).\n *\n * Populates `ctx.corefSourceMap` with entries linking\n * each coref entity to its source entity text, for\n * consistent placeholder numbering.\n */\nexport const findCoreferenceSpans = (\n fullText: string,\n terms: DefinedTerm[],\n ctx: PipelineContext = defaultContext,\n): Entity[] => {\n const results: Entity[] = [];\n\n for (const term of terms) {\n let searchFrom = 0;\n while (searchFrom < fullText.length) {\n const idx = fullText.indexOf(term.alias, searchFrom);\n if (idx === -1) {\n break;\n }\n\n const matchEnd = idx + term.alias.length;\n\n // Word boundary check: the character before the\n // match and after the match must not be a word\n // character. This prevents \"Kupující\" matching\n // inside \"Kupujícímu\".\n const charBefore = idx > 0 ? fullText[idx - 1] : undefined;\n const charAfter = fullText[matchEnd];\n\n if (isWordChar(charBefore) || isWordChar(charAfter)) {\n // Not at a word boundary — skip\n searchFrom = idx + 1;\n continue;\n }\n\n const entity: Entity = {\n start: idx,\n end: matchEnd,\n label: term.label,\n text: term.alias,\n score: 0.95,\n source: DETECTION_SOURCES.COREFERENCE,\n };\n ctx.corefSourceMap.set(corefKey(entity), term.sourceText);\n results.push(entity);\n\n searchFrom = matchEnd;\n }\n }\n\n return results;\n};\n","import type { Match, PatternEntry } from \"@stll/text-search\";\n\nimport { DETECTION_SOURCES } from \"../types\";\nimport type { Entity, GazetteerEntry } from \"../types\";\nimport type { GazetteerData } from \"../build-unified-search\";\n\nconst MAX_EDIT_DISTANCE = 2;\nconst MIN_FUZZY_LENGTH = 4;\n// \" s.r.o.\" = 7 bytes (space + 6 chars). Must be\n// at least 7 to capture the most common Czech LLC\n// suffix without truncating the trailing dot.\nconst MAX_PREFIX_OVERSHOOT = 7;\n\n/**\n * Collect all searchable strings (canonical + variants)\n * from gazetteer entries, mapped to their labels and\n * entry IDs.\n */\nexport const buildSearchTerms = (\n entries: GazetteerEntry[],\n): Map<string, { label: string; entryId: string }> => {\n const terms = new Map<string, { label: string; entryId: string }>();\n for (const entry of entries) {\n const meta = {\n label: entry.label,\n entryId: entry.id,\n };\n terms.set(entry.canonical, meta);\n for (const variant of entry.variants) {\n terms.set(variant, meta);\n }\n }\n return terms;\n};\n\n/**\n * Build TextSearch-compatible patterns from gazetteer\n * entries. Returns:\n * - Exact literal patterns for all terms\n * - Fuzzy patterns (distance: 2) for terms >= 4 chars\n * - Parallel metadata arrays for post-processing\n *\n * Patterns are ordered: all exact first, then all\n * fuzzy. The isFuzzy array marks which are which.\n */\nexport const buildGazetteerPatterns = (\n entries: GazetteerEntry[],\n): {\n patterns: PatternEntry[];\n data: GazetteerData;\n} => {\n const terms = buildSearchTerms(entries);\n\n const patterns: PatternEntry[] = [];\n const labels: string[] = [];\n const isFuzzy: boolean[] = [];\n\n // Pass 1: exact literals (all terms).\n // Use per-pattern wholeWords: false because\n // user-supplied entries may contain dots or\n // special chars (e.g. \"a.s.\", \"AT&T\") that\n // break word-boundary matching. Deny-list and\n // street-type patterns use per-pattern\n // wholeWords: true instead.\n for (const [term, meta] of terms) {\n patterns.push({\n pattern: term,\n literal: true as const,\n wholeWords: false,\n });\n labels.push(meta.label);\n\n isFuzzy.push(false);\n }\n\n // Pass 2: fuzzy patterns (terms >= 4 chars).\n // These are separate patterns with distance: 2,\n // routed to @stll/fuzzy-search by TextSearch.\n for (const [term, meta] of terms) {\n if (term.length < MIN_FUZZY_LENGTH) {\n continue;\n }\n patterns.push({\n pattern: term,\n distance: MAX_EDIT_DISTANCE,\n });\n labels.push(meta.label);\n\n isFuzzy.push(true);\n }\n\n return {\n patterns,\n data: { labels, isFuzzy },\n };\n};\n\n/**\n * Process gazetteer matches from the unified literal\n * search. Receives all matches; filters to the\n * gazetteer slice via sliceStart/sliceEnd.\n *\n * Exact matches get score 0.9; fuzzy matches get\n * 0.85. Fuzzy matches that overlap an exact match\n * are dropped.\n *\n * For exact matches, attempts prefix extension for\n * legal suffixes (\"a.s.\", \"GmbH\", \"s.r.o.\" after\n * the matched term).\n */\nexport const processGazetteerMatches = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText: string,\n data: GazetteerData,\n): Entity[] => {\n const results: Entity[] = [];\n // Track exact-match spans for overlap filtering\n const exactSpans: Array<{\n start: number;\n end: number;\n }> = [];\n\n // Pass 1: exact matches (isFuzzy === false)\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) {\n continue;\n }\n const localIdx = idx - sliceStart;\n if (data.isFuzzy[localIdx]) {\n continue;\n }\n\n const label = data.labels[localIdx];\n if (!label) {\n continue;\n }\n\n // Try prefix extension for legal entity suffixes\n const extended = tryPrefixExtension(fullText, match.start, match.end);\n const 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 results.push({\n start: match.start,\n end,\n label,\n text,\n score: 0.9,\n source: DETECTION_SOURCES.GAZETTEER,\n });\n }\n\n // Pass 2: fuzzy matches (isFuzzy === true),\n // skipping those that overlap exact spans\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) {\n continue;\n }\n const localIdx = idx - sliceStart;\n if (!data.isFuzzy[localIdx]) {\n continue;\n }\n\n // Skip distance-0 fuzzy hits (already caught\n // as exact matches above)\n if (match.distance === 0) {\n continue;\n }\n\n const label = data.labels[localIdx];\n if (!label) {\n continue;\n }\n\n // Skip if overlapping any exact span\n const overlapsExact = exactSpans.some(\n (e) => match.start < e.end && match.end > e.start,\n );\n if (overlapsExact) {\n continue;\n }\n\n const matchText = fullText.slice(match.start, match.end);\n results.push({\n start: match.start,\n end: match.end,\n label,\n text: matchText,\n score: 0.85,\n source: DETECTION_SOURCES.GAZETTEER,\n });\n }\n\n return results;\n};\n\n/**\n * Try to extend an exact match to capture one\n * trailing token (max 6 chars) that may be a legal\n * entity suffix (e.g., \"a.s.\", \"GmbH\", \"s.r.o.\").\n *\n * Does not validate the token against a legal-forms\n * list; false extensions are filtered by mergeAndDedup\n * when a legal-form detector produces a competing\n * entity with the correct span.\n */\nconst tryPrefixExtension = (\n fullText: string,\n start: number,\n end: number,\n): { end: number; text: string } | null => {\n const maxEnd = Math.min(end + MAX_PREFIX_OVERSHOOT, fullText.length);\n if (maxEnd <= end + 1) {\n return null;\n }\n\n const after = fullText.slice(end, maxEnd);\n if (!after.startsWith(\" \")) {\n return null;\n }\n const nextSpace = after.indexOf(\" \", 1);\n const suffixEnd = nextSpace !== -1 ? nextSpace : after.length;\n if (suffixEnd <= 1) {\n return null;\n }\n\n const newEnd = end + suffixEnd;\n return {\n end: newEnd,\n text: fullText.slice(start, newEnd),\n };\n};\n\n// Deprecated exports (kept for API compat).\n","/**\n * Shared text utilities for detectors.\n *\n * Extracted from names.ts and deny-list.ts to avoid\n * duplicating regex constants and helper functions.\n */\n\n/** Matches a string that starts with an uppercase letter. */\nexport const UPPER_START_RE = /^\\p{Lu}/u;\n\n/** Matches a string consisting entirely of uppercase letters. */\nexport const ALL_UPPER_RE = /^\\p{Lu}+$/u;\n\nconst SENTENCE_END_RE = /[.!?]/;\n\n/**\n * Detect whether a position is at the start of a sentence.\n * Looks backward past whitespace for sentence-ending\n * punctuation (.!?). Position 0 and positions preceded\n * only by whitespace are considered sentence starts.\n */\nexport const isSentenceStart = (text: string, pos: number): boolean => {\n if (pos === 0) {\n return true;\n }\n let i = pos - 1;\n while (i >= 0 && /\\s/.test(text[i] ?? \"\")) {\n i--;\n }\n if (i < 0) {\n return true;\n }\n return SENTENCE_END_RE.test(text[i] ?? \"\");\n};\n","import { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\nimport type { PipelineContext, NameCorpusData } from \"../context\";\nimport { defaultContext } from \"../context\";\nimport { ALL_UPPER_RE, UPPER_START_RE, isSentenceStart } from \"../util/text\";\n\n// ── Name corpus ──────────────────────────────────────\n// Per-language first names and surnames loaded from\n// Wikidata-sourced dictionaries in @stll/anonymize-data,\n// plus legacy config/names-*.json for backwards compat.\n// Merged at init time across all configured languages.\n\n// ── Accessors (read from context) ────────────────────\n\nconst getCorpus = (ctx: PipelineContext): NameCorpusData | null =>\n ctx.nameCorpus;\n\n// Exported accessors for deny-list.ts AC integration.\nexport const getNameCorpusFirstNames = (\n ctx: PipelineContext = defaultContext,\n): readonly string[] => ctx.nameCorpus?.firstNamesList ?? [];\nexport const getNameCorpusSurnames = (\n ctx: PipelineContext = defaultContext,\n): readonly string[] => ctx.nameCorpus?.surnamesList ?? [];\nexport const getNameCorpusTitles = (\n ctx: PipelineContext = defaultContext,\n): readonly string[] => ctx.nameCorpus?.titlesList ?? [];\nexport const getNameCorpusExcluded = (\n ctx: PipelineContext = defaultContext,\n): readonly string[] => ctx.nameCorpus?.excludedList ?? [];\n\n/**\n * Languages with per-language first/surname\n * dictionaries in @stll/anonymize-data.\n */\nconst NAME_LANGUAGES = [\n \"cs\",\n \"sk\",\n \"de\",\n \"pl\",\n \"hu\",\n \"ro\",\n \"fr\",\n \"es\",\n \"it\",\n \"en\",\n \"sv\",\n] as const;\n\ntype JsonArrayModule = {\n default: readonly string[];\n};\n\n/**\n * Try importing a JSON module; return empty array\n * if not found.\n */\nconst tryImportArray = async (path: string): Promise<readonly string[]> => {\n try {\n const mod = (await import(path)) as JsonArrayModule;\n return mod.default;\n } catch {\n return [];\n }\n};\n\n/**\n * Load name corpus data from per-language dictionary\n * files 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 */\nexport const initNameCorpus = (\n ctx: PipelineContext = defaultContext,\n): Promise<void> => {\n if (ctx.nameCorpusPromise) return ctx.nameCorpusPromise;\n const promise = (async () => {\n try {\n // Load legacy config files (backwards compat)\n const [legacyFirstMod, legacySurnameMod, titleMod, exclusionMod] =\n await Promise.all([\n import(\"@stll/anonymize-data/config/names-first.json\") as Promise<{\n default: { names: string[] };\n }>,\n import(\"@stll/anonymize-data/config/names-surnames.json\") as Promise<{\n default: { names: string[] };\n }>,\n import(\"@stll/anonymize-data/config/names-title-tokens.json\") as Promise<{\n default: { tokens: string[] };\n }>,\n import(\"@stll/anonymize-data/config/names-exclusions.json\") as Promise<{\n default: { words: string[] };\n }>,\n ]);\n\n // Load per-language dictionaries in parallel\n const firstImports = NAME_LANGUAGES.map((lang) =>\n tryImportArray(\n `@stll/anonymize-data/dictionaries/names/first/${lang}.json`,\n ),\n );\n const surnameImports = NAME_LANGUAGES.map((lang) =>\n tryImportArray(\n `@stll/anonymize-data/dictionaries/names/surnames/${lang}.json`,\n ),\n );\n\n const [firstResults, surnameResults] = await Promise.all([\n Promise.all(firstImports),\n Promise.all(surnameImports),\n ]);\n\n // Merge: legacy config + all per-language files\n const firstNames: string[] = [...legacyFirstMod.default.names];\n for (const names of firstResults) {\n for (const name of names) {\n firstNames.push(name);\n }\n }\n\n const surnames: string[] = [...legacySurnameMod.default.names];\n for (const names of surnameResults) {\n for (const name of names) {\n surnames.push(name);\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 dedupFirst = dedup(firstNames);\n const dedupSurnames = dedup(surnames);\n const titles = titleMod.default.tokens;\n const exclusions = exclusionMod.default.words;\n\n ctx.nameCorpus = {\n firstNames: Object.freeze(new Set(dedupFirst)),\n surnames: Object.freeze(new Set(dedupSurnames)),\n titleTokens: Object.freeze(new Set(titles)),\n excludedWords: Object.freeze(new Set(exclusions)),\n firstNamesList: Object.freeze(dedupFirst),\n surnamesList: Object.freeze(dedupSurnames),\n titlesList: Object.freeze(titles),\n excludedList: Object.freeze(exclusions),\n };\n } catch (err) {\n // Reset so the next call retries the load rather\n // than returning this (already-resolved) failed\n // promise. Current awaiters still get a resolved\n // (not rejected) Promise; ctx.nameCorpus stays null.\n ctx.nameCorpusPromise = null;\n console.warn(\n \"[anonymize] Failed to load name corpus JSON\" +\n \" — name detection disabled:\",\n err,\n );\n }\n })();\n ctx.nameCorpusPromise = promise;\n return promise;\n};\n\n// ── Czech/Slovak suffix stripping ────────────────────\n// Case suffixes commonly appended to names in declined\n// Czech/Slovak text. Ordered longest-first.\n\nconst INFLECTION_SUFFIXES = [\n \"ovi\", // dative\n \"em\", // instrumental\n \"om\", // instrumental (some stems)\n \"ou\", // instrumental feminine\n \"é\", // dative/locative feminine\n \"a\", // genitive\n \"u\", // accusative/locative\n] as const;\n\n/**\n * Strip common Czech/Slovak case suffixes from a token.\n * Returns candidate base forms if stripping produces a\n * plausible name (capitalised, length >= 3).\n *\n * For the \"-ou\" instrumental feminine suffix, also yields\n * base + \"a\" (e.g., \"Editou\" → \"Edit\" and \"Edita\")\n * because Czech feminine names decline -a → -ou.\n */\nconst stripInflection = (token: string): string[] => {\n const candidates: string[] = [];\n for (const suffix of INFLECTION_SUFFIXES) {\n if (token.length > suffix.length + 2 && token.endsWith(suffix)) {\n const base = token.slice(0, -suffix.length);\n if (/^\\p{Lu}/u.test(base)) {\n candidates.push(base);\n // Czech feminine: -a → -ou (instrumental),\n // -a → -é (dative/locative), -a → -u (accusative)\n if (suffix === \"ou\" || suffix === \"é\" || suffix === \"u\") {\n candidates.push(`${base}a`);\n }\n // Czech feminine: -a → -ovi is not valid, but\n // -e → -em is (e.g., \"Kalhousem\" → \"Kalhous\")\n // which is already handled by stripping \"em\".\n }\n }\n }\n return candidates;\n};\n\n// ── Token types ──────────────────────────────────────\n\nconst TOKEN_TYPE = {\n NAME: \"name\",\n SURNAME: \"surname\",\n TITLE: \"title\",\n ABBREVIATION: \"abbreviation\",\n CAPITALIZED: \"capitalized\",\n OTHER: \"other\",\n} as const;\n\ntype TokenType = (typeof TOKEN_TYPE)[keyof typeof TOKEN_TYPE];\n\ntype ClassifiedToken = {\n text: string;\n type: TokenType;\n start: number;\n end: number;\n};\n\nconst PERSON_CHAIN_BREAK_RE = /[!?;:]/u;\n\nconst isInitialContinuationGap = (text: string, gap: string): boolean =>\n (/^\\p{Lu}$/u.test(text) && /^\\.[^\\S\\n]{1,2}$/u.test(gap)) ||\n /^[^\\S\\n]{1,2}(?:\\p{Lu}\\.[^\\S\\n]{1,2})+$/u.test(gap);\n\n// ── Helpers ──────────────────────────────────────────\n\n/**\n * Check if a token is in the first-name set, either\n * directly or after stripping Czech/Slovak inflection.\n */\nconst isFirstNameToken = (token: string, corpus: NameCorpusData): boolean => {\n if (corpus.firstNames.has(token)) {\n return true;\n }\n return stripInflection(token).some((b) => corpus.firstNames.has(b));\n};\n\n/**\n * Check if a token is in the surname set, either\n * directly or after stripping Czech/Slovak inflection.\n */\nconst isSurnameToken = (token: string, corpus: NameCorpusData): boolean => {\n if (corpus.surnames.has(token)) {\n return true;\n }\n return stripInflection(token).some((b) => corpus.surnames.has(b));\n};\n\n/**\n * Check if a token looks like a single-letter\n * abbreviation: \"J.\", \"M.\", etc.\n */\nconst isAbbreviation = (token: string): boolean =>\n token.length === 2 && /^\\p{Lu}$/u.test(token[0] ?? \"\") && token[1] === \".\";\n\n// ── Word segmentation ────────────────────────────────\n\nconst segmenter = new Intl.Segmenter(undefined, {\n granularity: \"word\",\n});\n\ntype WordSegment = {\n text: string;\n start: number;\n end: number;\n};\n\n/**\n * Split text into word segments using Intl.Segmenter.\n * Only returns segments flagged as words.\n */\nconst segmentWords = (fullText: string): WordSegment[] => {\n const words: WordSegment[] = [];\n for (const seg of segmenter.segment(fullText)) {\n if (seg.isWordLike) {\n words.push({\n text: seg.segment,\n start: seg.index,\n end: seg.index + seg.segment.length,\n });\n }\n }\n return words;\n};\n\n// ── Helpers for chain scoring ────────────────────────\n\n/** NAME or SURNAME — both represent corpus-matched tokens */\nconst isCorpusMatch = (type: TokenType): boolean =>\n type === TOKEN_TYPE.NAME || type === TOKEN_TYPE.SURNAME;\n\n// ── Token classification ─────────────────────────────\n\nconst classifyToken = (\n word: WordSegment,\n corpus: NameCorpusData,\n): ClassifiedToken => {\n const { text, start, end } = word;\n const lower = text.toLowerCase();\n\n // Strip trailing period for title check (e.g., \"Ing.\")\n const stripped = text.endsWith(\".\") ? text.slice(0, -1).toLowerCase() : lower;\n\n if (corpus.titleTokens.has(stripped)) {\n return { text, type: TOKEN_TYPE.TITLE, start, end };\n }\n\n if (isAbbreviation(text)) {\n return {\n text,\n type: TOKEN_TYPE.ABBREVIATION,\n start,\n end,\n };\n }\n\n // Skip excluded words\n if (corpus.excludedWords.has(lower)) {\n return { text, type: TOKEN_TYPE.OTHER, start, end };\n }\n\n // Minimum length 3\n if (text.length < 3) {\n return { text, type: TOKEN_TYPE.OTHER, start, end };\n }\n\n // Skip all-uppercase tokens > 3 chars (likely acronyms)\n if (text.length > 3 && ALL_UPPER_RE.test(text)) {\n return { text, type: TOKEN_TYPE.OTHER, start, end };\n }\n\n // Must start with uppercase\n if (!UPPER_START_RE.test(text)) {\n return { text, type: TOKEN_TYPE.OTHER, start, end };\n }\n\n if (isFirstNameToken(text, corpus)) {\n return { text, type: TOKEN_TYPE.NAME, start, end };\n }\n\n if (isSurnameToken(text, corpus)) {\n return { text, type: TOKEN_TYPE.SURNAME, start, end };\n }\n\n // Capitalised word (not in corpus but starts uppercase)\n return {\n text,\n type: TOKEN_TYPE.CAPITALIZED,\n start,\n end,\n };\n};\n\n// ── Chain assembly ───────────────────────────────────\n\n/**\n * Detect person names by looking up tokens against the\n * name corpus, then chaining adjacent name-like tokens.\n *\n * Requires initNameCorpus() to have been called first.\n * If not initialized, returns an empty array.\n *\n * Scoring:\n * TITLE + NAME/SURNAME → 0.95\n * NAME + NAME/SURNAME → 0.9\n * SURNAME + NAME/SURNAME → 0.9\n * NAME + CAPITALIZED → 0.7\n * ABBREVIATION + NAME → 0.7\n * Standalone NAME → 0.5 (low confidence)\n * Standalone SURNAME → skip (too ambiguous)\n */\nexport const detectNameCorpus = (\n fullText: string,\n ctx: PipelineContext = defaultContext,\n): Entity[] => {\n const corpus = getCorpus(ctx);\n if (!corpus) {\n return [];\n }\n\n const words = segmentWords(fullText);\n const tokens = words.map((w) => classifyToken(w, corpus));\n const entities: Entity[] = [];\n const consumed = new Set<number>();\n\n for (let i = 0; i < tokens.length; i++) {\n if (consumed.has(i)) {\n continue;\n }\n\n const token = tokens[i];\n if (!token) {\n continue;\n }\n\n // Only start chains from TITLE, NAME, SURNAME,\n // or ABBREVIATION\n if (\n token.type !== TOKEN_TYPE.TITLE &&\n token.type !== TOKEN_TYPE.NAME &&\n token.type !== TOKEN_TYPE.SURNAME &&\n token.type !== TOKEN_TYPE.ABBREVIATION\n ) {\n continue;\n }\n\n // Build a chain of adjacent relevant tokens.\n // Max 5 tokens to prevent merging independent names\n // (e.g., \"Jan Novák Pavel Moc\" should be two entities).\n const MAX_CHAIN = 5;\n const chain: ClassifiedToken[] = [token];\n let j = i + 1;\n\n while (j < tokens.length && chain.length < MAX_CHAIN) {\n const next = tokens[j];\n if (!next) {\n break;\n }\n\n // Break chain if there's a newline between tokens\n const prev = chain.at(-1);\n if (prev) {\n const gap = fullText.slice(prev.end, next.start);\n const breaksOnPeriod =\n gap.includes(\".\") && !isInitialContinuationGap(prev.text, gap);\n if (\n gap.includes(\"\\n\") ||\n PERSON_CHAIN_BREAK_RE.test(gap) ||\n breaksOnPeriod\n ) {\n break;\n }\n }\n\n // Only chain NAME, SURNAME, TITLE, ABBREVIATION,\n // CAPITALIZED\n if (\n next.type === TOKEN_TYPE.NAME ||\n next.type === TOKEN_TYPE.SURNAME ||\n next.type === TOKEN_TYPE.TITLE ||\n next.type === TOKEN_TYPE.ABBREVIATION ||\n next.type === TOKEN_TYPE.CAPITALIZED\n ) {\n chain.push(next);\n j++;\n } else {\n break;\n }\n }\n\n // Score the chain\n const hasTitle = chain.some((t) => t.type === TOKEN_TYPE.TITLE);\n const hasCorpusName = chain.some((t) => isCorpusMatch(t.type));\n const hasFirstName = chain.some((t) => t.type === TOKEN_TYPE.NAME);\n const hasAbbreviation = chain.some(\n (t) => t.type === TOKEN_TYPE.ABBREVIATION,\n );\n const corpusCount = chain.filter((t) => isCorpusMatch(t.type)).length;\n const capitalizedCount = chain.filter(\n (t) => t.type === TOKEN_TYPE.CAPITALIZED,\n ).length;\n\n // Determine score based on chain composition\n let score = 0;\n\n if (hasTitle && hasCorpusName) {\n // TITLE + NAME/SURNAME → high confidence\n score = 0.95;\n } else if (corpusCount >= 2) {\n // NAME + NAME, NAME + SURNAME, etc. → high confidence\n score = 0.9;\n } else if (hasCorpusName && capitalizedCount > 0) {\n // NAME/SURNAME + CAPITALIZED → medium confidence\n score = 0.7;\n } else if (hasAbbreviation && hasCorpusName) {\n // ABBREVIATION + NAME/SURNAME → medium confidence\n score = 0.7;\n } else if (hasFirstName && chain.length === 1) {\n // Standalone first NAME → low confidence\n // Skip if at sentence start (likely not a name)\n if (isSentenceStart(fullText, token.start)) {\n continue;\n }\n score = 0.5;\n } else if (\n !hasFirstName &&\n chain.length === 1 &&\n chain[0]?.type === TOKEN_TYPE.SURNAME\n ) {\n // Standalone SURNAME → skip (too ambiguous alone)\n continue;\n } else if (hasTitle && chain.length === 1) {\n // Standalone TITLE → skip (not a name by itself)\n continue;\n } else {\n // No corpus match in chain → skip\n if (!hasCorpusName) {\n continue;\n }\n score = 0.5;\n }\n\n // Build entity span from first to last token in chain\n const first = chain.at(0);\n const last = chain.at(-1);\n if (!first || !last) {\n continue;\n }\n\n const start = first.start;\n const end = last.end;\n const text = fullText.slice(start, end);\n\n // Mark all chain tokens as consumed\n for (let k = i; k < i + chain.length; k++) {\n consumed.add(k);\n }\n\n entities.push({\n start,\n end,\n label: \"person\",\n text,\n score,\n source: DETECTION_SOURCES.REGEX,\n });\n }\n\n return entities;\n};\n","/**\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 \"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 \"Señor\",\n \"Señora\",\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] as const;\n","/**\n * Centralized Unicode character equivalence groups.\n *\n * Loads char-groups.json from @stll/anonymize-data and\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 \"@stll/anonymize-data/config/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\n// with @stll/anonymize-data.\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/** Regex char class matching all single-quote variants. */\nexport const QUOTE_SINGLE = charClass(\"quote-single\");\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 } from \"@stll/text-search\";\nimport type { Validator } from \"@stll/stdnum\";\nimport {\n at,\n be,\n bg,\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 pl,\n pt,\n ro,\n se,\n si,\n sk,\n} from \"@stll/stdnum\";\nimport { toRegex } from \"@stll/stdnum/patterns\";\n\nimport {\n HONORIFIC_BOUNDARY,\n HONORIFICS,\n POST_NOMINALS,\n TITLE_PREFIXES,\n} from \"../config/titles\";\nimport { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\nimport { DASH, DASH_INNER } from \"../util/char-groups\";\n\nconst MIN_PHONE_LENGTH = 7;\nconst MIN_MONTH_NAME_LENGTH = 3;\n\n// ── Shared helpers ──────────────────────────────────\n\nconst escapeTitle = (title: string): string =>\n title\n // eslint-disable-next-line no-useless-escape\n .replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\s+/g, \"\\\\s*\");\n\n/** Escape for use inside a regex alternation. */\nconst escapeRegex = (s: string): string =>\n // eslint-disable-next-line no-useless-escape\n s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\n/** Escape for use inside a regex character class. */\nconst escapeCharClass = (s: string): string => s.replace(/[\\]\\\\^-]/g, \"\\\\$&\");\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// biome-ignore lint/security/noSecrets: diacritics char class\nconst UPPER_CZ = \"A-ZÁČĎÉĚÍŇÓŘŠŤÚŮÝŽ\";\n// biome-ignore lint/security/noSecrets: diacritics char class\nconst LOWER_CZ = \"a-záčďéěíňóřšťúůýžäöüß\";\nconst NAME_WORD = `[${UPPER_CZ}][${LOWER_CZ}]+`;\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\nconst SP = \"[^\\\\S\\\\n\\\\t]\";\n\n/** Honorific alternation built from titles.ts config. */\nconst HONORIFIC_ALT = [...HONORIFICS]\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// ── Pattern definitions ─────────────────────────────\n\nexport type RegexMeta = {\n label: string;\n score: number;\n /** Post-match stdnum validator for confirmation. */\n validator?: Validator;\n};\n\ntype RegexDef = {\n pattern: string;\n label: string;\n score: number;\n validator?: Validator;\n};\n\n// ── stdnum validator entries ────────────────────────\n// Each entry pairs a @stll/stdnum validator with a\n// label and confidence score. The pattern derived via\n// toRegex(validator).source is used as the regex; the\n// validator itself is stored in META for post-match\n// confirmation (see processRegexMatches).\n\ntype StdnumEntry = {\n validator: Validator;\n label: string;\n score: number;\n pattern: string;\n};\n\nconst toEntry = (\n validator: Validator,\n label: string,\n score: number,\n): StdnumEntry | null => {\n const pattern = toRegex(validator).source;\n if (!pattern) return null;\n return {\n validator,\n label,\n score,\n pattern,\n };\n};\n\n/**\n * Stdnum validators for national/company IDs.\n *\n * Selection criteria: only patterns specific enough\n * to avoid excessive false positives (country-prefixed\n * VAT numbers, structured personal IDs). Generic\n * digit-only patterns (e.g. \\d{8}) are excluded unless\n * the validator's checksum is strong enough to filter.\n */\nconst STDNUM_ENTRIES: readonly StdnumEntry[] = [\n // ── Original PR #28 patterns (were 15-21) ────────\n toEntry(hu.vat, \"tax identification number\", 0.95),\n toEntry(it.codiceFiscale, \"national identification number\", 0.95),\n toEntry(es.dni, \"national identification number\", 0.9),\n toEntry(es.nie, \"national identification number\", 0.95),\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 // ── 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 // ── 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 toEntry(es.cif, \"registration number\", 0.95),\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].filter((e): e is StdnumEntry => e !== null);\n\n// ── Named pattern definitions ────────────────────────\n\nconst TITLED_PERSON: RegexDef = {\n pattern:\n `(?:${TITLE_PREFIX})` +\n `(?:${SP}+(?:${TITLE_PREFIX}))*` +\n `${SP}+` +\n `(?:${NAME_WORD})` +\n `(?:${SP}{1,4}(?:${PARTICLE}${SP}+)?` +\n `${NAME_WORD}){1,3}` +\n `(?:,?${SP}+(?:${POST_NOMINAL})(?:,?${SP}+(?:${POST_NOMINAL}))*)?`,\n label: \"person\",\n score: 0.95,\n};\n\nconst HONORIFIC_PERSON: RegexDef = {\n pattern:\n `(?:${HONORIFIC_ALT})` +\n `\\\\.?${SP}+${NAME_WORD}` +\n `(?:(?:${SP}|-){1,2}(?:${PARTICLE}${SP}+)?` +\n `${NAME_WORD}){0,3}` +\n `(?:${SP}+(?:QC|KC|SC|LJ|AG))?`,\n label: \"person\",\n score: 0.95,\n};\n\nconst IBAN: RegexDef = {\n pattern:\n `\\\\b[A-Z]{2}\\\\d{2}\\\\s?[\\\\dA-Z]{4}\\\\s?[\\\\dA-Z]{4}` +\n `\\\\s?[\\\\dA-Z]{4}\\\\s?[\\\\dA-Z]{4}` +\n `\\\\s?[\\\\dA-Z]{0,14}\\\\b`,\n label: \"iban\",\n score: 1,\n};\n\nconst EMAIL: RegexDef = {\n pattern: `\\\\b[\\\\w.+\\\\-]+@[\\\\w\\\\-]+(?:\\\\.[\\\\w\\\\-]+)+\\\\b`,\n label: \"email address\",\n score: 1,\n};\n\n// [^\\S\\n] instead of \\s: separators must not\n// match newlines (prevents cross-line bleeding).\nconst INTL_PHONE: RegexDef = {\n pattern:\n `\\\\+\\\\d{1,3}(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\(?\\\\d{2,4}\\\\)?` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{2,4}` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{0,4}\\\\b`,\n label: \"phone number\",\n score: 1,\n};\n\n// Czech phone numbers: mobiles start with 6/7,\n// landlines with 2-5. Restrict to [2-7] but require\n// the full 9-digit pattern to avoid matching monetary\n// amounts. The negative lookahead prevents bank\n// account patterns (digits/digits).\nconst CZ_PHONE: RegexDef = {\n pattern:\n `\\\\b[2-7]\\\\d{2}(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}` +\n `(?!(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d*/\\\\d)` +\n `(?![^\\\\S\\\\n]*(?:Kč|,-|korun|EUR|USD|€|\\\\$))\\\\b`,\n label: \"phone number\",\n score: 0.85,\n};\n\n/**\n * Phone numbers prefixed with \"tel.:\" or \"telefon:\".\n * Captures the number after the prefix, including\n * optional international code (+420).\n */\nconst TEL_PREFIX_PHONE: RegexDef = {\n pattern:\n `(?:\\\\b[Tt]el(?:efon)?\\\\.?\\\\s*:?\\\\s*)` +\n `(?:\\\\+?\\\\d{1,3}[^\\\\S\\\\n]?)?` +\n `\\\\d{3}(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}\\\\b`,\n label: \"phone number\",\n score: 0.95,\n};\n\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: \"czech birth number\",\n score: 1,\n validator: cz.rc,\n};\n\nconst DATE_NUMERIC: RegexDef = {\n pattern:\n `\\\\b(?:\\\\d{1,2}[./]\\\\d{1,2}[./]\\\\d{2,4}` +\n `|\\\\d{4}-\\\\d{2}-\\\\d{2}` +\n `|\\\\d{4}\\\\.\\\\d{2}\\\\.\\\\d{2})\\\\b`,\n label: \"date\",\n score: 1,\n};\n\nconst DATE_CZ_SPACED: RegexDef = {\n pattern: `\\\\b\\\\d{1,2}\\\\.[^\\\\S\\\\n]+\\\\d{1,2}\\\\.[^\\\\S\\\\n]+\\\\d{4}\\\\b`,\n label: \"date\",\n score: 1,\n};\n\nconst IP_ADDRESS: RegexDef = {\n pattern:\n `\\\\b(?:(?:25[0-5]|2[0-4]\\\\d|[01]?\\\\d\\\\d?)\\\\.){3}` +\n `(?:25[0-5]|2[0-4]\\\\d|[01]?\\\\d\\\\d?)\\\\b`,\n label: \"ip address\",\n score: 1,\n};\n\nconst CZ_BANK_ACCOUNT: RegexDef = {\n pattern: `\\\\b(?:\\\\d{1,6}-)?\\\\d{6,10}/\\\\d{4}(?!\\\\d)`,\n label: \"bank account number\",\n score: 0.95,\n};\n\n// Hungarian Budapest landline (+36 1 XXX XXXX).\n// 2+ digit area codes handled by INTL_PHONE.\nconst HU_LANDLINE: RegexDef = {\n pattern:\n `\\\\+36(?:[^\\\\S\\\\n]|[.\\\\-])?1(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{4}\\\\b`,\n label: \"phone number\",\n score: 0.9,\n};\n\n// Czech license plates (SPZ/RZ).\n// New format: 3SJ 0753 — digit, two letters, space?,\n// four digits. Old format: 1A2 3456 — digit, letter,\n// digit, space?, four digits.\n\n// Czech/Slovak postal code: \"110 00\", \"120 00\".\n// The distinctive XXX XX format with mandatory\n// space is specific enough to avoid most false\n// positives.\nconst CZ_POSTAL: RegexDef = {\n pattern: `\\\\b\\\\d{3}[^\\\\S\\\\n]\\\\d{2}\\\\b`,\n label: \"address\",\n score: 0.7,\n};\n\n// URL: scheme + host + optional port + path + query +\n// fragment. Trailing prose punctuation excluded but\n// ? = & # kept for query strings.\n// Allow missing // after http:/https: — common OCR\n// artifact (\"http:example.cz\" instead of\n// \"http://example.cz\"). Lookahead ensures bare scheme\n// is not matched in isolation (e.g., \"http:\" at EOL).\nconst URL: RegexDef = {\n pattern:\n `(?:https?://|https?:(?=[^\\\\s])|www\\\\.)` +\n `[\\\\w\\\\-]+(?:\\\\.[\\\\w\\\\-]+)+` +\n `(?::\\\\d+)?` +\n `(?:[/?#][^\\\\s)\\\\]>]*[^\\\\s.,;:!?)\\\\]>])?`,\n label: \"url\",\n score: 1,\n};\n\n// Bare domain: no protocol/www prefix, ends with a\n// known TLD. Catches \"fondkinematografie.cz\" etc.\n// Uses [a-zA-Z0-9] (no underscores — invalid in\n// hostnames). Short ambiguous TLDs (de, at, no, se,\n// fi, dk, be, it, uk) require at least one subdomain\n// dot to reduce false positives in European legal\n// text; unambiguous TLDs allow bare second-level\n// domains (e.g., \"fondkinematografie.cz\").\nconst LONG_TLDS =\n \"com|org|net|eu|cz|sk|pl|hu|ro|fr|es\" + \"|co\\\\.uk|nl|ch|info|io|dev\";\nconst SHORT_TLDS = \"de|at|be|se|fi|dk|no|it|uk\";\n// RFC 1123: labels cannot start or end with hyphen.\nconst HOST_LABEL = `[a-zA-Z0-9](?:[a-zA-Z0-9\\\\-]*[a-zA-Z0-9])?`;\nconst BARE_HOST = `\\\\b[a-zA-Z0-9][a-zA-Z0-9\\\\-]+[a-zA-Z0-9]`;\nconst PATH_SUFFIX = `(?:[/?#][^\\\\s)\\\\]>]*[^\\\\s.,;:!?)\\\\]>])?`;\nconst BARE_DOMAIN: RegexDef = {\n pattern:\n // Unambiguous TLDs: bare SLDs ok (one dot)\n `${BARE_HOST}(?:\\\\.${HOST_LABEL})*` +\n `\\\\.(?:${LONG_TLDS})\\\\b${PATH_SUFFIX}` +\n `|` +\n // Short/ambiguous TLDs: require subdomain (two+ dots)\n `${BARE_HOST}(?:\\\\.${HOST_LABEL})+` +\n `\\\\.(?:${SHORT_TLDS})\\\\b${PATH_SUFFIX}`,\n label: \"url\",\n score: 0.9,\n};\n\n// Full RFC 5952 IPv6. :: compressed form replaces\n// 1+ zero groups. Right side: 1-7 hex groups.\nconst IPV6_ADDRESS: RegexDef = {\n pattern:\n `\\\\b(?:[0-9a-fA-F]{1,4}:){7}` +\n `[0-9a-fA-F]{1,4}\\\\b` +\n `|\\\\b(?:[0-9a-fA-F]{1,4}:){1,7}:\\\\b` +\n `|::(?:[0-9a-fA-F]{1,4}:){0,6}` +\n `[0-9a-fA-F]{1,4}`,\n label: \"ip address\",\n score: 1,\n};\n\n// MAC: colon-only OR hyphen-only (no mixed).\nconst MAC_ADDRESS: RegexDef = {\n pattern:\n `\\\\b(?:[0-9a-fA-F]{2}:){5}` +\n `[0-9a-fA-F]{2}\\\\b` +\n `|\\\\b(?:[0-9a-fA-F]{2}-){5}` +\n `[0-9a-fA-F]{2}\\\\b`,\n label: \"mac address\",\n score: 1,\n};\n\n// SWIFT/BIC moved to trigger-based detection\n// (triggers.global.json) for better composability.\n\n// 12-hour time: \"5:00 p.m.\", \"12:30 AM\", \"5:00p.m.\",\n// \"11:00 a.m. Eastern Time\". Captures HH:MM and the\n// am/pm marker; optional timezone suffix is not\n// included (it's not PII). Case spelled out explicitly\n// (no (?i)) because DFA compilation fails with\n// Unicode + case-insensitive flag on this pattern.\nconst TIME_12H: RegexDef = {\n pattern:\n `\\\\b(?:1[0-2]|0?[1-9]):[0-5]\\\\d` +\n `[^\\\\S\\\\n]?(?:[aApP]\\\\.?[mM]\\\\.?)` +\n `(?=[\\\\s,;!?)]|$)`,\n label: \"date\",\n score: 0.9,\n};\n\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 IBAN,\n EMAIL,\n INTL_PHONE,\n CZ_PHONE,\n TEL_PREFIX_PHONE,\n CREDIT_CARD,\n CZ_BIRTH_NUMBER,\n DATE_NUMERIC,\n DATE_CZ_SPACED,\n IP_ADDRESS,\n CZ_BANK_ACCOUNT,\n HU_LANDLINE,\n CZ_POSTAL,\n URL,\n IPV6_ADDRESS,\n MAC_ADDRESS,\n BARE_DOMAIN,\n TIME_12H,\n ...STDNUM_ENTRIES,\n];\n\n/** Flat pattern array for text-search. */\nexport const REGEX_PATTERNS: readonly string[] = ALL_REGEX_DEFS.map(\n (d) => d.pattern,\n);\n\n/** Parallel metadata. Index = pattern index. */\nexport const REGEX_META: readonly RegexMeta[] = ALL_REGEX_DEFS.map(\n (d): RegexMeta => {\n const meta: RegexMeta = {\n label: d.label,\n score: d.score,\n };\n if (d.validator) {\n meta.validator = d.validator;\n }\n return meta;\n },\n);\n\n// ── Dynamic date patterns (22 languages) ────────────\n\n/**\n * JSON shape: language codes map to string arrays;\n * metadata keys (prefixed `_`) map to strings.\n * The `_` keys are skipped by `buildMonthAlternation`.\n */\ntype DateMonths = Record<string, string[] | string>;\n\n/**\n * Build month-name alternation from date-months.json.\n * Deduplicates across all 22 languages, filters names\n * shorter than 3 chars (too many false positives), and\n * sorts longest-first so the regex engine prefers the\n * longest match.\n */\nconst buildMonthAlternation = (months: DateMonths): string => {\n const seen = new Set<string>();\n for (const [key, value] of Object.entries(months)) {\n if (key.startsWith(\"_\")) continue;\n const names = Array.isArray(value) ? value : [value];\n for (const name of names) {\n // Strip trailing dots for the regex; date patterns\n // use `\\\\.?` after the alternation to match optional\n // abbreviation dots.\n const clean = name.replace(/\\.$/, \"\").toLowerCase();\n if (clean.length >= MIN_MONTH_NAME_LENGTH) {\n seen.add(clean);\n }\n }\n }\n return [...seen]\n .toSorted((a, b) => b.length - a.length)\n .map(escapeRegex)\n .join(\"|\");\n};\n\n/**\n * Build date patterns from a month-name alternation.\n * Returns 6 patterns covering the major written-date\n * formats across all supported languages.\n */\nconst buildDatePatternsFromMonths = (alt: string): string[] => {\n if (!alt) {\n // No month names survived filtering — return nothing\n // rather than emitting patterns with (?:) that match\n // arbitrary whitespace.\n return [];\n }\n // Optional time suffix: \"19:45:50\" or \"19:45\"\n const TIME = `(?:\\\\s+\\\\d{1,2}:\\\\d{2}(?::\\\\d{2})?)`;\n return [\n // a. DD[.] Month[.] YYYY [HH:MM[:SS]]\n `(?i)\\\\b\\\\d{1,2}\\\\.?\\\\s+(?:${alt})\\\\.?\\\\s+\\\\d{4}${TIME}?\\\\b`,\n // b. Month[.] DD[,] YYYY — \"March 7, 2023\" (US format)\n `(?i)\\\\b(?:${alt})\\\\.?\\\\s+\\\\d{1,2},?\\\\s+\\\\d{4}\\\\b`,\n // g. Month[.] DD — \"December 31\" (no year, US format)\n `(?i)\\\\b(?:${alt})\\\\.?\\\\s+\\\\d{1,2}(?=\\\\s|[.,;!?)]|$)`,\n // c. DDst/nd/rd/th Month[.] [YYYY] — \"1st January 2025\"\n `(?i)\\\\b\\\\d{1,2}(?:st|nd|rd|th)\\\\s+(?:${alt})\\\\.?` +\n `(?:\\\\s+\\\\d{4})?(?=\\\\s|[.,;!?)]|$)`,\n // d. Month[.] YYYY — \"October 1983\"\n `(?i)\\\\b(?:${alt})\\\\.?\\\\s+\\\\d{4}\\\\b`,\n // e. YYYY. Month[.] DD. — Hungarian \"2025. január 7.\"\n `(?i)\\\\b\\\\d{4}\\\\.\\\\s+(?:${alt})\\\\.?\\\\s+\\\\d{1,2}\\\\.?(?=\\\\s|[.,;!?)]|$)`,\n // f. DD de Month[.] [de] YYYY — Spanish \"7 de enero de 2025\"\n `(?i)\\\\b\\\\d{1,2}\\\\s+de\\\\s+(?:${alt})\\\\.?` + `(?:\\\\s+de)?\\\\s+\\\\d{4}\\\\b`,\n ];\n};\n\n/** Cached promise for date patterns. Loaded once. */\nlet datePatternPromise: Promise<string[]> | null = null;\n\nconst loadDatePatterns = async (): Promise<string[]> => {\n const mod = await import(\"@stll/anonymize-data/config/date-months.json\");\n // Dynamic import of JSON returns { default, ...keys }.\n // Use `default` if present (ESM wrapper), else the\n // module itself.\n const months: DateMonths = mod.default ?? mod;\n const alt = buildMonthAlternation(months);\n return buildDatePatternsFromMonths(alt);\n};\n\n/**\n * Get dynamically built date patterns from\n * date-months.json. Returns a cached promise; the JSON\n * is loaded only once.\n */\nexport const getDatePatterns = (): Promise<string[]> => {\n if (!datePatternPromise) {\n datePatternPromise = loadDatePatterns().catch((err) => {\n datePatternPromise = null;\n throw err;\n });\n }\n return datePatternPromise;\n};\n\n/** Date pattern metadata (all are score 1 dates). */\nexport const DATE_PATTERN_META: Readonly<RegexMeta> = Object.freeze({\n label: \"date\",\n score: 1,\n});\n\n// ── Dynamic currency patterns ──────────────────────\n\n/**\n * JSON shape from currencies.json: ISO 4217 codes,\n * common currency symbols, and local currency names.\n */\ntype CurrenciesData = {\n codes: string[];\n symbols: string[];\n localNames?: string[];\n};\n\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 buildCurrencyPatterns = (data: CurrenciesData): string[] => {\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 TrailingPart = { len: number; alt: string };\n const parts: TrailingPart[] = data.codes.map((code) => ({\n len: code.length,\n alt: escapeRegex(code),\n }));\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 parts.push({\n len: name.length,\n alt: `(?i:${escaped})`,\n });\n } else {\n parts.push({ len: name.length, alt: escaped });\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 if (symbols) {\n for (const ch of data.symbols) {\n parts.push({\n len: ch.length,\n alt: escapeRegex(ch),\n });\n }\n }\n\n const trailingAlt = parts\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\n const patterns: string[] = [];\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 // Leading symbol: $100, €1,000.50, € 100000\n if (symbols) {\n patterns.push(\n `(?:[${symbols}])` + `[^\\\\S\\\\n\\\\t]?` + `${NUM}${DECIMAL}${END}`,\n );\n }\n\n // Leading multi-char code: \"Kč 10,—\", \"Fr. 500\"\n if (trailingAlt) {\n patterns.push(\n `\\\\b(?:${trailingAlt})` + `[^\\\\S\\\\n\\\\t]{0,2}` + `${NUM}${DECIMAL}${END}`,\n );\n }\n\n // Trailing code/name: 100 USD, 1,000.50 CZK,\n // 100000 Kč, 500 korun, 100 Fr.\n if (trailingAlt) {\n patterns.push(\n `\\\\b${NUM}${DECIMAL}[^\\\\S\\\\n\\\\t]{0,4}` + `(?:${trailingAlt})${END}`,\n );\n }\n\n return patterns;\n};\n\n/** Cached promise for currency patterns. Loaded once. */\nlet currencyPatternPromise: Promise<string[]> | null = null;\n\nconst loadCurrencyPatterns = async (): Promise<string[]> => {\n const mod = await import(\"@stll/anonymize-data/config/currencies.json\");\n const data: CurrenciesData = mod.default ?? mod;\n return buildCurrencyPatterns(data);\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\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 (meta.label === \"phone number\" && match.text.length < MIN_PHONE_LENGTH) {\n continue;\n }\n\n // Post-match validation: if the pattern came from\n // a stdnum validator, compact (strip separators)\n // then validate. The candidate regex may capture\n // spaced/dashed variants that validate() rejects\n // without compaction.\n if (meta.validator) {\n const compacted = meta.validator.compact(match.text);\n const result = meta.validator.validate(compacted);\n if (!result.valid) {\n continue;\n }\n }\n\n results.push({\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 }\n\n return results;\n};\n\n// ── Dynamic signing clause patterns ────────────────\n\ntype SigningClauseConfig = {\n patterns: Array<{\n lang: string;\n prefix: string;\n suffix: string;\n prepositions: string[];\n }>;\n};\n\n/**\n * Build signing clause place-name patterns from\n * signing-clauses.json. Each pattern captures the\n * city/place name from contract signing locations.\n *\n * The place name sub-pattern:\n * \\p{Lu}\\p{Ll}+ (capitalized word)\n * optionally followed by preposition + capitalized\n * word (for \"nad Nisou\", \"am Main\", etc.)\n * optionally followed by more capitalized words\n * (for \"Hradec Králové\", \"New York\", etc.)\n */\nconst buildSigningClausePatterns = (data: SigningClauseConfig): string[] => {\n const patterns: string[] = [];\n\n for (const entry of data.patterns) {\n const prepAlt =\n entry.prepositions.length > 0 ? entry.prepositions.join(\"|\") : null;\n\n // Place name: Uppercase word, optionally with\n // preposition + uppercase, optionally more caps\n const place = prepAlt\n ? `(\\\\p{Lu}\\\\p{Ll}+` +\n `(?:\\\\s+(?:${prepAlt})\\\\s+\\\\p{Lu}\\\\p{Ll}+)*` +\n `(?:\\\\s+\\\\p{Lu}\\\\p{Ll}+)*)`\n : `(\\\\p{Lu}\\\\p{Ll}+(?:[- ]\\\\p{Lu}\\\\p{Ll}+)*)`;\n\n const full =\n `(?:^|\\\\n|[^\\\\S\\\\n])` +\n entry.prefix +\n place +\n (entry.suffix ? `(?:${entry.suffix})` : \"\");\n\n patterns.push(full);\n }\n\n return patterns;\n};\n\nexport const SIGNING_CLAUSE_META: Readonly<RegexMeta> = {\n label: \"address\",\n score: 0.9,\n};\n\nlet signingPatternPromise: Promise<string[]> | null = null;\n\nconst loadSigningPatterns = async (): Promise<string[]> => {\n const mod = await import(\"@stll/anonymize-data/config/signing-clauses.json\");\n const data: SigningClauseConfig = mod.default ?? mod;\n return buildSigningClausePatterns(data);\n};\n\nexport const getSigningClausePatterns = (): Promise<string[]> => {\n if (!signingPatternPromise) {\n signingPatternPromise = loadSigningPatterns().catch((err) => {\n signingPatternPromise = null;\n throw err;\n });\n }\n return signingPatternPromise;\n};\n","/**\n * Legal form 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 { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\nimport { DASH_INNER } from \"../util/char-groups\";\n\nconst UPPER = \"A-ZÁČĎÉĚÍŇÓŘŠŤÚŮÝŽÄÖÜÀÂÆÇÈÊËÎÏÔÙÛŸÑ\\\\u0130\";\nconst LOWER = \"a-záčďéěíňóřšťúůýžäöüßàâæçèêëîïôùûÿñ\\\\u0131\";\nconst CAP_WORD = `(?:[${UPPER}]{2,}|[${UPPER}][${LOWER}${UPPER}]+)`;\n// ANY_WORD: mixed-case word, short all-caps token\n// (2-3 chars, e.g. \"CZ\" in \"Metrostav CZ s.r.o.\"),\n// or digit-only token (1-4 digits, e.g. \"2028\" in\n// \"Invest 2028 s.r.o.\" or \"12\" in \"Praha 12 s.r.o.\").\nconst ANY_WORD =\n `(?:[${UPPER}${LOWER}][${LOWER}${UPPER}]+` +\n `|[${UPPER}]{2,3}` +\n `|\\\\d{1,4})`;\n// All-caps word: 2+ uppercase letters, no lowercase.\n// For company names like \"EAGLES BRNO\", max 3 words.\nconst ALLCAP_WORD = `[${UPPER}]{2,}`;\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\nconst escapeForRegex = (form: string): string =>\n form\n .replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\s+/g, \"\\\\s+\")\n // Use [^\\S\\n]? (optional horizontal whitespace)\n // instead of \\s* to prevent greedy matching across\n // newlines which causes DFA failures in regex-set.\n .replace(/\\\\\\./g, \"\\\\.[^\\\\S\\\\n]?\");\n\nconst isShortForm = (form: string): boolean =>\n form.replace(/[.\\s]/g, \"\").length <= 3 && !form.includes(\" \");\n\nconst buildPatternString = (forms: string[]): string | null => {\n if (forms.length === 0) {\n return null;\n }\n\n const sorted = forms.toSorted((a, b) => b.length - a.length);\n const alt = sorted.map(escapeForRegex).join(\"|\");\n // Separator between name words: space, ampersand,\n // comma, dot, hyphen (1-4 chars). Connector words\n // (a, and, und, et, e, y, i) are allowed only when\n // followed by a lowercase-starting word — this\n // prevents greedy extension across entity boundaries\n // like \"Foo s.r.o. a BAR s.r.o.\"\n const LOWER_CONNECTOR = `\\\\s+(?:a|and|und|et|e|y|i)\\\\s+(?=[${LOWER}])`;\n const SEPARATOR = `(?:[\\\\s&,.${DASH_INNER}]{1,4}|${LOWER_CONNECTOR})`;\n const prefix = `(?:${CAP_WORD})` + `(?:${SEPARATOR}(?:${ANY_WORD})){0,10}`;\n const separator = `(?:\\\\s+|,\\\\s*)`;\n\n return `${prefix}${separator}(?:${alt})(?![${LOWER}])`;\n};\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 let data: Record<string, string[]> = {};\n\n try {\n const mod = await import(\"@stll/anonymize-data/config/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 return [];\n }\n\n const allForms: string[] = [];\n const seen = new Set<string>();\n\n for (const forms of Object.values(data)) {\n for (const form of forms) {\n const key = form.toLowerCase();\n if (!seen.has(key)) {\n seen.add(key);\n allForms.push(form);\n }\n }\n }\n\n const patterns: string[] = [];\n\n const longPattern = buildPatternString(\n allForms.filter((f) => !isShortForm(f)),\n );\n if (longPattern) {\n patterns.push(longPattern);\n }\n\n const shortPattern = buildPatternString(allForms.filter(isShortForm));\n if (shortPattern) {\n patterns.push(shortPattern);\n }\n\n // All-caps company names: \"EAGLES BRNO, z.s.\"\n // Up to 3 all-caps words before any legal form.\n // Uses all forms (both long and short).\n // No connectors — backward extension handles them.\n const allcapPrefix =\n `(?:${ALLCAP_WORD})` +\n `(?:[\\\\s&,.${DASH_INNER}]{1,4}(?:${ALLCAP_WORD})){0,2}`;\n const allcapAlt = allForms\n .toSorted((a, b) => b.length - a.length)\n .map(escapeForRegex)\n .join(\"|\");\n patterns.push(\n `${allcapPrefix}(?:\\\\s+|,\\\\s*)` + `(?:${allcapAlt})(?![${LOWER}])`,\n );\n\n return patterns;\n};\n\n// ── Backward extension ──────────────────────────────\n\nconst CONNECTOR_RE = /^(?:a|and|und|et|e|y|i|&)$/i;\nconst LEADING_CLAUSE_RE = /(?:^|\\s)(?:by\\s+and\\s+between|is\\s+between)\\s+/giu;\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\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 */\nconst extendBackward = (fullText: string, matchStart: number): number => {\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 = /^\\p{Lu}/u.test(word);\n const isConnector = CONNECTOR_RE.test(word);\n\n if (isUpper) {\n // Uppercase word — always accept\n pos = wordStart;\n } else if (isConnector) {\n // Connector — only accept if there is a valid\n // (uppercase-starting) word before it\n const prev = findWordBefore(fullText, wordStart);\n if (!prev) break;\n const prevIsUpper = /^\\p{Lu}/u.test(prev.word);\n if (!prevIsUpper) break;\n // Move pos back to the start of the word that\n // precedes the connector; the connector and all\n // whitespace between it and prev.start are\n // included implicitly in the entity slice.\n pos = prev.start;\n } else {\n break;\n }\n }\n\n return pos;\n};\n\nconst trimLeadingClause = (text: string): { offset: number; text: string } => {\n let cut = -1;\n\n for (const match of text.matchAll(LEADING_CLAUSE_RE)) {\n cut = match.index + match[0].length;\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 */\nexport const processLegalFormMatches = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText?: string,\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 if (text.includes(\"\\n\")) {\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 = match.start;\n let entityText = text;\n if (fullText) {\n const extended = extendBackward(fullText, match.start);\n if (extended < match.start) {\n entityStart = extended;\n entityText = fullText\n .slice(extended, match.start + text.length)\n .trimEnd();\n }\n }\n\n const clauseTrim = trimLeadingClause(entityText);\n if (clauseTrim.offset > 0) {\n entityStart += clauseTrim.offset;\n entityText = clauseTrim.text;\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 // Check: is the surrounding line also all-caps?\n const lineStart = fullText.lastIndexOf(\"\\n\", entityStart);\n const lineEnd = fullText.indexOf(\"\\n\", entityStart + entityText.length);\n const line = fullText.slice(\n lineStart + 1,\n lineEnd === -1 ? fullText.length : lineEnd,\n );\n const lineLetters = line.replace(/[^a-zA-ZÀ-ž]/g, \"\");\n const upperCount = [...lineLetters].filter(\n (c) => c === c.toUpperCase(),\n ).length;\n const lineIsAllCaps =\n lineLetters.length > 5 && upperCount / lineLetters.length >= 0.95;\n if (lineIsAllCaps) {\n // Entire line is all-caps → heading, skip\n continue;\n }\n // Only the company name is all-caps → keep it\n // (but limit to 3 words in prefix)\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 // Keep the original regex match if backward\n // extension alone pushed the name past the\n // all-caps 3-word guard.\n entityStart = match.start;\n entityText = text;\n ({ prefixEnd, prefixPart } = getPrefixInfo(entityText));\n isAllCapsMatch =\n prefixPart.length > 2 && prefixPart === prefixPart.toUpperCase();\n }\n } else if (isAllCapsMatch) {\n // No fullText available — fall back to rejecting\n continue;\n }\n\n // Reject Roman numeral suffixes\n const lastSpace = entityText.lastIndexOf(\" \");\n const rawSuffix = lastSpace !== -1 ? entityText.slice(lastSpace + 1) : \"\";\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 /[^\\x00-\\x7F]/.test(\n entityText.slice(0, lastSpace !== -1 ? lastSpace : entityText.length),\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 return results;\n};\n","/**\n * Known legal form suffixes. Shared between trigger\n * detection (reclassification) and org-propagation\n * (suffix stripping). Within each family of related\n * forms, longer variants come first so that\n * \"spol. s r.o.\" matches before \"s.r.o.\" and\n * \"s. r. o.\" matches before \"s.r.o.\".\n */\nexport const LEGAL_SUFFIXES = [\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 \"GmbH\",\n \"AG\",\n \"SE\",\n \"KG\",\n \"OHG\",\n \"Ltd.\",\n \"Ltd\",\n \"LLC\",\n \"LLP\",\n \"Inc.\",\n \"S.A.\",\n \"SA\",\n \"SAS\",\n \"SARL\",\n \"Sp. z o.o.\",\n \"S.p.A.\",\n] as const;\n","import type { Match } from \"@stll/text-search\";\n\nimport { DETECTION_SOURCES } from \"../types\";\nimport type {\n CompiledValidation,\n Entity,\n TriggerGroupConfig,\n TriggerRule,\n TriggerValidation,\n} from \"../types\";\nimport { POST_NOMINALS } from \"../config/titles\";\nimport { LEGAL_SUFFIXES } from \"../config/legal-forms\";\nimport { loadLanguageConfigs } from \"../util/lang-loader\";\nimport { DASH } from \"../util/char-groups\";\n\nconst TRIGGER_SCORE = 0.95;\nconst WHITESPACE_RE = /\\s+/;\nconst LETTER_RE = /\\p{L}/u;\n/**\n * Decimal-comma pattern: comma followed by digit or\n * dash notation (\"0,05%\", \"1.529,50 Kč\", \"98.000,- Kč\").\n */\nconst DECIMAL_COMMA_RE = new RegExp(`^,(?:\\\\d|${DASH}{1,2})`);\n\n/**\n * Post-nominal degree regex. When a comma-stop is\n * followed by a known post-nominal (Ph.D., CSc., MBA\n * etc.), skip the comma and degree, then continue.\n */\nconst POST_NOMINAL_RE = new RegExp(\n `^,\\\\s*(?:${POST_NOMINALS.toSorted((a, b) => b.length - a.length)\n .map((d) =>\n d.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\").replace(/\\\\\\./g, \"\\\\.\\\\s*\"),\n )\n .join(\"|\")})\\\\.?`,\n \"i\",\n);\n\n// Definitive legal form suffixes (case-sensitive).\n// When a person-labeled trigger captures text containing\n// one of these, the entity is reclassified as\n// \"organization\". No \"i\" flag: short uppercase-only\n// forms (SE, SA, AG) must NOT match Czech/Slovak\n// reflexive pronouns \"se\", \"sa\" which appear in\n// person-trigger captures like\n// \"Ing. Jan Novák, se sídlem...\".\nconst DEFINITIVE_LEGAL_FORMS = LEGAL_SUFFIXES;\n// Build case-sensitive regex. Short dot-free forms\n// (AG, SE, KG) get word boundaries to prevent substring\n// matches. All forms are uppercase in the list; the\n// regex is case-sensitive so \"se\"/\"sa\" won't match.\nconst LEGAL_FORM_CHECK_RE = new RegExp(\n DEFINITIVE_LEGAL_FORMS.map((f) => {\n const escaped = f\n .replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\\\\\./g, \"\\\\.\\\\s*\");\n const isDotFree = !f.includes(\".\");\n const isShort = f.length <= 4;\n return isDotFree && isShort ? `\\\\b${escaped}\\\\b` : escaped;\n }).join(\"|\"),\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 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 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\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 */\nexport const buildTriggerPatterns = async (): Promise<{\n patterns: string[];\n rules: TriggerRule[];\n}> => {\n const rules: TriggerRule[] = [];\n\n const allGroups = await loadLanguageConfigs<readonly TriggerGroupConfig[]>(\n \"triggers\",\n (mod) => {\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config\n const m = mod as {\n default?: readonly TriggerGroupConfig[];\n };\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config\n return (m.default ?? mod) as readonly TriggerGroupConfig[];\n },\n );\n for (const groups of allGroups) {\n if (!Array.isArray(groups)) {\n console.warn(\n \"[anonymize] triggers: unexpected \" + \"config shape, skipping\",\n );\n continue;\n }\n rules.push(...expandTriggerGroups(groups as TriggerGroupConfig[]));\n }\n\n // Load global triggers (language-agnostic)\n try {\n const globalMod =\n await import(\"@stll/anonymize-data/config/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(\"@stll/anonymize-data/config/year-words.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON\n const data = ((yearMod as { default?: unknown }).default ??\n yearMod) as Record<string, string | string[]>;\n const seen = new Set<string>();\n const yearValidation = compileValidations([\n {\n type: \"matches-pattern\",\n pattern: \"^(?:19|20)\\\\d{2}\\\\.?$\",\n },\n ]);\n for (const [key, words] of Object.entries(data)) {\n if (key.startsWith(\"_\") || !Array.isArray(words)) {\n continue;\n }\n for (const word of words) {\n const lc = word.toLowerCase();\n if (seen.has(lc)) continue;\n seen.add(lc);\n rules.push({\n trigger: word,\n label: \"date\",\n strategy: { type: \"n-words\", count: 1 },\n validations: yearValidation,\n includeTrigger: false,\n });\n }\n }\n } catch {\n // year-words.json not available — skip\n }\n\n // Warn about cross-group trigger duplicates.\n // Duplicates cause redundant AC matches but are\n // not fatal (mergeAndDedup handles overlap).\n const seen = new Map<string, { label: string; strategy: string }>();\n for (const rule of rules) {\n const key = rule.trigger.toLowerCase();\n const prev = seen.get(key);\n if (prev !== undefined) {\n const labelDiff = prev.label !== rule.label;\n const stratDiff = prev.strategy !== rule.strategy.type;\n if (labelDiff || stratDiff) {\n console.warn(\n `[anonymize] duplicate trigger` +\n ` \"${rule.trigger}\":` +\n (labelDiff\n ? ` labels \"${prev.label}\" vs` + ` \"${rule.label}\"`\n : \"\") +\n (stratDiff\n ? ` strategies \"${prev.strategy}\" vs` + ` \"${rule.strategy.type}\"`\n : \"\"),\n );\n }\n }\n seen.set(key, {\n label: rule.label,\n strategy: rule.strategy.type,\n });\n }\n\n // Build patterns from lowercased trigger strings.\n // rules[i] corresponds to patterns[i].\n // Plain lowercased strings — the unified builder\n // sets caseInsensitive globally on the AC.\n const patterns: string[] = rules.map((r) => r.trigger.toLowerCase());\n\n return { patterns, rules };\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/** Hard stop characters for to-next-comma scanning. */\nconst COMMA_STOP_CHARS = new Set([\"\\n\", \"(\", \"\\t\"]);\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 */\nconst ADDRESS_STOP_KEYWORDS = [\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\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 remaining = text.slice(triggerEnd);\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 let end = 0;\n let foundStop = false;\n\n while (end < valueText.length) {\n const ch = valueText[end];\n // Hard stops: newline, paren, tab\n if (ch !== undefined && COMMA_STOP_CHARS.has(ch)) {\n foundStop = true;\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 foundStop = true;\n break;\n }\n end++;\n }\n\n // Only cap at 100 chars when no stop char was\n // found (fallback for unterminated values). When\n // a stop char exists, respect its position even\n // if > 100.\n if (!foundStop) {\n end = Math.min(end, 100);\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 // Stop at newline or tab (tab separates cells\n // in DOCX table rows).\n const LINE_STOPS = [\"\\n\"];\n let end = valueText.length;\n for (const ch of LINE_STOPS) {\n const idx = valueText.indexOf(ch);\n if (idx !== -1 && idx < end) {\n end = idx;\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 const allTokens = cellText.split(WHITESPACE_RE);\n const words = allTokens\n .filter((w) => !PUNCT_ONLY.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 const raw = text.slice(triggerEnd);\n const sepMatch = /^(?:\\s*:\\s*|\\s+)/.exec(raw);\n if (!sepMatch) {\n return null;\n }\n const afterSep = raw.slice(sepMatch[0].length);\n const idMatch = /^[A-Z]{0,6}\\s?\\d[\\d\\s\\-/]{4,}/i.exec(afterSep);\n if (!idMatch) {\n return null;\n }\n const idText = idMatch[0].trim();\n const leadingSpaces = idMatch[0].length - idMatch[0].trimStart().length;\n const idStart =\n triggerEnd +\n sepMatch[0].length +\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 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 // 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 // \"č.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. In address\n // context, this is always an abbreviation,\n // not end of sentence.\n if (\n next === \" \" &&\n afterNext !== undefined &&\n (/\\p{L}/u.test(afterNext) || /\\d/.test(afterNext))\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.\n const afterComma = valueText\n .slice(end + 1)\n .trimStart()\n .toLowerCase();\n const hitsKeyword = ADDRESS_STOP_KEYWORDS.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 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 if followed by a\n // letter — but skip this check when the trigger\n // itself ends with whitespace (e.g., \"pan \",\n // \"město \") since the trailing space already\n // acts as a boundary delimiter.\n if (\n !rule.trigger.endsWith(\" \") &&\n LETTER_RE.test(fullText[match.end] ?? \"\")\n ) {\n continue;\n }\n\n const triggerEnd = match.end;\n const rawValue = extractValue(\n fullText,\n triggerEnd,\n rule.strategy,\n rule.label,\n );\n const value = rawValue ? stripQuotes(rawValue) : null;\n\n if (value) {\n // Apply declarative validations to the captured\n // value text (not the full entity including\n // trigger). This is intentional: validations\n // like min-length should test the extracted\n // value, not the trigger keyword itself.\n if (!applyValidations(value.text, rule.validations)) {\n continue;\n }\n\n // When includeTrigger is set, the entity span\n // starts at the trigger match, not the value.\n const entityStart = rule.includeTrigger ? match.start : value.start;\n const entityEnd = value.end;\n const entityText = fullText.slice(entityStart, entityEnd);\n\n // Legal form reclassification: any person-labeled\n // trigger whose captured text contains a definitive\n // legal form suffix is reclassified as organization.\n // This is universal — every person with a legal form\n // is an organisation, no per-group config needed.\n const effectiveLabel =\n rule.label === \"person\" && LEGAL_FORM_CHECK_RE.test(entityText)\n ? \"organization\"\n : rule.label;\n\n results.push({\n start: entityStart,\n end: entityEnd,\n label: effectiveLabel,\n text: entityText,\n score: TRIGGER_SCORE,\n source: DETECTION_SOURCES.TRIGGER,\n });\n }\n }\n\n return results;\n};\n\n// Re-export for testing\nexport { expandTriggerGroups, compileValidations, applyValidations };\n","/**\n * Geographic regions and country code mappings for\n * scoping deny list dictionaries.\n */\n\nexport const REGIONS = {\n Global: null,\n International: null,\n Europe: [\n \"AL\",\n \"AD\",\n \"AT\",\n \"BE\",\n \"BA\",\n \"BG\",\n \"HR\",\n \"CY\",\n \"CZ\",\n \"DK\",\n \"EE\",\n \"FI\",\n \"FR\",\n \"DE\",\n \"GR\",\n \"HU\",\n \"IS\",\n \"IE\",\n \"IT\",\n \"XK\",\n \"LV\",\n \"LI\",\n \"LT\",\n \"LU\",\n \"MD\",\n \"ME\",\n \"MK\",\n \"MT\",\n \"MC\",\n \"NL\",\n \"NO\",\n \"PL\",\n \"PT\",\n \"RO\",\n \"RS\",\n \"SK\",\n \"SI\",\n \"ES\",\n \"SE\",\n \"CH\",\n \"UA\",\n \"GB\",\n ],\n Americas: [\n \"US\",\n \"CA\",\n \"MX\",\n \"BR\",\n \"AR\",\n \"CL\",\n \"CO\",\n \"PE\",\n \"EC\",\n \"VE\",\n \"UY\",\n \"PY\",\n \"BO\",\n \"CR\",\n \"PA\",\n \"DO\",\n \"GT\",\n \"HN\",\n \"SV\",\n \"NI\",\n \"CU\",\n ],\n AsiaPacific: [\n \"AU\",\n \"NZ\",\n \"JP\",\n \"KR\",\n \"CN\",\n \"TW\",\n \"SG\",\n \"MY\",\n \"TH\",\n \"VN\",\n \"PH\",\n \"ID\",\n \"IN\",\n \"PK\",\n \"BD\",\n \"LK\",\n \"NP\",\n \"HK\",\n \"MO\",\n ],\n MENA: [\n \"AE\",\n \"SA\",\n \"IL\",\n \"TR\",\n \"EG\",\n \"JO\",\n \"LB\",\n \"IQ\",\n \"IR\",\n \"QA\",\n \"KW\",\n \"BH\",\n \"OM\",\n \"MA\",\n \"TN\",\n \"DZ\",\n \"LY\",\n \"SY\",\n \"YE\",\n \"PS\",\n ],\n SubSaharanAfrica: [\n \"ZA\",\n \"NG\",\n \"KE\",\n \"GH\",\n \"TZ\",\n \"ET\",\n \"SN\",\n \"CI\",\n \"CM\",\n \"UG\",\n \"RW\",\n \"MZ\",\n \"AO\",\n \"ZW\",\n \"BW\",\n \"NA\",\n \"MU\",\n ],\n EU: [\n \"AT\",\n \"BE\",\n \"BG\",\n \"HR\",\n \"CY\",\n \"CZ\",\n \"DK\",\n \"EE\",\n \"FI\",\n \"FR\",\n \"DE\",\n \"GR\",\n \"HU\",\n \"IE\",\n \"IT\",\n \"LV\",\n \"LT\",\n \"LU\",\n \"MT\",\n \"NL\",\n \"PL\",\n \"PT\",\n \"RO\",\n \"SK\",\n \"SI\",\n \"ES\",\n \"SE\",\n ],\n DACH: [\"DE\", \"AT\", \"CH\"],\n Nordics: [\"DK\", \"SE\", \"NO\", \"FI\", \"IS\"],\n CEE: [\"CZ\", \"SK\", \"PL\", \"HU\", \"RO\", \"BG\", \"HR\", \"SI\", \"LT\", \"LV\", \"EE\"],\n Anglosphere: [\"GB\", \"US\", \"CA\", \"AU\", \"NZ\", \"IE\"],\n Benelux: [\"BE\", \"NL\", \"LU\"],\n GulfStates: [\"AE\", \"SA\", \"QA\", \"KW\", \"BH\", \"OM\"],\n SouthAsia: [\"IN\", \"PK\", \"BD\", \"LK\", \"NP\"],\n EastAsia: [\"CN\", \"JP\", \"KR\", \"TW\"],\n SoutheastAsia: [\"SG\", \"MY\", \"TH\", \"VN\", \"PH\", \"ID\"],\n Oceania: [\"AU\", \"NZ\"],\n} as const;\n\nexport type RegionId = keyof typeof REGIONS;\n\ntype RegionArrays = {\n [K in RegionId]: (typeof REGIONS)[K];\n};\n\ntype NonNullRegion = {\n [K in RegionId as RegionArrays[K] extends null ? never : K]: RegionArrays[K];\n};\n\nexport type CountryCode = NonNullRegion[keyof NonNullRegion][number];\n\n/**\n * Expand region names to country codes and merge with\n * explicit country codes. Returns null when both inputs\n * are empty/undefined (meaning \"match all countries\").\n */\nexport const resolveCountries = (\n regions?: string[],\n countries?: string[],\n): Set<string> | null => {\n const hasRegions = regions && regions.length > 0;\n const hasCountries = countries && countries.length > 0;\n\n if (!hasRegions && !hasCountries) {\n return null;\n }\n\n const result = new Set<string>();\n\n const isRegion = (name: string): name is RegionId => name in REGIONS;\n\n if (hasRegions) {\n for (const name of regions) {\n if (!isRegion(name)) {\n continue;\n }\n const codes = REGIONS[name];\n if (codes === null) {\n // Global/International: return null (all)\n return null;\n }\n for (const code of codes) {\n result.add(code);\n }\n }\n }\n\n if (hasCountries) {\n for (const code of countries) {\n result.add(code);\n }\n }\n\n return result;\n};\n","import type { Entity } from \"../types\";\nimport type { PipelineContext } from \"../context\";\nimport { defaultContext } from \"../context\";\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/;\nconst ADDRESS_COMPONENTS_RE =\n /(?:^|\\s)(?:ul\\.|ulice|nám\\.|náměstí|tř\\.|třída|nábř\\.|nábřeží|č\\.p\\.|č\\.ev\\.|č\\.|sídliště|bulvár)(?=[\\s,./]|$)/i;\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// Section/clause numbers: \"§ 3\", \"3.2.1\", \"12.\" but NOT\n// dates like \"4.3.2026\" or long digit strings like IČO.\n// A section number has 1-3 digit groups of 1-3 digits each,\n// never ending with a 4-digit group (that's a year).\nconst SECTION_NUMBER_RE = /^(?:§\\s*)?\\d{1,3}(?:\\.\\d{1,3}){0,4}\\.?$/;\nconst STANDALONE_YEAR_RE = /^(?:19|20)\\d{2}$/;\n\n// Number-abbreviation prefixes: \"č.\", \"Nr.\", \"No.\", \"nr.\",\n// \"no.\", \"n.\", \"čís.\" — when a numeric entity is preceded\n// by one of these, it's a reference number, not PII.\nconst NUMBER_ABBREV_RE = /(?:^|[\\s(])(?:č|čís|nr|no|n)\\.\\s*$/i;\nconst SIGNING_CLAUSE_ADDRESS_RE = /^(?:v|ve)\\s+[^\\d,\\n]{1,40},?\\s+dne$/iu;\nconst PERSON_TRAILING_NOUNS: ReadonlySet<string> = new Set([\n \"association\",\n \"period\",\n \"reform\",\n]);\nconst LEGAL_FORM_HEADING_RE = /\\b(?:agreement|amendment|contract|exhibit)\\b/iu;\nconst LEADING_ARTIFACT_RE = /^(?:\\.\\s)+/u;\nconst ADDRESS_ROLE_PREFIX_RE =\n /^(?:prodávajícího|kupujícího|objednatele|zhotovitele|pronajímatele|dodavatele|odběratele|zaměstnance|zaměstnavatele|nájemce)\\s+(?=(?:\\p{Lu}|\\d|ul\\.?|ulice|nám\\.?|náměstí|tř\\.?|třída|nábř\\.?|nábřeží|č\\.p\\.?|č\\.ev\\.?|sídliště))/iu;\nconst ADDRESS_INLINE_ABBREV_AFTER_RE =\n /^(?:\\p{Lu}[\\p{L}\\p{M}]{0,3}\\.|ul\\.?|nám\\.?|tř\\.?|nábř\\.?|č\\.p\\.?|č\\.ev\\.?)/u;\nconst ADDRESS_INLINE_ABBREV_BEFORE_RE =\n /(?:^|[\\s,])(?:st|ave|rd|dr|blvd|ln|hwy|pkwy|cir|ct|pl|sq|ter|trl|ste|apt|bldg|fl|ul|nám|tř|nábř|č\\.p|č\\.ev)$/iu;\nconst ADDRESS_CONTINUATION_WORD_RE =\n /^(?:suite|building|floor|unit|apartment|room|tower|wing|block|bldg|ste|apt|fl)\\b/iu;\n\nconst trimTrailingAddressProse = (text: string): string => {\n for (const match of text.matchAll(/\\.(?=\\s+\\p{Lu})/gu)) {\n const cutoff = match.index;\n if (cutoff === undefined) {\n continue;\n }\n const before = text.slice(0, cutoff);\n if (!HAS_DIGIT_RE.test(before)) {\n continue;\n }\n const after = text.slice(cutoff + 1).trimStart();\n if (\n after.length < 5 ||\n ADDRESS_INLINE_ABBREV_AFTER_RE.test(after) ||\n ADDRESS_INLINE_ABBREV_BEFORE_RE.test(before.trimEnd()) ||\n ADDRESS_CONTINUATION_WORD_RE.test(after)\n ) {\n continue;\n }\n return before.trimEnd();\n }\n\n return text;\n};\n\nconst normalizeEntity = (entity: Entity): Entity | null => {\n let start = entity.start;\n let text = entity.text;\n\n const trimLeading = (re: RegExp) => {\n const match = re.exec(text);\n if (!match) {\n return;\n }\n start += match[0].length;\n text = text.slice(match[0].length);\n };\n\n trimLeading(LEADING_ARTIFACT_RE);\n trimLeading(/^\\s+/u);\n\n if (entity.label === \"address\") {\n trimLeading(ADDRESS_ROLE_PREFIX_RE);\n text = trimTrailingAddressProse(text);\n }\n\n const trailingMatch = /[,\\s]+$/u.exec(text);\n if (trailingMatch) {\n text = text.slice(0, text.length - trailingMatch[0].length);\n }\n\n if (text.length === 0) {\n return null;\n }\n\n return {\n ...entity,\n start,\n end: start + text.length,\n text,\n };\n};\n\n// ── 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(\"@stll/anonymize-data/config/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/**\n * Filter out entities that are likely false positives:\n * template placeholders, clause/section numbers,\n * standalone years, and generic legal role terms.\n *\n * Runs as a post-processing step after all detection\n * layers have merged.\n */\nexport const filterFalsePositives = (\n entities: Entity[],\n ctx: PipelineContext = defaultContext,\n fullText?: string,\n): Entity[] => {\n const filtered: Entity[] = [];\n const roles = getGenericRoles(ctx);\n\n for (const entity of entities) {\n const normalized = normalizeEntity(entity);\n if (!normalized) {\n continue;\n }\n\n // Strip leading \". \" artifacts from trigger extraction\n // after abbreviations (\"dat. nar.\", \"č.p.\").\n const trimmed = normalized.text;\n\n if (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 // 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 // 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 if (normalized.label === \"person\") {\n const tokens = trimmed.split(/\\s+/u);\n const last = tokens\n .at(-1)\n ?.replace(/[.,;:!?]+$/u, \"\")\n .toLowerCase();\n if (tokens.length > 1 && last && PERSON_TRAILING_NOUNS.has(last)) {\n continue;\n }\n }\n\n if (\n (normalized.label === \"person\" || normalized.label === \"organization\") &&\n roles.has(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 !ADDRESS_COMPONENTS_RE.test(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.\n if (\n normalized.label === \"address\" &&\n normalized.source === \"trigger\" &&\n !HAS_DIGIT_RE.test(trimmed) &&\n !ADDRESS_COMPONENTS_RE.test(trimmed) &&\n !JURISDICTION_RE.test(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 * 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 REPLACEMENTS: readonly [number, number][] = [\n [0x00a0, 0x0020], // NBSP → space\n [0x2007, 0x0020], // figure space → space\n [0x202f, 0x0020], // narrow NBSP → space\n [0x2013, 0x002d], // en-dash → hyphen\n [0x2014, 0x002d], // em-dash → hyphen\n [0x201c, 0x0022], // left smart quote → \"\n [0x201d, 0x0022], // right smart quote → \"\n];\n\nconst CHAR_MAP = new Map<number, number>(REPLACEMENTS);\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 if (CHAR_MAP.has(text.charCodeAt(i))) {\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] = CHAR_MAP.get(code) ?? 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 getNameCorpusFirstNames,\n getNameCorpusSurnames,\n getNameCorpusTitles,\n initNameCorpus,\n} from \"./names\";\nimport { resolveCountries } from \"../regions\";\nimport { DETECTION_SOURCES } from \"../types\";\nimport type { Entity, PipelineConfig } from \"../types\";\nimport type { PipelineContext } from \"../context\";\nimport { defaultContext } from \"../context\";\nimport { loadGenericRoles } from \"../filters/false-positives\";\nimport { normalizeForSearch } from \"../util/normalize\";\nimport { ALL_UPPER_RE, UPPER_START_RE } from \"../util/text\";\nimport { DASH } from \"../util/char-groups\";\n\n/**\n * Try to load the optional @stll/anonymize-data package.\n * Returns null if not installed.\n */\nconst loadDataModule = async (): Promise<\n typeof import(\"@stll/anonymize-data\") | null\n> => {\n try {\n return await import(\"@stll/anonymize-data\");\n } catch {\n return null;\n }\n};\n\nexport type DenyListConfig = Pick<\n PipelineConfig,\n | \"enableDenyList\"\n | \"enableNameCorpus\"\n | \"denyListCountries\"\n | \"denyListRegions\"\n | \"denyListExcludeCategories\"\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(\"@stll/anonymize-data/config/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\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(\"@stll/anonymize-data/config/stopwords.json\");\n const list = (mod.default ?? []).filter(\n (w: string) => !getFirstNameExclusions(ctx).has(w),\n );\n const set: ReadonlySet<string> = new Set(list);\n ctx.stopwords = set;\n return set;\n } catch (err) {\n console.warn(\n \"[anonymize] Failed to load stopwords.json\" +\n \" — stopword filtering disabled:\",\n err,\n );\n const empty: ReadonlySet<string> = new Set();\n ctx.stopwords = empty;\n return empty;\n }\n })();\n return ctx.stopwordsPromise;\n};\n\nconst EMPTY_STOPWORDS: ReadonlySet<string> = new Set();\n\n/** Sync accessor — returns empty set before init. */\nconst getStopwords = (ctx: PipelineContext): ReadonlySet<string> =>\n ctx.stopwords ?? EMPTY_STOPWORDS;\n\n// ── Person stopwords (lazy-loaded from JSON) ─────────\n\nconst loadPersonStopwords = (\n ctx: PipelineContext,\n): Promise<ReadonlySet<string>> => {\n if (ctx.personStopwordsPromise) {\n return ctx.personStopwordsPromise;\n }\n ctx.personStopwordsPromise = (async () => {\n try {\n const mod: {\n default?: { words?: string[] };\n } = await import(\"@stll/anonymize-data/config/person-stopwords.json\");\n const set: ReadonlySet<string> = new Set(mod.default?.words ?? []);\n ctx.personStopwords = set;\n return set;\n } catch {\n const empty: ReadonlySet<string> = new Set();\n ctx.personStopwords = empty;\n return empty;\n }\n })();\n return ctx.personStopwordsPromise;\n};\n\nconst EMPTY_PERSON_STOPWORDS: ReadonlySet<string> = new Set();\n\n/** Sync accessor — returns empty set before init. */\nconst getPersonStopwords = (ctx: PipelineContext): ReadonlySet<string> =>\n ctx.personStopwords ?? EMPTY_PERSON_STOPWORDS;\n\nconst PERSON_CHAIN_BREAK_RE = /[!?;:]|,/u;\n\nconst isInitialContinuationGap = (text: string, gap: string): boolean =>\n (/^\\p{Lu}$/u.test(text) && /^\\.[^\\S\\n]{1,2}$/u.test(gap)) ||\n /^[^\\S\\n]{1,2}(?:\\p{Lu}\\.[^\\S\\n]{1,2})+$/u.test(gap);\n\n/**\n * Source tag for each pattern in the automaton.\n * \"deny-list\" = standard deny list entry\n * \"first-name\" = name corpus first name\n * \"surname\" = name corpus surname\n * \"title\" = academic/professional title\n */\ntype PatternSource = \"deny-list\" | \"first-name\" | \"surname\" | \"title\";\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: string[][];\n /** Maps pattern index → original pattern text. */\n originals: string[];\n /** Maps pattern index → source types (plural). */\n sources: PatternSource[][];\n};\n\n/**\n * Resolve which dictionaries to load based on country\n * and category filters, load them, and build the deny\n * list data. The returned data provides PatternEntry[]\n * for the unified builder and parallel arrays for\n * post-processing.\n *\n * Requires `@stll/anonymize-data` to be installed.\n * Returns null if the data package is not available.\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);\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 loadGenericRoles(ctx),\n ]);\n const dataModule = await loadDataModule();\n if (!dataModule) {\n return null;\n }\n\n const allowedCountries = resolveCountries(\n config.denyListRegions,\n config.denyListCountries,\n );\n\n const excluded = config.denyListExcludeCategories;\n const excludeCategories = excluded ? new Set(excluded) : new Set<string>();\n\n const allIds = [...dataModule.ALL_DICTIONARY_IDS];\n\n const ids = allIds.filter((id) => {\n const meta = dataModule.DICTIONARY_META[id];\n if (!meta) {\n return false;\n }\n\n if (!config.enableNameCorpus && meta.category === \"Names\") {\n return false;\n }\n\n if (excludeCategories.has(meta.category)) {\n return false;\n }\n\n if (allowedCountries === null) {\n return true;\n }\n\n if (meta.country === null) {\n return true;\n }\n\n return allowedCountries.has(meta.country);\n });\n\n const patternList: string[] = [];\n const labelList: string[][] = [];\n const sourceList: PatternSource[][] = [];\n // Maps lowercased pattern → index in patternList\n // for accumulating labels from multiple dictionaries\n const patternIndex = new Map<string, number>();\n\n const results = await Promise.all(\n ids.map(async (id) => {\n const entries = await dataModule.loadDictionary(id);\n return { id, entries };\n }),\n );\n\n const addDenyListEntry = (entry: string, label: string) => {\n // Strip | and \\ only — these caused the 12K FP\n // bug (| creates empty regex alternation, \\ is\n // an escape prefix). Other chars like () [] are\n // kept since they appear in real dictionary\n // entries and are matched literally by AC.\n const normalized = normalizeForSearch(entry).replace(/[|\\\\]/g, \"\");\n if (normalized.length === 0) {\n return;\n }\n const lower = normalized.toLowerCase();\n const existing = patternIndex.get(lower);\n if (existing !== undefined) {\n if (!labelList[existing]!.includes(label)) {\n labelList[existing]!.push(label);\n }\n if (!sourceList[existing]!.includes(\"deny-list\")) {\n sourceList[existing]!.push(\"deny-list\");\n }\n } else {\n patternIndex.set(lower, patternList.length);\n patternList.push(normalized);\n labelList.push([label]);\n sourceList.push([\"deny-list\"]);\n }\n };\n\n for (const { id, entries } of results) {\n const meta = dataModule.DICTIONARY_META[id];\n if (!meta) {\n continue;\n }\n for (const entry of entries) {\n addDenyListEntry(entry, meta.label);\n }\n }\n\n // Load city dictionaries dynamically for all\n // allowed countries. Cities cover 230 countries\n // via GeoNames and are loaded separately from\n // the static dictionary registry.\n if (!excludeCategories.has(\"Places\")) {\n const cityCountries =\n allowedCountries !== null\n ? [...allowedCountries]\n : // No country filter — load all. In practice\n // this would be massive, so limit to a\n // reasonable set of common legal jurisdictions.\n [\n \"AT\",\n \"AU\",\n \"BE\",\n \"BG\",\n \"BR\",\n \"CA\",\n \"CH\",\n \"CZ\",\n \"DE\",\n \"DK\",\n \"ES\",\n \"FI\",\n \"FR\",\n \"GB\",\n \"GR\",\n \"HR\",\n \"HU\",\n \"IE\",\n \"IT\",\n \"LU\",\n \"NL\",\n \"NO\",\n \"NZ\",\n \"PL\",\n \"PT\",\n \"RO\",\n \"SE\",\n \"SI\",\n \"SK\",\n \"US\",\n ];\n const cityEntries = await dataModule.loadCityDictionaries(cityCountries);\n for (const entry of cityEntries) {\n addDenyListEntry(entry, \"address\");\n }\n }\n\n // Add name corpus entries — accumulate labels\n // for entries that already exist from deny-list.\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 = normalizeForSearch(name).replace(/[|\\\\]/g, \"\");\n if (normalized.length === 0) {\n return;\n }\n const lower = normalized.toLowerCase();\n const existing = patternIndex.get(lower);\n if (existing !== undefined) {\n if (!labelList[existing]!.includes(\"person\")) {\n labelList[existing]!.push(\"person\");\n }\n if (!sourceList[existing]!.includes(source)) {\n sourceList[existing]!.push(source);\n }\n } else {\n patternIndex.set(lower, patternList.length);\n patternList.push(normalized);\n labelList.push([\"person\"]);\n sourceList.push([source]);\n }\n };\n\n if (config.enableNameCorpus && !excludeCategories.has(\"Names\")) {\n for (const name of getNameCorpusFirstNames(ctx)) {\n addNameEntry(name, \"first-name\");\n }\n for (const name of getNameCorpusSurnames(ctx)) {\n addNameEntry(name, \"surname\");\n }\n for (const title of getNameCorpusTitles(ctx)) {\n const norm = normalizeForSearch(title).replace(/[|\\\\]/g, \"\");\n if (norm.length === 0) continue;\n const lower = norm.toLowerCase();\n const existing = patternIndex.get(lower);\n if (existing !== undefined) {\n if (!sourceList[existing]!.includes(\"title\")) {\n sourceList[existing]!.push(\"title\");\n }\n } else {\n patternIndex.set(lower, patternList.length);\n patternList.push(norm);\n labelList.push([\"person\"]);\n sourceList.push([\"title\"]);\n }\n }\n }\n\n if (patternList.length === 0) {\n return null;\n }\n\n return {\n labels: labelList,\n originals: patternList,\n sources: sourceList,\n };\n};\n\ntype RawMatch = {\n start: number;\n end: number;\n /** All labels for this pattern (e.g., [\"person\", \"address\"]). */\n labels: string[];\n text: string;\n patternIdx: number;\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): 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);\n await Promise.all([\n loadStopwords(ctx),\n loadAllowList(ctx),\n loadPersonStopwords(ctx),\n loadGenericRoles(ctx),\n ]);\n};\n\n// ── Match processor ─────────────────────────────────\n\n/**\n * Process deny list matches from the unified search.\n * Receives all matches; filters to the deny list slice\n * via sliceStart/sliceEnd. Local index into data.labels,\n * data.originals, data.sources is match.pattern - sliceStart.\n *\n * Two-pass approach to reduce false positives:\n * 1. Collect all matches (case-insensitive,\n * whole-word via Rust automaton)\n * 2. Require uppercase start in source text\n * 3. For person names, require at least one\n * mid-sentence occurrence to prove proper noun\n * 4. Return all occurrences of validated terms\n */\nexport const processDenyListMatches = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText: string,\n data: DenyListData,\n ctx: PipelineContext = defaultContext,\n): Entity[] => {\n // Pass 1: collect valid matches grouped by pattern\n const matchesByPattern = new Map<number, RawMatch[]>();\n\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) {\n continue;\n }\n\n const localIdx = idx - sliceStart;\n\n const sourceChar = fullText[match.start] ?? \"\";\n if (!UPPER_START_RE.test(sourceChar)) {\n continue;\n }\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 keyword = matchText.toLowerCase();\n if (getStopwords(ctx).has(keyword) || getAllowList(ctx).has(keyword)) {\n continue;\n }\n\n // Skip ALL-CAPS tokens (likely acronyms, not PII)\n // unless surrounding context is also all-caps\n if (ALL_UPPER_RE.test(matchText)) {\n continue;\n }\n\n const labels = data.labels[localIdx];\n if (!labels || labels.length === 0) {\n continue;\n }\n\n const entry: RawMatch = {\n start: match.start,\n end: match.end,\n labels,\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 const hasPerson = first.labels.includes(\"person\");\n const nonPersonLabels = first.labels.filter((l) => l !== \"person\");\n\n // Person hits go to chain scoring (Pass 2b).\n // Skip words that are valid places/orgs but not\n // person names (months, states, languages).\n if (hasPerson) {\n const keyword = first.text.toLowerCase();\n if (!getPersonStopwords(ctx).has(keyword)) {\n for (const m of matches) {\n nameHits.push(m);\n }\n }\n }\n\n // Emit entities for all non-person labels\n for (const m of matches) {\n for (const label of nonPersonLabels) {\n results.push({\n start: m.start,\n end: m.end,\n label,\n text: m.text,\n score: 0.9,\n source: DETECTION_SOURCES.DENY_LIST,\n });\n }\n }\n }\n\n // Pass 2b: person hits — chain adjacent hits and\n // extend to following capitalised words.\n nameHits.sort((a, b) => a.start - b.start);\n\n const nameConsumed = new Set<number>();\n for (let i = 0; i < nameHits.length; i++) {\n if (nameConsumed.has(i)) {\n continue;\n }\n const hit = nameHits[i];\n if (!hit) {\n continue;\n }\n\n // Build chain of adjacent person hits\n const chain: RawMatch[] = [hit];\n let j = i + 1;\n\n while (j < nameHits.length && chain.length < 5) {\n const next = nameHits[j];\n if (!next) {\n break;\n }\n const prev = chain.at(-1);\n if (!prev) {\n break;\n }\n\n const gap = fullText.slice(prev.end, next.start);\n const breaksOnPeriod =\n gap.includes(\".\") && !isInitialContinuationGap(prev.text, gap);\n if (\n gap.length > 4 ||\n gap.length === 0 ||\n gap.includes(\"\\n\") ||\n gap.includes(\"\\t\") ||\n PERSON_CHAIN_BREAK_RE.test(gap) ||\n breaksOnPeriod\n ) {\n break;\n }\n\n chain.push(next);\n j++;\n }\n\n // Mark chain members consumed\n for (let k = i; k < i + chain.length; k++) {\n nameConsumed.add(k);\n }\n\n // Extend to following capitalised word (for\n // unknown surnames not in the corpus)\n const first = chain.at(0);\n const last = chain.at(-1);\n if (!first || !last) {\n continue;\n }\n\n const extended = extendPersonName(fullText, first.start, last.end, ctx);\n\n // Score: chained names get 0.9, single names 0.5\n const score = chain.length >= 2 ? 0.9 : 0.5;\n\n // Single-word deny-list matches are too noisy:\n // \"Rate\", \"Server\", \"Code\" etc. are surnames but\n // also common English words. Only accept single-\n // word matches when the next word is also uppercase\n // (likely a full name: \"Alena Zemanová\").\n if (chain.length === 1) {\n const afterEnd = last.end;\n const rest = fullText.slice(afterEnd).trimStart();\n const nextIsUpper = rest.length > 1 && /^\\p{Lu}\\p{Ll}/u.test(rest);\n if (!nextIsUpper) {\n continue;\n }\n }\n\n results.push({\n start: first.start,\n end: extended.end,\n label: \"person\",\n text: extended.text,\n score,\n source: DETECTION_SOURCES.DENY_LIST,\n });\n }\n\n // Post-process: extend city/address matches to\n // include adjacent trailing district numbers (e.g.,\n // \"Praha 1\", \"Brno 2\"). Czech and Slovak cities\n // commonly have numbered districts that are part of\n // the address.\n extendCityDistricts(results, fullText);\n\n return results;\n};\n\n/**\n * Extend address-label entities to absorb adjacent\n * integers: trailing district numbers (\"Praha 1\") and\n * leading postal codes (\"80336 München\").\n * Mutates the entities in place.\n */\n// District suffixes: digits (\"Praha 1\") or Roman\n// numerals (\"Příbram II\", \"Brno III\")\n// Valid Roman numerals only (I-XXX range, no invalid\n// combos like IC, LC, VC). Covers district suffixes\n// up to Praha XXX which is more than enough.\n// Roman numeral district suffixes II-XXX. Standalone\n// \"I\" and \"V\" excluded: \"V\" clashes with Czech\n// preposition \"in\"; \"I\" is too ambiguous.\nconst ROMAN_DISTRICT =\n \"XXX|XXIX|XXVIII|XXVII|XXVI|XXV|XXIV|XXIII\" +\n \"|XXII|XXI|XX|XIX|XVIII|XVII|XVI|XV|XIV|XIII\" +\n \"|XII|XI|X|IX|VIII|VII|VI|IV|III|II\";\nconst DISTRICT_SUFFIX_RE = new RegExp(\n `^ (\\\\d{1,2}(?!\\\\d)|(?:${ROMAN_DISTRICT}))` + `(?=[\\\\s,;.)\"\\\\n]|$)`,\n);\n// Postal code before city: \"163 00 \", \"16300 \",\n// \"16300 - \" (with dash separator).\nconst POSTAL_PREFIX_RE = new RegExp(\n `(?:\\\\d{5}|\\\\d{3}\\\\s\\\\d{2})\\\\s*${DASH}?\\\\s*$`,\n);\n\n// Words that must NOT be absorbed into an address span\n// when they follow a postal-code + city pattern. Party\n// roles, organizational nouns, and common legal terms.\nconst TRAILING_WORD_EXCLUSIONS: ReadonlySet<string> = new Set([\n // CZ/SK party roles\n \"nájemce\",\n \"pronajímatel\",\n \"kupující\",\n \"prodávající\",\n \"objednatel\",\n \"zhotovitel\",\n \"dodavatel\",\n \"odběratel\",\n \"věřitel\",\n \"dlužník\",\n \"zadavatel\",\n \"uchazeč\",\n \"příjemce\",\n \"plátce\",\n // Organizational nouns\n \"správa\",\n \"sekretariát\",\n \"kancelář\",\n \"odbor\",\n \"oddělení\",\n \"úřad\",\n \"inspekce\",\n \"agentura\",\n // Legal clause starters\n \"článek\",\n \"smlouva\",\n \"dodatek\",\n \"příloha\",\n \"předmět\",\n \"podmínky\",\n \"ustanovení\",\n]);\n\nconst extendCityDistricts = (entities: Entity[], fullText: string): void => {\n for (const entity of entities) {\n if (entity.label !== \"address\") {\n continue;\n }\n\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 = /^[\\s]*[-–][\\s]*(\\p{Lu}\\p{Ll}+)/u.exec(afterDistrict);\n if (dashDistrictM && !dashDistrictM[0].includes(\"\\n\")) {\n entity.end += dashDistrictM[0].length;\n entity.text = fullText.slice(entity.start, entity.end);\n }\n\n // Leading: \"80336 \" + \"München\" → \"80336 München\"\n // Absorbs 3-5 digit postal codes before the city.\n const beforeMatch = fullText.slice(\n Math.max(0, entity.start - 10),\n entity.start,\n );\n const prefixM = POSTAL_PREFIX_RE.exec(beforeMatch);\n if (prefixM) {\n entity.start -= prefixM[0].length;\n entity.text = fullText.slice(entity.start, entity.end);\n }\n\n // Trailing uppercase word: \"434 01\" + \" Most\" →\n // \"434 01 Most\". Absorb if the next word starts\n // with uppercase and is on the same line.\n // Guard: skip party-role or organizational terms.\n const afterExt = fullText.slice(entity.end);\n // Max 4 spaces gap — more means different column\n const trailingWordM = /^[\\s]{1,4}(\\p{Lu}\\p{Ll}+)/u.exec(afterExt);\n if (trailingWordM && !trailingWordM[0].includes(\"\\n\")) {\n const candidate = (trailingWordM[1] ?? \"\").toLowerCase();\n if (!TRAILING_WORD_EXCLUSIONS.has(candidate)) {\n entity.end += trailingWordM[0].length;\n entity.text = fullText.slice(entity.start, entity.end);\n }\n }\n }\n};\n\n/**\n * Extend a person name match to include subsequent\n * capitalized words. \"Pavel\" + \" Heřmánek\" → \"Pavel\n * Heřmánek\". Stops at lowercase words, punctuation,\n * or end of text. Also extends backward if preceded\n * by a capitalized word (for \"Miroslav Braňka\" when\n * only \"Braňka\" matched).\n */\nconst extendPersonName = (\n text: string,\n start: number,\n end: number,\n ctx: PipelineContext,\n): { end: number; text: string } => {\n let newEnd = end;\n\n // Extend forward: skip whitespace, check if next\n // word starts with uppercase\n let pos = newEnd;\n while (pos < text.length) {\n // Skip single whitespace\n if (pos < text.length && text[pos] === \" \") {\n const wordStart = pos + 1;\n if (wordStart >= text.length) {\n break;\n }\n\n const char = text[wordStart] ?? \"\";\n if (!UPPER_START_RE.test(char)) {\n break;\n }\n\n // Find end of this word\n let wordEnd = wordStart;\n while (wordEnd < text.length && !/\\s/.test(text[wordEnd] ?? \"\")) {\n wordEnd++;\n }\n\n // Skip trailing punctuation (commas, etc.)\n const word = text.slice(wordStart, wordEnd);\n const stripped = word.replace(/[,;.]+$/, \"\");\n if (stripped.length < 2) {\n break;\n }\n\n // Don't extend into global or person stopwords\n const lower = stripped.toLowerCase();\n if (getStopwords(ctx).has(lower) || getPersonStopwords(ctx).has(lower)) {\n break;\n }\n\n newEnd = wordStart + stripped.length;\n pos = newEnd;\n } else {\n break;\n }\n }\n\n return {\n end: newEnd,\n text: text.slice(start, newEnd),\n };\n};\n","/**\n * Address detection via seed expansion.\n *\n * 1. Find \"address seeds\" — high-confidence address\n * components (postal codes, cities, street types)\n * 2. Cluster nearby seeds (within ~150 chars)\n * 3. Expand each cluster to the full address span\n * 4. Score by seed diversity (more types = higher)\n *\n * Language-agnostic: street type words and boundary\n * words are loaded from data dictionaries, not\n * hardcoded per language.\n */\n\nimport type { Match } from \"@stll/text-search\";\n\nimport { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\n\n// ── Seed types ──────────────────────────────────────\n\ntype SeedType =\n | \"street-word\"\n | \"house-number\"\n | \"postal-code\"\n | \"city\"\n | \"address-trigger\";\n\ntype Seed = {\n type: SeedType;\n start: number;\n end: number;\n text: string;\n};\n\n// ── Dictionary loading ──────────────────────────────\n\ntype DictionaryConfig = Record<string, string[] | string>;\n\nlet cachedBoundaryRe: RegExp | null = null;\n\nconst loadBoundaryWords = async (): Promise<DictionaryConfig> => {\n try {\n const mod =\n await import(\"@stll/anonymize-data/config/address-boundaries.json\");\n return mod.default as DictionaryConfig;\n } catch {\n return {};\n }\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 cachedBoundaryRe =\n words.length > 0 ? new RegExp(`\\\\b(?:${words.join(\"|\")})\\\\b`, \"i\") : /(?!)/; // never matches\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 */\nexport const buildStreetTypePatterns = async (): Promise<string[]> => {\n let config: DictionaryConfig = {};\n try {\n const mod =\n await import(\"@stll/anonymize-data/config/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\n// ── Seed collection ─────────────────────────────────\n\nconst collectSeeds = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText: string,\n existingEntities: Entity[],\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 seeds.push({\n type: \"street-word\",\n start: match.start,\n end: match.end,\n text: match.text,\n });\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.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 const postalRe = /\\b(?:\\d{3}\\s\\d{2}|\\d{2}-\\d{3})\\b/g;\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 seeds.push({\n type: \"postal-code\",\n start,\n end,\n text: postalMatch[0],\n });\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(\"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 \"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\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 // 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 // Trim trailing punctuation\n while (rightPos > end && /[,;:\\s]/.test(fullText[rightPos - 1] ?? \"\")) {\n rightPos--;\n }\n\n return {\n start: Math.min(leftPos, start),\n end: Math.max(rightPos, end),\n };\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 seeds = collectSeeds(\n allMatches,\n sliceStart,\n sliceEnd,\n fullText,\n existingEntities,\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 text = fullText.slice(start, end).trim();\n\n // Skip very short or very long spans\n if (text.length < 5 || text.length > 300) {\n continue;\n }\n\n // Skip if it contains a newline (likely crossed\n // a structural boundary)\n if (text.includes(\"\\n\")) {\n continue;\n }\n\n results.push({\n start,\n end: start + text.length,\n label: \"address\",\n text,\n score,\n source: DETECTION_SOURCES.REGEX,\n });\n }\n\n return results;\n};\n","import { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\nimport { LEGAL_SUFFIXES } from \"../config/legal-forms\";\n\nconst TRAILING_SEP = /[,\\s]+$/;\nconst WORD_CHAR_RE = /[\\p{L}\\p{N}]/u;\nconst ORG_PROPAGATION_SCORE = 0.9;\n\ntype Seed = {\n baseName: string;\n label: string;\n};\n\n/**\n * After the main detection pass, collect organization\n * entities with a legal form suffix, strip the suffix\n * to get the base name, and re-scan the full text for\n * bare mentions of that base name. Returns new entities\n * for occurrences not already covered.\n */\nexport const propagateOrgNames = (\n entities: Entity[],\n fullText: string,\n): Entity[] => {\n const seeds: Seed[] = [];\n const seenBases = new Set<string>();\n\n for (const e of entities) {\n if (e.label !== \"organization\") continue;\n for (const suffix of LEGAL_SUFFIXES) {\n if (e.text.endsWith(suffix)) {\n const base = e.text\n .slice(0, -suffix.length)\n .replace(TRAILING_SEP, \"\")\n .trim();\n if (base.length >= 3 && !seenBases.has(base)) {\n seenBases.add(base);\n seeds.push({\n baseName: base,\n label: e.label,\n });\n }\n break;\n }\n }\n }\n\n if (seeds.length === 0) return [];\n\n // Build a mutable array of already-covered spans\n // for overlap checks. Updated as new entities are\n // emitted to prevent duplicate propagation.\n const covered: [number, number][] = entities.map((e) => [e.start, e.end]);\n const isOverlapping = (start: number, end: number): boolean =>\n covered.some(([cs, ce]) => start < ce && end > cs);\n\n const results: Entity[] = [];\n\n for (const seed of seeds) {\n const { baseName, label } = seed;\n let searchFrom = 0;\n while (searchFrom < fullText.length) {\n const idx = fullText.indexOf(baseName, searchFrom);\n if (idx === -1) break;\n\n const matchEnd = idx + baseName.length;\n\n // Word boundary: reject if preceded or followed\n // by a letter or digit (prevents substring\n // matches like \"ACME\" inside \"ACME2\").\n const prevCh = fullText[idx - 1] ?? \"\";\n const nextCh = fullText[matchEnd] ?? \"\";\n if (WORD_CHAR_RE.test(prevCh) || WORD_CHAR_RE.test(nextCh)) {\n searchFrom = idx + 1;\n continue;\n }\n\n // Skip if already covered by an existing entity\n // or a previously propagated result.\n if (!isOverlapping(idx, matchEnd)) {\n results.push({\n start: idx,\n end: matchEnd,\n label,\n text: baseName,\n score: ORG_PROPAGATION_SCORE,\n source: DETECTION_SOURCES.COREFERENCE,\n });\n covered.push([idx, matchEnd]);\n }\n\n searchFrom = matchEnd;\n }\n }\n\n return results;\n};\n","import type { Entity } from \"../types\";\n\n// Common Czech words that start uppercase at sentence\n// beginnings but are not street names. Module-level\n// to avoid allocation in a hot loop.\nconst BARE_STOPWORDS = new Set([\n \"Příloha\",\n \"Smlouva\",\n \"Článek\",\n \"Dodatek\",\n \"Celkem\",\n \"Strana\",\n \"Faktura\",\n \"Částka\",\n \"Položka\",\n \"Kapitola\",\n \"Zákon\",\n \"Vyhláška\",\n \"Nařízení\",\n \"Usnesení\",\n \"Rozsudek\",\n \"Bod\",\n \"Odstavec\",\n \"Záloha\",\n \"Zbývá\",\n \"Dne\",\n \"Platba\",\n \"Datum\",\n \"Splatnost\",\n \"Variabilní\",\n \"Konstantní\",\n \"Specifický\",\n]);\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 boosted.push({ ...entity, score: boostedScore });\n }\n }\n\n return boosted;\n};\n\n// ── Address backward scan ────────────────────────────\n\nconst UPPER_WORD_RE = /\\p{Lu}/u;\n\n/** Header zone: top 15% of document */\nconst HEADER_ZONE_FRACTION = 0.15;\n\n/** Context window for address adjacency */\nconst STREET_CONTEXT_WINDOW = 200;\n\n// ── Preposition data (lazy-loaded from JSON) ────────\n\ntype PrepositionData = {\n address: Record<string, string[] | string>;\n temporal: Record<string, string[] | string>;\n};\n\nlet _addressPreps: ReadonlySet<string> | null = null;\nlet _temporalPreps: ReadonlySet<string> | null = null;\nlet _prepsPromise: Promise<void> | null = null;\n\nconst loadPrepositions = async (): Promise<void> => {\n try {\n const mod =\n await import(\"@stll/anonymize-data/config/address-prepositions.json\");\n const data: PrepositionData = mod.default ?? mod;\n // Merge all languages into flat sets\n const addr = new Set<string>();\n const temp = new Set<string>();\n for (const words of Object.values(data.address)) {\n if (Array.isArray(words)) {\n for (const w of words) addr.add(w.toLowerCase());\n }\n }\n for (const words of Object.values(data.temporal)) {\n if (Array.isArray(words)) {\n for (const w of words) temp.add(w.toLowerCase());\n }\n }\n _addressPreps = addr;\n _temporalPreps = temp;\n } catch {\n _addressPreps = new Set();\n _temporalPreps = new Set();\n }\n};\n\n/** Ensure preposition data is loaded. */\nexport const initPrepositions = (): Promise<void> => {\n if (!_prepsPromise) {\n _prepsPromise = loadPrepositions();\n }\n return _prepsPromise;\n};\n\nconst getAddressPreps = (): ReadonlySet<string> => _addressPreps ?? new Set();\n\nconst getTemporalPreps = (): ReadonlySet<string> => _temporalPreps ?? new Set();\n\n// ── Street type abbreviations (lazy-loaded) ─────────\n\nlet _streetAbbrevs: ReadonlySet<string> | null = null;\nlet _streetAbbrevsPromise: Promise<void> | null = null;\n\nconst loadStreetAbbrevs = async (): Promise<void> => {\n try {\n const mod =\n await import(\"@stll/anonymize-data/config/address-street-types.json\");\n const data: Record<string, string[] | string> = mod.default ?? mod;\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 w of words) {\n // Only keep abbreviated forms (with dots)\n if (w.includes(\".\")) {\n abbrevs.add(w.toLowerCase());\n }\n }\n }\n _streetAbbrevs = abbrevs;\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\nconst getStreetAbbrevs = (): ReadonlySet<string> => _streetAbbrevs ?? new Set();\n\n/**\n * Scan backwards from known address entities and\n * house number patterns to find street names.\n *\n * Strategy (a): from a house number like \"2512/2a\",\n * walk left to find the first uppercase word — that's\n * the street name start. \"Mezi úvozy 2512/2a\" →\n * captures \"Mezi úvozy 2512/2a\".\n *\n * Strategy (b): if a colon \":\" appears within 3 chars\n * before the street start, boost confidence. Colons\n * signal \"label: value\" pairs universal in contracts.\n *\n * Strategy (c): in the header zone (top 15% of doc),\n * be more aggressive — detect street patterns even\n * without nearby address entities.\n */\nexport const detectStreetPatternsNearAddresses = (\n fullText: string,\n existingEntities: Entity[],\n): Entity[] => {\n const results: Entity[] = [];\n const addressEntities = existingEntities.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 const score = hasColon ? 0.95 : inHeader ? 0.85 : 0.8;\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\".\nconst ORPHAN_STREET_RE =\n /^\\s*(\\p{Lu}[\\p{Ll}\\p{Lu}]+(?:\\s+[\\p{Lu}\\p{Ll}][\\p{Ll}]+)*\\s+\\d{2,4}[a-zA-Z]?)\\s*$/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 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 = existingEntities.some(\n (e) => Math.abs(e.start - end) < 200 || Math.abs(e.end - start) < 200,\n );\n if (!hasContext) {\n continue;\n }\n\n results.push({\n start,\n end,\n label: \"address\",\n text: captured,\n score: 0.85,\n source: \"regex\",\n });\n }\n\n return results;\n};\n","import type { Entity } from \"../types\";\nimport type { PipelineContext } from \"../context\";\nimport { defaultContext } from \"../context\";\n\n// ── Zone types ───────────────────────────────────\n\nexport type DocumentZone = \"header\" | \"signature\" | \"body\" | \"table\";\n\nexport type ZoneSpan = {\n zone: DocumentZone;\n start: number;\n end: number;\n};\n\n// ── Score adjustments per zone ───────────────────\n\n/**\n * Additive score adjustments per document zone.\n * Header and signature blocks are dense with PII;\n * tables often contain structured identifying data.\n */\nexport const ZONE_SCORE_ADJUSTMENTS = {\n header: 0.1,\n signature: 0.15,\n body: 0,\n table: 0.05,\n} as const satisfies Record<DocumentZone, number>;\n\n// ── Lazy-loaded config ───────────────────────────\n\ntype SectionHeadingsConfig = {\n patterns: Array<{ re: string; flags: string }>;\n};\n\ntype SigningClauseConfig = {\n patterns: Array<{\n lang: string;\n prefix: string;\n suffix: string;\n prepositions: string[];\n }>;\n};\n\nconst loadSectionHeadings = async (): Promise<RegExp[]> => {\n const mod = await import(\"@stll/anonymize-data/config/section-headings.json\");\n const data: SectionHeadingsConfig = mod.default ?? mod;\n return data.patterns.map((p) => new RegExp(p.re, p.flags));\n};\n\nconst loadSigningClauses = async (): Promise<RegExp[]> => {\n const mod = await import(\"@stll/anonymize-data/config/signing-clauses.json\");\n const data: SigningClauseConfig = mod.default ?? mod;\n return data.patterns.map((p) => {\n // Build a pattern that matches the signing\n // clause prefix at the start of a line.\n // Note: prefix/suffix are regex fragments by\n // design (e.g. `(?:V|Ve)\\\\s+`), not literals.\n const prefix = p.prefix || \"\";\n const suffix = p.suffix || \"\";\n // Include prepositions so multi-word place\n // names like \"Ústí nad Labem\" are matched,\n // consistent with buildSigningClausePatterns\n // in detectors/regex.ts.\n const prepAlt = p.prepositions.length > 0 ? p.prepositions.join(\"|\") : null;\n const place = prepAlt\n ? `\\\\p{Lu}\\\\p{Ll}+` +\n `(?:\\\\s+(?:${prepAlt})` +\n `\\\\s+\\\\p{Lu}\\\\p{Ll}+)*` +\n `(?:\\\\s+\\\\p{Lu}\\\\p{Ll}+)*`\n : `\\\\p{Lu}\\\\p{Ll}+` + `(?:[- ]\\\\p{Lu}\\\\p{Ll}+)*`;\n // Anchor to the start of the line. Each line is\n // already split on \\n, so ^ suffices without m.\n const combined = `^\\\\s*(?:${prefix}${place}${suffix})`;\n return new RegExp(combined, \"u\");\n });\n};\n\n/**\n * Ensure config data is loaded. Call once before\n * classifyZones. Safe to call multiple times.\n */\nexport const initZoneClassifier = (\n ctx: PipelineContext = defaultContext,\n): Promise<void> => {\n if (ctx.zoneInitPromise) return ctx.zoneInitPromise;\n ctx.zoneInitPromise = Promise.all([\n loadSectionHeadings(),\n loadSigningClauses(),\n ])\n .then(([headings, clauses]) => {\n ctx.zoneHeadingPatterns = headings;\n ctx.zoneSigningPatterns = clauses;\n })\n .catch((err: unknown) => {\n // Clear cached promise so a subsequent call\n // can retry after a transient failure.\n ctx.zoneInitPromise = null;\n throw err;\n });\n return ctx.zoneInitPromise;\n};\n\n// ── Table detection ──────────────────────────────\n\nconst MIN_TABS_FOR_TABLE = 2;\n\nconst isTableLine = (line: string): boolean => {\n let tabCount = 0;\n for (const ch of line) {\n if (ch === \"\\t\") tabCount++;\n if (tabCount >= MIN_TABS_FOR_TABLE) return true;\n }\n return false;\n};\n\n// ── Zone classification ──────────────────────────\n\n/**\n * Classify a document into zones based on\n * structural heuristics. Zones are non-overlapping\n * and cover the entire text.\n *\n * Must call `initZoneClassifier()` first.\n */\nexport const classifyZones = (\n fullText: string,\n ctx: PipelineContext = defaultContext,\n): ZoneSpan[] => {\n if (fullText.length === 0) return [];\n\n const headingRes = ctx.zoneHeadingPatterns;\n const signingRes = ctx.zoneSigningPatterns;\n\n if (!headingRes || !signingRes) {\n console.warn(\n \"[anonymize] classifyZones called before \" +\n \"initZoneClassifier(); returning body-only\",\n );\n return [{ zone: \"body\", start: 0, end: fullText.length }];\n }\n\n const lines = fullText.split(\"\\n\");\n const zones: ZoneSpan[] = [];\n\n // Find header end: first section heading line\n let headerEndLine = -1;\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n if (line === undefined) continue;\n for (const re of headingRes) {\n if (re.test(line)) {\n headerEndLine = i;\n break;\n }\n }\n if (headerEndLine !== -1) break;\n }\n\n // Find signature start: last signing clause line\n let signatureStartLine = -1;\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i];\n if (line === undefined) continue;\n for (const re of signingRes) {\n if (re.test(line)) {\n signatureStartLine = i;\n break;\n }\n }\n if (signatureStartLine !== -1) break;\n }\n\n // Build offset map: line index -> char offset\n const lineOffsets: number[] = [];\n let offset = 0;\n for (const line of lines) {\n lineOffsets.push(offset);\n // +1 for the newline character\n offset += line.length + 1;\n }\n\n // Determine zone boundaries\n let headerEndOffset =\n headerEndLine >= 0 ? (lineOffsets[headerEndLine] ?? 0) : 0;\n\n const signatureStartOffset =\n signatureStartLine >= 0\n ? (lineOffsets[signatureStartLine] ?? fullText.length)\n : fullText.length;\n\n // Guard: if the signing clause appears before\n // the first section heading, the zones would\n // overlap. Treat as degenerate layout: drop the\n // header so the signature zone takes priority.\n if (\n headerEndLine > 0 &&\n signatureStartLine >= 0 &&\n headerEndOffset > signatureStartOffset\n ) {\n headerEndLine = -1;\n headerEndOffset = 0;\n }\n\n // Add header zone if detected. Using > 0 (not\n // >= 0) intentionally: when the section heading is\n // on line 0, there is no preamble to classify as a\n // header — everything starts as body.\n if (headerEndLine > 0) {\n zones.push({\n zone: \"header\",\n start: 0,\n end: headerEndOffset,\n });\n }\n\n // Scan body region for table zones\n const bodyStart = headerEndLine > 0 ? headerEndOffset : 0;\n const bodyEnd =\n signatureStartLine >= 0 ? signatureStartOffset : fullText.length;\n\n let tableStart = -1;\n for (\n let i = Math.max(headerEndLine, 0);\n i < (signatureStartLine >= 0 ? signatureStartLine : lines.length);\n i++\n ) {\n const line = lines[i];\n if (line === undefined) continue;\n const lineStart = lineOffsets[i] ?? 0;\n const lineEnd = lineStart + line.length;\n\n if (isTableLine(line)) {\n if (tableStart === -1) {\n tableStart = lineStart;\n }\n } else if (tableStart !== -1) {\n zones.push({\n zone: \"table\",\n start: tableStart,\n end: lineStart,\n });\n tableStart = -1;\n }\n\n // Close table at the end of body range\n if (\n i ===\n (signatureStartLine >= 0 ? signatureStartLine - 1 : lines.length - 1) &&\n tableStart !== -1\n ) {\n zones.push({\n zone: \"table\",\n start: tableStart,\n end: Math.min(lineEnd + 1, bodyEnd),\n });\n tableStart = -1;\n }\n }\n\n // Fill body gaps between header/table/signature\n const sortedSpecial = zones.toSorted((a, b) => a.start - b.start);\n\n let cursor = bodyStart;\n const bodyZones: ZoneSpan[] = [];\n for (const span of sortedSpecial) {\n if (span.zone === \"header\") continue;\n if (span.start > cursor) {\n bodyZones.push({\n zone: \"body\",\n start: cursor,\n end: span.start,\n });\n }\n cursor = Math.max(cursor, span.end);\n }\n if (cursor < bodyEnd) {\n bodyZones.push({\n zone: \"body\",\n start: cursor,\n end: bodyEnd,\n });\n }\n\n for (const z of bodyZones) zones.push(z);\n\n // Add signature zone if detected\n if (signatureStartLine >= 0) {\n zones.push({\n zone: \"signature\",\n start: signatureStartOffset,\n end: fullText.length,\n });\n }\n\n return zones.toSorted((a, b) => a.start - b.start);\n};\n\n// ── Entity zone lookup ───────────────────────────\n\n/**\n * Find which zone an entity's midpoint falls in.\n * Returns \"body\" if no zone matches (defensive).\n */\nconst findZone = (midpoint: number, zones: ZoneSpan[]): DocumentZone => {\n for (const span of zones) {\n if (midpoint >= span.start && midpoint < span.end) {\n return span.zone;\n }\n }\n return \"body\";\n};\n\n/**\n * Apply zone-based score adjustments to entities.\n * Entities in header/signature/table zones get a\n * small additive boost reflecting the higher PII\n * density in those regions.\n *\n * Returns a new array; does not mutate inputs.\n */\nexport const applyZoneAdjustments = (\n entities: Entity[],\n zones: ZoneSpan[],\n): Entity[] => {\n if (zones.length === 0) {\n return entities.map((e) => ({ ...e }));\n }\n\n const result: Entity[] = [];\n for (const entity of entities) {\n const midpoint = (entity.start + entity.end) / 2;\n const zone = findZone(midpoint, zones);\n const adjustment = ZONE_SCORE_ADJUSTMENTS[zone];\n\n if (adjustment > 0) {\n result.push({\n ...entity,\n score: Math.min(1, entity.score + adjustment),\n });\n } else {\n result.push({ ...entity });\n }\n }\n return result;\n};\n","import type { Match, PatternEntry } from \"@stll/text-search\";\n\nimport { getTextSearch } from \"../search-engine\";\nimport type { Entity } from \"../types\";\n\n// ── Types ───────────────────────────────────────────\n\nexport type HotwordRule = {\n hotwords: string[];\n targetLabels: string[];\n scoreAdjustment: number;\n reclassifyTo?: string;\n proximityBefore: number;\n proximityAfter: number;\n};\n\ntype HotwordRulesConfig = {\n rules: HotwordRule[];\n};\n\n// ── Lazy-loaded state ───────────────────────────────\n\nlet rules: HotwordRule[] | null = null;\nlet search: { findIter: (text: string) => Match[] } | null = null;\n/**\n * Maps each TextSearch pattern index back to the\n * rule index that owns it, so a single AC scan\n * resolves all hotword hits to their rule.\n */\nlet patternToRule: number[] | null = null;\nlet initPromise: Promise<void> | null = null;\n\n// ── Init ────────────────────────────────────────────\n\nconst loadRules = async (): Promise<void> => {\n const mod = await import(\"@stll/anonymize-data/config/hotword-rules.json\");\n const data: HotwordRulesConfig = mod.default ?? mod;\n const loaded = data.rules;\n\n // Build a flat pattern list and the reverse map.\n const patterns: PatternEntry[] = [];\n const mapping: number[] = [];\n\n for (let ruleIdx = 0; ruleIdx < loaded.length; ruleIdx++) {\n const rule = loaded[ruleIdx];\n if (!rule) continue;\n for (const hw of rule.hotwords) {\n patterns.push({\n pattern: hw,\n literal: true,\n caseInsensitive: true,\n });\n mapping.push(ruleIdx);\n }\n }\n\n const builtSearch =\n patterns.length > 0\n ? new (getTextSearch())(patterns, {\n overlapStrategy: \"all\",\n caseInsensitive: true,\n wholeWords: true,\n })\n : null;\n\n // Assign all module state atomically after all\n // failable operations succeed, so a mid-flight\n // error cannot leave a half-initialized state.\n patternToRule = mapping;\n search = builtSearch;\n rules = loaded;\n};\n\n/**\n * Load hotword rules from the data package.\n * Safe to call multiple times; subsequent calls\n * are no-ops.\n */\nexport const initHotwordRules = async (): Promise<void> => {\n if (rules !== null) return;\n if (initPromise !== null) return initPromise;\n initPromise = loadRules().catch((err) => {\n // Reset so callers can retry on transient failure.\n initPromise = null;\n throw err;\n });\n return initPromise;\n};\n\n/**\n * Expand requested output labels with any source labels\n * that hotword rules may reclassify into them.\n *\n * Example: requesting only \"date of birth\" still needs\n * \"date\" candidates to survive until the hotword pass.\n * If rules are not initialized, or if no labels were\n * requested, returns the input labels unchanged.\n */\nexport const expandLabelsForHotwordRules = (\n requestedLabels: readonly string[],\n): readonly string[] => {\n if (rules === null || requestedLabels.length === 0) {\n return requestedLabels;\n }\n\n const requested = new Set(requestedLabels);\n const expanded = new Set(requestedLabels);\n\n for (const rule of rules) {\n if (rule.reclassifyTo === undefined || !requested.has(rule.reclassifyTo)) {\n continue;\n }\n for (const label of rule.targetLabels) {\n expanded.add(label);\n }\n }\n\n return [...expanded];\n};\n\n// ── Application ─────────────────────────────────────\n\n/**\n * Apply hotword context rules to detected entities.\n *\n * Scans `fullText` once with a single AC automaton\n * for all hotwords across all rules, then checks\n * proximity to each entity. Distance-decayed\n * adjustment: closer hotwords give a stronger boost.\n *\n * Returns a new array; input entities are not mutated.\n */\nexport const applyHotwordRules = (\n entities: Entity[],\n fullText: string,\n): Entity[] => {\n if (\n rules === null ||\n rules.length === 0 ||\n search === null ||\n patternToRule === null\n ) {\n return entities;\n }\n\n // Single scan for all hotword positions.\n const hits = search.findIter(fullText);\n if (hits.length === 0) return entities;\n\n // Group hits by rule index for fast lookup.\n const hitsByRule = new Map<number, Match[]>();\n for (const hit of hits) {\n const ruleIdx = patternToRule[hit.pattern];\n if (ruleIdx === undefined) continue;\n let bucket = hitsByRule.get(ruleIdx);\n if (bucket === undefined) {\n bucket = [];\n hitsByRule.set(ruleIdx, bucket);\n }\n bucket.push(hit);\n }\n\n const result: Entity[] = [];\n\n for (const entity of entities) {\n let bestAdjustment = 0;\n let bestReclassify: string | undefined;\n\n for (let ruleIdx = 0; ruleIdx < rules.length; ruleIdx++) {\n const rule = rules[ruleIdx];\n if (!rule) continue;\n\n // Check if entity label matches this rule.\n if (!rule.targetLabels.includes(entity.label)) {\n continue;\n }\n\n const ruleHits = hitsByRule.get(ruleIdx);\n if (ruleHits === undefined) continue;\n\n for (const hit of ruleHits) {\n // Asymmetric proximity check.\n // Hotword BEFORE entity: hotword end <=\n // entity start, distance = entity.start -\n // hit.end\n // Hotword AFTER entity: hotword start >=\n // entity end, distance = hit.start -\n // entity.end\n let distance: number;\n let maxDistance: number;\n\n if (hit.end <= entity.start) {\n // Hotword is before the entity.\n distance = entity.start - hit.end;\n maxDistance = rule.proximityBefore;\n } else if (hit.start >= entity.end) {\n // Hotword is after the entity.\n distance = hit.start - entity.end;\n maxDistance = rule.proximityAfter;\n } else {\n // Hotword overlaps the entity: distance 0,\n // use larger window for max.\n distance = 0;\n maxDistance = Math.max(rule.proximityBefore, rule.proximityAfter);\n }\n\n if (distance > maxDistance) continue;\n\n // Distance-decayed adjustment.\n const decay = maxDistance === 0 ? 1 : 1 - distance / maxDistance;\n const adj = rule.scoreAdjustment * decay;\n\n if (Math.abs(adj) > Math.abs(bestAdjustment)) {\n bestAdjustment = adj;\n // Only carry reclassification from boost\n // rules; a penalty winner must not silently\n // overwrite a label set by a closer positive\n // rule.\n if (adj > 0) {\n bestReclassify = rule.reclassifyTo;\n } else {\n bestReclassify = undefined;\n }\n }\n }\n }\n\n if (bestAdjustment === 0) {\n result.push(entity);\n continue;\n }\n\n const newScore = Math.min(1, Math.max(0, entity.score + bestAdjustment));\n const newLabel =\n bestReclassify !== undefined ? bestReclassify : entity.label;\n\n result.push({\n ...entity,\n score: newScore,\n label: newLabel,\n });\n }\n\n return result;\n};\n","import type { Entity } from \"../types\";\n\n/** Max gap (in chars) between entities to merge. */\nconst MAX_GAP = 3;\n\n/**\n * Characters allowed in the gap between two adjacent\n * same-label entities that should be merged: spaces,\n * tabs, commas, and hyphens. Uses `[ \\t,\\-]` instead\n * of `\\s` to avoid merging entities across newlines.\n */\nconst GAP_PATTERN = /^[ \\t,\\-]+$/;\n\n/**\n * Build a set of word boundary offsets for the full\n * text using `Intl.Segmenter`. Returns a sorted array\n * of offsets where words start and end.\n */\nconst buildWordBoundaries = (text: string): Set<number> => {\n // Use \"und\" (undetermined) locale for consistent\n // word-boundary results across environments.\n const segmenter = new Intl.Segmenter(\"und\", {\n granularity: \"word\",\n });\n const boundaries = new Set<number>();\n for (const seg of segmenter.segment(text)) {\n if (!seg.isWordLike) continue;\n boundaries.add(seg.index);\n boundaries.add(seg.index + seg.segment.length);\n }\n return boundaries;\n};\n\n/**\n * Characters that act as hard stops when scanning\n * backward for a word boundary. Entity boundaries\n * should never extend past these.\n */\nconst WORD_START_STOPS = new Set([\n \"\\n\",\n \"\\r\",\n \",\",\n \";\",\n \"(\",\n \")\",\n \"[\",\n \"]\",\n \"&\",\n]);\n\n/**\n * Find the word-start offset at or before `pos`.\n * Scans left until a word boundary is found.\n */\nconst wordStartAt = (\n pos: number,\n boundaries: Set<number>,\n text: string,\n): number => {\n let p = pos;\n while (p > 0 && !boundaries.has(p)) {\n const prev = text[p - 1];\n if (prev !== undefined && WORD_START_STOPS.has(prev)) {\n return p;\n }\n p--;\n }\n return p;\n};\n\n/**\n * Characters that act as hard stops when scanning\n * forward for a word boundary. Entity boundaries\n * should never extend past these.\n */\nconst WORD_END_STOPS = new Set([\n \"\\n\",\n \"\\r\",\n \",\",\n \";\",\n \".\",\n \"(\",\n \")\",\n \"[\",\n \"]\",\n \"&\",\n]);\n\n/**\n * Find the word-end offset at or after `pos`.\n * Scans right until a word boundary is found.\n */\nconst wordEndAt = (\n pos: number,\n boundaries: Set<number>,\n text: string,\n): number => {\n let p = pos;\n while (p < text.length && !boundaries.has(p)) {\n const ch = text[p];\n if (ch !== undefined && WORD_END_STOPS.has(ch)) {\n return p;\n }\n p++;\n }\n return p;\n};\n\n/**\n * Binary search: find the leftmost index in `arr`\n * where `arr[index].start >= value`.\n */\nconst lowerBound = (arr: Entity[], value: number): number => {\n let lo = 0;\n let hi = arr.length;\n while (lo < hi) {\n const mid = (lo + hi) >>> 1;\n const el = arr[mid];\n if (el && el.start < value) {\n lo = mid + 1;\n } else {\n hi = mid;\n }\n }\n return lo;\n};\n\n/**\n * Merge adjacent same-label entities separated only by\n * whitespace, comma, or hyphen (max 3 chars). Also\n * merges same-label entities that partially overlap\n * (which can happen after word-boundary expansion).\n *\n * Looks for the last same-label entity in the result\n * (not just the very last entity) so that an\n * intervening different-label entity does not prevent\n * merging.\n *\n * Uses binary search for the `gapOccupied` check and\n * a Map for O(1) same-label prev lookup. O(n log n).\n */\nconst mergeAdjacent = (entities: Entity[], fullText: string): Entity[] => {\n const sorted = entities.toSorted((a, b) => a.start - b.start);\n const result: Entity[] = [];\n // O(1) lookup for the last same-label entity in\n // result, replacing the O(n) backward scan.\n const lastByLabel = new Map<string, Entity>();\n\n for (const entity of sorted) {\n const prev = lastByLabel.get(entity.label);\n\n if (!prev) {\n const copy = { ...entity };\n result.push(copy);\n lastByLabel.set(entity.label, copy);\n continue;\n }\n\n // Handle overlap created by fixPartialWords:\n // two same-label entities may now partially overlap\n // after word-boundary expansion.\n if (entity.start < prev.end) {\n prev.end = Math.max(prev.end, entity.end);\n prev.text = fullText.slice(prev.start, prev.end);\n prev.score = Math.max(prev.score, entity.score);\n continue;\n }\n\n const gap = fullText.slice(prev.end, entity.start);\n // GAP_PATTERN uses `+` quantifier, so empty gaps\n // (zero-gap / touching entities) won't match.\n // Also reject merging when a different-label entity\n // occupies the gap range (would create cross-label\n // overlap). Use binary search to find candidates\n // in the gap range instead of scanning all entities.\n const gapStart = prev.end;\n const gapEnd = entity.start;\n const searchIdx = lowerBound(sorted, gapStart);\n let gapOccupied = false;\n for (let k = searchIdx; k < sorted.length; k++) {\n const other = sorted[k];\n if (!other || other.start >= gapEnd) break;\n if (other.label !== entity.label && other.end > gapStart) {\n gapOccupied = true;\n break;\n }\n }\n\n if (!gapOccupied && gap.length <= MAX_GAP && GAP_PATTERN.test(gap)) {\n // Merge into prev\n prev.end = entity.end;\n prev.text = fullText.slice(prev.start, prev.end);\n prev.score = Math.max(prev.score, entity.score);\n } else {\n const copy = { ...entity };\n result.push(copy);\n lastByLabel.set(entity.label, copy);\n }\n }\n\n return result;\n};\n\n/**\n * Fix partial-word boundaries by extending entity\n * start/end to the nearest word boundary. Does not\n * extend across newlines or into spans occupied by\n * different-label entities.\n *\n * Uses binary search to skip irrelevant entries when\n * clamping at cross-label neighbors. O(n log n) in\n * the common case; O(n^2) worst case when many\n * same-label entities precede a cross-label boundary.\n */\nconst fixPartialWords = (entities: Entity[], fullText: string): Entity[] => {\n const boundaries = buildWordBoundaries(fullText);\n const sorted = entities.toSorted((a, b) => a.start - b.start);\n\n // Build a secondary array sorted by end position\n // for efficient \"nearest entity ending before me\"\n // lookups. Each entry tracks the original entity.\n const byEnd = sorted\n .map((e, idx) => ({ entity: e, idx }))\n .sort((a, b) => a.entity.end - b.entity.end);\n const endPositions = byEnd.map((x) => x.entity.end);\n\n return sorted.map((e, eIdx) => {\n let newStart = wordStartAt(e.start, boundaries, fullText);\n let newEnd = wordEndAt(e.end, boundaries, fullText);\n\n // Clamp start: find different-label entities whose\n // end is in (newStart, e.start]. We search byEnd\n // for entities with end > newStart and end <= e.start.\n // Binary search for the first entry with\n // end > newStart.\n let lo = 0;\n let hi = endPositions.length;\n while (lo < hi) {\n const mid = (lo + hi) >>> 1;\n if ((endPositions[mid] ?? Number.POSITIVE_INFINITY) <= newStart) {\n lo = mid + 1;\n } else {\n hi = mid;\n }\n }\n // Scan forward from lo; all entries have end >\n // newStart. Stop when end > e.start.\n for (let k = lo; k < byEnd.length; k++) {\n const entry = byEnd[k];\n if (!entry || entry.entity.end > e.start) break;\n if (entry.idx === eIdx) continue;\n if (entry.entity.label === e.label) continue;\n // This entity's end is in (newStart, e.start]\n // and has a different label: clamp.\n newStart = Math.max(newStart, entry.entity.end);\n }\n\n // Clamp end: find different-label entities whose\n // start is in [e.end, newEnd). Use the start-sorted\n // array with binary search.\n const startIdx = lowerBound(sorted, e.end);\n for (let k = startIdx; k < sorted.length; k++) {\n const other = sorted[k];\n if (!other || other.start >= newEnd) break;\n if (other === e) continue;\n if (other.label === e.label) continue;\n // This entity's start is in [e.end, newEnd)\n // and has a different label: clamp.\n newEnd = Math.min(newEnd, other.start);\n }\n\n if (newStart === e.start && newEnd === e.end) {\n return e;\n }\n return {\n ...e,\n start: newStart,\n end: newEnd,\n text: fullText.slice(newStart, newEnd),\n };\n });\n};\n\n/**\n * Deduplicate entities with identical [start, end, label].\n * Keeps the entry with the highest score.\n */\nconst deduplicateSpans = (entities: Entity[]): Entity[] => {\n const seen = new Map<string, Entity>();\n for (const entity of entities) {\n const key = `${entity.start}:${entity.end}:${entity.label}`;\n const existing = seen.get(key);\n if (!existing || entity.score > existing.score) {\n seen.set(key, entity);\n }\n }\n return [...seen.values()];\n};\n\n/**\n * Remove nested same-label entities. If a shorter\n * entity is fully contained within a longer entity\n * of the same label, drop the shorter one.\n *\n * Uses a \"max end seen\" sweep per label. O(n) after\n * the initial sort.\n */\nconst removeNestedSameLabel = (entities: Entity[]): Entity[] => {\n // Sort by start asc, then by length desc so the\n // longer entity comes first.\n const sorted = entities.toSorted((a, b) => {\n if (a.start !== b.start) return a.start - b.start;\n return b.end - a.end;\n });\n\n const result: Entity[] = [];\n // Track the furthest end seen per label. Any entity\n // whose end <= maxEnd for its label is nested inside\n // a previously seen entity of the same label.\n const maxEndByLabel = new Map<string, number>();\n\n for (const entity of sorted) {\n const maxEnd = maxEndByLabel.get(entity.label);\n if (maxEnd !== undefined && entity.end <= maxEnd) {\n // Nested inside a same-label entity: skip.\n continue;\n }\n maxEndByLabel.set(entity.label, entity.end);\n result.push(entity);\n }\n\n return result;\n};\n\n/**\n * Resolve cross-label overlaps that can arise when\n * `fixPartialWords` independently expands two\n * different-label entities toward the same word\n * boundary. The entity with the higher score (or\n * longer span on tie) keeps its boundary; the other\n * is trimmed so the overlap disappears.\n *\n * Preserved existing structure: sorted + early break\n * already gives good amortized behavior. O(n^2)\n * worst case but rare in practice.\n */\nconst resolveCrossLabelOverlaps = (\n entities: Entity[],\n fullText: string,\n): Entity[] => {\n const sorted = entities\n .map((e) => ({ ...e }))\n .sort((a, b) => a.start - b.start);\n\n for (let i = 0; i < sorted.length; i++) {\n for (let j = i + 1; j < sorted.length; j++) {\n const a = sorted[i];\n const b = sorted[j];\n if (!a || !b) continue;\n if (b.start >= a.end) break; // no overlap\n if (a.label === b.label) continue;\n\n // Skip full containment (one entity fully\n // inside another). Cross-label nesting is\n // valid and should be preserved.\n const aContainsB = a.start <= b.start && a.end >= b.end;\n const bContainsA = b.start <= a.start && b.end >= a.end;\n if (aContainsB || bContainsA) continue;\n\n // Partial overlap. Higher score wins; on tie\n // the longer span wins.\n const aLen = a.end - a.start;\n const bLen = b.end - b.start;\n const aWins = a.score > b.score || (a.score === b.score && aLen >= bLen);\n\n if (aWins) {\n // Trim b's start to a's end\n b.start = a.end;\n b.text = fullText.slice(b.start, b.end);\n } else {\n // Trim a's end to b's start. Because the\n // array is sorted by start and a.end can\n // only decrease, all remaining j will have\n // b.start >= a.end so the break fires\n // immediately.\n a.end = b.start;\n a.text = fullText.slice(a.start, a.end);\n }\n }\n }\n\n // Drop any entity that was trimmed to zero width.\n return sorted.filter((e) => e.start < e.end);\n};\n\n/**\n * Post-processing pass for entity boundary consistency.\n * Runs after mergeAndDedup, before false-positive\n * filtering.\n *\n * 1. Fix partial-word boundaries (respects cross-label\n * neighbors to avoid introducing new overlaps)\n * 2. Resolve any remaining cross-label overlaps\n * 3. Deduplicate identical [start, end, label] spans\n * 4. Merge adjacent same-label entities (catches any\n * new adjacency/overlap from step 1)\n * 5. Remove nested same-label entities\n */\nexport const enforceBoundaryConsistency = (\n entities: Entity[],\n fullText: string,\n): Entity[] => {\n const fixed = fixPartialWords(entities, fullText);\n const resolved = resolveCrossLabelOverlaps(fixed, fullText);\n const deduped = deduplicateSpans(resolved);\n const merged = mergeAdjacent(deduped, fullText);\n return removeNestedSameLabel(merged);\n};\n","/**\n * Build the unified search instances from all\n * detector pattern sources.\n *\n * Two TextSearch instances (not one) to avoid\n * 200K per-pattern object allocations:\n * 1. regex + triggers + legal-forms (mixed, ~140\n * patterns, caseInsensitive for trigger AC)\n * 2. deny-list + street-types + gazetteer\n * (caseInsensitive, overlap \"all\";\n * deny-list/street-type use per-pattern\n * wholeWords: true; gazetteer exact use\n * wholeWords: false; gazetteer fuzzy use\n * distance: 2 via @stll/fuzzy-search)\n *\n * All patterns are PatternEntry objects with\n * per-pattern literal/wholeWords settings.\n */\n\nimport type { PatternEntry, TextSearch } from \"@stll/text-search\";\n\nimport { getTextSearch } from \"./search-engine\";\n\nimport {\n isLegalFormsEnabled,\n type GazetteerEntry,\n type PipelineConfig,\n} from \"./types\";\nimport type { RegexMeta } from \"./detectors/regex\";\nimport type { TriggerRule } from \"./types\";\nimport type { DenyListData } from \"./detectors/deny-list\";\nimport type { PipelineContext } from \"./context\";\nimport { defaultContext } from \"./context\";\n\nimport {\n REGEX_PATTERNS,\n REGEX_META,\n getCurrencyPatterns,\n CURRENCY_PATTERN_META,\n getDatePatterns,\n DATE_PATTERN_META,\n getSigningClausePatterns,\n SIGNING_CLAUSE_META,\n} from \"./detectors/regex\";\nimport { buildLegalFormPatterns } from \"./detectors/legal-forms\";\nimport { buildTriggerPatterns } from \"./detectors/triggers\";\nimport { buildDenyList } from \"./detectors/deny-list\";\nimport { buildStreetTypePatterns } from \"./detectors/address-seeds\";\nimport { buildGazetteerPatterns } from \"./detectors/gazetteer\";\n\ntype PatternSlice = {\n start: number;\n end: number;\n};\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 /** Deny-list + street-types + gazetteer. */\n tsLiterals: TextSearch;\n slices: {\n regex: PatternSlice;\n legalForms: PatternSlice;\n triggers: PatternSlice;\n denyList: PatternSlice;\n streetTypes: PatternSlice;\n gazetteer: PatternSlice;\n };\n regexMeta: readonly RegexMeta[];\n triggerRules: readonly TriggerRule[];\n denyListData: DenyListData | null;\n gazetteerData: GazetteerData | null;\n};\n\nexport const buildUnifiedSearch = async (\n config: PipelineConfig,\n gazetteerEntries: GazetteerEntry[] = [],\n ctx: PipelineContext = defaultContext,\n): Promise<UnifiedSearchInstance> => {\n const legalFormsEnabled = isLegalFormsEnabled(config);\n const [\n legalForms,\n triggers,\n denyListData,\n streetTypes,\n currencyPatterns,\n datePatterns,\n signingPatterns,\n ] = await Promise.all([\n legalFormsEnabled\n ? buildLegalFormPatterns()\n : Promise.resolve([] as string[]),\n config.enableTriggerPhrases\n ? buildTriggerPatterns()\n : Promise.resolve({\n patterns: [] as string[],\n rules: [] as TriggerRule[],\n }),\n config.enableDenyList ? buildDenyList(config, ctx) : Promise.resolve(null),\n buildStreetTypePatterns(),\n getCurrencyPatterns(),\n getDatePatterns(),\n getSigningClausePatterns(),\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 = [\n ...(REGEX_PATTERNS as string[]),\n ...currencyPatterns,\n ...datePatterns,\n ...signingPatterns,\n ];\n const regexMeta: RegexMeta[] = [\n ...REGEX_META,\n ...currencyPatterns.map(() => CURRENCY_PATTERN_META),\n ...datePatterns.map(() => DATE_PATTERN_META),\n ...signingPatterns.map(() => SIGNING_CLAUSE_META),\n ];\n\n let offset = 0;\n\n const regexSlice = {\n start: offset,\n end: offset + allRegex.length,\n };\n offset = regexSlice.end;\n\n const legalFormsSlice = {\n start: offset,\n end: offset + legalForms.length,\n };\n offset = legalFormsSlice.end;\n\n const triggersSlice = {\n start: offset,\n end: offset + triggers.patterns.length,\n };\n\n // Trigger patterns need caseInsensitive on AC\n // (only ~120 objects, not 200K). Regex/legal-form\n // patterns are bare strings (auto-classified).\n const triggerEntries = triggers.patterns.map((p) => ({\n pattern: p,\n literal: true as const,\n caseInsensitive: true,\n }));\n\n const regexAllPatterns = [...allRegex, ...legalForms, ...triggerEntries];\n\n // TextSearch auto-detects DFA state explosion\n // (build time > 2ms) and falls back to individual\n // engines. No manual maxAlternations needed.\n const tsRegex = new (getTextSearch())(regexAllPatterns);\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 // 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): PatternEntry => ({\n pattern: s,\n literal: true as const,\n wholeWords: true,\n });\n const literalAllPatterns: PatternEntry[] = [\n ...denyListOriginals.map(wrapWholeWord),\n ...streetTypes.map(wrapWholeWord),\n ...(gazResult?.patterns ?? []),\n ];\n\n const tsLiterals =\n literalAllPatterns.length > 0\n ? new (getTextSearch())(literalAllPatterns, {\n caseInsensitive: true,\n overlapStrategy: \"all\",\n })\n : new (getTextSearch())([]);\n\n return {\n tsRegex,\n tsLiterals,\n slices: {\n regex: regexSlice,\n legalForms: legalFormsSlice,\n triggers: triggersSlice,\n denyList: denyListSlice,\n streetTypes: streetTypesSlice,\n gazetteer: gazetteerSlice,\n },\n regexMeta,\n triggerRules: triggers.rules,\n denyListData,\n gazetteerData: gazResult?.data ?? null,\n };\n};\n","/**\n * Run the two-instance unified search and return\n * raw matches. Two passes instead of six:\n * 1. regex + triggers + legal-forms\n * 2. deny-list + street-types (normalized text)\n */\n\nimport type { Match } from \"@stll/text-search\";\nimport type { UnifiedSearchInstance } from \"./build-unified-search\";\nimport { normalizeForSearch } from \"./util/normalize\";\n\nexport type UnifiedResult = {\n /** All matches from both instances combined. */\n regexMatches: Match[];\n literalMatches: Match[];\n};\n\nexport const runUnifiedSearch = (\n instance: UnifiedSearchInstance,\n fullText: string,\n): UnifiedResult => {\n // Pass 1: regex + triggers + legal-forms\n // on original text (regex patterns encode\n // their own case flags)\n const regexMatches = instance.tsRegex.findIter(fullText);\n\n // Pass 2: deny-list + street-types on\n // normalized text (NBSP, smart quotes folded)\n const normalized = normalizeForSearch(fullText);\n const literalMatches = instance.tsLiterals.findIter(normalized);\n\n return { regexMatches, literalMatches };\n};\n","import type { Entity } from \"../types\";\n\nconst MASK_TOKEN = \"[MASKED]\";\nconst MASK_LEN = MASK_TOKEN.length;\n\ntype OffsetSegment = {\n /** Start of this mask token in masked text */\n maskedStart: number;\n /** End of this mask token in masked text */\n maskedEnd: number;\n /** Cumulative shift: original - masked */\n shift: number;\n /** Original start of the masked span */\n origStart: number;\n /** Original end of the masked span */\n origEnd: number;\n};\n\nexport type MaskResult = {\n maskedText: string;\n /**\n * Maps masked-text offsets back to original offsets.\n * Returns null if the span overlaps a masked region.\n */\n offsetMap: (\n maskedStart: number,\n maskedEnd: number,\n ) => { start: number; end: number } | null;\n};\n\n/**\n * Replace detected entity spans with placeholder tokens.\n * Each entity span is replaced with \"[MASKED]\" (fixed\n * length). Returns the masked text and an offset mapping\n * function.\n */\nexport const maskDetectedSpans = (\n fullText: string,\n entities: Entity[],\n): MaskResult => {\n if (entities.length === 0) {\n return {\n maskedText: fullText,\n offsetMap: (s, e) => ({ start: s, end: e }),\n };\n }\n\n // Sort by start, then longest span first\n const sorted = entities.toSorted(\n (a, b) => a.start - b.start || b.end - a.end,\n );\n\n // Merge overlapping spans so we don't double-mask.\n // Adjacent (touching) spans are kept separate so each\n // produces its own [MASKED] token, preserving token\n // boundaries for the NER model.\n const spans: { start: number; end: number }[] = [];\n const first = sorted[0];\n if (!first) {\n return {\n maskedText: fullText,\n offsetMap: (s, e) => ({ start: s, end: e }),\n };\n }\n let cur = {\n start: first.start,\n end: first.end,\n };\n for (let i = 1; i < sorted.length; i++) {\n const s = sorted[i];\n if (!s) continue;\n if (s.start < cur.end) {\n cur.end = Math.max(cur.end, s.end);\n } else {\n spans.push(cur);\n cur = { start: s.start, end: s.end };\n }\n }\n spans.push(cur);\n\n // Build masked text and offset segments\n const segments: OffsetSegment[] = [];\n const parts: string[] = [];\n let prev = 0;\n let cumulativeShift = 0;\n\n for (const span of spans) {\n parts.push(fullText.slice(prev, span.start));\n parts.push(MASK_TOKEN);\n\n const origLen = span.end - span.start;\n const delta = origLen - MASK_LEN;\n cumulativeShift += delta;\n\n // Position of mask token in masked text\n const maskedStart = span.start - (cumulativeShift - delta);\n const maskedEnd = maskedStart + MASK_LEN;\n\n segments.push({\n maskedStart,\n maskedEnd,\n shift: cumulativeShift,\n origStart: span.start,\n origEnd: span.end,\n });\n\n prev = span.end;\n }\n parts.push(fullText.slice(prev));\n\n const maskedText = parts.join(\"\");\n\n const offsetMap = (\n maskedStart: number,\n maskedEnd: number,\n ): { start: number; end: number } | null => {\n // Check if span overlaps any masked region\n for (const seg of segments) {\n if (maskedStart < seg.maskedEnd && maskedEnd > seg.maskedStart) {\n return null;\n }\n }\n\n // Find cumulative shift for this position.\n // Since the overlap check above guarantees both\n // endpoints fall within the same gap zone, a\n // single shift value applies to both.\n let shift = 0;\n for (const seg of segments) {\n if (maskedStart >= seg.maskedEnd) {\n shift = seg.shift;\n } else {\n break;\n }\n }\n\n return {\n start: maskedStart + shift,\n end: maskedEnd + shift,\n };\n };\n\n return { maskedText, offsetMap };\n};\n\n/**\n * Map NER entities from masked-text offsets back to\n * original-text offsets. Discards any NER entity whose\n * mapped span overlaps a masked (rule-detected) region.\n */\nexport const unmaskNerEntities = (\n nerEntities: Entity[],\n maskResult: MaskResult,\n fullText: string,\n): Entity[] => {\n const result: Entity[] = [];\n\n for (const ner of nerEntities) {\n // offsetMap returns null when the mapped span\n // overlaps any merged rule-entity region, so no\n // secondary overlap check is needed.\n const mapped = maskResult.offsetMap(ner.start, ner.end);\n if (mapped === null) continue;\n\n result.push({\n ...ner,\n start: mapped.start,\n end: mapped.end,\n text: fullText.slice(mapped.start, mapped.end),\n });\n }\n\n return result;\n};\n","import {\n extractDefinedTerms,\n findCoreferenceSpans,\n} from \"./detectors/coreference\";\nimport { processGazetteerMatches } from \"./detectors/gazetteer\";\nimport { detectNameCorpus, initNameCorpus } from \"./detectors/names\";\nimport { processRegexMatches } from \"./detectors/regex\";\nimport { processLegalFormMatches } from \"./detectors/legal-forms\";\nimport { processTriggerMatches } from \"./detectors/triggers\";\nimport {\n ensureDenyListData,\n processDenyListMatches,\n} from \"./detectors/deny-list\";\nimport { processAddressSeeds } from \"./detectors/address-seeds\";\nimport { propagateOrgNames } from \"./detectors/org-propagation\";\nimport {\n boostNearMissEntities,\n detectStreetPatternsNearAddresses,\n detectOrphanStreetLines,\n initPrepositions,\n initStreetAbbrevs,\n} from \"./filters/confidence-boost\";\nimport {\n filterFalsePositives,\n loadGenericRoles,\n} from \"./filters/false-positives\";\nimport {\n applyZoneAdjustments,\n classifyZones,\n initZoneClassifier,\n type ZoneSpan,\n} from \"./filters/zone-classifier\";\nimport {\n applyHotwordRules,\n expandLabelsForHotwordRules,\n initHotwordRules,\n} from \"./filters/hotword-rules\";\nimport { enforceBoundaryConsistency } from \"./filters/boundary-consistency\";\nimport type { Entity, GazetteerEntry, PipelineConfig } from \"./types\";\nimport {\n DEFAULT_ENTITY_LABELS,\n DETECTOR_PRIORITY,\n isLegalFormsEnabled,\n} from \"./types\";\nimport {\n buildUnifiedSearch,\n type UnifiedSearchInstance,\n} from \"./build-unified-search\";\nimport { runUnifiedSearch } from \"./unified-search\";\nimport { maskDetectedSpans, unmaskNerEntities } from \"./util/entity-masking\";\nimport type { PipelineContext } from \"./context\";\nimport { defaultContext } from \"./context\";\n\n/**\n * Sources backed by curated literal dictionaries.\n * Longer matches from these sources are more specific,\n * so the containment rule trusts their length.\n */\nconst LITERAL_SOURCES: ReadonlySet<string> = new Set([\n \"deny-list\",\n \"gazetteer\",\n]);\n\nconst shouldReplace = (a: Entity, b: Entity): boolean => {\n const aLen = a.end - a.start;\n const bLen = b.end - b.start;\n // Containment: when a literal-match entity (deny-list\n // or gazetteer) fully contains a shorter entity with\n // the same label, prefer the longer one. Curated\n // dictionary entries are more specific when longer:\n // \"656 91 Brno\" (deny-list) should beat \"656 91\"\n // (regex) even though regex has higher priority.\n // Non-literal sources (trigger, regex, NER) are\n // excluded because their length does not reliably\n // indicate accuracy.\n if (\n a.label === b.label &&\n LITERAL_SOURCES.has(a.source) &&\n a.start <= b.start &&\n a.end >= b.end &&\n aLen > bLen\n ) {\n return true;\n }\n if (\n a.label === b.label &&\n LITERAL_SOURCES.has(b.source) &&\n b.start <= a.start &&\n b.end >= a.end &&\n bLen > aLen\n ) {\n return false;\n }\n const aPri = DETECTOR_PRIORITY[a.source] ?? 0;\n const bPri = DETECTOR_PRIORITY[b.source] ?? 0;\n if (aPri !== bPri) return aPri > bPri;\n return a.score > b.score || (a.score === b.score && aLen > bLen);\n};\n\n/** Labels where colons are structurally significant. */\nconst COLON_LABELS = new Set([\"ip address\", \"mac address\"]);\n\n/** Strip leading/trailing whitespace and punctuation. */\nexport const sanitizeEntities = (entities: Entity[]): Entity[] =>\n entities.flatMap((e) => {\n const strip = COLON_LABELS.has(e.label) ? /[\\s,;]+/ : /[\\s:,;]+/;\n // Also strip leading dots followed by whitespace —\n // artifact from trigger extraction after abbreviations\n // like \"dat. nar.\" or \"č.p.\" where the extraction\n // starts at the trailing dot of the abbreviation.\n const leadTrimmed = e.text\n .replace(/^(?:\\.\\s)+/, \"\")\n .replace(new RegExp(`^${strip.source}`, strip.flags), \"\");\n const lead = e.text.length - leadTrimmed.length;\n const cleaned = leadTrimmed.replace(\n new RegExp(`${strip.source}$`, strip.flags),\n \"\",\n );\n if (cleaned.length === 0) return [];\n // Reject entities with no alphanumeric content\n if (!/[\\p{L}\\p{N}]/u.test(cleaned)) return [];\n // Collapse internal whitespace runs (address entities\n // spanning multiple lines in structured documents)\n const collapsed = cleaned.replace(/\\s*\\n\\s*/g, \" \").replace(/\\s{2,}/g, \" \");\n if (collapsed === e.text) return [e];\n return [\n {\n ...e,\n start: e.start + lead,\n end: e.start + lead + collapsed.length,\n text: collapsed,\n },\n ];\n });\n\nexport const mergeAndDedup = (...layers: Entity[][]): Entity[] => {\n const all: Entity[] = [];\n for (const layer of layers) {\n for (const entity of layer) {\n all.push(entity);\n }\n }\n if (all.length === 0) return [];\n\n const sorted = all.toSorted((a, b) => a.start - b.start);\n\n // Single-pass sweep-line: since entities are sorted\n // by start, a new entity can only overlap the tail\n // of merged[] (all earlier entries end before it).\n const first = sorted[0];\n if (!first) return [];\n const merged: Entity[] = [{ ...first }];\n\n for (let i = 1; i < sorted.length; i++) {\n const entity = sorted[i];\n const last = merged[merged.length - 1];\n if (!entity || !last) continue;\n\n if (last.end <= entity.start) {\n // No overlap: append.\n merged.push({ ...entity });\n } else if (shouldReplace(entity, last)) {\n // Overlap: new entity wins.\n merged[merged.length - 1] = { ...entity };\n }\n // else: overlap but existing wins; discard entity.\n }\n\n return sanitizeEntities(merged);\n};\n\nexport type NerInferenceFn = (\n fullText: string,\n labels: string[],\n threshold: number,\n signal?: AbortSignal,\n) => Promise<Entity[]>;\n\n/**\n * Extend monetary amount entities to include a trailing\n * \"(slovy ...)\" or \"(slovně ...)\" parenthetical that\n * spells out the amount in words. Common in Czech legal\n * documents, e.g. \"1 529,-Kč (slovy jeden-tisíc)\".\n *\n * Keywords are loaded from amount-words.json config so\n * new languages can be added without code changes.\n */\ntype AmountWordsConfig = {\n patterns: { lang: string; keywords: string[] }[];\n};\n\nconst escapeRegex = (s: string): string =>\n s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\nlet amountWordsRe: RegExp | null = null;\nlet amountWordsLoaded = false;\n\nconst getAmountWordsRe = async (): Promise<RegExp> => {\n if (amountWordsLoaded && amountWordsRe) {\n return amountWordsRe;\n }\n try {\n const mod = await import(\"@stll/anonymize-data/config/amount-words.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON module shape\n const data = (mod as { default: AmountWordsConfig }).default;\n const keywords = data.patterns.flatMap((p) => p.keywords);\n const alt = keywords.map(escapeRegex).join(\"|\");\n amountWordsRe = new RegExp(\n `^[,;]?[^\\\\S\\\\n]*(\\\\((?:${alt})[:\\\\s][^)\\\\n]{1,120}\\\\))`,\n \"i\",\n );\n } catch {\n // Fallback: original Czech-only pattern\n amountWordsRe = /^[,;]?[^\\S\\n]*(\\((?:slovy|slovně)[:\\s][^)\\n]{1,120}\\))/i;\n }\n amountWordsLoaded = true;\n return amountWordsRe;\n};\n\nconst extendMonetaryAmountWords = (\n entities: Entity[],\n fullText: string,\n re: RegExp,\n): Entity[] =>\n entities.map((e) => {\n if (e.label !== \"monetary amount\") return e;\n const after = fullText.slice(e.end);\n const m = re.exec(after);\n if (!m) return e;\n const newEnd = e.end + m[0].length;\n return {\n ...e,\n end: newEnd,\n text: fullText.slice(e.start, newEnd),\n };\n });\n\ntype AllowedLabelSet = ReadonlySet<string> | null;\n\nconst createAllowedLabelSetFromLabels = (\n labels: readonly string[],\n): AllowedLabelSet => (labels.length > 0 ? new Set(labels) : null);\n\nconst createAllowedLabelSet = (config: PipelineConfig): AllowedLabelSet =>\n createAllowedLabelSetFromLabels(config.labels);\n\nconst filterAllowedLabels = (\n entities: Entity[],\n allowedLabels: AllowedLabelSet,\n): Entity[] => {\n if (!allowedLabels) {\n return entities;\n }\n return entities.filter((e) => allowedLabels.has(e.label));\n};\n\nconst labelIsAllowed = (\n label: string,\n allowedLabels: AllowedLabelSet,\n): boolean => !allowedLabels || allowedLabels.has(label);\n\nconst getRequestedNerLabels = (\n config: PipelineConfig,\n expandForHotwords = false,\n): readonly string[] => {\n const labels =\n config.labels.length > 0 ? config.labels : DEFAULT_ENTITY_LABELS;\n return expandForHotwords ? expandLabelsForHotwordRules(labels) : labels;\n};\n\nconst checkAbort = (signal?: AbortSignal): void => {\n if (signal?.aborted) {\n throw new DOMException(\"Pipeline aborted\", \"AbortError\");\n }\n};\n\nconst configKey = (\n config: PipelineConfig,\n gazetteerEntries: GazetteerEntry[],\n): string => {\n const legalFormsEnabled = isLegalFormsEnabled(config);\n // Gazetteer fingerprint: sorted entry IDs,\n // canonical forms, labels, and variants.\n // Skip when gazetteer is disabled to avoid\n // unnecessary cache misses.\n const gazFingerprint =\n config.enableGazetteer && gazetteerEntries.length > 0\n ? gazetteerEntries\n .map(\n (e) =>\n `${e.id}:${e.canonical}:${e.label}:${[...e.variants].sort().join(\",\")}`,\n )\n .toSorted()\n .join(\";\")\n : \"\";\n return (\n `${config.enableDenyList}:` +\n `${config.enableTriggerPhrases}:` +\n `${legalFormsEnabled}:` +\n `${config.enableNameCorpus}:` +\n `${config.denyListCountries?.toSorted().join(\",\") ?? \"\"}:` +\n `${config.denyListRegions?.toSorted().join(\",\") ?? \"\"}:` +\n `${config.denyListExcludeCategories?.toSorted().join(\",\") ?? \"\"}:` +\n `${config.enableGazetteer}:${gazFingerprint}`\n );\n};\n\n/**\n * Get or build a cached search instance. Cache state\n * lives on the provided PipelineContext, not at module\n * level.\n */\nconst getCachedSearch = async (\n config: PipelineConfig,\n gazetteerEntries: GazetteerEntry[],\n ctx: PipelineContext,\n): Promise<UnifiedSearchInstance> => {\n const key = configKey(config, gazetteerEntries);\n if (ctx.search && ctx.searchKey === key) {\n return ctx.search;\n }\n if (ctx.searchPromise && ctx.searchKey === key) {\n return ctx.searchPromise;\n }\n // Build new search. Null the cached instance first\n // so concurrent callers don't use stale data while\n // the new build is in flight.\n ctx.search = null;\n ctx.searchKey = key;\n const promise = buildUnifiedSearch(config, gazetteerEntries, ctx);\n ctx.searchPromise = promise;\n const result = await promise;\n // Guard: another call may have replaced the key\n // while we were awaiting. Only cache if still ours.\n if (ctx.searchKey === key) {\n ctx.search = result;\n }\n return result;\n};\n\n/**\n * Options for {@link runPipeline}.\n *\n * @property cachedSearch Pre-built search instance.\n * When provided, `config` and `gazetteerEntries`\n * are not used for building; the caller must\n * ensure the instance matches both parameters.\n */\nexport type PipelineOptions = {\n fullText: string;\n config: PipelineConfig;\n gazetteerEntries: GazetteerEntry[];\n nerInference?: NerInferenceFn | null;\n onProgress?: (step: string, detail: string) => void;\n cachedSearch?: UnifiedSearchInstance;\n signal?: AbortSignal;\n context?: PipelineContext;\n};\n\n/**\n * Run the full detection pipeline.\n *\n * Two TextSearch instances scan the text (regex +\n * literals). Results are dispatched to each\n * detector's post-processor by pattern index range.\n *\n * Pass an AbortSignal to cancel the pipeline between\n * stages. Throws a DOMException with name \"AbortError\"\n * when cancelled.\n *\n * Pass an optional `context` to isolate cached state\n * from other pipeline runs. If omitted, a module-level\n * default context is used (backward compatible).\n */\nexport const runPipeline = async (\n options: PipelineOptions,\n): Promise<Entity[]> => {\n const {\n fullText,\n config,\n gazetteerEntries,\n nerInference = null,\n onProgress,\n cachedSearch,\n signal,\n context,\n } = options;\n const ctx = context ?? defaultContext;\n const allowedLabels = createAllowedLabelSet(config);\n const legalFormsEnabled = isLegalFormsEnabled(config);\n\n const log = (step: string, detail: string) => {\n onProgress?.(step, detail);\n };\n\n checkAbort(signal);\n\n // Ensure generic-roles data, zone config, hotword\n // rules, and prepositions are loaded before the\n // pipeline runs. All are no-ops after the first call.\n let zoneInitOk = false;\n const enableHotwords = config.enableHotwordRules === true;\n let hotwordInitOk = false;\n const hotwordInit = enableHotwords\n ? initHotwordRules()\n .then(() => {\n hotwordInitOk = true;\n })\n .catch((err: unknown) => {\n log(\"hotwords\", \"init failed; skipping\");\n console.warn(\"[anonymize] hotword rules init failed\", err);\n })\n : Promise.resolve();\n if (config.enableZoneClassification) {\n const zoneInit = initZoneClassifier(ctx)\n .then(() => {\n zoneInitOk = true;\n })\n .catch((err: unknown) => {\n log(\"zones\", \"init failed; skipping\");\n console.warn(\"[anonymize] zone classifier init failed\", err);\n });\n await Promise.all([\n loadGenericRoles(ctx),\n initPrepositions(),\n initStreetAbbrevs(),\n zoneInit,\n hotwordInit,\n ]);\n } else {\n await Promise.all([\n loadGenericRoles(ctx),\n initPrepositions(),\n initStreetAbbrevs(),\n hotwordInit,\n ]);\n }\n\n // When a pre-built search is provided, buildDenyList\n // was skipped for this context. Ensure stopwords,\n // allow list, and person stopwords are loaded so\n // processDenyListMatches filters correctly.\n if (cachedSearch && config.enableDenyList) {\n await ensureDenyListData(ctx);\n }\n\n // Classify document zones once up front\n let zones: ZoneSpan[] = [];\n if (config.enableZoneClassification && zoneInitOk) {\n zones = classifyZones(fullText, ctx);\n if (zones.length > 0) {\n const zoneNames = [...new Set(zones.map((z) => z.zone))];\n log(\"zones\", zoneNames.join(\", \"));\n }\n }\n\n checkAbort(signal);\n\n const hotwordsActive = enableHotwords && hotwordInitOk;\n const preHotwordAllowedLabels = hotwordsActive\n ? createAllowedLabelSetFromLabels(\n expandLabelsForHotwordRules(config.labels),\n )\n : allowedLabels;\n\n const search =\n cachedSearch ?? (await getCachedSearch(config, gazetteerEntries, ctx));\n\n checkAbort(signal);\n\n // Two-pass scan (regex + literals)\n const { regexMatches, literalMatches } = runUnifiedSearch(search, fullText);\n const { slices } = search;\n\n const rawRegexEntities = config.enableRegex\n ? processRegexMatches(\n regexMatches,\n slices.regex.start,\n slices.regex.end,\n search.regexMeta,\n )\n : [];\n const regexEntities = filterAllowedLabels(\n rawRegexEntities,\n preHotwordAllowedLabels,\n );\n if (regexEntities.length > 0) log(\"regex\", `${regexEntities.length} matches`);\n\n const rawLegalFormEntities = legalFormsEnabled\n ? processLegalFormMatches(\n regexMatches,\n slices.legalForms.start,\n slices.legalForms.end,\n fullText,\n )\n : [];\n const legalFormEntities = filterAllowedLabels(\n rawLegalFormEntities,\n preHotwordAllowedLabels,\n );\n if (legalFormEntities.length > 0)\n log(\"legal-forms\", `${legalFormEntities.length} matches`);\n\n const rawTriggerEntities = config.enableTriggerPhrases\n ? processTriggerMatches(\n regexMatches,\n slices.triggers.start,\n slices.triggers.end,\n fullText,\n search.triggerRules,\n )\n : [];\n const triggerEntities = filterAllowedLabels(\n rawTriggerEntities,\n preHotwordAllowedLabels,\n );\n if (triggerEntities.length > 0)\n log(\"trigger-phrases\", `${triggerEntities.length} matches`);\n\n checkAbort(signal);\n\n let rawNameCorpusEntities: Entity[] = [];\n let nameCorpusEntities: Entity[] = [];\n if (config.enableNameCorpus && !config.enableDenyList) {\n await initNameCorpus(ctx);\n checkAbort(signal);\n rawNameCorpusEntities = detectNameCorpus(fullText, ctx);\n nameCorpusEntities = filterAllowedLabels(\n rawNameCorpusEntities,\n preHotwordAllowedLabels,\n );\n log(\"name-corpus\", `${nameCorpusEntities.length} matches`);\n }\n\n const rawDenyListEntities =\n config.enableDenyList && search.denyListData\n ? processDenyListMatches(\n literalMatches,\n slices.denyList.start,\n slices.denyList.end,\n fullText,\n search.denyListData,\n ctx,\n )\n : [];\n const denyListEntities = filterAllowedLabels(\n rawDenyListEntities,\n preHotwordAllowedLabels,\n );\n if (denyListEntities.length > 0)\n log(\"deny-list\", `${denyListEntities.length} matches`);\n\n // Gazetteer: unified into tsLiterals\n const rawGazetteerEntities =\n config.enableGazetteer && search.gazetteerData\n ? processGazetteerMatches(\n literalMatches,\n slices.gazetteer.start,\n slices.gazetteer.end,\n fullText,\n search.gazetteerData,\n )\n : [];\n const gazetteerEntities = filterAllowedLabels(\n rawGazetteerEntities,\n preHotwordAllowedLabels,\n );\n if (gazetteerEntities.length > 0)\n log(\"gazetteer\", `${gazetteerEntities.length} matches`);\n\n checkAbort(signal);\n\n const ruleContextEntities = [\n ...rawTriggerEntities,\n ...rawRegexEntities,\n ...rawLegalFormEntities,\n ...rawNameCorpusEntities,\n ...rawDenyListEntities,\n ...rawGazetteerEntities,\n ];\n\n // NER (mask rule-detected spans so the model doesn't\n // produce contradictory boundaries for known entities)\n let rawNerEntities: Entity[] = [];\n let nerEntities: Entity[] = [];\n if (config.enableNer && nerInference) {\n const maskResult = maskDetectedSpans(fullText, ruleContextEntities);\n log(\"ner\", \"running inference...\");\n const rawNer = await nerInference(\n maskResult.maskedText,\n [...getRequestedNerLabels(config, hotwordsActive)],\n config.threshold,\n signal,\n );\n rawNerEntities = unmaskNerEntities(rawNer, maskResult, fullText);\n nerEntities = filterAllowedLabels(rawNerEntities, preHotwordAllowedLabels);\n const masked = rawNer.length - rawNerEntities.length;\n const labelFiltered = rawNerEntities.length - nerEntities.length;\n log(\n \"ner\",\n `${nerEntities.length} entities` +\n (masked > 0 ? ` (${masked} masked)` : \"\") +\n (labelFiltered > 0 ? ` (${labelFiltered} label-filtered)` : \"\"),\n );\n }\n\n checkAbort(signal);\n\n // Address seed expansion\n const preAddressEntities = [\n ...triggerEntities,\n ...regexEntities,\n ...legalFormEntities,\n ...nameCorpusEntities,\n ...denyListEntities,\n ...gazetteerEntities,\n ...nerEntities,\n ];\n const addressSeedEntities = labelIsAllowed(\"address\", allowedLabels)\n ? await processAddressSeeds(\n literalMatches,\n slices.streetTypes.start,\n slices.streetTypes.end,\n fullText,\n [...ruleContextEntities, ...rawNerEntities],\n )\n : [];\n if (addressSeedEntities.length > 0)\n log(\"address-seeds\", `${addressSeedEntities.length} expanded`);\n\n checkAbort(signal);\n\n // Zone-based score adjustment: apply before\n // threshold filtering so entities in PII-dense zones\n // can cross the threshold.\n const zoneAdjusted = applyZoneAdjustments(\n [...preAddressEntities, ...addressSeedEntities],\n zones,\n );\n\n // Hotword context rules: boost or reclassify\n // entities near relevant keywords. Applied after\n // zone adjustments so both effects stack.\n const preBoostEntities = hotwordsActive\n ? filterAllowedLabels(\n applyHotwordRules(zoneAdjusted, fullText),\n allowedLabels,\n )\n : zoneAdjusted;\n\n // Confidence boost + threshold filter\n let allEntities: Entity[];\n if (config.enableConfidenceBoost) {\n allEntities = boostNearMissEntities(preBoostEntities, config.threshold);\n const boosted =\n allEntities.length -\n preBoostEntities.filter((e) => e.score >= config.threshold).length;\n if (boosted > 0) log(\"confidence-boost\", `${boosted} near-miss promoted`);\n } else {\n allEntities = preBoostEntities.filter((e) => e.score >= config.threshold);\n }\n\n // Street patterns near existing addresses\n // (e.g. \"Ostrovni 225/1\" near \"110 00 Praha 1\")\n const streetPatterns = labelIsAllowed(\"address\", allowedLabels)\n ? detectStreetPatternsNearAddresses(fullText, allEntities)\n : [];\n if (streetPatterns.length > 0) {\n allEntities = [...allEntities, ...streetPatterns];\n log(\n \"street-context\",\n `${streetPatterns.length} street patterns near addresses`,\n );\n }\n\n // Orphan street lines in header zone\n const orphanStreets = labelIsAllowed(\"address\", allowedLabels)\n ? detectOrphanStreetLines(fullText, allEntities)\n : [];\n if (orphanStreets.length > 0) {\n allEntities = [...allEntities, ...orphanStreets];\n log(\"orphan-streets\", `${orphanStreets.length} header street lines`);\n }\n\n // Merge + dedup\n const rawMerged = mergeAndDedup(allEntities);\n log(\"merge\", `${rawMerged.length} after dedup`);\n\n // Extend monetary amounts to include trailing\n // \"(slovy ...)\" or \"(slovně ...)\" parentheticals.\n // Runs after dedup so each monetary span is unique,\n // preventing duplicate extensions from clobbering\n // unrelated entities between e.end and newEnd.\n const amountWordsRe = await getAmountWordsRe();\n const mergedExtended = extendMonetaryAmountWords(\n rawMerged,\n fullText,\n amountWordsRe,\n );\n\n // Boundary consistency (merge adjacent, fix partial\n // words, remove nested same-label)\n const consistent = enforceBoundaryConsistency(mergedExtended, fullText);\n if (consistent.length < mergedExtended.length)\n log(\n \"boundary\",\n `${mergedExtended.length - consistent.length} consolidated`,\n );\n\n // Organization name propagation: strip legal form\n // suffixes from detected orgs and re-scan for bare\n // mentions of the base name. Gated by enableCoreference\n // since this is a coreference-like pass. Propagated\n // entities are filtered by the configured threshold to\n // ensure they respect the caller's confidence floor.\n let postOrgEntities = consistent;\n if (\n config.enableCoreference &&\n labelIsAllowed(\"organization\", allowedLabels)\n ) {\n const orgPropagated = propagateOrgNames(consistent, fullText);\n const thresholded = orgPropagated.filter(\n (e) => e.score >= config.threshold,\n );\n if (thresholded.length > 0) {\n postOrgEntities = mergeAndDedup(consistent, thresholded);\n log(\"org-propagation\", `${thresholded.length} base names`);\n }\n }\n\n // False-positive filtering\n const merged = filterFalsePositives(postOrgEntities, ctx, fullText);\n if (merged.length < postOrgEntities.length)\n log(\"filter\", `removed ${postOrgEntities.length - merged.length} FPs`);\n\n checkAbort(signal);\n\n // Coreference\n // Clear stale entries unconditionally so a reused\n // context doesn't leak sourceText across documents.\n ctx.corefSourceMap.clear();\n if (config.enableCoreference) {\n const terms = await extractDefinedTerms(fullText, merged, ctx);\n if (terms.length > 0) {\n log(\"coreference\", `${terms.length} defined terms`);\n const corefSpans = findCoreferenceSpans(fullText, terms, ctx);\n if (corefSpans.length > 0) {\n log(\"coreference-rescan\", `${corefSpans.length} aliases`);\n const corefMerged = mergeAndDedup(merged, corefSpans);\n const corefConsistent = enforceBoundaryConsistency(\n corefMerged,\n fullText,\n );\n return sanitizeEntities(\n filterAllowedLabels(\n filterFalsePositives(corefConsistent, ctx, fullText),\n allowedLabels,\n ),\n );\n }\n }\n }\n\n // Re-sanitize: enforceBoundaryConsistency may adjust\n // entity boundaries after mergeAndDedup's sanitization,\n // potentially re-introducing whitespace or punctuation.\n return sanitizeEntities(filterAllowedLabels(merged, allowedLabels));\n};\n","import type {\n AnonymisationOperator,\n OperatorConfig,\n OperatorType,\n} from \"./types\";\n\n// ── Operator registry ──────────────────────────────────\n\nconst replaceOperator: AnonymisationOperator = {\n type: \"replace\",\n reversibility: \"reversible\",\n apply: (_text, _label, placeholder) => placeholder,\n};\n\nconst redactOperator: AnonymisationOperator = {\n type: \"redact\",\n reversibility: \"irreversible\",\n apply: (_text, _label, _placeholder, redactString) => redactString,\n};\n\nexport const OPERATOR_REGISTRY = {\n replace: replaceOperator,\n redact: redactOperator,\n} as const satisfies Record<OperatorType, AnonymisationOperator>;\n\nconst DEFAULT_REDACT_STRING = \"[REDACTED]\";\n\n/**\n * Default operator config: replace for all labels.\n * Preserves existing pipeline behaviour.\n */\nexport const DEFAULT_OPERATOR_CONFIG: OperatorConfig = {\n operators: {},\n redactString: DEFAULT_REDACT_STRING,\n};\n\n/**\n * Resolve the operator for a label, falling back to \"replace\".\n */\nexport const resolveOperator = (\n config: OperatorConfig,\n label: string,\n): OperatorType => config.operators[label] ?? \"replace\";\n","import {\n DEFAULT_OPERATOR_CONFIG,\n OPERATOR_REGISTRY,\n resolveOperator,\n} from \"./operators\";\nimport type {\n Entity,\n OperatorConfig,\n OperatorType,\n RedactionResult,\n} from \"./types\";\nimport type { PipelineContext } from \"./context\";\nimport { corefKey, defaultContext } from \"./context\";\n\nconst WHITESPACE_RE = /\\s+/g;\nconst PHONE_NOISE_RE = /[()\\s-]/g;\nconst SPACE_DASH_RE = /[\\s-]/g;\n\n/**\n * Normalize entity text so that surface-form variations\n * of the same real-world value map to a single canonical\n * key. Lowercased emails, stripped phone formatting, etc.\n */\nconst normalizeEntityText = (label: string, text: string): string => {\n const upper = label.toUpperCase().replace(WHITESPACE_RE, \"_\");\n\n if (upper === \"EMAIL_ADDRESS\" || upper === \"EMAIL\") {\n return text.toLowerCase().trim();\n }\n if (upper === \"PHONE_NUMBER\" || upper === \"PHONE\") {\n return text.replace(PHONE_NOISE_RE, \"\");\n }\n if (\n upper === \"IBAN\" ||\n upper === \"BANK_ACCOUNT_NUMBER\" ||\n upper === \"TAX_IDENTIFICATION_NUMBER\" ||\n upper === \"REGISTRATION_NUMBER\"\n ) {\n return text.replace(SPACE_DASH_RE, \"\").toUpperCase();\n }\n if (\n upper === \"PERSON\" ||\n upper === \"ORGANIZATION\" ||\n upper === \"ADDRESS\" ||\n upper === \"LAND_PARCEL\"\n ) {\n return text.replace(WHITESPACE_RE, \" \").toLowerCase().trim();\n }\n return text.trim();\n};\n\n/**\n * Build a stable mapping from entity text to numbered\n * placeholders. Same real-world value always maps to the\n * same placeholder (e.g., \"Dr. Muller\" and \"Dr. Muller\"\n * both become [PERSON_1]).\n *\n * Placeholder format: [LABEL_N] where LABEL is uppercase\n * and N is a 1-based counter per label.\n *\n * @param ctx Pipeline context. Must be the same instance\n * passed to `runPipeline` (or `findCoreferenceSpans`)\n * so coreference placeholder links are preserved.\n * Defaults to `defaultContext` for single-tenant usage.\n */\nexport const buildPlaceholderMap = (\n entities: Entity[],\n ctx: PipelineContext = defaultContext,\n): Map<string, string> => {\n const counters = new Map<string, number>();\n const textLabelToPlaceholder = new Map<string, string>();\n const normalizedToPlaceholder = new Map<string, string>();\n\n const sorted = entities.toSorted((a, b) => a.start - b.start);\n\n for (const entity of sorted) {\n const compositeKey = `${entity.label}\\0${entity.text}`;\n if (textLabelToPlaceholder.has(compositeKey)) {\n continue;\n }\n\n const labelKey = entity.label.toUpperCase().replace(WHITESPACE_RE, \"_\");\n\n // Coreference side-channel: if this entity is a\n // coref alias, look up the source entity's\n // placeholder so both get the same number.\n const sourceText = ctx.corefSourceMap.get(corefKey(entity));\n if (sourceText !== undefined) {\n const sourceNormalized = normalizeEntityText(entity.label, sourceText);\n const sourceNormalizedKey = `${labelKey}\\0${sourceNormalized}`;\n const sourceExisting = normalizedToPlaceholder.get(sourceNormalizedKey);\n if (sourceExisting) {\n textLabelToPlaceholder.set(compositeKey, sourceExisting);\n continue;\n }\n }\n\n const normalized = normalizeEntityText(entity.label, entity.text);\n const normalizedKey = `${labelKey}\\0${normalized}`;\n const existing = normalizedToPlaceholder.get(normalizedKey);\n if (existing) {\n textLabelToPlaceholder.set(compositeKey, existing);\n continue;\n }\n\n const count = (counters.get(labelKey) ?? 0) + 1;\n counters.set(labelKey, count);\n\n const placeholder = `[${labelKey}_${count}]`;\n textLabelToPlaceholder.set(compositeKey, placeholder);\n normalizedToPlaceholder.set(normalizedKey, placeholder);\n }\n\n return textLabelToPlaceholder;\n};\n\n/**\n * Apply redactions to the source text, replacing each\n * confirmed entity span using the configured operator.\n *\n * Co-references are consistent: if the same text appears\n * multiple times, all occurrences get the same placeholder.\n *\n * @param ctx Pipeline context. Must be the same instance\n * passed to `runPipeline` (or `findCoreferenceSpans`)\n * so coreference placeholder links are preserved.\n * Defaults to `defaultContext` for single-tenant usage.\n */\nexport const redactText = (\n fullText: string,\n entities: Entity[],\n config: OperatorConfig = DEFAULT_OPERATOR_CONFIG,\n ctx: PipelineContext = defaultContext,\n): RedactionResult => {\n if (entities.length === 0) {\n return {\n redactedText: fullText,\n redactionMap: new Map(),\n operatorMap: new Map(),\n entityCount: 0,\n };\n }\n\n const placeholderMap = buildPlaceholderMap(entities, ctx);\n\n const sorted = entities.toSorted((a, b) => a.start - b.start);\n\n // Remove overlapping spans (keep first occurrence)\n const nonOverlapping: Entity[] = [];\n let lastEnd = 0;\n for (const entity of sorted) {\n if (entity.start >= lastEnd) {\n nonOverlapping.push(entity);\n lastEnd = entity.end;\n }\n }\n\n const parts: string[] = [];\n const redactionMap = new Map<string, string>();\n const operatorMap = new Map<string, OperatorType>();\n let cursor = 0;\n\n for (const entity of nonOverlapping) {\n if (entity.start > cursor) {\n parts.push(fullText.slice(cursor, entity.start));\n }\n\n const placeholder =\n placeholderMap.get(`${entity.label}\\0${entity.text}`) ??\n `[${entity.label.toUpperCase().replace(/\\s+/g, \"_\")}]`;\n\n const opType = resolveOperator(config, entity.label);\n const operator = OPERATOR_REGISTRY[opType];\n\n const replacement = operator.apply(\n entity.text,\n entity.label,\n placeholder,\n config.redactString,\n );\n\n parts.push(replacement);\n // operatorMap is keyed by the conceptual placeholder\n // ([LABEL_N]), not by the replacement text. For \"redact\"\n // operators the placeholder never appears in the output;\n // the map is only consulted via exportRedactionKey which\n // iterates redactionMap (replace entries only).\n operatorMap.set(placeholder, opType);\n\n // Only populate redactionMap for reversible operators\n if (\n operator.reversibility === \"reversible\" &&\n !redactionMap.has(placeholder)\n ) {\n redactionMap.set(placeholder, entity.text);\n }\n\n cursor = entity.end;\n }\n\n if (cursor < fullText.length) {\n parts.push(fullText.slice(cursor));\n }\n\n return {\n redactedText: parts.join(\"\"),\n redactionMap,\n operatorMap,\n entityCount: nonOverlapping.length,\n };\n};\n\n/**\n * Serialize the redaction key to JSON for export.\n * Includes operator metadata so the export is self-describing.\n */\nexport const exportRedactionKey = (\n redactionMap: Map<string, string>,\n operatorMap: Map<string, OperatorType>,\n): string => {\n const entries: Record<string, { original: string; operator: OperatorType }> =\n {};\n\n for (const [placeholder, value] of redactionMap) {\n entries[placeholder] = {\n original: value,\n operator: operatorMap.get(placeholder) ?? \"replace\",\n };\n }\n\n return JSON.stringify({ entries }, null, 2);\n};\n\n/**\n * De-anonymise text using a redaction key.\n * Replaces placeholders back with original values.\n * Only works for reversible operators (replace).\n */\nexport const deanonymise = (\n redactedText: string,\n redactionMap: Map<string, string>,\n): string => {\n let result = redactedText;\n\n for (const [placeholder, original] of redactionMap) {\n result = result.replaceAll(placeholder, original);\n }\n\n return result;\n};\n","/**\n * Decode ONNX model output into entity spans.\n *\n * The span-level model outputs a logits tensor of shape\n * [batch, inputLength, maxWidth, numEntities]. This module\n * applies sigmoid, thresholds, and greedy non-overlapping\n * selection to produce the final entity list.\n */\nimport type { RawInferenceResult } from \"./types\";\n\ntype Span = [string, number, number, string, number];\n\nconst sigmoid = (x: number): number => 1 / (1 + Math.exp(-x));\n\n/** Check if two spans overlap (optionally allowing multi-label). */\nconst hasOverlapping = (\n a: number[],\n b: number[],\n multiLabel: boolean,\n): boolean => {\n const a0 = a[0] ?? 0;\n const a1 = a[1] ?? 0;\n const b0 = b[0] ?? 0;\n const b1 = b[1] ?? 0;\n if (a0 === b0 && a1 === b1) {\n return !multiLabel;\n }\n return !(a0 > b1 || b0 > a1);\n};\n\n/**\n * Greedy non-overlapping span selection.\n * Sorts by score descending, keeps each span only if it\n * doesn't overlap with already-selected spans.\n */\nconst greedySearch = (\n spans: Span[],\n flatNer: boolean,\n multiLabel: boolean,\n): Span[] => {\n const sorted = spans.toSorted((a, b) => b[4] - a[4]);\n const selected: Span[] = [];\n\n for (const span of sorted) {\n const overlaps = selected.some((s) => {\n if (flatNer) {\n return hasOverlapping([span[1], span[2]], [s[1], s[2]], multiLabel);\n }\n // Non-flat: also allow nested spans\n const isNested =\n (span[1] <= s[1] && span[2] >= s[2]) ||\n (s[1] <= span[1] && s[2] >= span[2]);\n if (isNested) {\n return false;\n }\n return hasOverlapping([span[1], span[2]], [s[1], s[2]], multiLabel);\n });\n\n if (!overlaps) {\n selected.push(span);\n }\n }\n\n return selected.toSorted((a, b) => a[1] - b[1]);\n};\n\n/**\n * Decode span-level model logits into entity results.\n */\nexport const decodeSpans = (\n batchSize: number,\n inputLength: number,\n maxWidth: number,\n numEntities: number,\n texts: string[],\n batchIds: number[],\n batchWordsStartIdx: number[][],\n batchWordsEndIdx: number[][],\n idToClass: Record<number, string>,\n modelOutput: ArrayLike<number>,\n flatNer: boolean,\n threshold: number,\n multiLabel: boolean,\n): RawInferenceResult => {\n const spans: Span[][] = Array.from({ length: batchSize }, () => []);\n\n const batchPadding = inputLength * maxWidth * numEntities;\n const startTokenPadding = maxWidth * numEntities;\n const endTokenPadding = numEntities;\n\n for (let id = 0; id < modelOutput.length; id++) {\n const batch = Math.floor(id / batchPadding);\n const startToken = Math.floor(id / startTokenPadding) % inputLength;\n const endToken = startToken + (Math.floor(id / endTokenPadding) % maxWidth);\n const entity = id % numEntities;\n\n const prob = sigmoid(modelOutput[id] ?? 0);\n\n const batchStarts = batchWordsStartIdx[batch];\n const batchEnds = batchWordsEndIdx[batch];\n const batchSpans = spans[batch];\n if (!batchStarts || !batchEnds || !batchSpans) {\n continue;\n }\n\n if (\n prob >= threshold &&\n startToken < batchStarts.length &&\n endToken < batchEnds.length\n ) {\n const globalBatch = batchIds[batch] ?? 0;\n const startIdx = batchStarts[startToken] ?? 0;\n const endIdx = batchEnds[endToken] ?? 0;\n const spanText = (texts[globalBatch] ?? \"\").slice(startIdx, endIdx);\n batchSpans.push([\n spanText,\n startIdx,\n endIdx,\n idToClass[entity + 1] ?? \"\",\n prob,\n ]);\n }\n }\n\n return spans.map((batchSpans) =>\n greedySearch(batchSpans, flatNer, multiLabel),\n );\n};\n","/**\n * Token-level BIO decoder for GLiNER TokenGLiNER models\n * (e.g., gliner-pii-edge-v1.0).\n *\n * These models output logits of shape [B, L, C, 3] where:\n * B = batch size\n * L = number of words (text_lengths)\n * C = number of entity classes\n * 3 = BIO tags: [B(egin), I(nside), O(utside)]\n *\n * This decoder converts BIO-tagged logits into entity spans\n * with character offsets.\n */\nimport type { RawInferenceResult } from \"./types\";\n\ntype Span = [string, number, number, string, number];\n\nconst sigmoid = (x: number): number => 1 / (1 + Math.exp(-x));\n\nconst B_TAG = 0;\nconst I_TAG = 1;\n\n/**\n * Decode token-level BIO logits into entity spans.\n *\n * For each word, checks if the B(egin) logit for any class\n * exceeds the threshold. If so, extends the span by consuming\n * subsequent I(nside) tokens of the same class.\n */\nexport const decodeTokenSpans = (\n batchSize: number,\n numWords: number,\n numEntities: number,\n texts: string[],\n batchIds: number[],\n batchWordsStartIdx: number[][],\n batchWordsEndIdx: number[][],\n idToClass: Record<number, string>,\n modelOutput: ArrayLike<number>,\n threshold: number,\n): RawInferenceResult => {\n const results: Span[][] = Array.from({ length: batchSize }, () => []);\n\n const wordStride = numEntities * 3;\n const batchStride = numWords * wordStride;\n\n for (let b = 0; b < batchSize; b++) {\n const batchOffset = b * batchStride;\n const starts = batchWordsStartIdx[b];\n const ends = batchWordsEndIdx[b];\n const globalBatch = batchIds[b] ?? 0;\n const text = texts[globalBatch] ?? \"\";\n const batchSpans = results[b];\n\n if (!starts || !ends || !batchSpans) {\n continue;\n }\n\n const actualWords = starts.length;\n\n for (let e = 0; e < numEntities; e++) {\n let w = 0;\n\n while (w < actualWords) {\n const bLogitIdx = batchOffset + w * wordStride + e * 3 + B_TAG;\n const bScore = sigmoid(modelOutput[bLogitIdx] ?? 0);\n\n if (bScore < threshold) {\n w++;\n continue;\n }\n\n const spanStart = w;\n let spanEnd = w;\n let maxScore = bScore;\n\n while (spanEnd + 1 < actualWords) {\n const iLogitIdx =\n batchOffset + (spanEnd + 1) * wordStride + e * 3 + I_TAG;\n const iScore = sigmoid(modelOutput[iLogitIdx] ?? 0);\n\n if (iScore < threshold) {\n break;\n }\n\n spanEnd++;\n maxScore = Math.max(maxScore, iScore);\n }\n\n const charStart = starts[spanStart] ?? 0;\n const charEnd = ends[spanEnd] ?? 0;\n const spanText = text.slice(charStart, charEnd);\n const label = idToClass[e + 1] ?? \"\";\n\n if (spanText.trim().length > 0 && label) {\n batchSpans.push([spanText, charStart, charEnd, label, maxScore]);\n }\n\n w = spanEnd + 1;\n }\n }\n\n const selected: Span[] = [];\n for (const span of batchSpans.toSorted((x, y) => y[4] - x[4])) {\n const overlaps = selected.some((s) => span[1] < s[2] && span[2] > s[1]);\n if (!overlaps) {\n selected.push(span);\n }\n }\n\n results[b] = selected.toSorted((x, y) => x[1] - y[1]);\n }\n\n return results;\n};\n","/**\n * Tokenization and input preparation for GLiNER span model.\n *\n * Splits text into word tokens, encodes via HuggingFace\n * tokenizer, and builds the span index grid that the ONNX\n * model expects as input.\n */\nimport type { Encoding, Tokenizer } from \"@huggingface/tokenizers\";\n\nconst segmenter = new Intl.Segmenter(undefined, {\n granularity: \"word\",\n});\n\n/** Tokenize text into words with character offsets. */\nexport const tokenizeText = (\n text: string,\n): [words: string[], starts: number[], ends: number[]] => {\n const words: string[] = [];\n const starts: number[] = [];\n const ends: number[] = [];\n\n for (const { segment, index, isWordLike } of segmenter.segment(text)) {\n // Include words and numeric segments (Intl.Segmenter\n // marks pure digit sequences as non-word-like, but the\n // NER model needs them for entity detection).\n if (!isWordLike && !/\\d/u.test(segment)) {\n continue;\n }\n words.push(segment);\n starts.push(index);\n ends.push(index + segment.length);\n }\n\n return [words, starts, ends];\n};\n\n/** Build entity label <-> id mappings. */\nconst createMappings = (\n labels: string[],\n): {\n classToId: Record<string, number>;\n idToClass: Record<number, string>;\n} => {\n const classToId: Record<string, number> = {};\n const idToClass: Record<number, string> = {};\n for (let i = 0; i < labels.length; i++) {\n const label = labels[i];\n if (label === undefined) {\n continue;\n }\n const id = i + 1;\n classToId[label] = id;\n idToClass[id] = label;\n }\n return { classToId, idToClass };\n};\n\n/** Prepend entity prompt tokens to word tokens. */\nconst prepareTextInputs = (\n batchTokens: string[][],\n entities: string[],\n): [inputTexts: string[][], textLengths: number[], promptLengths: number[]] => {\n const inputTexts: string[][] = [];\n const promptLengths: number[] = [];\n const textLengths: number[] = [];\n\n for (const tokens of batchTokens) {\n textLengths.push(tokens.length);\n\n const prompt: string[] = [];\n for (const ent of entities) {\n prompt.push(\"<<ENT>>\");\n prompt.push(ent);\n }\n prompt.push(\"<<SEP>>\");\n\n promptLengths.push(prompt.length);\n inputTexts.push([...prompt, ...tokens]);\n }\n\n return [inputTexts, textLengths, promptLengths];\n};\n\n/** Encode word sequences into token IDs with masks. */\nconst encodeInputs = (\n tokenizer: Tokenizer,\n texts: string[][],\n promptLengths: number[],\n): [\n inputIds: number[][],\n attentionMasks: number[][],\n wordsMasks: number[][],\n] => {\n // Resolve special token IDs dynamically instead of\n // hardcoding DeBERTa-v3 values. Fallbacks (1, 2) match\n // the current model but future models may differ.\n const clsTokenId = tokenizer.token_to_id(\"[CLS]\") ?? 1;\n const sepTokenId = tokenizer.token_to_id(\"[SEP]\") ?? 2;\n\n const allInputIds: number[][] = [];\n const allAttentionMasks: number[][] = [];\n const allWordsMasks: number[][] = [];\n\n for (let idx = 0; idx < texts.length; idx++) {\n const promptLength = promptLengths[idx] ?? 0;\n const words = texts[idx];\n if (!words) {\n continue;\n }\n const wordsMask: number[] = [0];\n const inputIds: number[] = [clsTokenId];\n const attentionMask: number[] = [1];\n\n let wordCounter = 1;\n for (let wordId = 0; wordId < words.length; wordId++) {\n const word = words[wordId];\n if (word === undefined) {\n continue;\n }\n // encode() returns { ids, tokens, attention_mask }\n // with BOS/EOS; strip them with slice(1, -1)\n const encoded: Encoding = tokenizer.encode(word);\n const wordTokens: number[] = encoded.ids.slice(1, -1);\n\n for (let tokenId = 0; tokenId < wordTokens.length; tokenId++) {\n attentionMask.push(1);\n if (wordId < promptLength) {\n wordsMask.push(0);\n } else if (tokenId === 0) {\n wordsMask.push(wordCounter);\n wordCounter++;\n } else {\n wordsMask.push(0);\n }\n inputIds.push(wordTokens[tokenId] ?? 0);\n }\n }\n\n inputIds.push(sepTokenId);\n wordsMask.push(0);\n attentionMask.push(1);\n\n allInputIds.push(inputIds);\n allAttentionMasks.push(attentionMask);\n allWordsMasks.push(wordsMask);\n }\n\n return [allInputIds, allAttentionMasks, allWordsMasks];\n};\n\n/** Build span index pairs and masks for the model. */\nconst prepareSpans = (\n batchTokens: string[][],\n maxWidth: number,\n): { spanIdxs: number[][][]; spanMasks: boolean[][] } => {\n const spanIdxs: number[][][] = [];\n const spanMasks: boolean[][] = [];\n\n for (const tokens of batchTokens) {\n const len = tokens.length;\n const idx: number[][] = [];\n const mask: boolean[] = [];\n\n for (let i = 0; i < len; i++) {\n for (let j = 0; j < maxWidth; j++) {\n const endIdx = Math.min(i + j, len - 1);\n idx.push([i, endIdx]);\n mask.push(endIdx < len);\n }\n }\n\n spanIdxs.push(idx);\n spanMasks.push(mask);\n }\n\n return { spanIdxs, spanMasks };\n};\n\n/** Pad a 2D or 3D array to uniform inner length. */\nexport const padArray = <T>(arr: T[][], dimensions: number = 2): T[][] => {\n if (arr.length === 0) {\n return [];\n }\n const maxLength = Math.max(...arr.map((sub) => sub.length));\n // For 3D arrays, infer inner dimension from the first\n // non-empty element (arr[0] may be empty when a batch\n // element had zero word tokens).\n let finalDim = 0;\n if (dimensions === 3) {\n for (const sub of arr) {\n if (sub.length > 0) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- 3D arrays have number[] inner elements\n const inner = sub[0] as unknown as number[];\n finalDim = inner.length;\n break;\n }\n }\n }\n\n return arr.map((sub) => {\n const padCount = maxLength - sub.length;\n const fill =\n dimensions === 3\n ? Array.from({ length: padCount }, () =>\n Array.from<number>({ length: finalDim }).fill(0),\n )\n : Array.from<number>({ length: padCount }).fill(0);\n // SAFETY: fill values (zero arrays) match the shape of T\n // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- generic padding for ONNX tensor arrays\n return [...sub, ...(fill as T[])] as T[];\n });\n};\n\n/** Prepare a complete batch for ONNX inference. */\nexport const prepareBatch = (\n tokenizer: Tokenizer,\n texts: string[],\n entities: string[],\n maxWidth: number,\n): {\n inputsIds: number[][];\n attentionMasks: number[][];\n wordsMasks: number[][];\n textLengths: number[];\n spanIdxs: number[][][];\n spanMasks: boolean[][];\n idToClass: Record<number, string>;\n batchTokens: string[][];\n batchWordsStartIdx: number[][];\n batchWordsEndIdx: number[][];\n} => {\n const batchTokens: string[][] = [];\n const batchWordsStartIdx: number[][] = [];\n const batchWordsEndIdx: number[][] = [];\n\n for (const text of texts) {\n const [words, starts, ends] = tokenizeText(text);\n batchTokens.push(words);\n batchWordsStartIdx.push(starts);\n batchWordsEndIdx.push(ends);\n }\n\n const { idToClass } = createMappings(entities);\n\n const [inputTokens, textLengths, promptLengths] = prepareTextInputs(\n batchTokens,\n entities,\n );\n\n let [inputsIds, attentionMasks, wordsMasks] = encodeInputs(\n tokenizer,\n inputTokens,\n promptLengths,\n );\n\n inputsIds = padArray(inputsIds);\n attentionMasks = padArray(attentionMasks);\n wordsMasks = padArray(wordsMasks);\n\n let { spanIdxs, spanMasks } = prepareSpans(batchTokens, maxWidth);\n\n spanIdxs = padArray(spanIdxs, 3);\n spanMasks = padArray(spanMasks);\n\n return {\n inputsIds,\n attentionMasks,\n wordsMasks,\n textLengths,\n spanIdxs,\n spanMasks,\n idToClass,\n batchTokens,\n batchWordsStartIdx,\n batchWordsEndIdx,\n };\n};\n","import type { Entity } from \"../types\";\n\nconst MAX_CHUNK_CHARS = 1500;\nconst OVERLAP_CHARS = 50;\nconst MIN_CHUNK_LENGTH = 10;\n\n/**\n * Split text into overlapping chunks for GLiNER's\n * ~512 token context window. Character-based splitting\n * (rough approximation of token limits).\n *\n * Tries to break at sentence boundaries when possible.\n */\nexport const chunkText = (text: string): string[] => {\n const chunks: string[] = [];\n let offset = 0;\n\n while (offset < text.length) {\n let end = Math.min(offset + MAX_CHUNK_CHARS, text.length);\n\n if (end < text.length) {\n const slice = text.slice(offset, end);\n const lastPeriod = slice.lastIndexOf(\". \");\n if (lastPeriod > MAX_CHUNK_CHARS * 0.5) {\n end = offset + lastPeriod + 2;\n }\n }\n\n const chunk = text.slice(offset, end);\n if (chunk.trim().length > MIN_CHUNK_LENGTH) {\n chunks.push(chunk);\n }\n offset = Math.max(offset + 1, end - OVERLAP_CHARS);\n }\n\n return chunks;\n};\n\n/**\n * Compute the byte offset of each chunk within the\n * original document text.\n */\nexport const computeChunkOffsets = (\n fullText: string,\n chunks: string[],\n): number[] => {\n const offsets: number[] = [];\n let searchFrom = 0;\n\n for (const chunk of chunks) {\n const idx = fullText.indexOf(chunk, searchFrom);\n offsets.push(idx !== -1 ? idx : searchFrom);\n searchFrom =\n idx !== -1 ? idx + Math.max(1, chunk.length - OVERLAP_CHARS) : searchFrom;\n }\n\n return offsets;\n};\n\nconst POSITION_THRESHOLD = 5;\n\n/**\n * Merge entities from overlapping chunks back to\n * document-level offsets. Deduplicates entities that\n * appear in overlap regions (keeps highest score).\n *\n * Dedup invariant: each incoming entity is compared\n * against the highest-scored same-label near-dup in\n * its proximity window. If it loses, it is dropped.\n * This does NOT guarantee that all pairwise near-dup\n * relationships in the output are resolved; a lower-\n * scored entity can survive if the bridging entity\n * that would have replaced it was itself dropped by\n * a higher-scored match.\n *\n * Uses a reverse-scan over the sorted merged array\n * so each entity only compares against nearby\n * predecessors — O(n * w) average where w is the max\n * entities per POSITION_THRESHOLD window, O(n²) worst\n * case when replacements dominate (splice is O(n)).\n */\nexport const mergeChunkEntities = (\n chunkOffsets: number[],\n chunkResults: Entity[][],\n): Entity[] => {\n const allEntities: Entity[] = [];\n\n for (let i = 0; i < chunkResults.length; i++) {\n const offset = chunkOffsets[i] ?? 0;\n const entities = chunkResults[i];\n if (!entities) {\n continue;\n }\n for (const entity of entities) {\n allEntities.push({\n ...entity,\n start: entity.start + offset,\n end: entity.end + offset,\n });\n }\n }\n\n const sorted = allEntities.toSorted((a, b) => a.start - b.start);\n const merged: Entity[] = [];\n\n for (const entity of sorted) {\n let bestDupIndex = -1;\n let bestDupScore = -1;\n\n // Reverse-scan the full proximity window. Collect\n // the highest-scored same-label match so we dedup\n // against the strongest existing entity, not just\n // the nearest one.\n for (let j = merged.length - 1; j >= 0; j--) {\n const existing = merged[j];\n if (existing === undefined) {\n continue;\n }\n // merged is kept sorted by start (splice+push\n // maintains this); elements further back have\n // even smaller starts, so we can break early.\n if (entity.start - existing.start >= POSITION_THRESHOLD) {\n break;\n }\n if (\n existing.label === entity.label &&\n Math.abs(existing.end - entity.end) < POSITION_THRESHOLD &&\n existing.score > bestDupScore\n ) {\n bestDupIndex = j;\n bestDupScore = existing.score;\n }\n }\n\n if (bestDupIndex !== -1) {\n const existing = merged[bestDupIndex];\n if (existing !== undefined && entity.score > existing.score) {\n // Replace with winner. Splice out the old entry\n // and re-insert at the end to maintain sorted\n // order (entity.start >= all prior starts).\n merged.splice(bestDupIndex, 1);\n merged.push({ ...entity });\n }\n // Entity loses to the best match and is dropped.\n // Other lower-scored near-dups of this entity are\n // not revisited (see docstring dedup invariant).\n } else {\n merged.push({ ...entity });\n }\n }\n\n return merged;\n};\n","/**\n * Compute the Levenshtein edit distance between two\n * strings. O(n*m) time, O(min(n,m)) space using a\n * single-row DP approach.\n */\nexport const levenshtein = (rawA: string, rawB: string): number => {\n if (rawA === rawB) {\n return 0;\n }\n if (rawA.length === 0) {\n return rawB.length;\n }\n if (rawB.length === 0) {\n return rawA.length;\n }\n\n const [shorter, longer] =\n rawA.length <= rawB.length ? [rawA, rawB] : [rawB, rawA];\n\n const aLen = shorter.length;\n const bLen = longer.length;\n const row = new Uint16Array(aLen + 1);\n\n for (let i = 0; i <= aLen; i++) {\n row[i] = i;\n }\n\n for (let j = 1; j <= bLen; j++) {\n let prev = row[0] ?? 0;\n row[0] = j;\n\n for (let i = 1; i <= aLen; i++) {\n const cost = shorter[i - 1] === longer[j - 1] ? 0 : 1;\n const temp = row[i] ?? 0;\n row[i] = Math.min((row[i] ?? 0) + 1, (row[i - 1] ?? 0) + 1, prev + cost);\n prev = temp;\n }\n }\n\n return row[aLen] ?? 0;\n};\n","/* Browser/WASM entry point — loads TextSearch\n * from @stll/text-search-wasm and re-exports\n * the full anonymize API. */\n\nimport { TextSearch } from \"@stll/text-search-wasm\";\n\nimport { initTextSearch } from \"./search-engine\";\n\ninitTextSearch(TextSearch);\n\nexport * from \"./index-shared\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"wasm.mjs","names":["PERSON_CHAIN_BREAK_RE","isInitialContinuationGap","segmenter","charGroupsJson","escapeRegex","WHITESPACE_RE","sigmoid"],"sources":["../../src/search-engine.ts","../../src/types.ts","../../src/context.ts","../../src/util/lang-loader.ts","../../src/detectors/coreference.ts","../../src/detectors/gazetteer.ts","../../src/util/text.ts","../../src/detectors/names.ts","../../src/config/titles.ts","../../src/data/char-groups.json","../../src/util/char-groups.ts","../../src/detectors/regex.ts","../../src/detectors/legal-forms.ts","../../src/config/legal-forms.ts","../../src/detectors/triggers.ts","../../src/regions.ts","../../src/filters/false-positives.ts","../../src/util/normalize.ts","../../src/detectors/deny-list.ts","../../src/detectors/address-seeds.ts","../../src/detectors/org-propagation.ts","../../src/filters/confidence-boost.ts","../../src/filters/zone-classifier.ts","../../src/filters/hotword-rules.ts","../../src/filters/boundary-consistency.ts","../../src/build-unified-search.ts","../../src/unified-search.ts","../../src/util/entity-masking.ts","../../src/pipeline.ts","../../src/operators.ts","../../src/redact.ts","../../src/gliner/decoder.ts","../../src/gliner/token-decoder.ts","../../src/gliner/processor.ts","../../src/util/chunker.ts","../../src/util/levenshtein.ts","../../src/wasm.ts"],"sourcesContent":["/**\n * Late-bound TextSearch constructor.\n *\n * Native entry injects @stll/text-search,\n * WASM entry injects @stll/text-search-wasm.\n * Both expose the same API.\n */\n\n/* eslint-disable-next-line @typescript-eslint/no-explicit-any */\ntype TextSearchCtor = new (...args: any[]) => any;\n\nlet _TextSearch: TextSearchCtor | undefined;\n\nexport const initTextSearch = (ctor: TextSearchCtor): void => {\n _TextSearch = ctor;\n};\n\nexport const getTextSearch = (): TextSearchCtor => {\n if (!_TextSearch) {\n throw new Error(\n \"TextSearch not initialized. Import from \" +\n \"@stll/anonymize or @stll/anonymize-wasm, \" +\n \"not from internal modules.\",\n );\n }\n return _TextSearch;\n};\n","/**\n * Source of a detected entity span.\n * Ordered by detection layer in the pipeline.\n */\nexport const DETECTION_SOURCES = {\n TRIGGER: \"trigger\",\n REGEX: \"regex\",\n DENY_LIST: \"deny-list\",\n LEGAL_FORM: \"legal-form\",\n GAZETTEER: \"gazetteer\",\n NER: \"ner\",\n COREFERENCE: \"coreference\",\n} as const;\n\nexport type DetectionSource =\n (typeof DETECTION_SOURCES)[keyof typeof DETECTION_SOURCES];\n\n/**\n * Priority levels for detection sources.\n * Higher = more structurally reliable. Used during\n * overlap resolution so deterministic detectors beat\n * probabilistic ones regardless of raw score.\n */\nexport const DETECTOR_PRIORITY: Record<DetectionSource, number> = {\n gazetteer: 5,\n trigger: 4,\n \"legal-form\": 3,\n regex: 3,\n \"deny-list\": 2,\n coreference: 2,\n ner: 1,\n};\n\n/**\n * A detected PII entity span in the source text.\n * Every detection layer produces these.\n */\nexport type Entity = {\n start: number;\n end: number;\n label: string;\n text: string;\n score: number;\n source: DetectionSource;\n};\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 | { type: \"to-next-comma\" }\n | { type: \"to-end-of-line\" }\n | { type: \"n-words\"; count: number }\n | { type: \"company-id-value\" }\n | { type: \"address\"; maxChars?: number };\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/** 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/**\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\n/**\n * Anonymisation operator types. Each operator defines\n * how a confirmed entity is replaced in the output.\n */\nexport const OPERATOR_TYPES = [\"replace\", \"redact\"] as const;\n\nexport type OperatorType = (typeof OPERATOR_TYPES)[number];\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\nexport type PipelineConfig = {\n threshold: number;\n enableTriggerPhrases: boolean;\n enableRegex: boolean;\n /**\n * Enables legal-form organization detection.\n * Required for typed callers; legacy untyped\n * callers that omit this field are treated as\n * enabled at runtime for backward compatibility.\n */\n enableLegalForms: boolean;\n /**\n * Enables first-name/surname/title corpus matching.\n * When deny-list mode is enabled, this also controls\n * whether name-corpus entries are injected into the\n * deny-list search automaton.\n */\n enableNameCorpus: boolean;\n enableDenyList: boolean;\n denyListCountries?: string[];\n denyListRegions?: string[];\n denyListExcludeCategories?: string[];\n enableGazetteer: 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\n/**\n * Canonical entity labels used across the pipeline.\n * NER models may use different native labels; the bench\n * NER wrapper maps model output to these canonical names.\n *\n * These labels are ephemeral: entities are regenerated on\n * every pipeline run and never persisted to the database.\n * Renaming a label here requires no migration.\n */\nexport const DEFAULT_ENTITY_LABELS = [\n \"person\",\n \"organization\",\n \"phone number\",\n \"address\",\n \"email address\",\n \"date\",\n \"date of birth\",\n \"bank account number\",\n \"iban\",\n \"tax identification number\",\n \"identity card number\",\n \"registration number\",\n \"credit card number\",\n \"passport number\",\n \"monetary amount\",\n \"land parcel\",\n] as const;\n\nexport const isLegalFormsEnabled = (\n config: Pick<PipelineConfig, \"enableLegalForms\">,\n): boolean => config.enableLegalForms !== false;\n","import type { UnifiedSearchInstance } from \"./build-unified-search\";\nimport type { Entity } from \"./types\";\n\n/**\n * Build a stable cache key for an entity that survives\n * shallow copies (spread). Uses position + label so the\n * key is identical for the original object and any\n * `{ ...entity }` copy produced by mergeAndDedup.\n */\nexport const corefKey = (e: Entity): string => `${e.start}:${e.end}:${e.label}`;\n\n/**\n * Compiled RegExp pattern used for coreference\n * definition extraction.\n */\nexport type DefinitionPattern = {\n pattern: RegExp;\n};\n\n/**\n * Cached data for the name corpus detector.\n * Populated by initNameCorpus; consumed by\n * detectNameCorpus and deny-list AC integration.\n */\nexport type NameCorpusData = {\n firstNames: ReadonlySet<string>;\n surnames: ReadonlySet<string>;\n titleTokens: ReadonlySet<string>;\n excludedWords: ReadonlySet<string>;\n /** Raw arrays exposed for deny-list AC integration. */\n firstNamesList: readonly string[];\n surnamesList: readonly string[];\n titlesList: readonly string[];\n excludedList: readonly string[];\n};\n\n/**\n * All cached state for a single pipeline run (or\n * sequence of runs sharing the same config). Replacing\n * module-level singletons with this object enables\n * concurrent pipelines with different configs and\n * simplifies testing.\n *\n * Each field starts null and is populated lazily on\n * first use by the corresponding loader function.\n */\nexport type PipelineContext = {\n // ── Unified search cache ──────────────────────\n search: UnifiedSearchInstance | null;\n searchKey: string;\n searchPromise: Promise<UnifiedSearchInstance> | null;\n\n // ── Name corpus ───────────────────────────────\n nameCorpus: NameCorpusData | null;\n 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 /** First-name exclusions for stopword filtering. */\n firstNameExclusions: ReadonlySet<string> | null;\n firstNameExclusionCorpusLen: number;\n\n // ── Generic roles (false-positive filter) ─────\n genericRoles: ReadonlySet<string> | null;\n genericRolesPromise: Promise<ReadonlySet<string>> | null;\n\n // ── Coreference ───────────────────────────────\n corefPatterns: DefinitionPattern[] | null;\n corefPatternsPromise: Promise<DefinitionPattern[]> | null;\n corefLoadAttempted: boolean;\n roleStopSet: ReadonlySet<string> | null;\n roleStopSetPromise: Promise<ReadonlySet<string>> | null;\n\n // ── Zone classifier ───────────────────────────\n zoneHeadingPatterns: RegExp[] | null;\n zoneSigningPatterns: RegExp[] | null;\n zoneInitPromise: Promise<void> | null;\n\n // ── Coreference source map ────────────────────\n /**\n * Maps coreference entities to their source entity\n * text. Populated by findCoreferenceSpans, consumed\n * by buildPlaceholderMap for consistent placeholder\n * numbering across aliases and source entities.\n *\n * Keyed by `start:end:label` composite string so\n * lookups survive shallow copies (e.g. from\n * mergeAndDedup's spread operator).\n */\n corefSourceMap: Map<string, string>;\n};\n\n/** Create a fresh, empty pipeline context. */\nexport const createPipelineContext = (): PipelineContext => ({\n search: null,\n searchKey: \"\",\n searchPromise: null,\n\n nameCorpus: null,\n nameCorpusPromise: null,\n\n stopwords: null,\n stopwordsPromise: null,\n allowList: null,\n allowListPromise: null,\n personStopwords: null,\n personStopwordsPromise: null,\n firstNameExclusions: null,\n firstNameExclusionCorpusLen: 0,\n\n genericRoles: null,\n genericRolesPromise: null,\n\n corefPatterns: null,\n corefPatternsPromise: null,\n corefLoadAttempted: false,\n roleStopSet: null,\n roleStopSetPromise: null,\n\n zoneHeadingPatterns: null,\n zoneSigningPatterns: null,\n zoneInitPromise: null,\n\n corefSourceMap: new Map(),\n});\n\n/**\n * Module-level default context. Used when callers\n * don't provide an explicit context, preserving full\n * backward compatibility with the existing API.\n */\nexport const defaultContext: PipelineContext = createPipelineContext();\n","/**\n * Language manifest loader and config auto-discovery.\n *\n * Language manifest loader and config auto-discovery.\n *\n * Reads manifest.json to determine which languages have\n * which config types, then loads them via a static\n * registry of import() calls (string literals for\n * bundler compatibility).\n */\n\n// ── Types ────────────────────────────────────────────\n\ntype ManifestLanguage = {\n triggers?: boolean;\n coreference?: boolean;\n};\n\ntype Manifest = {\n languages: Record<string, ManifestLanguage>;\n};\n\ntype ConfigType = \"triggers\" | \"coreference\";\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 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 sk: () => import(\"../data/coreference.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};\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: [\"cs\", \"de\", \"en\", \"es\", \"fr\", \"hu\", \"it\", \"pl\", \"ro\", \"sk\", \"sv\"],\n coreference: [\"cs\", \"de\", \"en\", \"sk\"],\n};\n\n// ── Public API ───────────────────────────────────────\n\n/**\n * Load all config files of a given type for all\n * languages enabled in the manifest.\n *\n * Falls back to the hardcoded language list when the\n * manifest is unavailable (backward compatibility).\n */\nexport const loadLanguageConfigs = async <T extends NonNullable<unknown>>(\n configType: ConfigType,\n mapFn: (mod: unknown) => T,\n): Promise<T[]> => {\n const manifest = await loadManifest();\n const registry = LOADER_REGISTRIES[configType];\n\n // Determine which language codes to load\n const hasManifestLanguages = Object.keys(manifest.languages).length > 0;\n\n const codes = hasManifestLanguages\n ? Object.entries(manifest.languages)\n .filter(([code, lang]) => {\n if (!lang || typeof lang !== \"object\") {\n console.warn(\n `[anonymize] lang-loader: manifest ` +\n `entry for \"${code}\" is not an ` +\n `object, skipping`,\n );\n return false;\n }\n return lang[configType] === true;\n })\n .map(([code]) => code)\n : [...FALLBACK_LANGUAGES[configType]];\n\n // Use indexed assignment so results preserve\n // manifest declaration order regardless of import\n // resolution timing.\n const results: (T | undefined)[] = new Array(codes.length);\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","import { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\nimport type { DefinitionPattern, PipelineContext } from \"../context\";\nimport { corefKey, defaultContext } from \"../context\";\nimport { loadLanguageConfigs } from \"../util/lang-loader\";\n\ntype CoreferenceConfigRow = {\n pattern: string;\n flags: string;\n label: string;\n};\n\n/**\n * Load coreference definition patterns from per-language\n * JSON configs in src/data/. Uses the\n * language manifest for auto-discovery.\n */\nconst loadDefinitionPatterns = async (): Promise<DefinitionPattern[]> => {\n const patterns: DefinitionPattern[] = [];\n\n const allRows = await loadLanguageConfigs<readonly CoreferenceConfigRow[]>(\n \"coreference\",\n (mod) => {\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config\n const m = mod as {\n default?: readonly CoreferenceConfigRow[];\n };\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config\n return (m.default ?? mod) as readonly CoreferenceConfigRow[];\n },\n );\n\n for (const rows of allRows) {\n if (!Array.isArray(rows)) {\n console.warn(\n \"[anonymize] coreference: unexpected \" + \"config shape, skipping\",\n );\n continue;\n }\n for (const row of rows) {\n try {\n patterns.push({\n pattern: new RegExp(row.pattern, row.flags),\n });\n } catch (err) {\n console.warn(\n `[anonymize] coreference: invalid ` + `regex \"${row.pattern}\":`,\n err,\n );\n }\n }\n }\n\n return patterns;\n};\n\n/**\n * Load generic role terms that should NOT be treated\n * as PII coreferences. \"Prodávající\" (Seller),\n * \"Kupující\" (Buyer), etc. are legal roles, not\n * identifying information.\n */\nconst getRoleStopSet = async (\n ctx: PipelineContext,\n): Promise<ReadonlySet<string>> => {\n if (ctx.roleStopSet) return ctx.roleStopSet;\n if (ctx.roleStopSetPromise) return ctx.roleStopSetPromise;\n const promise = (async () => {\n let result: ReadonlySet<string>;\n try {\n const mod = await import(\"../data/generic-roles.json\");\n const data = (mod.default ?? mod) as {\n roles: string[];\n };\n result = new Set(data.roles.map((r: string) => r.toLowerCase()));\n } catch {\n result = new Set();\n }\n ctx.roleStopSet = result;\n return result;\n })();\n ctx.roleStopSetPromise = promise;\n return promise;\n};\n\nconst getDefinitionPatterns = async (\n ctx: PipelineContext,\n): Promise<DefinitionPattern[]> => {\n if (ctx.corefPatterns) {\n return ctx.corefPatterns;\n }\n if (ctx.corefPatternsPromise) {\n return ctx.corefPatternsPromise;\n }\n ctx.corefPatternsPromise = loadDefinitionPatterns();\n const patterns = await ctx.corefPatternsPromise;\n if (patterns.length === 0) {\n // All loads failed; cache empty array permanently\n // to avoid retrying dynamic imports and flooding\n // logs on every call in high-volume pipelines.\n ctx.corefPatterns = patterns;\n if (!ctx.corefLoadAttempted) {\n ctx.corefLoadAttempted = true;\n console.warn(\n \"[anonymize] coreference: no definition \" +\n \"patterns loaded; coreference detection \" +\n \"will be inactive\",\n );\n }\n return patterns;\n }\n ctx.corefPatterns = patterns;\n return patterns;\n};\n\nconst SEARCH_WINDOW = 200;\n\n/**\n * Check whether an alias has textual similarity to\n * the source entity. Prevents roles and structural\n * terms from being treated as name aliases.\n *\n * Three checks (any passes → similar):\n * 1. Word overlap: a word in the alias appears in the\n * entity (case-insensitive, min 2 chars)\n * 2. Initials: alias letters match first letters of\n * entity words (\"TB\" ↔ \"Tomas Bata\")\n * 3. Substring: alias is a substring of the entity\n * or vice versa (min 3 chars)\n */\nconst hasEntitySimilarity = (alias: string, entityText: string): boolean => {\n const aliasLower = alias.toLowerCase();\n const entityLower = entityText.toLowerCase();\n\n // Substring check (min 3 chars to avoid noise)\n if (aliasLower.length >= 3 && entityLower.includes(aliasLower)) {\n return true;\n }\n if (entityLower.length >= 3 && aliasLower.includes(entityLower)) {\n return true;\n }\n\n // Word overlap: split both into words, check for\n // any shared word of 2+ characters\n const aliasWords = aliasLower\n .split(/[\\s.,;:'\"()/-]+/)\n .filter((w) => w.length >= 2);\n const entityWords = entityLower\n .split(/[\\s.,;:'\"()/-]+/)\n .filter((w) => w.length >= 2);\n const entityWordSet = new Set(entityWords);\n for (const word of aliasWords) {\n if (entityWordSet.has(word)) {\n return true;\n }\n }\n\n // Initials: alias is all uppercase and each letter\n // matches the first letter of a consecutive run of\n // entity words. \"TP\" ↔ \"Ing. Tomáš Procházka\"\n // (skips \"Ing.\" and matches T+P). Check all\n // starting positions to handle title prefixes.\n if (\n /^[\\p{Lu}]+$/u.test(alias) &&\n alias.length >= 2 &&\n alias.length <= entityWords.length\n ) {\n for (let start = 0; start <= entityWords.length - alias.length; start++) {\n const initials = entityWords\n .slice(start, start + alias.length)\n .map((w) => w.charAt(0))\n .join(\"\");\n if (initials === aliasLower) {\n return true;\n }\n }\n }\n\n return false;\n};\n\ntype DefinedTerm = {\n alias: string;\n label: string;\n /** Position of the definition in the source text */\n definitionStart: number;\n /** Original entity text the alias refers to */\n sourceText: string;\n};\n\n/**\n * Scan for defined-term patterns near known entities.\n *\n * Legal documents universally follow:\n * \"Dr. Heinrich Muller (hereinafter 'the Seller')...\"\n *\n * After NER detects the entity, this function scans for\n * definitional patterns within +/-200 chars and extracts\n * the alias. Returns alias + label pairs that can be added\n * to the gazetteer for a full-text re-scan.\n */\n/**\n * Labels that can be the source of a coreference alias.\n * Only parties (person, organization) have defined-term\n * aliases in legal text. Dates, addresses, IDs do not.\n */\nconst COREF_SOURCE_LABELS = new Set([\"person\", \"organization\"]);\n\nexport const extractDefinedTerms = async (\n fullText: string,\n entities: Entity[],\n ctx: PipelineContext = defaultContext,\n): Promise<DefinedTerm[]> => {\n const [definitionPatterns, roleStops] = await Promise.all([\n getDefinitionPatterns(ctx),\n getRoleStopSet(ctx),\n ]);\n const terms: DefinedTerm[] = [];\n const seen = new Set<string>();\n\n // Sort entities by position for nearest-preceding lookup\n const sorted = [...entities].sort((a, b) => a.start - b.start);\n\n // Strategy: find all \"dále jen\" definitions in the\n // text, then for each one, find the nearest PRECEDING\n // person/organization entity. This is more accurate\n // than \"any entity within 200 chars\" because:\n // - The alias always refers to a party defined BEFORE\n // the parenthetical, not after\n // - Multiple entities (name, IČO, address) may appear\n // between the party name and the \"dále jen\"; only\n // the party name is the referent\n\n for (const { pattern } of definitionPatterns) {\n pattern.lastIndex = 0;\n\n for (\n let match = pattern.exec(fullText);\n match !== null;\n match = pattern.exec(fullText)\n ) {\n const alias = match[1]?.trim();\n if (!alias || alias.length < 2) {\n continue;\n }\n\n // Skip generic legal roles — \"Prodávající\",\n // \"Kupující\", \"Seller\", etc. are NOT PII.\n // Only track aliases that are themselves\n // identifying (initials, name fragments, etc.)\n if (roleStops.has(alias.toLowerCase())) {\n continue;\n }\n\n const defPos = match.index;\n\n // Find the nearest preceding person/org entity\n // within SEARCH_WINDOW chars before this definition\n let bestEntity: Entity | null = null;\n for (let i = sorted.length - 1; i >= 0; i--) {\n const e = sorted[i];\n if (e === undefined) {\n continue;\n }\n // Must be before the definition\n if (e.end > defPos) {\n continue;\n }\n // Must be within the search window\n if (defPos - e.end > SEARCH_WINDOW) {\n break;\n }\n // Must be a party label\n if (!COREF_SOURCE_LABELS.has(e.label)) {\n continue;\n }\n bestEntity = e;\n break;\n }\n\n if (bestEntity === null) {\n continue;\n }\n\n // Clause-boundary gate: reject if a semicolon or\n // sentence-ending period sits between the source\n // entity and the definition. Periods inside\n // abbreviations like \"r.č.\" should not block an\n // otherwise valid definition.\n const gapText = fullText.slice(bestEntity.end, defPos);\n if (/(?:;|\\.(?=\\s*(?:[\"'„‚(]*\\p{Lu}|$)))/u.test(gapText)) {\n continue;\n }\n\n // Similarity gate: the alias must have textual\n // overlap with the source entity. Prevents roles\n // (\"Executive\", \"Seller\") and structural terms\n // (\"Agreement\", \"Effective Date\") from being\n // treated as name aliases.\n if (!hasEntitySimilarity(alias, bestEntity.text)) {\n continue;\n }\n\n const key = `${alias.toLowerCase()}::${bestEntity.label}`;\n if (seen.has(key)) {\n continue;\n }\n seen.add(key);\n\n terms.push({\n alias,\n label: bestEntity.label,\n definitionStart: defPos,\n sourceText: bestEntity.text,\n });\n }\n }\n\n return terms;\n};\n\n/**\n * Check if a character is a Unicode word character\n * (letter, digit, or combining mark). Used for word\n * boundary checks in coreference matching.\n */\nconst isWordChar = (ch: string | undefined): boolean => {\n if (ch === undefined) {\n return false;\n }\n return /[\\p{L}\\p{M}\\p{N}]/u.test(ch);\n};\n\n/**\n * Find all occurrences of defined-term aliases in the\n * full text. Returns Entity spans for each match.\n *\n * Respects word boundaries: \"Kupující\" must not match\n * inside \"Kupujícímu\". A match is valid only if the\n * character before the start and after the end are NOT\n * word characters (letter/digit).\n *\n * Populates `ctx.corefSourceMap` with entries linking\n * each coref entity to its source entity text, for\n * consistent placeholder numbering.\n */\nexport const findCoreferenceSpans = (\n fullText: string,\n terms: DefinedTerm[],\n ctx: PipelineContext = defaultContext,\n): Entity[] => {\n const results: Entity[] = [];\n\n for (const term of terms) {\n let searchFrom = 0;\n while (searchFrom < fullText.length) {\n const idx = fullText.indexOf(term.alias, searchFrom);\n if (idx === -1) {\n break;\n }\n\n const matchEnd = idx + term.alias.length;\n\n // Word boundary check: the character before the\n // match and after the match must not be a word\n // character. This prevents \"Kupující\" matching\n // inside \"Kupujícímu\".\n const charBefore = idx > 0 ? fullText[idx - 1] : undefined;\n const charAfter = fullText[matchEnd];\n\n if (isWordChar(charBefore) || isWordChar(charAfter)) {\n // Not at a word boundary — skip\n searchFrom = idx + 1;\n continue;\n }\n\n const entity: Entity = {\n start: idx,\n end: matchEnd,\n label: term.label,\n text: term.alias,\n score: 0.95,\n source: DETECTION_SOURCES.COREFERENCE,\n };\n ctx.corefSourceMap.set(corefKey(entity), term.sourceText);\n results.push(entity);\n\n searchFrom = matchEnd;\n }\n }\n\n return results;\n};\n","import type { Match, PatternEntry } from \"@stll/text-search\";\n\nimport { DETECTION_SOURCES } from \"../types\";\nimport type { Entity, GazetteerEntry } from \"../types\";\nimport type { GazetteerData } from \"../build-unified-search\";\n\nconst MAX_EDIT_DISTANCE = 2;\nconst MIN_FUZZY_LENGTH = 4;\n// \" s.r.o.\" = 7 bytes (space + 6 chars). Must be\n// at least 7 to capture the most common Czech LLC\n// suffix without truncating the trailing dot.\nconst MAX_PREFIX_OVERSHOOT = 7;\n\n/**\n * Collect all searchable strings (canonical + variants)\n * from gazetteer entries, mapped to their labels and\n * entry IDs.\n */\nexport const buildSearchTerms = (\n entries: GazetteerEntry[],\n): Map<string, { label: string; entryId: string }> => {\n const terms = new Map<string, { label: string; entryId: string }>();\n for (const entry of entries) {\n const meta = {\n label: entry.label,\n entryId: entry.id,\n };\n terms.set(entry.canonical, meta);\n for (const variant of entry.variants) {\n terms.set(variant, meta);\n }\n }\n return terms;\n};\n\n/**\n * Build TextSearch-compatible patterns from gazetteer\n * entries. Returns:\n * - Exact literal patterns for all terms\n * - Fuzzy patterns (distance: 2) for terms >= 4 chars\n * - Parallel metadata arrays for post-processing\n *\n * Patterns are ordered: all exact first, then all\n * fuzzy. The isFuzzy array marks which are which.\n */\nexport const buildGazetteerPatterns = (\n entries: GazetteerEntry[],\n): {\n patterns: PatternEntry[];\n data: GazetteerData;\n} => {\n const terms = buildSearchTerms(entries);\n\n const patterns: PatternEntry[] = [];\n const labels: string[] = [];\n const isFuzzy: boolean[] = [];\n\n // Pass 1: exact literals (all terms).\n // Use per-pattern wholeWords: false because\n // user-supplied entries may contain dots or\n // special chars (e.g. \"a.s.\", \"AT&T\") that\n // break word-boundary matching. Deny-list and\n // street-type patterns use per-pattern\n // wholeWords: true instead.\n for (const [term, meta] of terms) {\n patterns.push({\n pattern: term,\n literal: true as const,\n wholeWords: false,\n });\n labels.push(meta.label);\n\n isFuzzy.push(false);\n }\n\n // Pass 2: fuzzy patterns (terms >= 4 chars).\n // These are separate patterns with distance: 2,\n // routed to @stll/fuzzy-search by TextSearch.\n for (const [term, meta] of terms) {\n if (term.length < MIN_FUZZY_LENGTH) {\n continue;\n }\n patterns.push({\n pattern: term,\n distance: MAX_EDIT_DISTANCE,\n });\n labels.push(meta.label);\n\n isFuzzy.push(true);\n }\n\n return {\n patterns,\n data: { labels, isFuzzy },\n };\n};\n\n/**\n * Process gazetteer matches from the unified literal\n * search. Receives all matches; filters to the\n * gazetteer slice via sliceStart/sliceEnd.\n *\n * Exact matches get score 0.9; fuzzy matches get\n * 0.85. Fuzzy matches that overlap an exact match\n * are dropped.\n *\n * For exact matches, attempts prefix extension for\n * legal suffixes (\"a.s.\", \"GmbH\", \"s.r.o.\" after\n * the matched term).\n */\nexport const processGazetteerMatches = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText: string,\n data: GazetteerData,\n): Entity[] => {\n const results: Entity[] = [];\n // Track exact-match spans for overlap filtering\n const exactSpans: Array<{\n start: number;\n end: number;\n }> = [];\n\n // Pass 1: exact matches (isFuzzy === false)\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) {\n continue;\n }\n const localIdx = idx - sliceStart;\n if (data.isFuzzy[localIdx]) {\n continue;\n }\n\n const label = data.labels[localIdx];\n if (!label) {\n continue;\n }\n\n // Try prefix extension for legal entity suffixes\n const extended = tryPrefixExtension(fullText, match.start, match.end);\n const 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 results.push({\n start: match.start,\n end,\n label,\n text,\n score: 0.9,\n source: DETECTION_SOURCES.GAZETTEER,\n });\n }\n\n // Pass 2: fuzzy matches (isFuzzy === true),\n // skipping those that overlap exact spans\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) {\n continue;\n }\n const localIdx = idx - sliceStart;\n if (!data.isFuzzy[localIdx]) {\n continue;\n }\n\n // Skip distance-0 fuzzy hits (already caught\n // as exact matches above)\n if (match.distance === 0) {\n continue;\n }\n\n const label = data.labels[localIdx];\n if (!label) {\n continue;\n }\n\n // Skip if overlapping any exact span\n const overlapsExact = exactSpans.some(\n (e) => match.start < e.end && match.end > e.start,\n );\n if (overlapsExact) {\n continue;\n }\n\n const matchText = fullText.slice(match.start, match.end);\n results.push({\n start: match.start,\n end: match.end,\n label,\n text: matchText,\n score: 0.85,\n source: DETECTION_SOURCES.GAZETTEER,\n });\n }\n\n return results;\n};\n\n/**\n * Try to extend an exact match to capture one\n * trailing token (max 6 chars) that may be a legal\n * entity suffix (e.g., \"a.s.\", \"GmbH\", \"s.r.o.\").\n *\n * Does not validate the token against a legal-forms\n * list; false extensions are filtered by mergeAndDedup\n * when a legal-form detector produces a competing\n * entity with the correct span.\n */\nconst tryPrefixExtension = (\n fullText: string,\n start: number,\n end: number,\n): { end: number; text: string } | null => {\n const maxEnd = Math.min(end + MAX_PREFIX_OVERSHOOT, fullText.length);\n if (maxEnd <= end + 1) {\n return null;\n }\n\n const after = fullText.slice(end, maxEnd);\n if (!after.startsWith(\" \")) {\n return null;\n }\n const nextSpace = after.indexOf(\" \", 1);\n const suffixEnd = nextSpace !== -1 ? nextSpace : after.length;\n if (suffixEnd <= 1) {\n return null;\n }\n\n const newEnd = end + suffixEnd;\n return {\n end: newEnd,\n text: fullText.slice(start, newEnd),\n };\n};\n\n// Deprecated exports (kept for API compat).\n","/**\n * Shared text utilities for detectors.\n *\n * Extracted from names.ts and deny-list.ts to avoid\n * duplicating regex constants and helper functions.\n */\n\n/** Matches a string that starts with an uppercase letter. */\nexport const UPPER_START_RE = /^\\p{Lu}/u;\n\n/** Matches a string consisting entirely of uppercase letters. */\nexport const ALL_UPPER_RE = /^\\p{Lu}+$/u;\n\nconst SENTENCE_END_RE = /[.!?]/;\n\n/**\n * Detect whether a position is at the start of a sentence.\n * Looks backward past whitespace for sentence-ending\n * punctuation (.!?). Position 0 and positions preceded\n * only by whitespace are considered sentence starts.\n */\nexport const isSentenceStart = (text: string, pos: number): boolean => {\n if (pos === 0) {\n return true;\n }\n let i = pos - 1;\n while (i >= 0 && /\\s/.test(text[i] ?? \"\")) {\n i--;\n }\n if (i < 0) {\n return true;\n }\n return SENTENCE_END_RE.test(text[i] ?? \"\");\n};\n","import { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\nimport type { PipelineContext, NameCorpusData } from \"../context\";\nimport { defaultContext } from \"../context\";\nimport { ALL_UPPER_RE, UPPER_START_RE, isSentenceStart } from \"../util/text\";\n\n// ── Name corpus ──────────────────────────────────────\n// Per-language first names and surnames loaded from\n// Wikidata-sourced dictionaries from @stll/anonymize-data (optional),\n// plus legacy config/names-*.json for backwards compat.\n// Merged at init time across all configured languages.\n\n// ── Accessors (read from context) ────────────────────\n\nconst getCorpus = (ctx: PipelineContext): NameCorpusData | null =>\n ctx.nameCorpus;\n\n// Exported accessors for deny-list.ts AC integration.\nexport const getNameCorpusFirstNames = (\n ctx: PipelineContext = defaultContext,\n): readonly string[] => ctx.nameCorpus?.firstNamesList ?? [];\nexport const getNameCorpusSurnames = (\n ctx: PipelineContext = defaultContext,\n): readonly string[] => ctx.nameCorpus?.surnamesList ?? [];\nexport const getNameCorpusTitles = (\n ctx: PipelineContext = defaultContext,\n): readonly string[] => ctx.nameCorpus?.titlesList ?? [];\nexport const getNameCorpusExcluded = (\n ctx: PipelineContext = defaultContext,\n): readonly string[] => ctx.nameCorpus?.excludedList ?? [];\n\n/**\n * Languages with per-language first/surname\n * dictionaries from @stll/anonymize-data (optional).\n */\nconst NAME_LANGUAGES = [\n \"cs\",\n \"sk\",\n \"de\",\n \"pl\",\n \"hu\",\n \"ro\",\n \"fr\",\n \"es\",\n \"it\",\n \"en\",\n \"sv\",\n] as const;\n\ntype JsonArrayModule = {\n default: readonly string[];\n};\n\n/**\n * Try importing a JSON module; return empty array\n * if not found.\n */\nconst tryImportArray = async (path: string): Promise<readonly string[]> => {\n try {\n const mod = (await import(path)) as JsonArrayModule;\n return mod.default;\n } catch {\n return [];\n }\n};\n\n/**\n * Load name corpus data from per-language dictionary\n * files 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 */\nexport const initNameCorpus = (\n ctx: PipelineContext = defaultContext,\n): Promise<void> => {\n if (ctx.nameCorpusPromise) return ctx.nameCorpusPromise;\n const promise = (async () => {\n try {\n // Load legacy config files (backwards compat)\n const [legacyFirstMod, legacySurnameMod, titleMod, exclusionMod] =\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 ]);\n\n // Load per-language dictionaries in parallel\n const firstImports = NAME_LANGUAGES.map((lang) =>\n tryImportArray(\n `@stll/anonymize-data/dictionaries/names/first/${lang}.json`,\n ),\n );\n const surnameImports = NAME_LANGUAGES.map((lang) =>\n tryImportArray(\n `@stll/anonymize-data/dictionaries/names/surnames/${lang}.json`,\n ),\n );\n\n const [firstResults, surnameResults] = await Promise.all([\n Promise.all(firstImports),\n Promise.all(surnameImports),\n ]);\n\n // Merge: legacy config + all per-language files\n const firstNames: string[] = [...legacyFirstMod.default.names];\n for (const names of firstResults) {\n for (const name of names) {\n firstNames.push(name);\n }\n }\n\n const surnames: string[] = [...legacySurnameMod.default.names];\n for (const names of surnameResults) {\n for (const name of names) {\n surnames.push(name);\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 dedupFirst = dedup(firstNames);\n const dedupSurnames = dedup(surnames);\n const titles = titleMod.default.tokens;\n const exclusions = exclusionMod.default.words;\n\n ctx.nameCorpus = {\n firstNames: Object.freeze(new Set(dedupFirst)),\n surnames: Object.freeze(new Set(dedupSurnames)),\n titleTokens: Object.freeze(new Set(titles)),\n excludedWords: Object.freeze(new Set(exclusions)),\n firstNamesList: Object.freeze(dedupFirst),\n surnamesList: Object.freeze(dedupSurnames),\n titlesList: Object.freeze(titles),\n excludedList: Object.freeze(exclusions),\n };\n } catch (err) {\n // Reset so the next call retries the load rather\n // than returning this (already-resolved) failed\n // promise. Current awaiters still get a resolved\n // (not rejected) Promise; ctx.nameCorpus stays null.\n ctx.nameCorpusPromise = null;\n console.warn(\n \"[anonymize] Failed to load name corpus JSON\" +\n \" — name detection disabled:\",\n err,\n );\n }\n })();\n ctx.nameCorpusPromise = promise;\n return promise;\n};\n\n// ── Czech/Slovak suffix stripping ────────────────────\n// Case suffixes commonly appended to names in declined\n// Czech/Slovak text. Ordered longest-first.\n\nconst INFLECTION_SUFFIXES = [\n \"ovi\", // dative\n \"em\", // instrumental\n \"om\", // instrumental (some stems)\n \"ou\", // instrumental feminine\n \"é\", // dative/locative feminine\n \"a\", // genitive\n \"u\", // accusative/locative\n] as const;\n\n/**\n * Strip common Czech/Slovak case suffixes from a token.\n * Returns candidate base forms if stripping produces a\n * plausible name (capitalised, length >= 3).\n *\n * For the \"-ou\" instrumental feminine suffix, also yields\n * base + \"a\" (e.g., \"Editou\" → \"Edit\" and \"Edita\")\n * because Czech feminine names decline -a → -ou.\n */\nconst stripInflection = (token: string): string[] => {\n const candidates: string[] = [];\n for (const suffix of INFLECTION_SUFFIXES) {\n if (token.length > suffix.length + 2 && token.endsWith(suffix)) {\n const base = token.slice(0, -suffix.length);\n if (/^\\p{Lu}/u.test(base)) {\n candidates.push(base);\n // Czech feminine: -a → -ou (instrumental),\n // -a → -é (dative/locative), -a → -u (accusative)\n if (suffix === \"ou\" || suffix === \"é\" || suffix === \"u\") {\n candidates.push(`${base}a`);\n }\n // Czech feminine: -a → -ovi is not valid, but\n // -e → -em is (e.g., \"Kalhousem\" → \"Kalhous\")\n // which is already handled by stripping \"em\".\n }\n }\n }\n return candidates;\n};\n\n// ── Token types ──────────────────────────────────────\n\nconst TOKEN_TYPE = {\n NAME: \"name\",\n SURNAME: \"surname\",\n TITLE: \"title\",\n ABBREVIATION: \"abbreviation\",\n CAPITALIZED: \"capitalized\",\n OTHER: \"other\",\n} as const;\n\ntype TokenType = (typeof TOKEN_TYPE)[keyof typeof TOKEN_TYPE];\n\ntype ClassifiedToken = {\n text: string;\n type: TokenType;\n start: number;\n end: number;\n};\n\nconst PERSON_CHAIN_BREAK_RE = /[!?;:]/u;\n\nconst isInitialContinuationGap = (text: string, gap: string): boolean =>\n (/^\\p{Lu}$/u.test(text) && /^\\.[^\\S\\n]{1,2}$/u.test(gap)) ||\n /^[^\\S\\n]{1,2}(?:\\p{Lu}\\.[^\\S\\n]{1,2})+$/u.test(gap);\n\n// ── Helpers ──────────────────────────────────────────\n\n/**\n * Check if a token is in the first-name set, either\n * directly or after stripping Czech/Slovak inflection.\n */\nconst isFirstNameToken = (token: string, corpus: NameCorpusData): boolean => {\n if (corpus.firstNames.has(token)) {\n return true;\n }\n return stripInflection(token).some((b) => corpus.firstNames.has(b));\n};\n\n/**\n * Check if a token is in the surname set, either\n * directly or after stripping Czech/Slovak inflection.\n */\nconst isSurnameToken = (token: string, corpus: NameCorpusData): boolean => {\n if (corpus.surnames.has(token)) {\n return true;\n }\n return stripInflection(token).some((b) => corpus.surnames.has(b));\n};\n\n/**\n * Check if a token looks like a single-letter\n * abbreviation: \"J.\", \"M.\", etc.\n */\nconst isAbbreviation = (token: string): boolean =>\n token.length === 2 && /^\\p{Lu}$/u.test(token[0] ?? \"\") && token[1] === \".\";\n\n// ── Word segmentation ────────────────────────────────\n\nconst segmenter = new Intl.Segmenter(undefined, {\n granularity: \"word\",\n});\n\ntype WordSegment = {\n text: string;\n start: number;\n end: number;\n};\n\n/**\n * Split text into word segments using Intl.Segmenter.\n * Only returns segments flagged as words.\n */\nconst segmentWords = (fullText: string): WordSegment[] => {\n const words: WordSegment[] = [];\n for (const seg of segmenter.segment(fullText)) {\n if (seg.isWordLike) {\n words.push({\n text: seg.segment,\n start: seg.index,\n end: seg.index + seg.segment.length,\n });\n }\n }\n return words;\n};\n\n// ── Helpers for chain scoring ────────────────────────\n\n/** NAME or SURNAME — both represent corpus-matched tokens */\nconst isCorpusMatch = (type: TokenType): boolean =>\n type === TOKEN_TYPE.NAME || type === TOKEN_TYPE.SURNAME;\n\n// ── Token classification ─────────────────────────────\n\nconst classifyToken = (\n word: WordSegment,\n corpus: NameCorpusData,\n): ClassifiedToken => {\n const { text, start, end } = word;\n const lower = text.toLowerCase();\n\n // Strip trailing period for title check (e.g., \"Ing.\")\n const stripped = text.endsWith(\".\") ? text.slice(0, -1).toLowerCase() : lower;\n\n if (corpus.titleTokens.has(stripped)) {\n return { text, type: TOKEN_TYPE.TITLE, start, end };\n }\n\n if (isAbbreviation(text)) {\n return {\n text,\n type: TOKEN_TYPE.ABBREVIATION,\n start,\n end,\n };\n }\n\n // Skip excluded words\n if (corpus.excludedWords.has(lower)) {\n return { text, type: TOKEN_TYPE.OTHER, start, end };\n }\n\n // Minimum length 3\n if (text.length < 3) {\n return { text, type: TOKEN_TYPE.OTHER, start, end };\n }\n\n // Skip all-uppercase tokens > 3 chars (likely acronyms)\n if (text.length > 3 && ALL_UPPER_RE.test(text)) {\n return { text, type: TOKEN_TYPE.OTHER, start, end };\n }\n\n // Must start with uppercase\n if (!UPPER_START_RE.test(text)) {\n return { text, type: TOKEN_TYPE.OTHER, start, end };\n }\n\n if (isFirstNameToken(text, corpus)) {\n return { text, type: TOKEN_TYPE.NAME, start, end };\n }\n\n if (isSurnameToken(text, corpus)) {\n return { text, type: TOKEN_TYPE.SURNAME, start, end };\n }\n\n // Capitalised word (not in corpus but starts uppercase)\n return {\n text,\n type: TOKEN_TYPE.CAPITALIZED,\n start,\n end,\n };\n};\n\n// ── Chain assembly ───────────────────────────────────\n\n/**\n * Detect person names by looking up tokens against the\n * name corpus, then chaining adjacent name-like tokens.\n *\n * Requires initNameCorpus() to have been called first.\n * If not initialized, returns an empty array.\n *\n * Scoring:\n * TITLE + NAME/SURNAME → 0.95\n * NAME + NAME/SURNAME → 0.9\n * SURNAME + NAME/SURNAME → 0.9\n * NAME + CAPITALIZED → 0.7\n * ABBREVIATION + NAME → 0.7\n * Standalone NAME → 0.5 (low confidence)\n * Standalone SURNAME → skip (too ambiguous)\n */\nexport const detectNameCorpus = (\n fullText: string,\n ctx: PipelineContext = defaultContext,\n): Entity[] => {\n const corpus = getCorpus(ctx);\n if (!corpus) {\n return [];\n }\n\n const words = segmentWords(fullText);\n const tokens = words.map((w) => classifyToken(w, corpus));\n const entities: Entity[] = [];\n const consumed = new Set<number>();\n\n for (let i = 0; i < tokens.length; i++) {\n if (consumed.has(i)) {\n continue;\n }\n\n const token = tokens[i];\n if (!token) {\n continue;\n }\n\n // Only start chains from TITLE, NAME, SURNAME,\n // or ABBREVIATION\n if (\n token.type !== TOKEN_TYPE.TITLE &&\n token.type !== TOKEN_TYPE.NAME &&\n token.type !== TOKEN_TYPE.SURNAME &&\n token.type !== TOKEN_TYPE.ABBREVIATION\n ) {\n continue;\n }\n\n // Build a chain of adjacent relevant tokens.\n // Max 5 tokens to prevent merging independent names\n // (e.g., \"Jan Novák Pavel Moc\" should be two entities).\n const MAX_CHAIN = 5;\n const chain: ClassifiedToken[] = [token];\n let j = i + 1;\n\n while (j < tokens.length && chain.length < MAX_CHAIN) {\n const next = tokens[j];\n if (!next) {\n break;\n }\n\n // Break chain if there's a newline between tokens\n const prev = chain.at(-1);\n if (prev) {\n const gap = fullText.slice(prev.end, next.start);\n const breaksOnPeriod =\n gap.includes(\".\") && !isInitialContinuationGap(prev.text, gap);\n if (\n gap.includes(\"\\n\") ||\n PERSON_CHAIN_BREAK_RE.test(gap) ||\n breaksOnPeriod\n ) {\n break;\n }\n }\n\n // Only chain NAME, SURNAME, TITLE, ABBREVIATION,\n // CAPITALIZED\n if (\n next.type === TOKEN_TYPE.NAME ||\n next.type === TOKEN_TYPE.SURNAME ||\n next.type === TOKEN_TYPE.TITLE ||\n next.type === TOKEN_TYPE.ABBREVIATION ||\n next.type === TOKEN_TYPE.CAPITALIZED\n ) {\n chain.push(next);\n j++;\n } else {\n break;\n }\n }\n\n // Score the chain\n const hasTitle = chain.some((t) => t.type === TOKEN_TYPE.TITLE);\n const hasCorpusName = chain.some((t) => isCorpusMatch(t.type));\n const hasFirstName = chain.some((t) => t.type === TOKEN_TYPE.NAME);\n const hasAbbreviation = chain.some(\n (t) => t.type === TOKEN_TYPE.ABBREVIATION,\n );\n const corpusCount = chain.filter((t) => isCorpusMatch(t.type)).length;\n const capitalizedCount = chain.filter(\n (t) => t.type === TOKEN_TYPE.CAPITALIZED,\n ).length;\n\n // Determine score based on chain composition\n let score = 0;\n\n if (hasTitle && hasCorpusName) {\n // TITLE + NAME/SURNAME → high confidence\n score = 0.95;\n } else if (corpusCount >= 2) {\n // NAME + NAME, NAME + SURNAME, etc. → high confidence\n score = 0.9;\n } else if (hasCorpusName && capitalizedCount > 0) {\n // NAME/SURNAME + CAPITALIZED → medium confidence\n score = 0.7;\n } else if (hasAbbreviation && hasCorpusName) {\n // ABBREVIATION + NAME/SURNAME → medium confidence\n score = 0.7;\n } else if (hasFirstName && chain.length === 1) {\n // Standalone first NAME → low confidence\n // Skip if at sentence start (likely not a name)\n if (isSentenceStart(fullText, token.start)) {\n continue;\n }\n score = 0.5;\n } else if (\n !hasFirstName &&\n chain.length === 1 &&\n chain[0]?.type === TOKEN_TYPE.SURNAME\n ) {\n // Standalone SURNAME → skip (too ambiguous alone)\n continue;\n } else if (hasTitle && chain.length === 1) {\n // Standalone TITLE → skip (not a name by itself)\n continue;\n } else {\n // No corpus match in chain → skip\n if (!hasCorpusName) {\n continue;\n }\n score = 0.5;\n }\n\n // Build entity span from first to last token in chain\n const first = chain.at(0);\n const last = chain.at(-1);\n if (!first || !last) {\n continue;\n }\n\n const start = first.start;\n const end = last.end;\n const text = fullText.slice(start, end);\n\n // Mark all chain tokens as consumed\n for (let k = i; k < i + chain.length; k++) {\n consumed.add(k);\n }\n\n entities.push({\n start,\n end,\n label: \"person\",\n text,\n score,\n source: DETECTION_SOURCES.REGEX,\n });\n }\n\n return entities;\n};\n","/**\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 \"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 \"Señor\",\n \"Señora\",\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] as const;\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/** Regex char class matching all single-quote variants. */\nexport const QUOTE_SINGLE = charClass(\"quote-single\");\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 } from \"@stll/text-search\";\nimport type { Validator } from \"@stll/stdnum\";\nimport {\n at,\n be,\n bg,\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 pl,\n pt,\n ro,\n se,\n si,\n sk,\n} from \"@stll/stdnum\";\nimport { toRegex } from \"@stll/stdnum/patterns\";\n\nimport {\n HONORIFIC_BOUNDARY,\n HONORIFICS,\n POST_NOMINALS,\n TITLE_PREFIXES,\n} from \"../config/titles\";\nimport { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\nimport { DASH, DASH_INNER } from \"../util/char-groups\";\n\nconst MIN_PHONE_LENGTH = 7;\nconst MIN_MONTH_NAME_LENGTH = 3;\n\n// ── Shared helpers ──────────────────────────────────\n\nconst escapeTitle = (title: string): string =>\n title\n // eslint-disable-next-line no-useless-escape\n .replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\s+/g, \"\\\\s*\");\n\n/** Escape for use inside a regex alternation. */\nconst escapeRegex = (s: string): string =>\n // eslint-disable-next-line no-useless-escape\n s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\n/** Escape for use inside a regex character class. */\nconst escapeCharClass = (s: string): string => s.replace(/[\\]\\\\^-]/g, \"\\\\$&\");\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// biome-ignore lint/security/noSecrets: diacritics char class\nconst UPPER_CZ = \"A-ZÁČĎÉĚÍŇÓŘŠŤÚŮÝŽ\";\n// biome-ignore lint/security/noSecrets: diacritics char class\nconst LOWER_CZ = \"a-záčďéěíňóřšťúůýžäöüß\";\nconst NAME_WORD = `[${UPPER_CZ}][${LOWER_CZ}]+`;\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\nconst SP = \"[^\\\\S\\\\n\\\\t]\";\n\n/** Honorific alternation built from titles.ts config. */\nconst HONORIFIC_ALT = [...HONORIFICS]\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// ── Pattern definitions ─────────────────────────────\n\nexport type RegexMeta = {\n label: string;\n score: number;\n /** Post-match stdnum validator for confirmation. */\n validator?: Validator;\n};\n\ntype RegexDef = {\n pattern: string;\n label: string;\n score: number;\n validator?: Validator;\n};\n\n// ── stdnum validator entries ────────────────────────\n// Each entry pairs a @stll/stdnum validator with a\n// label and confidence score. The pattern derived via\n// toRegex(validator).source is used as the regex; the\n// validator itself is stored in META for post-match\n// confirmation (see processRegexMatches).\n\ntype StdnumEntry = {\n validator: Validator;\n label: string;\n score: number;\n pattern: string;\n};\n\nconst toEntry = (\n validator: Validator,\n label: string,\n score: number,\n): StdnumEntry | null => {\n const pattern = toRegex(validator).source;\n if (!pattern) return null;\n return {\n validator,\n label,\n score,\n pattern,\n };\n};\n\n/**\n * Stdnum validators for national/company IDs.\n *\n * Selection criteria: only patterns specific enough\n * to avoid excessive false positives (country-prefixed\n * VAT numbers, structured personal IDs). Generic\n * digit-only patterns (e.g. \\d{8}) are excluded unless\n * the validator's checksum is strong enough to filter.\n */\nconst STDNUM_ENTRIES: readonly StdnumEntry[] = [\n // ── Original PR #28 patterns (were 15-21) ────────\n toEntry(hu.vat, \"tax identification number\", 0.95),\n toEntry(it.codiceFiscale, \"national identification number\", 0.95),\n toEntry(es.dni, \"national identification number\", 0.9),\n toEntry(es.nie, \"national identification number\", 0.95),\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 // ── 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 // ── 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 toEntry(es.cif, \"registration number\", 0.95),\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].filter((e): e is StdnumEntry => e !== null);\n\n// ── Named pattern definitions ────────────────────────\n\nconst TITLED_PERSON: RegexDef = {\n pattern:\n `(?:${TITLE_PREFIX})` +\n `(?:${SP}+(?:${TITLE_PREFIX}))*` +\n `${SP}+` +\n `(?:${NAME_WORD})` +\n `(?:${SP}{1,4}(?:${PARTICLE}${SP}+)?` +\n `${NAME_WORD}){1,3}` +\n `(?:,?${SP}+(?:${POST_NOMINAL})(?:,?${SP}+(?:${POST_NOMINAL}))*)?`,\n label: \"person\",\n score: 0.95,\n};\n\nconst HONORIFIC_PERSON: RegexDef = {\n pattern:\n `(?:${HONORIFIC_ALT})` +\n `\\\\.?${SP}+${NAME_WORD}` +\n `(?:(?:${SP}|-){1,2}(?:${PARTICLE}${SP}+)?` +\n `${NAME_WORD}){0,3}` +\n `(?:${SP}+(?:QC|KC|SC|LJ|AG))?`,\n label: \"person\",\n score: 0.95,\n};\n\nconst IBAN: RegexDef = {\n pattern:\n `\\\\b[A-Z]{2}\\\\d{2}\\\\s?[\\\\dA-Z]{4}\\\\s?[\\\\dA-Z]{4}` +\n `\\\\s?[\\\\dA-Z]{4}\\\\s?[\\\\dA-Z]{4}` +\n `\\\\s?[\\\\dA-Z]{0,14}\\\\b`,\n label: \"iban\",\n score: 1,\n};\n\nconst EMAIL: RegexDef = {\n pattern: `\\\\b[\\\\w.+\\\\-]+@[\\\\w\\\\-]+(?:\\\\.[\\\\w\\\\-]+)+\\\\b`,\n label: \"email address\",\n score: 1,\n};\n\n// [^\\S\\n] instead of \\s: separators must not\n// match newlines (prevents cross-line bleeding).\nconst INTL_PHONE: RegexDef = {\n pattern:\n `\\\\+\\\\d{1,3}(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\(?\\\\d{2,4}\\\\)?` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{2,4}` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{0,4}\\\\b`,\n label: \"phone number\",\n score: 1,\n};\n\n// Czech phone numbers: mobiles start with 6/7,\n// landlines with 2-5. Restrict to [2-7] but require\n// the full 9-digit pattern to avoid matching monetary\n// amounts. The negative lookahead prevents bank\n// account patterns (digits/digits).\nconst CZ_PHONE: RegexDef = {\n pattern:\n `\\\\b[2-7]\\\\d{2}(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}` +\n `(?!(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d*/\\\\d)` +\n `(?![^\\\\S\\\\n]*(?:Kč|,-|korun|EUR|USD|€|\\\\$))\\\\b`,\n label: \"phone number\",\n score: 0.85,\n};\n\n/**\n * Phone numbers prefixed with \"tel.:\" or \"telefon:\".\n * Captures the number after the prefix, including\n * optional international code (+420).\n */\nconst TEL_PREFIX_PHONE: RegexDef = {\n pattern:\n `(?:\\\\b[Tt]el(?:efon)?\\\\.?\\\\s*:?\\\\s*)` +\n `(?:\\\\+?\\\\d{1,3}[^\\\\S\\\\n]?)?` +\n `\\\\d{3}(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}\\\\b`,\n label: \"phone number\",\n score: 0.95,\n};\n\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: \"czech birth number\",\n score: 1,\n validator: cz.rc,\n};\n\nconst DATE_NUMERIC: RegexDef = {\n pattern:\n `\\\\b(?:\\\\d{1,2}[./]\\\\d{1,2}[./]\\\\d{2,4}` +\n `|\\\\d{4}-\\\\d{2}-\\\\d{2}` +\n `|\\\\d{4}\\\\.\\\\d{2}\\\\.\\\\d{2})\\\\b`,\n label: \"date\",\n score: 1,\n};\n\nconst DATE_CZ_SPACED: RegexDef = {\n pattern: `\\\\b\\\\d{1,2}\\\\.[^\\\\S\\\\n]+\\\\d{1,2}\\\\.[^\\\\S\\\\n]+\\\\d{4}\\\\b`,\n label: \"date\",\n score: 1,\n};\n\nconst IP_ADDRESS: RegexDef = {\n pattern:\n `\\\\b(?:(?:25[0-5]|2[0-4]\\\\d|[01]?\\\\d\\\\d?)\\\\.){3}` +\n `(?:25[0-5]|2[0-4]\\\\d|[01]?\\\\d\\\\d?)\\\\b`,\n label: \"ip address\",\n score: 1,\n};\n\nconst CZ_BANK_ACCOUNT: RegexDef = {\n pattern: `\\\\b(?:\\\\d{1,6}-)?\\\\d{6,10}/\\\\d{4}(?!\\\\d)`,\n label: \"bank account number\",\n score: 0.95,\n};\n\n// Hungarian Budapest landline (+36 1 XXX XXXX).\n// 2+ digit area codes handled by INTL_PHONE.\nconst HU_LANDLINE: RegexDef = {\n pattern:\n `\\\\+36(?:[^\\\\S\\\\n]|[.\\\\-])?1(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{3}` +\n `(?:[^\\\\S\\\\n]|[.\\\\-])?\\\\d{4}\\\\b`,\n label: \"phone number\",\n score: 0.9,\n};\n\n// Czech license plates (SPZ/RZ).\n// New format: 3SJ 0753 — digit, two letters, space?,\n// four digits. Old format: 1A2 3456 — digit, letter,\n// digit, space?, four digits.\n\n// Czech/Slovak postal code: \"110 00\", \"120 00\".\n// The distinctive XXX XX format with mandatory\n// space is specific enough to avoid most false\n// positives.\nconst CZ_POSTAL: RegexDef = {\n pattern: `\\\\b\\\\d{3}[^\\\\S\\\\n]\\\\d{2}\\\\b`,\n label: \"address\",\n score: 0.7,\n};\n\n// URL: scheme + host + optional port + path + query +\n// fragment. Trailing prose punctuation excluded but\n// ? = & # kept for query strings.\n// Allow missing // after http:/https: — common OCR\n// artifact (\"http:example.cz\" instead of\n// \"http://example.cz\"). Lookahead ensures bare scheme\n// is not matched in isolation (e.g., \"http:\" at EOL).\nconst URL: RegexDef = {\n pattern:\n `(?:https?://|https?:(?=[^\\\\s])|www\\\\.)` +\n `[\\\\w\\\\-]+(?:\\\\.[\\\\w\\\\-]+)+` +\n `(?::\\\\d+)?` +\n `(?:[/?#][^\\\\s)\\\\]>]*[^\\\\s.,;:!?)\\\\]>])?`,\n label: \"url\",\n score: 1,\n};\n\n// Bare domain: no protocol/www prefix, ends with a\n// known TLD. Catches \"fondkinematografie.cz\" etc.\n// Uses [a-zA-Z0-9] (no underscores — invalid in\n// hostnames). Short ambiguous TLDs (de, at, no, se,\n// fi, dk, be, it, uk) require at least one subdomain\n// dot to reduce false positives in European legal\n// text; unambiguous TLDs allow bare second-level\n// domains (e.g., \"fondkinematografie.cz\").\nconst LONG_TLDS =\n \"com|org|net|eu|cz|sk|pl|hu|ro|fr|es\" + \"|co\\\\.uk|nl|ch|info|io|dev\";\nconst SHORT_TLDS = \"de|at|be|se|fi|dk|no|it|uk\";\n// RFC 1123: labels cannot start or end with hyphen.\nconst HOST_LABEL = `[a-zA-Z0-9](?:[a-zA-Z0-9\\\\-]*[a-zA-Z0-9])?`;\nconst BARE_HOST = `\\\\b[a-zA-Z0-9][a-zA-Z0-9\\\\-]+[a-zA-Z0-9]`;\nconst PATH_SUFFIX = `(?:[/?#][^\\\\s)\\\\]>]*[^\\\\s.,;:!?)\\\\]>])?`;\nconst BARE_DOMAIN: RegexDef = {\n pattern:\n // Unambiguous TLDs: bare SLDs ok (one dot)\n `${BARE_HOST}(?:\\\\.${HOST_LABEL})*` +\n `\\\\.(?:${LONG_TLDS})\\\\b${PATH_SUFFIX}` +\n `|` +\n // Short/ambiguous TLDs: require subdomain (two+ dots)\n `${BARE_HOST}(?:\\\\.${HOST_LABEL})+` +\n `\\\\.(?:${SHORT_TLDS})\\\\b${PATH_SUFFIX}`,\n label: \"url\",\n score: 0.9,\n};\n\n// Full RFC 5952 IPv6. :: compressed form replaces\n// 1+ zero groups. Right side: 1-7 hex groups.\nconst IPV6_ADDRESS: RegexDef = {\n pattern:\n `\\\\b(?:[0-9a-fA-F]{1,4}:){7}` +\n `[0-9a-fA-F]{1,4}\\\\b` +\n `|\\\\b(?:[0-9a-fA-F]{1,4}:){1,7}:\\\\b` +\n `|::(?:[0-9a-fA-F]{1,4}:){0,6}` +\n `[0-9a-fA-F]{1,4}`,\n label: \"ip address\",\n score: 1,\n};\n\n// MAC: colon-only OR hyphen-only (no mixed).\nconst MAC_ADDRESS: RegexDef = {\n pattern:\n `\\\\b(?:[0-9a-fA-F]{2}:){5}` +\n `[0-9a-fA-F]{2}\\\\b` +\n `|\\\\b(?:[0-9a-fA-F]{2}-){5}` +\n `[0-9a-fA-F]{2}\\\\b`,\n label: \"mac address\",\n score: 1,\n};\n\n// SWIFT/BIC moved to trigger-based detection\n// (triggers.global.json) for better composability.\n\n// 12-hour time: \"5:00 p.m.\", \"12:30 AM\", \"5:00p.m.\",\n// \"11:00 a.m. Eastern Time\". Captures HH:MM and the\n// am/pm marker; optional timezone suffix is not\n// included (it's not PII). Case spelled out explicitly\n// (no (?i)) because DFA compilation fails with\n// Unicode + case-insensitive flag on this pattern.\nconst TIME_12H: RegexDef = {\n pattern:\n `\\\\b(?:1[0-2]|0?[1-9]):[0-5]\\\\d` +\n `[^\\\\S\\\\n]?(?:[aApP]\\\\.?[mM]\\\\.?)` +\n `(?=[\\\\s,;!?)]|$)`,\n label: \"date\",\n score: 0.9,\n};\n\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 IBAN,\n EMAIL,\n INTL_PHONE,\n CZ_PHONE,\n TEL_PREFIX_PHONE,\n CREDIT_CARD,\n CZ_BIRTH_NUMBER,\n DATE_NUMERIC,\n DATE_CZ_SPACED,\n IP_ADDRESS,\n CZ_BANK_ACCOUNT,\n HU_LANDLINE,\n CZ_POSTAL,\n URL,\n IPV6_ADDRESS,\n MAC_ADDRESS,\n BARE_DOMAIN,\n TIME_12H,\n ...STDNUM_ENTRIES,\n];\n\n/** Flat pattern array for text-search. */\nexport const REGEX_PATTERNS: readonly string[] = ALL_REGEX_DEFS.map(\n (d) => d.pattern,\n);\n\n/** Parallel metadata. Index = pattern index. */\nexport const REGEX_META: readonly RegexMeta[] = ALL_REGEX_DEFS.map(\n (d): RegexMeta => {\n const meta: RegexMeta = {\n label: d.label,\n score: d.score,\n };\n if (d.validator) {\n meta.validator = d.validator;\n }\n return meta;\n },\n);\n\n// ── Dynamic date patterns (22 languages) ────────────\n\n/**\n * JSON shape: language codes map to string arrays;\n * metadata keys (prefixed `_`) map to strings.\n * The `_` keys are skipped by `buildMonthAlternation`.\n */\ntype DateMonths = Record<string, string[] | string>;\n\n/**\n * Build month-name alternation from date-months.json.\n * Deduplicates across all 22 languages, filters names\n * shorter than 3 chars (too many false positives), and\n * sorts longest-first so the regex engine prefers the\n * longest match.\n */\nconst buildMonthAlternation = (months: DateMonths): string => {\n const seen = new Set<string>();\n for (const [key, value] of Object.entries(months)) {\n if (key.startsWith(\"_\")) continue;\n const names = Array.isArray(value) ? value : [value];\n for (const name of names) {\n // Strip trailing dots for the regex; date patterns\n // use `\\\\.?` after the alternation to match optional\n // abbreviation dots.\n const clean = name.replace(/\\.$/, \"\").toLowerCase();\n if (clean.length >= MIN_MONTH_NAME_LENGTH) {\n seen.add(clean);\n }\n }\n }\n return [...seen]\n .toSorted((a, b) => b.length - a.length)\n .map(escapeRegex)\n .join(\"|\");\n};\n\n/**\n * Build date patterns from a month-name alternation.\n * Returns 6 patterns covering the major written-date\n * formats across all supported languages.\n */\nconst buildDatePatternsFromMonths = (alt: string): string[] => {\n if (!alt) {\n // No month names survived filtering — return nothing\n // rather than emitting patterns with (?:) that match\n // arbitrary whitespace.\n return [];\n }\n // Optional time suffix: \"19:45:50\" or \"19:45\"\n const TIME = `(?:\\\\s+\\\\d{1,2}:\\\\d{2}(?::\\\\d{2})?)`;\n return [\n // a. DD[.] Month[.] YYYY [HH:MM[:SS]]\n `(?i)\\\\b\\\\d{1,2}\\\\.?\\\\s+(?:${alt})\\\\.?\\\\s+\\\\d{4}${TIME}?\\\\b`,\n // b. Month[.] DD[,] YYYY — \"March 7, 2023\" (US format)\n `(?i)\\\\b(?:${alt})\\\\.?\\\\s+\\\\d{1,2},?\\\\s+\\\\d{4}\\\\b`,\n // g. Month[.] DD — \"December 31\" (no year, US format)\n `(?i)\\\\b(?:${alt})\\\\.?\\\\s+\\\\d{1,2}(?=\\\\s|[.,;!?)]|$)`,\n // c. DDst/nd/rd/th Month[.] [YYYY] — \"1st January 2025\"\n `(?i)\\\\b\\\\d{1,2}(?:st|nd|rd|th)\\\\s+(?:${alt})\\\\.?` +\n `(?:\\\\s+\\\\d{4})?(?=\\\\s|[.,;!?)]|$)`,\n // d. Month[.] YYYY — \"October 1983\"\n `(?i)\\\\b(?:${alt})\\\\.?\\\\s+\\\\d{4}\\\\b`,\n // e. YYYY. Month[.] DD. — Hungarian \"2025. január 7.\"\n `(?i)\\\\b\\\\d{4}\\\\.\\\\s+(?:${alt})\\\\.?\\\\s+\\\\d{1,2}\\\\.?(?=\\\\s|[.,;!?)]|$)`,\n // f. DD de Month[.] [de] YYYY — Spanish \"7 de enero de 2025\"\n `(?i)\\\\b\\\\d{1,2}\\\\s+de\\\\s+(?:${alt})\\\\.?` + `(?:\\\\s+de)?\\\\s+\\\\d{4}\\\\b`,\n ];\n};\n\n/** Cached promise for date patterns. Loaded once. */\nlet datePatternPromise: Promise<string[]> | null = null;\n\nconst loadDatePatterns = async (): Promise<string[]> => {\n const mod = await import(\"../data/date-months.json\");\n // Dynamic import of JSON returns { default, ...keys }.\n // Use `default` if present (ESM wrapper), else the\n // module itself.\n const months: DateMonths = mod.default ?? mod;\n const alt = buildMonthAlternation(months);\n return buildDatePatternsFromMonths(alt);\n};\n\n/**\n * Get dynamically built date patterns from\n * date-months.json. Returns a cached promise; the JSON\n * is loaded only once.\n */\nexport const getDatePatterns = (): Promise<string[]> => {\n if (!datePatternPromise) {\n datePatternPromise = loadDatePatterns().catch((err) => {\n datePatternPromise = null;\n throw err;\n });\n }\n return datePatternPromise;\n};\n\n/** Date pattern metadata (all are score 1 dates). */\nexport const DATE_PATTERN_META: Readonly<RegexMeta> = Object.freeze({\n label: \"date\",\n score: 1,\n});\n\n// ── Dynamic currency patterns ──────────────────────\n\n/**\n * JSON shape from currencies.json: ISO 4217 codes,\n * common currency symbols, and local currency names.\n */\ntype CurrenciesData = {\n codes: string[];\n symbols: string[];\n localNames?: string[];\n};\n\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 buildCurrencyPatterns = (data: CurrenciesData): string[] => {\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 TrailingPart = { len: number; alt: string };\n const parts: TrailingPart[] = data.codes.map((code) => ({\n len: code.length,\n alt: escapeRegex(code),\n }));\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 parts.push({\n len: name.length,\n alt: `(?i:${escaped})`,\n });\n } else {\n parts.push({ len: name.length, alt: escaped });\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 if (symbols) {\n for (const ch of data.symbols) {\n parts.push({\n len: ch.length,\n alt: escapeRegex(ch),\n });\n }\n }\n\n const trailingAlt = parts\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\n const patterns: string[] = [];\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 // Leading symbol: $100, €1,000.50, € 100000\n if (symbols) {\n patterns.push(\n `(?:[${symbols}])` + `[^\\\\S\\\\n\\\\t]?` + `${NUM}${DECIMAL}${END}`,\n );\n }\n\n // Leading multi-char code: \"Kč 10,—\", \"Fr. 500\"\n if (trailingAlt) {\n patterns.push(\n `\\\\b(?:${trailingAlt})` + `[^\\\\S\\\\n\\\\t]{0,2}` + `${NUM}${DECIMAL}${END}`,\n );\n }\n\n // Trailing code/name: 100 USD, 1,000.50 CZK,\n // 100000 Kč, 500 korun, 100 Fr.\n if (trailingAlt) {\n patterns.push(\n `\\\\b${NUM}${DECIMAL}[^\\\\S\\\\n\\\\t]{0,4}` + `(?:${trailingAlt})${END}`,\n );\n }\n\n return patterns;\n};\n\n/** Cached promise for currency patterns. Loaded once. */\nlet currencyPatternPromise: Promise<string[]> | null = null;\n\nconst loadCurrencyPatterns = async (): Promise<string[]> => {\n const mod = await import(\"../data/currencies.json\");\n const data: CurrenciesData = mod.default ?? mod;\n return buildCurrencyPatterns(data);\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\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 (meta.label === \"phone number\" && match.text.length < MIN_PHONE_LENGTH) {\n continue;\n }\n\n // Post-match validation: if the pattern came from\n // a stdnum validator, compact (strip separators)\n // then validate. The candidate regex may capture\n // spaced/dashed variants that validate() rejects\n // without compaction.\n if (meta.validator) {\n const compacted = meta.validator.compact(match.text);\n const result = meta.validator.validate(compacted);\n if (!result.valid) {\n continue;\n }\n }\n\n results.push({\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 }\n\n return results;\n};\n\n// ── Dynamic signing clause patterns ────────────────\n\ntype SigningClauseConfig = {\n patterns: Array<{\n lang: string;\n prefix: string;\n suffix: string;\n prepositions: string[];\n }>;\n};\n\n/**\n * Build signing clause place-name patterns from\n * signing-clauses.json. Each pattern captures the\n * city/place name from contract signing locations.\n *\n * The place name sub-pattern:\n * \\p{Lu}\\p{Ll}+ (capitalized word)\n * optionally followed by preposition + capitalized\n * word (for \"nad Nisou\", \"am Main\", etc.)\n * optionally followed by more capitalized words\n * (for \"Hradec Králové\", \"New York\", etc.)\n */\nconst buildSigningClausePatterns = (data: SigningClauseConfig): string[] => {\n const patterns: string[] = [];\n\n for (const entry of data.patterns) {\n const prepAlt =\n entry.prepositions.length > 0 ? entry.prepositions.join(\"|\") : null;\n\n // Place name: Uppercase word, optionally with\n // preposition + uppercase, optionally more caps\n const place = prepAlt\n ? `(\\\\p{Lu}\\\\p{Ll}+` +\n `(?:\\\\s+(?:${prepAlt})\\\\s+\\\\p{Lu}\\\\p{Ll}+)*` +\n `(?:\\\\s+\\\\p{Lu}\\\\p{Ll}+)*)`\n : `(\\\\p{Lu}\\\\p{Ll}+(?:[- ]\\\\p{Lu}\\\\p{Ll}+)*)`;\n\n const full =\n `(?:^|\\\\n|[^\\\\S\\\\n])` +\n entry.prefix +\n place +\n (entry.suffix ? `(?:${entry.suffix})` : \"\");\n\n patterns.push(full);\n }\n\n return patterns;\n};\n\nexport const SIGNING_CLAUSE_META: Readonly<RegexMeta> = {\n label: \"address\",\n score: 0.9,\n};\n\nlet signingPatternPromise: Promise<string[]> | null = null;\n\nconst loadSigningPatterns = async (): Promise<string[]> => {\n const mod = await import(\"../data/signing-clauses.json\");\n const data: SigningClauseConfig = mod.default ?? mod;\n return buildSigningClausePatterns(data);\n};\n\nexport const getSigningClausePatterns = (): Promise<string[]> => {\n if (!signingPatternPromise) {\n signingPatternPromise = loadSigningPatterns().catch((err) => {\n signingPatternPromise = null;\n throw err;\n });\n }\n return signingPatternPromise;\n};\n","/**\n * Legal form 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 { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\nimport { DASH_INNER } from \"../util/char-groups\";\n\nconst UPPER = \"A-ZÁČĎÉĚÍŇÓŘŠŤÚŮÝŽÄÖÜÀÂÆÇÈÊËÎÏÔÙÛŸÑ\\\\u0130\";\nconst LOWER = \"a-záčďéěíňóřšťúůýžäöüßàâæçèêëîïôùûÿñ\\\\u0131\";\nconst CAP_WORD = `(?:[${UPPER}]{2,}|[${UPPER}][${LOWER}${UPPER}]+)`;\n// ANY_WORD: mixed-case word, short all-caps token\n// (2-3 chars, e.g. \"CZ\" in \"Metrostav CZ s.r.o.\"),\n// or digit-only token (1-4 digits, e.g. \"2028\" in\n// \"Invest 2028 s.r.o.\" or \"12\" in \"Praha 12 s.r.o.\").\nconst ANY_WORD =\n `(?:[${UPPER}${LOWER}][${LOWER}${UPPER}]+` +\n `|[${UPPER}]{2,3}` +\n `|\\\\d{1,4})`;\n// All-caps word: 2+ uppercase letters, no lowercase.\n// For company names like \"EAGLES BRNO\", max 3 words.\nconst ALLCAP_WORD = `[${UPPER}]{2,}`;\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\nconst escapeForRegex = (form: string): string =>\n form\n .replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\s+/g, \"\\\\s+\")\n // Use [^\\S\\n]? (optional horizontal whitespace)\n // instead of \\s* to prevent greedy matching across\n // newlines which causes DFA failures in regex-set.\n .replace(/\\\\\\./g, \"\\\\.[^\\\\S\\\\n]?\");\n\nconst isShortForm = (form: string): boolean =>\n form.replace(/[.\\s]/g, \"\").length <= 3 && !form.includes(\" \");\n\nconst buildPatternString = (forms: string[]): string | null => {\n if (forms.length === 0) {\n return null;\n }\n\n const sorted = forms.toSorted((a, b) => b.length - a.length);\n const alt = sorted.map(escapeForRegex).join(\"|\");\n // Separator between name words: space, ampersand,\n // comma, dot, hyphen (1-4 chars). Connector words\n // (a, and, und, et, e, y, i) are allowed only when\n // followed by a lowercase-starting word — this\n // prevents greedy extension across entity boundaries\n // like \"Foo s.r.o. a BAR s.r.o.\"\n const LOWER_CONNECTOR = `\\\\s+(?:a|and|und|et|e|y|i)\\\\s+(?=[${LOWER}])`;\n const SEPARATOR = `(?:[\\\\s&,.${DASH_INNER}]{1,4}|${LOWER_CONNECTOR})`;\n const prefix = `(?:${CAP_WORD})` + `(?:${SEPARATOR}(?:${ANY_WORD})){0,10}`;\n const separator = `(?:\\\\s+|,\\\\s*)`;\n\n return `${prefix}${separator}(?:${alt})(?![${LOWER}])`;\n};\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 let data: Record<string, string[]> = {};\n\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 return [];\n }\n\n const allForms: string[] = [];\n const seen = new Set<string>();\n\n for (const forms of Object.values(data)) {\n for (const form of forms) {\n const key = form.toLowerCase();\n if (!seen.has(key)) {\n seen.add(key);\n allForms.push(form);\n }\n }\n }\n\n const patterns: string[] = [];\n\n const longPattern = buildPatternString(\n allForms.filter((f) => !isShortForm(f)),\n );\n if (longPattern) {\n patterns.push(longPattern);\n }\n\n const shortPattern = buildPatternString(allForms.filter(isShortForm));\n if (shortPattern) {\n patterns.push(shortPattern);\n }\n\n // All-caps company names: \"EAGLES BRNO, z.s.\"\n // Up to 3 all-caps words before any legal form.\n // Uses all forms (both long and short).\n // No connectors — backward extension handles them.\n const allcapPrefix =\n `(?:${ALLCAP_WORD})` +\n `(?:[\\\\s&,.${DASH_INNER}]{1,4}(?:${ALLCAP_WORD})){0,2}`;\n const allcapAlt = allForms\n .toSorted((a, b) => b.length - a.length)\n .map(escapeForRegex)\n .join(\"|\");\n patterns.push(\n `${allcapPrefix}(?:\\\\s+|,\\\\s*)` + `(?:${allcapAlt})(?![${LOWER}])`,\n );\n\n return patterns;\n};\n\n// ── Backward extension ──────────────────────────────\n\nconst CONNECTOR_RE = /^(?:a|and|und|et|e|y|i|&)$/i;\nconst LEADING_CLAUSE_RE = /(?:^|\\s)(?:by\\s+and\\s+between|is\\s+between)\\s+/giu;\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\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 */\nconst extendBackward = (fullText: string, matchStart: number): number => {\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 = /^\\p{Lu}/u.test(word);\n const isConnector = CONNECTOR_RE.test(word);\n\n if (isUpper) {\n // Uppercase word — always accept\n pos = wordStart;\n } else if (isConnector) {\n // Connector — only accept if there is a valid\n // (uppercase-starting) word before it\n const prev = findWordBefore(fullText, wordStart);\n if (!prev) break;\n const prevIsUpper = /^\\p{Lu}/u.test(prev.word);\n if (!prevIsUpper) break;\n // Move pos back to the start of the word that\n // precedes the connector; the connector and all\n // whitespace between it and prev.start are\n // included implicitly in the entity slice.\n pos = prev.start;\n } else {\n break;\n }\n }\n\n return pos;\n};\n\nconst trimLeadingClause = (text: string): { offset: number; text: string } => {\n let cut = -1;\n\n for (const match of text.matchAll(LEADING_CLAUSE_RE)) {\n cut = match.index + match[0].length;\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 */\nexport const processLegalFormMatches = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText?: string,\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 if (text.includes(\"\\n\")) {\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 = match.start;\n let entityText = text;\n if (fullText) {\n const extended = extendBackward(fullText, match.start);\n if (extended < match.start) {\n entityStart = extended;\n entityText = fullText\n .slice(extended, match.start + text.length)\n .trimEnd();\n }\n }\n\n const clauseTrim = trimLeadingClause(entityText);\n if (clauseTrim.offset > 0) {\n entityStart += clauseTrim.offset;\n entityText = clauseTrim.text;\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 // Check: is the surrounding line also all-caps?\n const lineStart = fullText.lastIndexOf(\"\\n\", entityStart);\n const lineEnd = fullText.indexOf(\"\\n\", entityStart + entityText.length);\n const line = fullText.slice(\n lineStart + 1,\n lineEnd === -1 ? fullText.length : lineEnd,\n );\n const lineLetters = line.replace(/[^a-zA-ZÀ-ž]/g, \"\");\n const upperCount = [...lineLetters].filter(\n (c) => c === c.toUpperCase(),\n ).length;\n const lineIsAllCaps =\n lineLetters.length > 5 && upperCount / lineLetters.length >= 0.95;\n if (lineIsAllCaps) {\n // Entire line is all-caps → heading, skip\n continue;\n }\n // Only the company name is all-caps → keep it\n // (but limit to 3 words in prefix)\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 // Keep the original regex match if backward\n // extension alone pushed the name past the\n // all-caps 3-word guard.\n entityStart = match.start;\n entityText = text;\n ({ prefixEnd, prefixPart } = getPrefixInfo(entityText));\n isAllCapsMatch =\n prefixPart.length > 2 && prefixPart === prefixPart.toUpperCase();\n }\n } else if (isAllCapsMatch) {\n // No fullText available — fall back to rejecting\n continue;\n }\n\n // Reject Roman numeral suffixes\n const lastSpace = entityText.lastIndexOf(\" \");\n const rawSuffix = lastSpace !== -1 ? entityText.slice(lastSpace + 1) : \"\";\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 /[^\\x00-\\x7F]/.test(\n entityText.slice(0, lastSpace !== -1 ? lastSpace : entityText.length),\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 return results;\n};\n","/**\n * Known legal form suffixes. Shared between trigger\n * detection (reclassification) and org-propagation\n * (suffix stripping). Within each family of related\n * forms, longer variants come first so that\n * \"spol. s r.o.\" matches before \"s.r.o.\" and\n * \"s. r. o.\" matches before \"s.r.o.\".\n */\nexport const LEGAL_SUFFIXES = [\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 \"GmbH\",\n \"AG\",\n \"SE\",\n \"KG\",\n \"OHG\",\n \"Ltd.\",\n \"Ltd\",\n \"LLC\",\n \"LLP\",\n \"Inc.\",\n \"S.A.\",\n \"SA\",\n \"SAS\",\n \"SARL\",\n \"Sp. z o.o.\",\n \"S.p.A.\",\n] as const;\n","import type { Match } from \"@stll/text-search\";\n\nimport { DETECTION_SOURCES } from \"../types\";\nimport type {\n CompiledValidation,\n Entity,\n TriggerGroupConfig,\n TriggerRule,\n TriggerValidation,\n} from \"../types\";\nimport { POST_NOMINALS } from \"../config/titles\";\nimport { LEGAL_SUFFIXES } from \"../config/legal-forms\";\nimport { loadLanguageConfigs } from \"../util/lang-loader\";\nimport { DASH } from \"../util/char-groups\";\n\nconst TRIGGER_SCORE = 0.95;\nconst WHITESPACE_RE = /\\s+/;\nconst LETTER_RE = /\\p{L}/u;\n/**\n * Decimal-comma pattern: comma followed by digit or\n * dash notation (\"0,05%\", \"1.529,50 Kč\", \"98.000,- Kč\").\n */\nconst DECIMAL_COMMA_RE = new RegExp(`^,(?:\\\\d|${DASH}{1,2})`);\n\n/**\n * Post-nominal degree regex. When a comma-stop is\n * followed by a known post-nominal (Ph.D., CSc., MBA\n * etc.), skip the comma and degree, then continue.\n */\nconst POST_NOMINAL_RE = new RegExp(\n `^,\\\\s*(?:${POST_NOMINALS.toSorted((a, b) => b.length - a.length)\n .map((d) =>\n d.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\").replace(/\\\\\\./g, \"\\\\.\\\\s*\"),\n )\n .join(\"|\")})\\\\.?`,\n \"i\",\n);\n\n// Definitive legal form suffixes (case-sensitive).\n// When a person-labeled trigger captures text containing\n// one of these, the entity is reclassified as\n// \"organization\". No \"i\" flag: short uppercase-only\n// forms (SE, SA, AG) must NOT match Czech/Slovak\n// reflexive pronouns \"se\", \"sa\" which appear in\n// person-trigger captures like\n// \"Ing. Jan Novák, se sídlem...\".\nconst DEFINITIVE_LEGAL_FORMS = LEGAL_SUFFIXES;\n// Build case-sensitive regex. Short dot-free forms\n// (AG, SE, KG) get word boundaries to prevent substring\n// matches. All forms are uppercase in the list; the\n// regex is case-sensitive so \"se\"/\"sa\" won't match.\nconst LEGAL_FORM_CHECK_RE = new RegExp(\n DEFINITIVE_LEGAL_FORMS.map((f) => {\n const escaped = f\n .replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\\\\\./g, \"\\\\.\\\\s*\");\n const isDotFree = !f.includes(\".\");\n const isShort = f.length <= 4;\n return isDotFree && isShort ? `\\\\b${escaped}\\\\b` : escaped;\n }).join(\"|\"),\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 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 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\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 */\nexport const buildTriggerPatterns = async (): Promise<{\n patterns: string[];\n rules: TriggerRule[];\n}> => {\n const rules: TriggerRule[] = [];\n\n const allGroups = await loadLanguageConfigs<readonly TriggerGroupConfig[]>(\n \"triggers\",\n (mod) => {\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config\n const m = mod as {\n default?: readonly TriggerGroupConfig[];\n };\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config\n return (m.default ?? mod) as readonly TriggerGroupConfig[];\n },\n );\n for (const groups of allGroups) {\n if (!Array.isArray(groups)) {\n console.warn(\n \"[anonymize] triggers: unexpected \" + \"config shape, skipping\",\n );\n continue;\n }\n rules.push(...expandTriggerGroups(groups as TriggerGroupConfig[]));\n }\n\n // Load global triggers (language-agnostic)\n try {\n const globalMod = await import(\"../data/triggers.global.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON config\n const globalGroups = ((globalMod as { default?: unknown }).default ??\n globalMod) as TriggerGroupConfig[];\n if (Array.isArray(globalGroups)) {\n rules.push(...expandTriggerGroups(globalGroups));\n }\n } catch (err) {\n // Only suppress \"module not found\"; re-throw\n // other errors (JSON parse, etc.).\n if (\n !(err instanceof Error) ||\n !err.message.includes(\"Cannot find module\")\n ) {\n throw err;\n }\n }\n\n // Load year-words from JSON dictionary and create\n // date triggers: \"rok 2022\" → date entity.\n try {\n const yearMod = await import(\"../data/year-words.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON\n const data = ((yearMod as { default?: unknown }).default ??\n yearMod) as Record<string, string | string[]>;\n const seen = new Set<string>();\n const yearValidation = compileValidations([\n {\n type: \"matches-pattern\",\n pattern: \"^(?:19|20)\\\\d{2}\\\\.?$\",\n },\n ]);\n for (const [key, words] of Object.entries(data)) {\n if (key.startsWith(\"_\") || !Array.isArray(words)) {\n continue;\n }\n for (const word of words) {\n const lc = word.toLowerCase();\n if (seen.has(lc)) continue;\n seen.add(lc);\n rules.push({\n trigger: word,\n label: \"date\",\n strategy: { type: \"n-words\", count: 1 },\n validations: yearValidation,\n includeTrigger: false,\n });\n }\n }\n } catch {\n // year-words.json not available — skip\n }\n\n // Warn about cross-group trigger duplicates.\n // Duplicates cause redundant AC matches but are\n // not fatal (mergeAndDedup handles overlap).\n const seen = new Map<string, { label: string; strategy: string }>();\n for (const rule of rules) {\n const key = rule.trigger.toLowerCase();\n const prev = seen.get(key);\n if (prev !== undefined) {\n const labelDiff = prev.label !== rule.label;\n const stratDiff = prev.strategy !== rule.strategy.type;\n if (labelDiff || stratDiff) {\n console.warn(\n `[anonymize] duplicate trigger` +\n ` \"${rule.trigger}\":` +\n (labelDiff\n ? ` labels \"${prev.label}\" vs` + ` \"${rule.label}\"`\n : \"\") +\n (stratDiff\n ? ` strategies \"${prev.strategy}\" vs` + ` \"${rule.strategy.type}\"`\n : \"\"),\n );\n }\n }\n seen.set(key, {\n label: rule.label,\n strategy: rule.strategy.type,\n });\n }\n\n // Build patterns from lowercased trigger strings.\n // rules[i] corresponds to patterns[i].\n // Plain lowercased strings — the unified builder\n // sets caseInsensitive globally on the AC.\n const patterns: string[] = rules.map((r) => r.trigger.toLowerCase());\n\n return { patterns, rules };\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/** Hard stop characters for to-next-comma scanning. */\nconst COMMA_STOP_CHARS = new Set([\"\\n\", \"(\", \"\\t\"]);\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 */\nconst ADDRESS_STOP_KEYWORDS = [\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\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 remaining = text.slice(triggerEnd);\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 let end = 0;\n let foundStop = false;\n\n while (end < valueText.length) {\n const ch = valueText[end];\n // Hard stops: newline, paren, tab\n if (ch !== undefined && COMMA_STOP_CHARS.has(ch)) {\n foundStop = true;\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 foundStop = true;\n break;\n }\n end++;\n }\n\n // Only cap at 100 chars when no stop char was\n // found (fallback for unterminated values). When\n // a stop char exists, respect its position even\n // if > 100.\n if (!foundStop) {\n end = Math.min(end, 100);\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 // Stop at newline or tab (tab separates cells\n // in DOCX table rows).\n const LINE_STOPS = [\"\\n\"];\n let end = valueText.length;\n for (const ch of LINE_STOPS) {\n const idx = valueText.indexOf(ch);\n if (idx !== -1 && idx < end) {\n end = idx;\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 const allTokens = cellText.split(WHITESPACE_RE);\n const words = allTokens\n .filter((w) => !PUNCT_ONLY.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 const raw = text.slice(triggerEnd);\n const sepMatch = /^(?:\\s*:\\s*|\\s+)/.exec(raw);\n if (!sepMatch) {\n return null;\n }\n const afterSep = raw.slice(sepMatch[0].length);\n const idMatch = /^[A-Z]{0,6}\\s?\\d[\\d\\s\\-/]{4,}/i.exec(afterSep);\n if (!idMatch) {\n return null;\n }\n const idText = idMatch[0].trim();\n const leadingSpaces = idMatch[0].length - idMatch[0].trimStart().length;\n const idStart =\n triggerEnd +\n sepMatch[0].length +\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 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 // 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 // \"č.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. In address\n // context, this is always an abbreviation,\n // not end of sentence.\n if (\n next === \" \" &&\n afterNext !== undefined &&\n (/\\p{L}/u.test(afterNext) || /\\d/.test(afterNext))\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.\n const afterComma = valueText\n .slice(end + 1)\n .trimStart()\n .toLowerCase();\n const hitsKeyword = ADDRESS_STOP_KEYWORDS.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 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 if followed by a\n // letter — but skip this check when the trigger\n // itself ends with whitespace (e.g., \"pan \",\n // \"město \") since the trailing space already\n // acts as a boundary delimiter.\n if (\n !rule.trigger.endsWith(\" \") &&\n LETTER_RE.test(fullText[match.end] ?? \"\")\n ) {\n continue;\n }\n\n const triggerEnd = match.end;\n const rawValue = extractValue(\n fullText,\n triggerEnd,\n rule.strategy,\n rule.label,\n );\n const value = rawValue ? stripQuotes(rawValue) : null;\n\n if (value) {\n // Apply declarative validations to the captured\n // value text (not the full entity including\n // trigger). This is intentional: validations\n // like min-length should test the extracted\n // value, not the trigger keyword itself.\n if (!applyValidations(value.text, rule.validations)) {\n continue;\n }\n\n // When includeTrigger is set, the entity span\n // starts at the trigger match, not the value.\n const entityStart = rule.includeTrigger ? match.start : value.start;\n const entityEnd = value.end;\n const entityText = fullText.slice(entityStart, entityEnd);\n\n // Legal form reclassification: any person-labeled\n // trigger whose captured text contains a definitive\n // legal form suffix is reclassified as organization.\n // This is universal — every person with a legal form\n // is an organisation, no per-group config needed.\n const effectiveLabel =\n rule.label === \"person\" && LEGAL_FORM_CHECK_RE.test(entityText)\n ? \"organization\"\n : rule.label;\n\n results.push({\n start: entityStart,\n end: entityEnd,\n label: effectiveLabel,\n text: entityText,\n score: TRIGGER_SCORE,\n source: DETECTION_SOURCES.TRIGGER,\n });\n }\n }\n\n return results;\n};\n\n// Re-export for testing\nexport { expandTriggerGroups, compileValidations, applyValidations };\n","/**\n * Geographic regions and country code mappings for\n * scoping deny list dictionaries.\n */\n\nexport const REGIONS = {\n Global: null,\n International: null,\n Europe: [\n \"AL\",\n \"AD\",\n \"AT\",\n \"BE\",\n \"BA\",\n \"BG\",\n \"HR\",\n \"CY\",\n \"CZ\",\n \"DK\",\n \"EE\",\n \"FI\",\n \"FR\",\n \"DE\",\n \"GR\",\n \"HU\",\n \"IS\",\n \"IE\",\n \"IT\",\n \"XK\",\n \"LV\",\n \"LI\",\n \"LT\",\n \"LU\",\n \"MD\",\n \"ME\",\n \"MK\",\n \"MT\",\n \"MC\",\n \"NL\",\n \"NO\",\n \"PL\",\n \"PT\",\n \"RO\",\n \"RS\",\n \"SK\",\n \"SI\",\n \"ES\",\n \"SE\",\n \"CH\",\n \"UA\",\n \"GB\",\n ],\n Americas: [\n \"US\",\n \"CA\",\n \"MX\",\n \"BR\",\n \"AR\",\n \"CL\",\n \"CO\",\n \"PE\",\n \"EC\",\n \"VE\",\n \"UY\",\n \"PY\",\n \"BO\",\n \"CR\",\n \"PA\",\n \"DO\",\n \"GT\",\n \"HN\",\n \"SV\",\n \"NI\",\n \"CU\",\n ],\n AsiaPacific: [\n \"AU\",\n \"NZ\",\n \"JP\",\n \"KR\",\n \"CN\",\n \"TW\",\n \"SG\",\n \"MY\",\n \"TH\",\n \"VN\",\n \"PH\",\n \"ID\",\n \"IN\",\n \"PK\",\n \"BD\",\n \"LK\",\n \"NP\",\n \"HK\",\n \"MO\",\n ],\n MENA: [\n \"AE\",\n \"SA\",\n \"IL\",\n \"TR\",\n \"EG\",\n \"JO\",\n \"LB\",\n \"IQ\",\n \"IR\",\n \"QA\",\n \"KW\",\n \"BH\",\n \"OM\",\n \"MA\",\n \"TN\",\n \"DZ\",\n \"LY\",\n \"SY\",\n \"YE\",\n \"PS\",\n ],\n SubSaharanAfrica: [\n \"ZA\",\n \"NG\",\n \"KE\",\n \"GH\",\n \"TZ\",\n \"ET\",\n \"SN\",\n \"CI\",\n \"CM\",\n \"UG\",\n \"RW\",\n \"MZ\",\n \"AO\",\n \"ZW\",\n \"BW\",\n \"NA\",\n \"MU\",\n ],\n EU: [\n \"AT\",\n \"BE\",\n \"BG\",\n \"HR\",\n \"CY\",\n \"CZ\",\n \"DK\",\n \"EE\",\n \"FI\",\n \"FR\",\n \"DE\",\n \"GR\",\n \"HU\",\n \"IE\",\n \"IT\",\n \"LV\",\n \"LT\",\n \"LU\",\n \"MT\",\n \"NL\",\n \"PL\",\n \"PT\",\n \"RO\",\n \"SK\",\n \"SI\",\n \"ES\",\n \"SE\",\n ],\n DACH: [\"DE\", \"AT\", \"CH\"],\n Nordics: [\"DK\", \"SE\", \"NO\", \"FI\", \"IS\"],\n CEE: [\"CZ\", \"SK\", \"PL\", \"HU\", \"RO\", \"BG\", \"HR\", \"SI\", \"LT\", \"LV\", \"EE\"],\n Anglosphere: [\"GB\", \"US\", \"CA\", \"AU\", \"NZ\", \"IE\"],\n Benelux: [\"BE\", \"NL\", \"LU\"],\n GulfStates: [\"AE\", \"SA\", \"QA\", \"KW\", \"BH\", \"OM\"],\n SouthAsia: [\"IN\", \"PK\", \"BD\", \"LK\", \"NP\"],\n EastAsia: [\"CN\", \"JP\", \"KR\", \"TW\"],\n SoutheastAsia: [\"SG\", \"MY\", \"TH\", \"VN\", \"PH\", \"ID\"],\n Oceania: [\"AU\", \"NZ\"],\n} as const;\n\nexport type RegionId = keyof typeof REGIONS;\n\ntype RegionArrays = {\n [K in RegionId]: (typeof REGIONS)[K];\n};\n\ntype NonNullRegion = {\n [K in RegionId as RegionArrays[K] extends null ? never : K]: RegionArrays[K];\n};\n\nexport type CountryCode = NonNullRegion[keyof NonNullRegion][number];\n\n/**\n * Expand region names to country codes and merge with\n * explicit country codes. Returns null when both inputs\n * are empty/undefined (meaning \"match all countries\").\n */\nexport const resolveCountries = (\n regions?: string[],\n countries?: string[],\n): Set<string> | null => {\n const hasRegions = regions && regions.length > 0;\n const hasCountries = countries && countries.length > 0;\n\n if (!hasRegions && !hasCountries) {\n return null;\n }\n\n const result = new Set<string>();\n\n const isRegion = (name: string): name is RegionId => name in REGIONS;\n\n if (hasRegions) {\n for (const name of regions) {\n if (!isRegion(name)) {\n continue;\n }\n const codes = REGIONS[name];\n if (codes === null) {\n // Global/International: return null (all)\n return null;\n }\n for (const code of codes) {\n result.add(code);\n }\n }\n }\n\n if (hasCountries) {\n for (const code of countries) {\n result.add(code);\n }\n }\n\n return result;\n};\n","import type { Entity } from \"../types\";\nimport type { PipelineContext } from \"../context\";\nimport { defaultContext } from \"../context\";\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/;\nconst ADDRESS_COMPONENTS_RE =\n /(?:^|\\s)(?:ul\\.|ulice|nám\\.|náměstí|tř\\.|třída|nábř\\.|nábřeží|č\\.p\\.|č\\.ev\\.|č\\.|sídliště|bulvár)(?=[\\s,./]|$)/i;\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// Section/clause numbers: \"§ 3\", \"3.2.1\", \"12.\" but NOT\n// dates like \"4.3.2026\" or long digit strings like IČO.\n// A section number has 1-3 digit groups of 1-3 digits each,\n// never ending with a 4-digit group (that's a year).\nconst SECTION_NUMBER_RE = /^(?:§\\s*)?\\d{1,3}(?:\\.\\d{1,3}){0,4}\\.?$/;\nconst STANDALONE_YEAR_RE = /^(?:19|20)\\d{2}$/;\n\n// Number-abbreviation prefixes: \"č.\", \"Nr.\", \"No.\", \"nr.\",\n// \"no.\", \"n.\", \"čís.\" — when a numeric entity is preceded\n// by one of these, it's a reference number, not PII.\nconst NUMBER_ABBREV_RE = /(?:^|[\\s(])(?:č|čís|nr|no|n)\\.\\s*$/i;\nconst SIGNING_CLAUSE_ADDRESS_RE = /^(?:v|ve)\\s+[^\\d,\\n]{1,40},?\\s+dne$/iu;\nconst PERSON_TRAILING_NOUNS: ReadonlySet<string> = new Set([\n \"association\",\n \"period\",\n \"reform\",\n]);\nconst LEGAL_FORM_HEADING_RE = /\\b(?:agreement|amendment|contract|exhibit)\\b/iu;\nconst LEADING_ARTIFACT_RE = /^(?:\\.\\s)+/u;\nconst ADDRESS_ROLE_PREFIX_RE =\n /^(?:prodávajícího|kupujícího|objednatele|zhotovitele|pronajímatele|dodavatele|odběratele|zaměstnance|zaměstnavatele|nájemce)\\s+(?=(?:\\p{Lu}|\\d|ul\\.?|ulice|nám\\.?|náměstí|tř\\.?|třída|nábř\\.?|nábřeží|č\\.p\\.?|č\\.ev\\.?|sídliště))/iu;\nconst ADDRESS_INLINE_ABBREV_AFTER_RE =\n /^(?:\\p{Lu}[\\p{L}\\p{M}]{0,3}\\.|ul\\.?|nám\\.?|tř\\.?|nábř\\.?|č\\.p\\.?|č\\.ev\\.?)/u;\nconst ADDRESS_INLINE_ABBREV_BEFORE_RE =\n /(?:^|[\\s,])(?:st|ave|rd|dr|blvd|ln|hwy|pkwy|cir|ct|pl|sq|ter|trl|ste|apt|bldg|fl|ul|nám|tř|nábř|č\\.p|č\\.ev)$/iu;\nconst ADDRESS_CONTINUATION_WORD_RE =\n /^(?:suite|building|floor|unit|apartment|room|tower|wing|block|bldg|ste|apt|fl)\\b/iu;\n\nconst trimTrailingAddressProse = (text: string): string => {\n for (const match of text.matchAll(/\\.(?=\\s+\\p{Lu})/gu)) {\n const cutoff = match.index;\n if (cutoff === undefined) {\n continue;\n }\n const before = text.slice(0, cutoff);\n if (!HAS_DIGIT_RE.test(before)) {\n continue;\n }\n const after = text.slice(cutoff + 1).trimStart();\n if (\n after.length < 5 ||\n ADDRESS_INLINE_ABBREV_AFTER_RE.test(after) ||\n ADDRESS_INLINE_ABBREV_BEFORE_RE.test(before.trimEnd()) ||\n ADDRESS_CONTINUATION_WORD_RE.test(after)\n ) {\n continue;\n }\n return before.trimEnd();\n }\n\n return text;\n};\n\nconst normalizeEntity = (entity: Entity): Entity | null => {\n let start = entity.start;\n let text = entity.text;\n\n const trimLeading = (re: RegExp) => {\n const match = re.exec(text);\n if (!match) {\n return;\n }\n start += match[0].length;\n text = text.slice(match[0].length);\n };\n\n trimLeading(LEADING_ARTIFACT_RE);\n trimLeading(/^\\s+/u);\n\n if (entity.label === \"address\") {\n trimLeading(ADDRESS_ROLE_PREFIX_RE);\n text = trimTrailingAddressProse(text);\n }\n\n const trailingMatch = /[,\\s]+$/u.exec(text);\n if (trailingMatch) {\n text = text.slice(0, text.length - trailingMatch[0].length);\n }\n\n if (text.length === 0) {\n return null;\n }\n\n return {\n ...entity,\n start,\n end: start + text.length,\n text,\n };\n};\n\n// ── 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/**\n * Filter out entities that are likely false positives:\n * template placeholders, clause/section numbers,\n * standalone years, and generic legal role terms.\n *\n * Runs as a post-processing step after all detection\n * layers have merged.\n */\nexport const filterFalsePositives = (\n entities: Entity[],\n ctx: PipelineContext = defaultContext,\n fullText?: string,\n): Entity[] => {\n const filtered: Entity[] = [];\n const roles = getGenericRoles(ctx);\n\n for (const entity of entities) {\n const normalized = normalizeEntity(entity);\n if (!normalized) {\n continue;\n }\n\n // Strip leading \". \" artifacts from trigger extraction\n // after abbreviations (\"dat. nar.\", \"č.p.\").\n const trimmed = normalized.text;\n\n if (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 // 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 // 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 if (normalized.label === \"person\") {\n const tokens = trimmed.split(/\\s+/u);\n const last = tokens\n .at(-1)\n ?.replace(/[.,;:!?]+$/u, \"\")\n .toLowerCase();\n if (tokens.length > 1 && last && PERSON_TRAILING_NOUNS.has(last)) {\n continue;\n }\n }\n\n if (\n (normalized.label === \"person\" || normalized.label === \"organization\") &&\n roles.has(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 !ADDRESS_COMPONENTS_RE.test(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.\n if (\n normalized.label === \"address\" &&\n normalized.source === \"trigger\" &&\n !HAS_DIGIT_RE.test(trimmed) &&\n !ADDRESS_COMPONENTS_RE.test(trimmed) &&\n !JURISDICTION_RE.test(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 * 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 REPLACEMENTS: readonly [number, number][] = [\n [0x00a0, 0x0020], // NBSP → space\n [0x2007, 0x0020], // figure space → space\n [0x202f, 0x0020], // narrow NBSP → space\n [0x2013, 0x002d], // en-dash → hyphen\n [0x2014, 0x002d], // em-dash → hyphen\n [0x201c, 0x0022], // left smart quote → \"\n [0x201d, 0x0022], // right smart quote → \"\n];\n\nconst CHAR_MAP = new Map<number, number>(REPLACEMENTS);\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 if (CHAR_MAP.has(text.charCodeAt(i))) {\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] = CHAR_MAP.get(code) ?? 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 getNameCorpusFirstNames,\n getNameCorpusSurnames,\n getNameCorpusTitles,\n initNameCorpus,\n} from \"./names\";\nimport { resolveCountries } from \"../regions\";\nimport { DETECTION_SOURCES } from \"../types\";\nimport type { Entity, PipelineConfig } from \"../types\";\nimport type { PipelineContext } from \"../context\";\nimport { defaultContext } from \"../context\";\nimport { loadGenericRoles } from \"../filters/false-positives\";\nimport { normalizeForSearch } from \"../util/normalize\";\nimport { ALL_UPPER_RE, UPPER_START_RE } from \"../util/text\";\nimport { DASH } from \"../util/char-groups\";\n\n/**\n * Try to load the optional @stll/anonymize-data package.\n * Returns null if not installed.\n */\nconst loadDataModule = async (): Promise<\n typeof import(\"@stll/anonymize-data\") | null\n> => {\n try {\n return await import(\"@stll/anonymize-data\");\n } catch {\n return null;\n }\n};\n\nexport type DenyListConfig = Pick<\n PipelineConfig,\n | \"enableDenyList\"\n | \"enableNameCorpus\"\n | \"denyListCountries\"\n | \"denyListRegions\"\n | \"denyListExcludeCategories\"\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\n/**\n * Common EU given names present in the stopwords-iso dataset\n * but absent from the first-name corpus. Without this\n * supplementary set, these names would pass through the\n * corpus-based filter and remain in the stopwords, silently\n * suppressing person detection.\n *\n * Sourced from EU member state birth registries (top-100\n * names) cross-referenced with stopwords.json.\n */\nconst SUPPLEMENTARY_NAME_EXCLUSIONS: ReadonlySet<string> = new Set([\n \"ana\",\n \"ben\",\n \"dan\",\n \"eden\",\n \"ella\",\n \"ina\",\n \"jo\",\n \"kai\",\n \"lena\",\n \"may\",\n \"mia\",\n \"sam\",\n \"sara\",\n \"sue\",\n \"tim\",\n \"tom\",\n]);\n\n/**\n * Names from the first-name corpus (lowercased) that also\n * appear in the stopwords-iso dataset, plus supplementary\n * common EU given names not in the corpus. These must be\n * kept out of global STOPWORDS so that person detection is\n * not silently suppressed for real given names.\n *\n * Computed lazily after initNameCorpus() has populated\n * the first-name corpus. Re-builds if corpus size changes.\n */\nconst getFirstNameExclusions = (ctx: PipelineContext): ReadonlySet<string> => {\n const corpus = getNameCorpusFirstNames(ctx);\n // Re-build if corpus has been populated since last call\n if (\n ctx.firstNameExclusions &&\n corpus.length === ctx.firstNameExclusionCorpusLen\n ) {\n return ctx.firstNameExclusions;\n }\n ctx.firstNameExclusionCorpusLen = corpus.length;\n const set: ReadonlySet<string> = new Set([\n ...corpus.map((n) => n.toLowerCase()),\n ...SUPPLEMENTARY_NAME_EXCLUSIONS,\n ]);\n ctx.firstNameExclusions = set;\n return set;\n};\n\n/**\n * Global stopwords: common words across 23 EU languages\n * sourced from the stopwords-iso dataset (MIT license).\n * Checked case-insensitively against matches.\n *\n * Entries that collide with the first-name corpus are\n * excluded so they can still be detected as person names.\n *\n * Regenerate: bun packages/data/scripts/generate-stopwords.ts\n */\n\n// INVARIANT: must be called after initNameCorpus() has\n// resolved, so getFirstNameExclusions() sees the full\n// corpus. buildDenyList() enforces this ordering.\nconst loadStopwords = (ctx: PipelineContext): Promise<ReadonlySet<string>> => {\n if (ctx.stopwordsPromise) return ctx.stopwordsPromise;\n ctx.stopwordsPromise = (async () => {\n try {\n const mod: { default?: string[] } =\n await import(\"../data/stopwords.json\");\n const list = (mod.default ?? []).filter(\n (w: string) => !getFirstNameExclusions(ctx).has(w),\n );\n const set: ReadonlySet<string> = new Set(list);\n ctx.stopwords = set;\n return set;\n } catch (err) {\n console.warn(\n \"[anonymize] Failed to load stopwords.json\" +\n \" — stopword filtering disabled:\",\n err,\n );\n const empty: ReadonlySet<string> = new Set();\n ctx.stopwords = empty;\n return empty;\n }\n })();\n return ctx.stopwordsPromise;\n};\n\nconst EMPTY_STOPWORDS: ReadonlySet<string> = new Set();\n\n/** Sync accessor — returns empty set before init. */\nconst getStopwords = (ctx: PipelineContext): ReadonlySet<string> =>\n ctx.stopwords ?? EMPTY_STOPWORDS;\n\n// ── Person stopwords (lazy-loaded from JSON) ─────────\n\nconst loadPersonStopwords = (\n ctx: PipelineContext,\n): Promise<ReadonlySet<string>> => {\n if (ctx.personStopwordsPromise) {\n return ctx.personStopwordsPromise;\n }\n ctx.personStopwordsPromise = (async () => {\n try {\n const mod: {\n default?: { words?: string[] };\n } = await import(\"../data/person-stopwords.json\");\n const set: ReadonlySet<string> = new Set(mod.default?.words ?? []);\n ctx.personStopwords = set;\n return set;\n } catch {\n const empty: ReadonlySet<string> = new Set();\n ctx.personStopwords = empty;\n return empty;\n }\n })();\n return ctx.personStopwordsPromise;\n};\n\nconst EMPTY_PERSON_STOPWORDS: ReadonlySet<string> = new Set();\n\n/** Sync accessor — returns empty set before init. */\nconst getPersonStopwords = (ctx: PipelineContext): ReadonlySet<string> =>\n ctx.personStopwords ?? EMPTY_PERSON_STOPWORDS;\n\nconst PERSON_CHAIN_BREAK_RE = /[!?;:]|,/u;\n\nconst isInitialContinuationGap = (text: string, gap: string): boolean =>\n (/^\\p{Lu}$/u.test(text) && /^\\.[^\\S\\n]{1,2}$/u.test(gap)) ||\n /^[^\\S\\n]{1,2}(?:\\p{Lu}\\.[^\\S\\n]{1,2})+$/u.test(gap);\n\n/**\n * Source tag for each pattern in the automaton.\n * \"deny-list\" = standard deny list entry\n * \"first-name\" = name corpus first name\n * \"surname\" = name corpus surname\n * \"title\" = academic/professional title\n */\ntype PatternSource = \"deny-list\" | \"first-name\" | \"surname\" | \"title\";\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: string[][];\n /** Maps pattern index → original pattern text. */\n originals: string[];\n /** Maps pattern index → source types (plural). */\n sources: PatternSource[][];\n};\n\n/**\n * Resolve which dictionaries to load based on country\n * and category filters, load them, and build the deny\n * list data. The returned data provides PatternEntry[]\n * for the unified builder and parallel arrays for\n * post-processing.\n *\n * Requires `@stll/anonymize-data` to be installed.\n * Returns null if the data package is not available.\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);\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 loadGenericRoles(ctx),\n ]);\n const dataModule = await loadDataModule();\n if (!dataModule) {\n return null;\n }\n\n const allowedCountries = resolveCountries(\n config.denyListRegions,\n config.denyListCountries,\n );\n\n const excluded = config.denyListExcludeCategories;\n const excludeCategories = excluded ? new Set(excluded) : new Set<string>();\n\n const allIds = [...dataModule.ALL_DICTIONARY_IDS];\n\n const ids = allIds.filter((id) => {\n const meta = dataModule.DICTIONARY_META[id];\n if (!meta) {\n return false;\n }\n\n if (!config.enableNameCorpus && meta.category === \"Names\") {\n return false;\n }\n\n if (excludeCategories.has(meta.category)) {\n return false;\n }\n\n if (allowedCountries === null) {\n return true;\n }\n\n if (meta.country === null) {\n return true;\n }\n\n return allowedCountries.has(meta.country);\n });\n\n const patternList: string[] = [];\n const labelList: string[][] = [];\n const sourceList: PatternSource[][] = [];\n // Maps lowercased pattern → index in patternList\n // for accumulating labels from multiple dictionaries\n const patternIndex = new Map<string, number>();\n\n const results = await Promise.all(\n ids.map(async (id) => {\n const entries = await dataModule.loadDictionary(id);\n return { id, entries };\n }),\n );\n\n const addDenyListEntry = (entry: string, label: string) => {\n // Strip | and \\ only — these caused the 12K FP\n // bug (| creates empty regex alternation, \\ is\n // an escape prefix). Other chars like () [] are\n // kept since they appear in real dictionary\n // entries and are matched literally by AC.\n const normalized = normalizeForSearch(entry).replace(/[|\\\\]/g, \"\");\n if (normalized.length === 0) {\n return;\n }\n const lower = normalized.toLowerCase();\n const existing = patternIndex.get(lower);\n if (existing !== undefined) {\n if (!labelList[existing]!.includes(label)) {\n labelList[existing]!.push(label);\n }\n if (!sourceList[existing]!.includes(\"deny-list\")) {\n sourceList[existing]!.push(\"deny-list\");\n }\n } else {\n patternIndex.set(lower, patternList.length);\n patternList.push(normalized);\n labelList.push([label]);\n sourceList.push([\"deny-list\"]);\n }\n };\n\n for (const { id, entries } of results) {\n const meta = dataModule.DICTIONARY_META[id];\n if (!meta) {\n continue;\n }\n for (const entry of entries) {\n addDenyListEntry(entry, meta.label);\n }\n }\n\n // Load city dictionaries dynamically for all\n // allowed countries. Cities cover 230 countries\n // via GeoNames and are loaded separately from\n // the static dictionary registry.\n if (!excludeCategories.has(\"Places\")) {\n const cityCountries =\n allowedCountries !== null\n ? [...allowedCountries]\n : // No country filter — load all. In practice\n // this would be massive, so limit to a\n // reasonable set of common legal jurisdictions.\n [\n \"AT\",\n \"AU\",\n \"BE\",\n \"BG\",\n \"BR\",\n \"CA\",\n \"CH\",\n \"CZ\",\n \"DE\",\n \"DK\",\n \"ES\",\n \"FI\",\n \"FR\",\n \"GB\",\n \"GR\",\n \"HR\",\n \"HU\",\n \"IE\",\n \"IT\",\n \"LU\",\n \"NL\",\n \"NO\",\n \"NZ\",\n \"PL\",\n \"PT\",\n \"RO\",\n \"SE\",\n \"SI\",\n \"SK\",\n \"US\",\n ];\n const cityEntries = await dataModule.loadCityDictionaries(cityCountries);\n for (const entry of cityEntries) {\n addDenyListEntry(entry, \"address\");\n }\n }\n\n // Add name corpus entries — accumulate labels\n // for entries that already exist from deny-list.\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 = normalizeForSearch(name).replace(/[|\\\\]/g, \"\");\n if (normalized.length === 0) {\n return;\n }\n const lower = normalized.toLowerCase();\n const existing = patternIndex.get(lower);\n if (existing !== undefined) {\n if (!labelList[existing]!.includes(\"person\")) {\n labelList[existing]!.push(\"person\");\n }\n if (!sourceList[existing]!.includes(source)) {\n sourceList[existing]!.push(source);\n }\n } else {\n patternIndex.set(lower, patternList.length);\n patternList.push(normalized);\n labelList.push([\"person\"]);\n sourceList.push([source]);\n }\n };\n\n if (config.enableNameCorpus && !excludeCategories.has(\"Names\")) {\n for (const name of getNameCorpusFirstNames(ctx)) {\n addNameEntry(name, \"first-name\");\n }\n for (const name of getNameCorpusSurnames(ctx)) {\n addNameEntry(name, \"surname\");\n }\n for (const title of getNameCorpusTitles(ctx)) {\n const norm = normalizeForSearch(title).replace(/[|\\\\]/g, \"\");\n if (norm.length === 0) continue;\n const lower = norm.toLowerCase();\n const existing = patternIndex.get(lower);\n if (existing !== undefined) {\n if (!sourceList[existing]!.includes(\"title\")) {\n sourceList[existing]!.push(\"title\");\n }\n } else {\n patternIndex.set(lower, patternList.length);\n patternList.push(norm);\n labelList.push([\"person\"]);\n sourceList.push([\"title\"]);\n }\n }\n }\n\n if (patternList.length === 0) {\n return null;\n }\n\n return {\n labels: labelList,\n originals: patternList,\n sources: sourceList,\n };\n};\n\ntype RawMatch = {\n start: number;\n end: number;\n /** All labels for this pattern (e.g., [\"person\", \"address\"]). */\n labels: string[];\n text: string;\n patternIdx: number;\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): 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);\n await Promise.all([\n loadStopwords(ctx),\n loadAllowList(ctx),\n loadPersonStopwords(ctx),\n loadGenericRoles(ctx),\n ]);\n};\n\n// ── Match processor ─────────────────────────────────\n\n/**\n * Process deny list matches from the unified search.\n * Receives all matches; filters to the deny list slice\n * via sliceStart/sliceEnd. Local index into data.labels,\n * data.originals, data.sources is match.pattern - sliceStart.\n *\n * Two-pass approach to reduce false positives:\n * 1. Collect all matches (case-insensitive,\n * whole-word via Rust automaton)\n * 2. Require uppercase start in source text\n * 3. For person names, require at least one\n * mid-sentence occurrence to prove proper noun\n * 4. Return all occurrences of validated terms\n */\nexport const processDenyListMatches = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText: string,\n data: DenyListData,\n ctx: PipelineContext = defaultContext,\n): Entity[] => {\n // Pass 1: collect valid matches grouped by pattern\n const matchesByPattern = new Map<number, RawMatch[]>();\n\n for (const match of allMatches) {\n const idx = match.pattern;\n if (idx < sliceStart || idx >= sliceEnd) {\n continue;\n }\n\n const localIdx = idx - sliceStart;\n\n const sourceChar = fullText[match.start] ?? \"\";\n if (!UPPER_START_RE.test(sourceChar)) {\n continue;\n }\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 keyword = matchText.toLowerCase();\n if (getStopwords(ctx).has(keyword) || getAllowList(ctx).has(keyword)) {\n continue;\n }\n\n // Skip ALL-CAPS tokens (likely acronyms, not PII)\n // unless surrounding context is also all-caps\n if (ALL_UPPER_RE.test(matchText)) {\n continue;\n }\n\n const labels = data.labels[localIdx];\n if (!labels || labels.length === 0) {\n continue;\n }\n\n const entry: RawMatch = {\n start: match.start,\n end: match.end,\n labels,\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 const hasPerson = first.labels.includes(\"person\");\n const nonPersonLabels = first.labels.filter((l) => l !== \"person\");\n\n // Person hits go to chain scoring (Pass 2b).\n // Skip words that are valid places/orgs but not\n // person names (months, states, languages).\n if (hasPerson) {\n const keyword = first.text.toLowerCase();\n if (!getPersonStopwords(ctx).has(keyword)) {\n for (const m of matches) {\n nameHits.push(m);\n }\n }\n }\n\n // Emit entities for all non-person labels\n for (const m of matches) {\n for (const label of nonPersonLabels) {\n results.push({\n start: m.start,\n end: m.end,\n label,\n text: m.text,\n score: 0.9,\n source: DETECTION_SOURCES.DENY_LIST,\n });\n }\n }\n }\n\n // Pass 2b: person hits — chain adjacent hits and\n // extend to following capitalised words.\n nameHits.sort((a, b) => a.start - b.start);\n\n const nameConsumed = new Set<number>();\n for (let i = 0; i < nameHits.length; i++) {\n if (nameConsumed.has(i)) {\n continue;\n }\n const hit = nameHits[i];\n if (!hit) {\n continue;\n }\n\n // Build chain of adjacent person hits\n const chain: RawMatch[] = [hit];\n let j = i + 1;\n\n while (j < nameHits.length && chain.length < 5) {\n const next = nameHits[j];\n if (!next) {\n break;\n }\n const prev = chain.at(-1);\n if (!prev) {\n break;\n }\n\n const gap = fullText.slice(prev.end, next.start);\n const breaksOnPeriod =\n gap.includes(\".\") && !isInitialContinuationGap(prev.text, gap);\n if (\n gap.length > 4 ||\n gap.length === 0 ||\n gap.includes(\"\\n\") ||\n gap.includes(\"\\t\") ||\n PERSON_CHAIN_BREAK_RE.test(gap) ||\n breaksOnPeriod\n ) {\n break;\n }\n\n chain.push(next);\n j++;\n }\n\n // Mark chain members consumed\n for (let k = i; k < i + chain.length; k++) {\n nameConsumed.add(k);\n }\n\n // Extend to following capitalised word (for\n // unknown surnames not in the corpus)\n const first = chain.at(0);\n const last = chain.at(-1);\n if (!first || !last) {\n continue;\n }\n\n const extended = extendPersonName(fullText, first.start, last.end, ctx);\n\n // Score: chained names get 0.9, single names 0.5\n const score = chain.length >= 2 ? 0.9 : 0.5;\n\n // Single-word deny-list matches are too noisy:\n // \"Rate\", \"Server\", \"Code\" etc. are surnames but\n // also common English words. Only accept single-\n // word matches when the next word is also uppercase\n // (likely a full name: \"Alena Zemanová\").\n if (chain.length === 1) {\n const afterEnd = last.end;\n const rest = fullText.slice(afterEnd).trimStart();\n const nextIsUpper = rest.length > 1 && /^\\p{Lu}\\p{Ll}/u.test(rest);\n if (!nextIsUpper) {\n continue;\n }\n }\n\n results.push({\n start: first.start,\n end: extended.end,\n label: \"person\",\n text: extended.text,\n score,\n source: DETECTION_SOURCES.DENY_LIST,\n });\n }\n\n // Post-process: extend city/address matches to\n // include adjacent trailing district numbers (e.g.,\n // \"Praha 1\", \"Brno 2\"). Czech and Slovak cities\n // commonly have numbered districts that are part of\n // the address.\n extendCityDistricts(results, fullText);\n\n return results;\n};\n\n/**\n * Extend address-label entities to absorb adjacent\n * integers: trailing district numbers (\"Praha 1\") and\n * leading postal codes (\"80336 München\").\n * Mutates the entities in place.\n */\n// District suffixes: digits (\"Praha 1\") or Roman\n// numerals (\"Příbram II\", \"Brno III\")\n// Valid Roman numerals only (I-XXX range, no invalid\n// combos like IC, LC, VC). Covers district suffixes\n// up to Praha XXX which is more than enough.\n// Roman numeral district suffixes II-XXX. Standalone\n// \"I\" and \"V\" excluded: \"V\" clashes with Czech\n// preposition \"in\"; \"I\" is too ambiguous.\nconst ROMAN_DISTRICT =\n \"XXX|XXIX|XXVIII|XXVII|XXVI|XXV|XXIV|XXIII\" +\n \"|XXII|XXI|XX|XIX|XVIII|XVII|XVI|XV|XIV|XIII\" +\n \"|XII|XI|X|IX|VIII|VII|VI|IV|III|II\";\nconst DISTRICT_SUFFIX_RE = new RegExp(\n `^ (\\\\d{1,2}(?!\\\\d)|(?:${ROMAN_DISTRICT}))` + `(?=[\\\\s,;.)\"\\\\n]|$)`,\n);\n// Postal code before city: \"163 00 \", \"16300 \",\n// \"16300 - \" (with dash separator).\nconst POSTAL_PREFIX_RE = new RegExp(\n `(?:\\\\d{5}|\\\\d{3}\\\\s\\\\d{2})\\\\s*${DASH}?\\\\s*$`,\n);\n\n// Words that must NOT be absorbed into an address span\n// when they follow a postal-code + city pattern. Party\n// roles, organizational nouns, and common legal terms.\nconst TRAILING_WORD_EXCLUSIONS: ReadonlySet<string> = new Set([\n // CZ/SK party roles\n \"nájemce\",\n \"pronajímatel\",\n \"kupující\",\n \"prodávající\",\n \"objednatel\",\n \"zhotovitel\",\n \"dodavatel\",\n \"odběratel\",\n \"věřitel\",\n \"dlužník\",\n \"zadavatel\",\n \"uchazeč\",\n \"příjemce\",\n \"plátce\",\n // Organizational nouns\n \"správa\",\n \"sekretariát\",\n \"kancelář\",\n \"odbor\",\n \"oddělení\",\n \"úřad\",\n \"inspekce\",\n \"agentura\",\n // Legal clause starters\n \"článek\",\n \"smlouva\",\n \"dodatek\",\n \"příloha\",\n \"předmět\",\n \"podmínky\",\n \"ustanovení\",\n]);\n\nconst extendCityDistricts = (entities: Entity[], fullText: string): void => {\n for (const entity of entities) {\n if (entity.label !== \"address\") {\n continue;\n }\n\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 = /^[\\s]*[-–][\\s]*(\\p{Lu}\\p{Ll}+)/u.exec(afterDistrict);\n if (dashDistrictM && !dashDistrictM[0].includes(\"\\n\")) {\n entity.end += dashDistrictM[0].length;\n entity.text = fullText.slice(entity.start, entity.end);\n }\n\n // Leading: \"80336 \" + \"München\" → \"80336 München\"\n // Absorbs 3-5 digit postal codes before the city.\n const beforeMatch = fullText.slice(\n Math.max(0, entity.start - 10),\n entity.start,\n );\n const prefixM = POSTAL_PREFIX_RE.exec(beforeMatch);\n if (prefixM) {\n entity.start -= prefixM[0].length;\n entity.text = fullText.slice(entity.start, entity.end);\n }\n\n // Trailing uppercase word: \"434 01\" + \" Most\" →\n // \"434 01 Most\". Absorb if the next word starts\n // with uppercase and is on the same line.\n // Guard: skip party-role or organizational terms.\n const afterExt = fullText.slice(entity.end);\n // Max 4 spaces gap — more means different column\n const trailingWordM = /^[\\s]{1,4}(\\p{Lu}\\p{Ll}+)/u.exec(afterExt);\n if (trailingWordM && !trailingWordM[0].includes(\"\\n\")) {\n const candidate = (trailingWordM[1] ?? \"\").toLowerCase();\n if (!TRAILING_WORD_EXCLUSIONS.has(candidate)) {\n entity.end += trailingWordM[0].length;\n entity.text = fullText.slice(entity.start, entity.end);\n }\n }\n }\n};\n\n/**\n * Extend a person name match to include subsequent\n * capitalized words. \"Pavel\" + \" Heřmánek\" → \"Pavel\n * Heřmánek\". Stops at lowercase words, punctuation,\n * or end of text. Also extends backward if preceded\n * by a capitalized word (for \"Miroslav Braňka\" when\n * only \"Braňka\" matched).\n */\nconst extendPersonName = (\n text: string,\n start: number,\n end: number,\n ctx: PipelineContext,\n): { end: number; text: string } => {\n let newEnd = end;\n\n // Extend forward: skip whitespace, check if next\n // word starts with uppercase\n let pos = newEnd;\n while (pos < text.length) {\n // Skip single whitespace\n if (pos < text.length && text[pos] === \" \") {\n const wordStart = pos + 1;\n if (wordStart >= text.length) {\n break;\n }\n\n const char = text[wordStart] ?? \"\";\n if (!UPPER_START_RE.test(char)) {\n break;\n }\n\n // Find end of this word\n let wordEnd = wordStart;\n while (wordEnd < text.length && !/\\s/.test(text[wordEnd] ?? \"\")) {\n wordEnd++;\n }\n\n // Skip trailing punctuation (commas, etc.)\n const word = text.slice(wordStart, wordEnd);\n const stripped = word.replace(/[,;.]+$/, \"\");\n if (stripped.length < 2) {\n break;\n }\n\n // Don't extend into global or person stopwords\n const lower = stripped.toLowerCase();\n if (getStopwords(ctx).has(lower) || getPersonStopwords(ctx).has(lower)) {\n break;\n }\n\n newEnd = wordStart + stripped.length;\n pos = newEnd;\n } else {\n break;\n }\n }\n\n return {\n end: newEnd,\n text: text.slice(start, newEnd),\n };\n};\n","/**\n * Address detection via seed expansion.\n *\n * 1. Find \"address seeds\" — high-confidence address\n * components (postal codes, cities, street types)\n * 2. Cluster nearby seeds (within ~150 chars)\n * 3. Expand each cluster to the full address span\n * 4. Score by seed diversity (more types = higher)\n *\n * Language-agnostic: street type words and boundary\n * words are loaded from data dictionaries, not\n * hardcoded per language.\n */\n\nimport type { Match } from \"@stll/text-search\";\n\nimport { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\n\n// ── Seed types ──────────────────────────────────────\n\ntype SeedType =\n | \"street-word\"\n | \"house-number\"\n | \"postal-code\"\n | \"city\"\n | \"address-trigger\";\n\ntype Seed = {\n type: SeedType;\n start: number;\n end: number;\n text: string;\n};\n\n// ── Dictionary loading ──────────────────────────────\n\ntype DictionaryConfig = Record<string, string[] | string>;\n\nlet cachedBoundaryRe: RegExp | null = null;\n\nconst loadBoundaryWords = async (): Promise<DictionaryConfig> => {\n try {\n const mod = await import(\"../data/address-boundaries.json\");\n return mod.default as DictionaryConfig;\n } catch {\n return {};\n }\n};\n\n/**\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 cachedBoundaryRe =\n words.length > 0 ? new RegExp(`\\\\b(?:${words.join(\"|\")})\\\\b`, \"i\") : /(?!)/; // never matches\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 */\nexport const buildStreetTypePatterns = 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\n// ── Seed collection ─────────────────────────────────\n\nconst collectSeeds = (\n allMatches: Match[],\n sliceStart: number,\n sliceEnd: number,\n fullText: string,\n existingEntities: Entity[],\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 seeds.push({\n type: \"street-word\",\n start: match.start,\n end: match.end,\n text: match.text,\n });\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.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 const postalRe = /\\b(?:\\d{3}\\s\\d{2}|\\d{2}-\\d{3})\\b/g;\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 seeds.push({\n type: \"postal-code\",\n start,\n end,\n text: postalMatch[0],\n });\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(\"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 \"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\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 // 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 // Trim trailing punctuation\n while (rightPos > end && /[,;:\\s]/.test(fullText[rightPos - 1] ?? \"\")) {\n rightPos--;\n }\n\n return {\n start: Math.min(leftPos, start),\n end: Math.max(rightPos, end),\n };\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 seeds = collectSeeds(\n allMatches,\n sliceStart,\n sliceEnd,\n fullText,\n existingEntities,\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 text = fullText.slice(start, end).trim();\n\n // Skip very short or very long spans\n if (text.length < 5 || text.length > 300) {\n continue;\n }\n\n // Skip if it contains a newline (likely crossed\n // a structural boundary)\n if (text.includes(\"\\n\")) {\n continue;\n }\n\n results.push({\n start,\n end: start + text.length,\n label: \"address\",\n text,\n score,\n source: DETECTION_SOURCES.REGEX,\n });\n }\n\n return results;\n};\n","import { DETECTION_SOURCES } from \"../types\";\nimport type { Entity } from \"../types\";\nimport { LEGAL_SUFFIXES } from \"../config/legal-forms\";\n\nconst TRAILING_SEP = /[,\\s]+$/;\nconst WORD_CHAR_RE = /[\\p{L}\\p{N}]/u;\nconst ORG_PROPAGATION_SCORE = 0.9;\n\ntype Seed = {\n baseName: string;\n label: string;\n};\n\n/**\n * After the main detection pass, collect organization\n * entities with a legal form suffix, strip the suffix\n * to get the base name, and re-scan the full text for\n * bare mentions of that base name. Returns new entities\n * for occurrences not already covered.\n */\nexport const propagateOrgNames = (\n entities: Entity[],\n fullText: string,\n): Entity[] => {\n const seeds: Seed[] = [];\n const seenBases = new Set<string>();\n\n for (const e of entities) {\n if (e.label !== \"organization\") continue;\n for (const suffix of LEGAL_SUFFIXES) {\n if (e.text.endsWith(suffix)) {\n const base = e.text\n .slice(0, -suffix.length)\n .replace(TRAILING_SEP, \"\")\n .trim();\n if (base.length >= 3 && !seenBases.has(base)) {\n seenBases.add(base);\n seeds.push({\n baseName: base,\n label: e.label,\n });\n }\n break;\n }\n }\n }\n\n if (seeds.length === 0) return [];\n\n // Build a mutable array of already-covered spans\n // for overlap checks. Updated as new entities are\n // emitted to prevent duplicate propagation.\n const covered: [number, number][] = entities.map((e) => [e.start, e.end]);\n const isOverlapping = (start: number, end: number): boolean =>\n covered.some(([cs, ce]) => start < ce && end > cs);\n\n const results: Entity[] = [];\n\n for (const seed of seeds) {\n const { baseName, label } = seed;\n let searchFrom = 0;\n while (searchFrom < fullText.length) {\n const idx = fullText.indexOf(baseName, searchFrom);\n if (idx === -1) break;\n\n const matchEnd = idx + baseName.length;\n\n // Word boundary: reject if preceded or followed\n // by a letter or digit (prevents substring\n // matches like \"ACME\" inside \"ACME2\").\n const prevCh = fullText[idx - 1] ?? \"\";\n const nextCh = fullText[matchEnd] ?? \"\";\n if (WORD_CHAR_RE.test(prevCh) || WORD_CHAR_RE.test(nextCh)) {\n searchFrom = idx + 1;\n continue;\n }\n\n // Skip if already covered by an existing entity\n // or a previously propagated result.\n if (!isOverlapping(idx, matchEnd)) {\n results.push({\n start: idx,\n end: matchEnd,\n label,\n text: baseName,\n score: ORG_PROPAGATION_SCORE,\n source: DETECTION_SOURCES.COREFERENCE,\n });\n covered.push([idx, matchEnd]);\n }\n\n searchFrom = matchEnd;\n }\n }\n\n return results;\n};\n","import type { Entity } from \"../types\";\n\n// Common Czech words that start uppercase at sentence\n// beginnings but are not street names. Module-level\n// to avoid allocation in a hot loop.\nconst BARE_STOPWORDS = new Set([\n \"Příloha\",\n \"Smlouva\",\n \"Článek\",\n \"Dodatek\",\n \"Celkem\",\n \"Strana\",\n \"Faktura\",\n \"Částka\",\n \"Položka\",\n \"Kapitola\",\n \"Zákon\",\n \"Vyhláška\",\n \"Nařízení\",\n \"Usnesení\",\n \"Rozsudek\",\n \"Bod\",\n \"Odstavec\",\n \"Záloha\",\n \"Zbývá\",\n \"Dne\",\n \"Platba\",\n \"Datum\",\n \"Splatnost\",\n \"Variabilní\",\n \"Konstantní\",\n \"Specifický\",\n]);\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 boosted.push({ ...entity, score: boostedScore });\n }\n }\n\n return boosted;\n};\n\n// ── Address backward scan ────────────────────────────\n\nconst UPPER_WORD_RE = /\\p{Lu}/u;\n\n/** Header zone: top 15% of document */\nconst HEADER_ZONE_FRACTION = 0.15;\n\n/** Context window for address adjacency */\nconst STREET_CONTEXT_WINDOW = 200;\n\n// ── Preposition data (lazy-loaded from JSON) ────────\n\ntype PrepositionData = {\n address: Record<string, string[] | string>;\n temporal: Record<string, string[] | string>;\n};\n\nlet _addressPreps: ReadonlySet<string> | null = null;\nlet _temporalPreps: ReadonlySet<string> | null = null;\nlet _prepsPromise: Promise<void> | null = null;\n\nconst loadPrepositions = async (): Promise<void> => {\n try {\n const mod = await import(\"../data/address-prepositions.json\");\n const data: PrepositionData = mod.default ?? mod;\n // Merge all languages into flat sets\n const addr = new Set<string>();\n const temp = new Set<string>();\n for (const words of Object.values(data.address)) {\n if (Array.isArray(words)) {\n for (const w of words) addr.add(w.toLowerCase());\n }\n }\n for (const words of Object.values(data.temporal)) {\n if (Array.isArray(words)) {\n for (const w of words) temp.add(w.toLowerCase());\n }\n }\n _addressPreps = addr;\n _temporalPreps = temp;\n } catch {\n _addressPreps = new Set();\n _temporalPreps = new Set();\n }\n};\n\n/** Ensure preposition data is loaded. */\nexport const initPrepositions = (): Promise<void> => {\n if (!_prepsPromise) {\n _prepsPromise = loadPrepositions();\n }\n return _prepsPromise;\n};\n\nconst getAddressPreps = (): ReadonlySet<string> => _addressPreps ?? new Set();\n\nconst getTemporalPreps = (): ReadonlySet<string> => _temporalPreps ?? new Set();\n\n// ── Street type abbreviations (lazy-loaded) ─────────\n\nlet _streetAbbrevs: ReadonlySet<string> | null = null;\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 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 w of words) {\n // Only keep abbreviated forms (with dots)\n if (w.includes(\".\")) {\n abbrevs.add(w.toLowerCase());\n }\n }\n }\n _streetAbbrevs = abbrevs;\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\nconst getStreetAbbrevs = (): ReadonlySet<string> => _streetAbbrevs ?? new Set();\n\n/**\n * Scan backwards from known address entities and\n * house number patterns to find street names.\n *\n * Strategy (a): from a house number like \"2512/2a\",\n * walk left to find the first uppercase word — that's\n * the street name start. \"Mezi úvozy 2512/2a\" →\n * captures \"Mezi úvozy 2512/2a\".\n *\n * Strategy (b): if a colon \":\" appears within 3 chars\n * before the street start, boost confidence. Colons\n * signal \"label: value\" pairs universal in contracts.\n *\n * Strategy (c): in the header zone (top 15% of doc),\n * be more aggressive — detect street patterns even\n * without nearby address entities.\n */\nexport const detectStreetPatternsNearAddresses = (\n fullText: string,\n existingEntities: Entity[],\n): Entity[] => {\n const results: Entity[] = [];\n const addressEntities = existingEntities.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 const score = hasColon ? 0.95 : inHeader ? 0.85 : 0.8;\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\".\nconst ORPHAN_STREET_RE =\n /^\\s*(\\p{Lu}[\\p{Ll}\\p{Lu}]+(?:\\s+[\\p{Lu}\\p{Ll}][\\p{Ll}]+)*\\s+\\d{2,4}[a-zA-Z]?)\\s*$/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 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 = existingEntities.some(\n (e) => Math.abs(e.start - end) < 200 || Math.abs(e.end - start) < 200,\n );\n if (!hasContext) {\n continue;\n }\n\n results.push({\n start,\n end,\n label: \"address\",\n text: captured,\n score: 0.85,\n source: \"regex\",\n });\n }\n\n return results;\n};\n","import type { Entity } from \"../types\";\nimport type { PipelineContext } from \"../context\";\nimport { defaultContext } from \"../context\";\n\n// ── Zone types ───────────────────────────────────\n\nexport type DocumentZone = \"header\" | \"signature\" | \"body\" | \"table\";\n\nexport type ZoneSpan = {\n zone: DocumentZone;\n start: number;\n end: number;\n};\n\n// ── Score adjustments per zone ───────────────────\n\n/**\n * Additive score adjustments per document zone.\n * Header and signature blocks are dense with PII;\n * tables often contain structured identifying data.\n */\nexport const ZONE_SCORE_ADJUSTMENTS = {\n header: 0.1,\n signature: 0.15,\n body: 0,\n table: 0.05,\n} as const satisfies Record<DocumentZone, number>;\n\n// ── Lazy-loaded config ───────────────────────────\n\ntype SectionHeadingsConfig = {\n patterns: Array<{ re: string; flags: string }>;\n};\n\ntype SigningClauseConfig = {\n patterns: Array<{\n lang: string;\n prefix: string;\n suffix: string;\n prepositions: string[];\n }>;\n};\n\nconst loadSectionHeadings = async (): Promise<RegExp[]> => {\n const mod = await import(\"../data/section-headings.json\");\n const data: SectionHeadingsConfig = mod.default ?? mod;\n return data.patterns.map((p) => new RegExp(p.re, p.flags));\n};\n\nconst loadSigningClauses = async (): Promise<RegExp[]> => {\n const mod = await import(\"../data/signing-clauses.json\");\n const data: SigningClauseConfig = mod.default ?? mod;\n return data.patterns.map((p) => {\n // Build a pattern that matches the signing\n // clause prefix at the start of a line.\n // Note: prefix/suffix are regex fragments by\n // design (e.g. `(?:V|Ve)\\\\s+`), not literals.\n const prefix = p.prefix || \"\";\n const suffix = p.suffix || \"\";\n // Include prepositions so multi-word place\n // names like \"Ústí nad Labem\" are matched,\n // consistent with buildSigningClausePatterns\n // in detectors/regex.ts.\n const prepAlt = p.prepositions.length > 0 ? p.prepositions.join(\"|\") : null;\n const place = prepAlt\n ? `\\\\p{Lu}\\\\p{Ll}+` +\n `(?:\\\\s+(?:${prepAlt})` +\n `\\\\s+\\\\p{Lu}\\\\p{Ll}+)*` +\n `(?:\\\\s+\\\\p{Lu}\\\\p{Ll}+)*`\n : `\\\\p{Lu}\\\\p{Ll}+` + `(?:[- ]\\\\p{Lu}\\\\p{Ll}+)*`;\n // Anchor to the start of the line. Each line is\n // already split on \\n, so ^ suffices without m.\n const combined = `^\\\\s*(?:${prefix}${place}${suffix})`;\n return new RegExp(combined, \"u\");\n });\n};\n\n/**\n * Ensure config data is loaded. Call once before\n * classifyZones. Safe to call multiple times.\n */\nexport const initZoneClassifier = (\n ctx: PipelineContext = defaultContext,\n): Promise<void> => {\n if (ctx.zoneInitPromise) return ctx.zoneInitPromise;\n ctx.zoneInitPromise = Promise.all([\n loadSectionHeadings(),\n loadSigningClauses(),\n ])\n .then(([headings, clauses]) => {\n ctx.zoneHeadingPatterns = headings;\n ctx.zoneSigningPatterns = clauses;\n })\n .catch((err: unknown) => {\n // Clear cached promise so a subsequent call\n // can retry after a transient failure.\n ctx.zoneInitPromise = null;\n throw err;\n });\n return ctx.zoneInitPromise;\n};\n\n// ── Table detection ──────────────────────────────\n\nconst MIN_TABS_FOR_TABLE = 2;\n\nconst isTableLine = (line: string): boolean => {\n let tabCount = 0;\n for (const ch of line) {\n if (ch === \"\\t\") tabCount++;\n if (tabCount >= MIN_TABS_FOR_TABLE) return true;\n }\n return false;\n};\n\n// ── Zone classification ──────────────────────────\n\n/**\n * Classify a document into zones based on\n * structural heuristics. Zones are non-overlapping\n * and cover the entire text.\n *\n * Must call `initZoneClassifier()` first.\n */\nexport const classifyZones = (\n fullText: string,\n ctx: PipelineContext = defaultContext,\n): ZoneSpan[] => {\n if (fullText.length === 0) return [];\n\n const headingRes = ctx.zoneHeadingPatterns;\n const signingRes = ctx.zoneSigningPatterns;\n\n if (!headingRes || !signingRes) {\n console.warn(\n \"[anonymize] classifyZones called before \" +\n \"initZoneClassifier(); returning body-only\",\n );\n return [{ zone: \"body\", start: 0, end: fullText.length }];\n }\n\n const lines = fullText.split(\"\\n\");\n const zones: ZoneSpan[] = [];\n\n // Find header end: first section heading line\n let headerEndLine = -1;\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n if (line === undefined) continue;\n for (const re of headingRes) {\n if (re.test(line)) {\n headerEndLine = i;\n break;\n }\n }\n if (headerEndLine !== -1) break;\n }\n\n // Find signature start: last signing clause line\n let signatureStartLine = -1;\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i];\n if (line === undefined) continue;\n for (const re of signingRes) {\n if (re.test(line)) {\n signatureStartLine = i;\n break;\n }\n }\n if (signatureStartLine !== -1) break;\n }\n\n // Build offset map: line index -> char offset\n const lineOffsets: number[] = [];\n let offset = 0;\n for (const line of lines) {\n lineOffsets.push(offset);\n // +1 for the newline character\n offset += line.length + 1;\n }\n\n // Determine zone boundaries\n let headerEndOffset =\n headerEndLine >= 0 ? (lineOffsets[headerEndLine] ?? 0) : 0;\n\n const signatureStartOffset =\n signatureStartLine >= 0\n ? (lineOffsets[signatureStartLine] ?? fullText.length)\n : fullText.length;\n\n // Guard: if the signing clause appears before\n // the first section heading, the zones would\n // overlap. Treat as degenerate layout: drop the\n // header so the signature zone takes priority.\n if (\n headerEndLine > 0 &&\n signatureStartLine >= 0 &&\n headerEndOffset > signatureStartOffset\n ) {\n headerEndLine = -1;\n headerEndOffset = 0;\n }\n\n // Add header zone if detected. Using > 0 (not\n // >= 0) intentionally: when the section heading is\n // on line 0, there is no preamble to classify as a\n // header — everything starts as body.\n if (headerEndLine > 0) {\n zones.push({\n zone: \"header\",\n start: 0,\n end: headerEndOffset,\n });\n }\n\n // Scan body region for table zones\n const bodyStart = headerEndLine > 0 ? headerEndOffset : 0;\n const bodyEnd =\n signatureStartLine >= 0 ? signatureStartOffset : fullText.length;\n\n let tableStart = -1;\n for (\n let i = Math.max(headerEndLine, 0);\n i < (signatureStartLine >= 0 ? signatureStartLine : lines.length);\n i++\n ) {\n const line = lines[i];\n if (line === undefined) continue;\n const lineStart = lineOffsets[i] ?? 0;\n const lineEnd = lineStart + line.length;\n\n if (isTableLine(line)) {\n if (tableStart === -1) {\n tableStart = lineStart;\n }\n } else if (tableStart !== -1) {\n zones.push({\n zone: \"table\",\n start: tableStart,\n end: lineStart,\n });\n tableStart = -1;\n }\n\n // Close table at the end of body range\n if (\n i ===\n (signatureStartLine >= 0 ? signatureStartLine - 1 : lines.length - 1) &&\n tableStart !== -1\n ) {\n zones.push({\n zone: \"table\",\n start: tableStart,\n end: Math.min(lineEnd + 1, bodyEnd),\n });\n tableStart = -1;\n }\n }\n\n // Fill body gaps between header/table/signature\n const sortedSpecial = zones.toSorted((a, b) => a.start - b.start);\n\n let cursor = bodyStart;\n const bodyZones: ZoneSpan[] = [];\n for (const span of sortedSpecial) {\n if (span.zone === \"header\") continue;\n if (span.start > cursor) {\n bodyZones.push({\n zone: \"body\",\n start: cursor,\n end: span.start,\n });\n }\n cursor = Math.max(cursor, span.end);\n }\n if (cursor < bodyEnd) {\n bodyZones.push({\n zone: \"body\",\n start: cursor,\n end: bodyEnd,\n });\n }\n\n for (const z of bodyZones) zones.push(z);\n\n // Add signature zone if detected\n if (signatureStartLine >= 0) {\n zones.push({\n zone: \"signature\",\n start: signatureStartOffset,\n end: fullText.length,\n });\n }\n\n return zones.toSorted((a, b) => a.start - b.start);\n};\n\n// ── Entity zone lookup ───────────────────────────\n\n/**\n * Find which zone an entity's midpoint falls in.\n * Returns \"body\" if no zone matches (defensive).\n */\nconst findZone = (midpoint: number, zones: ZoneSpan[]): DocumentZone => {\n for (const span of zones) {\n if (midpoint >= span.start && midpoint < span.end) {\n return span.zone;\n }\n }\n return \"body\";\n};\n\n/**\n * Apply zone-based score adjustments to entities.\n * Entities in header/signature/table zones get a\n * small additive boost reflecting the higher PII\n * density in those regions.\n *\n * Returns a new array; does not mutate inputs.\n */\nexport const applyZoneAdjustments = (\n entities: Entity[],\n zones: ZoneSpan[],\n): Entity[] => {\n if (zones.length === 0) {\n return entities.map((e) => ({ ...e }));\n }\n\n const result: Entity[] = [];\n for (const entity of entities) {\n const midpoint = (entity.start + entity.end) / 2;\n const zone = findZone(midpoint, zones);\n const adjustment = ZONE_SCORE_ADJUSTMENTS[zone];\n\n if (adjustment > 0) {\n result.push({\n ...entity,\n score: Math.min(1, entity.score + adjustment),\n });\n } else {\n result.push({ ...entity });\n }\n }\n return result;\n};\n","import type { Match, PatternEntry } from \"@stll/text-search\";\n\nimport { getTextSearch } from \"../search-engine\";\nimport type { Entity } from \"../types\";\n\n// ── Types ───────────────────────────────────────────\n\nexport type HotwordRule = {\n hotwords: string[];\n targetLabels: string[];\n scoreAdjustment: number;\n reclassifyTo?: string;\n proximityBefore: number;\n proximityAfter: number;\n};\n\ntype HotwordRulesConfig = {\n rules: HotwordRule[];\n};\n\n// ── Lazy-loaded state ───────────────────────────────\n\nlet rules: HotwordRule[] | null = null;\nlet search: { findIter: (text: string) => Match[] } | null = null;\n/**\n * Maps each TextSearch pattern index back to the\n * rule index that owns it, so a single AC scan\n * resolves all hotword hits to their rule.\n */\nlet patternToRule: number[] | null = null;\nlet initPromise: Promise<void> | null = null;\n\n// ── Init ────────────────────────────────────────────\n\nconst loadRules = async (): Promise<void> => {\n const mod = await import(\"../data/hotword-rules.json\");\n const data: HotwordRulesConfig = mod.default ?? mod;\n const loaded = data.rules;\n\n // Build a flat pattern list and the reverse map.\n const patterns: PatternEntry[] = [];\n const mapping: number[] = [];\n\n for (let ruleIdx = 0; ruleIdx < loaded.length; ruleIdx++) {\n const rule = loaded[ruleIdx];\n if (!rule) continue;\n for (const hw of rule.hotwords) {\n patterns.push({\n pattern: hw,\n literal: true,\n caseInsensitive: true,\n });\n mapping.push(ruleIdx);\n }\n }\n\n const builtSearch =\n patterns.length > 0\n ? new (getTextSearch())(patterns, {\n overlapStrategy: \"all\",\n caseInsensitive: true,\n wholeWords: true,\n })\n : null;\n\n // Assign all module state atomically after all\n // failable operations succeed, so a mid-flight\n // error cannot leave a half-initialized state.\n patternToRule = mapping;\n search = builtSearch;\n rules = loaded;\n};\n\n/**\n * Load hotword rules from the data package.\n * Safe to call multiple times; subsequent calls\n * are no-ops.\n */\nexport const initHotwordRules = async (): Promise<void> => {\n if (rules !== null) return;\n if (initPromise !== null) return initPromise;\n initPromise = loadRules().catch((err) => {\n // Reset so callers can retry on transient failure.\n initPromise = null;\n throw err;\n });\n return initPromise;\n};\n\n/**\n * Expand requested output labels with any source labels\n * that hotword rules may reclassify into them.\n *\n * Example: requesting only \"date of birth\" still needs\n * \"date\" candidates to survive until the hotword pass.\n * If rules are not initialized, or if no labels were\n * requested, returns the input labels unchanged.\n */\nexport const expandLabelsForHotwordRules = (\n requestedLabels: readonly string[],\n): readonly string[] => {\n if (rules === null || requestedLabels.length === 0) {\n return requestedLabels;\n }\n\n const requested = new Set(requestedLabels);\n const expanded = new Set(requestedLabels);\n\n for (const rule of rules) {\n if (rule.reclassifyTo === undefined || !requested.has(rule.reclassifyTo)) {\n continue;\n }\n for (const label of rule.targetLabels) {\n expanded.add(label);\n }\n }\n\n return [...expanded];\n};\n\n// ── Application ─────────────────────────────────────\n\n/**\n * Apply hotword context rules to detected entities.\n *\n * Scans `fullText` once with a single AC automaton\n * for all hotwords across all rules, then checks\n * proximity to each entity. Distance-decayed\n * adjustment: closer hotwords give a stronger boost.\n *\n * Returns a new array; input entities are not mutated.\n */\nexport const applyHotwordRules = (\n entities: Entity[],\n fullText: string,\n): Entity[] => {\n if (\n rules === null ||\n rules.length === 0 ||\n search === null ||\n patternToRule === null\n ) {\n return entities;\n }\n\n // Single scan for all hotword positions.\n const hits = search.findIter(fullText);\n if (hits.length === 0) return entities;\n\n // Group hits by rule index for fast lookup.\n const hitsByRule = new Map<number, Match[]>();\n for (const hit of hits) {\n const ruleIdx = patternToRule[hit.pattern];\n if (ruleIdx === undefined) continue;\n let bucket = hitsByRule.get(ruleIdx);\n if (bucket === undefined) {\n bucket = [];\n hitsByRule.set(ruleIdx, bucket);\n }\n bucket.push(hit);\n }\n\n const result: Entity[] = [];\n\n for (const entity of entities) {\n let bestAdjustment = 0;\n let bestReclassify: string | undefined;\n\n for (let ruleIdx = 0; ruleIdx < rules.length; ruleIdx++) {\n const rule = rules[ruleIdx];\n if (!rule) continue;\n\n // Check if entity label matches this rule.\n if (!rule.targetLabels.includes(entity.label)) {\n continue;\n }\n\n const ruleHits = hitsByRule.get(ruleIdx);\n if (ruleHits === undefined) continue;\n\n for (const hit of ruleHits) {\n // Asymmetric proximity check.\n // Hotword BEFORE entity: hotword end <=\n // entity start, distance = entity.start -\n // hit.end\n // Hotword AFTER entity: hotword start >=\n // entity end, distance = hit.start -\n // entity.end\n let distance: number;\n let maxDistance: number;\n\n if (hit.end <= entity.start) {\n // Hotword is before the entity.\n distance = entity.start - hit.end;\n maxDistance = rule.proximityBefore;\n } else if (hit.start >= entity.end) {\n // Hotword is after the entity.\n distance = hit.start - entity.end;\n maxDistance = rule.proximityAfter;\n } else {\n // Hotword overlaps the entity: distance 0,\n // use larger window for max.\n distance = 0;\n maxDistance = Math.max(rule.proximityBefore, rule.proximityAfter);\n }\n\n if (distance > maxDistance) continue;\n\n // Distance-decayed adjustment.\n const decay = maxDistance === 0 ? 1 : 1 - distance / maxDistance;\n const adj = rule.scoreAdjustment * decay;\n\n if (Math.abs(adj) > Math.abs(bestAdjustment)) {\n bestAdjustment = adj;\n // Only carry reclassification from boost\n // rules; a penalty winner must not silently\n // overwrite a label set by a closer positive\n // rule.\n if (adj > 0) {\n bestReclassify = rule.reclassifyTo;\n } else {\n bestReclassify = undefined;\n }\n }\n }\n }\n\n if (bestAdjustment === 0) {\n result.push(entity);\n continue;\n }\n\n const newScore = Math.min(1, Math.max(0, entity.score + bestAdjustment));\n const newLabel =\n bestReclassify !== undefined ? bestReclassify : entity.label;\n\n result.push({\n ...entity,\n score: newScore,\n label: newLabel,\n });\n }\n\n return result;\n};\n","import type { Entity } from \"../types\";\n\n/** Max gap (in chars) between entities to merge. */\nconst MAX_GAP = 3;\n\n/**\n * Characters allowed in the gap between two adjacent\n * same-label entities that should be merged: spaces,\n * tabs, commas, and hyphens. Uses `[ \\t,\\-]` instead\n * of `\\s` to avoid merging entities across newlines.\n */\nconst GAP_PATTERN = /^[ \\t,\\-]+$/;\n\n/**\n * Build a set of word boundary offsets for the full\n * text using `Intl.Segmenter`. Returns a sorted array\n * of offsets where words start and end.\n */\nconst buildWordBoundaries = (text: string): Set<number> => {\n // Use \"und\" (undetermined) locale for consistent\n // word-boundary results across environments.\n const segmenter = new Intl.Segmenter(\"und\", {\n granularity: \"word\",\n });\n const boundaries = new Set<number>();\n for (const seg of segmenter.segment(text)) {\n if (!seg.isWordLike) continue;\n boundaries.add(seg.index);\n boundaries.add(seg.index + seg.segment.length);\n }\n return boundaries;\n};\n\n/**\n * Characters that act as hard stops when scanning\n * backward for a word boundary. Entity boundaries\n * should never extend past these.\n */\nconst WORD_START_STOPS = new Set([\n \"\\n\",\n \"\\r\",\n \",\",\n \";\",\n \"(\",\n \")\",\n \"[\",\n \"]\",\n \"&\",\n]);\n\n/**\n * Find the word-start offset at or before `pos`.\n * Scans left until a word boundary is found.\n */\nconst wordStartAt = (\n pos: number,\n boundaries: Set<number>,\n text: string,\n): number => {\n let p = pos;\n while (p > 0 && !boundaries.has(p)) {\n const prev = text[p - 1];\n if (prev !== undefined && WORD_START_STOPS.has(prev)) {\n return p;\n }\n p--;\n }\n return p;\n};\n\n/**\n * Characters that act as hard stops when scanning\n * forward for a word boundary. Entity boundaries\n * should never extend past these.\n */\nconst WORD_END_STOPS = new Set([\n \"\\n\",\n \"\\r\",\n \",\",\n \";\",\n \".\",\n \"(\",\n \")\",\n \"[\",\n \"]\",\n \"&\",\n]);\n\n/**\n * Find the word-end offset at or after `pos`.\n * Scans right until a word boundary is found.\n */\nconst wordEndAt = (\n pos: number,\n boundaries: Set<number>,\n text: string,\n): number => {\n let p = pos;\n while (p < text.length && !boundaries.has(p)) {\n const ch = text[p];\n if (ch !== undefined && WORD_END_STOPS.has(ch)) {\n return p;\n }\n p++;\n }\n return p;\n};\n\n/**\n * Binary search: find the leftmost index in `arr`\n * where `arr[index].start >= value`.\n */\nconst lowerBound = (arr: Entity[], value: number): number => {\n let lo = 0;\n let hi = arr.length;\n while (lo < hi) {\n const mid = (lo + hi) >>> 1;\n const el = arr[mid];\n if (el && el.start < value) {\n lo = mid + 1;\n } else {\n hi = mid;\n }\n }\n return lo;\n};\n\n/**\n * Merge adjacent same-label entities separated only by\n * whitespace, comma, or hyphen (max 3 chars). Also\n * merges same-label entities that partially overlap\n * (which can happen after word-boundary expansion).\n *\n * Looks for the last same-label entity in the result\n * (not just the very last entity) so that an\n * intervening different-label entity does not prevent\n * merging.\n *\n * Uses binary search for the `gapOccupied` check and\n * a Map for O(1) same-label prev lookup. O(n log n).\n */\nconst mergeAdjacent = (entities: Entity[], fullText: string): Entity[] => {\n const sorted = entities.toSorted((a, b) => a.start - b.start);\n const result: Entity[] = [];\n // O(1) lookup for the last same-label entity in\n // result, replacing the O(n) backward scan.\n const lastByLabel = new Map<string, Entity>();\n\n for (const entity of sorted) {\n const prev = lastByLabel.get(entity.label);\n\n if (!prev) {\n const copy = { ...entity };\n result.push(copy);\n lastByLabel.set(entity.label, copy);\n continue;\n }\n\n // Handle overlap created by fixPartialWords:\n // two same-label entities may now partially overlap\n // after word-boundary expansion.\n if (entity.start < prev.end) {\n prev.end = Math.max(prev.end, entity.end);\n prev.text = fullText.slice(prev.start, prev.end);\n prev.score = Math.max(prev.score, entity.score);\n continue;\n }\n\n const gap = fullText.slice(prev.end, entity.start);\n // GAP_PATTERN uses `+` quantifier, so empty gaps\n // (zero-gap / touching entities) won't match.\n // Also reject merging when a different-label entity\n // occupies the gap range (would create cross-label\n // overlap). Use binary search to find candidates\n // in the gap range instead of scanning all entities.\n const gapStart = prev.end;\n const gapEnd = entity.start;\n const searchIdx = lowerBound(sorted, gapStart);\n let gapOccupied = false;\n for (let k = searchIdx; k < sorted.length; k++) {\n const other = sorted[k];\n if (!other || other.start >= gapEnd) break;\n if (other.label !== entity.label && other.end > gapStart) {\n gapOccupied = true;\n break;\n }\n }\n\n if (!gapOccupied && gap.length <= MAX_GAP && GAP_PATTERN.test(gap)) {\n // Merge into prev\n prev.end = entity.end;\n prev.text = fullText.slice(prev.start, prev.end);\n prev.score = Math.max(prev.score, entity.score);\n } else {\n const copy = { ...entity };\n result.push(copy);\n lastByLabel.set(entity.label, copy);\n }\n }\n\n return result;\n};\n\n/**\n * Fix partial-word boundaries by extending entity\n * start/end to the nearest word boundary. Does not\n * extend across newlines or into spans occupied by\n * different-label entities.\n *\n * Uses binary search to skip irrelevant entries when\n * clamping at cross-label neighbors. O(n log n) in\n * the common case; O(n^2) worst case when many\n * same-label entities precede a cross-label boundary.\n */\nconst fixPartialWords = (entities: Entity[], fullText: string): Entity[] => {\n const boundaries = buildWordBoundaries(fullText);\n const sorted = entities.toSorted((a, b) => a.start - b.start);\n\n // Build a secondary array sorted by end position\n // for efficient \"nearest entity ending before me\"\n // lookups. Each entry tracks the original entity.\n const byEnd = sorted\n .map((e, idx) => ({ entity: e, idx }))\n .sort((a, b) => a.entity.end - b.entity.end);\n const endPositions = byEnd.map((x) => x.entity.end);\n\n return sorted.map((e, eIdx) => {\n let newStart = wordStartAt(e.start, boundaries, fullText);\n let newEnd = wordEndAt(e.end, boundaries, fullText);\n\n // Clamp start: find different-label entities whose\n // end is in (newStart, e.start]. We search byEnd\n // for entities with end > newStart and end <= e.start.\n // Binary search for the first entry with\n // end > newStart.\n let lo = 0;\n let hi = endPositions.length;\n while (lo < hi) {\n const mid = (lo + hi) >>> 1;\n if ((endPositions[mid] ?? Number.POSITIVE_INFINITY) <= newStart) {\n lo = mid + 1;\n } else {\n hi = mid;\n }\n }\n // Scan forward from lo; all entries have end >\n // newStart. Stop when end > e.start.\n for (let k = lo; k < byEnd.length; k++) {\n const entry = byEnd[k];\n if (!entry || entry.entity.end > e.start) break;\n if (entry.idx === eIdx) continue;\n if (entry.entity.label === e.label) continue;\n // This entity's end is in (newStart, e.start]\n // and has a different label: clamp.\n newStart = Math.max(newStart, entry.entity.end);\n }\n\n // Clamp end: find different-label entities whose\n // start is in [e.end, newEnd). Use the start-sorted\n // array with binary search.\n const startIdx = lowerBound(sorted, e.end);\n for (let k = startIdx; k < sorted.length; k++) {\n const other = sorted[k];\n if (!other || other.start >= newEnd) break;\n if (other === e) continue;\n if (other.label === e.label) continue;\n // This entity's start is in [e.end, newEnd)\n // and has a different label: clamp.\n newEnd = Math.min(newEnd, other.start);\n }\n\n if (newStart === e.start && newEnd === e.end) {\n return e;\n }\n return {\n ...e,\n start: newStart,\n end: newEnd,\n text: fullText.slice(newStart, newEnd),\n };\n });\n};\n\n/**\n * Deduplicate entities with identical [start, end, label].\n * Keeps the entry with the highest score.\n */\nconst deduplicateSpans = (entities: Entity[]): Entity[] => {\n const seen = new Map<string, Entity>();\n for (const entity of entities) {\n const key = `${entity.start}:${entity.end}:${entity.label}`;\n const existing = seen.get(key);\n if (!existing || entity.score > existing.score) {\n seen.set(key, entity);\n }\n }\n return [...seen.values()];\n};\n\n/**\n * Remove nested same-label entities. If a shorter\n * entity is fully contained within a longer entity\n * of the same label, drop the shorter one.\n *\n * Uses a \"max end seen\" sweep per label. O(n) after\n * the initial sort.\n */\nconst removeNestedSameLabel = (entities: Entity[]): Entity[] => {\n // Sort by start asc, then by length desc so the\n // longer entity comes first.\n const sorted = entities.toSorted((a, b) => {\n if (a.start !== b.start) return a.start - b.start;\n return b.end - a.end;\n });\n\n const result: Entity[] = [];\n // Track the furthest end seen per label. Any entity\n // whose end <= maxEnd for its label is nested inside\n // a previously seen entity of the same label.\n const maxEndByLabel = new Map<string, number>();\n\n for (const entity of sorted) {\n const maxEnd = maxEndByLabel.get(entity.label);\n if (maxEnd !== undefined && entity.end <= maxEnd) {\n // Nested inside a same-label entity: skip.\n continue;\n }\n maxEndByLabel.set(entity.label, entity.end);\n result.push(entity);\n }\n\n return result;\n};\n\n/**\n * Resolve cross-label overlaps that can arise when\n * `fixPartialWords` independently expands two\n * different-label entities toward the same word\n * boundary. The entity with the higher score (or\n * longer span on tie) keeps its boundary; the other\n * is trimmed so the overlap disappears.\n *\n * Preserved existing structure: sorted + early break\n * already gives good amortized behavior. O(n^2)\n * worst case but rare in practice.\n */\nconst resolveCrossLabelOverlaps = (\n entities: Entity[],\n fullText: string,\n): Entity[] => {\n const sorted = entities\n .map((e) => ({ ...e }))\n .sort((a, b) => a.start - b.start);\n\n for (let i = 0; i < sorted.length; i++) {\n for (let j = i + 1; j < sorted.length; j++) {\n const a = sorted[i];\n const b = sorted[j];\n if (!a || !b) continue;\n if (b.start >= a.end) break; // no overlap\n if (a.label === b.label) continue;\n\n // Skip full containment (one entity fully\n // inside another). Cross-label nesting is\n // valid and should be preserved.\n const aContainsB = a.start <= b.start && a.end >= b.end;\n const bContainsA = b.start <= a.start && b.end >= a.end;\n if (aContainsB || bContainsA) continue;\n\n // Partial overlap. Higher score wins; on tie\n // the longer span wins.\n const aLen = a.end - a.start;\n const bLen = b.end - b.start;\n const aWins = a.score > b.score || (a.score === b.score && aLen >= bLen);\n\n if (aWins) {\n // Trim b's start to a's end\n b.start = a.end;\n b.text = fullText.slice(b.start, b.end);\n } else {\n // Trim a's end to b's start. Because the\n // array is sorted by start and a.end can\n // only decrease, all remaining j will have\n // b.start >= a.end so the break fires\n // immediately.\n a.end = b.start;\n a.text = fullText.slice(a.start, a.end);\n }\n }\n }\n\n // Drop any entity that was trimmed to zero width.\n return sorted.filter((e) => e.start < e.end);\n};\n\n/**\n * Post-processing pass for entity boundary consistency.\n * Runs after mergeAndDedup, before false-positive\n * filtering.\n *\n * 1. Fix partial-word boundaries (respects cross-label\n * neighbors to avoid introducing new overlaps)\n * 2. Resolve any remaining cross-label overlaps\n * 3. Deduplicate identical [start, end, label] spans\n * 4. Merge adjacent same-label entities (catches any\n * new adjacency/overlap from step 1)\n * 5. Remove nested same-label entities\n */\nexport const enforceBoundaryConsistency = (\n entities: Entity[],\n fullText: string,\n): Entity[] => {\n const fixed = fixPartialWords(entities, fullText);\n const resolved = resolveCrossLabelOverlaps(fixed, fullText);\n const deduped = deduplicateSpans(resolved);\n const merged = mergeAdjacent(deduped, fullText);\n return removeNestedSameLabel(merged);\n};\n","/**\n * Build the unified search instances from all\n * detector pattern sources.\n *\n * Two TextSearch instances (not one) to avoid\n * 200K per-pattern object allocations:\n * 1. regex + triggers + legal-forms (mixed, ~140\n * patterns, caseInsensitive for trigger AC)\n * 2. deny-list + street-types + gazetteer\n * (caseInsensitive, overlap \"all\";\n * deny-list/street-type use per-pattern\n * wholeWords: true; gazetteer exact use\n * wholeWords: false; gazetteer fuzzy use\n * distance: 2 via @stll/fuzzy-search)\n *\n * All patterns are PatternEntry objects with\n * per-pattern literal/wholeWords settings.\n */\n\nimport type { PatternEntry, TextSearch } from \"@stll/text-search\";\n\nimport { getTextSearch } from \"./search-engine\";\n\nimport {\n isLegalFormsEnabled,\n type GazetteerEntry,\n type PipelineConfig,\n} from \"./types\";\nimport type { RegexMeta } from \"./detectors/regex\";\nimport type { TriggerRule } from \"./types\";\nimport type { DenyListData } from \"./detectors/deny-list\";\nimport type { PipelineContext } from \"./context\";\nimport { defaultContext } from \"./context\";\n\nimport {\n REGEX_PATTERNS,\n REGEX_META,\n getCurrencyPatterns,\n CURRENCY_PATTERN_META,\n getDatePatterns,\n DATE_PATTERN_META,\n getSigningClausePatterns,\n SIGNING_CLAUSE_META,\n} from \"./detectors/regex\";\nimport { buildLegalFormPatterns } from \"./detectors/legal-forms\";\nimport { buildTriggerPatterns } from \"./detectors/triggers\";\nimport { buildDenyList } from \"./detectors/deny-list\";\nimport { buildStreetTypePatterns } from \"./detectors/address-seeds\";\nimport { buildGazetteerPatterns } from \"./detectors/gazetteer\";\n\ntype PatternSlice = {\n start: number;\n end: number;\n};\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 /** Deny-list + street-types + gazetteer. */\n tsLiterals: TextSearch;\n slices: {\n regex: PatternSlice;\n legalForms: PatternSlice;\n triggers: PatternSlice;\n denyList: PatternSlice;\n streetTypes: PatternSlice;\n gazetteer: PatternSlice;\n };\n regexMeta: readonly RegexMeta[];\n triggerRules: readonly TriggerRule[];\n denyListData: DenyListData | null;\n gazetteerData: GazetteerData | null;\n};\n\nexport const buildUnifiedSearch = async (\n config: PipelineConfig,\n gazetteerEntries: GazetteerEntry[] = [],\n ctx: PipelineContext = defaultContext,\n): Promise<UnifiedSearchInstance> => {\n const legalFormsEnabled = isLegalFormsEnabled(config);\n const [\n legalForms,\n triggers,\n denyListData,\n streetTypes,\n currencyPatterns,\n datePatterns,\n signingPatterns,\n ] = await Promise.all([\n legalFormsEnabled\n ? buildLegalFormPatterns()\n : Promise.resolve([] as string[]),\n config.enableTriggerPhrases\n ? buildTriggerPatterns()\n : Promise.resolve({\n patterns: [] as string[],\n rules: [] as TriggerRule[],\n }),\n config.enableDenyList ? buildDenyList(config, ctx) : Promise.resolve(null),\n buildStreetTypePatterns(),\n getCurrencyPatterns(),\n getDatePatterns(),\n getSigningClausePatterns(),\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 = [\n ...(REGEX_PATTERNS as string[]),\n ...currencyPatterns,\n ...datePatterns,\n ...signingPatterns,\n ];\n const regexMeta: RegexMeta[] = [\n ...REGEX_META,\n ...currencyPatterns.map(() => CURRENCY_PATTERN_META),\n ...datePatterns.map(() => DATE_PATTERN_META),\n ...signingPatterns.map(() => SIGNING_CLAUSE_META),\n ];\n\n let offset = 0;\n\n const regexSlice = {\n start: offset,\n end: offset + allRegex.length,\n };\n offset = regexSlice.end;\n\n const legalFormsSlice = {\n start: offset,\n end: offset + legalForms.length,\n };\n offset = legalFormsSlice.end;\n\n const triggersSlice = {\n start: offset,\n end: offset + triggers.patterns.length,\n };\n\n // Trigger patterns need caseInsensitive on AC\n // (only ~120 objects, not 200K). Regex/legal-form\n // patterns are bare strings (auto-classified).\n const triggerEntries = triggers.patterns.map((p) => ({\n pattern: p,\n literal: true as const,\n caseInsensitive: true,\n }));\n\n const regexAllPatterns = [...allRegex, ...legalForms, ...triggerEntries];\n\n // TextSearch auto-detects DFA state explosion\n // (build time > 2ms) and falls back to individual\n // engines. No manual maxAlternations needed.\n const tsRegex = new (getTextSearch())(regexAllPatterns);\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 // 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): PatternEntry => ({\n pattern: s,\n literal: true as const,\n wholeWords: true,\n });\n const literalAllPatterns: PatternEntry[] = [\n ...denyListOriginals.map(wrapWholeWord),\n ...streetTypes.map(wrapWholeWord),\n ...(gazResult?.patterns ?? []),\n ];\n\n const tsLiterals =\n literalAllPatterns.length > 0\n ? new (getTextSearch())(literalAllPatterns, {\n caseInsensitive: true,\n overlapStrategy: \"all\",\n })\n : new (getTextSearch())([]);\n\n return {\n tsRegex,\n tsLiterals,\n slices: {\n regex: regexSlice,\n legalForms: legalFormsSlice,\n triggers: triggersSlice,\n denyList: denyListSlice,\n streetTypes: streetTypesSlice,\n gazetteer: gazetteerSlice,\n },\n regexMeta,\n triggerRules: triggers.rules,\n denyListData,\n gazetteerData: gazResult?.data ?? null,\n };\n};\n","/**\n * Run the two-instance unified search and return\n * raw matches. Two passes instead of six:\n * 1. regex + triggers + legal-forms\n * 2. deny-list + street-types (normalized text)\n */\n\nimport type { Match } from \"@stll/text-search\";\nimport type { UnifiedSearchInstance } from \"./build-unified-search\";\nimport { normalizeForSearch } from \"./util/normalize\";\n\nexport type UnifiedResult = {\n /** All matches from both instances combined. */\n regexMatches: Match[];\n literalMatches: Match[];\n};\n\nexport const runUnifiedSearch = (\n instance: UnifiedSearchInstance,\n fullText: string,\n): UnifiedResult => {\n // Pass 1: regex + triggers + legal-forms\n // on original text (regex patterns encode\n // their own case flags)\n const regexMatches = instance.tsRegex.findIter(fullText);\n\n // Pass 2: deny-list + street-types on\n // normalized text (NBSP, smart quotes folded)\n const normalized = normalizeForSearch(fullText);\n const literalMatches = instance.tsLiterals.findIter(normalized);\n\n return { regexMatches, literalMatches };\n};\n","import type { Entity } from \"../types\";\n\nconst MASK_TOKEN = \"[MASKED]\";\nconst MASK_LEN = MASK_TOKEN.length;\n\ntype OffsetSegment = {\n /** Start of this mask token in masked text */\n maskedStart: number;\n /** End of this mask token in masked text */\n maskedEnd: number;\n /** Cumulative shift: original - masked */\n shift: number;\n /** Original start of the masked span */\n origStart: number;\n /** Original end of the masked span */\n origEnd: number;\n};\n\nexport type MaskResult = {\n maskedText: string;\n /**\n * Maps masked-text offsets back to original offsets.\n * Returns null if the span overlaps a masked region.\n */\n offsetMap: (\n maskedStart: number,\n maskedEnd: number,\n ) => { start: number; end: number } | null;\n};\n\n/**\n * Replace detected entity spans with placeholder tokens.\n * Each entity span is replaced with \"[MASKED]\" (fixed\n * length). Returns the masked text and an offset mapping\n * function.\n */\nexport const maskDetectedSpans = (\n fullText: string,\n entities: Entity[],\n): MaskResult => {\n if (entities.length === 0) {\n return {\n maskedText: fullText,\n offsetMap: (s, e) => ({ start: s, end: e }),\n };\n }\n\n // Sort by start, then longest span first\n const sorted = entities.toSorted(\n (a, b) => a.start - b.start || b.end - a.end,\n );\n\n // Merge overlapping spans so we don't double-mask.\n // Adjacent (touching) spans are kept separate so each\n // produces its own [MASKED] token, preserving token\n // boundaries for the NER model.\n const spans: { start: number; end: number }[] = [];\n const first = sorted[0];\n if (!first) {\n return {\n maskedText: fullText,\n offsetMap: (s, e) => ({ start: s, end: e }),\n };\n }\n let cur = {\n start: first.start,\n end: first.end,\n };\n for (let i = 1; i < sorted.length; i++) {\n const s = sorted[i];\n if (!s) continue;\n if (s.start < cur.end) {\n cur.end = Math.max(cur.end, s.end);\n } else {\n spans.push(cur);\n cur = { start: s.start, end: s.end };\n }\n }\n spans.push(cur);\n\n // Build masked text and offset segments\n const segments: OffsetSegment[] = [];\n const parts: string[] = [];\n let prev = 0;\n let cumulativeShift = 0;\n\n for (const span of spans) {\n parts.push(fullText.slice(prev, span.start));\n parts.push(MASK_TOKEN);\n\n const origLen = span.end - span.start;\n const delta = origLen - MASK_LEN;\n cumulativeShift += delta;\n\n // Position of mask token in masked text\n const maskedStart = span.start - (cumulativeShift - delta);\n const maskedEnd = maskedStart + MASK_LEN;\n\n segments.push({\n maskedStart,\n maskedEnd,\n shift: cumulativeShift,\n origStart: span.start,\n origEnd: span.end,\n });\n\n prev = span.end;\n }\n parts.push(fullText.slice(prev));\n\n const maskedText = parts.join(\"\");\n\n const offsetMap = (\n maskedStart: number,\n maskedEnd: number,\n ): { start: number; end: number } | null => {\n // Check if span overlaps any masked region\n for (const seg of segments) {\n if (maskedStart < seg.maskedEnd && maskedEnd > seg.maskedStart) {\n return null;\n }\n }\n\n // Find cumulative shift for this position.\n // Since the overlap check above guarantees both\n // endpoints fall within the same gap zone, a\n // single shift value applies to both.\n let shift = 0;\n for (const seg of segments) {\n if (maskedStart >= seg.maskedEnd) {\n shift = seg.shift;\n } else {\n break;\n }\n }\n\n return {\n start: maskedStart + shift,\n end: maskedEnd + shift,\n };\n };\n\n return { maskedText, offsetMap };\n};\n\n/**\n * Map NER entities from masked-text offsets back to\n * original-text offsets. Discards any NER entity whose\n * mapped span overlaps a masked (rule-detected) region.\n */\nexport const unmaskNerEntities = (\n nerEntities: Entity[],\n maskResult: MaskResult,\n fullText: string,\n): Entity[] => {\n const result: Entity[] = [];\n\n for (const ner of nerEntities) {\n // offsetMap returns null when the mapped span\n // overlaps any merged rule-entity region, so no\n // secondary overlap check is needed.\n const mapped = maskResult.offsetMap(ner.start, ner.end);\n if (mapped === null) continue;\n\n result.push({\n ...ner,\n start: mapped.start,\n end: mapped.end,\n text: fullText.slice(mapped.start, mapped.end),\n });\n }\n\n return result;\n};\n","import {\n extractDefinedTerms,\n findCoreferenceSpans,\n} from \"./detectors/coreference\";\nimport { processGazetteerMatches } from \"./detectors/gazetteer\";\nimport { detectNameCorpus, initNameCorpus } from \"./detectors/names\";\nimport { processRegexMatches } from \"./detectors/regex\";\nimport { processLegalFormMatches } from \"./detectors/legal-forms\";\nimport { processTriggerMatches } from \"./detectors/triggers\";\nimport {\n ensureDenyListData,\n processDenyListMatches,\n} from \"./detectors/deny-list\";\nimport { processAddressSeeds } from \"./detectors/address-seeds\";\nimport { propagateOrgNames } from \"./detectors/org-propagation\";\nimport {\n boostNearMissEntities,\n detectStreetPatternsNearAddresses,\n detectOrphanStreetLines,\n initPrepositions,\n initStreetAbbrevs,\n} from \"./filters/confidence-boost\";\nimport {\n filterFalsePositives,\n loadGenericRoles,\n} from \"./filters/false-positives\";\nimport {\n applyZoneAdjustments,\n classifyZones,\n initZoneClassifier,\n type ZoneSpan,\n} from \"./filters/zone-classifier\";\nimport {\n applyHotwordRules,\n expandLabelsForHotwordRules,\n initHotwordRules,\n} from \"./filters/hotword-rules\";\nimport { enforceBoundaryConsistency } from \"./filters/boundary-consistency\";\nimport type { Entity, GazetteerEntry, PipelineConfig } from \"./types\";\nimport {\n DEFAULT_ENTITY_LABELS,\n DETECTOR_PRIORITY,\n isLegalFormsEnabled,\n} from \"./types\";\nimport {\n buildUnifiedSearch,\n type UnifiedSearchInstance,\n} from \"./build-unified-search\";\nimport { runUnifiedSearch } from \"./unified-search\";\nimport { maskDetectedSpans, unmaskNerEntities } from \"./util/entity-masking\";\nimport type { PipelineContext } from \"./context\";\nimport { defaultContext } from \"./context\";\n\n/**\n * Sources backed by curated literal dictionaries.\n * Longer matches from these sources are more specific,\n * so the containment rule trusts their length.\n */\nconst LITERAL_SOURCES: ReadonlySet<string> = new Set([\n \"deny-list\",\n \"gazetteer\",\n]);\n\nconst shouldReplace = (a: Entity, b: Entity): boolean => {\n const aLen = a.end - a.start;\n const bLen = b.end - b.start;\n // Containment: when a literal-match entity (deny-list\n // or gazetteer) fully contains a shorter entity with\n // the same label, prefer the longer one. Curated\n // dictionary entries are more specific when longer:\n // \"656 91 Brno\" (deny-list) should beat \"656 91\"\n // (regex) even though regex has higher priority.\n // Non-literal sources (trigger, regex, NER) are\n // excluded because their length does not reliably\n // indicate accuracy.\n if (\n a.label === b.label &&\n LITERAL_SOURCES.has(a.source) &&\n a.start <= b.start &&\n a.end >= b.end &&\n aLen > bLen\n ) {\n return true;\n }\n if (\n a.label === b.label &&\n LITERAL_SOURCES.has(b.source) &&\n b.start <= a.start &&\n b.end >= a.end &&\n bLen > aLen\n ) {\n return false;\n }\n const aPri = DETECTOR_PRIORITY[a.source] ?? 0;\n const bPri = DETECTOR_PRIORITY[b.source] ?? 0;\n if (aPri !== bPri) return aPri > bPri;\n return a.score > b.score || (a.score === b.score && aLen > bLen);\n};\n\n/** Labels where colons are structurally significant. */\nconst COLON_LABELS = new Set([\"ip address\", \"mac address\"]);\n\n/** Strip leading/trailing whitespace and punctuation. */\nexport const sanitizeEntities = (entities: Entity[]): Entity[] =>\n entities.flatMap((e) => {\n const strip = COLON_LABELS.has(e.label) ? /[\\s,;]+/ : /[\\s:,;]+/;\n // Also strip leading dots followed by whitespace —\n // artifact from trigger extraction after abbreviations\n // like \"dat. nar.\" or \"č.p.\" where the extraction\n // starts at the trailing dot of the abbreviation.\n const leadTrimmed = e.text\n .replace(/^(?:\\.\\s)+/, \"\")\n .replace(new RegExp(`^${strip.source}`, strip.flags), \"\");\n const lead = e.text.length - leadTrimmed.length;\n const cleaned = leadTrimmed.replace(\n new RegExp(`${strip.source}$`, strip.flags),\n \"\",\n );\n if (cleaned.length === 0) return [];\n // Reject entities with no alphanumeric content\n if (!/[\\p{L}\\p{N}]/u.test(cleaned)) return [];\n // Collapse internal whitespace runs (address entities\n // spanning multiple lines in structured documents)\n const collapsed = cleaned.replace(/\\s*\\n\\s*/g, \" \").replace(/\\s{2,}/g, \" \");\n if (collapsed === e.text) return [e];\n return [\n {\n ...e,\n start: e.start + lead,\n end: e.start + lead + collapsed.length,\n text: collapsed,\n },\n ];\n });\n\nexport const mergeAndDedup = (...layers: Entity[][]): Entity[] => {\n const all: Entity[] = [];\n for (const layer of layers) {\n for (const entity of layer) {\n all.push(entity);\n }\n }\n if (all.length === 0) return [];\n\n const sorted = all.toSorted((a, b) => a.start - b.start);\n\n // Single-pass sweep-line: since entities are sorted\n // by start, a new entity can only overlap the tail\n // of merged[] (all earlier entries end before it).\n const first = sorted[0];\n if (!first) return [];\n const merged: Entity[] = [{ ...first }];\n\n for (let i = 1; i < sorted.length; i++) {\n const entity = sorted[i];\n const last = merged[merged.length - 1];\n if (!entity || !last) continue;\n\n if (last.end <= entity.start) {\n // No overlap: append.\n merged.push({ ...entity });\n } else if (shouldReplace(entity, last)) {\n // Overlap: new entity wins.\n merged[merged.length - 1] = { ...entity };\n }\n // else: overlap but existing wins; discard entity.\n }\n\n return sanitizeEntities(merged);\n};\n\nexport type NerInferenceFn = (\n fullText: string,\n labels: string[],\n threshold: number,\n signal?: AbortSignal,\n) => Promise<Entity[]>;\n\n/**\n * Extend monetary amount entities to include a trailing\n * \"(slovy ...)\" or \"(slovně ...)\" parenthetical that\n * spells out the amount in words. Common in Czech legal\n * documents, e.g. \"1 529,-Kč (slovy jeden-tisíc)\".\n *\n * Keywords are loaded from amount-words.json config so\n * new languages can be added without code changes.\n */\ntype AmountWordsConfig = {\n patterns: { lang: string; keywords: string[] }[];\n};\n\nconst escapeRegex = (s: string): string =>\n s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\nlet amountWordsRe: RegExp | null = null;\nlet amountWordsLoaded = false;\n\nconst getAmountWordsRe = async (): Promise<RegExp> => {\n if (amountWordsLoaded && amountWordsRe) {\n return amountWordsRe;\n }\n try {\n const mod = await import(\"./data/amount-words.json\");\n // eslint-disable-next-line no-unsafe-type-assertion -- JSON module shape\n const data = (mod as { default: AmountWordsConfig }).default;\n const keywords = data.patterns.flatMap((p) => p.keywords);\n const alt = keywords.map(escapeRegex).join(\"|\");\n amountWordsRe = new RegExp(\n `^[,;]?[^\\\\S\\\\n]*(\\\\((?:${alt})[:\\\\s][^)\\\\n]{1,120}\\\\))`,\n \"i\",\n );\n } catch {\n // Fallback: original Czech-only pattern\n amountWordsRe = /^[,;]?[^\\S\\n]*(\\((?:slovy|slovně)[:\\s][^)\\n]{1,120}\\))/i;\n }\n amountWordsLoaded = true;\n return amountWordsRe;\n};\n\nconst extendMonetaryAmountWords = (\n entities: Entity[],\n fullText: string,\n re: RegExp,\n): Entity[] =>\n entities.map((e) => {\n if (e.label !== \"monetary amount\") return e;\n const after = fullText.slice(e.end);\n const m = re.exec(after);\n if (!m) return e;\n const newEnd = e.end + m[0].length;\n return {\n ...e,\n end: newEnd,\n text: fullText.slice(e.start, newEnd),\n };\n });\n\ntype AllowedLabelSet = ReadonlySet<string> | null;\n\nconst createAllowedLabelSetFromLabels = (\n labels: readonly string[],\n): AllowedLabelSet => (labels.length > 0 ? new Set(labels) : null);\n\nconst createAllowedLabelSet = (config: PipelineConfig): AllowedLabelSet =>\n createAllowedLabelSetFromLabels(config.labels);\n\nconst filterAllowedLabels = (\n entities: Entity[],\n allowedLabels: AllowedLabelSet,\n): Entity[] => {\n if (!allowedLabels) {\n return entities;\n }\n return entities.filter((e) => allowedLabels.has(e.label));\n};\n\nconst labelIsAllowed = (\n label: string,\n allowedLabels: AllowedLabelSet,\n): boolean => !allowedLabels || allowedLabels.has(label);\n\nconst getRequestedNerLabels = (\n config: PipelineConfig,\n expandForHotwords = false,\n): readonly string[] => {\n const labels =\n config.labels.length > 0 ? config.labels : DEFAULT_ENTITY_LABELS;\n return expandForHotwords ? expandLabelsForHotwordRules(labels) : labels;\n};\n\nconst checkAbort = (signal?: AbortSignal): void => {\n if (signal?.aborted) {\n throw new DOMException(\"Pipeline aborted\", \"AbortError\");\n }\n};\n\nconst configKey = (\n config: PipelineConfig,\n gazetteerEntries: GazetteerEntry[],\n): string => {\n const legalFormsEnabled = isLegalFormsEnabled(config);\n // Gazetteer fingerprint: sorted entry IDs,\n // canonical forms, labels, and variants.\n // Skip when gazetteer is disabled to avoid\n // unnecessary cache misses.\n const gazFingerprint =\n config.enableGazetteer && gazetteerEntries.length > 0\n ? gazetteerEntries\n .map(\n (e) =>\n `${e.id}:${e.canonical}:${e.label}:${[...e.variants].sort().join(\",\")}`,\n )\n .toSorted()\n .join(\";\")\n : \"\";\n return (\n `${config.enableDenyList}:` +\n `${config.enableTriggerPhrases}:` +\n `${legalFormsEnabled}:` +\n `${config.enableNameCorpus}:` +\n `${config.denyListCountries?.toSorted().join(\",\") ?? \"\"}:` +\n `${config.denyListRegions?.toSorted().join(\",\") ?? \"\"}:` +\n `${config.denyListExcludeCategories?.toSorted().join(\",\") ?? \"\"}:` +\n `${config.enableGazetteer}:${gazFingerprint}`\n );\n};\n\n/**\n * Get or build a cached search instance. Cache state\n * lives on the provided PipelineContext, not at module\n * level.\n */\nconst getCachedSearch = async (\n config: PipelineConfig,\n gazetteerEntries: GazetteerEntry[],\n ctx: PipelineContext,\n): Promise<UnifiedSearchInstance> => {\n const key = configKey(config, gazetteerEntries);\n if (ctx.search && ctx.searchKey === key) {\n return ctx.search;\n }\n if (ctx.searchPromise && ctx.searchKey === key) {\n return ctx.searchPromise;\n }\n // Build new search. Null the cached instance first\n // so concurrent callers don't use stale data while\n // the new build is in flight.\n ctx.search = null;\n ctx.searchKey = key;\n const promise = buildUnifiedSearch(config, gazetteerEntries, ctx);\n ctx.searchPromise = promise;\n const result = await promise;\n // Guard: another call may have replaced the key\n // while we were awaiting. Only cache if still ours.\n if (ctx.searchKey === key) {\n ctx.search = result;\n }\n return result;\n};\n\n/**\n * Options for {@link runPipeline}.\n *\n * @property cachedSearch Pre-built search instance.\n * When provided, `config` and `gazetteerEntries`\n * are not used for building; the caller must\n * ensure the instance matches both parameters.\n */\nexport type PipelineOptions = {\n fullText: string;\n config: PipelineConfig;\n gazetteerEntries: GazetteerEntry[];\n nerInference?: NerInferenceFn | null;\n onProgress?: (step: string, detail: string) => void;\n cachedSearch?: UnifiedSearchInstance;\n signal?: AbortSignal;\n context?: PipelineContext;\n};\n\n/**\n * Run the full detection pipeline.\n *\n * Two TextSearch instances scan the text (regex +\n * literals). Results are dispatched to each\n * detector's post-processor by pattern index range.\n *\n * Pass an AbortSignal to cancel the pipeline between\n * stages. Throws a DOMException with name \"AbortError\"\n * when cancelled.\n *\n * Pass an optional `context` to isolate cached state\n * from other pipeline runs. If omitted, a module-level\n * default context is used (backward compatible).\n */\nexport const runPipeline = async (\n options: PipelineOptions,\n): Promise<Entity[]> => {\n const {\n fullText,\n config,\n gazetteerEntries,\n nerInference = null,\n onProgress,\n cachedSearch,\n signal,\n context,\n } = options;\n const ctx = context ?? defaultContext;\n const allowedLabels = createAllowedLabelSet(config);\n const legalFormsEnabled = isLegalFormsEnabled(config);\n\n const log = (step: string, detail: string) => {\n onProgress?.(step, detail);\n };\n\n checkAbort(signal);\n\n // Ensure generic-roles data, zone config, hotword\n // rules, and prepositions are loaded before the\n // pipeline runs. All are no-ops after the first call.\n let zoneInitOk = false;\n const enableHotwords = config.enableHotwordRules === true;\n let hotwordInitOk = false;\n const hotwordInit = enableHotwords\n ? initHotwordRules()\n .then(() => {\n hotwordInitOk = true;\n })\n .catch((err: unknown) => {\n log(\"hotwords\", \"init failed; skipping\");\n console.warn(\"[anonymize] hotword rules init failed\", err);\n })\n : Promise.resolve();\n if (config.enableZoneClassification) {\n const zoneInit = initZoneClassifier(ctx)\n .then(() => {\n zoneInitOk = true;\n })\n .catch((err: unknown) => {\n log(\"zones\", \"init failed; skipping\");\n console.warn(\"[anonymize] zone classifier init failed\", err);\n });\n await Promise.all([\n loadGenericRoles(ctx),\n initPrepositions(),\n initStreetAbbrevs(),\n zoneInit,\n hotwordInit,\n ]);\n } else {\n await Promise.all([\n loadGenericRoles(ctx),\n initPrepositions(),\n initStreetAbbrevs(),\n hotwordInit,\n ]);\n }\n\n // When a pre-built search is provided, buildDenyList\n // was skipped for this context. Ensure stopwords,\n // allow list, and person stopwords are loaded so\n // processDenyListMatches filters correctly.\n if (cachedSearch && config.enableDenyList) {\n await ensureDenyListData(ctx);\n }\n\n // Classify document zones once up front\n let zones: ZoneSpan[] = [];\n if (config.enableZoneClassification && zoneInitOk) {\n zones = classifyZones(fullText, ctx);\n if (zones.length > 0) {\n const zoneNames = [...new Set(zones.map((z) => z.zone))];\n log(\"zones\", zoneNames.join(\", \"));\n }\n }\n\n checkAbort(signal);\n\n const hotwordsActive = enableHotwords && hotwordInitOk;\n const preHotwordAllowedLabels = hotwordsActive\n ? createAllowedLabelSetFromLabels(\n expandLabelsForHotwordRules(config.labels),\n )\n : allowedLabels;\n\n const search =\n cachedSearch ?? (await getCachedSearch(config, gazetteerEntries, ctx));\n\n checkAbort(signal);\n\n // Two-pass scan (regex + literals)\n const { regexMatches, literalMatches } = runUnifiedSearch(search, fullText);\n const { slices } = search;\n\n const rawRegexEntities = config.enableRegex\n ? processRegexMatches(\n regexMatches,\n slices.regex.start,\n slices.regex.end,\n search.regexMeta,\n )\n : [];\n const regexEntities = filterAllowedLabels(\n rawRegexEntities,\n preHotwordAllowedLabels,\n );\n if (regexEntities.length > 0) log(\"regex\", `${regexEntities.length} matches`);\n\n const rawLegalFormEntities = legalFormsEnabled\n ? processLegalFormMatches(\n regexMatches,\n slices.legalForms.start,\n slices.legalForms.end,\n fullText,\n )\n : [];\n const legalFormEntities = filterAllowedLabels(\n rawLegalFormEntities,\n preHotwordAllowedLabels,\n );\n if (legalFormEntities.length > 0)\n log(\"legal-forms\", `${legalFormEntities.length} matches`);\n\n const rawTriggerEntities = config.enableTriggerPhrases\n ? processTriggerMatches(\n regexMatches,\n slices.triggers.start,\n slices.triggers.end,\n fullText,\n search.triggerRules,\n )\n : [];\n const triggerEntities = filterAllowedLabels(\n rawTriggerEntities,\n preHotwordAllowedLabels,\n );\n if (triggerEntities.length > 0)\n log(\"trigger-phrases\", `${triggerEntities.length} matches`);\n\n checkAbort(signal);\n\n let rawNameCorpusEntities: Entity[] = [];\n let nameCorpusEntities: Entity[] = [];\n if (config.enableNameCorpus && !config.enableDenyList) {\n await initNameCorpus(ctx);\n checkAbort(signal);\n rawNameCorpusEntities = detectNameCorpus(fullText, ctx);\n nameCorpusEntities = filterAllowedLabels(\n rawNameCorpusEntities,\n preHotwordAllowedLabels,\n );\n log(\"name-corpus\", `${nameCorpusEntities.length} matches`);\n }\n\n const rawDenyListEntities =\n config.enableDenyList && search.denyListData\n ? processDenyListMatches(\n literalMatches,\n slices.denyList.start,\n slices.denyList.end,\n fullText,\n search.denyListData,\n ctx,\n )\n : [];\n const denyListEntities = filterAllowedLabels(\n rawDenyListEntities,\n preHotwordAllowedLabels,\n );\n if (denyListEntities.length > 0)\n log(\"deny-list\", `${denyListEntities.length} matches`);\n\n // Gazetteer: unified into tsLiterals\n const rawGazetteerEntities =\n config.enableGazetteer && search.gazetteerData\n ? processGazetteerMatches(\n literalMatches,\n slices.gazetteer.start,\n slices.gazetteer.end,\n fullText,\n search.gazetteerData,\n )\n : [];\n const gazetteerEntities = filterAllowedLabels(\n rawGazetteerEntities,\n preHotwordAllowedLabels,\n );\n if (gazetteerEntities.length > 0)\n log(\"gazetteer\", `${gazetteerEntities.length} matches`);\n\n checkAbort(signal);\n\n const ruleContextEntities = [\n ...rawTriggerEntities,\n ...rawRegexEntities,\n ...rawLegalFormEntities,\n ...rawNameCorpusEntities,\n ...rawDenyListEntities,\n ...rawGazetteerEntities,\n ];\n\n // NER (mask rule-detected spans so the model doesn't\n // produce contradictory boundaries for known entities)\n let rawNerEntities: Entity[] = [];\n let nerEntities: Entity[] = [];\n if (config.enableNer && nerInference) {\n const maskResult = maskDetectedSpans(fullText, ruleContextEntities);\n log(\"ner\", \"running inference...\");\n const rawNer = await nerInference(\n maskResult.maskedText,\n [...getRequestedNerLabels(config, hotwordsActive)],\n config.threshold,\n signal,\n );\n rawNerEntities = unmaskNerEntities(rawNer, maskResult, fullText);\n nerEntities = filterAllowedLabels(rawNerEntities, preHotwordAllowedLabels);\n const masked = rawNer.length - rawNerEntities.length;\n const labelFiltered = rawNerEntities.length - nerEntities.length;\n log(\n \"ner\",\n `${nerEntities.length} entities` +\n (masked > 0 ? ` (${masked} masked)` : \"\") +\n (labelFiltered > 0 ? ` (${labelFiltered} label-filtered)` : \"\"),\n );\n }\n\n checkAbort(signal);\n\n // Address seed expansion\n const preAddressEntities = [\n ...triggerEntities,\n ...regexEntities,\n ...legalFormEntities,\n ...nameCorpusEntities,\n ...denyListEntities,\n ...gazetteerEntities,\n ...nerEntities,\n ];\n const addressSeedEntities = labelIsAllowed(\"address\", allowedLabels)\n ? await processAddressSeeds(\n literalMatches,\n slices.streetTypes.start,\n slices.streetTypes.end,\n fullText,\n [...ruleContextEntities, ...rawNerEntities],\n )\n : [];\n if (addressSeedEntities.length > 0)\n log(\"address-seeds\", `${addressSeedEntities.length} expanded`);\n\n checkAbort(signal);\n\n // Zone-based score adjustment: apply before\n // threshold filtering so entities in PII-dense zones\n // can cross the threshold.\n const zoneAdjusted = applyZoneAdjustments(\n [...preAddressEntities, ...addressSeedEntities],\n zones,\n );\n\n // Hotword context rules: boost or reclassify\n // entities near relevant keywords. Applied after\n // zone adjustments so both effects stack.\n const preBoostEntities = hotwordsActive\n ? filterAllowedLabels(\n applyHotwordRules(zoneAdjusted, fullText),\n allowedLabels,\n )\n : zoneAdjusted;\n\n // Confidence boost + threshold filter\n let allEntities: Entity[];\n if (config.enableConfidenceBoost) {\n allEntities = boostNearMissEntities(preBoostEntities, config.threshold);\n const boosted =\n allEntities.length -\n preBoostEntities.filter((e) => e.score >= config.threshold).length;\n if (boosted > 0) log(\"confidence-boost\", `${boosted} near-miss promoted`);\n } else {\n allEntities = preBoostEntities.filter((e) => e.score >= config.threshold);\n }\n\n // Street patterns near existing addresses\n // (e.g. \"Ostrovni 225/1\" near \"110 00 Praha 1\")\n const streetPatterns = labelIsAllowed(\"address\", allowedLabels)\n ? detectStreetPatternsNearAddresses(fullText, allEntities)\n : [];\n if (streetPatterns.length > 0) {\n allEntities = [...allEntities, ...streetPatterns];\n log(\n \"street-context\",\n `${streetPatterns.length} street patterns near addresses`,\n );\n }\n\n // Orphan street lines in header zone\n const orphanStreets = labelIsAllowed(\"address\", allowedLabels)\n ? detectOrphanStreetLines(fullText, allEntities)\n : [];\n if (orphanStreets.length > 0) {\n allEntities = [...allEntities, ...orphanStreets];\n log(\"orphan-streets\", `${orphanStreets.length} header street lines`);\n }\n\n // Merge + dedup\n const rawMerged = mergeAndDedup(allEntities);\n log(\"merge\", `${rawMerged.length} after dedup`);\n\n // Extend monetary amounts to include trailing\n // \"(slovy ...)\" or \"(slovně ...)\" parentheticals.\n // Runs after dedup so each monetary span is unique,\n // preventing duplicate extensions from clobbering\n // unrelated entities between e.end and newEnd.\n const amountWordsRe = await getAmountWordsRe();\n const mergedExtended = extendMonetaryAmountWords(\n rawMerged,\n fullText,\n amountWordsRe,\n );\n\n // Boundary consistency (merge adjacent, fix partial\n // words, remove nested same-label)\n const consistent = enforceBoundaryConsistency(mergedExtended, fullText);\n if (consistent.length < mergedExtended.length)\n log(\n \"boundary\",\n `${mergedExtended.length - consistent.length} consolidated`,\n );\n\n // Organization name propagation: strip legal form\n // suffixes from detected orgs and re-scan for bare\n // mentions of the base name. Gated by enableCoreference\n // since this is a coreference-like pass. Propagated\n // entities are filtered by the configured threshold to\n // ensure they respect the caller's confidence floor.\n let postOrgEntities = consistent;\n if (\n config.enableCoreference &&\n labelIsAllowed(\"organization\", allowedLabels)\n ) {\n const orgPropagated = propagateOrgNames(consistent, fullText);\n const thresholded = orgPropagated.filter(\n (e) => e.score >= config.threshold,\n );\n if (thresholded.length > 0) {\n postOrgEntities = mergeAndDedup(consistent, thresholded);\n log(\"org-propagation\", `${thresholded.length} base names`);\n }\n }\n\n // False-positive filtering\n const merged = filterFalsePositives(postOrgEntities, ctx, fullText);\n if (merged.length < postOrgEntities.length)\n log(\"filter\", `removed ${postOrgEntities.length - merged.length} FPs`);\n\n checkAbort(signal);\n\n // Coreference\n // Clear stale entries unconditionally so a reused\n // context doesn't leak sourceText across documents.\n ctx.corefSourceMap.clear();\n if (config.enableCoreference) {\n const terms = await extractDefinedTerms(fullText, merged, ctx);\n if (terms.length > 0) {\n log(\"coreference\", `${terms.length} defined terms`);\n const corefSpans = findCoreferenceSpans(fullText, terms, ctx);\n if (corefSpans.length > 0) {\n log(\"coreference-rescan\", `${corefSpans.length} aliases`);\n const corefMerged = mergeAndDedup(merged, corefSpans);\n const corefConsistent = enforceBoundaryConsistency(\n corefMerged,\n fullText,\n );\n return sanitizeEntities(\n filterAllowedLabels(\n filterFalsePositives(corefConsistent, ctx, fullText),\n allowedLabels,\n ),\n );\n }\n }\n }\n\n // Re-sanitize: enforceBoundaryConsistency may adjust\n // entity boundaries after mergeAndDedup's sanitization,\n // potentially re-introducing whitespace or punctuation.\n return sanitizeEntities(filterAllowedLabels(merged, allowedLabels));\n};\n","import type {\n AnonymisationOperator,\n OperatorConfig,\n OperatorType,\n} from \"./types\";\n\n// ── Operator registry ──────────────────────────────────\n\nconst replaceOperator: AnonymisationOperator = {\n type: \"replace\",\n reversibility: \"reversible\",\n apply: (_text, _label, placeholder) => placeholder,\n};\n\nconst redactOperator: AnonymisationOperator = {\n type: \"redact\",\n reversibility: \"irreversible\",\n apply: (_text, _label, _placeholder, redactString) => redactString,\n};\n\nexport const OPERATOR_REGISTRY = {\n replace: replaceOperator,\n redact: redactOperator,\n} as const satisfies Record<OperatorType, AnonymisationOperator>;\n\nconst DEFAULT_REDACT_STRING = \"[REDACTED]\";\n\n/**\n * Default operator config: replace for all labels.\n * Preserves existing pipeline behaviour.\n */\nexport const DEFAULT_OPERATOR_CONFIG: OperatorConfig = {\n operators: {},\n redactString: DEFAULT_REDACT_STRING,\n};\n\n/**\n * Resolve the operator for a label, falling back to \"replace\".\n */\nexport const resolveOperator = (\n config: OperatorConfig,\n label: string,\n): OperatorType => config.operators[label] ?? \"replace\";\n","import {\n DEFAULT_OPERATOR_CONFIG,\n OPERATOR_REGISTRY,\n resolveOperator,\n} from \"./operators\";\nimport type {\n Entity,\n OperatorConfig,\n OperatorType,\n RedactionResult,\n} from \"./types\";\nimport type { PipelineContext } from \"./context\";\nimport { corefKey, defaultContext } from \"./context\";\n\nconst WHITESPACE_RE = /\\s+/g;\nconst PHONE_NOISE_RE = /[()\\s-]/g;\nconst SPACE_DASH_RE = /[\\s-]/g;\n\n/**\n * Normalize entity text so that surface-form variations\n * of the same real-world value map to a single canonical\n * key. Lowercased emails, stripped phone formatting, etc.\n */\nconst normalizeEntityText = (label: string, text: string): string => {\n const upper = label.toUpperCase().replace(WHITESPACE_RE, \"_\");\n\n if (upper === \"EMAIL_ADDRESS\" || upper === \"EMAIL\") {\n return text.toLowerCase().trim();\n }\n if (upper === \"PHONE_NUMBER\" || upper === \"PHONE\") {\n return text.replace(PHONE_NOISE_RE, \"\");\n }\n if (\n upper === \"IBAN\" ||\n upper === \"BANK_ACCOUNT_NUMBER\" ||\n upper === \"TAX_IDENTIFICATION_NUMBER\" ||\n upper === \"REGISTRATION_NUMBER\"\n ) {\n return text.replace(SPACE_DASH_RE, \"\").toUpperCase();\n }\n if (\n upper === \"PERSON\" ||\n upper === \"ORGANIZATION\" ||\n upper === \"ADDRESS\" ||\n upper === \"LAND_PARCEL\"\n ) {\n return text.replace(WHITESPACE_RE, \" \").toLowerCase().trim();\n }\n return text.trim();\n};\n\n/**\n * Build a stable mapping from entity text to numbered\n * placeholders. Same real-world value always maps to the\n * same placeholder (e.g., \"Dr. Muller\" and \"Dr. Muller\"\n * both become [PERSON_1]).\n *\n * Placeholder format: [LABEL_N] where LABEL is uppercase\n * and N is a 1-based counter per label.\n *\n * @param ctx Pipeline context. Must be the same instance\n * passed to `runPipeline` (or `findCoreferenceSpans`)\n * so coreference placeholder links are preserved.\n * Defaults to `defaultContext` for single-tenant usage.\n */\nexport const buildPlaceholderMap = (\n entities: Entity[],\n ctx: PipelineContext = defaultContext,\n): Map<string, string> => {\n const counters = new Map<string, number>();\n const textLabelToPlaceholder = new Map<string, string>();\n const normalizedToPlaceholder = new Map<string, string>();\n\n const sorted = entities.toSorted((a, b) => a.start - b.start);\n\n for (const entity of sorted) {\n const compositeKey = `${entity.label}\\0${entity.text}`;\n if (textLabelToPlaceholder.has(compositeKey)) {\n continue;\n }\n\n const labelKey = entity.label.toUpperCase().replace(WHITESPACE_RE, \"_\");\n\n // Coreference side-channel: if this entity is a\n // coref alias, look up the source entity's\n // placeholder so both get the same number.\n const sourceText = ctx.corefSourceMap.get(corefKey(entity));\n if (sourceText !== undefined) {\n const sourceNormalized = normalizeEntityText(entity.label, sourceText);\n const sourceNormalizedKey = `${labelKey}\\0${sourceNormalized}`;\n const sourceExisting = normalizedToPlaceholder.get(sourceNormalizedKey);\n if (sourceExisting) {\n textLabelToPlaceholder.set(compositeKey, sourceExisting);\n continue;\n }\n }\n\n const normalized = normalizeEntityText(entity.label, entity.text);\n const normalizedKey = `${labelKey}\\0${normalized}`;\n const existing = normalizedToPlaceholder.get(normalizedKey);\n if (existing) {\n textLabelToPlaceholder.set(compositeKey, existing);\n continue;\n }\n\n const count = (counters.get(labelKey) ?? 0) + 1;\n counters.set(labelKey, count);\n\n const placeholder = `[${labelKey}_${count}]`;\n textLabelToPlaceholder.set(compositeKey, placeholder);\n normalizedToPlaceholder.set(normalizedKey, placeholder);\n }\n\n return textLabelToPlaceholder;\n};\n\n/**\n * Apply redactions to the source text, replacing each\n * confirmed entity span using the configured operator.\n *\n * Co-references are consistent: if the same text appears\n * multiple times, all occurrences get the same placeholder.\n *\n * @param ctx Pipeline context. Must be the same instance\n * passed to `runPipeline` (or `findCoreferenceSpans`)\n * so coreference placeholder links are preserved.\n * Defaults to `defaultContext` for single-tenant usage.\n */\nexport const redactText = (\n fullText: string,\n entities: Entity[],\n config: OperatorConfig = DEFAULT_OPERATOR_CONFIG,\n ctx: PipelineContext = defaultContext,\n): RedactionResult => {\n if (entities.length === 0) {\n return {\n redactedText: fullText,\n redactionMap: new Map(),\n operatorMap: new Map(),\n entityCount: 0,\n };\n }\n\n const placeholderMap = buildPlaceholderMap(entities, ctx);\n\n const sorted = entities.toSorted((a, b) => a.start - b.start);\n\n // Remove overlapping spans (keep first occurrence)\n const nonOverlapping: Entity[] = [];\n let lastEnd = 0;\n for (const entity of sorted) {\n if (entity.start >= lastEnd) {\n nonOverlapping.push(entity);\n lastEnd = entity.end;\n }\n }\n\n const parts: string[] = [];\n const redactionMap = new Map<string, string>();\n const operatorMap = new Map<string, OperatorType>();\n let cursor = 0;\n\n for (const entity of nonOverlapping) {\n if (entity.start > cursor) {\n parts.push(fullText.slice(cursor, entity.start));\n }\n\n const placeholder =\n placeholderMap.get(`${entity.label}\\0${entity.text}`) ??\n `[${entity.label.toUpperCase().replace(/\\s+/g, \"_\")}]`;\n\n const opType = resolveOperator(config, entity.label);\n const operator = OPERATOR_REGISTRY[opType];\n\n const replacement = operator.apply(\n entity.text,\n entity.label,\n placeholder,\n config.redactString,\n );\n\n parts.push(replacement);\n // operatorMap is keyed by the conceptual placeholder\n // ([LABEL_N]), not by the replacement text. For \"redact\"\n // operators the placeholder never appears in the output;\n // the map is only consulted via exportRedactionKey which\n // iterates redactionMap (replace entries only).\n operatorMap.set(placeholder, opType);\n\n // Only populate redactionMap for reversible operators\n if (\n operator.reversibility === \"reversible\" &&\n !redactionMap.has(placeholder)\n ) {\n redactionMap.set(placeholder, entity.text);\n }\n\n cursor = entity.end;\n }\n\n if (cursor < fullText.length) {\n parts.push(fullText.slice(cursor));\n }\n\n return {\n redactedText: parts.join(\"\"),\n redactionMap,\n operatorMap,\n entityCount: nonOverlapping.length,\n };\n};\n\n/**\n * Serialize the redaction key to JSON for export.\n * Includes operator metadata so the export is self-describing.\n */\nexport const exportRedactionKey = (\n redactionMap: Map<string, string>,\n operatorMap: Map<string, OperatorType>,\n): string => {\n const entries: Record<string, { original: string; operator: OperatorType }> =\n {};\n\n for (const [placeholder, value] of redactionMap) {\n entries[placeholder] = {\n original: value,\n operator: operatorMap.get(placeholder) ?? \"replace\",\n };\n }\n\n return JSON.stringify({ entries }, null, 2);\n};\n\n/**\n * De-anonymise text using a redaction key.\n * Replaces placeholders back with original values.\n * Only works for reversible operators (replace).\n */\nexport const deanonymise = (\n redactedText: string,\n redactionMap: Map<string, string>,\n): string => {\n let result = redactedText;\n\n for (const [placeholder, original] of redactionMap) {\n result = result.replaceAll(placeholder, original);\n }\n\n return result;\n};\n","/**\n * Decode ONNX model output into entity spans.\n *\n * The span-level model outputs a logits tensor of shape\n * [batch, inputLength, maxWidth, numEntities]. This module\n * applies sigmoid, thresholds, and greedy non-overlapping\n * selection to produce the final entity list.\n */\nimport type { RawInferenceResult } from \"./types\";\n\ntype Span = [string, number, number, string, number];\n\nconst sigmoid = (x: number): number => 1 / (1 + Math.exp(-x));\n\n/** Check if two spans overlap (optionally allowing multi-label). */\nconst hasOverlapping = (\n a: number[],\n b: number[],\n multiLabel: boolean,\n): boolean => {\n const a0 = a[0] ?? 0;\n const a1 = a[1] ?? 0;\n const b0 = b[0] ?? 0;\n const b1 = b[1] ?? 0;\n if (a0 === b0 && a1 === b1) {\n return !multiLabel;\n }\n return !(a0 > b1 || b0 > a1);\n};\n\n/**\n * Greedy non-overlapping span selection.\n * Sorts by score descending, keeps each span only if it\n * doesn't overlap with already-selected spans.\n */\nconst greedySearch = (\n spans: Span[],\n flatNer: boolean,\n multiLabel: boolean,\n): Span[] => {\n const sorted = spans.toSorted((a, b) => b[4] - a[4]);\n const selected: Span[] = [];\n\n for (const span of sorted) {\n const overlaps = selected.some((s) => {\n if (flatNer) {\n return hasOverlapping([span[1], span[2]], [s[1], s[2]], multiLabel);\n }\n // Non-flat: also allow nested spans\n const isNested =\n (span[1] <= s[1] && span[2] >= s[2]) ||\n (s[1] <= span[1] && s[2] >= span[2]);\n if (isNested) {\n return false;\n }\n return hasOverlapping([span[1], span[2]], [s[1], s[2]], multiLabel);\n });\n\n if (!overlaps) {\n selected.push(span);\n }\n }\n\n return selected.toSorted((a, b) => a[1] - b[1]);\n};\n\n/**\n * Decode span-level model logits into entity results.\n */\nexport const decodeSpans = (\n batchSize: number,\n inputLength: number,\n maxWidth: number,\n numEntities: number,\n texts: string[],\n batchIds: number[],\n batchWordsStartIdx: number[][],\n batchWordsEndIdx: number[][],\n idToClass: Record<number, string>,\n modelOutput: ArrayLike<number>,\n flatNer: boolean,\n threshold: number,\n multiLabel: boolean,\n): RawInferenceResult => {\n const spans: Span[][] = Array.from({ length: batchSize }, () => []);\n\n const batchPadding = inputLength * maxWidth * numEntities;\n const startTokenPadding = maxWidth * numEntities;\n const endTokenPadding = numEntities;\n\n for (let id = 0; id < modelOutput.length; id++) {\n const batch = Math.floor(id / batchPadding);\n const startToken = Math.floor(id / startTokenPadding) % inputLength;\n const endToken = startToken + (Math.floor(id / endTokenPadding) % maxWidth);\n const entity = id % numEntities;\n\n const prob = sigmoid(modelOutput[id] ?? 0);\n\n const batchStarts = batchWordsStartIdx[batch];\n const batchEnds = batchWordsEndIdx[batch];\n const batchSpans = spans[batch];\n if (!batchStarts || !batchEnds || !batchSpans) {\n continue;\n }\n\n if (\n prob >= threshold &&\n startToken < batchStarts.length &&\n endToken < batchEnds.length\n ) {\n const globalBatch = batchIds[batch] ?? 0;\n const startIdx = batchStarts[startToken] ?? 0;\n const endIdx = batchEnds[endToken] ?? 0;\n const spanText = (texts[globalBatch] ?? \"\").slice(startIdx, endIdx);\n batchSpans.push([\n spanText,\n startIdx,\n endIdx,\n idToClass[entity + 1] ?? \"\",\n prob,\n ]);\n }\n }\n\n return spans.map((batchSpans) =>\n greedySearch(batchSpans, flatNer, multiLabel),\n );\n};\n","/**\n * Token-level BIO decoder for GLiNER TokenGLiNER models\n * (e.g., gliner-pii-edge-v1.0).\n *\n * These models output logits of shape [B, L, C, 3] where:\n * B = batch size\n * L = number of words (text_lengths)\n * C = number of entity classes\n * 3 = BIO tags: [B(egin), I(nside), O(utside)]\n *\n * This decoder converts BIO-tagged logits into entity spans\n * with character offsets.\n */\nimport type { RawInferenceResult } from \"./types\";\n\ntype Span = [string, number, number, string, number];\n\nconst sigmoid = (x: number): number => 1 / (1 + Math.exp(-x));\n\nconst B_TAG = 0;\nconst I_TAG = 1;\n\n/**\n * Decode token-level BIO logits into entity spans.\n *\n * For each word, checks if the B(egin) logit for any class\n * exceeds the threshold. If so, extends the span by consuming\n * subsequent I(nside) tokens of the same class.\n */\nexport const decodeTokenSpans = (\n batchSize: number,\n numWords: number,\n numEntities: number,\n texts: string[],\n batchIds: number[],\n batchWordsStartIdx: number[][],\n batchWordsEndIdx: number[][],\n idToClass: Record<number, string>,\n modelOutput: ArrayLike<number>,\n threshold: number,\n): RawInferenceResult => {\n const results: Span[][] = Array.from({ length: batchSize }, () => []);\n\n const wordStride = numEntities * 3;\n const batchStride = numWords * wordStride;\n\n for (let b = 0; b < batchSize; b++) {\n const batchOffset = b * batchStride;\n const starts = batchWordsStartIdx[b];\n const ends = batchWordsEndIdx[b];\n const globalBatch = batchIds[b] ?? 0;\n const text = texts[globalBatch] ?? \"\";\n const batchSpans = results[b];\n\n if (!starts || !ends || !batchSpans) {\n continue;\n }\n\n const actualWords = starts.length;\n\n for (let e = 0; e < numEntities; e++) {\n let w = 0;\n\n while (w < actualWords) {\n const bLogitIdx = batchOffset + w * wordStride + e * 3 + B_TAG;\n const bScore = sigmoid(modelOutput[bLogitIdx] ?? 0);\n\n if (bScore < threshold) {\n w++;\n continue;\n }\n\n const spanStart = w;\n let spanEnd = w;\n let maxScore = bScore;\n\n while (spanEnd + 1 < actualWords) {\n const iLogitIdx =\n batchOffset + (spanEnd + 1) * wordStride + e * 3 + I_TAG;\n const iScore = sigmoid(modelOutput[iLogitIdx] ?? 0);\n\n if (iScore < threshold) {\n break;\n }\n\n spanEnd++;\n maxScore = Math.max(maxScore, iScore);\n }\n\n const charStart = starts[spanStart] ?? 0;\n const charEnd = ends[spanEnd] ?? 0;\n const spanText = text.slice(charStart, charEnd);\n const label = idToClass[e + 1] ?? \"\";\n\n if (spanText.trim().length > 0 && label) {\n batchSpans.push([spanText, charStart, charEnd, label, maxScore]);\n }\n\n w = spanEnd + 1;\n }\n }\n\n const selected: Span[] = [];\n for (const span of batchSpans.toSorted((x, y) => y[4] - x[4])) {\n const overlaps = selected.some((s) => span[1] < s[2] && span[2] > s[1]);\n if (!overlaps) {\n selected.push(span);\n }\n }\n\n results[b] = selected.toSorted((x, y) => x[1] - y[1]);\n }\n\n return results;\n};\n","/**\n * Tokenization and input preparation for GLiNER span model.\n *\n * Splits text into word tokens, encodes via HuggingFace\n * tokenizer, and builds the span index grid that the ONNX\n * model expects as input.\n */\nimport type { Encoding, Tokenizer } from \"@huggingface/tokenizers\";\n\nconst segmenter = new Intl.Segmenter(undefined, {\n granularity: \"word\",\n});\n\n/** Tokenize text into words with character offsets. */\nexport const tokenizeText = (\n text: string,\n): [words: string[], starts: number[], ends: number[]] => {\n const words: string[] = [];\n const starts: number[] = [];\n const ends: number[] = [];\n\n for (const { segment, index, isWordLike } of segmenter.segment(text)) {\n // Include words and numeric segments (Intl.Segmenter\n // marks pure digit sequences as non-word-like, but the\n // NER model needs them for entity detection).\n if (!isWordLike && !/\\d/u.test(segment)) {\n continue;\n }\n words.push(segment);\n starts.push(index);\n ends.push(index + segment.length);\n }\n\n return [words, starts, ends];\n};\n\n/** Build entity label <-> id mappings. */\nconst createMappings = (\n labels: string[],\n): {\n classToId: Record<string, number>;\n idToClass: Record<number, string>;\n} => {\n const classToId: Record<string, number> = {};\n const idToClass: Record<number, string> = {};\n for (let i = 0; i < labels.length; i++) {\n const label = labels[i];\n if (label === undefined) {\n continue;\n }\n const id = i + 1;\n classToId[label] = id;\n idToClass[id] = label;\n }\n return { classToId, idToClass };\n};\n\n/** Prepend entity prompt tokens to word tokens. */\nconst prepareTextInputs = (\n batchTokens: string[][],\n entities: string[],\n): [inputTexts: string[][], textLengths: number[], promptLengths: number[]] => {\n const inputTexts: string[][] = [];\n const promptLengths: number[] = [];\n const textLengths: number[] = [];\n\n for (const tokens of batchTokens) {\n textLengths.push(tokens.length);\n\n const prompt: string[] = [];\n for (const ent of entities) {\n prompt.push(\"<<ENT>>\");\n prompt.push(ent);\n }\n prompt.push(\"<<SEP>>\");\n\n promptLengths.push(prompt.length);\n inputTexts.push([...prompt, ...tokens]);\n }\n\n return [inputTexts, textLengths, promptLengths];\n};\n\n/** Encode word sequences into token IDs with masks. */\nconst encodeInputs = (\n tokenizer: Tokenizer,\n texts: string[][],\n promptLengths: number[],\n): [\n inputIds: number[][],\n attentionMasks: number[][],\n wordsMasks: number[][],\n] => {\n // Resolve special token IDs dynamically instead of\n // hardcoding DeBERTa-v3 values. Fallbacks (1, 2) match\n // the current model but future models may differ.\n const clsTokenId = tokenizer.token_to_id(\"[CLS]\") ?? 1;\n const sepTokenId = tokenizer.token_to_id(\"[SEP]\") ?? 2;\n\n const allInputIds: number[][] = [];\n const allAttentionMasks: number[][] = [];\n const allWordsMasks: number[][] = [];\n\n for (let idx = 0; idx < texts.length; idx++) {\n const promptLength = promptLengths[idx] ?? 0;\n const words = texts[idx];\n if (!words) {\n continue;\n }\n const wordsMask: number[] = [0];\n const inputIds: number[] = [clsTokenId];\n const attentionMask: number[] = [1];\n\n let wordCounter = 1;\n for (let wordId = 0; wordId < words.length; wordId++) {\n const word = words[wordId];\n if (word === undefined) {\n continue;\n }\n // encode() returns { ids, tokens, attention_mask }\n // with BOS/EOS; strip them with slice(1, -1)\n const encoded: Encoding = tokenizer.encode(word);\n const wordTokens: number[] = encoded.ids.slice(1, -1);\n\n for (let tokenId = 0; tokenId < wordTokens.length; tokenId++) {\n attentionMask.push(1);\n if (wordId < promptLength) {\n wordsMask.push(0);\n } else if (tokenId === 0) {\n wordsMask.push(wordCounter);\n wordCounter++;\n } else {\n wordsMask.push(0);\n }\n inputIds.push(wordTokens[tokenId] ?? 0);\n }\n }\n\n inputIds.push(sepTokenId);\n wordsMask.push(0);\n attentionMask.push(1);\n\n allInputIds.push(inputIds);\n allAttentionMasks.push(attentionMask);\n allWordsMasks.push(wordsMask);\n }\n\n return [allInputIds, allAttentionMasks, allWordsMasks];\n};\n\n/** Build span index pairs and masks for the model. */\nconst prepareSpans = (\n batchTokens: string[][],\n maxWidth: number,\n): { spanIdxs: number[][][]; spanMasks: boolean[][] } => {\n const spanIdxs: number[][][] = [];\n const spanMasks: boolean[][] = [];\n\n for (const tokens of batchTokens) {\n const len = tokens.length;\n const idx: number[][] = [];\n const mask: boolean[] = [];\n\n for (let i = 0; i < len; i++) {\n for (let j = 0; j < maxWidth; j++) {\n const endIdx = Math.min(i + j, len - 1);\n idx.push([i, endIdx]);\n mask.push(endIdx < len);\n }\n }\n\n spanIdxs.push(idx);\n spanMasks.push(mask);\n }\n\n return { spanIdxs, spanMasks };\n};\n\n/** Pad a 2D or 3D array to uniform inner length. */\nexport const padArray = <T>(arr: T[][], dimensions: number = 2): T[][] => {\n if (arr.length === 0) {\n return [];\n }\n const maxLength = Math.max(...arr.map((sub) => sub.length));\n // For 3D arrays, infer inner dimension from the first\n // non-empty element (arr[0] may be empty when a batch\n // element had zero word tokens).\n let finalDim = 0;\n if (dimensions === 3) {\n for (const sub of arr) {\n if (sub.length > 0) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- 3D arrays have number[] inner elements\n const inner = sub[0] as unknown as number[];\n finalDim = inner.length;\n break;\n }\n }\n }\n\n return arr.map((sub) => {\n const padCount = maxLength - sub.length;\n const fill =\n dimensions === 3\n ? Array.from({ length: padCount }, () =>\n Array.from<number>({ length: finalDim }).fill(0),\n )\n : Array.from<number>({ length: padCount }).fill(0);\n // SAFETY: fill values (zero arrays) match the shape of T\n // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- generic padding for ONNX tensor arrays\n return [...sub, ...(fill as T[])] as T[];\n });\n};\n\n/** Prepare a complete batch for ONNX inference. */\nexport const prepareBatch = (\n tokenizer: Tokenizer,\n texts: string[],\n entities: string[],\n maxWidth: number,\n): {\n inputsIds: number[][];\n attentionMasks: number[][];\n wordsMasks: number[][];\n textLengths: number[];\n spanIdxs: number[][][];\n spanMasks: boolean[][];\n idToClass: Record<number, string>;\n batchTokens: string[][];\n batchWordsStartIdx: number[][];\n batchWordsEndIdx: number[][];\n} => {\n const batchTokens: string[][] = [];\n const batchWordsStartIdx: number[][] = [];\n const batchWordsEndIdx: number[][] = [];\n\n for (const text of texts) {\n const [words, starts, ends] = tokenizeText(text);\n batchTokens.push(words);\n batchWordsStartIdx.push(starts);\n batchWordsEndIdx.push(ends);\n }\n\n const { idToClass } = createMappings(entities);\n\n const [inputTokens, textLengths, promptLengths] = prepareTextInputs(\n batchTokens,\n entities,\n );\n\n let [inputsIds, attentionMasks, wordsMasks] = encodeInputs(\n tokenizer,\n inputTokens,\n promptLengths,\n );\n\n inputsIds = padArray(inputsIds);\n attentionMasks = padArray(attentionMasks);\n wordsMasks = padArray(wordsMasks);\n\n let { spanIdxs, spanMasks } = prepareSpans(batchTokens, maxWidth);\n\n spanIdxs = padArray(spanIdxs, 3);\n spanMasks = padArray(spanMasks);\n\n return {\n inputsIds,\n attentionMasks,\n wordsMasks,\n textLengths,\n spanIdxs,\n spanMasks,\n idToClass,\n batchTokens,\n batchWordsStartIdx,\n batchWordsEndIdx,\n };\n};\n","import type { Entity } from \"../types\";\n\nconst MAX_CHUNK_CHARS = 1500;\nconst OVERLAP_CHARS = 50;\nconst MIN_CHUNK_LENGTH = 10;\n\n/**\n * Split text into overlapping chunks for GLiNER's\n * ~512 token context window. Character-based splitting\n * (rough approximation of token limits).\n *\n * Tries to break at sentence boundaries when possible.\n */\nexport const chunkText = (text: string): string[] => {\n const chunks: string[] = [];\n let offset = 0;\n\n while (offset < text.length) {\n let end = Math.min(offset + MAX_CHUNK_CHARS, text.length);\n\n if (end < text.length) {\n const slice = text.slice(offset, end);\n const lastPeriod = slice.lastIndexOf(\". \");\n if (lastPeriod > MAX_CHUNK_CHARS * 0.5) {\n end = offset + lastPeriod + 2;\n }\n }\n\n const chunk = text.slice(offset, end);\n if (chunk.trim().length > MIN_CHUNK_LENGTH) {\n chunks.push(chunk);\n }\n offset = Math.max(offset + 1, end - OVERLAP_CHARS);\n }\n\n return chunks;\n};\n\n/**\n * Compute the byte offset of each chunk within the\n * original document text.\n */\nexport const computeChunkOffsets = (\n fullText: string,\n chunks: string[],\n): number[] => {\n const offsets: number[] = [];\n let searchFrom = 0;\n\n for (const chunk of chunks) {\n const idx = fullText.indexOf(chunk, searchFrom);\n offsets.push(idx !== -1 ? idx : searchFrom);\n searchFrom =\n idx !== -1 ? idx + Math.max(1, chunk.length - OVERLAP_CHARS) : searchFrom;\n }\n\n return offsets;\n};\n\nconst POSITION_THRESHOLD = 5;\n\n/**\n * Merge entities from overlapping chunks back to\n * document-level offsets. Deduplicates entities that\n * appear in overlap regions (keeps highest score).\n *\n * Dedup invariant: each incoming entity is compared\n * against the highest-scored same-label near-dup in\n * its proximity window. If it loses, it is dropped.\n * This does NOT guarantee that all pairwise near-dup\n * relationships in the output are resolved; a lower-\n * scored entity can survive if the bridging entity\n * that would have replaced it was itself dropped by\n * a higher-scored match.\n *\n * Uses a reverse-scan over the sorted merged array\n * so each entity only compares against nearby\n * predecessors — O(n * w) average where w is the max\n * entities per POSITION_THRESHOLD window, O(n²) worst\n * case when replacements dominate (splice is O(n)).\n */\nexport const mergeChunkEntities = (\n chunkOffsets: number[],\n chunkResults: Entity[][],\n): Entity[] => {\n const allEntities: Entity[] = [];\n\n for (let i = 0; i < chunkResults.length; i++) {\n const offset = chunkOffsets[i] ?? 0;\n const entities = chunkResults[i];\n if (!entities) {\n continue;\n }\n for (const entity of entities) {\n allEntities.push({\n ...entity,\n start: entity.start + offset,\n end: entity.end + offset,\n });\n }\n }\n\n const sorted = allEntities.toSorted((a, b) => a.start - b.start);\n const merged: Entity[] = [];\n\n for (const entity of sorted) {\n let bestDupIndex = -1;\n let bestDupScore = -1;\n\n // Reverse-scan the full proximity window. Collect\n // the highest-scored same-label match so we dedup\n // against the strongest existing entity, not just\n // the nearest one.\n for (let j = merged.length - 1; j >= 0; j--) {\n const existing = merged[j];\n if (existing === undefined) {\n continue;\n }\n // merged is kept sorted by start (splice+push\n // maintains this); elements further back have\n // even smaller starts, so we can break early.\n if (entity.start - existing.start >= POSITION_THRESHOLD) {\n break;\n }\n if (\n existing.label === entity.label &&\n Math.abs(existing.end - entity.end) < POSITION_THRESHOLD &&\n existing.score > bestDupScore\n ) {\n bestDupIndex = j;\n bestDupScore = existing.score;\n }\n }\n\n if (bestDupIndex !== -1) {\n const existing = merged[bestDupIndex];\n if (existing !== undefined && entity.score > existing.score) {\n // Replace with winner. Splice out the old entry\n // and re-insert at the end to maintain sorted\n // order (entity.start >= all prior starts).\n merged.splice(bestDupIndex, 1);\n merged.push({ ...entity });\n }\n // Entity loses to the best match and is dropped.\n // Other lower-scored near-dups of this entity are\n // not revisited (see docstring dedup invariant).\n } else {\n merged.push({ ...entity });\n }\n }\n\n return merged;\n};\n","/**\n * Compute the Levenshtein edit distance between two\n * strings. O(n*m) time, O(min(n,m)) space using a\n * single-row DP approach.\n */\nexport const levenshtein = (rawA: string, rawB: string): number => {\n if (rawA === rawB) {\n return 0;\n }\n if (rawA.length === 0) {\n return rawB.length;\n }\n if (rawB.length === 0) {\n return rawA.length;\n }\n\n const [shorter, longer] =\n rawA.length <= rawB.length ? [rawA, rawB] : [rawB, rawA];\n\n const aLen = shorter.length;\n const bLen = longer.length;\n const row = new Uint16Array(aLen + 1);\n\n for (let i = 0; i <= aLen; i++) {\n row[i] = i;\n }\n\n for (let j = 1; j <= bLen; j++) {\n let prev = row[0] ?? 0;\n row[0] = j;\n\n for (let i = 1; i <= aLen; i++) {\n const cost = shorter[i - 1] === longer[j - 1] ? 0 : 1;\n const temp = row[i] ?? 0;\n row[i] = Math.min((row[i] ?? 0) + 1, (row[i - 1] ?? 0) + 1, prev + cost);\n prev = temp;\n }\n }\n\n return row[aLen] ?? 0;\n};\n","/* Browser/WASM entry point — loads TextSearch\n * from @stll/text-search-wasm and re-exports\n * the full anonymize API. */\n\nimport { TextSearch } from \"@stll/text-search-wasm\";\n\nimport { initTextSearch } from \"./search-engine\";\n\ninitTextSearch(TextSearch);\n\nexport * from \"./index-shared\";\n"],"mappings":"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OAAO;IACP,QAAQ,kBAAkB;IAC3B,CAAC;;;AAIN,QAAO;;;;;;;;ACzwBT,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;EACD;CACD,UAAU;EACR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD;CACD,aAAa;EACX;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD;CACD,MAAM;EACJ;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD;CACD,kBAAkB;EAChB;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD;CACD,IAAI;EACF;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD;CACD,MAAM;EAAC;EAAM;EAAM;EAAK;CACxB,SAAS;EAAC;EAAM;EAAM;EAAM;EAAM;EAAK;CACvC,KAAK;EAAC;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAK;CACvE,aAAa;EAAC;EAAM;EAAM;EAAM;EAAM;EAAM;EAAK;CACjD,SAAS;EAAC;EAAM;EAAM;EAAK;CAC3B,YAAY;EAAC;EAAM;EAAM;EAAM;EAAM;EAAM;EAAK;CAChD,WAAW;EAAC;EAAM;E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"}