eyecite-ts 0.15.6 → 0.15.8

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["getReportersSync","findAbbreviatedCode","findNamedCode","buildAbbreviatedCodeRegex","getReportersSync"],"sources":["../src/types/guards.ts","../src/types/span.ts","../src/clean/cleaners.ts","../src/clean/segmentMap.ts","../src/clean/cleanText.ts","../src/extract/unionFind.ts","../src/footnotes/htmlDetector.ts","../src/footnotes/textDetector.ts","../src/footnotes/detectFootnotes.ts","../src/footnotes/mapZones.ts","../src/footnotes/tagging.ts","../src/extract/dates.ts","../src/extract/courtInference.ts","../src/extract/pincite.ts","../src/extract/courtNormalization.ts","../src/extract/extractCase.ts","../src/patterns/constitutionalPatterns.ts","../src/extract/extractConstitutional.ts","../src/extract/extractDocket.ts","../src/extract/extractFederalRegister.ts","../src/extract/extractJournal.ts","../src/extract/extractNeutral.ts","../src/extract/extractPublicLaw.ts","../src/extract/extractShortForms.ts","../src/extract/statutes/parseBody.ts","../src/extract/statutes/extractAbbreviated.ts","../src/extract/statutes/extractAlaCode1940.ts","../src/data/caBareCodes.ts","../src/extract/statutes/extractCaBareCode.ts","../src/extract/statutes/extractChapterAct.ts","../src/extract/statutes/extractColoradoProse.ts","../src/extract/statutes/extractFederal.ts","../src/extract/statutes/extractIllRevStat.ts","../src/extract/statutes/extractNamedCode.ts","../src/extract/statutes/extractProse.ts","../src/extract/extractStatute.ts","../src/extract/extractStatutesAtLarge.ts","../src/patterns/casePatterns.ts","../src/patterns/docketPatterns.ts","../src/patterns/journalPatterns.ts","../src/patterns/neutralPatterns.ts","../src/patterns/shortForm.ts","../src/patterns/statutePatterns.ts","../src/tokenize/tokenizer.ts","../src/resolve/bkTree.ts","../src/resolve/levenshtein.ts","../src/resolve/scopeBoundary.ts","../src/resolve/DocumentResolver.ts","../src/resolve/index.ts","../src/extract/detectParallel.ts","../src/extract/detectStringCites.ts","../src/extract/filterFalsePositives.ts","../src/extract/extractCitations.ts"],"sourcesContent":["import type {\n Citation,\n CitationOfType,\n CitationType,\n FullCaseCitation,\n FullCitation,\n ShortFormCitation,\n} from \"./citation\"\n\n/**\n * Type guard: narrows Citation to a full citation (case, statute, journal, neutral, publicLaw, federalRegister, statutesAtLarge, constitutional).\n */\nexport function isFullCitation(citation: Citation): citation is FullCitation {\n return (\n citation.type === \"case\" ||\n citation.type === \"docket\" ||\n citation.type === \"statute\" ||\n citation.type === \"journal\" ||\n citation.type === \"neutral\" ||\n citation.type === \"publicLaw\" ||\n citation.type === \"federalRegister\" ||\n citation.type === \"statutesAtLarge\" ||\n citation.type === \"constitutional\"\n )\n}\n\n/**\n * Type guard: narrows Citation to a short-form citation (id, supra, shortFormCase).\n */\nexport function isShortFormCitation(citation: Citation): citation is ShortFormCitation {\n return citation.type === \"id\" || citation.type === \"supra\" || citation.type === \"shortFormCase\"\n}\n\n/**\n * Type guard: narrows Citation to a full case citation.\n */\nexport function isCaseCitation(citation: Citation): citation is FullCaseCitation {\n return citation.type === \"case\"\n}\n\n/**\n * Generic type guard that narrows a Citation to a specific type.\n * Useful when the target type is dynamic or generic.\n */\nexport function isCitationType<T extends CitationType>(\n citation: Citation,\n type: T,\n): citation is CitationOfType<T> {\n return citation.type === type\n}\n\n/**\n * Exhaustiveness helper for switch statements on discriminated unions.\n *\n * Place in the `default` branch to get a compile-time error if a new\n * variant is added but not handled.\n *\n * @example\n * ```typescript\n * switch (citation.type) {\n * case 'case': ...\n * case 'statute': ...\n * // If you forget a variant, TypeScript errors here:\n * default: assertUnreachable(citation.type)\n * }\n * ```\n */\nexport function assertUnreachable(x: never): never {\n throw new Error(`Unexpected value: ${x}`)\n}\n","/**\n * Represents a text span with positions tracked through transformations.\n *\n * During text cleaning (HTML removal, whitespace normalization), positions\n * shift. Span tracks BOTH cleaned positions (for parsing) and original\n * positions (for user-facing results).\n *\n * @example\n * const original = \"Smith v. Doe, 500 F.2d 123 (2020)\"\n * // After cleaning, positions may shift\n * const span: Span = {\n * cleanStart: 14, // Position in cleaned text\n * cleanEnd: 27,\n * originalStart: 14, // Position in original text\n * originalEnd: 27\n * }\n */\nexport interface Span {\n /** Start position in cleaned/tokenized text (used during parsing) */\n cleanStart: number\n\n /** End position in cleaned/tokenized text (used during parsing) */\n cleanEnd: number\n\n /** Start position in original input text (returned to user) */\n originalStart: number\n\n /** End position in original input text (returned to user) */\n originalEnd: number\n}\n\n/**\n * Maps positions between cleaned and original text.\n *\n * Built during text transformation to track how character positions shift\n * when HTML entities are removed, whitespace is normalized, etc.\n */\nexport interface TransformationMap {\n /** Maps cleaned text position to original text position */\n cleanToOriginal: Map<number, number>\n\n /** Maps original text position to cleaned text position */\n originalToClean: Map<number, number>\n\n /** Compressed segment-based clean→original mapping for O(log k) lookup */\n cleanToOriginalSegments?: import(\"../clean/segmentMap\").SegmentMap\n}\n\n/**\n * Build a Span for a regex capture group using match.indices (ES2022 `d` flag).\n *\n * Requires the regex to have the `d` flag so match.indices is populated.\n * The indices are relative to the token text — tokenCleanStart translates\n * them to document-level clean-text positions, then resolveOriginalSpan\n * maps to original positions via TransformationMap.\n *\n * @param tokenCleanStart - The token's cleanStart position in the document\n * @param indices - match.indices[n] for the capture group: [start, end]\n * @param map - TransformationMap for clean→original resolution\n * @returns Span with both clean and original coordinates\n */\nexport function spanFromGroupIndex(\n tokenCleanStart: number,\n indices: [number, number],\n map: TransformationMap,\n): Span {\n const cleanStart = tokenCleanStart + indices[0]\n const cleanEnd = tokenCleanStart + indices[1]\n const { originalStart, originalEnd } = resolveOriginalSpan(\n { cleanStart, cleanEnd },\n map,\n )\n return { cleanStart, cleanEnd, originalStart, originalEnd }\n}\n\n/** Translate clean-text span positions back to original-text positions. */\nexport function resolveOriginalSpan(\n span: { cleanStart: number; cleanEnd: number },\n map: TransformationMap,\n): { originalStart: number; originalEnd: number } {\n // Prefer segment map (binary search) when available\n if (map.cleanToOriginalSegments) {\n return {\n originalStart: map.cleanToOriginalSegments.lookup(span.cleanStart),\n originalEnd: map.cleanToOriginalSegments.lookup(span.cleanEnd),\n }\n }\n return {\n originalStart: map.cleanToOriginal.get(span.cleanStart) ?? span.cleanStart,\n originalEnd: map.cleanToOriginal.get(span.cleanEnd) ?? span.cleanEnd,\n }\n}\n","/**\n * Built-in text cleaner functions for preprocessing legal documents.\n *\n * Each cleaner is a simple transformation: (text: string) => string\n * Cleaners can be composed via the cleanText() pipeline.\n */\n\n/**\n * Remove all HTML tags from text.\n *\n * @example\n * stripHtmlTags(\"Smith v. <b>Doe</b>, 500 F.2d 123\")\n * // => \"Smith v. Doe, 500 F.2d 123\"\n */\nexport function stripHtmlTags(text: string): string {\n return text.replace(/<[^>]+>/g, \"\")\n}\n\n/**\n * Rejoin words split across line breaks by a hyphen.\n *\n * Court opinions often wrap long words (party names, reporter abbreviations)\n * with a hyphen at the line break: \"Dil-\\nlinger\" or \"F. Sup-\\np. 3d\".\n * This cleaner removes the hyphen + line break to restore the original word.\n *\n * Must run before normalizeWhitespace (which converts \\n to spaces, leaving\n * \"Dil- linger\" instead of \"Dillinger\").\n *\n * @example\n * rejoinHyphenatedWords(\"Dil-\\nlinger V, 672 F. Supp. 3d\")\n * // => \"Dillinger V, 672 F. Supp. 3d\"\n *\n * @example\n * rejoinHyphenatedWords(\"F. Sup-\\np. 3d 100\")\n * // => \"F. Supp. 3d 100\"\n */\nexport function rejoinHyphenatedWords(text: string): string {\n return text.replace(/(\\w)-\\s*[\\n\\r]+\\s*(\\w)/g, \"$1$2\")\n}\n\n/**\n * Replace each whitespace character (tab, newline, etc.) with a regular space.\n * Does NOT collapse consecutive spaces — that's a separate step so the position\n * mapper can handle each transformation type correctly (same-length replacement\n * vs. length-reducing collapse).\n *\n * @example\n * replaceWhitespace(\"Smith\\tv.\\nDoe\")\n * // => \"Smith v. Doe\"\n */\nexport function replaceWhitespace(text: string): string {\n return text.replace(/[\\t\\n\\r\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g, \" \")\n}\n\n/**\n * Collapse runs of multiple spaces into a single space.\n *\n * @example\n * collapseSpaces(\"Smith v. Doe, 500 F.2d 123\")\n * // => \"Smith v. Doe, 500 F.2d 123\"\n */\nexport function collapseSpaces(text: string): string {\n return text.replace(/ {2,}/g, \" \")\n}\n\n/**\n * Normalize whitespace: convert tabs/newlines to spaces, collapse multiple spaces.\n * Kept for backwards compatibility — new pipeline uses replaceWhitespace + collapseSpaces.\n *\n * @example\n * normalizeWhitespace(\"Smith v. Doe, 500 F.2d 123\")\n * // => \"Smith v. Doe, 500 F.2d 123\"\n */\nexport function normalizeWhitespace(text: string): string {\n return text.replace(/[\\t\\n\\r\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]+/g, \" \").replace(/ {2,}/g, \" \")\n}\n\n/**\n * Apply Unicode NFKC normalization (ligatures → separate chars).\n *\n * @example\n * normalizeUnicode(\"Smith v. Doe, 500 F.2d 123\") // with ligature \"fi\"\n * // => \"Smith v. Doe, 500 F.2d 123\" // normalized\n */\nexport function normalizeUnicode(text: string): string {\n return text.normalize(\"NFKC\")\n}\n\n/**\n * Replace curly quotes and apostrophes with straight quotes.\n *\n * @example\n * fixSmartQuotes(\"\"Smith\" v. 'Doe', 500 F.2d 123\")\n * // => \"\\\"Smith\\\" v. 'Doe', 500 F.2d 123\"\n */\nexport function fixSmartQuotes(text: string): string {\n return text\n .replace(/[\\u201C\\u201D]/g, '\"') // curly double quotes\n .replace(/[\\u2018\\u2019]/g, \"'\") // curly single quotes/apostrophes\n}\n\n/**\n * Remove underscore OCR artifacts (common in scanned documents).\n *\n * @example\n * removeOcrArtifacts(\"Smith v. Doe, 500 F._2d 123\")\n * // => \"Smith v. Doe, 500 F.2d 123\"\n */\nexport function removeOcrArtifacts(text: string): string {\n return text.replace(/_/g, \"\")\n}\n\n/**\n * Normalize Unicode dashes to ASCII equivalents.\n *\n * En-dash (U+2013) maps to a single hyphen (page ranges like 105–107).\n *\n * Em-dash (U+2014) is context-aware: between word characters (Illinois\n * Revised Statutes paragraph subdivisions like `par. 13—214`,\n * docket-number separators like `No. 84—C—4508`, page-range pincites\n * like `875—877`) it maps to a single hyphen; standalone (the\n * `500 F.4th — (2024)` blank-page placeholder) it maps to triple\n * hyphen so the existing `-{3,}` blank-page pattern still matches.\n *\n * The in-word substitution runs first with zero-width\n * lookbehind/lookahead so adjacent em-dashes (`84—C—4508`) are both\n * rewritten in one pass and don't fall through to the blank-page rule\n * (#333).\n *\n * @example\n * normalizeDashes(\"500 F.2d 123, 125–130\") // en-dash in range\n * // => \"500 F.2d 123, 125-130\"\n *\n * @example\n * normalizeDashes(\"par. 13—214(a)\") // in-word em-dash (#333)\n * // => \"par. 13-214(a)\"\n *\n * @example\n * normalizeDashes(\"No. 84—C—4508\") // docket separator (#333)\n * // => \"No. 84-C-4508\"\n *\n * @example\n * normalizeDashes(\"500 F.4th — (2024)\") // em-dash blank page\n * // => \"500 F.4th --- (2024)\"\n */\nexport function normalizeDashes(text: string): string {\n return text\n .replace(/(?<=\\w)[\\u2014\\u2015](?=\\w)/g, \"-\") // in-word em-dash \\u2192 hyphen (#333)\n .replace(/[\\u2014\\u2015]/g, \"---\") // standalone em-dash, horizontal bar → triple hyphen\n .replace(/[\\u2010\\u2012\\u2013]/g, \"-\") // hyphen, figure dash, en-dash → hyphen\n}\n\n/**\n * Decode common HTML entities relevant to legal text.\n *\n * Handles named entities (&sect;, &para;, &amp;, &nbsp;) and numeric entities\n * (&#NNN; and &#xHHH;). Should be called after stripHtmlTags to decode any\n * remaining entities.\n *\n * @example\n * decodeHtmlEntities(\"42 U.S.C. &sect; 1983\")\n * // => \"42 U.S.C. § 1983\"\n *\n * @example\n * decodeHtmlEntities(\"Smith &amp; Jones, 500 F.2d 123\")\n * // => \"Smith & Jones, 500 F.2d 123\"\n */\nexport function decodeHtmlEntities(text: string): string {\n return (\n text\n // Named entities\n .replace(/&sect;/gi, \"§\")\n .replace(/&para;/gi, \"¶\")\n .replace(/&amp;/gi, \"&\")\n .replace(/&nbsp;/gi, \" \")\n .replace(/&lt;/gi, \"<\")\n .replace(/&gt;/gi, \">\")\n .replace(/&quot;/gi, '\"')\n .replace(/&apos;/gi, \"'\")\n // Numeric entities - decimal\n .replace(/&#(\\d+);/g, (_match, dec) => {\n const code = Number.parseInt(dec, 10)\n return Number.isNaN(code) ? _match : String.fromCharCode(code)\n })\n // Numeric entities - hexadecimal\n .replace(/&#x([0-9a-fA-F]+);/g, (_match, hex) => {\n const code = Number.parseInt(hex, 16)\n return Number.isNaN(code) ? _match : String.fromCharCode(code)\n })\n )\n}\n\n/**\n * Normalize spacing in reporter abbreviations.\n *\n * Collapses \"letter. space\" sequences common in legal reporter abbreviations\n * where the space is inconsistent (e.g., OCR or copy-paste artifacts).\n *\n * @example\n * normalizeReporterSpacing(\"550 U. S. 544\") // => \"550 U.S. 544\"\n * normalizeReporterSpacing(\"500 F. 2d 123\") // => \"500 F.2d 123\"\n * normalizeReporterSpacing(\"127 S. Ct. 1955\") // => \"127 S.Ct. 1955\"\n */\nexport function normalizeReporterSpacing(text: string): string {\n // Targeted approach: collapse spacing in known reporter abbreviation patterns,\n // then apply a general ordinal-suffix rule. This avoids affecting non-reporter\n // abbreviations like \"L. Rev.\" or \"L. J.\" in journal citations.\n let result = text\n\n // Three-letter code abbreviations (U.S.C., C.F.R.) — must run BEFORE the\n // two-letter rules below so the full 3-letter shape collapses in one pass\n // regardless of spacing pattern (`U. S. C.` / `U.S. C.` / `U. S.C.`). #284\n result = result.replace(/\\bU\\.\\s*S\\.\\s*C\\./g, \"U.S.C.\")\n result = result.replace(/\\bC\\.\\s*F\\.\\s*R\\./g, \"C.F.R.\")\n\n // Specific reporter abbreviation collapses\n result = result.replace(/\\bU\\.\\s+S\\./g, \"U.S.\")\n result = result.replace(/\\bS\\.\\s+Ct\\./g, \"S.Ct.\")\n result = result.replace(/\\bL\\.\\s+Ed\\./g, \"L.Ed.\")\n result = result.replace(/\\bF\\.\\s+Supp\\./g, \"F.Supp.\")\n result = result.replace(/\\bF\\.\\s+(\\d+[a-z]+)/g, \"F.$1\")\n\n // General ordinal-suffix collapse: \"Supp. 2d\" → \"Supp.2d\", \"Ed. 2d\" → \"Ed.2d\",\n // \"St. 3d\" → \"St.3d\", \"So. 2d\" → \"So.2d\", \"Wis. 2d\" → \"Wis.2d\"\n result = result.replace(/([A-Za-z])\\.\\s+(\\d+[a-z]+)/g, \"$1.$2\")\n\n return result\n}\n\n/**\n * Normalize typographical symbols and strip zero-width characters.\n *\n * Handles prime marks (common OCR substitution for apostrophes) and invisible\n * Unicode characters that can silently break regex pattern matching.\n *\n * @example\n * normalizeTypography(\"Doe\\u2032s case\") // prime mark\n * // => \"Doe's case\"\n *\n * @example\n * normalizeTypography(\"500\\u200BF.2d\") // zero-width space\n * // => \"500F.2d\"\n */\nexport function normalizeTypography(text: string): string {\n return text\n .replace(/[\\u2032\\u2035]/g, \"'\") // prime, reversed prime → apostrophe\n .replace(/\\u200B|\\u200C|\\u200D|\\u2060|\\uFEFF/g, \"\") // zero-width chars\n}\n\n/**\n * Strip diacritical marks from text (opt-in OCR cleaner).\n *\n * Uses Unicode NFD decomposition to separate base characters from combining\n * marks, then strips the marks. Useful for OCR'd legal documents where\n * accented characters are artifacts of misrecognition.\n *\n * NOT included in the default pipeline — call explicitly or pass in cleaners array.\n *\n * @example\n * stripDiacritics(\"Hernández v. García\")\n * // => \"Hernandez v. Garcia\"\n */\nexport function stripDiacritics(text: string): string {\n return text.normalize(\"NFD\").replace(/[\\u0300-\\u036F]/g, \"\")\n}\n","/**\n * Segment-based position mapping.\n *\n * Compresses a per-character position map into contiguous segments where the\n * offset between clean and original coordinates is constant. Lookups use\n * binary search (O(log k) where k = number of segments, typically 50-200).\n */\n\nexport interface Segment {\n /** Start position in clean text */\n cleanPos: number\n /** Corresponding start position in original text */\n origPos: number\n /** Number of positions covered by this segment */\n len: number\n}\n\nexport class SegmentMap {\n readonly segments: readonly Segment[]\n\n constructor(segments: Segment[]) {\n this.segments = segments\n }\n\n /**\n * Create an identity map (clean position === original position).\n */\n static identity(length: number): SegmentMap {\n return new SegmentMap([{ cleanPos: 0, origPos: 0, len: length + 1 }])\n }\n\n /**\n * Compress a per-position Map into a SegmentMap.\n * Adjacent entries with the same offset (origPos - cleanPos) are merged\n * into a single segment.\n */\n static fromMap(map: Map<number, number>): SegmentMap {\n if (map.size === 0) return new SegmentMap([])\n\n // Sort entries by clean position (Map iteration order may not be sorted)\n const entries = [...map.entries()].sort((a, b) => a[0] - b[0])\n\n const segments: Segment[] = []\n let segCleanStart = entries[0][0]\n let segOrigStart = entries[0][1]\n let segLen = 1\n\n for (let i = 1; i < entries.length; i++) {\n const [cleanPos, origPos] = entries[i]\n const expectedCleanPos = segCleanStart + segLen\n const expectedOrigPos = segOrigStart + segLen\n\n if (cleanPos === expectedCleanPos && origPos === expectedOrigPos) {\n segLen++\n } else {\n segments.push({ cleanPos: segCleanStart, origPos: segOrigStart, len: segLen })\n segCleanStart = cleanPos\n segOrigStart = origPos\n segLen = 1\n }\n }\n segments.push({ cleanPos: segCleanStart, origPos: segOrigStart, len: segLen })\n\n return new SegmentMap(segments)\n }\n\n /**\n * Look up the original position for a clean-text position.\n * Uses binary search on sorted segments.\n */\n lookup(cleanPos: number): number {\n const segs = this.segments\n if (segs.length === 0) return cleanPos\n\n let lo = 0\n let hi = segs.length - 1\n\n while (lo <= hi) {\n const mid = (lo + hi) >>> 1\n const seg = segs[mid]\n\n if (cleanPos < seg.cleanPos) {\n hi = mid - 1\n } else if (cleanPos >= seg.cleanPos + seg.len) {\n lo = mid + 1\n } else {\n return seg.origPos + (cleanPos - seg.cleanPos)\n }\n }\n\n // Position beyond all segments: extrapolate from last segment\n const last = segs[segs.length - 1]\n return last.origPos + (cleanPos - last.cleanPos)\n }\n}\n","import type { Warning } from \"../types/citation\"\nimport type { TransformationMap } from \"../types/span\"\nimport { SegmentMap } from \"./segmentMap\"\nimport {\n collapseSpaces,\n decodeHtmlEntities,\n fixSmartQuotes,\n normalizeDashes,\n normalizeReporterSpacing,\n normalizeTypography,\n normalizeUnicode,\n rejoinHyphenatedWords,\n replaceWhitespace,\n stripHtmlTags,\n} from \"./cleaners\"\n\n/**\n * Result of text cleaning operation.\n */\nexport interface CleanTextResult {\n /** Cleaned text after all transformations */\n cleaned: string\n\n /** Position mappings between cleaned and original text */\n transformationMap: TransformationMap\n\n /** Warnings generated during cleaning (currently unused) */\n warnings: Warning[]\n}\n\n/**\n * Clean text using a pipeline of transformation functions.\n *\n * Applies cleaners sequentially while maintaining accurate position mappings\n * between the original and cleaned text. This enables citation extraction from\n * cleaned text while reporting positions in the original text.\n *\n * @param original - Original input text\n * @param cleaners - Array of cleaner functions to apply (default: stripHtmlTags, decodeHtmlEntities, normalizeWhitespace, normalizeUnicode, normalizeDashes, fixSmartQuotes, normalizeTypography, normalizeReporterSpacing)\n * @returns Cleaned text with position mappings and warnings\n *\n * @example\n * const result = cleanText(\"Smith v. <b>Doe</b>, 500 F.2d 123\")\n * // result.cleaned: \"Smith v. Doe, 500 F.2d 123\"\n * // result.transformationMap tracks position shifts from HTML removal\n */\nexport function cleanText(\n original: string,\n cleaners: Array<(text: string) => string> = [\n stripHtmlTags,\n decodeHtmlEntities,\n rejoinHyphenatedWords,\n replaceWhitespace,\n collapseSpaces,\n normalizeUnicode,\n normalizeDashes,\n fixSmartQuotes,\n normalizeTypography,\n normalizeReporterSpacing,\n ],\n): CleanTextResult {\n // Initialize 1:1 position mapping\n let currentText = original\n let cleanToOriginal = new Map<number, number>()\n let originalToClean = new Map<number, number>()\n\n // Identity mapping: cleanToOriginal[i] = i, originalToClean[i] = i\n for (let i = 0; i <= original.length; i++) {\n cleanToOriginal.set(i, i)\n originalToClean.set(i, i)\n }\n\n // Apply each cleaner sequentially, rebuilding position maps\n for (const cleaner of cleaners) {\n const beforeText = currentText\n const afterText = cleaner(currentText)\n\n if (beforeText !== afterText) {\n // Text changed - rebuild position maps\n const { newCleanToOriginal, newOriginalToClean } = rebuildPositionMaps(\n beforeText,\n afterText,\n cleanToOriginal,\n originalToClean,\n )\n\n cleanToOriginal = newCleanToOriginal\n originalToClean = newOriginalToClean\n currentText = afterText\n }\n }\n\n const transformationMap: TransformationMap = {\n cleanToOriginal,\n originalToClean,\n cleanToOriginalSegments: SegmentMap.fromMap(cleanToOriginal),\n }\n\n return {\n cleaned: currentText,\n transformationMap,\n warnings: [],\n }\n}\n\n/**\n * Rebuild position maps after a text transformation.\n *\n * Uses a simplified algorithm that scans through both strings, matching\n * characters where possible and tracking the offset accumulation.\n *\n * @param beforeText - Text before transformation\n * @param afterText - Text after transformation\n * @param oldCleanToOriginal - Previous clean-to-original mapping\n * @param oldOriginalToClean - Previous original-to-clean mapping\n * @returns New position maps\n */\nfunction rebuildPositionMaps(\n beforeText: string,\n afterText: string,\n oldCleanToOriginal: Map<number, number>,\n _oldOriginalToClean: Map<number, number>,\n): {\n newCleanToOriginal: Map<number, number>\n newOriginalToClean: Map<number, number>\n} {\n const newCleanToOriginal = new Map<number, number>()\n const newOriginalToClean = new Map<number, number>()\n\n let beforeIdx = 0\n let afterIdx = 0\n\n // Scan through both strings, matching characters where possible\n while (beforeIdx <= beforeText.length || afterIdx <= afterText.length) {\n // Both at end\n if (beforeIdx >= beforeText.length && afterIdx >= afterText.length) {\n const originalPos = oldCleanToOriginal.get(beforeIdx) ?? beforeIdx\n newCleanToOriginal.set(afterIdx, originalPos)\n newOriginalToClean.set(originalPos, afterIdx)\n break\n }\n\n // Before text exhausted (expansion case)\n if (beforeIdx >= beforeText.length) {\n const originalPos = oldCleanToOriginal.get(beforeIdx) ?? beforeIdx\n newCleanToOriginal.set(afterIdx, originalPos)\n afterIdx++\n continue\n }\n\n // After text exhausted (removal case)\n if (afterIdx >= afterText.length) {\n const originalPos = oldCleanToOriginal.get(beforeIdx) ?? beforeIdx\n newOriginalToClean.set(originalPos, afterIdx)\n beforeIdx++\n continue\n }\n\n // Characters match - carry forward the mapping\n if (beforeText[beforeIdx] === afterText[afterIdx]) {\n const originalPos = oldCleanToOriginal.get(beforeIdx) ?? beforeIdx\n newCleanToOriginal.set(afterIdx, originalPos)\n newOriginalToClean.set(originalPos, afterIdx)\n beforeIdx++\n afterIdx++\n } else {\n // Characters differ - need to determine if this is insertion/deletion/replacement\n\n // If remaining lengths are equal, every mismatch is a pure character\n // replacement (no insertions or deletions from this point on).\n // This prevents the lookahead from misinterpreting replacements like \\n→' '\n // as multi-char deletions when the replacement char appears later in the text.\n if (beforeText.length - beforeIdx === afterText.length - afterIdx) {\n const originalPos = oldCleanToOriginal.get(beforeIdx) ?? beforeIdx\n newCleanToOriginal.set(afterIdx, originalPos)\n newOriginalToClean.set(originalPos, afterIdx)\n beforeIdx++\n afterIdx++\n continue\n }\n\n // Look ahead to find next match\n let foundMatch = false\n // Lookahead must span the largest single deletion (e.g., a long HTML tag\n // like <span class=\"citation\" data-id=\"1\"> is 35+ chars). A fixed 20\n // caused Issue #154: tags longer than the window produced corrupted\n // position mappings, collapsing many clean positions to a single\n // original position. Scale to the length delta with a reasonable floor.\n const maxLookAhead = Math.max(40, Math.abs(beforeText.length - afterText.length) + 10)\n\n // Find the closest CONFIRMED match in both directions simultaneously.\n // A \"confirmed\" match requires that at least CONFIRM_LEN characters\n // after the match point also align. This prevents greedy false matches\n // (Issue #161) where, e.g., normalizeDashes expands \"—\" → \"---\" and the\n // deletion lookahead grabs a \"-\" from a nearby page range instead.\n const CONFIRM_LEN = 3\n let bestDelLA = -1\n let bestInsLA = -1\n\n for (let la = 1; la <= maxLookAhead; la++) {\n // Check deletion direction (skipping chars in before)\n if (bestDelLA < 0 && beforeIdx + la < beforeText.length) {\n if (beforeText[beforeIdx + la] === afterText[afterIdx]) {\n let ok = true\n for (let c = 1; c < CONFIRM_LEN; c++) {\n const bi = beforeIdx + la + c\n const ai = afterIdx + c\n if (bi >= beforeText.length || ai >= afterText.length) break\n if (beforeText[bi] !== afterText[ai]) { ok = false; break }\n }\n if (ok) bestDelLA = la\n }\n }\n\n // Check insertion direction (skipping chars in after)\n if (bestInsLA < 0 && afterIdx + la < afterText.length) {\n if (beforeText[beforeIdx] === afterText[afterIdx + la]) {\n let ok = true\n for (let c = 1; c < CONFIRM_LEN; c++) {\n const bi = beforeIdx + c\n const ai = afterIdx + la + c\n if (bi >= beforeText.length || ai >= afterText.length) break\n if (beforeText[bi] !== afterText[ai]) { ok = false; break }\n }\n if (ok) bestInsLA = la\n }\n }\n\n // Stop early if we found matches in both directions\n if (bestDelLA >= 0 && bestInsLA >= 0) break\n }\n\n // Pick the shorter confirmed match (prefer smaller displacement)\n if (bestDelLA >= 0 && (bestInsLA < 0 || bestDelLA <= bestInsLA)) {\n // Deletion: chars before[beforeIdx .. beforeIdx+bestDelLA-1] were removed\n for (let i = 0; i < bestDelLA; i++) {\n const originalPos = oldCleanToOriginal.get(beforeIdx + i) ?? beforeIdx + i\n newOriginalToClean.set(originalPos, afterIdx)\n }\n beforeIdx += bestDelLA\n foundMatch = true\n } else if (bestInsLA >= 0) {\n // Insertion: chars after[afterIdx .. afterIdx+bestInsLA-1] are new\n const originalPos = oldCleanToOriginal.get(beforeIdx) ?? beforeIdx\n for (let i = 0; i < bestInsLA; i++) {\n newCleanToOriginal.set(afterIdx + i, originalPos)\n }\n afterIdx += bestInsLA\n foundMatch = true\n }\n\n if (foundMatch) continue\n\n // No match found within lookahead - treat as replacement\n const originalPos = oldCleanToOriginal.get(beforeIdx) ?? beforeIdx\n newCleanToOriginal.set(afterIdx, originalPos)\n newOriginalToClean.set(originalPos, afterIdx)\n beforeIdx++\n afterIdx++\n }\n }\n\n return { newCleanToOriginal, newOriginalToClean }\n}\n","/**\n * Union-Find (Disjoint-Set Forest)\n *\n * Tracks connected components for subsequent history chain linking.\n * Uses path halving and union by rank with lower-index-wins tie-breaking.\n */\n\nexport class UnionFind {\n private readonly parent: number[]\n private readonly rank: number[]\n\n constructor(n: number) {\n this.parent = Array.from({ length: n }, (_, i) => i)\n this.rank = new Array<number>(n).fill(0)\n }\n\n /** Find the root (canonical representative) of the set containing x. */\n find(x: number): number {\n let current = x\n while (this.parent[current] !== current) {\n this.parent[current] = this.parent[this.parent[current]] // path halving\n current = this.parent[current]\n }\n return current\n }\n\n /** Merge the sets containing x and y. Lower index becomes root. */\n union(x: number, y: number): void {\n let rootX = this.find(x)\n let rootY = this.find(y)\n if (rootX === rootY) return\n\n // Lower index is always the canonical representative\n if (rootX > rootY) {\n const tmp = rootX\n rootX = rootY\n rootY = tmp\n }\n this.parent[rootY] = rootX\n if (this.rank[rootX] === this.rank[rootY]) this.rank[rootX]++\n }\n\n /** Check if x and y are in the same set. */\n connected(x: number, y: number): boolean {\n return this.find(x) === this.find(y)\n }\n\n /** Return all connected components as a map from root → sorted member indices. */\n components(): Map<number, number[]> {\n const result = new Map<number, number[]>()\n for (let i = 0; i < this.parent.length; i++) {\n const root = this.find(i)\n let members = result.get(root)\n if (!members) {\n members = []\n result.set(root, members)\n }\n members.push(i)\n }\n return result\n }\n}\n","import type { FootnoteMap, FootnoteZone } from \"./types\"\n\n/**\n * Source pattern for opening tags of footnote container elements.\n * Matches: <footnote ...>, <fn ...>, <div class=\"footnote\" ...>,\n * <div id=\"fn1\" ...>, <aside class=\"footnote\" ...>, etc.\n *\n * Created as a fresh RegExp per call to avoid shared mutable lastIndex state.\n */\nconst FOOTNOTE_OPEN_SRC =\n /<(footnote|fn)\\b[^>]*>|<(div|aside|section|p|span)\\b[^>]*(?:class\\s*=\\s*[\"'][^\"']*\\bfootnote\\b[^\"']*[\"']|id\\s*=\\s*[\"'](?:fn|footnote)\\d*[\"'])[^>]*>/gi.source\n\n/**\n * Extract a footnote number from an HTML tag's attributes or from leading content.\n *\n * Priority: label attr > id digits > content leading digits > sequential fallback.\n */\nfunction extractFootnoteNumber(tag: string, content: string, sequentialIndex: number): number {\n // Try label=\"N\" attribute\n const labelMatch = /\\blabel\\s*=\\s*[\"'](\\d+)[\"']/.exec(tag)\n if (labelMatch) return Number.parseInt(labelMatch[1], 10)\n\n // Try id=\"fn3\" or id=\"footnote3\" attribute\n const idMatch = /\\bid\\s*=\\s*[\"'](?:fn|footnote)(\\d+)[\"']/.exec(tag)\n if (idMatch) return Number.parseInt(idMatch[1], 10)\n\n // Strip HTML tags from content before checking for leading digits\n const textContent = content.replace(/<[^>]*>/g, \"\")\n\n // Try leading digit in text content\n const contentMatch = /^\\s*(\\d+)[.\\s):]/.exec(textContent)\n if (contentMatch) return Number.parseInt(contentMatch[1], 10)\n\n // Fallback: sequential\n return sequentialIndex + 1\n}\n\n/**\n * Result of closing-tag search: the position where inner content ends\n * (start of `</tag>`) and the position after the closing tag.\n */\ninterface ClosingTagResult {\n /** Index of the `<` in `</tagName>` — marks the end of inner content */\n contentEnd: number\n /** Index of the character after `</tagName>` — marks the end of the element */\n tagEnd: number\n}\n\n/**\n * Find the matching closing tag for a given element, handling nesting.\n *\n * @returns Positions of the closing tag, or null if unmatched.\n */\nfunction findClosingTag(\n html: string,\n tagName: string,\n startAfterOpen: number,\n): ClosingTagResult | null {\n const openPattern = new RegExp(`<${tagName}\\\\b[^>]*>`, \"gi\")\n const closePattern = new RegExp(`</${tagName}\\\\s*>`, \"gi\")\n\n openPattern.lastIndex = startAfterOpen\n closePattern.lastIndex = startAfterOpen\n\n let depth = 1\n\n while (depth > 0) {\n const nextOpen = openPattern.exec(html)\n const nextClose = closePattern.exec(html)\n\n if (!nextClose) return null\n\n if (nextOpen && nextOpen.index < nextClose.index) {\n depth++\n closePattern.lastIndex = nextOpen.index + nextOpen[0].length\n } else {\n depth--\n if (depth === 0) {\n return { contentEnd: nextClose.index, tagEnd: nextClose.index + nextClose[0].length }\n }\n openPattern.lastIndex = nextClose.index + nextClose[0].length\n }\n }\n\n return null\n}\n\n/**\n * Detect footnote zones from HTML structural elements.\n *\n * Uses regex-based tag scanning (no DOM dependency) to find footnote\n * containers and record their content ranges.\n *\n * @param html - Raw HTML text\n * @returns FootnoteMap with zones in raw-text coordinates, sorted by start position\n */\nexport function detectHtmlFootnotes(html: string): FootnoteMap {\n const zones: FootnoteZone[] = []\n let match: RegExpExecArray | null\n\n // Fresh regex per call to avoid shared mutable lastIndex state\n const footnoteOpenRe = new RegExp(FOOTNOTE_OPEN_SRC, \"gi\")\n\n while ((match = footnoteOpenRe.exec(html)) !== null) {\n const openTag = match[0]\n const openTagStart = match.index\n const contentStart = openTagStart + openTag.length\n\n const tagName = match[1] || match[2]\n\n const closing = findClosingTag(html, tagName, contentStart)\n if (!closing) continue\n\n const content = html.slice(contentStart, closing.contentEnd)\n const footnoteNumber = extractFootnoteNumber(openTag, content, zones.length)\n\n zones.push({\n start: contentStart,\n end: closing.contentEnd,\n footnoteNumber,\n })\n\n footnoteOpenRe.lastIndex = closing.tagEnd\n }\n\n return zones.sort((a, b) => a.start - b.start)\n}\n","import type { FootnoteMap } from \"./types\"\n\n/** Separator line pattern: 5+ dashes or underscores on their own line. */\nconst SEPARATOR_RE = /^\\s*[-_]{5,}\\s*$/m\n\n/**\n * Source pattern for footnote markers at line start.\n * Captures the footnote number from whichever group matches.\n * Created as a fresh RegExp per call to avoid shared mutable lastIndex state.\n */\nconst MARKER_SRC =\n /^\\s*(?:FN\\s*(\\d+)[.\\s:)]|\\[(\\d+)\\]\\s|n\\.\\s*(\\d+)\\s|(\\d+)\\.\\s)/gm.source\n\n/**\n * Detect footnote zones in plain text using separator + marker heuristics.\n *\n * Strategy: find a separator line, then parse numbered markers in the text\n * that follows. Each footnote zone extends from its marker to the start\n * of the next marker (or end of text).\n *\n * @param text - Raw text (not cleaned -- needs newlines intact)\n * @returns FootnoteMap with zones in input-text coordinates, sorted by start position\n */\nexport function detectTextFootnotes(text: string): FootnoteMap {\n const sepMatch = SEPARATOR_RE.exec(text)\n if (!sepMatch) return []\n\n const sectionOffset = sepMatch.index + sepMatch[0].length\n\n const footnoteSection = text.slice(sectionOffset)\n\n // Fresh regex per call to avoid shared mutable lastIndex state\n const markerRe = new RegExp(MARKER_SRC, \"gm\")\n const markers: { index: number; footnoteNumber: number }[] = []\n let match: RegExpExecArray | null\n\n while ((match = markerRe.exec(footnoteSection)) !== null) {\n const numStr = match[1] || match[2] || match[3] || match[4]\n if (!numStr) continue\n markers.push({\n index: match.index + sectionOffset,\n footnoteNumber: Number.parseInt(numStr, 10),\n })\n }\n\n if (markers.length === 0) return []\n\n const zones: FootnoteMap = []\n for (let i = 0; i < markers.length; i++) {\n const start = markers[i].index\n const end = i + 1 < markers.length ? markers[i + 1].index : text.length\n\n zones.push({\n start,\n end,\n footnoteNumber: markers[i].footnoteNumber,\n })\n }\n\n return zones\n}\n","import { detectHtmlFootnotes } from \"./htmlDetector\"\nimport { detectTextFootnotes } from \"./textDetector\"\nimport type { FootnoteMap } from \"./types\"\n\nconst HAS_HTML_RE = /<[^>]+>/\n\n/**\n * Detect footnote zones in text (HTML or plain text).\n *\n * Strategy: if the input contains HTML tags, try HTML structural detection\n * first. If that yields no results (HTML without footnote elements), fall\n * back to plain-text heuristic detection. For non-HTML input, use plain-text\n * detection directly.\n *\n * @param text - Raw input text (HTML or plain text)\n * @returns FootnoteMap with zones in input-text coordinates, sorted by start\n */\nexport function detectFootnotes(text: string): FootnoteMap {\n if (HAS_HTML_RE.test(text)) {\n const htmlZones = detectHtmlFootnotes(text)\n if (htmlZones.length > 0) return htmlZones\n }\n\n return detectTextFootnotes(text)\n}\n","import type { TransformationMap } from \"@/types/span\"\nimport type { FootnoteMap } from \"./types\"\n\n/**\n * Search nearby positions in the map to find the closest mapped coordinate.\n * This handles cases where a zone boundary falls on a character that was\n * removed during cleaning (e.g., an HTML tag boundary).\n *\n * For zone starts, search forward (the first surviving character after the boundary).\n * For zone ends, search backward (the last surviving character before the boundary).\n *\n * @param pos - Original-text position to look up\n * @param originalToClean - Position mapping from TransformationMap\n * @param direction - \"forward\" for zone starts, \"backward\" for zone ends\n * @param maxSearch - Maximum positions to scan (matches cleanText maxLookAhead)\n * @returns Mapped clean-text position, or undefined if nothing found within range\n */\nfunction findNearestCleanPosition(\n pos: number,\n originalToClean: Map<number, number>,\n direction: \"forward\" | \"backward\",\n maxSearch = 20,\n): number | undefined {\n for (let offset = 1; offset <= maxSearch; offset++) {\n const candidate = direction === \"forward\" ? pos + offset : pos - offset\n const mapped = originalToClean.get(candidate)\n if (mapped !== undefined) return mapped\n }\n return undefined\n}\n\n/**\n * Map FootnoteMap zones from raw-text coordinates to clean-text coordinates.\n *\n * Uses TransformationMap.originalToClean to translate each zone's start/end.\n * When an exact position isn't in the map (e.g., it fell on a stripped HTML tag),\n * scans nearby positions: forward for zone starts, backward for zone ends.\n *\n * @param zones - FootnoteMap in raw-text coordinates\n * @param map - TransformationMap from cleanText()\n * @returns FootnoteMap in clean-text coordinates\n */\nexport function mapFootnoteZones(zones: FootnoteMap, map: TransformationMap): FootnoteMap {\n if (zones.length === 0) return []\n\n return zones.map((zone) => ({\n start:\n map.originalToClean.get(zone.start) ??\n findNearestCleanPosition(zone.start, map.originalToClean, \"forward\") ??\n zone.start,\n end:\n map.originalToClean.get(zone.end) ??\n findNearestCleanPosition(zone.end, map.originalToClean, \"backward\") ??\n zone.end,\n footnoteNumber: zone.footnoteNumber,\n }))\n}\n","import type { Citation } from \"@/types/citation\"\nimport type { FootnoteMap } from \"./types\"\n\n/**\n * Tag citations with footnote metadata by looking up each citation's\n * clean-text span position in the footnote zone map.\n *\n * Uses binary search on the sorted FootnoteMap for O(log n) lookup per citation.\n * Mutates citations in place.\n *\n * @param citations - Citations to tag (mutated in place)\n * @param footnoteMap - Footnote zones in clean-text coordinates, sorted by start\n */\nexport function tagCitationsWithFootnotes(\n citations: Citation[],\n footnoteMap: FootnoteMap,\n): void {\n if (footnoteMap.length === 0) return\n\n for (const citation of citations) {\n const pos = citation.span.cleanStart\n\n let lo = 0\n let hi = footnoteMap.length - 1\n\n while (lo <= hi) {\n const mid = (lo + hi) >>> 1\n const zone = footnoteMap[mid]\n\n if (pos < zone.start) {\n hi = mid - 1\n } else if (pos >= zone.end) {\n lo = mid + 1\n } else {\n citation.inFootnote = true\n citation.footnoteNumber = zone.footnoteNumber\n break\n }\n }\n }\n}\n","/**\n * Date Parsing Utilities for Legal Citations\n *\n * Parses dates from parentheticals in legal citations. Supports three formats:\n * 1. Abbreviated month: \"Jan. 15, 2020\"\n * 2. Full month: \"January 15, 2020\"\n * 3. Numeric US: \"1/15/2020\"\n * 4. Year-only: \"2020\"\n *\n * @module extract/dates\n */\n\n/**\n * Structured date components.\n * Month and day are optional to support year-only dates.\n */\nexport interface ParsedDate {\n year: number\n month?: number\n day?: number\n}\n\n/**\n * Date in both ISO string and structured format.\n */\nexport interface StructuredDate {\n /** ISO 8601 format: YYYY-MM-DD, YYYY-MM, or YYYY */\n iso: string\n /** Structured date components */\n parsed: ParsedDate\n}\n\n/**\n * Month name/abbreviation to numeric value (1-12).\n * Includes both 3-letter and 4-letter (Sept) abbreviations.\n */\nconst MONTH_MAP: Record<string, number> = {\n jan: 1,\n january: 1,\n feb: 2,\n february: 2,\n mar: 3,\n march: 3,\n apr: 4,\n april: 4,\n may: 5,\n jun: 6,\n june: 6,\n jul: 7,\n july: 7,\n aug: 8,\n august: 8,\n sep: 9,\n sept: 9,\n september: 9,\n oct: 10,\n october: 10,\n nov: 11,\n november: 11,\n dec: 12,\n december: 12,\n}\n\n/**\n * Parse a month name or abbreviation to numeric value (1-12).\n *\n * @param monthStr - Month name or abbreviation (e.g., \"Jan\", \"January\", \"Sept.\")\n * @returns Numeric month (1-12)\n * @throws Error if month name is not recognized\n *\n * @example\n * ```typescript\n * parseMonth(\"Jan\") // 1\n * parseMonth(\"Sept.\") // 9\n * parseMonth(\"December\") // 12\n * ```\n */\nexport function parseMonth(monthStr: string): number {\n // Normalize: lowercase, strip trailing period\n const normalized = monthStr.toLowerCase().replace(/\\.$/, \"\")\n const month = MONTH_MAP[normalized]\n\n if (month === undefined) {\n throw new Error(`Invalid month name: ${monthStr}`)\n }\n\n return month\n}\n\n/**\n * Convert structured date components to ISO 8601 string.\n * Handles full dates, month+year, and year-only formats.\n *\n * @param parsed - Structured date components\n * @returns ISO 8601 string (YYYY-MM-DD, YYYY-MM, or YYYY)\n *\n * @example\n * ```typescript\n * toIsoDate({ year: 2020, month: 1, day: 15 }) // \"2020-01-15\"\n * toIsoDate({ year: 2020, month: 1 }) // \"2020-01\"\n * toIsoDate({ year: 2020 }) // \"2020\"\n * ```\n */\nexport function toIsoDate(parsed: ParsedDate): string {\n const { year, month, day } = parsed\n\n if (month !== undefined && day !== undefined) {\n // Full date: YYYY-MM-DD with zero-padding\n const monthStr = String(month).padStart(2, \"0\")\n const dayStr = String(day).padStart(2, \"0\")\n return `${year}-${monthStr}-${dayStr}`\n }\n\n if (month !== undefined) {\n // Month+year: YYYY-MM with zero-padding\n const monthStr = String(month).padStart(2, \"0\")\n return `${year}-${monthStr}`\n }\n\n // Year-only: YYYY\n return String(year)\n}\n\n/**\n * Parse a date string into structured format.\n * Tries multiple formats in order:\n * 1. Abbreviated month (Jan. 15, 2020)\n * 2. Full month (January 15, 2020)\n * 3. Numeric US format (1/15/2020)\n * 4. Year-only (2020)\n *\n * @param dateStr - Date string in any supported format\n * @returns Structured date with ISO string, or undefined if no match\n *\n * @example\n * ```typescript\n * parseDate(\"Jan. 15, 2020\") // { iso: \"2020-01-15\", parsed: { year: 2020, month: 1, day: 15 } }\n * parseDate(\"January 15, 2020\") // { iso: \"2020-01-15\", parsed: { year: 2020, month: 1, day: 15 } }\n * parseDate(\"1/15/2020\") // { iso: \"2020-01-15\", parsed: { year: 2020, month: 1, day: 15 } }\n * parseDate(\"2020\") // { iso: \"2020\", parsed: { year: 2020 } }\n * parseDate(\"no date\") // undefined\n * ```\n */\nexport function parseDate(dateStr: string): StructuredDate | undefined {\n // Try abbreviated month format: Jan. 15, 2020 or Feb 9, 2015\n const abbrMatch = dateStr.match(\n /\\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)\\.?\\s+(\\d{1,2}),?\\s+(\\d{4})\\b/i,\n )\n if (abbrMatch) {\n const month = parseMonth(abbrMatch[1])\n const day = Number.parseInt(abbrMatch[2], 10)\n const year = Number.parseInt(abbrMatch[3], 10)\n const parsed = { year, month, day }\n return { iso: toIsoDate(parsed), parsed }\n }\n\n // Try full month format: January 15, 2020\n const fullMatch = dateStr.match(\n /\\b(January|February|March|April|May|June|July|August|September|October|November|December)\\s+(\\d{1,2}),?\\s+(\\d{4})\\b/i,\n )\n if (fullMatch) {\n const month = parseMonth(fullMatch[1])\n const day = Number.parseInt(fullMatch[2], 10)\n const year = Number.parseInt(fullMatch[3], 10)\n const parsed = { year, month, day }\n return { iso: toIsoDate(parsed), parsed }\n }\n\n // Try numeric US format: 1/15/2020 (full year) or 10/3/07 (two-digit year,\n // Louisiana docket-prefix and other regional shorthand; #232). Two-digit\n // years pivot at 50: 00-50 → 21st century, 51-99 → 20th century.\n const numericMatch = dateStr.match(/\\b(\\d{1,2})\\/(\\d{1,2})\\/(\\d{2}|\\d{4})\\b/)\n if (numericMatch) {\n const month = Number.parseInt(numericMatch[1], 10)\n const day = Number.parseInt(numericMatch[2], 10)\n const rawYear = numericMatch[3]\n let year = Number.parseInt(rawYear, 10)\n if (rawYear.length === 2) {\n year = year <= 50 ? 2000 + year : 1900 + year\n }\n const parsed = { year, month, day }\n return { iso: toIsoDate(parsed), parsed }\n }\n\n // Try year-only: 2020\n const yearMatch = dateStr.match(/\\b(\\d{4})\\b/)\n if (yearMatch) {\n const year = Number.parseInt(yearMatch[1], 10)\n const parsed = { year }\n return { iso: toIsoDate(parsed), parsed }\n }\n\n // No match\n return undefined\n}\n","/**\n * Court Inference from Reporter Series\n *\n * Infers court level and jurisdiction from reporter abbreviation using a\n * curated static lookup table.\n *\n * Design decision: This uses a hand-curated table rather than parsing\n * mlz_jurisdiction from the reporter DB. Using the reporter DB would make\n * it a hard dependency of core extraction, defeating the lazy-loading\n * architecture where eyecite-ts/data is a separate entry point for\n * tree-shaking. A curated table keeps court inference zero-dependency\n * and fast. Full reporter DB coverage can be added later as an opt-in\n * function in the eyecite-ts/data entry point.\n *\n * @module extract/courtInference\n */\n\nimport type { CourtInference } from \"@/types/citation\"\n\n/** Helper to reduce repetition when building the lookup table. */\nfunction federal(level: CourtInference[\"level\"]): CourtInference {\n return { level, jurisdiction: \"federal\", confidence: 1.0 }\n}\n\nfunction state(level: CourtInference[\"level\"], st: string): CourtInference {\n return { level, jurisdiction: \"state\", state: st, confidence: 1.0 }\n}\n\nfunction regional(level: CourtInference[\"level\"]): CourtInference {\n return { level, jurisdiction: \"state\", confidence: 0.7 }\n}\n\n/**\n * Curated reporter → court inference mapping.\n *\n * Covers ~80 reporter abbreviations (including spacing variants). Unknown\n * reporters return undefined from inferCourtFromReporter() — no guessing.\n */\nconst REPORTER_COURT_MAP = new Map<string, CourtInference>([\n // ── Federal Supreme ──────────────────────────────────────────────\n // After normalizeReporterSpacing, reporters arrive collapsed (e.g. \"S.Ct.\").\n // Spaced forms kept for direct-call / pre-cleaned paths.\n [\"U.S.\", federal(\"supreme\")],\n [\"S.Ct.\", federal(\"supreme\")],\n [\"L.Ed.\", federal(\"supreme\")],\n [\"L.Ed.2d\", federal(\"supreme\")],\n [\"S. Ct.\", federal(\"supreme\")],\n [\"L. Ed.\", federal(\"supreme\")],\n [\"L. Ed. 2d\", federal(\"supreme\")],\n [\"U. S.\", federal(\"supreme\")],\n [\"L.Ed. 2d\", federal(\"supreme\")],\n [\"L. Ed.2d\", federal(\"supreme\")],\n\n // ── Federal Appellate ────────────────────────────────────────────\n [\"F.\", federal(\"appellate\")],\n [\"F.2d\", federal(\"appellate\")],\n [\"F.3d\", federal(\"appellate\")],\n [\"F.4th\", federal(\"appellate\")],\n [\"F. App'x\", federal(\"appellate\")],\n\n // ── Federal Trial ────────────────────────────────────────────────\n [\"F.Supp.\", federal(\"trial\")],\n [\"F.Supp.2d\", federal(\"trial\")],\n [\"F.Supp.3d\", federal(\"trial\")],\n [\"F.Supp.4th\", federal(\"trial\")],\n [\"F. Supp.\", federal(\"trial\")],\n [\"F. Supp. 2d\", federal(\"trial\")],\n [\"F. Supp. 3d\", federal(\"trial\")],\n [\"F. Supp. 4th\", federal(\"trial\")],\n [\"F.R.D.\", federal(\"trial\")],\n [\"B.R.\", federal(\"trial\")],\n\n // ── California ───────────────────────────────────────────────────\n [\"Cal.\", state(\"supreme\", \"CA\")],\n [\"Cal.2d\", state(\"supreme\", \"CA\")],\n [\"Cal.3d\", state(\"supreme\", \"CA\")],\n [\"Cal.4th\", state(\"supreme\", \"CA\")],\n [\"Cal.5th\", state(\"supreme\", \"CA\")],\n [\"Cal.App.\", state(\"appellate\", \"CA\")],\n [\"Cal.App.2d\", state(\"appellate\", \"CA\")],\n [\"Cal.App.3d\", state(\"appellate\", \"CA\")],\n [\"Cal.App.4th\", state(\"appellate\", \"CA\")],\n [\"Cal.App.5th\", state(\"appellate\", \"CA\")],\n [\"Cal.Rptr.\", state(\"unknown\", \"CA\")],\n [\"Cal.Rptr.2d\", state(\"unknown\", \"CA\")],\n [\"Cal.Rptr.3d\", state(\"unknown\", \"CA\")],\n\n // ── New York ─────────────────────────────────────────────────────\n [\"N.Y.\", state(\"supreme\", \"NY\")],\n [\"N.Y.2d\", state(\"supreme\", \"NY\")],\n [\"N.Y.3d\", state(\"supreme\", \"NY\")],\n [\"A.D.\", state(\"appellate\", \"NY\")],\n [\"A.D.2d\", state(\"appellate\", \"NY\")],\n [\"A.D.3d\", state(\"appellate\", \"NY\")],\n [\"Misc.\", state(\"trial\", \"NY\")],\n [\"Misc.2d\", state(\"trial\", \"NY\")],\n [\"Misc.3d\", state(\"trial\", \"NY\")],\n [\"N.Y.S.\", state(\"unknown\", \"NY\")],\n [\"N.Y.S.2d\", state(\"unknown\", \"NY\")],\n [\"N.Y.S.3d\", state(\"unknown\", \"NY\")],\n\n // ── Illinois ─────────────────────────────────────────────────────\n [\"Ill.\", state(\"supreme\", \"IL\")],\n [\"Ill.2d\", state(\"supreme\", \"IL\")],\n [\"Ill.App.\", state(\"appellate\", \"IL\")],\n [\"Ill.App.2d\", state(\"appellate\", \"IL\")],\n [\"Ill.App.3d\", state(\"appellate\", \"IL\")],\n [\"Ill.Dec.\", state(\"unknown\", \"IL\")],\n\n // ── Ohio ────────────────────────────────────────────────────────\n [\"Ohio St.\", state(\"supreme\", \"OH\")],\n [\"Ohio St.2d\", state(\"supreme\", \"OH\")],\n [\"Ohio St.3d\", state(\"supreme\", \"OH\")],\n [\"Ohio App.3d\", state(\"appellate\", \"OH\")],\n\n // ── Pennsylvania ────────────────────────────────────────────────\n [\"Pa.\", state(\"supreme\", \"PA\")],\n [\"Pa. Super.\", state(\"appellate\", \"PA\")],\n\n // ── Texas ───────────────────────────────────────────────────────\n [\"Tex.\", state(\"supreme\", \"TX\")],\n\n // ── Florida ─────────────────────────────────────────────────────\n [\"Fla.\", state(\"supreme\", \"FL\")],\n\n // ── Massachusetts ───────────────────────────────────────────────\n [\"Mass.\", state(\"supreme\", \"MA\")],\n [\"Mass. App. Ct.\", state(\"appellate\", \"MA\")],\n\n // ── Regional (multi-state, no state field) ───────────────────────\n // Level is \"unknown\" because regional reporters carry both supreme\n // and appellate court opinions (e.g., A.3d includes MD Court of\n // Appeals decisions). The lower confidence already signals ambiguity.\n [\"A.\", regional(\"unknown\")],\n [\"A.2d\", regional(\"unknown\")],\n [\"A.3d\", regional(\"unknown\")],\n [\"S.E.\", regional(\"unknown\")],\n [\"S.E.2d\", regional(\"unknown\")],\n [\"S.E.3d\", regional(\"unknown\")],\n [\"S.W.\", regional(\"unknown\")],\n [\"S.W.2d\", regional(\"unknown\")],\n [\"S.W.3d\", regional(\"unknown\")],\n [\"N.E.\", regional(\"unknown\")],\n [\"N.E.2d\", regional(\"unknown\")],\n [\"N.E.3d\", regional(\"unknown\")],\n [\"N.W.\", regional(\"unknown\")],\n [\"N.W.2d\", regional(\"unknown\")],\n [\"N.W.3d\", regional(\"unknown\")],\n [\"So.\", regional(\"unknown\")],\n [\"So.2d\", regional(\"unknown\")],\n [\"So.3d\", regional(\"unknown\")],\n [\"P.\", regional(\"unknown\")],\n [\"P.2d\", regional(\"unknown\")],\n [\"P.3d\", regional(\"unknown\")],\n])\n\n/**\n * Infer court level and jurisdiction from a reporter abbreviation.\n *\n * @param reporter - Reporter abbreviation (e.g., \"F.3d\", \"Cal.App.5th\")\n * @returns CourtInference if reporter is in the curated table, undefined otherwise\n */\nexport function inferCourtFromReporter(reporter: string): CourtInference | undefined {\n return REPORTER_COURT_MAP.get(reporter)\n}\n","/**\n * Structured pincite information parsed from citation text.\n *\n * `page` and `paragraph` are mutually exclusive — a pincite is either a page\n * reference (the common case) or a paragraph reference (#204; common in\n * NY Slip Op, Canadian neutrals, and other paragraph-numbered sources). The\n * top-level convenience `pincite: number` field on the citation continues to\n * mirror `page` only; paragraph consumers read `paragraph` / `endParagraph`\n * from this struct directly.\n */\nexport interface PinciteInfo {\n /** Primary page number. Undefined when the pincite is paragraph-only (#204). */\n page?: number\n /** End page for ranges: \"570-75\" → 575 */\n endPage?: number\n /** Footnote number: \"570 n.3\" → 3. For multi-footnote refs (\"nn.3-5\"), the\n * first note; see `footnoteEnd` for the range end. */\n footnote?: number\n /** End footnote for multi-note refs: \"570 nn.3-5\" → 5 */\n footnoteEnd?: number\n /** True if this is a page or paragraph range */\n isRange: boolean\n /** True when the pincite uses star-pagination (e.g., \"*2\"), denoting a\n * slip-opinion page or unreported-decision page rather than a reporter page.\n * Common on NY Slip Op, Westlaw, and Lexis citations. */\n starPage?: boolean\n /** Paragraph number for `¶ N` / `para. N` pincites (#204). */\n paragraph?: number\n /** End paragraph for `¶¶ N-M` / `paras. N-M` pincites (#204). */\n endParagraph?: number\n /** Additional discrete pincites following the primary one (#247). E.g.,\n * `410 U.S. 113, 115, 153` → first pincite is page=115, additionalPincites\n * is `[{ page: 153, ... }]`. Each entry is a full `PinciteInfo` so ranges\n * / footnotes / star-pages inside the comma chain are preserved\n * (`115, 105-110` → additional has `endPage` set). The top-level\n * convenience `pincite: number` field on the citation continues to mirror\n * only the primary pincite; consumers needing all pincites read this array. */\n additionalPincites?: PinciteInfo[]\n /** Original text before parsing */\n raw: string\n}\n\n/** Paragraph-marker prefix: `¶`, `¶¶`, `para.`, `paras.` with optional leading\n * `at`. Routes the rest of the string into paragraph parsing. (#204) */\nconst PARA_PREFIX_REGEX = /^(?:at\\s+)?(?:¶¶?|paras?\\.?)\\s*/i\n\n/** Body of a paragraph pincite once the marker has been consumed: `N` or `N-M`. */\nconst PARA_NUM_REGEX = /^(\\d+)(?:\\s*[-–—]\\s*(\\d+))?\\s*$/\n\n/** Matches: optional \"at \", optional \"*\" (star pagination), digits, optional\n * \"-/–/—[*]digits\", optional \"n./nn./note digits\" with optional range end.\n * The footnote separator accepts a comma+space variant (`, fn. 3` — common\n * in California opinions, #311) in addition to the canonical whitespace\n * separator. `fn` / `fns` are recognized alongside `n` / `nn` / `note`. */\nconst PINCITE_PARSE_REGEX =\n /^(?:at\\s+)?(\\*?)(\\d+)(?:[-–—]\\*?(\\d+))?(?:\\s*,)?\\s*(?:(?:nn?|fns?|note)\\s*\\.?\\s*(\\d+)(?:[-–—](\\d+))?)?$/i\n\n/**\n * Parse a pincite string into structured components.\n *\n * Handles simple pages, ranges (with abbreviated end pages),\n * footnote references, and \"at\" prefixes.\n *\n * @example\n * parsePincite(\"570\") // { page: 570, isRange: false, raw: \"570\" }\n * parsePincite(\"570-75\") // { page: 570, endPage: 575, isRange: true, raw: \"570-75\" }\n * parsePincite(\"570 n.3\") // { page: 570, footnote: 3, isRange: false, raw: \"570 n.3\" }\n *\n * @returns Parsed pincite info, or null if unparseable\n */\nexport function parsePincite(raw: string): PinciteInfo | null {\n const trimmed = raw.trim()\n if (!trimmed) return null\n\n // Paragraph-marker pincite (`¶ 12`, `¶¶ 12-14`, `para. 12`, `paras. 12-14`).\n // Checked first because the page parser would reject these forms anyway. (#204)\n const paraPrefix = PARA_PREFIX_REGEX.exec(trimmed)\n if (paraPrefix) {\n const rest = trimmed.substring(paraPrefix[0].length)\n const numMatch = PARA_NUM_REGEX.exec(rest)\n if (numMatch) {\n const paragraph = Number.parseInt(numMatch[1], 10)\n const endParagraph = numMatch[2]\n ? Number.parseInt(numMatch[2], 10)\n : undefined\n const result: PinciteInfo = {\n paragraph,\n isRange: endParagraph !== undefined,\n raw: trimmed,\n }\n if (endParagraph !== undefined) result.endParagraph = endParagraph\n return result\n }\n // Falls through to page parsing if the body isn't a clean number — defensive.\n }\n\n const match = PINCITE_PARSE_REGEX.exec(trimmed)\n if (!match) return null\n\n const starPrefix = match[1]\n const pageRaw = match[2]\n const endRaw = match[3]\n const footnoteRaw = match[4]\n const footnoteEndRaw = match[5]\n const page = Number.parseInt(pageRaw, 10)\n\n let endPage: number | undefined\n let isRange = false\n\n if (endRaw) {\n isRange = true\n const endNum = Number.parseInt(endRaw, 10)\n // Handle abbreviated end pages: \"570-75\" means 575\n if (endRaw.length < pageRaw.length) {\n const prefix = pageRaw.slice(0, pageRaw.length - endRaw.length)\n endPage = Number.parseInt(prefix + endRaw, 10)\n } else {\n endPage = endNum\n }\n }\n\n const footnote = footnoteRaw ? Number.parseInt(footnoteRaw, 10) : undefined\n const footnoteEnd = footnoteEndRaw ? Number.parseInt(footnoteEndRaw, 10) : undefined\n\n const result: PinciteInfo = { page, isRange, raw: trimmed }\n if (endPage !== undefined) result.endPage = endPage\n if (footnote !== undefined) result.footnote = footnote\n if (footnoteEnd !== undefined) result.footnoteEnd = footnoteEnd\n if (starPrefix === \"*\") result.starPage = true\n\n return result\n}\n","/**\n * Normalize a court string extracted from a citation parenthetical.\n *\n * - Collapses spaces after periods: \"S.D. N.Y.\" → \"S.D.N.Y.\"\n * - Ensures trailing period on abbreviated forms: \"2d Cir\" → \"2d Cir.\"\n * - Returns undefined for empty/undefined input\n *\n * @example\n * normalizeCourt(\"S.D. N.Y.\") // \"S.D.N.Y.\"\n * normalizeCourt(\"2d Cir\") // \"2d Cir.\"\n * normalizeCourt(\"U.S.\") // \"U.S.\"\n */\nexport function normalizeCourt(court: string | undefined): string | undefined {\n if (!court || !court.trim()) return undefined\n\n let normalized = court.trim()\n\n // Collapse spaces after periods before letters: \"S.D. N.Y.\" → \"S.D.N.Y.\", \"D. del.\" → \"D.del.\"\n normalized = normalized.replace(/\\.\\s+(?=[A-Za-z])/g, \".\")\n\n // Ensure trailing period on abbreviated forms that end with a letter\n // Only add period when the string contains a period (abbreviation) or\n // starts with ordinal+word (e.g., \"2d Cir\", \"9th Cir\")\n if (\n /[A-Za-z]$/.test(normalized) &&\n (/\\./.test(normalized) || /^\\d+\\w*\\s+[A-Z]/i.test(normalized))\n ) {\n normalized += \".\"\n }\n\n return normalized\n}\n","/**\n * Case Citation Extraction\n *\n * Parses tokenized case citations to extract volume, reporter, page, and\n * optional metadata (pincite, court, year). This is the third stage of\n * the parsing pipeline:\n * 1. Clean text (remove HTML, normalize Unicode)\n * 2. Tokenize (apply patterns to find candidates)\n * 3. Extract (parse metadata, validate) ← THIS MODULE\n *\n * Extraction parses structured data from token text. Validation against\n * reporters-db happens in Phase 3 (resolution layer).\n *\n * @module extract/extractCase\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type {\n CitationSignal,\n FullCaseCitation,\n HistorySignal,\n Parenthetical,\n ParentheticalType,\n SubsequentHistoryEntry,\n} from \"@/types/citation\"\nimport {\n resolveOriginalSpan,\n spanFromGroupIndex,\n type Span,\n type TransformationMap,\n} from \"@/types/span\"\nimport type { CaseComponentSpans } from \"@/types/componentSpans\"\nimport { parseDate, type StructuredDate } from \"./dates\"\nimport { getReportersSync } from \"@/data/reportersCache\"\nimport { inferCourtFromReporter } from \"./courtInference\"\nimport { parsePincite, type PinciteInfo } from \"./pincite\"\nimport { normalizeCourt } from \"./courtNormalization\"\n\n/** Valid CitationSignal values for safe validation after regex capture + normalization. */\nconst VALID_SIGNALS = new Set([\n \"see\",\n \"see also\",\n \"see generally\",\n \"cf\",\n \"but see\",\n \"but cf\",\n \"compare\",\n \"accord\",\n \"contra\",\n // Combined `, e.g.` forms (Bluebook Rule 1.3) — must be matched by SIGNAL_PATTERNS\n // in detectStringCites.ts before the bare-signal forms (#239).\n \"e.g.\",\n \"see, e.g.\",\n \"see also, e.g.\",\n \"but see, e.g.\",\n \"cf., e.g.\",\n \"but cf., e.g.\",\n])\n\n/**\n * Regex matching any VALID_SIGNALS entry at the start of a string, followed by whitespace.\n * Derived from VALID_SIGNALS to ensure a single source of truth.\n * Multi-word signals are listed first so \"See also\" matches before \"See\".\n * The trailing `,?` accommodates combined `, e.g.` signals (Bluebook Rule 1.3)\n * whose source-text form has a trailing comma between the signal and citation.\n */\nconst SIGNAL_STRIP_REGEX = (() => {\n const sorted = [...VALID_SIGNALS].sort((a, b) => b.length - a.length)\n const alternatives = sorted.map((s) => s.replace(/\\s+/g, \"\\\\s+\").replace(/\\./g, \"\\\\.\"))\n return new RegExp(`^(${alternatives.join(\"|\")}),?\\\\s+`, \"i\")\n})()\n\n/** Parse a volume string as number when purely numeric, string when hyphenated */\nfunction parseVolume(raw: string): number | string {\n const num = Number.parseInt(raw, 10)\n return String(num) === raw ? num : raw\n}\n\n/** Month abbreviations and full names found in legal citation parentheticals */\nconst MONTH_PATTERN =\n /(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec|January|February|March|April|May|June|July|August|September|October|November|December)\\.?/\n\n// ============================================================================\n// Compiled regex patterns for performance (hoisted to module level)\n// ============================================================================\n\n/** Cached current year to avoid Date allocation per extraction call. */\nconst CURRENT_YEAR = new Date().getFullYear()\n\n/** Common US reporters for confidence boost. Exact match to avoid substring false positives.\n * Shared across extractCase and extractShortForms.\n *\n * Future editions are pre-registered defensively (#234) so the eventual rollout\n * of F.5th / N.E.4th / etc. does not silently regress confidence scores. The\n * generalized federal-reporter regex captures these formats; this set ensures\n * they earn the +0.3 reporter-match boost out of the box. */\nexport const COMMON_REPORTERS: ReadonlySet<string> = new Set([\n \"F.\",\n \"F.2d\",\n \"F.3d\",\n \"F.4th\",\n \"F.5th\",\n \"F.6th\",\n \"F.7th\",\n \"U.S.\",\n \"S. Ct.\",\n \"L. Ed.\",\n \"L. Ed. 2d\",\n \"L. Ed. 3d\",\n \"P.\",\n \"P.2d\",\n \"P.3d\",\n \"P.4th\",\n \"A.\",\n \"A.2d\",\n \"A.3d\",\n \"A.4th\",\n \"N.E.\",\n \"N.E.2d\",\n \"N.E.3d\",\n \"N.E.4th\",\n \"N.W.\",\n \"N.W.2d\",\n \"N.W.3d\",\n \"S.E.\",\n \"S.E.2d\",\n \"S.E.3d\",\n \"S.W.\",\n \"S.W.2d\",\n \"S.W.3d\",\n \"S.W.4th\",\n \"So.\",\n \"So. 2d\",\n \"So. 3d\",\n \"So. 4th\",\n \"F. Supp.\",\n \"F. Supp. 2d\",\n \"F. Supp. 3d\",\n \"F. Supp. 4th\",\n \"F. Supp. 5th\",\n \"F. Supp. 6th\",\n \"F. App'x\",\n])\n\n/** Matches volume-reporter-page format in citation core, with optional nominative reporter parenthetical.\n * Reporter character class includes `&` so the BIA `I&N Dec.` / `I. & N. Dec.`\n * variants parse correctly (#244). */\nconst VOLUME_REPORTER_PAGE_REGEX =\n /^(\\d+(?:-\\d+)?)\\s+([A-Za-z0-9.\\s'&]+)\\s+(?:\\((\\d+)\\s+([A-Z][A-Za-z.]+)\\)\\s+)?(\\d+|_{3,}|-{3,})/d\n\n/** Detects blank page placeholders (3+ underscores or dashes) */\nconst BLANK_PAGE_REGEX = /^[_-]{3,}$/\n\n/** Extracts pincite (page reference after comma). Accepts optional \"at \"\n * keyword, optional \"*\" prefix for star-pagination (NY Slip Op, Westlaw,\n * Lexis, and other slip-opinion citations; see #191), and an optional\n * trailing footnote suffix \" n.14\" / \" nn.14-15\" (see #202). */\nconst PINCITE_REGEX =\n /,\\s*(?:at\\s+)?(\\*?\\d+(?:-\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?)/d\n\n/** Matches parenthetical content */\nconst PAREN_REGEX = /\\(([^)]+)\\)/\n\n/** Look-ahead pattern for parenthetical after token. Skips pincite text\n * (including star-pagination) before the court/year parenthetical. */\nconst LOOKAHEAD_PAREN_REGEX =\n /^(?:,\\s*(?:at\\s+)?\\*?\\d+(?:-\\d+)?)*(?:\\s+(?:n|note)\\s*\\.?\\s*\\d+)?\\s*\\(([^)]+)\\)/\n\n/** Extracts pincite from look-ahead text.\n * Accepts five prefix forms:\n * - \", 125\" (comma-separated, numeric)\n * - \", at *1\" (comma + \"at\" keyword; common with star-pagination)\n * - \" at *2\" (whitespace + \"at\" keyword; NY Slip Op repeat form)\n * - \", at p. 115\" (CSM form with `p.` / `pp.` prefix; #236)\n * - \", ¶ 12\" (paragraph-marker form; #204)\n * The \"*\" prefix marks star-pagination (#191); a trailing \" n.14\" /\n * \" nn.14-15\" footnote suffix is captured when present (#202). Paragraph\n * forms (`¶ N` / `¶¶ N-M` / `para. N` / `paras. N-M`) are accepted in the\n * capture; `parsePincite` routes them to the `paragraph` field (#204). */\n// Parallel-cite disambiguation: a real pincite is bounded by end-of-string,\n// sentence punctuation, a paren or bracket close, or whitespace NOT followed\n// by a capital letter (which would start a parallel cite's reporter token,\n// e.g., `, 198 A. 154` or `, 93 S. Ct. 705`). The anchored positive lookahead\n// prevents regex backtracking into shorter digit prefixes.\n//\n// Footnote suffix (#311): the suffix-bearing forms `n.3`, `note 3` accept\n// either `\\s+` (the original `768 n.3` form) or `,\\s+` (the California\n// `768, fn. 3` form). `fn` / `fns` are added to the alternation alongside\n// `n` / `nn` / `note`.\nconst LOOKAHEAD_PINCITE_REGEX =\n /^(?:\\s+at\\s+(?:pp?\\.\\s*)?|,\\s*(?:at\\s+(?:pp?\\.\\s*)?)?)(\\*?\\d+(?:-\\d+)?(?:(?:\\s+|,\\s+)(?:nn?|fns?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?|¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?)(?=$|[.,;)(\\]]|\\s(?![A-Z]))/d\n\n/** Citation boundary pattern (digit-period-space) */\nconst CITATION_BOUNDARY_REGEX = /\\d\\.\\s+/g\n\n/** Whitespace/comma skip pattern for parenthetical scanning */\nconst PAREN_SKIP_REGEX = /[\\s,]/\n\n/** Additional discrete pincite (`, NNN` continuation) after the primary\n * pincite has been consumed (#247). Matches a comma + optional whitespace\n * followed by a pincite body. Used in a loop after `LOOKAHEAD_PINCITE_REGEX`\n * to collect `115, 153, 200` chains.\n *\n * Excludes paragraph forms (`¶ 12` mixed with page numbers is exceedingly\n * rare and would conflict with the citation core's lookahead boundary). */\n// Parallel-cite disambiguation: tighten the trailing whitespace branch to\n// reject `\\s+[A-Z]` (a parallel-cite reporter token). Allow bracket close\n// `]` as a terminator so bracketed parallel pincites still capture.\nconst ADDITIONAL_PINCITE_REGEX =\n /^,\\s*(\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?)(?=$|[.,;)(\\]]|\\s(?![A-Z]))/\n\n/** Pincite text that appears between core citation and parentheticals.\n * Matches: comma-separated page numbers/ranges and optional note refs.\n * E.g., \", 199 n.2\", \", 999-1000\", \", 130 n.5\", \", at p. 115\" (CSM, #236),\n * \", ¶ 12\" / \", paras. 12-14\" (paragraph form, #204).\n * The outer `+` is intentionally greedy to handle multi-pincite citations\n * (e.g., \", 199, 205, 210\"). Safe because the scan window is bounded by maxLookahead. */\nconst PINCITE_SKIP_REGEX =\n /^(?:,\\s*(?:(?:at\\s+(?:pp?\\.\\s*)?)?\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:n|note)\\s*\\.?\\s*\\d+)?|(?:at\\s+)?(?:¶¶?|paras?\\.?)\\s*\\d+(?:[-–—]\\d+)?))+/\n\n/**\n * Signal normalization table. Longer patterns first so \"aff'd on other grounds\"\n * matches before \"aff'd\". Each entry: [regex, normalized HistorySignal].\n */\nconst SIGNAL_TABLE: ReadonlyArray<readonly [RegExp, HistorySignal]> = [\n // affirmed (longer variants first)\n [/^aff'?d\\s+on\\s+other\\s+grounds\\b/i, \"affirmed\"],\n [/^affirmed\\s+on\\s+other\\s+grounds\\b/i, \"affirmed\"],\n [/^aff'?d\\b/i, \"affirmed\"],\n [/^affirmed\\b/i, \"affirmed\"],\n // reversed\n [/^rev'?d\\s+and\\s+remanded\\b/i, \"reversed\"],\n [/^rev'?d\\s+on\\s+other\\s+grounds\\b/i, \"reversed\"],\n [/^reversed\\s+and\\s+remanded\\b/i, \"reversed\"],\n [/^rev'?d\\b/i, \"reversed\"],\n [/^reversed\\b/i, \"reversed\"],\n // cert denied\n [/^certiorari\\s+denied\\b/i, \"cert_denied\"],\n [/^cert\\.\\s*den(ied|\\.)(?=[\\s,;(]|$)/i, \"cert_denied\"],\n // cert granted\n [/^certiorari\\s+granted\\b/i, \"cert_granted\"],\n [/^cert\\.\\s*granted\\b/i, \"cert_granted\"],\n // overruled\n [/^overruled\\s+by\\b/i, \"overruled\"],\n [/^overruled\\s+in\\b/i, \"overruled\"],\n [/^overruling\\b/i, \"overruled\"],\n [/^overruled\\b/i, \"overruled\"],\n // vacated\n [/^vacated\\s+by\\b/i, \"vacated\"],\n [/^vacated\\b/i, \"vacated\"],\n // remanded\n [/^remanded\\s+for\\s+reconsideration\\b/i, \"remanded\"],\n [/^remanded\\b/i, \"remanded\"],\n // modified\n [/^modified\\s+by\\b/i, \"modified\"],\n [/^modified\\b/i, \"modified\"],\n // abrogated\n [/^abrogated\\s+by\\b/i, \"abrogated\"],\n [/^abrogated\\s+in\\b/i, \"abrogated\"],\n [/^abrogated\\b/i, \"abrogated\"],\n // additional signals — CA-specific \"superseded by grant of review\" precedes\n // the bare \"superseded by\" so alternation prefers the more specific match.\n [/^superseded\\s+by\\s+grant\\s+of\\s+review\\b/i, \"superseded_by_grant_of_review\"],\n [/^superseded\\s+by\\b/i, \"superseded\"],\n [/^superseded\\b/i, \"superseded\"],\n // CA-specific \"disapproved on other grounds\" precedes the bare/of forms\n // so alternation prefers the more specific match (#238).\n [/^disapproved\\s+on\\s+other\\s+grounds\\b/i, \"disapproved_other_grounds\"],\n [/^disapproved\\s+of\\b/i, \"disapproved\"],\n [/^disapproved\\b/i, \"disapproved\"],\n [/^questioned\\s+by\\b/i, \"questioned\"],\n [/^questioned\\b/i, \"questioned\"],\n [/^distinguished\\s+by\\b/i, \"distinguished\"],\n [/^distinguished\\b/i, \"distinguished\"],\n [/^withdrawn\\b/i, \"withdrawn\"],\n [/^reinstated\\b/i, \"reinstated\"],\n // Federal rehearing history (#246). `as modified on denial of rehearing`\n // (CA compound, listed later) anchors on `^as modified` so the bare\n // `reh'g denied` / `rehearing denied` entries here do not conflict.\n [/^reh'?g\\s+denied\\b/i, \"rehearing_denied\"],\n [/^rehearing\\s+denied\\b/i, \"rehearing_denied\"],\n [/^reh'?g\\s+granted\\b/i, \"rehearing_granted\"],\n [/^rehearing\\s+granted\\b/i, \"rehearing_granted\"],\n // Texas writ-of-error history (Tex. R. App. P. 47.7, pre-Sept. 1997).\n // Longer disposition modifiers must precede the bare forms so alternation\n // picks the more specific match (#229).\n [/^writ\\s+ref'?d\\s+n\\.r\\.e\\./i, \"writ_refused\"],\n [/^writ\\s+ref'?d\\s+w\\.m\\.j\\./i, \"writ_refused\"],\n [/^writ\\s+ref'?d\\b/i, \"writ_refused\"],\n [/^writ\\s+dism'?d\\s+w\\.o\\.j\\./i, \"writ_dismissed\"],\n [/^writ\\s+dism'?d\\b/i, \"writ_dismissed\"],\n [/^writ\\s+denied\\b/i, \"writ_denied\"],\n [/^writ\\s+granted\\b/i, \"writ_granted\"],\n [/^no\\s+writ\\b/i, \"no_writ\"],\n // Texas petition history (post-Sept. 1997).\n [/^pet\\.\\s+ref'?d\\b/i, \"pet_refused\"],\n [/^pet\\.\\s+denied\\b/i, \"pet_denied\"],\n [/^pet\\.\\s+dism'?d\\b/i, \"pet_dismissed\"],\n [/^pet\\.\\s+granted\\b/i, \"pet_granted\"],\n [/^pet\\.\\s+filed\\b/i, \"pet_filed\"],\n [/^no\\s+pet\\.\\s+h\\./i, \"no_pet\"],\n [/^no\\s+pet\\./i, \"no_pet\"],\n // California Supreme Court review history (#238). Bluebook T8 only covers\n // federal cert. denied/granted — these CA-specific forms appear in Cal.,\n // Cal.App., and federal opinions citing CA cases.\n [/^review\\s+den(?:ied|\\.)/i, \"review_denied\"],\n [/^review\\s+granted\\b/i, \"review_granted\"],\n [/^opinion\\s+vacated\\b/i, \"opinion_vacated\"],\n // CA Tier 1 research additions (2026-05-11). Longer disposition modifiers\n // precede the bare forms so alternation prefers the more specific match.\n // (`superseded_by_grant_of_review` is placed earlier in SIGNAL_TABLE next to\n // the bare `superseded by` entry — see comment there.)\n [/^petition\\s+for\\s+review\\s+filed\\b/i, \"petition_for_review_filed\"],\n [/^petition\\s+for\\s+review\\s+granted\\b/i, \"petition_for_review_granted\"],\n [/^petition\\s+for\\s+review\\s+denied\\b/i, \"petition_for_review_denied\"],\n [/^as\\s+modified\\s+on\\s+denial\\s+of\\s+rehearing\\b/i, \"modified_on_denial_of_rehearing\"],\n // Depublication signals — order: longest-first\n [/^ordered\\s+not\\s+pub\\.?/i, \"not_published\"],\n [/^not\\s+for\\s+publication\\b/i, \"not_published\"],\n [/^nonpubl?\\.?\\s+opn\\.?/i, \"not_published\"],\n]\n\n/**\n * Match a string against SIGNAL_TABLE and return the normalized signal + match length.\n * Returns undefined if the string doesn't start with a known signal.\n */\nfunction normalizeSignal(raw: string): { signal: HistorySignal; matchLength: number } | undefined {\n for (const [regex, signal] of SIGNAL_TABLE) {\n const match = regex.exec(raw)\n if (match) {\n return { signal, matchLength: match[0].length }\n }\n }\n return undefined\n}\n\n/** Signal words that identify explanatory parentheticals */\nconst SIGNAL_WORDS: ReadonlySet<string> = new Set([\n \"holding\",\n \"finding\",\n \"stating\",\n \"noting\",\n \"explaining\",\n \"quoting\",\n \"citing\",\n \"discussing\",\n \"describing\",\n \"recognizing\",\n \"applying\",\n \"rejecting\",\n \"adopting\",\n \"requiring\",\n])\n\n/** Type guard: validates a string is a known signal word */\nfunction isSignalWord(word: string): word is ParentheticalType {\n return SIGNAL_WORDS.has(word)\n}\n\n/** Matches a leading word (used to extract signal word candidate) */\nconst LEADING_WORD_REGEX = /^([a-z]+)\\b/i\n\n/** Standard \"v.\" or \"vs.\" case name format.\n *\n * The trailing alternation accepts either a comma (Bluebook form:\n * `Smith v. Jones, 50 Cal.3d 100 (Cal. 1990)`) or a year paren (California\n * Style Manual year-first form: `Smith v. Jones (2d Cir. 2005) 396 F.3d 96`\n * / `Smith v. Jones (1990) 50 Cal.3d 100`). The CSM paren may carry an\n * optional court abbreviation before the year — `(2d Cir. 2005)`,\n * `(N.Y. 1991)` — which the caller routes to `precedingDocketMeta.court`.\n * The court text must contain a period so loose forms like `(March 1991)`\n * don't get misread as courts (Bluebook T7 court abbreviations all contain\n * at least one period). Capture group 3 = court (optional), 4 = year.\n * The `d` flag enables `match.indices` so the caller can compute a year\n * span. See #19, #293. */\nconst V_CASE_NAME_REGEX =\n /([A-Z][A-Za-z0-9\\s.,'&()/-]+?)\\s+v(?:s)?\\.?\\s+([A-Za-z0-9\\s.,'&()/-]+?)\\s*(?:,|\\((?:([^)]*?\\.[^)]*?)\\s+)?(\\d{4})\\))\\s*$/d\n\n/** Procedural prefix case name format.\n * Longer prefixes listed first so the alternation prefers the longer match\n * (e.g., `In the Matter of the Liquidation of X` beats `In the Matter of X`,\n * `In re Marriage of X` beats `In re X`, `Commonwealth of Puerto Rico ex rel.`\n * beats `Commonwealth ex rel.`). See #193, #242, and the six 2026-05-11\n * procedural-prefix research dispatches in `docs/research/`.\n *\n * The trailing alternation matches either `,` (Bluebook) or\n * `((<court>)? <year>)` (CSM year-first form, #19 / #293). Captures:\n * 1: prefix word, 2: party body, 3: court (optional), 4: year.\n * The court text must contain a period so loose forms like `(March 1991)`\n * don't get misread as courts. The `d` flag enables `match.indices`. */\nconst PROCEDURAL_PREFIX_REGEX =\n /\\b(In\\s+the\\s+Matter\\s+of\\s+the\\s+Liquidation\\s+of|In\\s+the\\s+Matter\\s+of\\s+the\\s+Rehabilitation\\s+of|In\\s+the\\s+Matter\\s+of\\s+the\\s+Receivership\\s+of|In\\s+the\\s+Matter\\s+of\\s+the\\s+Extradition\\s+of|In\\s+the\\s+Matter\\s+of\\s+the\\s+Application\\s+of|In\\s+the\\s+Matter\\s+of\\s+the\\s+Welfare\\s+of|In\\s+the\\s+Matter\\s+of|In\\s+re\\s+Petition\\s+for\\s+Naturalization\\s+of|In\\s+re\\s+Termination\\s+of\\s+Parental\\s+Rights\\s+as\\s+to|In\\s+re\\s+Termination\\s+of\\s+Parental\\s+Rights\\s+to|In\\s+re\\s+Termination\\s+of\\s+Parental\\s+Rights\\s+of|In\\s+re\\s+Marriage\\s+of|In\\s+re\\s+Liquidation\\s+of|In\\s+re\\s+Rehabilitation\\s+of|In\\s+re\\s+Receivership\\s+of|In\\s+re\\s+Naturalization\\s+of|In\\s+re\\s+Extradition\\s+of|In\\s+re\\s+Application\\s+of|In\\s+re\\s+Welfare\\s+of|In\\s+re\\s+Dependency\\s+of|In\\s+re\\s+Paternity\\s+of|In\\s+re\\s+Parentage\\s+of|In\\s+re\\s+Conservatorship\\s+of|In\\s+re\\s+Guardianship\\s+of|In\\s+re\\s+Adoption\\s+of|In\\s+the\\s+Interest\\s+of|Matter\\s+of\\s+Liquidation\\s+of|Matter\\s+of\\s+Rehabilitation\\s+of|Commonwealth\\s+of\\s+Puerto\\s+Rico\\s+ex\\s+rel\\.|Government\\s+of\\s+the\\s+Virgin\\s+Islands\\s+ex\\s+rel\\.|Commonwealth\\s+ex\\s+rel\\.|Petition\\s+for\\s+Naturalization\\s+of|People\\s+ex\\s+rel\\.|District\\s+of\\s+Columbia\\s+ex\\s+rel\\.|Conservatorship\\s+of\\s+the\\s+Person\\s+and\\s+Estate\\s+of|Conservatorship\\s+of\\s+the\\s+Person\\s+of|Conservatorship\\s+of\\s+the\\s+Estate\\s+of|Inquiry\\s+Concerning\\s+Judge|Appeal\\s+of|Care\\s+and\\s+Protection\\s+of|Succession\\s+of|In re|Ex parte|Matter of|Estate of|State ex rel\\.|United States ex rel\\.|Application of|On Petition of|Petition of|Adoption of|Conservatorship of|Guardianship of)\\s+([A-Za-z0-9\\s.,'&()/-]+?)\\s*(?:,|\\((?:([^)]*?\\.[^)]*?)\\s+)?(\\d{4})\\))\\s*$/id\n\n/**\n * Lowercase words that legitimately appear in legal party names.\n * Articles, prepositions, and legal connectors (e.g., \"of\", \"the\", \"ex\", \"rel\").\n * Used to distinguish real party names from sentence context captured by the regex.\n *\n * Note: \"in\" is intentionally NOT a connector. It's overwhelmingly a prose\n * preposition (\"the holding in\", \"the rule announced in\") rather than a\n * party-name internal token. Treating \"in\" as a connector lets lead-in\n * clauses bleed into the captured plaintiff (#223). Procedural \"In re\"\n * captions go through PROCEDURAL_PREFIX_REGEX instead.\n */\nconst PARTY_NAME_CONNECTORS = new Set([\n \"of\",\n \"the\",\n \"and\",\n \"for\",\n \"on\",\n \"by\",\n \"a\",\n \"an\",\n \"to\",\n \"at\",\n \"as\",\n \"de\",\n \"la\",\n \"el\",\n \"del\",\n \"von\",\n \"van\",\n \"ex\",\n \"rel\",\n \"et\",\n \"al\",\n \"d\",\n \"or\",\n])\n\n/**\n * Internal qualifier markers that appear inside legitimate party names\n * (e.g., \"Smith d/b/a Old Bob's Diner v. Jones\", \"Jones aka Johnson v. Smith\").\n * When such a marker is present, the plaintiff is correctly anchored at its\n * first word — even if that word is followed by lowercase non-connector\n * tokens. Without this signal, the firstWordIsProperName guard incorrectly\n * preserves lead-in prose (#223).\n */\nconst INTERNAL_QUALIFIER_REGEX = /\\b(?:d\\/?b\\/?a|a\\/?k\\/?a|f\\/?k\\/?a|n\\/?k\\/?a)\\b/i\n\n/**\n * Check whether a string looks like a legal party name vs. sentence context.\n *\n * Valid party names consist of capitalized words and legal connectors:\n * \"Smith\" ✓, \"United States\" ✓, \"People of the State of New York\" ✓\n *\n * Sentence context contains lowercase non-connector words (verbs, nouns):\n * \"The court cited Smith\" ✗ (\"court\", \"cited\" are not connectors)\n */\nfunction isLikelyPartyName(name: string): boolean {\n const words = name.split(/\\s+/)\n // Reject names whose first word is a sentence-initial transition word\n // (`Invoking Younger`, `Citing Pederson`, `Under People`). These pass\n // the all-capitalized-words check below because every word starts capital,\n // but the first word is prose, not a party name. (#323)\n const firstWord = words[0] ?? \"\"\n const firstWordClean = firstWord.toLowerCase().replace(/[.,']+$/, \"\")\n if (SENTENCE_INITIAL_WORDS.has(firstWordClean)) return false\n for (const word of words) {\n if (!word) continue\n // Standalone ampersand is ubiquitous in corporate captions\n // (\"Smith & Jones\", \"Goldman, Sachs & Co.\").\n if (word === \"&\") continue\n // Strip trailing punctuation for comparison (handles \"Inc.\", \"Corp.,\")\n const clean = word.toLowerCase().replace(/[.,']+$/, \"\")\n if (PARTY_NAME_CONNECTORS.has(clean)) continue\n if (/^[A-Z]/.test(word)) continue\n // Numeric words are valid in party names (e.g., \"Doe No. 2\", \"Route 66\")\n if (/^\\d/.test(word)) continue\n // Lowercase non-connector word → not a party name\n return false\n }\n return true\n}\n\n/**\n * Capitalized words that are never proper names — only uppercase because they're\n * sentence-initial. Prevents the firstWordIsProperName guard from treating\n * \"This landmark decision...\" or \"Those cases...\" as party-name-anchored text.\n *\n * Includes citation-introducing transition words (#323): `Under`, `Invoking`,\n * `Citing`, `Following`, `Unlike`, `Whereas`, `Pursuant`, `Applying`. These\n * appear at the start of sentences that introduce a citation and get\n * incorrectly captured as part of the plaintiff name by V_CASE_NAME_REGEX's\n * greedy lookback.\n */\nconst SENTENCE_INITIAL_WORDS = new Set([\n \"this\",\n \"that\",\n \"these\",\n \"those\",\n \"here\",\n \"there\",\n \"such\",\n \"its\",\n \"his\",\n \"her\",\n \"their\",\n \"our\",\n // Citation-introducing transition words (#323)\n \"under\",\n \"invoking\",\n \"citing\",\n \"following\",\n \"unlike\",\n \"whereas\",\n \"pursuant\",\n \"applying\",\n])\n\n/**\n * Strips date components (month, day, year) from parenthetical content\n * to isolate the court abbreviation.\n * E.g., \"2d Cir. Jan. 15, 2020\" → \"2d Cir.\"\n * \"C.D. Cal. Feb. 9, 2015\" → \"C.D. Cal.\"\n * \"D. Mass. Mar. 2020\" → \"D. Mass.\"\n * \"D. Mass. 1/15/2020\" → \"D. Mass.\"\n */\nfunction stripDateFromCourt(content: string): string | undefined {\n // Strip trailing numeric date format first (1/15/2020)\n let court = content.replace(/\\s*\\d{1,2}\\/\\d{1,2}\\/\\d{4}\\s*$/, \"\").trim()\n // Strip trailing year\n court = court.replace(/\\s*\\d{4}\\s*$/, \"\").trim()\n // Strip trailing date components: optional day+comma, month abbreviation or full name\n court = court.replace(/\\s*,?\\s*\\d{1,2}\\s*,?\\s*$/, \"\").trim()\n court = court.replace(new RegExp(`\\\\s*${MONTH_PATTERN.source}\\\\s*$`, \"i\"), \"\").trim()\n // Strip any trailing commas left over\n court = court.replace(/,\\s*$/, \"\").trim()\n return court && /[A-Za-z]/.test(court) ? court : undefined\n}\n\n// ============================================================================\n// Case-name boundary detection: abbreviation set + heuristics\n// ============================================================================\n\n/**\n * Comprehensive set of legal abbreviation stems (lowercase, without trailing period)\n * used to distinguish abbreviation periods from sentence-ending periods during\n * backward case-name scanning.\n *\n * Sources: Bluebook T6 (case name abbreviations), T7 (court abbreviations),\n * T10 (geographic abbreviations), plus common titles and corporate suffixes.\n */\nconst CASE_NAME_ABBREVS: ReadonlySet<string> = new Set([\n // ── Bluebook T6: Case name and institutional abbreviations ──\n \"acad\",\n \"acct\",\n \"accts\",\n \"admin\",\n \"adm\",\n \"advert\",\n \"advoc\",\n \"aff\",\n \"affs\",\n \"afr\",\n \"agric\",\n \"all\",\n \"alt\",\n \"am\",\n \"ann\",\n \"app\",\n \"arb\",\n \"assoc\",\n \"assocs\",\n \"atl\",\n \"auth\",\n \"auto\",\n \"ave\",\n \"bankr\",\n \"behav\",\n \"bd\",\n \"bor\",\n \"brit\",\n \"broad\",\n \"bhd\",\n \"bros\",\n \"bldg\",\n \"bull\",\n \"bus\",\n \"can\",\n \"cap\",\n \"cas\",\n \"cath\",\n \"ctr\",\n \"ctrs\",\n \"cent\",\n \"chem\",\n \"child\",\n \"chron\",\n \"coal\",\n \"coll\",\n \"com\",\n \"comm\",\n \"compar\",\n \"comp\",\n \"comput\",\n \"condo\",\n \"conf\",\n \"cong\",\n \"consol\",\n \"const\",\n \"constr\",\n \"cont\",\n \"coop\",\n \"corp\",\n \"corps\",\n \"corr\",\n \"cosm\",\n \"couns\",\n \"cntys\",\n \"cnty\",\n \"crim\",\n \"def\",\n \"delinq\",\n \"det\",\n \"dev\",\n \"dig\",\n \"dir\",\n \"disc\",\n \"disp\",\n \"distrib\",\n \"dist\",\n \"div\",\n \"econ\",\n \"educ\",\n \"elec\",\n \"emp\",\n \"eng\",\n \"enter\",\n \"enters\", // Enters. (Enterprises, plural of Bluebook T6 \"Enter.\") — common in NY/4th Dep't captions (\"Fields Enters. Inc.\"). #288 surfaced this gap.\n \"ent\",\n \"equal\",\n \"equip\",\n \"est\",\n \"eur\",\n \"exam\",\n \"exch\",\n \"exec\",\n \"expl\",\n \"exp\",\n \"fac\",\n \"fam\",\n \"fams\",\n \"fed\",\n \"fid\",\n \"fin\",\n \"found\",\n \"gen\",\n \"glob\",\n \"grp\",\n \"guar\",\n \"hist\",\n \"hosp\",\n \"hous\",\n \"hum\",\n \"immigr\",\n \"imp\",\n \"inc\",\n \"indem\",\n \"indep\",\n \"indus\",\n \"info\",\n \"inj\",\n \"inst\",\n \"ins\",\n \"intell\",\n \"intel\",\n \"int\",\n \"inv\",\n \"invs\",\n \"jurid\",\n \"just\",\n \"juv\",\n \"lab\",\n \"law\",\n \"liab\",\n \"ltd\",\n \"loc\",\n \"mach\",\n \"mag\",\n \"maint\",\n \"mgmt\",\n \"mgt\",\n \"mfr\",\n \"mfrs\",\n \"mfg\",\n \"mar\",\n \"mkt\",\n \"mktg\",\n \"matrim\",\n \"mech\",\n \"med\",\n \"merch\",\n \"metro\",\n \"min\",\n \"misc\",\n \"mod\",\n \"mortg\",\n \"mun\",\n \"mut\",\n \"nat\",\n \"negl\",\n \"negot\",\n \"nw\",\n \"no\",\n \"nos\",\n \"off\",\n \"org\",\n \"orgs\",\n \"pac\",\n \"pat\",\n \"pers\",\n \"pharm\",\n \"phil\",\n \"plan\",\n \"pol\",\n \"prac\",\n \"pres\",\n \"priv\",\n \"prob\",\n \"proc\",\n \"prod\",\n \"pro\",\n \"prop\",\n \"psych\",\n \"pub\",\n \"rec\",\n \"reg\",\n \"regul\",\n \"rehab\",\n \"rel\",\n \"rels\",\n \"rep\",\n \"reprod\",\n \"rsch\",\n \"rsrv\",\n \"resol\",\n \"res\",\n \"resp\",\n \"rest\",\n \"ret\",\n \"rd\",\n \"sav\",\n \"sch\",\n \"schs\",\n \"sci\",\n \"sec\",\n \"serv\",\n \"servs\",\n \"sess\",\n \"soc\",\n \"solic\",\n \"spec\",\n \"stat\",\n \"subcomm\",\n \"sur\",\n \"surv\",\n \"sys\",\n \"tchr\",\n \"tech\",\n \"telecomm\",\n \"tel\",\n \"temp\",\n \"twp\",\n \"transcon\",\n \"transp\",\n \"treas\",\n \"tr\",\n \"trs\",\n \"tpk\",\n \"unemplmt\",\n \"unif\",\n \"univ\",\n \"urb\",\n \"util\",\n \"veh\",\n \"vehs\",\n \"vill\",\n \"voc\",\n \"whse\",\n \"whol\",\n \"litig\",\n // ── T6: Directional abbreviations ──\n \"n\",\n \"s\",\n \"e\",\n \"w\",\n \"m\",\n \"ne\",\n \"se\",\n \"sw\",\n // ── T6/T10: Geographic features and street types ──\n // Appear mid-party-name as \"Long Is.\", \"Mt. Sinai\", \"Ft. Worth\", \"Stony Pt.\",\n // \"Route 66\" (Rt.), \"St. Paul\" / \"Main St.\", \"Wilshire Blvd.\", \"Times Sq.\",\n // \"Pacific Hwy.\", \"Grand Central Pkwy.\", \"Washington Hts.\". Without these,\n // the backward scanner treats \"Is. R\" / \"Mt. S\" as sentence boundaries and\n // truncates the case name. See #188.\n \"is\",\n \"mt\",\n \"ft\",\n \"pt\",\n \"rt\",\n \"st\",\n \"blvd\",\n \"sq\",\n \"hwy\",\n \"pkwy\",\n \"hts\",\n // ── T7: Court abbreviations ──\n \"v\",\n \"vs\",\n \"ct\",\n \"cir\",\n \"supp\",\n \"cl\",\n \"jud\",\n \"super\",\n \"sup\",\n \"magis\",\n \"mil\",\n \"terr\",\n // ── T10: US state abbreviations ──\n \"ala\",\n \"ariz\",\n \"ark\",\n \"cal\",\n \"colo\",\n \"conn\",\n \"del\",\n \"fla\",\n \"ga\",\n \"haw\",\n \"ida\",\n \"ill\",\n \"ind\",\n \"kan\",\n \"ky\",\n \"la\",\n \"me\",\n \"md\",\n \"mass\",\n \"mich\",\n \"minn\",\n \"miss\",\n \"mo\",\n \"mont\",\n \"neb\",\n \"nev\",\n \"okla\",\n \"or\",\n \"pa\",\n \"tenn\",\n \"tex\",\n \"vt\",\n \"va\",\n \"wash\",\n \"wis\",\n \"wyo\",\n // ── Titles and honorifics ──\n \"mr\",\n \"mrs\",\n \"ms\",\n \"dr\",\n \"jr\",\n \"sr\",\n \"prof\",\n \"rev\",\n \"hon\",\n \"sgt\",\n \"capt\",\n \"col\",\n \"lt\",\n // ── Other common legal abbreviations ──\n \"ed\",\n \"op\",\n \"ad\",\n \"dep\",\n \"ass\",\n \"ry\",\n // ── reporters-db alignment (Bluebook T6-derived, 19th ed) ──\n // Period-form abbreviations. Source: freelawproject/reporters-db\n // data/case_name_abbreviations.json. `co` (Co./Company) was the most\n // impactful gap — \"Smith & Co. United States Corp.\" was truncated to\n // just \"United States Corp.\" because the sentence-boundary scan fired\n // on \"Co. U\".\n \"co\",\n \"cmty\",\n \"cty\",\n \"envtl\",\n \"gend\",\n \"par\",\n \"prot\",\n \"ref\",\n \"sol\",\n \"adver\",\n // Apostrophe-form abbreviations. Stored as pure-letter stems because\n // isLikelyAbbreviationPeriod now strips all apostrophes/periods before\n // set lookup. These appear in nearly every NY appellate citation\n // (\"2d Dep't\", \"Nat'l\", \"Int'l\", \"Ass'n\", \"Gov't\", etc.).\n \"admr\",\n \"admx\",\n \"assn\",\n \"commcn\",\n \"commn\",\n \"commr\",\n \"contl\",\n \"dept\",\n \"empr\",\n \"empt\",\n \"engg\",\n \"engr\",\n \"entmt\",\n \"envt\",\n \"examr\",\n \"exr\",\n \"exx\",\n \"fedn\",\n \"govt\",\n \"intl\",\n \"invr\",\n \"meml\",\n \"natl\",\n \"profl\",\n \"pship\",\n \"publg\",\n \"publn\",\n \"regl\",\n \"secy\",\n \"sholder\",\n \"socy\",\n // ── Cornell § 4-100 / state-practice gaps not in Bluebook T6 source ──\n // Used in real case captions across multiple jurisdictions:\n // - \"Tp.\" (NJ alternative to Bluebook \"Twp.\" Township) —\n // \"Parsippany-Troy Hills Tp. Council\", \"Bernards Tp. v. ...\"\n // - \"Vil.\" (NY single-L variant of Bluebook \"Vill.\" Village) — #288\n // NY Reporter / Slip Opinion captions, esp. 4th Dep't:\n // \"Bristol Harbour Vil. Assn., Inc.\", \"Smithtown Vil. Bd.\"\n // - \"Tax'n\" (Taxation) — \"Dep't of Tax'n v. ...\"\n // - \"Enf't\" (Enforcement) — \"Drug Enf't Admin. v. ...\"\n // - \"Rts.\" (Rights) — \"Human Rts. Watch v. ...\", \"Civ. Rts. Div.\"\n \"tp\",\n \"vil\",\n \"taxn\",\n \"enft\",\n \"rts\",\n // ── 2026-05-10 jurisdiction-survey additions ──\n // Cross-agent research canvassing 15 jurisdictional clusters (NY/NJ, PA/DE/\n // MD/DC/WV, New England, CA, TX/OK, Southeast, Deep South, Great Lakes,\n // Western/Pacific, federal courts, federal specialty courts, govt agencies +\n // corporate entity forms, ALWD + Bluebook 21st + reporters-db sweep, and\n // foreign/tribal/territorial) plus a parser-quirks audit. Reports retained\n // in docs/research/2026-05-10-citation-abbrevs-*.md.\n //\n // Universal apostrophe-form + Bluebook BT1.2 party designations:\n \"atty\", // Att'y / Att'y Gen. — 32k+ corpus matches; every state + federal AG case\n \"attys\", // Att'ys (plural)\n \"petr\", // Pet'r — Bluebook 21st BT1.2 (habeas, immigration, PTAB captions)\n \"respt\", // Resp't — Bluebook 21st BT1.2 counterpart to Pet'r\n \"commrs\", // Comm'rs (plural of existing commr) — \"Bd. of Cnty. Comm'rs\"\n // Plurals of existing singular stems (modern LLC-era captions):\n \"hldgs\", // Hldgs. (Holdings) — DE Chancery, NY 1st Dep't, GA LLC\n \"hldg\", // Hldg. (singular)\n \"props\", // Props. — Lanvale Props. LLC (NC), Ryan Jackson Props.\n \"prods\", // Prods. (Products plural) — product-liability captions nationwide\n \"ents\", // Ents. (Enterprises plural) — \"NC Ents., L.L.C.\"\n \"invests\", //Invests. — Ohio \"A.A.A. Invests. v. Columbus\"\n \"scis\", // Scis. (Sciences plural)\n \"emps\", // Emps. — \"Okla. Pub. Emps. Ret. Sys.\", \"Pub. Emps. Rel. Comm'n\"\n \"sols\", // Sols. (Solutions plural) — modern LLC captions \"Med-Care Sols., LLC\"\n \"corrs\", // Corrs. (Corrections plural) — \"Ark. Bd. of Corrs.\"\n \"telecomms\", //Telecomms. (plural) — \"BellSouth Telecomms., Inc.\"\n \"examrs\", // Exam'rs (Examiners plural) — \"Med. Exam'rs Comm'n\", \"Bar Exam'rs\"\n \"cmtys\", // Cmtys. (Communities plural) — \"Fla. Cmtys. Tr.\"\n \"colls\", // Colls. (Colleges plural) — \"State Bd. of Cmty. Colls.\"\n \"cts\", // Cts. (Courts plural) — \"Off. of the St. Cts. Admin'r\"\n \"amends\", // Amends. (Amendments plural)\n // Standard institutional / agency abbreviations:\n \"civ\", // Civ. (Civil) — Ala. Civ. App., Civ. Rts. Div., Civ. Liberties Union\n \"enf\", // Enf. (Enforcement, distinct from existing enft) — \"Drug Enf. Admin.\"\n \"advis\", // Advis. (Advisory) — \"Advis. Council/Comm.\"\n \"utils\", // Utils. — \"Utils. Comm'n\", \"Pub. Utils. Comm'n\"\n \"lic\", // Lic. (License) — \"Bd. of License Comm'rs\" (Tiverton, 469 U.S. 238)\n \"bur\", // Bur. (Bureau) — \"Bur. of Driver Lic.\", \"Bur. of Land Mgmt.\"\n \"insp\", // Insp. (Inspection) — \"Bd. of Lic. & Insp. Review\"\n \"conserv\", //Conserv. (Conservation) — Bluebook 21st; 1.5k corpus matches\n \"retire\", // Retire. (Retirement) — \"W. Va. Consol. Pub. Retire. Bd.\" (distinct from ret)\n \"discipl\", //Discipl. (Disciplinary) — \"Lawyer Disciplinary Bd.\"\n \"supers\", // Supers. (Supervisors) — PA \"Twp. Bd. of Supers.\" (hundreds of captions)\n \"edn\", // Edn. (Ohio variant of Educ.) — \"Bd. of Edn.\"\n \"coun\", // Coun. (Council) — NLRB \"Dist. Council 9\", distinct from couns (Counsel)\n \"stds\", // Stds. (Standards) — \"Crim. Just. Stds. & Training Comm'n\"\n \"procs\", // Procs. (Procedures)\n \"quals\", // Quals. (Qualifications) — \"Jud. Quals. Comm'n\"\n // Regional / state-specific:\n \"boro\", // NJ \"Boro.\" — alternative long form to existing \"Bor.\" (Borough)\n \"commw\", // Commw. — PA Commonwealth Court (\"Pa. Commw. Ct.\")\n \"adv\", // Adv. (Advance) — NV \"Nev., Adv. Op.\" form\n \"comn\", // Com'n — Hawaii single-m variant of Comm'n\n \"irrig\", // Irrig. (Irrigation) — ID/WY/WA \"Pioneer Irrig. Dist.\"\n \"reclam\", // Reclam. (Reclamation) — federal-project captions\n \"rptr\", // Rptr. — CA \"Cal.Rptr.\" nested in bracketed parallel cites\n \"vet\", // Vet. (Veterans) — \"Vet. App.\", \"Sec'y of Vet. Aff.\"\n \"trib\", // Trib. — Tribune (Bluebook 21st T6) + Tribal Ct.\n \"adj\", // Adj. — Adjustment (VT/NH \"Zoning Bd. of Adj.\") + Adjudicatory (FL)\n \"vol\", // Vol. (Volunteer) — PA \"Univ. Vol. Fire Dept.\"; volume cites are pre-digit\n // Corporate entity forms:\n \"pty\", // Pty. — Australian \"Pty. Ltd.\"\n // Bluebook 21st ed. (2020) T6 / T13.2 merger additions:\n \"poly\", // Pol'y (Policy)\n \"stud\", // Stud. (Studies)\n \"libr\", // Libr. (Library)\n \"refin\", // Refin. (Refining) — distinct from existing ref (Referee/Reference)\n \"socio\", // Socio. (Sociology) — distinct from existing soc (Social)\n \"laby\", // Lab'y (Laboratory) — distinct from existing lab (Labor)\n \"naty\", // Nat'y (Nationality)\n \"wkly\", // Wkly. (Weekly)\n \"appx\", // App'x (Appendix) — \"F. App'x\" reporter\n // Plains + Upper Midwest (re-dispatch agent, report retained):\n \"comr\", // Comr. — Nebraska apostrophe-dropping single-m variant of Comm'r\n \"comrs\", // Comrs. — NE plural variant; \"Cherry Cty. Bd. of Comrs.\"\n \"reins\", // Reins. — Bluebook T6; \"Grinnell Mut. Reins. Co.\" (ND insurance)\n])\n\n/**\n * Detect whether a period at `dotIndex` in `text` is likely an abbreviation\n * rather than a sentence boundary.\n *\n * Three-tier check:\n * 1. Word stem is in the comprehensive CASE_NAME_ABBREVS set\n * 2. Single uppercase letter (initial: A., B., J., N.)\n * 3. Word contains internal periods (dotted initialism: N.Y., U.S., D.C.)\n */\nfunction isLikelyAbbreviationPeriod(text: string, dotIndex: number): boolean {\n // Walk backward from the period to find the word\n let start = dotIndex\n while (start > 0 && /[-A-Za-z.']/.test(text[start - 1])) {\n start--\n }\n const word = text.substring(start, dotIndex)\n if (!word) return false\n\n // Strip ALL periods and apostrophes for set lookup. This normalizes\n // apostrophe-form abbreviations (\"Ass'n\" → \"assn\", \"Dep't\" → \"dept\",\n // \"Nat'l\" → \"natl\") so the set can store pure-letter stems.\n const stem = word.replace(/['.]/g, \"\").toLowerCase()\n\n // Tier 1: Known legal abbreviation\n if (CASE_NAME_ABBREVS.has(stem)) return true\n\n // Tier 2: Single uppercase letter (initial)\n if (stem.length === 1 && /[a-z]/i.test(stem)) return true\n\n // Tier 3: Contains internal periods (dotted initialism like N.Y, U.S, D.C)\n if (/\\.[A-Za-z]/.test(word)) return true\n\n return false\n}\n\n/** Hard boundary: Id. citation marker — the scan must not cross this.\n * Case-sensitive: Bluebook convention is always capitalized \"Id.\" */\nconst ID_BOUNDARY_REGEX = /\\bId\\.\\s+/g\n\n/** Hard boundary: parenthetical signal words that introduce nested citations.\n * Matches opening paren + optional space + signal word (+ optional \", e.g.,\")\n * + whitespace.\n *\n * E.g., \"(quoting \", \"(citing \", \"(cited in \", \"(quoted in \", \"(accord \",\n * \"(citing, e.g., \". The optional `, e.g.[,]` tail handles the common form\n * where a citing parenthetical introduces multiple authorities. See #187. */\nconst PAREN_SIGNAL_BOUNDARY_REGEX =\n /\\(\\s*(?:quoting|citing|cited\\s+in|quoted\\s+in|accord|discussing|noting|explaining|describing|recognizing|applying|rejecting|adopting|requiring|overruling|overruled\\s+by|abrogated\\s+by)(?:,\\s*e\\.g\\.,?)?\\s+/gi\n\n/** Sentence boundary: closing paren or period, followed by space + uppercase\n * letter or open-paren. The `(` lookahead handles parenthesized citations\n * inside running prose — `... discretion. (Burquet v. Brumbaugh, ...)` —\n * where the citation envelope opens with `(` immediately after the\n * sentence-ending period. Without it, the case-name backward walk crosses\n * the boundary and absorbs the entire preceding sentence into the\n * plaintiff field. #323 */\nconst SENTENCE_BOUNDARY_REGEX = /[.)]\\s+(?=[A-Z(])/g\n\n/** Louisiana docket-prefix boundary (#232). Matches the Louisiana citation\n * shape `NN-NNNN (La. ... M/D/YY)` or `YYYY-K-NNNN (La. ... M/D/YY)` that\n * precedes the parallel `So. 2d` / `So. 3d` reporter citation. The capture\n * groups expose the court (group 2) and the date string (group 3) so the\n * trailing reporter citation can inherit the metadata. Includes an optional\n * `, p. N` pincite segment commonly present in LA practice.\n *\n * The trailing `,` + whitespace is consumed so that everything BEFORE this\n * pattern is the caption. */\nconst LA_DOCKET_BOUNDARY_REGEX =\n /,?\\s*(\\d{2,4}-[A-Z\\d-]+)(?:,\\s*p\\.\\s*\\d+)?\\s*\\((La\\.[^)]*?)\\s+(\\d{1,2}\\/\\d{1,2}\\/\\d{2,4})\\),\\s*/g\n\n/**\n * Extract case name via backward search from citation core.\n * Looks for \"v.\" pattern or procedural prefixes (In re, Ex parte, Matter of).\n *\n * @param cleanedText - Full cleaned text\n * @param coreStart - Position where citation core begins (volume start)\n * @param maxLookback - Maximum characters to search backward (default 150)\n * @param options - Optional original text + transformationMap to detect\n * paragraph-break boundaries that the cleaner has collapsed (#221).\n * @returns Case name and start position, or undefined if not found\n *\n * @example\n * ```typescript\n * extractCaseName(text, 20, 150)\n * // Returns: { caseName: \"Smith v. Jones\", nameStart: 0 }\n * ```\n */\nexport function extractCaseName(\n cleanedText: string,\n coreStart: number,\n maxLookback = 150,\n options?: { originalText?: string; transformationMap?: TransformationMap },\n):\n | {\n caseName: string\n nameStart: number\n /** Year captured from CSM year-first form (`In re K.F. (2009)`). */\n year?: number\n /** Clean-coordinate position of the year digits (excluding parens). */\n yearStart?: number\n /** Clean-coordinate position after the year digits. */\n yearEnd?: number\n /** Metadata recovered from a Louisiana docket-prefix paren that sits\n * between the caption and the citation core (#232). Applied by the\n * caller as fallback for `year` / `court` / `date` when the citation's\n * own trailing paren is absent. */\n precedingDocketMeta?: {\n court: string\n year: number\n date: StructuredDate\n }\n }\n | undefined {\n const searchStart = Math.max(0, coreStart - maxLookback)\n let precedingText = cleanedText.substring(searchStart, coreStart)\n let adjustedSearchStart = searchStart\n\n // Split at last boundary to avoid crossing citation/sentence boundaries.\n // We check five boundary types:\n // 1. Citation boundary: digit-period-space (e.g., \"10. \" from a previous cite's page number)\n // 2. Id. boundary: \"Id. \" short-form citation marker (#182)\n // 3. Parenthetical signal boundary: \"(quoting \", \"(citing \", \"(cited in \" (#182)\n // 4. Sentence boundary: period/paren + space + uppercase, skipped when the\n // word before the period is a legal abbreviation (Bluebook T6/T10/T7)\n // 5. Paragraph boundary: \\n\\s*\\n in the original text, recovered via\n // transformationMap because the cleaner collapses newlines to spaces (#221)\n let lastBoundaryIndex = -1\n let match: RegExpExecArray | null\n\n // Check paragraph boundaries via original text (#221).\n // The default cleaner pipeline replaces \\n with space, so paragraph breaks\n // are invisible in cleanedText. Recover them from originalText by mapping\n // the search window back to original coordinates.\n if (options?.originalText && options.transformationMap) {\n const { originalText, transformationMap } = options\n const searchOriginalStart =\n transformationMap.cleanToOriginal.get(searchStart) ?? searchStart\n const coreOriginalStart =\n transformationMap.cleanToOriginal.get(coreStart) ?? coreStart\n if (coreOriginalStart > searchOriginalStart) {\n const originalWindow = originalText.substring(searchOriginalStart, coreOriginalStart)\n const paragraphBreakRegex = /\\n[ \\t\\r]*\\n/g\n let pMatch: RegExpExecArray | null\n while ((pMatch = paragraphBreakRegex.exec(originalWindow)) !== null) {\n const breakOriginalEnd = searchOriginalStart + pMatch.index + pMatch[0].length\n // Find the clean position immediately at/after the paragraph break.\n // The break itself collapses to a space; the next non-whitespace char\n // is the start of the new paragraph in cleanedText.\n let cleanPos: number | undefined\n for (let off = 0; off < 10; off++) {\n cleanPos = transformationMap.originalToClean.get(breakOriginalEnd + off)\n if (cleanPos !== undefined) break\n }\n if (cleanPos !== undefined && cleanPos >= searchStart && cleanPos <= coreStart) {\n const relIndex = cleanPos - searchStart\n if (relIndex > lastBoundaryIndex) {\n lastBoundaryIndex = relIndex\n }\n }\n }\n }\n }\n\n // Check citation boundaries (digit-period-space)\n CITATION_BOUNDARY_REGEX.lastIndex = 0\n while ((match = CITATION_BOUNDARY_REGEX.exec(precedingText)) !== null) {\n const boundaryEnd = match.index + match[0].length\n if (boundaryEnd > lastBoundaryIndex) {\n lastBoundaryIndex = boundaryEnd\n }\n }\n\n // Check Id. boundaries (#182)\n ID_BOUNDARY_REGEX.lastIndex = 0\n while ((match = ID_BOUNDARY_REGEX.exec(precedingText)) !== null) {\n const boundaryEnd = match.index + match[0].length\n if (boundaryEnd > lastBoundaryIndex) {\n lastBoundaryIndex = boundaryEnd\n }\n }\n\n // Check parenthetical signal boundaries (#182)\n PAREN_SIGNAL_BOUNDARY_REGEX.lastIndex = 0\n while ((match = PAREN_SIGNAL_BOUNDARY_REGEX.exec(precedingText)) !== null) {\n const boundaryEnd = match.index + match[0].length\n if (boundaryEnd > lastBoundaryIndex) {\n lastBoundaryIndex = boundaryEnd\n }\n }\n\n // Louisiana docket-prefix segments (#232) sit *between* the caption and\n // the trailing reporter citation: `Smith v. Jones, 07-393, p. 2 (La. App.\n // 3d Cir. 10/3/07), 966 So. 2d 1127`. Unlike sentence / Id. / paren-signal\n // boundaries, the segment is INTERIOR — stripping it from `precedingText`\n // preserves the caption to its left. Capture the docket paren's court +\n // date for metadata transfer onto the trailing reporter citation.\n let precedingDocketMeta:\n | { court: string; year: number; date: StructuredDate }\n | undefined\n LA_DOCKET_BOUNDARY_REGEX.lastIndex = 0\n const laDocketMatch = LA_DOCKET_BOUNDARY_REGEX.exec(precedingText)\n if (laDocketMatch) {\n const dateStr = laDocketMatch[3]\n const date = parseDate(dateStr)\n if (date) {\n precedingDocketMeta = {\n court: laDocketMatch[2].trim(),\n year: date.parsed.year,\n date,\n }\n }\n // Excise the docket segment, leaving just the trailing \", \" so the\n // V_CASE_NAME_REGEX still sees a comma-terminated caption to its left.\n precedingText =\n precedingText.substring(0, laDocketMatch.index) +\n \", \" +\n precedingText.substring(laDocketMatch.index + laDocketMatch[0].length)\n }\n\n // Check sentence boundaries: \"). \" or \". \" followed by uppercase letter.\n // Skip when the period belongs to a legal abbreviation (comprehensive T6/T10/T7 check).\n SENTENCE_BOUNDARY_REGEX.lastIndex = 0\n while ((match = SENTENCE_BOUNDARY_REGEX.exec(precedingText)) !== null) {\n // Only check abbreviation for period boundaries, not close-paren boundaries\n if (\n precedingText[match.index] === \".\" &&\n isLikelyAbbreviationPeriod(precedingText, match.index)\n ) {\n continue\n }\n const boundaryEnd = match.index + match[0].length\n if (boundaryEnd > lastBoundaryIndex) {\n lastBoundaryIndex = boundaryEnd\n }\n }\n\n if (lastBoundaryIndex !== -1) {\n precedingText = precedingText.substring(lastBoundaryIndex)\n adjustedSearchStart = searchStart + lastBoundaryIndex\n }\n\n // Priority 1: Standard \"v.\" or \"vs.\" format with comma before citation\n // Match party names with letters, numbers (for \"Doe No. 2\"), periods, apostrophes, ampersands, hyphens, slashes\n const vMatch = V_CASE_NAME_REGEX.exec(precedingText)\n if (vMatch) {\n // Check for semicolon in matched text (multi-citation separator)\n if (!vMatch[0].includes(\";\")) {\n let plaintiff = vMatch[1].trim()\n let trimOffset = 0\n\n // Validate plaintiff: real party names are capitalized words + legal connectors.\n // If the plaintiff contains lowercase non-connector words (e.g., \"The court cited Smith\"),\n // it captured sentence context. Trim from the left to the first valid party name start.\n //\n // The firstWordIsProperName guard preserves the original plaintiff when the\n // first word is a real party name and the lowercase content is an internal\n // qualifier (\"Smith d/b/a Old Bob's Diner\"). Without an internal-qualifier\n // marker, a capitalized first word alone is NOT enough to suppress trimming —\n // sentence-initial prepositions like \"Under\", \"Pursuant\", \"Following\" would\n // otherwise be preserved as if they were proper nouns (#223).\n if (!isLikelyPartyName(plaintiff)) {\n const words = plaintiff.split(/\\s+/)\n const firstWord = words[0] ?? \"\"\n const firstWordClean = firstWord.toLowerCase().replace(/[.,']+$/, \"\")\n const firstWordIsProperName =\n /^[A-Z]/.test(firstWord) &&\n !PARTY_NAME_CONNECTORS.has(firstWordClean) &&\n !SENTENCE_INITIAL_WORDS.has(firstWordClean) &&\n INTERNAL_QUALIFIER_REGEX.test(plaintiff)\n if (!firstWordIsProperName) {\n // Check if the prefix starts with a signal word (See, See also, But see, etc.).\n // If so, keep it — extractPartyNames handles signal stripping downstream.\n const signalMatch = SIGNAL_STRIP_REGEX.exec(plaintiff)\n if (!signalMatch) {\n for (let i = 1; i < words.length; i++) {\n const candidate = words.slice(i).join(\" \")\n if (/^[A-Z]/.test(candidate) && isLikelyPartyName(candidate)) {\n // Compute offset from word positions rather than indexOf,\n // which could match the wrong position if a word repeats.\n const prefix = words.slice(0, i).join(\" \")\n trimOffset = prefix.length + 1\n plaintiff = candidate\n break\n }\n }\n }\n }\n }\n\n // Detect consolidated captions: vMatch[0] contains 2+ \"v.\" anchors.\n // The non-greedy regex defendant (group 2) is anchored at the trailing\n // \",$\" and so absorbs downstream comma-separated caption segments\n // including their own \"v.\" anchors (#222). Recovery: truncate the\n // defendant at its first comma to keep just the first \"X v. Y\" pair.\n const vAnchorMatches = vMatch[0].match(/\\bv(?:s)?\\.\\s/g)\n let defendantText = vMatch[2].trim()\n if (vAnchorMatches && vAnchorMatches.length >= 2) {\n const firstCommaInDefendant = vMatch[2].indexOf(\",\")\n if (firstCommaInDefendant !== -1) {\n defendantText = vMatch[2].substring(0, firstCommaInDefendant).trim()\n }\n }\n\n // Preserve the source's `v` punctuation form in `caseName`. New York\n // courts use `v` (no period); federal/most state courts use `v.`. The\n // existing V_CASE_NAME_REGEX accepts both via `v(?:s)?\\.?` — extract\n // whichever form actually appears in the matched text so the\n // assembled caseName is faithful to the source. #326\n const sepMatch = /\\bvs?\\.?(?=\\s)/.exec(vMatch[0])\n const sep = sepMatch?.[0] ?? \"v.\"\n\n const caseName = `${plaintiff} ${sep} ${defendantText}`\n const nameStart = adjustedSearchStart + vMatch.index + trimOffset\n // vMatch[3] = optional court text from the CSM year-first paren\n // (`Smith v. Jones (2d Cir. 2005)` — #293); vMatch[4] = the year\n // (`Smith v. Jones (1990)` — #19). Bluebook form leaves both undefined.\n // vMatch.indices[4] (enabled by `d` flag) gives the year position;\n // translate to cleanedText coordinates.\n const courtFromCsm = vMatch[3]?.trim()\n const year = vMatch[4] ? Number.parseInt(vMatch[4], 10) : undefined\n let yearStart: number | undefined\n let yearEnd: number | undefined\n if (year !== undefined && vMatch.indices?.[4]) {\n yearStart = adjustedSearchStart + vMatch.indices[4][0]\n yearEnd = adjustedSearchStart + vMatch.indices[4][1]\n }\n // CSM `(court year)` form (#293): synthesize a precedingDocketMeta so\n // the existing consumer at extractCase line ~2502 propagates court,\n // year, and date onto the citation. Skip when only year is present\n // (year-only handled by the dedicated `year`/`yearStart`/`yearEnd`\n // fields above).\n let csmDocketMeta = precedingDocketMeta\n if (!csmDocketMeta && courtFromCsm && year !== undefined && vMatch[4]) {\n csmDocketMeta = {\n court: courtFromCsm,\n year,\n date: { iso: vMatch[4], parsed: { year } },\n }\n }\n return {\n caseName,\n nameStart,\n year,\n yearStart,\n yearEnd,\n precedingDocketMeta: csmDocketMeta,\n }\n }\n }\n\n // Priority 2: Procedural prefixes (including Estate of, In the Matter of)\n const procMatch = PROCEDURAL_PREFIX_REGEX.exec(precedingText)\n if (procMatch) {\n // Check for semicolon in matched text (multi-citation separator)\n if (!procMatch[0].includes(\";\")) {\n const caseName = `${procMatch[1]} ${procMatch[2].trim()}`\n const nameStart = adjustedSearchStart + procMatch.index\n // procMatch[3] = optional court text from the CSM year-first paren\n // (`In re Cellphone (9th Cir. 2014)` — #293); procMatch[4] = the year\n // (`In re K.F. (2009)` — #19). Bluebook form leaves both undefined.\n const courtFromCsm = procMatch[3]?.trim()\n const year = procMatch[4] ? Number.parseInt(procMatch[4], 10) : undefined\n let yearStart: number | undefined\n let yearEnd: number | undefined\n if (year !== undefined && procMatch.indices?.[4]) {\n yearStart = adjustedSearchStart + procMatch.indices[4][0]\n yearEnd = adjustedSearchStart + procMatch.indices[4][1]\n }\n let csmDocketMeta = precedingDocketMeta\n if (!csmDocketMeta && courtFromCsm && year !== undefined && procMatch[4]) {\n csmDocketMeta = {\n court: courtFromCsm,\n year,\n date: { iso: procMatch[4], parsed: { year } },\n }\n }\n return {\n caseName,\n nameStart,\n year,\n yearStart,\n yearEnd,\n precedingDocketMeta: csmDocketMeta,\n }\n }\n }\n\n // Priority 3: Generic single-party caption (#193).\n //\n // V. and procedural-prefix scans failed. The precedingText is already\n // bounded by sentence/citation/paren-signal boundaries, so whatever\n // remains — typically a capitalized-words-only caption ending at \", \" —\n // is the caption candidate. Strip any leading signal word (See, cf., etc.)\n // and validate via isLikelyPartyName to filter out sentence prose.\n //\n // Handles single-party corporate captions like \"Board of Mgrs. of X\",\n // \"Board of Directors of X\", and unrecognized organizational prefixes\n // that don't fit PROCEDURAL_PREFIX_REGEX.\n const commaStrippedBody = precedingText.replace(/,\\s*$/, \"\")\n const leadingWsLen = commaStrippedBody.length - commaStrippedBody.trimStart().length\n let captionBody = commaStrippedBody.substring(leadingWsLen)\n let signalStripLen = 0\n const sigStripMatch = SIGNAL_STRIP_REGEX.exec(captionBody)\n if (sigStripMatch) {\n signalStripLen = sigStripMatch[0].length\n captionBody = captionBody.substring(signalStripLen)\n }\n const caption = captionBody.trim()\n\n if (caption.length > 0 && isLikelyPartyName(caption)) {\n const firstWord = caption.split(/\\s+/)[0] ?? \"\"\n const firstWordClean = firstWord.toLowerCase().replace(/[.,']+$/, \"\")\n if (!SENTENCE_INITIAL_WORDS.has(firstWordClean)) {\n // Skip multi-citation strings (joined by semicolons)\n if (!caption.includes(\";\")) {\n const nameStart = adjustedSearchStart + leadingWsLen + signalStripLen\n return { caseName: caption, nameStart, precedingDocketMeta }\n }\n }\n }\n\n return undefined\n}\n\n/** A raw parenthetical block extracted from text */\ninterface RawParenthetical {\n /** Content between the parentheses (excluding parens themselves) */\n text: string\n /** Position of opening '(' in the text */\n start: number\n /** Position after closing ')' in the text (exclusive) */\n end: number\n}\n\n/** A subsequent history signal found between parenthetical groups */\ninterface RawSignal {\n /** Raw signal text (e.g., \"aff'd\", \"cert. denied\") */\n text: string\n /** Normalized signal classification */\n normalized: HistorySignal\n /** Position of signal start in the text */\n start: number\n /** Position after signal end (exclusive) */\n end: number\n}\n\n/** Result of collecting parentheticals with signal awareness */\ninterface CollectedParentheticals {\n /** All parenthetical blocks in order */\n parens: RawParenthetical[]\n /** Signals found between groups, each paired with the index of the next paren */\n signals: Array<{ signal: RawSignal; nextParenIndex: number }>\n}\n\n/**\n * Collect all top-level parenthetical blocks starting from a position.\n * Uses depth tracking to handle nested parens. Continues scanning through\n * chained parentheticals and subsequent history signals.\n *\n * @param text - Full text to scan\n * @param startPos - Position to start scanning (typically after citation core)\n * @param maxLookahead - Maximum characters to scan forward (default 500)\n * @returns Collected parentheticals with associated signals\n */\nfunction collectParentheticals(\n text: string,\n startPos: number,\n maxLookahead = 500,\n): CollectedParentheticals {\n const parens: RawParenthetical[] = []\n const signals: CollectedParentheticals[\"signals\"] = []\n let pos = startPos\n const endLimit = Math.min(text.length, startPos + maxLookahead)\n let pendingSignal: RawSignal | undefined\n\n // Skip past any pincite text between core citation and parentheticals.\n // E.g., \", 199 n.2\" in \"982 N.W.2d 189, 199 n.2 (Minn. 2022)\".\n // This must happen before the main loop because pincite text includes\n // commas and digits that would otherwise block the scanner.\n const pinciteText = text.substring(pos, endLimit)\n const pinciteSkip = PINCITE_SKIP_REGEX.exec(pinciteText)\n if (pinciteSkip) {\n pos += pinciteSkip[0].length\n }\n\n while (pos < endLimit) {\n // Skip whitespace and commas between parentheticals\n while (pos < endLimit && PAREN_SKIP_REGEX.test(text[pos])) {\n pos++\n }\n\n if (pos >= endLimit || text[pos] !== \"(\") {\n // Check for subsequent history signal before giving up.\n // Normalize in-place to avoid a second SIGNAL_TABLE scan later.\n const remainingText = text.substring(pos, endLimit)\n const normalized = normalizeSignal(remainingText)\n if (normalized) {\n // Multi-stage chain (e.g., \"review granted, opinion vacated\"): if a\n // prior signal is still pending with no following paren, flush it\n // before overwriting. Without this, only the last link of a chain\n // survives. (#238)\n if (pendingSignal) {\n signals.push({ signal: pendingSignal, nextParenIndex: -1 })\n }\n pendingSignal = {\n text: remainingText.substring(0, normalized.matchLength).replace(/\\s+$/, \"\"),\n normalized: normalized.signal,\n start: pos,\n end: pos + normalized.matchLength,\n }\n pos += normalized.matchLength\n continue\n }\n break\n }\n\n // Found opening paren — track depth to find matching close\n const parenStart = pos\n let depth = 0\n const contentStart = pos + 1\n\n while (pos < endLimit) {\n const char = text[pos]\n if (char === \"(\") {\n depth++\n } else if (char === \")\") {\n depth--\n if (depth === 0) {\n pos++ // move past closing paren\n const content = text.substring(contentStart, pos - 1).trim()\n if (content.length > 0) {\n parens.push({ text: content, start: parenStart, end: pos })\n // If there was a pending signal, associate it with this paren\n if (pendingSignal) {\n signals.push({ signal: pendingSignal, nextParenIndex: parens.length - 1 })\n pendingSignal = undefined\n }\n }\n break\n }\n }\n pos++\n }\n\n // If we never closed the paren, stop\n if (depth > 0) break\n }\n\n // Handle trailing signal with no following paren\n if (pendingSignal) {\n signals.push({ signal: pendingSignal, nextParenIndex: -1 })\n }\n\n return { parens, signals }\n}\n\n/**\n * Parse parenthetical content to extract court, year, date, and disposition.\n * Unified parser replacing the old year-only logic.\n *\n * @param content - Parenthetical content (without the parens themselves)\n * @returns Structured parenthetical data\n *\n * @example\n * ```typescript\n * parseParenthetical(\"9th Cir. 2020\")\n * // Returns: { court: \"9th Cir.\", year: 2020, date: { iso: \"2020\", parsed: { year: 2020 } } }\n *\n * parseParenthetical(\"2d Cir. Jan. 15, 2020\")\n * // Returns: { court: \"2d Cir.\", year: 2020, date: { iso: \"2020-01-15\", parsed: { year: 2020, month: 1, day: 15 } } }\n *\n * parseParenthetical(\"en banc\")\n * // Returns: { disposition: \"en banc\" }\n * ```\n */\nexport function parseParenthetical(content: string): {\n court?: string\n year?: number\n date?: StructuredDate\n disposition?: string\n /** Surname(s) of justice(s) attributed to a justice-attribution paren (#235) */\n justices?: string[]\n /** Scope qualifier for a justice-attribution paren (#235): in_judgment | in_part | from_denial */\n scope?: string\n /** Texas Greenbook writ/petition history clause inside the parenthetical (#229) */\n internalHistory?: { signal: HistorySignal; rawSignal: string; start: number; end: number }\n courtStart?: number\n courtEnd?: number\n yearStart?: number\n yearEnd?: number\n} {\n const result: {\n court?: string\n year?: number\n date?: StructuredDate\n disposition?: string\n justices?: string[]\n scope?: string\n internalHistory?: {\n signal: HistorySignal\n rawSignal: string\n start: number\n end: number\n }\n courtStart?: number\n courtEnd?: number\n yearStart?: number\n yearEnd?: number\n } = {}\n\n // Parse structured date using dates.ts\n const dateResult = parseDate(content)\n if (dateResult) {\n result.date = dateResult\n result.year = dateResult.parsed.year\n }\n\n // Texas writ/pet history: detect trailing \",\\s*<signal>\" clause after year\n // (e.g., \"Tex. App.—Dallas 2010, writ ref'd n.r.e.\"). Strip the clause from\n // the working content so stripDateFromCourt sees the conventional shape.\n let workingContent = content\n if (result.year) {\n const yearStr = String(result.year)\n const yearIdx = content.lastIndexOf(yearStr)\n if (yearIdx !== -1) {\n const afterYearStart = yearIdx + yearStr.length\n const afterYear = content.substring(afterYearStart)\n const trailing = /^\\s*,\\s*(.+?)\\s*$/.exec(afterYear)\n if (trailing) {\n const sigText = trailing[1]\n const normalized = normalizeSignal(sigText)\n if (normalized) {\n const rawSignal = sigText.substring(0, normalized.matchLength)\n // Compute the absolute offset of the signal text within the content.\n const sigOffset = content.indexOf(rawSignal, afterYearStart)\n result.internalHistory = {\n signal: normalized.signal,\n rawSignal,\n start: sigOffset !== -1 ? sigOffset : afterYearStart,\n end:\n (sigOffset !== -1 ? sigOffset : afterYearStart) + rawSignal.length,\n }\n workingContent = content.substring(0, afterYearStart)\n }\n }\n }\n }\n\n // Extract court (strips date components) — runs on workingContent so the\n // Texas trailing-history clause does not interfere with date-end detection.\n const courtResult = stripDateFromCourt(workingContent)\n if (courtResult) {\n result.court = courtResult\n const courtIdx = content.indexOf(courtResult)\n if (courtIdx !== -1) {\n result.courtStart = courtIdx\n result.courtEnd = courtIdx + courtResult.length\n }\n }\n\n // Year offset within parenthetical content\n if (result.year) {\n const yearStr = String(result.year)\n const yearIdx = content.lastIndexOf(yearStr)\n if (yearIdx !== -1) {\n result.yearStart = yearIdx\n result.yearEnd = yearIdx + yearStr.length\n }\n }\n\n // Justice-attribution parenthetical (#235). Detected BEFORE the bare\n // en banc / per curiam check so a parenthetical like\n // `Cabranes, J., dissenting from denial of rehearing en banc` doesn't\n // false-positive on the trailing `en banc` substring.\n //\n // Pattern: <Surname>(, <Surname>)*(?:,? and <Surname>)?,? (C\\.J\\.|J\\.|JJ\\.),? <role>\n const justiceMatch = /^(?<surnames>[A-Z][a-z]+(?:(?:,\\s+|\\s+and\\s+)[A-Z][a-z]+)*)\\s*,?\\s*(?<title>C\\.J\\.|J\\.|JJ\\.)\\s*,?\\s*(?<role>.+)$/.exec(\n content.trim(),\n )\n if (justiceMatch?.groups) {\n const surnameText = justiceMatch.groups.surnames\n const roleText = justiceMatch.groups.role.trim().replace(/[.,]+$/, \"\")\n const justices = surnameText\n .split(/(?:,\\s+and\\s+|,\\s+|\\s+and\\s+)/)\n .map((s) => s.trim())\n .filter(Boolean)\n result.justices = justices\n\n // Classify the role into a disposition + optional scope.\n const lower = roleText.toLowerCase()\n if (/^concurring\\s+in\\s+part\\s+and\\s+dissenting\\s+in\\s+part/.test(lower)) {\n result.disposition = \"mixed\"\n result.scope = \"in_part\"\n } else if (/^concurring\\s+in\\s+the\\s+judgment/.test(lower)) {\n result.disposition = \"concurrence\"\n result.scope = \"in_judgment\"\n } else if (/^concurring\\s+in\\s+part/.test(lower)) {\n result.disposition = \"concurrence\"\n result.scope = \"in_part\"\n } else if (/^dissenting\\s+in\\s+part/.test(lower)) {\n result.disposition = \"dissent\"\n result.scope = \"in_part\"\n } else if (/^dissenting\\s+from\\s+denial\\s+of/.test(lower)) {\n result.disposition = \"dissent\"\n result.scope = \"from_denial\"\n } else if (/^concurring/.test(lower)) {\n result.disposition = \"concurrence\"\n } else if (/^dissenting/.test(lower)) {\n result.disposition = \"dissent\"\n } else if (/^joining/.test(lower)) {\n result.disposition = \"majority\"\n }\n return result\n }\n\n // Non-justice disposition parens (#235): plurality opinion, mem.,\n // unpublished table decision. Checked before en banc/per curiam.\n if (/^plurality\\s+opinion\\b/i.test(content.trim())) {\n result.disposition = \"plurality opinion\"\n return result\n }\n if (/^mem\\.\\s*$/i.test(content.trim())) {\n result.disposition = \"mem.\"\n return result\n }\n if (/^unpublished\\s+table\\s+decision\\b/i.test(content.trim())) {\n result.disposition = \"unpublished table decision\"\n return result\n }\n\n // Check for disposition (en banc / in bank / per curiam). Anchored at\n // content end (\\s*$) so a parenthetical like `Cabranes, J., dissenting from\n // denial of rehearing en banc` — caught above by the justice-attribution\n // branch — does not also trip the en-banc check via substring match (#235).\n // `(in bank)` is the California Supreme Court's equivalent of `(en banc)`\n // — added as a separate disposition value to preserve the CA distinction.\n if (/\\ben banc\\b\\s*$/i.test(content.trim())) {\n result.disposition = \"en banc\"\n } else if (/\\bin bank\\b\\s*$/i.test(content.trim())) {\n result.disposition = \"in bank\"\n } else if (/\\bper curiam\\b\\s*$/i.test(content.trim())) {\n result.disposition = \"per curiam\"\n }\n\n return result\n}\n\n/**\n * Classify a raw parenthetical block as metadata or explanatory.\n *\n * @param raw - Raw parenthetical text (content between parens)\n * @returns Classification result with kind discriminator\n */\nfunction classifyParenthetical(raw: string):\n | {\n kind: \"metadata\"\n court?: string\n year?: number\n date?: StructuredDate\n disposition?: string\n justices?: string[]\n scope?: string\n }\n | {\n kind: \"explanatory\"\n text: string\n type: ParentheticalType\n } {\n // Check for signal word first — signal-word parens are always explanatory\n const leadingMatch = LEADING_WORD_REGEX.exec(raw)\n if (leadingMatch) {\n const candidate = leadingMatch[1].toLowerCase()\n if (isSignalWord(candidate)) {\n return { kind: \"explanatory\", text: raw, type: candidate }\n }\n }\n\n // Try metadata parse: court, year, date, disposition\n // Note: \"other\"-type parens with embedded years (e.g., \"the court, in 2019, held X\")\n // will be classified as metadata. This is a known limitation — most explanatory\n // parentheticals start with a signal word and are handled above.\n // Note: meta.court alone is insufficient — stripDateFromCourt returns any\n // text with letters as a \"court\", so a standalone court-only second paren\n // like \"(9th Cir.)\" will fall through to \"other\". This is acceptable since\n // court-only parens without year/date are extremely rare in legal text.\n const meta = parseParenthetical(raw)\n if (meta.year || meta.date || meta.disposition || meta.justices) {\n return { kind: \"metadata\", ...meta }\n }\n\n // No signal word and no metadata — classify as \"other\" explanatory\n return { kind: \"explanatory\", text: raw, type: \"other\" }\n}\n\n/**\n * Normalize party name for matching by removing legal noise.\n * Normalization pipeline:\n * 1. Strip \"et al.\" (case-insensitive)\n * 2. Strip slash-aliases \"d/b/a\", \"f/k/a\", \"n/k/a\", \"a/k/a\" and everything after\n * 3. Strip \"aka\" and everything after (case-insensitive, word boundary)\n * 4. Strip trailing corporate suffixes (Inc., LLC, Corp., Ltd., Co., LLP, LP, P.C.) - iterative\n * 5. Strip leading articles (The, A, An)\n * 6. Normalize whitespace\n * 7. Trim and lowercase\n *\n * @param name - Raw party name\n * @returns Normalized party name\n *\n * @example\n * ```typescript\n * normalizePartyName(\"The Smith Corp., Inc.\") // \"smith\"\n * normalizePartyName(\"Doe et al.\") // \"doe\"\n * normalizePartyName(\"United States\") // \"united states\" (not stripped)\n * ```\n */\nfunction normalizePartyName(name: string): string {\n let normalized = name\n\n // Strip \"et al.\" (with or without period, case-insensitive)\n normalized = normalized.replace(/\\bet\\s+al\\.?/gi, \"\")\n\n // Strip slash-alias variants (\"d/b/a\", \"f/k/a\", \"n/k/a\", \"a/k/a\") and\n // everything after them. Matches the slash forms produced by Bluebook-style\n // captions; the non-slash \"aka\" form is handled below (#240).\n normalized = normalized.replace(/\\s+(?:d\\/b\\/a|[fna]\\/k\\/a)\\b.*/gi, \"\")\n\n // Strip \"aka\" and everything after it (case-insensitive, word boundary)\n normalized = normalized.replace(/\\s+aka\\b.*/gi, \"\")\n\n // Strip trailing corporate suffixes (with or without trailing period, handle comma)\n // Repeat to handle multiple suffixes like \"Corp., Inc.\"\n let prev = \"\"\n while (prev !== normalized) {\n prev = normalized\n normalized = normalized.replace(/,?\\s*(Inc|LLC|Corp|Ltd|Co|LLP|LP|P\\.C)\\.?$/gi, \"\")\n }\n\n // Strip leading articles (only at start)\n normalized = normalized.replace(/^(The|A|An)\\s+/i, \"\")\n\n // Normalize whitespace (collapse multiple spaces)\n normalized = normalized.replace(/\\s+/g, \" \")\n\n // Trim and lowercase\n return normalized.trim().toLowerCase()\n}\n\n/**\n * Extract plaintiff and defendant party names from case name.\n * Handles adversarial cases (v.) and procedural prefixes (In re, Ex parte, etc.).\n *\n * @param caseName - Case name string\n * @returns Party name data with raw and normalized fields\n *\n * @example\n * ```typescript\n * extractPartyNames(\"Smith v. Jones\")\n * // Returns: { plaintiff: \"Smith\", plaintiffNormalized: \"smith\", defendant: \"Jones\", defendantNormalized: \"jones\" }\n *\n * extractPartyNames(\"In re Smith\")\n * // Returns: { plaintiff: \"In re Smith\", plaintiffNormalized: \"smith\", proceduralPrefix: \"In re\" }\n *\n * extractPartyNames(\"People v. Smith\")\n * // Returns: { plaintiff: \"People\", plaintiffNormalized: \"people\", defendant: \"Smith\", defendantNormalized: \"smith\" }\n * ```\n */\nexport function extractPartyNames(caseName: string): {\n plaintiff?: string\n plaintiffNormalized?: string\n defendant?: string\n defendantNormalized?: string\n proceduralPrefix?: string\n signal?: CitationSignal\n /** Bankruptcy adversary admin parenthetical (#241), e.g., \"In re Hintze\". */\n adminParenthetical?: string\n} {\n let signal: CitationSignal | undefined\n // Procedural prefix patterns (anchored to start, case-insensitive).\n // Longer prefixes first so the for-loop's `prefixRegex.exec(caseName)` finds\n // the most specific match. Six 2026-05-11 cross-domain research dispatches\n // (family, probate, bankruptcy, immigration, criminal/habeas, ex rel./qui tam)\n // identified the additions; corpus-sourced examples live in\n // `docs/research/2026-05-11-procedural-prefixes-*.md`.\n const proceduralPrefixes = [\n // \"In the Matter of the X of\" cluster — must precede \"In the Matter of\"\n \"In the Matter of the Liquidation of\",\n \"In the Matter of the Rehabilitation of\",\n \"In the Matter of the Receivership of\",\n \"In the Matter of the Extradition of\",\n \"In the Matter of the Application of\",\n \"In the Matter of the Welfare of\",\n \"In the Matter of\",\n // \"In re X of\" cluster — must precede \"In re\"\n \"In re Petition for Naturalization of\",\n \"In re Termination of Parental Rights as to\",\n \"In re Termination of Parental Rights to\",\n \"In re Termination of Parental Rights of\",\n \"In re Marriage of\",\n \"In re Liquidation of\",\n \"In re Rehabilitation of\",\n \"In re Receivership of\",\n \"In re Naturalization of\",\n \"In re Extradition of\",\n \"In re Application of\",\n \"In re Welfare of\",\n \"In re Dependency of\",\n \"In re Paternity of\",\n \"In re Parentage of\",\n // CA Tier 1 — In re precision upgrades for conservatorship/guardianship/adoption\n \"In re Conservatorship of\",\n \"In re Guardianship of\",\n \"In re Adoption of\",\n \"In the Interest of\",\n \"In re\",\n \"Ex parte\",\n // \"Matter of X of\" cluster — must precede \"Matter of\"\n \"Matter of Liquidation of\",\n \"Matter of Rehabilitation of\",\n \"Matter of\",\n // Sovereign ex rel. — long forms precede short forms\n \"Commonwealth of Puerto Rico ex rel.\",\n \"Government of the Virgin Islands ex rel.\",\n \"Commonwealth ex rel.\",\n \"State ex rel.\",\n \"United States ex rel.\",\n \"People ex rel.\",\n \"District of Columbia ex rel.\",\n // Petition variants — \"Petition for Naturalization of\" precedes \"Petition of\"\n \"Petition for Naturalization of\",\n \"Application of\",\n \"On Petition of\",\n \"Petition of\",\n // Other \"X of\" forms\n \"Adoption of\",\n // CA Tier 1 — Conservatorship extended forms must precede bare \"Conservatorship of\"\n \"Conservatorship of the Person and Estate of\",\n \"Conservatorship of the Person of\",\n \"Conservatorship of the Estate of\",\n \"Conservatorship of\",\n \"Guardianship of\",\n \"Estate of\",\n // Bare forms with no \"In re\" prefix (no alternation-ordering collisions)\n \"Care and Protection of\",\n \"Succession of\",\n // CA Tier 1 — agency / discipline procedural prefixes (2026-05-11)\n \"Inquiry Concerning Judge\",\n \"Appeal of\",\n ]\n\n // Check for procedural prefix first\n for (const prefix of proceduralPrefixes) {\n const prefixRegex = new RegExp(`^(${prefix})\\\\s+(.+)$`, \"i\")\n const match = prefixRegex.exec(caseName)\n if (match) {\n const matchedPrefix = match[1]\n const subject = match[2]\n\n // Check if there's a \"v.\" after the prefix (adversarial case)\n if (/\\s+vs?\\.?\\s+/i.test(subject)) {\n // Adversarial case with procedural-looking plaintiff (e.g., \"Estate of X v. Y\")\n // Split on \"v.\"\n const vMatch = /^(.+?)\\s+vs?\\.?\\s+(.+)$/i.exec(caseName)\n if (vMatch) {\n const plaintiff = vMatch[1].trim()\n const defendant = vMatch[2].trim()\n return {\n plaintiff,\n plaintiffNormalized: normalizePartyName(plaintiff),\n defendant,\n defendantNormalized: normalizePartyName(defendant),\n }\n }\n } else {\n // Pure procedural (no \"v.\")\n return {\n plaintiff: caseName,\n plaintiffNormalized: normalizePartyName(subject),\n proceduralPrefix: matchedPrefix,\n }\n }\n }\n }\n\n // Split on \"v.\" for adversarial cases\n const vRegex = /^(.+?)\\s+vs?\\.?\\s+(.+)$/i\n const vMatch = vRegex.exec(caseName)\n if (vMatch) {\n let plaintiff = vMatch[1].trim()\n let defendant = vMatch[2].trim()\n\n // Bankruptcy adversary admin parenthetical (#241): trailing\n // `(In re <Debtor>)` immediately after the defendant identifies the\n // underlying bankruptcy debtor. Strip from defendant; expose separately\n // via `adminParenthetical`. The leading \"In re\" anchor distinguishes the\n // adversary admin form from explanatory parens which appear *after* the\n // citation core, not inside the case name.\n let adminParenthetical: string | undefined\n const adminMatch = /\\s*\\(\\s*(In\\s+re\\s+[^)]+?)\\s*\\)\\s*$/i.exec(defendant)\n if (adminMatch) {\n adminParenthetical = adminMatch[1]\n defendant = defendant.substring(0, adminMatch.index).trim()\n }\n\n // Strip signal words from plaintiff (e.g., \"See Jones\" → \"Jones\")\n // Uses SIGNAL_STRIP_REGEX derived from VALID_SIGNALS for single source of truth.\n // Also strips \"Also\" and \"In\" (not valid signals) that can precede party names.\n const signalMatch =\n plaintiff.match(SIGNAL_STRIP_REGEX) ?? plaintiff.match(/^(Also|In(?!\\s+re\\b))\\s+/i)\n if (signalMatch) {\n // Guard against false-positive signal capture from over-greedy\n // case-name extraction (#304). When the V_CASE_NAME_REGEX captures\n // sentence prose like `Contra plaintiff's argument, Bolling v. Sharpe`,\n // the leading `Contra` looks like a Bluebook signal — but the next\n // token is lowercase prose, not a capitalized party name. Only strip\n // the signal when the remainder after stripping starts with a capital\n // letter (real case-name context) so we don't manufacture phantom\n // signals from sentence-internal English.\n const remainderAfterStrip = plaintiff.substring(signalMatch[0].length).trimStart()\n const firstChar = remainderAfterStrip[0] ?? \"\"\n const remainderIsCaseNameLike = firstChar >= \"A\" && firstChar <= \"Z\"\n if (remainderIsCaseNameLike) {\n const lowered = signalMatch[1].toLowerCase()\n // Combined `, e.g.` signals end with a period that is part of the canonical\n // form (e.g., \"see, e.g.\"); strip the trailing period only if the lowered\n // form isn't itself a valid signal (handles \"Cf.\" → \"cf\" without breaking\n // \"see, e.g.\" → \"see, e.g.\").\n if (VALID_SIGNALS.has(lowered)) {\n signal = lowered as CitationSignal\n } else {\n const stripped = lowered.replace(/\\.$/, \"\")\n if (VALID_SIGNALS.has(stripped)) {\n signal = stripped as CitationSignal\n }\n }\n plaintiff = plaintiff.substring(signalMatch[0].length).trim()\n }\n }\n\n return {\n plaintiff: plaintiff || vMatch[1].trim(), // Fallback to original if strip leaves nothing\n plaintiffNormalized: normalizePartyName(plaintiff || vMatch[1].trim()),\n defendant,\n defendantNormalized: normalizePartyName(defendant),\n signal,\n ...(adminParenthetical ? { adminParenthetical } : {}),\n }\n }\n\n // No \"v.\" and no procedural prefix - no parties extracted\n return {}\n}\n\n/**\n * Extracts case citation metadata from a tokenized citation.\n *\n * Parses token text to extract:\n * - Volume: Leading digits (e.g., \"500\" from \"500 F.2d 123\")\n * - Reporter: Alphabetic abbreviation (e.g., \"F.2d\")\n * - Page: Trailing digits after reporter (e.g., \"123\")\n * - Pincite: Optional page reference after comma (e.g., \", 125\")\n * - Court: Optional court abbreviation in parentheses (e.g., \"(9th Cir.)\")\n * - Year: Optional year in parentheses (e.g., \"(2020)\")\n *\n * Confidence scoring:\n * - Base: 0.5\n * - Common reporter pattern (F., U.S., etc.): +0.3\n * - Valid year (not future): +0.2\n * - Capped at 1.0\n *\n * Position translation:\n * - Uses TransformationMap to convert clean positions → original positions\n * - cleanStart/cleanEnd from token span\n * - originalStart/originalEnd via transformationMap.cleanToOriginal\n *\n * Note: This function does NOT validate against reporters-db. That happens\n * in Phase 3 (resolution layer). Phase 2 extraction only parses structure.\n *\n * @param token - Token from tokenizer containing matched text and clean positions\n * @param transformationMap - Position mapping from clean → original text\n * @returns FullCaseCitation with parsed metadata and translated positions\n *\n * @example\n * ```typescript\n * const token = {\n * text: \"500 F.2d 123, 125\",\n * span: { cleanStart: 10, cleanEnd: 27 },\n * type: \"case\",\n * patternId: \"federal-reporter\"\n * }\n * const citation = extractCase(token, transformationMap)\n * // citation = {\n * // type: \"case\",\n * // text: \"500 F.2d 123, 125\",\n * // volume: 500,\n * // reporter: \"F.2d\",\n * // page: 123,\n * // pincite: 125,\n * // span: { cleanStart: 10, cleanEnd: 27, originalStart: 10, originalEnd: 27 },\n * // confidence: 0.8,\n * // ...\n * // }\n * ```\n */\nexport function extractCase(\n token: Token,\n transformationMap: TransformationMap,\n cleanedText?: string,\n originalText?: string,\n /** Clean-coordinate spans of sibling tokens. Used to:\n * - bound the case-name backward walk so a parallel cite's caption is\n * not absorbed into this cite's caseName,\n * - skip past a contiguous parallel-cite chain (`, 198 A. 154, 35 L.Ed.2d 147`)\n * when searching for the shared trailing year parenthetical so each\n * cite in the chain gets year/court populated. */\n siblings?: ReadonlyArray<{ cleanStart: number; cleanEnd: number }>,\n): FullCaseCitation {\n const { text, span } = token\n\n // Parse volume-reporter-page using regex\n // Pattern: volume (digits) + reporter (letters/periods/spaces/numbers) + page (digits or blank placeholder)\n // Use greedy matching for reporter to capture full abbreviation including spaces\n const match = VOLUME_REPORTER_PAGE_REGEX.exec(text)\n\n if (!match) {\n // Fallback if pattern doesn't match (shouldn't happen if tokenizer is correct)\n throw new Error(`Failed to parse case citation: ${text}`)\n }\n\n const volume = parseVolume(match[1])\n const reporter = match[2].trim()\n\n // Extract nominative reporter if present (e.g., \"1 Cranch\" from \"5 U.S. (1 Cranch) 137\")\n const nominativeVolume = match[3] ? Number.parseInt(match[3], 10) : undefined\n const nominativeReporter = match[4] || undefined\n\n // Check if page is a blank placeholder (group 5 after nominative groups)\n const pageStr = match[5]\n const isBlankPage = BLANK_PAGE_REGEX.test(pageStr)\n const page = isBlankPage ? undefined : Number.parseInt(pageStr, 10)\n const hasBlankPage = isBlankPage ? true : undefined\n\n // Extract optional pincite (page reference after comma).\n // Pattern: \", digits\" (e.g., \", 125\") or \", at *N\" (star-pagination, #191).\n // Route the numeric part through parsePincite so star-page rawText (\"*2\")\n // doesn't blow up Number.parseInt.\n const pinciteMatch = PINCITE_REGEX.exec(text)\n let pinciteInfo: PinciteInfo | undefined = pinciteMatch\n ? (parsePincite(pinciteMatch[1]) ?? undefined)\n : undefined\n let pincite = pinciteInfo?.page\n\n // Initialize component spans for core regex-extracted fields\n const spans: CaseComponentSpans = {}\n\n if (match.indices) {\n // Group 1 = volume, Group 2 = reporter, Group 5 = page\n // Groups 3, 4 are optional nominative reporter (not tracked here)\n if (match.indices[1]) {\n spans.volume = spanFromGroupIndex(span.cleanStart, match.indices[1], transformationMap)\n }\n if (match.indices[2]) {\n // Trim whitespace from reporter span to match the trimmed reporter value\n const [rStart, rEnd] = match.indices[2]\n const rawReporter = text.substring(rStart, rEnd)\n const leadTrim = rawReporter.length - rawReporter.trimStart().length\n const trailTrim = rawReporter.length - rawReporter.trimEnd().length\n spans.reporter = spanFromGroupIndex(\n span.cleanStart,\n [rStart + leadTrim, rEnd - trailTrim],\n transformationMap,\n )\n }\n if (match.indices[5]) {\n spans.page = spanFromGroupIndex(span.cleanStart, match.indices[5], transformationMap)\n }\n }\n\n // Pincite span (from the token-level pincite match)\n if (pinciteMatch?.indices?.[1]) {\n spans.pincite = spanFromGroupIndex(span.cleanStart, pinciteMatch.indices[1], transformationMap)\n }\n\n // Initialize Phase 6 fields\n let year: number | undefined\n let court: string | undefined\n let date: StructuredDate | undefined\n let disposition: string | undefined\n let justices: string[] | undefined\n let scope: string | undefined\n let caseName: string | undefined\n let fullSpan: Span | undefined\n\n // Extract parenthetical from token text\n let parentheticalContent: string | undefined\n // Shared parenResult for court/year span computation (used by both code paths)\n let metaParenResult: ReturnType<typeof parseParenthetical> | undefined\n // Whether the metadata paren was found in token text (vs lookahead)\n let metaParenFromToken = false\n // Match any parenthetical (with or without letters)\n // When a nominative reporter is present, the first paren in token text is the\n // nominative (e.g., \"(2 Black)\") — skip it so the year/court look-ahead runs.\n const parenMatch = PAREN_REGEX.exec(text)\n if (parenMatch && !nominativeVolume) {\n parentheticalContent = parenMatch[1]\n // Parse parenthetical using unified parser\n metaParenResult = parseParenthetical(parentheticalContent)\n metaParenFromToken = true\n year = metaParenResult.year\n court = metaParenResult.court\n date = metaParenResult.date\n disposition = metaParenResult.disposition\n justices = metaParenResult.justices\n scope = metaParenResult.scope\n }\n\n // NY Slip Op unpublished marker (#231): `(U)` (older) or `[U]` (newer)\n // appears immediately after the page number and must be consumed *before*\n // LOOKAHEAD_PAREN_REGEX runs, otherwise the regex captures `(U)` as the\n // court parenthetical and produces `court = \"U\"`. Detected once and used\n // both in the in-token paren path and the lookahead path.\n let unpublished = false\n if (cleanedText) {\n const afterTokenForFlag = cleanedText.substring(span.cleanEnd)\n if (/^\\s*(?:\\(U\\)|\\[U\\])/.test(afterTokenForFlag)) {\n unpublished = true\n }\n }\n\n // Parallel-cite chain skip: when this cite is followed by another citation\n // separated only by parallel-chain junk (commas, whitespace, digit/dash\n // runs for intervening pincites), the shared trailing parenthetical sits\n // AFTER the last cite in the chain — e.g., `329 Pa. 256, 198 A. 154 (1938)`\n // or `410 U.S. 113, 117, 93 S. Ct. 705 (1973)` where the `, 117,` is the\n // first cite's pincite. Compute the post-chain start position once and\n // share it between the look-ahead paren scan and `collectParentheticals`\n // so the trailing year paren is found AND fullSpan extends through it.\n let postChainStart = span.cleanEnd\n if (cleanedText && siblings && siblings.length > 0) {\n const CHAIN_BRIDGE_REGEX = /^[\\s,\\d\\-–—]*$/\n while (true) {\n const next = siblings.find(\n (s) =>\n s.cleanStart > postChainStart &&\n CHAIN_BRIDGE_REGEX.test(\n cleanedText.substring(postChainStart, s.cleanStart),\n ),\n )\n if (!next) break\n postChainStart = next.cleanEnd\n }\n }\n\n // Look ahead in cleaned text for parenthetical after the token\n // Tokenization patterns only capture volume-reporter-page, so parentheticals\n // like \"(1989)\" or \"(9th Cir. 2020)\" are not in the token text.\n if (cleanedText && !parentheticalContent) {\n // The pincite scan below must still operate on the original afterToken\n // (starting at span.cleanEnd) so `, 117` is parseable as this cite's\n // pincite; the paren scan uses the post-chain window instead.\n const afterToken = cleanedText.substring(span.cleanEnd)\n let parenAfterToken =\n postChainStart === span.cleanEnd\n ? afterToken\n : cleanedText.substring(postChainStart)\n // Consume any leading (U)/[U] marker so the real court paren is found.\n const unpubMatch = /^\\s*(?:\\(U\\)|\\[U\\])/.exec(parenAfterToken)\n if (unpubMatch) {\n parenAfterToken = parenAfterToken.substring(unpubMatch[0].length)\n }\n const lookAheadMatch = LOOKAHEAD_PAREN_REGEX.exec(parenAfterToken)\n if (lookAheadMatch) {\n parentheticalContent = lookAheadMatch[1]\n // Parse parenthetical using unified parser\n metaParenResult = parseParenthetical(parentheticalContent)\n metaParenFromToken = false\n year = metaParenResult.year\n court = metaParenResult.court\n date = metaParenResult.date\n disposition = metaParenResult.disposition\n justices = metaParenResult.justices\n scope = metaParenResult.scope\n }\n\n // Extract pincite from look-ahead independently of the parenthetical match.\n // A citation can carry a pincite without a trailing court/year parenthetical,\n // e.g. \"2020 NY Slip Op 00001 at *2.\" — the second occurrence is classified\n // as a full-case cite (because shortFormCase requires no page between reporter\n // and \"at\"), but the pincite is still meaningful data. See #191.\n if (pincite === undefined) {\n const laPinciteMatch = LOOKAHEAD_PINCITE_REGEX.exec(afterToken)\n if (laPinciteMatch) {\n if (!pinciteInfo) {\n pinciteInfo = parsePincite(laPinciteMatch[1]) ?? undefined\n }\n pincite = pinciteInfo?.page\n // Pincite span: indices are relative to afterToken (which starts at span.cleanEnd)\n if (laPinciteMatch.indices?.[1]) {\n spans.pincite = spanFromGroupIndex(\n span.cleanEnd,\n laPinciteMatch.indices[1],\n transformationMap,\n )\n }\n\n // Multiple discrete pincites (#247): continue scanning for additional\n // comma-separated pincites (`, 115, 153, 200`). Each entry is parsed\n // through `parsePincite` so range / footnote / paragraph semantics\n // inside the chain are preserved. The convenience `pincite` field\n // continues to point at the primary; consumers walk `additionalPincites`.\n if (pinciteInfo) {\n const additionalPincites: PinciteInfo[] = []\n let scanStart =\n (laPinciteMatch.index ?? 0) + laPinciteMatch[0].length\n while (scanStart < afterToken.length) {\n const remainder = afterToken.substring(scanStart)\n const addMatch = ADDITIONAL_PINCITE_REGEX.exec(remainder)\n if (!addMatch) break\n const addInfo = parsePincite(addMatch[1])\n if (!addInfo) break\n additionalPincites.push(addInfo)\n scanStart += addMatch[0].length\n }\n if (additionalPincites.length > 0) {\n pinciteInfo = { ...pinciteInfo, additionalPincites }\n }\n }\n }\n }\n }\n\n // Classify chained parentheticals: extract disposition and explanatory content\n let parentheticals: Parenthetical[] | undefined\n let allParens: RawParenthetical[] | undefined\n let collected: CollectedParentheticals | undefined\n if (cleanedText) {\n // Use postChainStart so fullSpan / chained-paren classification can see\n // the shared trailing paren that sits past a parallel-cite chain.\n collected = collectParentheticals(cleanedText, postChainStart)\n allParens = collected.parens\n // Skip first paren (already parsed above as court/year)\n const remaining = parentheticalContent ? allParens.slice(1) : allParens\n for (const raw of remaining) {\n const classified = classifyParenthetical(raw.text)\n if (classified.kind === \"metadata\") {\n // Accept court from later metadata parens if we don't have a real one.\n // The primary parse can set court to the disposition text (e.g., \"en banc\")\n // as a side effect of stripDateFromCourt, so treat that as unset.\n if (classified.court && (!court || court === disposition)) {\n court = classified.court\n }\n if (classified.year && !year) {\n year = classified.year\n date = classified.date\n }\n if (classified.disposition && !disposition) {\n disposition = classified.disposition\n }\n if (classified.justices && !justices) {\n justices = classified.justices\n }\n if (classified.scope && !scope) {\n scope = classified.scope\n }\n } else {\n parentheticals ??= []\n const parenOrig = resolveOriginalSpan(\n { cleanStart: raw.start, cleanEnd: raw.end },\n transformationMap,\n )\n parentheticals.push({\n text: classified.text,\n type: classified.type,\n span: {\n cleanStart: raw.start,\n cleanEnd: raw.end,\n originalStart: parenOrig.originalStart,\n originalEnd: parenOrig.originalEnd,\n },\n })\n }\n }\n }\n\n // Metadata parenthetical span (the first paren that yielded court/year)\n if (allParens && allParens.length > 0 && (court || year)) {\n const metaParen = parentheticalContent ? allParens[0] : undefined\n if (metaParen) {\n const metaOrig = resolveOriginalSpan(\n { cleanStart: metaParen.start, cleanEnd: metaParen.end },\n transformationMap,\n )\n spans.metadataParenthetical = {\n cleanStart: metaParen.start,\n cleanEnd: metaParen.end,\n originalStart: metaOrig.originalStart,\n originalEnd: metaOrig.originalEnd,\n }\n\n // Court and year spans from parseParenthetical content offsets.\n // The content starts at metaParen.start + 1 (past the opening \"(\").\n if (metaParenResult) {\n const contentStart = metaParen.start + 1\n if (metaParenResult.courtStart !== undefined) {\n const courtCS = contentStart + metaParenResult.courtStart\n const courtCE = contentStart + metaParenResult.courtEnd!\n const courtOrig = resolveOriginalSpan(\n { cleanStart: courtCS, cleanEnd: courtCE },\n transformationMap,\n )\n spans.court = {\n cleanStart: courtCS,\n cleanEnd: courtCE,\n originalStart: courtOrig.originalStart,\n originalEnd: courtOrig.originalEnd,\n }\n }\n if (metaParenResult.yearStart !== undefined) {\n const yearCS = contentStart + metaParenResult.yearStart\n const yearCE = contentStart + metaParenResult.yearEnd!\n const yearOrig = resolveOriginalSpan(\n { cleanStart: yearCS, cleanEnd: yearCE },\n transformationMap,\n )\n spans.year = {\n cleanStart: yearCS,\n cleanEnd: yearCE,\n originalStart: yearOrig.originalStart,\n originalEnd: yearOrig.originalEnd,\n }\n }\n }\n }\n }\n\n // Build subsequentHistoryEntries from captured signals (already normalized\n // during collection to avoid a second SIGNAL_TABLE scan).\n // Texas Greenbook writ/petition history (#229) lives *inside* the\n // court-and-year parenthetical, so it's captured by parseParenthetical's\n // `internalHistory` field rather than the between-parens collector. Emit\n // it first so it appears at order=0 in the chain — it semantically precedes\n // any later signals between separate parens.\n let subsequentHistoryEntries: SubsequentHistoryEntry[] | undefined\n if (cleanedText && metaParenResult?.internalHistory && allParens && allParens.length > 0) {\n const metaParen = parentheticalContent ? allParens[0] : undefined\n if (metaParen) {\n const contentStart = metaParen.start + 1\n const ih = metaParenResult.internalHistory\n const sigCleanStart = contentStart + ih.start\n const sigCleanEnd = contentStart + ih.end\n const { originalStart: sigOrigStart, originalEnd: sigOrigEnd } =\n resolveOriginalSpan(\n { cleanStart: sigCleanStart, cleanEnd: sigCleanEnd },\n transformationMap,\n )\n subsequentHistoryEntries ??= []\n subsequentHistoryEntries.push({\n signal: ih.signal,\n rawSignal: ih.rawSignal,\n signalSpan: {\n cleanStart: sigCleanStart,\n cleanEnd: sigCleanEnd,\n originalStart: sigOrigStart,\n originalEnd: sigOrigEnd,\n },\n order: 0,\n })\n }\n }\n if (cleanedText && collected && collected.signals.length > 0) {\n for (let i = 0; i < collected.signals.length; i++) {\n const { signal: rawSig } = collected.signals[i]\n subsequentHistoryEntries ??= []\n const { originalStart: sigOrigStart, originalEnd: sigOrigEnd } = resolveOriginalSpan(\n { cleanStart: rawSig.start, cleanEnd: rawSig.end },\n transformationMap,\n )\n subsequentHistoryEntries.push({\n signal: rawSig.normalized,\n rawSignal: rawSig.text,\n signalSpan: {\n cleanStart: rawSig.start,\n cleanEnd: rawSig.end,\n originalStart: sigOrigStart,\n originalEnd: sigOrigEnd,\n },\n order: subsequentHistoryEntries.length,\n })\n }\n }\n\n // Infer court level/jurisdiction from reporter series\n const inferredCourt = inferCourtFromReporter(reporter)\n\n // Backward compat: set court string for SCOTUS when not already extracted\n if (!court && inferredCourt?.level === \"supreme\" && inferredCourt?.jurisdiction === \"federal\") {\n court = \"scotus\"\n }\n\n // Phase 6: Extract case name via backward search.\n // Bound the lookback by the previous sibling token's end (if any) so the\n // backward walk for a parallel cite (e.g., the `198 A. 154` half of\n // `Nixon v. Nixon, 329 Pa. 256, 198 A. 154`) does not absorb the earlier\n // reporter cite into the case name.\n let caseNameLookback: number | undefined\n if (siblings && siblings.length > 0) {\n const prev = siblings\n .filter((s) => s.cleanEnd <= span.cleanStart)\n .reduce<{ cleanEnd: number } | undefined>(\n (best, s) =>\n !best || s.cleanEnd > best.cleanEnd ? s : best,\n undefined,\n )\n if (prev) {\n caseNameLookback = span.cleanStart - prev.cleanEnd\n }\n }\n let caseNameResult: ReturnType<typeof extractCaseName> | undefined\n if (cleanedText) {\n caseNameResult = extractCaseName(\n cleanedText,\n span.cleanStart,\n caseNameLookback,\n {\n originalText,\n transformationMap,\n },\n )\n if (caseNameResult) {\n caseName = caseNameResult.caseName\n\n // CSM year-first form puts the year *before* volume-reporter-page\n // (`In re K.F. (2009) 173 Cal.App.4th 655` — #19). Pick it up here when\n // there's no trailing court parenthetical to recover it from. Don't\n // overwrite a year already parsed from a trailing paren — the trailing\n // paren may also carry court information that the year-first paren lacks.\n if (caseNameResult.year && !year) {\n year = caseNameResult.year\n if (\n caseNameResult.yearStart !== undefined &&\n caseNameResult.yearEnd !== undefined &&\n !spans.year\n ) {\n const yearOrig = resolveOriginalSpan(\n {\n cleanStart: caseNameResult.yearStart,\n cleanEnd: caseNameResult.yearEnd,\n },\n transformationMap,\n )\n spans.year = {\n cleanStart: caseNameResult.yearStart,\n cleanEnd: caseNameResult.yearEnd,\n originalStart: yearOrig.originalStart,\n originalEnd: yearOrig.originalEnd,\n }\n }\n }\n\n // Louisiana docket-prefix paren metadata transfer (#232). When a Louisiana\n // citation places `NN-NNNN (La. ... M/D/YY)` between the caption and the\n // reporter, the trailing reporter citation typically carries no court\n // paren of its own — pull court/year/date from the docket paren so the\n // citation surfaces structured metadata instead of dropping it.\n if (caseNameResult.precedingDocketMeta) {\n const meta = caseNameResult.precedingDocketMeta\n if (!year) year = meta.year\n if (!court) court = meta.court\n if (!date) date = meta.date\n }\n\n // Calculate fullSpan: case name start through parenthetical end\n // Reuse allParens from classify loop to avoid scanning twice\n const parenEnd =\n allParens && allParens.length > 0 ? allParens[allParens.length - 1].end : span.cleanEnd\n const fullCleanStart = caseNameResult.nameStart\n const fullCleanEnd = parenEnd\n\n // Translate to original positions\n const fullOriginalStart =\n transformationMap.cleanToOriginal.get(fullCleanStart) ?? fullCleanStart\n const fullOriginalEnd = transformationMap.cleanToOriginal.get(fullCleanEnd) ?? fullCleanEnd\n\n fullSpan = {\n cleanStart: fullCleanStart,\n cleanEnd: fullCleanEnd,\n originalStart: fullOriginalStart,\n originalEnd: fullOriginalEnd,\n }\n\n // Case name span — computed BEFORE signal stripping rebuilds caseName\n const caseNameCleanStart = caseNameResult.nameStart\n const caseNameCleanEnd = caseNameCleanStart + caseName!.length\n const caseNameOrig = resolveOriginalSpan(\n { cleanStart: caseNameCleanStart, cleanEnd: caseNameCleanEnd },\n transformationMap,\n )\n spans.caseName = {\n cleanStart: caseNameCleanStart,\n cleanEnd: caseNameCleanEnd,\n originalStart: caseNameOrig.originalStart,\n originalEnd: caseNameOrig.originalEnd,\n }\n }\n }\n\n // Parallel-cite fullSpan fallback: when this cite is a secondary parallel\n // (no case-name extracted because the bounded lookback hits the prior\n // cite's end) AND there is a close preceding sibling indicating a parallel\n // chain, still extend fullSpan through the shared trailing paren so\n // string-citation grouping and downstream span consumers see the full\n // citation extent. The bare cite's own cleanStart anchors the lower bound.\n // Cites without a preceding sibling (e.g., a standalone `500 F.2d 123 (2020)`\n // with no caption) intentionally do not get a fullSpan — that's existing\n // contract: \"no case name → no fullSpan\".\n const hasCloseParallelPrev =\n caseNameLookback !== undefined && caseNameLookback < 30\n if (\n !fullSpan &&\n hasCloseParallelPrev &&\n allParens &&\n allParens.length > 0\n ) {\n const lastParen = allParens[allParens.length - 1]\n if (lastParen.end > span.cleanEnd) {\n const fullCleanStart = span.cleanStart\n const fullCleanEnd = lastParen.end\n fullSpan = {\n cleanStart: fullCleanStart,\n cleanEnd: fullCleanEnd,\n originalStart:\n transformationMap.cleanToOriginal.get(fullCleanStart) ??\n fullCleanStart,\n originalEnd:\n transformationMap.cleanToOriginal.get(fullCleanEnd) ?? fullCleanEnd,\n }\n }\n }\n\n // Phase 7: Extract party names from case name\n let plaintiff: string | undefined\n let plaintiffNormalized: string | undefined\n let defendant: string | undefined\n let defendantNormalized: string | undefined\n let proceduralPrefix: string | undefined\n let adminParenthetical: string | undefined\n\n let signal: CitationSignal | undefined\n if (caseName) {\n const partyResult = extractPartyNames(caseName)\n plaintiff = partyResult.plaintiff\n plaintiffNormalized = partyResult.plaintiffNormalized\n defendant = partyResult.defendant\n defendantNormalized = partyResult.defendantNormalized\n proceduralPrefix = partyResult.proceduralPrefix\n signal = partyResult.signal\n adminParenthetical = partyResult.adminParenthetical\n\n // Rebuild caseName when extractPartyNames modified the plaintiff (signal stripped,\n // \"In\"/\"Also\" prefix removed, etc.). Find the plaintiff's actual position in the\n // cleaned text to update fullSpan and caseName span. Bankruptcy admin\n // parenthetical is preserved as part of the rebuilt caseName so it remains\n // visible to consumers even though it's stripped off the `defendant` field.\n if (plaintiff && defendant) {\n const adminSuffix = adminParenthetical ? ` (${adminParenthetical})` : \"\"\n // Preserve the source's `v` punctuation form when rebuilding (#326).\n // The existing caseName already carries the right separator (set by\n // extractCaseName / V_CASE_NAME_REGEX); detect it and reuse.\n const existingSepMatch = caseName ? /\\s+(vs?\\.?)\\s+/.exec(caseName) : null\n const rebuildSep = existingSepMatch?.[1] ?? \"v.\"\n const rebuiltName = `${plaintiff} ${rebuildSep} ${defendant}${adminSuffix}`\n if (rebuiltName !== caseName && fullSpan && cleanedText) {\n caseName = rebuiltName\n\n // Advance fullSpan.cleanStart to where the plaintiff actually starts\n const prefixRegion = cleanedText.substring(fullSpan.cleanStart, span.cleanStart)\n const vSep = /\\s+vs?\\.?\\s+/i.exec(prefixRegion)\n if (vSep) {\n const beforeV = prefixRegion.substring(0, vSep.index)\n const pIdx = beforeV.lastIndexOf(plaintiff)\n if (pIdx !== -1) {\n const newCleanStart = fullSpan.cleanStart + pIdx\n const newOriginalStart =\n transformationMap.cleanToOriginal.get(newCleanStart) ?? newCleanStart\n fullSpan = { ...fullSpan, cleanStart: newCleanStart, originalStart: newOriginalStart }\n }\n }\n\n // Update caseName span to reflect the cleaned name\n if (caseNameResult) {\n const strippedCleanStart = fullSpan.cleanStart\n const strippedCleanEnd = strippedCleanStart + caseName.length\n const strippedOrig = resolveOriginalSpan(\n { cleanStart: strippedCleanStart, cleanEnd: strippedCleanEnd },\n transformationMap,\n )\n spans.caseName = {\n cleanStart: strippedCleanStart,\n cleanEnd: strippedCleanEnd,\n originalStart: strippedOrig.originalStart,\n originalEnd: strippedOrig.originalEnd,\n }\n }\n }\n }\n\n // Plaintiff and defendant spans — split the search region at the \"v.\" separator\n // so each name is only matched on the correct side, avoiding indexOf collisions\n // when a name substring appears in both halves (e.g., \"Smith v. Smith\").\n if (plaintiff && caseNameResult && cleanedText) {\n const nameAnchor = fullSpan?.cleanStart ?? caseNameResult.nameStart\n const searchRegion = cleanedText.substring(nameAnchor, span.cleanStart)\n const vSepMatch = /\\s+vs?\\.?\\s+/i.exec(searchRegion)\n if (vSepMatch) {\n // Plaintiff: search only in the region before \"v.\"\n const plaintiffRegion = searchRegion.substring(0, vSepMatch.index)\n const pIdx = plaintiffRegion.lastIndexOf(plaintiff)\n if (pIdx !== -1) {\n const pCleanStart = nameAnchor + pIdx\n const pCleanEnd = pCleanStart + plaintiff.length\n const pOrig = resolveOriginalSpan(\n { cleanStart: pCleanStart, cleanEnd: pCleanEnd },\n transformationMap,\n )\n spans.plaintiff = {\n cleanStart: pCleanStart,\n cleanEnd: pCleanEnd,\n originalStart: pOrig.originalStart,\n originalEnd: pOrig.originalEnd,\n }\n }\n // Defendant: search only in the region after \"v.\"\n if (defendant) {\n const defRegionStart = vSepMatch.index + vSepMatch[0].length\n const defendantRegion = searchRegion.substring(defRegionStart)\n const dIdx = defendantRegion.indexOf(defendant)\n if (dIdx !== -1) {\n const dCleanStart = nameAnchor + defRegionStart + dIdx\n const dCleanEnd = dCleanStart + defendant.length\n const dOrig = resolveOriginalSpan(\n { cleanStart: dCleanStart, cleanEnd: dCleanEnd },\n transformationMap,\n )\n spans.defendant = {\n cleanStart: dCleanStart,\n cleanEnd: dCleanEnd,\n originalStart: dOrig.originalStart,\n originalEnd: dOrig.originalEnd,\n }\n }\n }\n } else {\n // No \"v.\" separator — procedural prefix case (e.g., \"In re X\").\n // Plaintiff is the full case name; no defendant to locate.\n const pIdx = searchRegion.indexOf(plaintiff)\n if (pIdx !== -1) {\n const pCleanStart = nameAnchor + pIdx\n const pCleanEnd = pCleanStart + plaintiff.length\n const pOrig = resolveOriginalSpan(\n { cleanStart: pCleanStart, cleanEnd: pCleanEnd },\n transformationMap,\n )\n spans.plaintiff = {\n cleanStart: pCleanStart,\n cleanEnd: pCleanEnd,\n originalStart: pOrig.originalStart,\n originalEnd: pOrig.originalEnd,\n }\n }\n }\n }\n\n // Signal span — the signal word was part of the original case name, found\n // at caseNameResult.nameStart. After signal stripping, fullSpan.cleanStart\n // was advanced past it, so the signal occupies [nameStart, fullSpan.cleanStart).\n if (signal && fullSpan && cleanedText && caseNameResult) {\n const sigRegion = cleanedText.substring(caseNameResult.nameStart, span.cleanStart)\n const sigMatch = SIGNAL_STRIP_REGEX.exec(sigRegion)\n if (sigMatch) {\n const sigCleanStart = caseNameResult.nameStart\n const sigCleanEnd = sigCleanStart + sigMatch[1].length\n const sigOrig = resolveOriginalSpan(\n { cleanStart: sigCleanStart, cleanEnd: sigCleanEnd },\n transformationMap,\n )\n spans.signal = {\n cleanStart: sigCleanStart,\n cleanEnd: sigCleanEnd,\n originalStart: sigOrig.originalStart,\n originalEnd: sigOrig.originalEnd,\n }\n }\n }\n }\n\n // Translate positions from clean → original (citation core only - span unchanged)\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Calculate confidence score using multi-factor model.\n // Base is low — unvalidated matches are uncertain. Real signals earn confidence.\n let confidence = 0.2\n\n // Known reporter: strong signal.\n // Check reporters-db first (precise), fall back to common reporter set.\n const reportersDb = getReportersSync()\n const dbMatch = reportersDb?.byAbbreviation.get(reporter.toLowerCase())\n if (dbMatch && dbMatch.length > 0) {\n confidence += 0.3\n } else if (COMMON_REPORTERS.has(reporter)) {\n confidence += 0.3\n }\n\n // Year present and plausible: moderate signal\n if (year !== undefined) {\n if (year <= CURRENT_YEAR) {\n confidence += 0.2\n }\n }\n\n // Case name found: moderate signal\n if (caseName) {\n confidence += 0.15\n }\n\n // Court identified: confirmatory signal\n if (court) {\n confidence += 0.1\n }\n\n // Cap at 1.0 and round to avoid floating point artifacts (e.g., 0.7999...9)\n confidence = Math.round(Math.min(confidence, 1.0) * 100) / 100\n\n // Blank page citations: intentional placeholders (3+ underscores/dashes in legal\n // briefs). The pattern is very specific so they deserve at least moderate confidence,\n // but don't let them exceed the signals they actually have.\n if (hasBlankPage) {\n confidence = Math.max(confidence, 0.5)\n }\n\n return {\n type: \"case\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0, // Placeholder - timing handled by orchestration layer\n patternsChecked: 1, // Single token processed\n volume,\n reporter,\n page,\n nominativeVolume,\n nominativeReporter,\n pincite,\n pinciteInfo,\n court,\n normalizedCourt: normalizeCourt(court),\n year,\n hasBlankPage,\n date,\n fullSpan,\n caseName,\n disposition,\n parentheticals,\n subsequentHistoryEntries,\n ...(unpublished ? { unpublished: true } : {}),\n ...(justices ? { justices } : {}),\n ...(scope ? { scope } : {}),\n ...(adminParenthetical ? { adminParenthetical } : {}),\n plaintiff,\n plaintiffNormalized,\n defendant,\n defendantNormalized,\n proceduralPrefix,\n inferredCourt,\n signal,\n spans,\n }\n}\n","/**\n * Constitutional Citation Regex Patterns\n *\n * Patterns for U.S. Constitution, state constitutions, and bare \"Const.\" citations.\n * Intentionally broad for tokenization — extraction layer parses structured fields.\n *\n * Four patterns (ordered by specificity):\n * - us-constitution: \"U.S. Const. art. III, § 2\"\n * - state-constitution: \"Cal. Const. art. I, § 7\"\n * - bare-constitution: \"Const. art. I, § 8, cl. 3\"\n * - bare-article: \"Art. I, §8, cl. 3\" (requires Roman numeral + § section)\n */\n\nimport type { Pattern } from \"./casePatterns\"\n\n// Shared tail: art./amend. + numeral + optional § section + optional cl. clause\n// Roman numerals: I, II, III, IV, V, VI, VII, VIII, IX, X, XI, XII, XIII, XIV, XV, XVI, XVII, XVIII, XIX, XX, XXI, XXII, XXIII, XXIV, XXV, XXVI, XXVII\n// Also accepts Arabic numerals as fallback\nconst ARTICLE_OR_AMENDMENT = String.raw`(?:art(?:icle)?\\.?|amend(?:ment)?\\.?|amdt\\.?)\\s+([IVX]+|\\d+)`\nconst OPTIONAL_SECTION = String.raw`(?:[,;]\\s*§\\s*([\\w-]+))?`\nconst OPTIONAL_CLAUSE = String.raw`(?:[,;]\\s*cl\\.?\\s*(\\d+))?`\nconst BODY_TAIL = `${ARTICLE_OR_AMENDMENT}${OPTIONAL_SECTION}${OPTIONAL_CLAUSE}`\n\n/** Compiled body regex shared with the extractor to avoid duplicate definitions. */\nexport const CONSTITUTIONAL_BODY_RE: RegExp = new RegExp(BODY_TAIL, \"id\")\n\nexport const constitutionalPatterns: Pattern[] = [\n {\n id: \"us-constitution\",\n regex: new RegExp(\n String.raw`\\b(?:United\\s+States\\s+Constitution|U\\.?\\s*S\\.?\\s+Const\\.?),?\\s+${BODY_TAIL}`,\n \"gi\",\n ),\n description:\n 'U.S. Constitution citations (e.g., \"U.S. Const. art. III, § 2\", \"U.S. Const. amend. XIV\")',\n type: \"constitutional\",\n },\n {\n id: \"state-constitution\",\n // Separator between state abbrev and `Const.` uses `(?:\\.\\s*|\\s+)`:\n // accepts canonical `Pa. Const.`, abbreviated no-space `Pa.Const.`\n // (#329), and bare-space `Pa Const.`. The `.` branch requires a dot\n // (forces a separator), so `PaConst.` does not match — avoids false\n // positives from words that happen to start with a state stem.\n regex: new RegExp(\n String.raw`\\b(?:Ala|Alaska|Ariz|Ark|Cal(?:if)?|Colo|Conn|Del|Fla|Ga|Haw|Idaho|Ill|Ind|Iowa|Kan|Ky|La|Me|Md|Mass|Mich|Minn|Miss|Mo|Mont|Neb|Nev|N\\.?\\s*H|N\\.?\\s*J|N\\.?\\s*M|N\\.?\\s*Y|N\\.?\\s*C|N\\.?\\s*D|Ohio|Okla|Or(?:e)?|Pa|R\\.?\\s*I|S\\.?\\s*C|S\\.?\\s*D|Tenn|Tex|Utah|Vt|W\\.?\\s*Va|Va|Wash|Wis|Wyo)(?:\\.\\s*|\\s+)Const\\.?,?\\s+${BODY_TAIL}`,\n \"gi\",\n ),\n description:\n 'State constitution citations (e.g., \"Cal. Const. art. I, § 7\", \"N.Y. Const. art. VI, § 20\", \"Pa.Const. art. VIII, § 4\")',\n type: \"constitutional\",\n },\n {\n id: \"bare-constitution\",\n // \"g\" (not \"gi\") is intentional: the lookbehind uses [A-Z] which requires case sensitivity.\n // Consequence: lowercase \"const.\" is never matched — acceptable in formal legal citations.\n // Consequence: all-caps preceding words like \"THE Const.\" won't match due to [A-Z]\\s lookbehind — rare, acceptable tradeoff.\n // Known limitation: multi-character state abbreviations ending in lowercase (Alaska, Idaho, etc.)\n // bypass the lookbehind and produce a second bare match — tokenizer span dedup handles this.\n regex: new RegExp(String.raw`(?<!\\.\\s)(?<![A-Z]\\s)\\bConst\\.?,?\\s+${BODY_TAIL}`, \"g\"),\n description:\n 'Bare constitutional citations without jurisdiction prefix (e.g., \"Const. art. I, § 8, cl. 3\")',\n type: \"constitutional\",\n },\n {\n id: \"bare-article\",\n // Lowest-priority pattern: bare \"Art.\" with no \"Const.\" prefix at all.\n // Constrained to reduce false positives: Roman numerals only (no Arabic),\n // must include a § section reference, and lookbehind rejects \"Const.\" prefix\n // (already handled by higher-priority patterns). Confidence set to 0.5 in extractor.\n regex: new RegExp(\n String.raw`(?<!Const\\.?,?\\s)\\bArt\\.?\\s+[IVX]+[,;]\\s*§\\s*[\\w-]+(?:[,;]\\s*cl\\.?\\s*\\d+)?`,\n \"g\",\n ),\n description:\n 'Bare article references without \"Const.\" prefix (e.g., \"Art. I, §8, cl. 3\")',\n type: \"constitutional\",\n },\n]\n","/**\n * Constitutional Citation Extraction\n *\n * Parses tokenized constitutional citations to extract jurisdiction,\n * article/amendment, section, and clause fields.\n *\n * Dispatch by patternId:\n * - \"us-constitution\" → jurisdiction: \"US\"\n * - \"state-constitution\" → jurisdiction mapped from state abbreviation\n * - \"bare-constitution\" → jurisdiction: undefined\n *\n * @module extract/extractConstitutional\n */\n\nimport { CONSTITUTIONAL_BODY_RE } from \"@/patterns/constitutionalPatterns\"\nimport type { Token } from \"@/tokenize\"\nimport type { ConstitutionalCitation } from \"@/types/citation\"\nimport type { ConstitutionalComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\n\n/**\n * Roman numeral lookup table (I–XXVII).\n * Covers all U.S. constitutional articles (I–VII) and amendments (I–XXVII).\n */\nconst ROMAN_TO_INT: Record<string, number> = {\n I: 1,\n II: 2,\n III: 3,\n IV: 4,\n V: 5,\n VI: 6,\n VII: 7,\n VIII: 8,\n IX: 9,\n X: 10,\n XI: 11,\n XII: 12,\n XIII: 13,\n XIV: 14,\n XV: 15,\n XVI: 16,\n XVII: 17,\n XVIII: 18,\n XIX: 19,\n XX: 20,\n XXI: 21,\n XXII: 22,\n XXIII: 23,\n XXIV: 24,\n XXV: 25,\n XXVI: 26,\n XXVII: 27,\n}\n\n/** Parse a Roman numeral or Arabic number string to an integer. */\nfunction parseNumeral(raw: string): number | undefined {\n const upper = raw.toUpperCase()\n if (upper in ROMAN_TO_INT) return ROMAN_TO_INT[upper]\n const n = Number.parseInt(raw, 10)\n return Number.isNaN(n) ? undefined : n\n}\n\n/**\n * State abbreviation → 2-letter code mapping.\n * Keys are lowercase abbreviation stems (without trailing period).\n */\nconst STATE_ABBREV_TO_CODE: Record<string, string> = {\n ala: \"AL\",\n alaska: \"AK\",\n ariz: \"AZ\",\n ark: \"AR\",\n cal: \"CA\",\n calif: \"CA\",\n colo: \"CO\",\n conn: \"CT\",\n del: \"DE\",\n fla: \"FL\",\n ga: \"GA\",\n haw: \"HI\",\n idaho: \"ID\",\n ill: \"IL\",\n ind: \"IN\",\n iowa: \"IA\",\n kan: \"KS\",\n ky: \"KY\",\n la: \"LA\",\n me: \"ME\",\n md: \"MD\",\n mass: \"MA\",\n mich: \"MI\",\n minn: \"MN\",\n miss: \"MS\",\n mo: \"MO\",\n mont: \"MT\",\n neb: \"NE\",\n nev: \"NV\",\n \"n.h\": \"NH\",\n \"n.j\": \"NJ\",\n \"n.m\": \"NM\",\n \"n.y\": \"NY\",\n \"n.c\": \"NC\",\n \"n.d\": \"ND\",\n ohio: \"OH\",\n okla: \"OK\",\n or: \"OR\",\n ore: \"OR\",\n pa: \"PA\",\n \"r.i\": \"RI\",\n \"s.c\": \"SC\",\n \"s.d\": \"SD\",\n tenn: \"TN\",\n tex: \"TX\",\n utah: \"UT\",\n vt: \"VT\",\n va: \"VA\",\n wash: \"WA\",\n \"w.va\": \"WV\",\n wis: \"WI\",\n wyo: \"WY\",\n}\n\nconst IS_AMENDMENT_RE = /amend|amdt/i\n\n/**\n * Regex to extract the state abbreviation prefix from state-constitution tokens.\n *\n * Trailing `\\.?\\s*Const` (rather than `\\.?\\s+Const`) accepts both the\n * canonical spaced form (`Pa. Const.`) and the abbreviated no-space form\n * (`Pa.Const.`, `N.Y.Const.`) introduced in #329. The greedy `[A-Za-z]+`\n * still backtracks correctly so the prefix capture stops at the state\n * abbreviation rather than swallowing `Const`.\n */\nconst STATE_PREFIX_RE = /^([A-Za-z]+(?:\\.\\s*[A-Za-z]+)?(?:\\.\\s*[A-Za-z]+)?)\\.?\\s*Const/i\n\n/**\n * Resolve state abbreviation from token text to 2-letter code.\n */\nfunction resolveStateJurisdiction(text: string): string | undefined {\n const prefixMatch = STATE_PREFIX_RE.exec(text)\n if (!prefixMatch) return undefined\n\n // Normalize: collapse spaces, lowercase, remove trailing dots\n const raw = prefixMatch[1].replace(/\\s+/g, \"\").replace(/\\.$/g, \"\").toLowerCase()\n\n if (raw in STATE_ABBREV_TO_CODE) return STATE_ABBREV_TO_CODE[raw]\n\n return undefined\n}\n\n/**\n * Extract a constitutional citation from a tokenized match.\n *\n * @param token - Tokenized citation candidate from the tokenizer\n * @param transformationMap - Maps cleaned text positions to original text positions\n * @returns Parsed constitutional citation with structured fields\n */\nexport function extractConstitutional(\n token: Token,\n transformationMap: TransformationMap,\n): ConstitutionalCitation {\n const { text, span } = token\n\n const bodyMatch = CONSTITUTIONAL_BODY_RE.exec(text)\n\n let article: number | undefined\n let amendment: number | undefined\n let section: string | undefined\n let clause: number | undefined\n\n if (bodyMatch) {\n const numeral = parseNumeral(bodyMatch[1])\n\n if (IS_AMENDMENT_RE.test(bodyMatch[0])) {\n amendment = numeral\n } else {\n article = numeral\n }\n\n section = bodyMatch[2] || undefined\n clause = bodyMatch[3] ? Number.parseInt(bodyMatch[3], 10) : undefined\n }\n\n let jurisdiction: string | undefined\n switch (token.patternId) {\n case \"us-constitution\":\n jurisdiction = \"US\"\n break\n case \"state-constitution\":\n jurisdiction = resolveStateJurisdiction(text)\n break\n default:\n jurisdiction = undefined\n break\n }\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let confidence: number\n if (token.patternId === \"bare-article\") {\n confidence = 0.5\n } else if (token.patternId === \"bare-constitution\") {\n confidence = 0.7\n } else if (section) {\n confidence = 0.95\n } else {\n confidence = 0.9\n }\n\n // The section regex may greedily consume a sentence-terminating period (\"§ 1.\")\n const matchedText = text.endsWith(\".\") ? text.slice(0, -1) : text\n\n // Build component spans\n const spans: ConstitutionalComponentSpans = {}\n\n // Jurisdiction span: find the jurisdiction text in the token\n if (jurisdiction === \"US\") {\n const usIdx = text.indexOf(\"U.S.\")\n if (usIdx !== -1) {\n spans.jurisdiction = spanFromGroupIndex(span.cleanStart, [usIdx, usIdx + 4], transformationMap)\n }\n } else if (jurisdiction && token.patternId === \"state-constitution\") {\n // State prefix is at the start of the token text\n const prefixMatch = STATE_PREFIX_RE.exec(text)\n if (prefixMatch) {\n // The prefix is the abbreviation stem; add 1 for the trailing period\n const prefixEnd = prefixMatch[1].length + 1\n spans.jurisdiction = spanFromGroupIndex(span.cleanStart, [0, prefixEnd], transformationMap)\n }\n }\n\n // Body match groups for article/amendment, section, clause\n // bodyMatch.indices[n] gives positions relative to the full token text\n if (bodyMatch?.indices) {\n if (IS_AMENDMENT_RE.test(bodyMatch[0])) {\n if (bodyMatch.indices[1]) {\n spans.amendment = spanFromGroupIndex(span.cleanStart, bodyMatch.indices[1], transformationMap)\n }\n } else {\n if (bodyMatch.indices[1]) {\n spans.article = spanFromGroupIndex(span.cleanStart, bodyMatch.indices[1], transformationMap)\n }\n }\n if (bodyMatch.indices[2]) {\n spans.section = spanFromGroupIndex(span.cleanStart, bodyMatch.indices[2], transformationMap)\n }\n if (bodyMatch.indices[3]) {\n spans.clause = spanFromGroupIndex(span.cleanStart, bodyMatch.indices[3], transformationMap)\n }\n }\n\n return {\n type: \"constitutional\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText,\n processTimeMs: 0,\n patternsChecked: 1,\n jurisdiction,\n article,\n amendment,\n section,\n clause,\n spans,\n }\n}\n","/**\n * Docket-Number Citation Extraction\n *\n * Parses tokenized docket citations of the form\n * `Party v. Party, No. <docket> (<court> <year>)`\n *\n * Disambiguation strategy: a bare `No. 51 (N.Y. 2023)` is too generic to\n * extract on its own. The extractor backward-searches for a case-name\n * anchor and only emits a `DocketCitation` when one is found. Tokens\n * without a case-name anchor are silently dropped (the extractor returns\n * `undefined`).\n *\n * @module extract/extractDocket\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { DocketCitation } from \"@/types/citation\"\nimport { resolveOriginalSpan, type TransformationMap } from \"@/types/span\"\nimport { normalizeCourt } from \"./courtNormalization\"\nimport { extractCaseName, extractPartyNames, parseParenthetical } from \"./extractCase\"\n\n/**\n * Extracts a docket-number citation from a tokenized match.\n *\n * Parses token text to extract:\n * - `docketNumber`: digits with optional hyphens (e.g. \"51\", \"12-3456\")\n * - `court`, `year`, `date`: from the trailing parenthetical\n * - `caseName`, `plaintiff`, `defendant`: via backward case-name search\n *\n * Returns `undefined` when no case-name anchor is found — the bare docket\n * shape is too ambiguous to surface without context.\n *\n * Confidence: 0.7 (lower than reporter-based citations because there is no\n * reporter to validate against).\n *\n * @param token - Tokenizer output containing matched text and clean positions\n * @param transformationMap - Position mapping from clean → original text\n * @param cleanedText - Full cleaned document (needed for backward case-name search)\n * @returns DocketCitation when a case-name anchor is found, otherwise undefined\n *\n * @example\n * ```typescript\n * const text = \"IKB Int'l, S.A. v. Wells Fargo, N.A., No. 51 (N.Y. 2023).\"\n * const citation = extractDocket(token, transformationMap, text)\n * // citation = {\n * // type: \"docket\",\n * // docketNumber: \"51\",\n * // court: \"N.Y.\",\n * // year: 2023,\n * // caseName: \"IKB Int'l, S.A. v. Wells Fargo, N.A.\",\n * // ...\n * // }\n * ```\n */\nexport function extractDocket(\n token: Token,\n transformationMap: TransformationMap,\n cleanedText: string,\n originalText?: string,\n): DocketCitation | undefined {\n const { text, span } = token\n\n // Parse the token text: \"No. <docket> (<paren-content>)\"\n const tokenRegex = /\\bNo\\.\\s+([\\d]+(?:-[\\w\\d]+)*)\\s+\\(([^)]+)\\)/\n const match = tokenRegex.exec(text)\n if (!match) return undefined\n\n const docketNumber = match[1]\n const parenContent = match[2]\n\n // Backward case-name search — anchor for disambiguation. Without a\n // case-name we don't emit a citation: a bare \"No. 51 (N.Y. 2023)\" lacks\n // the context needed to be confident this is a citation at all.\n const caseNameResult = extractCaseName(cleanedText, span.cleanStart, undefined, {\n originalText,\n transformationMap,\n })\n if (!caseNameResult) return undefined\n\n // The case-name extractor's V_CASE_NAME_REGEX requires \"Party v. Party\"\n // or \"In re Party\" plus a trailing comma. Validate the result actually\n // looks like a case name (contains \"v.\" or a procedural prefix).\n const partyResult = extractPartyNames(caseNameResult.caseName)\n const hasAdversarial = partyResult.plaintiff && partyResult.defendant\n const hasProceduralPrefix = !!partyResult.proceduralPrefix\n if (!hasAdversarial && !hasProceduralPrefix) return undefined\n\n // Parse court/year/date from the parenthetical content. parseParenthetical\n // is the same helper used by extractCase, so docket citations get the\n // same court-string normalization and date handling.\n const meta = parseParenthetical(parenContent)\n\n // Resolve clean → original positions for the citation core.\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // fullSpan: case-name start through closing paren. cleanEnd is the end\n // of the matched token (after the closing `)`). When extractPartyNames\n // strips a signal word (\"See\", \"But see\", etc.) from the plaintiff,\n // advance fullCleanStart past it to mirror extractCase's behavior.\n let fullCleanStart = caseNameResult.nameStart\n if (partyResult.plaintiff && partyResult.defendant) {\n const prefixRegion = cleanedText.substring(fullCleanStart, span.cleanStart)\n const vSep = /\\s+v\\.?\\s+/i.exec(prefixRegion)\n if (vSep) {\n const beforeV = prefixRegion.substring(0, vSep.index)\n const pIdx = beforeV.lastIndexOf(partyResult.plaintiff)\n if (pIdx !== -1) fullCleanStart += pIdx\n }\n }\n const fullCleanEnd = span.cleanEnd\n const fullOriginalStart = transformationMap.cleanToOriginal.get(fullCleanStart) ?? fullCleanStart\n const fullOriginalEnd = transformationMap.cleanToOriginal.get(fullCleanEnd) ?? fullCleanEnd\n\n return {\n type: \"docket\",\n text,\n matchedText: text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence: 0.7,\n processTimeMs: 0,\n patternsChecked: 1,\n docketNumber,\n caseName:\n partyResult.plaintiff && partyResult.defendant\n ? `${partyResult.plaintiff} v. ${partyResult.defendant}`\n : caseNameResult.caseName,\n plaintiff: partyResult.plaintiff,\n defendant: partyResult.defendant,\n plaintiffNormalized: partyResult.plaintiffNormalized,\n defendantNormalized: partyResult.defendantNormalized,\n proceduralPrefix: partyResult.proceduralPrefix,\n court: meta.court,\n normalizedCourt: normalizeCourt(meta.court),\n year: meta.year,\n date: meta.date,\n fullSpan: {\n cleanStart: fullCleanStart,\n cleanEnd: fullCleanEnd,\n originalStart: fullOriginalStart,\n originalEnd: fullOriginalEnd,\n },\n }\n}\n","/**\n * Federal Register Citation Extraction\n *\n * Parses tokenized Federal Register citations to extract volume, page, and\n * optional year. Examples: \"85 Fed. Reg. 12345\", \"86 Fed. Reg. 56789 (Jan. 15, 2021)\"\n *\n * @module extract/extractFederalRegister\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { FederalRegisterCitation } from \"@/types/citation\"\nimport type { FederalRegisterComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\n\n/**\n * Extracts Federal Register citation metadata from a tokenized citation.\n *\n * Parses token text to extract:\n * - Volume: Federal Register volume number (e.g., \"85\")\n * - Page: Page number (e.g., \"12345\")\n * - Year: Optional publication year in parentheses (e.g., \"(2021)\")\n *\n * Confidence scoring:\n * - 0.9 (Federal Register format is standardized)\n *\n * @param token - Token from tokenizer containing matched text and clean positions\n * @param transformationMap - Position mapping from clean → original text\n * @returns FederalRegisterCitation with parsed metadata and translated positions\n *\n * @example\n * ```typescript\n * const token = {\n * text: \"85 Fed. Reg. 12345\",\n * span: { cleanStart: 10, cleanEnd: 28 },\n * type: \"federalRegister\",\n * patternId: \"federal-register\"\n * }\n * const citation = extractFederalRegister(token, transformationMap)\n * // citation = {\n * // type: \"federalRegister\",\n * // volume: 85,\n * // page: 12345,\n * // confidence: 0.9,\n * // ...\n * // }\n * ```\n */\nexport function extractFederalRegister(\n token: Token,\n transformationMap: TransformationMap,\n): FederalRegisterCitation {\n const { text, span } = token\n\n // Parse volume-page using regex\n // Pattern: volume (digits) + \"Fed. Reg.\" + page (digits)\n const federalRegisterRegex = /^(\\d+(?:-\\d+)?)\\s+Fed\\.\\s?Reg\\.\\s+(\\d+)/d\n const match = federalRegisterRegex.exec(text)\n\n if (!match) {\n throw new Error(`Failed to parse Federal Register citation: ${text}`)\n }\n\n const rawVolume = match[1]\n const volume = /^\\d+$/.test(rawVolume) ? Number.parseInt(rawVolume, 10) : rawVolume\n const page = Number.parseInt(match[2], 10)\n\n let spans: FederalRegisterComponentSpans | undefined\n if (match.indices) {\n spans = {\n volume: spanFromGroupIndex(span.cleanStart, match.indices[1]!, transformationMap),\n page: spanFromGroupIndex(span.cleanStart, match.indices[2]!, transformationMap),\n }\n }\n\n // Extract optional year in parentheses\n // Pattern: \"(year)\" or \"(month day, year)\"\n const yearRegex = /\\((?:.*?\\s)?(\\d{4})\\)/\n const yearMatch = yearRegex.exec(text)\n const year = yearMatch ? Number.parseInt(yearMatch[1], 10) : undefined\n\n // Translate positions from clean → original\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Confidence: 0.9 (Federal Register format is standardized)\n const confidence = 0.9\n\n return {\n type: \"federalRegister\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n volume,\n page,\n year,\n spans,\n }\n}\n","/**\n * Journal Citation Extraction\n *\n * Parses tokenized journal citations to extract volume, journal name, page,\n * and optional metadata. Examples: \"123 Harv. L. Rev. 456\", \"75 Yale L.J. 789, 791\"\n *\n * @module extract/extractJournal\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { JournalCitation } from \"@/types/citation\"\nimport type { JournalComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\n\n/**\n * Extracts journal citation metadata from a tokenized citation.\n *\n * Parses token text to extract:\n * - Volume: Leading digits (e.g., \"123\" from \"123 Harv. L. Rev. 456\")\n * - Journal: Journal abbreviation (e.g., \"Harv. L. Rev.\")\n * - Page: Starting page number (e.g., \"456\")\n * - Pincite: Optional specific page reference after comma (e.g., \", 458\")\n * - Year: Optional publication year in parenthetical (e.g., \"(2020)\")\n *\n * When `cleanedText` is provided, the extractor performs lookahead beyond the token\n * boundary to extract optional pincite and year components that the tokenizer does\n * not capture in the token text.\n *\n * Confidence scoring:\n * - Base: 0.6 (journal validation happens in Phase 3)\n *\n * Note: Author and title extraction from preceding text is not implemented\n * in Phase 2. That requires context analysis in Phase 3.\n *\n * @param token - Token from tokenizer containing matched text and clean positions\n * @param transformationMap - Position mapping from clean → original text\n * @param cleanedText - Full cleaned document text (optional; enables pincite/year lookahead)\n * @returns JournalCitation with parsed metadata and translated positions\n *\n * @example\n * ```typescript\n * const token = {\n * text: \"123 Harv. L. Rev. 456\",\n * span: { cleanStart: 10, cleanEnd: 31 },\n * type: \"journal\",\n * patternId: \"journal-standard\"\n * }\n * const citation = extractJournal(token, transformationMap)\n * // citation = {\n * // type: \"journal\",\n * // volume: 123,\n * // journal: \"Harv. L. Rev.\",\n * // abbreviation: \"Harv. L. Rev.\",\n * // page: 456,\n * // ...\n * // }\n * ```\n */\nexport function extractJournal(\n token: Token,\n transformationMap: TransformationMap,\n cleanedText?: string,\n): JournalCitation {\n const { text, span } = token\n\n // Parse volume-journal-page using regex\n // Pattern: volume (digits) + journal (letters/periods/spaces) + page (digits)\n const journalRegex = /^(\\d+(?:-\\d+)?)\\s+([A-Za-z.\\s]+?)\\s+(\\d+)/d\n const match = journalRegex.exec(text)\n\n if (!match) {\n throw new Error(`Failed to parse journal citation: ${text}`)\n }\n\n const rawVolume = match[1]\n const volume = /^\\d+$/.test(rawVolume) ? Number.parseInt(rawVolume, 10) : rawVolume\n const journal = match[2].trim()\n const page = Number.parseInt(match[3], 10)\n\n // Determine where to search for pincite/year.\n //\n // When cleanedText is available (pipeline context), we search a window starting\n // at the token end position. This handles the normal tokenizer case where the\n // tokenizer only captures the core citation (e.g., \"75 Yale L.J. 456\") and the\n // pincite/year appear immediately after in the document.\n //\n // When cleanedText is not available (manually constructed tokens in tests),\n // we fall back to the token text itself, which may contain them directly.\n const lookaheadWindow = 30\n\n // afterTokenText: text immediately after the core token match, in clean coordinates.\n // For pipeline tokens: this is a window of cleanedText starting at span.cleanEnd.\n // For manually constructed tokens (no cleanedText): the remainder of token.text after\n // the core match (text after match[0].length characters, if any).\n let afterTokenText: string\n let afterTokenCleanStart: number\n if (cleanedText !== undefined) {\n afterTokenText = cleanedText.slice(span.cleanEnd, span.cleanEnd + lookaheadWindow)\n afterTokenCleanStart = span.cleanEnd\n } else {\n // Token text may already include pincite/year (e.g., \"123 Harv. L. Rev. 456, 458\")\n // The core match ends at match[0].length within the token text.\n afterTokenText = text.slice(match[0].length)\n afterTokenCleanStart = span.cleanStart + match[0].length\n }\n\n // Build full context string (from token start) for year search\n const fullContext = cleanedText\n ? cleanedText.slice(span.cleanStart, span.cleanEnd + lookaheadWindow)\n : text\n\n // Extract optional pincite (page reference after comma) immediately after core match\n const pinciteRegex = /^,\\s*(\\d+)/d\n const pinciteMatch = pinciteRegex.exec(afterTokenText)\n const pincite = pinciteMatch ? Number.parseInt(pinciteMatch[1], 10) : undefined\n\n // Extract optional year from parenthetical (e.g., \"(2020)\") anywhere in the context\n const yearRegex = /\\((?:.*?\\s)?(\\d{4})\\)/d\n const yearMatch = yearRegex.exec(fullContext)\n const year = yearMatch ? Number.parseInt(yearMatch[1], 10) : undefined\n\n // Build component spans using match indices from `d` flag\n let spans: JournalComponentSpans | undefined\n if (match.indices) {\n spans = {\n volume: spanFromGroupIndex(span.cleanStart, match.indices[1]!, transformationMap),\n journal: spanFromGroupIndex(span.cleanStart, match.indices[2]!, transformationMap),\n page: spanFromGroupIndex(span.cleanStart, match.indices[3]!, transformationMap),\n }\n if (pinciteMatch?.indices?.[1]) {\n // pinciteMatch.indices[1] is relative to afterTokenText which starts at afterTokenCleanStart\n spans.pincite = spanFromGroupIndex(\n afterTokenCleanStart,\n pinciteMatch.indices[1],\n transformationMap,\n )\n }\n if (yearMatch?.indices?.[1]) {\n // yearMatch.indices[1] is relative to fullContext which starts at span.cleanStart\n spans.year = spanFromGroupIndex(span.cleanStart, yearMatch.indices[1], transformationMap)\n }\n }\n\n // Translate positions from clean → original\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Confidence: 0.6 base (journal validation against database happens in Phase 3)\n const confidence = 0.6\n\n return {\n type: \"journal\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n volume,\n journal,\n abbreviation: journal, // For Phase 2, abbreviation = journal name\n page,\n pincite,\n year,\n spans,\n }\n}\n","/**\n * Neutral Citation Extraction\n *\n * Parses tokenized neutral (vendor-neutral) citations to extract year, court,\n * and document number. Examples: \"2020 WL 123456\", \"2020 U.S. LEXIS 456\"\n *\n * @module extract/extractNeutral\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { NeutralCitation } from \"@/types/citation\"\nimport type { NeutralComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport type { StructuredDate } from \"./dates\"\nimport { parseParenthetical } from \"./extractCase\"\nimport { parsePincite, type PinciteInfo } from \"./pincite\"\n\n/** Matches a trailing pincite on a neutral citation. Accepts both\n * \", at *3\" (comma + \"at\" keyword) and \" at *3\" (whitespace + \"at\") forms,\n * with optional \"*\" prefix for star-pagination on both ends of a range\n * (#191, #203 — \"*3-*5\" is common on Westlaw/Lexis/NY Slip Op), and an\n * optional trailing \" n.14\" / \" nn.14-15\" footnote suffix (#202). Also\n * accepts paragraph-marker pincites `, ¶ N` / `, ¶¶ N-M` / `, paras. N-M`\n * for state neutral-cite forms like `2015-NMCA-072, ¶ 2` where the\n * paragraph numbering is the canonical pinpoint format (#311). When the\n * pincite is a paragraph form, `at` is optional. */\nconst NEUTRAL_PINCITE_LOOKAHEAD =\n /^(?:\\s+at\\s+|,\\s*(?:at\\s+(?:pp?\\.\\s*)?)?)(\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?|¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?)/d\n\n/** Trailing `(court date)` parenthetical lookahead for database cites.\n * Allows optional intervening pincite (`, at *3`) per #191. The body\n * is anything inside one set of parens. Parsing is delegated to\n * `parseParenthetical`. #294 */\nconst NEUTRAL_PAREN_LOOKAHEAD =\n /^(?:\\s*,?\\s*(?:at\\s+)?\\*?\\d+(?:[-–—]\\*?\\d+)?)?\\s*\\(([^)]+)\\)/\n\n/** Identifies whether a captured \"court\" string is actually a database\n * identifier (WL/LEXIS/BL) rather than a real jurisdictional code. #294 */\nfunction isDatabaseIdentifier(s: string): boolean {\n if (s === \"WL\" || s === \"BL\") return true\n return /\\bLEXIS\\b/.test(s)\n}\n\n/**\n * Extracts neutral citation metadata from a tokenized citation.\n *\n * Parses token text to extract:\n * - Year: 4-digit year (e.g., \"2020\")\n * - Court: Vendor identifier (e.g., \"WL\", \"U.S. LEXIS\")\n * - Document number: Unique document identifier (e.g., \"123456\")\n *\n * Confidence scoring:\n * - 1.0 (neutral format is unambiguous and standardized)\n *\n * @param token - Token from tokenizer containing matched text and clean positions\n * @param transformationMap - Position mapping from clean → original text\n * @returns NeutralCitation with parsed metadata and translated positions\n *\n * @example\n * ```typescript\n * const token = {\n * text: \"2020 WL 123456\",\n * span: { cleanStart: 10, cleanEnd: 24 },\n * type: \"neutral\",\n * patternId: \"westlaw-neutral\"\n * }\n * const citation = extractNeutral(token, transformationMap)\n * // citation = {\n * // type: \"neutral\",\n * // year: 2020,\n * // court: \"WL\",\n * // documentNumber: \"123456\",\n * // confidence: 1.0,\n * // ...\n * // }\n * ```\n */\nexport function extractNeutral(\n token: Token,\n transformationMap: TransformationMap,\n cleanedText?: string,\n): NeutralCitation {\n const { text, span } = token\n\n // Parse year-court-documentNumber. Two-step:\n // 1. Try the Mississippi 4-segment hyphenated form (#233):\n // year-caseType-number-appellateTrack, e.g., \"2010-CT-01234-SCT\".\n // Court is composed as `${caseType}-${appellateTrack}` so the single\n // `court` field preserves the full sovereign identifier.\n // 2. Try the 3-segment hyphenated form (NM/Ohio/NC) or the whitespace form.\n let year: number\n let court: string\n let documentNumber: string\n let unpublished = false\n let spans: NeutralComponentSpans | undefined\n\n const msMatch = /^(\\d{4})-([A-Z]+)-(\\d+)-([A-Z]+)$/d.exec(text)\n if (msMatch) {\n year = Number.parseInt(msMatch[1], 10)\n court = `${msMatch[2]}-${msMatch[4]}`\n documentNumber = msMatch[3]\n if (msMatch.indices) {\n const caseTypeIndices = msMatch.indices[2]!\n const trackIndices = msMatch.indices[4]!\n // Span covers the case-type token through the appellate-track token so\n // the position range reflects the combined court identifier.\n const courtIndices: [number, number] = [caseTypeIndices[0], trackIndices[1]]\n spans = {\n year: spanFromGroupIndex(span.cleanStart, msMatch.indices[1]!, transformationMap),\n court: spanFromGroupIndex(span.cleanStart, courtIndices, transformationMap),\n documentNumber: spanFromGroupIndex(\n span.cleanStart,\n msMatch.indices[3]!,\n transformationMap,\n ),\n }\n }\n } else {\n // 3-segment forms: hyphenated (NM/Ohio/NC) or whitespace (UT/WI/IL/WL).\n // Trailing `(-U)?` captures Illinois Rule 23 unpublished marker (#230);\n // the suffix is consumed but excluded from `documentNumber`.\n const neutralRegex = /^(\\d{4})[-\\s]+(.+?)[-\\s]+(\\d+)(-U)?$/d\n const match = neutralRegex.exec(text)\n if (!match) {\n throw new Error(`Failed to parse neutral citation: ${text}`)\n }\n year = Number.parseInt(match[1], 10)\n court = match[2]\n documentNumber = match[3]\n if (match[4] === \"-U\") {\n unpublished = true\n }\n if (match.indices) {\n spans = {\n year: spanFromGroupIndex(span.cleanStart, match.indices[1]!, transformationMap),\n court: spanFromGroupIndex(span.cleanStart, match.indices[2]!, transformationMap),\n documentNumber: spanFromGroupIndex(\n span.cleanStart,\n match.indices[3]!,\n transformationMap,\n ),\n }\n }\n }\n\n // Look ahead in cleaned text for a trailing pincite (e.g., \", at *3\" on\n // Westlaw and Lexis citations). See #191.\n let pincite: number | undefined\n let pinciteInfo: PinciteInfo | undefined\n if (cleanedText) {\n const afterToken = cleanedText.substring(span.cleanEnd)\n const laMatch = NEUTRAL_PINCITE_LOOKAHEAD.exec(afterToken)\n if (laMatch) {\n pinciteInfo = parsePincite(laMatch[1]) ?? undefined\n // Neutral cites in state appellate practice use paragraph pinpoints\n // (`2015-NMCA-072, ¶ 2`) rather than page numbers. Fall back to the\n // paragraph number when no page is set so the top-level `pincite`\n // field reflects the pinpoint regardless of form. #311\n pincite = pinciteInfo?.page ?? pinciteInfo?.paragraph\n // Component span for pincite (#210). Indices are relative to afterToken,\n // which starts at span.cleanEnd in cleanedText.\n if (laMatch.indices?.[1]) {\n if (!spans) spans = {}\n spans.pincite = spanFromGroupIndex(\n span.cleanEnd,\n laMatch.indices[1],\n transformationMap,\n )\n }\n }\n }\n\n // Database vs. real-court routing (#294). Tokenizer captures \"WL\" or\n // \"U.S. LEXIS\" as the middle segment, which lands here as `court`. These\n // are vendor-database identifiers, not courts — route them to `database`\n // and leave `court` undefined so downstream consumers don't treat the\n // database tag as a court abbreviation.\n let database: string | undefined\n let courtOut: string | undefined = court\n if (isDatabaseIdentifier(court)) {\n database = court\n courtOut = undefined\n // The mistakenly-captured \"court\" span is meaningless for a database tag.\n if (spans) spans.court = undefined\n }\n\n // Trailing `(court date)` parenthetical lookahead (#294). For database\n // cites the trailing paren is the only place the real court appears —\n // `2001 WL 1077846 (N.D. Cal. Sept. 4, 2001)`. Reuses parseParenthetical\n // so the same court/date parser that handles case-cite parens applies.\n let date: StructuredDate | undefined\n if (cleanedText && database) {\n const afterToken = cleanedText.substring(span.cleanEnd)\n const parenMatch = NEUTRAL_PAREN_LOOKAHEAD.exec(afterToken)\n if (parenMatch) {\n const parsed = parseParenthetical(parenMatch[1])\n if (parsed.court) courtOut = parsed.court\n if (parsed.date) {\n date = parsed.date\n // Prefer the more-precise date.parsed.year over the cite's\n // documentary year if the trailing paren disambiguates it. The\n // tokenizer's year (e.g., 2001 in \"2001 WL ...\") is always the\n // citation year and typically matches the paren — leave year alone.\n }\n }\n }\n\n // Translate positions from clean → original\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Confidence: 1.0 (neutral format is unambiguous)\n const confidence = 1.0\n\n return {\n type: \"neutral\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n year,\n court: courtOut,\n ...(database ? { database } : {}),\n documentNumber,\n ...(unpublished ? { unpublished: true } : {}),\n pincite,\n pinciteInfo,\n ...(date ? { date } : {}),\n spans,\n }\n}\n","/**\n * Public Law Citation Extraction\n *\n * Parses tokenized public law citations to extract congress number and law number.\n * Examples: \"Pub. L. No. 116-283\", \"Pub. L. 117-58\"\n *\n * @module extract/extractPublicLaw\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { PublicLawCitation } from \"@/types/citation\"\nimport type { PublicLawComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\n\n/**\n * Extracts public law citation metadata from a tokenized citation.\n *\n * Parses token text to extract:\n * - Congress: Congress number (e.g., \"116\" from \"Pub. L. No. 116-283\")\n * - Law number: Law number within that Congress (e.g., \"283\")\n *\n * Confidence scoring:\n * - 0.9 (public law format is fairly standard)\n *\n * Note: Bill title extraction from nearby text is not implemented in Phase 2.\n * That requires context analysis in Phase 3.\n *\n * @param token - Token from tokenizer containing matched text and clean positions\n * @param transformationMap - Position mapping from clean → original text\n * @returns PublicLawCitation with parsed metadata and translated positions\n *\n * @example\n * ```typescript\n * const token = {\n * text: \"Pub. L. No. 116-283\",\n * span: { cleanStart: 10, cleanEnd: 29 },\n * type: \"publicLaw\",\n * patternId: \"public-law\"\n * }\n * const citation = extractPublicLaw(token, transformationMap)\n * // citation = {\n * // type: \"publicLaw\",\n * // congress: 116,\n * // lawNumber: 283,\n * // confidence: 0.9,\n * // ...\n * // }\n * ```\n */\nexport function extractPublicLaw(\n token: Token,\n transformationMap: TransformationMap,\n): PublicLawCitation {\n const { text, span } = token\n\n // Parse congress-lawNumber using regex\n // Pattern: \"Pub. L.\" (with optional \"No.\") + congress number + \"-\" + law number\n const publicLawRegex = /Pub\\.\\s?L\\.(?:\\s?No\\.)?\\s?(\\d+)-(\\d+)/d\n const match = publicLawRegex.exec(text)\n\n if (!match) {\n throw new Error(`Failed to parse public law citation: ${text}`)\n }\n\n const congress = Number.parseInt(match[1], 10)\n const lawNumber = Number.parseInt(match[2], 10)\n\n let spans: PublicLawComponentSpans | undefined\n if (match.indices) {\n spans = {\n congress: spanFromGroupIndex(span.cleanStart, match.indices[1]!, transformationMap),\n lawNumber: spanFromGroupIndex(span.cleanStart, match.indices[2]!, transformationMap),\n }\n }\n\n // Translate positions from clean → original\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Confidence: 0.9 (public law format is fairly standard)\n const confidence = 0.9\n\n return {\n type: \"publicLaw\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n congress,\n lawNumber,\n spans,\n }\n}\n","/**\n * Short-form Citation Extraction\n *\n * Parses tokenized short-form citations (Id., supra, short-form case) to extract\n * metadata. Short-form citations refer to earlier citations in the document.\n *\n * @module extract/extractShortForms\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { IdCitation, ShortFormCaseCitation, SupraCitation } from \"@/types/citation\"\nimport type {\n IdComponentSpans,\n ShortFormCaseComponentSpans,\n SupraComponentSpans,\n} from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { COMMON_REPORTERS } from \"./extractCase\"\nimport { parsePincite, type PinciteInfo } from \"./pincite\"\n\n/**\n * Strip leading citation signals (`See`, `See also`, `Cf.`, `Compare`,\n * `Accord`, `But see`, `But cf.`, `E.g.`) and sentence-initial connectors\n * (`Also`, `Then`, `In` (but never `In re`)) from a captured supra party name.\n *\n * The `SUPRA_PATTERN` tokenizer is greedy with leading capitalized words, so\n * `See Gall, supra` produces `partyName = \"See Gall\"` and prevents the\n * resolver from matching the supra to its `Gall v. Colon-Sylvain` antecedent.\n * The `In(?!\\s+re\\b)` negative lookahead preserves `In re Smith` — only the\n * bare `In` directly preceding a proper-name party gets stripped (#216).\n *\n * The original captured name is returned unchanged when stripping would leave\n * an empty string (defensive: prevents a wholesale signal token from blanking\n * out the party name).\n */\nconst SUPRA_PARTY_PREFIX_REGEX =\n /^(?:But\\s+(?:see|cf\\.?)|See(?:\\s+also)?(?:\\s*,\\s*e\\.\\s*g\\.?)?|Compare|Cf\\.?|Accord|E\\.\\s*g\\.?|Also|In(?!\\s+re\\b)|Then)\\s+/i\n\nfunction stripSupraPartyPrefix(raw: string): string {\n const stripped = raw.replace(SUPRA_PARTY_PREFIX_REGEX, \"\").trim()\n return stripped.length > 0 ? stripped : raw\n}\n\n/**\n * Trailing-parenthetical lookahead for short-form citations (#303).\n *\n * Captures content of a single `(...)` parenthetical immediately after the\n * citation core, allowing optional whitespace/comma between. The body uses\n * `[^()]*` (no nesting) — `parenthetical` is the raw text inside one set of\n * parens. Suitable for `Id. at N (Marsh)`, `Id. (citation omitted)`,\n * `Smith, supra (holding that ...)`, `Smith, 500 F.2d at 125 (citations omitted)`.\n */\nconst TRAILING_PAREN_REGEX = /^[\\s,]*\\(([^()]*)\\)/\n\n/**\n * Scan the cleaned text after a short-form citation's span end for an\n * immediately-trailing `(...)` parenthetical. Returns the inner text\n * (excluding the parens) or `undefined` if none found. #303\n */\nfunction extractTrailingParenthetical(\n cleanedText: string | undefined,\n cleanEnd: number,\n): string | undefined {\n if (!cleanedText) return undefined\n const after = cleanedText.slice(cleanEnd)\n const m = TRAILING_PAREN_REGEX.exec(after)\n if (!m) return undefined\n const content = m[1].trim()\n return content.length > 0 ? content : undefined\n}\n\n/**\n * Extracts Id. citation metadata from a tokenized citation.\n *\n * Parses token text to extract:\n * - Pincite: Optional page reference (e.g., \"253\" from \"Id. at 253\")\n *\n * Confidence scoring:\n * - 1.0 (Id. format is unambiguous and standardized)\n *\n * @param token - Token from tokenizer containing matched text and clean positions\n * @param transformationMap - Position mapping from clean → original text\n * @returns IdCitation with parsed metadata and translated positions\n *\n * @example\n * ```typescript\n * const token = {\n * text: \"Id. at 253\",\n * span: { cleanStart: 10, cleanEnd: 20 },\n * type: \"case\",\n * patternId: \"id\"\n * }\n * const citation = extractId(token, transformationMap)\n * // citation = {\n * // type: \"id\",\n * // pincite: 253,\n * // confidence: 1.0,\n * // ...\n * // }\n * ```\n */\nexport function extractId(\n token: Token,\n transformationMap: TransformationMap,\n cleanedText?: string,\n): IdCitation {\n const { text, span } = token\n\n // Parse Id. with optional pincite.\n // Pattern: Id. or Ibid. with optional comma + \"at [page]\" (handles \"Id., at 5\").\n //\n // Punctuation tolerance (#305):\n // - Optional whitespace before the period — `Id . at 326`, `Ibid .`\n // (OCR + older typesetting).\n // - Comma instead of period — `Id, at 1483` — guarded by `(?=\\s+at\\s)`\n // so bare `Id,` in prose (\"his Id, but ...\") is not misread.\n //\n // Group layout: 1=initial char (\"I\"/\"i\"), 2=`.` when canonical form,\n // 3=`,` when typo form (mutually exclusive with 2), 4=connector before\n // pincite (`, ` Connecticut-style, `,? at`, or `,? (?=¶)`), 5=pincite.\n //\n // Connector alternation accepts three forms (#353):\n // a) `, <pincite>` — Connecticut comma-pincite (`Id., 253`)\n // b) `[, ]?at <pincite>` — Bluebook at-form, optional leading comma\n // c) `[, ]?(?=¶|para)` — paragraph marker\n //\n // Pincite accepts optional \"*\" prefix for star-pagination (#191), an optional\n // trailing footnote suffix \" n.14\" / \" nn.14-15\" (#202), an optional\n // `p.` / `pp.` prefix for CSM form (`Id. at p. 125`; see #236), and\n // `¶` / `¶¶` / `para.` / `paras.` paragraph markers (#204). When the\n // pincite is a paragraph form, `at` is optional (`Id. ¶ 12`).\n const idRegex = /([Ii])(?:d|bid)\\s*(?:(\\.)|(,)(?=\\s+at\\s))(?:(,\\s+|,?\\s+(?:at\\s+(?:pp?\\.\\s*)?|(?=¶|paras?\\.?\\b)))(\\*?\\d+(?:\\s*[-–]\\s*\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?|¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?))?/d\n const match = idRegex.exec(text)\n\n if (!match) {\n throw new Error(`Failed to parse Id. citation: ${text}`)\n }\n\n const firstChar = match[1]\n // Non-standard punctuation signals:\n // - `isTypoComma`: comma replacing the period (`Id, at 1483`) — lower confidence\n // - `hasComma`: post-period comma (`Id., at 253` or `Id., 253`) — slightly\n // lower confidence than canonical. Connector capture (group 4) starts\n // with `,` for both the post-period-comma-at form and the Connecticut\n // comma-pincite form.\n const isTypoComma = match[3] === \",\"\n const hasComma = isTypoComma || match[4]?.startsWith(\",\") === true\n const pinciteInfo: PinciteInfo | undefined = match[5]\n ? (parsePincite(match[5]) ?? undefined)\n : undefined\n const pincite = pinciteInfo?.page\n\n // Component span for pincite (#210)\n let spans: IdComponentSpans | undefined\n if (match[5] && match.indices?.[5]) {\n spans = {\n pincite: spanFromGroupIndex(span.cleanStart, match.indices[5], transformationMap),\n }\n }\n\n // Confidence scoring based on variant\n let confidence = 1.0\n const isLowercase = firstChar === \"i\"\n if (isLowercase) confidence = 0.85 // Lowercase id. is non-standard\n if (hasComma) confidence = Math.min(confidence, 0.9) // Comma variant (Id., at N)\n if (isTypoComma) confidence = Math.min(confidence, 0.7) // `Id, at N` typo (#305)\n\n // Context validation: check whether Id. appears in a citation context.\n // Real Id. citations follow sentence-ending punctuation, semicolons,\n // or paragraph breaks — not mid-sentence prose like \"The Id. card\".\n if (cleanedText && span.cleanStart > 0) {\n const preceding = cleanedText.slice(Math.max(0, span.cleanStart - 20), span.cleanStart)\n // Look for the last non-whitespace character before Id.\n const trimmed = preceding.trimEnd()\n if (trimmed.length > 0) {\n const lastChar = trimmed[trimmed.length - 1]\n // Citation contexts end with: . ; ) ] — or follow certain patterns\n const isCitationContext = /[.;)\\]—:]$/.test(trimmed)\n if (!isCitationContext) {\n // Mid-sentence Id. (e.g., \"The Id. card\") — likely not a citation\n confidence = Math.min(confidence, 0.4)\n }\n }\n }\n\n // Translate positions from clean → original\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Trailing parenthetical (#303): `Id. at 770 (Marsh)`, `Id. (citation omitted)`.\n const parenthetical = extractTrailingParenthetical(cleanedText, span.cleanEnd)\n\n return {\n type: \"id\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n pincite,\n pinciteInfo,\n ...(parenthetical ? { parenthetical } : {}),\n spans,\n }\n}\n\n/**\n * Extracts supra citation metadata from a tokenized citation.\n *\n * Parses token text to extract:\n * - Party name: Name preceding \"supra\" (e.g., \"Smith\" from \"Smith, supra\")\n * - Pincite: Optional page reference (e.g., \"460\" from \"Smith, supra, at 460\")\n *\n * Confidence scoring:\n * - 0.9 (supra format is fairly standard but party name extraction can vary)\n *\n * @param token - Token from tokenizer containing matched text and clean positions\n * @param transformationMap - Position mapping from clean → original text\n * @returns SupraCitation with parsed metadata and translated positions\n *\n * @example\n * ```typescript\n * const token = {\n * text: \"Smith, supra, at 460\",\n * span: { cleanStart: 10, cleanEnd: 30 },\n * type: \"case\",\n * patternId: \"supra\"\n * }\n * const citation = extractSupra(token, transformationMap)\n * // citation = {\n * // type: \"supra\",\n * // partyName: \"Smith\",\n * // pincite: 460,\n * // confidence: 0.9,\n * // ...\n * // }\n * ```\n */\nexport function extractSupra(\n token: Token,\n transformationMap: TransformationMap,\n cleanedText?: string,\n): SupraCitation {\n const { text, span } = token\n\n // Bracketed supra (#306): `State v. Jarzbek, [supra, 705]` /\n // `[supra at 78-82]`. Connecticut Supreme/Appellate convention. The\n // comma-pincite shape `[supra, 705]` accepts no `at` before the page.\n // When the token text matches this shape, parse it via the bracketed\n // regex; otherwise fall through to the canonical partySupraRegex.\n const bracketedSupraRegex =\n /(?:\\b([A-Z][a-zA-Z''\\-]+\\.?(?:(?:\\s+v\\.?\\s+|\\s+&\\s+|,\\s+|\\s+)[A-Z][a-zA-Z''\\-]+\\.?)*)\\s*,?\\s+)?\\[supra(?:(?:,\\s+|\\s+at\\s+(?:pp?\\.\\s*)?)(\\d+(?:[-–—]\\d+)?))?\\]/d\n const bracketedMatch = text.includes(\"[supra\") ? bracketedSupraRegex.exec(text) : null\n\n // Try party-name pattern first: \"Smith, supra [note N] [, at page]\".\n // Party-name capture mirrors SUPRA_PATTERN in src/patterns/shortForm.ts:\n // `v.` / `&` / `,` continuations (#301) so multi-word names like\n // `Thorn Americas, Inc.` and `Walker & Horwich` capture the whole\n // caption rather than just the last word. `In re` prefix is NOT included\n // — the resolver's BKTree indexes full cites without the prefix (#216 /\n // #21), and adding it here would break supra resolution for `In re X`.\n // Pincite accepts optional \"*\" prefix for star-pagination (#191), an optional\n // range end / `p.` / `pp.` prefix for CSM form (#236), an optional trailing\n // footnote suffix (#202), and `¶` / `¶¶` / `para.` / `paras.` paragraph\n // markers (#204). When the pincite is a paragraph form, `at` is optional.\n // Connector before pincite accepts the Connecticut comma-pincite form\n // (`Smith, supra, 522`) alongside the Bluebook `, at` and paragraph\n // forms (#353).\n const partySupraRegex =\n /\\b([A-Z][a-zA-Z''\\-]+\\.?(?:(?:\\s+v\\.?\\s+|\\s+&\\s+|,\\s+|\\s+)[A-Z][a-zA-Z''\\-]+\\.?)*)\\s*,?\\s+supra(?:\\s+note\\s+(\\d+))?(?:(?:,\\s+|,?\\s+(?:at\\s+(?:pp?\\.\\s*)?|(?=¶|paras?\\.?\\b)))(\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?|¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?))?/d\n const partyMatch = bracketedMatch ? null : partySupraRegex.exec(text)\n\n // Fallback: standalone supra — \"supra note N\", \"supra at N\", \"supra § N\".\n // The `at` page accepts the same `p.` / `pp.` prefix and range form (#236)\n // plus paragraph markers (#204).\n const standaloneRegex =\n /supra(?:\\s+note\\s+(\\d+)(?:,?\\s+(?:at\\s+(?:pp?\\.\\s*)?|(?=¶|paras?\\.?\\b))(\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?|¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?))?|\\s+(?:at\\s+(?:pp?\\.\\s*)?|(?=¶|paras?\\.?\\b))(\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?|¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?))?/d\n const match = bracketedMatch || partyMatch || standaloneRegex.exec(text)\n\n if (!match) {\n throw new Error(`Failed to parse supra citation: ${text}`)\n }\n\n let partyName: string | undefined\n let pinciteInfo: PinciteInfo | undefined\n let confidence: number\n let pinciteGroupIdx: number | undefined\n\n if (bracketedMatch) {\n // Bracketed form (#306): group 1 = optional party, group 2 = optional pincite.\n partyName = bracketedMatch[1] ? stripSupraPartyPrefix(bracketedMatch[1]) : undefined\n pinciteInfo = bracketedMatch[2]\n ? (parsePincite(bracketedMatch[2]) ?? undefined)\n : undefined\n confidence = partyName ? 0.9 : 0.8\n if (bracketedMatch[2]) pinciteGroupIdx = 2\n } else if (partyMatch) {\n partyName = stripSupraPartyPrefix(partyMatch[1])\n pinciteInfo = partyMatch[3]\n ? (parsePincite(partyMatch[3]) ?? undefined)\n : undefined\n confidence = 0.9\n if (partyMatch[3]) pinciteGroupIdx = 3\n } else {\n // Standalone supra — no party name\n partyName = undefined\n const noteAtPage = match[2]\n const atPage = match[3]\n const rawPin = noteAtPage ?? atPage\n pinciteInfo = rawPin ? (parsePincite(rawPin) ?? undefined) : undefined\n confidence = 0.8 // Slightly lower — standalone supra is less specific\n if (noteAtPage) pinciteGroupIdx = 2\n else if (atPage) pinciteGroupIdx = 3\n }\n\n const pincite = pinciteInfo?.page\n\n // Component span for pincite (#210)\n let spans: SupraComponentSpans | undefined\n if (pinciteGroupIdx !== undefined && match.indices?.[pinciteGroupIdx]) {\n spans = {\n pincite: spanFromGroupIndex(\n span.cleanStart,\n match.indices[pinciteGroupIdx],\n transformationMap,\n ),\n }\n }\n\n // Translate positions from clean → original\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Trailing parenthetical (#303): `Smith, supra (holding ...)`.\n const parenthetical = extractTrailingParenthetical(cleanedText, span.cleanEnd)\n\n return {\n type: \"supra\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n partyName,\n pincite,\n pinciteInfo,\n ...(parenthetical ? { parenthetical } : {}),\n spans,\n }\n}\n\n/**\n * Extracts short-form case citation metadata from a tokenized citation.\n *\n * Parses token text to extract:\n * - Volume: Volume number\n * - Reporter: Reporter abbreviation\n * - Pincite: Page reference (from \"at [page]\" pattern)\n *\n * Confidence scoring:\n * - 0.7 (short-form case citations are more ambiguous than full citations)\n *\n * @param token - Token from tokenizer containing matched text and clean positions\n * @param transformationMap - Position mapping from clean → original text\n * @returns ShortFormCaseCitation with parsed metadata and translated positions\n *\n * @example\n * ```typescript\n * const token = {\n * text: \"500 F.2d at 125\",\n * span: { cleanStart: 10, cleanEnd: 25 },\n * type: \"case\",\n * patternId: \"short-form-case\"\n * }\n * const citation = extractShortFormCase(token, transformationMap)\n * // citation = {\n * // type: \"shortFormCase\",\n * // volume: 500,\n * // reporter: \"F.2d\",\n * // pincite: 125,\n * // confidence: 0.7,\n * // ...\n * // }\n * ```\n */\nexport function extractShortFormCase(\n token: Token,\n transformationMap: TransformationMap,\n cleanedText?: string,\n): ShortFormCaseCitation {\n const { text, span } = token\n\n // Parse [Party,] volume-reporter-[,]-at-page.\n // Pattern: optional Party name then number space abbreviation [, ] at space number.\n // Supports reporters with 1-2 letter ordinal suffixes (e.g., F.4th, Cal.4th).\n // Handles comma-before-at: \"597 U.S., at 721\", \"116 F.4th, at 1193\".\n // Pincite accepts optional \"*\" prefix for star-pagination (#191), an optional\n // range end \"462-65\" / \"462-*65\" (#201), an optional trailing footnote\n // suffix \" n.14\" / \" nn.14-15\" (#202), an optional `p.` / `pp.` prefix for\n // CSM form (`18 Cal.4th at p. 717`; see #236), and `¶` / `¶¶` / `para.` /\n // `paras.` paragraph markers (#204).\n // Optional leading party-name group (#278) captures Bluebook back-references\n // (`Smith, 500 F.2d at 125`). Group order:\n // 1: party name (optional, undefined for bare form)\n // 2: volume\n // 3: reporter\n // 4: pincite\n // Party-name capture mirrors SHORT_FORM_CASE_PATTERN: `v.` / `&` / `,`\n // continuations (#301). `In re` prefix intentionally omitted (see\n // partySupraRegex above for rationale). Pincite-prefix alternation also\n // accepts spelled-out `page` / `pages` (#344).\n const shortFormRegex =\n /(?:([A-Z][a-zA-Z''\\-]+\\.?(?:(?:\\s+v\\.?\\s+|\\s+&\\s+|,\\s+|\\s+)[A-Z][a-zA-Z''\\-]+\\.?)*),\\s+)?(\\d+(?:-\\d+)?)\\s+([A-Z][A-Za-z.''\\s]+?(?:\\d[a-z]{1,2})?)\\s*,?\\s+at\\s+(?:pp?\\.\\s*|pages?\\s+)?(\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?|¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?)/d\n const match = shortFormRegex.exec(text)\n\n if (!match) {\n throw new Error(`Failed to parse short-form case citation: ${text}`)\n }\n\n const rawPartyName = match[1]\n const rawVolume = match[2]\n const volume = /^\\d+$/.test(rawVolume) ? Number.parseInt(rawVolume, 10) : rawVolume\n const reporter = match[3].trim() // Remove trailing spaces\n const pinciteInfo: PinciteInfo | undefined = parsePincite(match[4]) ?? undefined\n const pincite = pinciteInfo?.page\n\n // Strip leading citation signals from the captured party name (#216 helper).\n // The optional party-name group itself doesn't include signal prefixes —\n // the outer SHORT_FORM_CASE_PATTERN's `\\b` anchor lands at the signal word\n // (e.g., `See` is matched as the first capitalized token, then `Smith` as\n // the second). `stripSupraPartyPrefix` peels off any leading signal /\n // sentence-initial connector, mirroring the supra handling.\n let partyName: string | undefined\n let partyNameNormalized: string | undefined\n if (rawPartyName) {\n partyName = stripSupraPartyPrefix(rawPartyName)\n partyNameNormalized = partyName.toLowerCase().replace(/\\s+/g, \" \").trim()\n }\n\n // Component span for pincite (#210)\n let spans: ShortFormCaseComponentSpans | undefined\n if (match.indices?.[4]) {\n spans = {\n pincite: spanFromGroupIndex(span.cleanStart, match.indices[4], transformationMap),\n }\n }\n\n // Translate positions from clean → original\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Confidence: base 0.4, boosted for recognized reporters\n let confidence = 0.4\n if (COMMON_REPORTERS.has(reporter)) {\n confidence += 0.3\n }\n\n // Trailing parenthetical (#303): `Smith, 500 F.2d at 125 (citations omitted)`.\n const parenthetical = extractTrailingParenthetical(cleanedText, span.cleanEnd)\n\n return {\n type: \"shortFormCase\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n volume,\n reporter,\n pincite,\n pinciteInfo,\n partyName,\n partyNameNormalized,\n ...(parenthetical ? { parenthetical } : {}),\n spans,\n }\n}\n","/**\n * Shared body-parsing utilities for statute extractors.\n *\n * Extracts section number, subsection chain, and et seq. indicator\n * from the \"body\" portion of a tokenized statute citation.\n *\n * @module extract/statutes/parseBody\n */\n\n/** Separate subsection chain from section number */\nconst SUBSECTION_RE = /^([^(]+?)\\s*((?:\\([^)]*\\))*)$/\n\n/** Et seq. at end of string */\nconst ET_SEQ_RE = /\\s*et\\s+seq\\.?\\s*$/i\n\nexport interface ParsedBody {\n section: string\n subsection?: string\n hasEtSeq: boolean\n}\n\n/**\n * Parse a raw body string into section, subsection, and et seq.\n *\n * @example\n * parseBody(\"1983(a)(1) et seq.\") → { section: \"1983\", subsection: \"(a)(1)\", hasEtSeq: true }\n * parseBody(\"122.26(b)(14)\") → { section: \"122.26\", subsection: \"(b)(14)\", hasEtSeq: false }\n * parseBody(\"1983\") → { section: \"1983\", hasEtSeq: false }\n */\nexport function parseBody(rawBody: string): ParsedBody {\n // Strip et seq. — single replace + compare (avoids double regex execution)\n const stripped = rawBody.replace(ET_SEQ_RE, \"\")\n const hasEtSeq = stripped !== rawBody\n\n // Split section from subsections: \"1983(a)(1)\" → section=\"1983\", subsection=\"(a)(1)\"\n const trimmed = stripped.trim()\n const subMatch = SUBSECTION_RE.exec(trimmed)\n const subGroups = subMatch?.[2]\n\n if (subMatch !== null && subGroups) {\n return {\n section: subMatch[1].trim(),\n subsection: subGroups,\n hasEtSeq,\n }\n }\n\n return { section: trimmed, hasEtSeq }\n}\n","/**\n * Abbreviated-Code Statute Extraction (Family 3)\n *\n * Parses tokenized citations from states that use compact abbreviations\n * (e.g., \"Fla. Stat.\", \"R.C.\", \"MCL\"). Looks up the abbreviation in the\n * knownCodes registry to determine jurisdiction.\n *\n * Jurisdictions: FL, OH, MI, UT, CO, WA, NC, GA, PA, IN, NJ, DE\n *\n * @module extract/statutes/extractAbbreviated\n */\n\nimport { findAbbreviatedCode } from \"@/data/knownCodes\"\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\n// Section body: period only allowed when followed by alphanumeric so a\n// trailing sentence period is never captured (#283).\n// Section connector mirrors the tokenizer pattern: `§`, `§§`, or the\n// spelled-out word `section(s)` / `Section(s)` (#348). Without this, the\n// lazy abbreviation capture would absorb the word `section` and break\n// `findAbbreviatedCode` lookups (e.g., `Arkansas Code Annotated section\n// 11-9-102` would emit abbrevText=\"Arkansas Code Annotated section\").\nconst ABBREVIATED_RE =\n /^(?:(\\d+)\\s+)?(.+?)\\s*(?:§§?|[Ss]ections?)?\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)$/d\n\nexport function extractAbbreviated(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = ABBREVIATED_RE.exec(text)\n\n let title: number | undefined\n let abbrevText: string\n let rawBody: string\n\n if (match) {\n title = match[1] ? Number.parseInt(match[1], 10) : undefined\n abbrevText = match[2].trim()\n rawBody = match[3]\n } else {\n abbrevText = text\n rawBody = \"\"\n }\n\n const codeEntry = findAbbreviatedCode(abbrevText)\n const jurisdiction = codeEntry?.jurisdiction\n // Normalize OCR/spacing variants (`AR.S.`, `ARS`, `A. R.S.`) to the canonical\n // short abbreviation when the input doesn't already match a recognized\n // pattern verbatim — the stripped-form fallback in `findAbbreviatedCode`\n // returns the canonical entry, so `entry.abbreviation` is the right\n // normalized `code`. Exact matches keep their original-form `code`. #348\n const code =\n codeEntry && !codeEntry.patterns.some((p) => p.toLowerCase() === abbrevText.toLowerCase())\n ? codeEntry.abbreviation\n : abbrevText\n\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match?.indices) {\n spans = {}\n if (match.indices[1])\n spans.title = spanFromGroupIndex(span.cleanStart, match.indices[1], transformationMap)\n if (match.indices[2])\n spans.code = spanFromGroupIndex(span.cleanStart, match.indices[2], transformationMap)\n if (match.indices[3] && section) {\n const bodyStart = match.indices[3][0]\n // Use section without trailing sentence punctuation for span boundary.\n // Note: section comes from parseBody() which strips et seq. and splits\n // subsections — the leading text still matches the raw match position.\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n const hasSection = text.includes(\"§\")\n let confidence: number\n if (codeEntry && hasSection) {\n confidence = 0.95\n } else if (codeEntry) {\n confidence = 0.85\n } else if (hasSection) {\n confidence = 0.6\n } else {\n confidence = 0.4\n }\n if (title !== undefined) confidence += 0.05\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n title,\n code,\n section,\n subsection,\n pincite: subsection,\n jurisdiction,\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Pre-1975 Alabama Code Extraction\n *\n * Parses citations to the Code of Alabama 1940 — the dominant pre-1975\n * Alabama statutory citation form. Modern Alabama opinions still cite this\n * version when referencing the historical text of a statute:\n *\n * Code 1940, T. 15, § 389\n * Title 26, Section 214, Code of Alabama 1940, as Recompiled 1958\n * Tit. 52, § 361\n *\n * Three tokenizer patternIds route here:\n * - `ala-code-prefix` → Code-first form (year hardcoded to 1940)\n * - `ala-title-trailer` → Title-first with mandatory Code trailer\n * - `ala-tit-bare` → abbreviated `Tit.` form (optional Code trailer)\n *\n * @module extract/statutes/extractAlaCode1940\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\n// Anchored re-match regexes for each Alabama patternId — mirror the\n// tokenizer patterns in `src/patterns/statutePatterns.ts` so the extractor\n// re-parses the same span the tokenizer captured. `d` flag enables\n// `match.indices` for component spans.\n\nconst ALA_CODE_PREFIX_RE =\n /^Code(?:\\s+of\\s+Alabama)?,?\\s+1940,?\\s+T(?:itle|it)?\\.\\s+(\\d+),?\\s+§\\s+(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*)$/d\n\nconst ALA_TITLE_TRAILER_RE =\n /^Title\\s+(\\d+),?\\s+(?:§|Sec(?:tion)?s?\\.?)\\s+(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*),?\\s+Code(?:\\s+of\\s+Alabama)?,?\\s+(\\d{4})(?:,?\\s+(?:as\\s+)?[Rr]ecompiled\\s+(\\d{4}))?$/d\n\nconst ALA_TIT_BARE_RE =\n /^Tit\\.\\s+(\\d+),?\\s+§\\s+(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*)(?:,?\\s+Code(?:\\s+of\\s+Alabama)?,?\\s+(\\d{4})(?:,?\\s+(?:as\\s+)?[Rr]ecompiled\\s+(\\d{4}))?)?$/d\n\nexport function extractAlaCode1940(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n\n let titleRaw: string\n let sectionRaw: string\n let year: number | undefined\n let recompiledYear: number | undefined\n let titleGroupIdx: number\n let sectionGroupIdx: number\n let match: RegExpExecArray\n\n switch (token.patternId) {\n case \"ala-code-prefix\": {\n match = ALA_CODE_PREFIX_RE.exec(text)!\n titleRaw = match[1]\n sectionRaw = match[2]\n year = 1940 // prefix asserts the 1940 edition\n titleGroupIdx = 1\n sectionGroupIdx = 2\n break\n }\n case \"ala-title-trailer\": {\n match = ALA_TITLE_TRAILER_RE.exec(text)!\n titleRaw = match[1]\n sectionRaw = match[2]\n year = Number.parseInt(match[3], 10)\n if (match[4]) recompiledYear = Number.parseInt(match[4], 10)\n titleGroupIdx = 1\n sectionGroupIdx = 2\n break\n }\n default: {\n // ala-tit-bare\n match = ALA_TIT_BARE_RE.exec(text)!\n titleRaw = match[1]\n sectionRaw = match[2]\n if (match[3]) year = Number.parseInt(match[3], 10)\n if (match[4]) recompiledYear = Number.parseInt(match[4], 10)\n titleGroupIdx = 1\n sectionGroupIdx = 2\n break\n }\n }\n\n const title = Number.parseInt(titleRaw, 10)\n const { section, subsection, hasEtSeq } = parseBody(sectionRaw)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n const titleIdx = match.indices[titleGroupIdx]\n if (titleIdx) spans.title = spanFromGroupIndex(span.cleanStart, titleIdx, transformationMap)\n const bodyIdx = match.indices[sectionGroupIdx]\n if (bodyIdx && section) {\n const bodyStart = bodyIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n // Confidence: 0.95 baseline (closed-shape Alabama Code matches are\n // unambiguous when the Code prefix / trailer or `Tit.` abbreviation is\n // present, which all three patternIds enforce). +0.05 with a subsection.\n let confidence = 0.95\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n title,\n code: \"Code of Alabama 1940\",\n section,\n subsection,\n pincite: subsection,\n year,\n recompiledYear,\n jurisdiction: \"AL\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * California bare statute code abbreviations (#296).\n *\n * In single-jurisdiction California practice, counsel and courts establish\n * the California jurisdiction at the top of a document and then drop to\n * bare-code abbreviations for the rest — `Pen. Code § 148`,\n * `Code Civ. Proc., § 1021.5`, `Veh. Code § 23550.5`. The fully-qualified\n * `Cal. Penal Code § 148` form is handled by the existing `named-code`\n * tokenizer pattern; this file supports the bare-code variant via a\n * closed alternation so non-citation prose like \"Insurance Law applies\"\n * cannot accidentally match.\n *\n * Each entry's canonical form is the string returned in the\n * `StatuteCitation.code` field. The `regexAlternative` is what the\n * tokenizer matches in source text — periods are optional/spaces flexible\n * to handle typographic variation.\n */\n\nexport interface CaBareCodeEntry {\n /** Canonical code name returned in `StatuteCitation.code` */\n canonical: string\n /** Regex fragment for tokenizer alternation (periods/whitespace flexible) */\n regexFragment: string\n}\n\n/**\n * Order matters: list longest-first so PEG-style ordered choice picks the\n * most specific match before any shorter prefix. Example: `Code Civ. Proc.`\n * must come before any rule that could match just `Code` or `Civ. Code`.\n */\nexport const caBareCodeEntries: CaBareCodeEntry[] = [\n // Multi-word \"Code <Subject> Proc.\" forms come first — longest prefixes\n { canonical: \"Code Civ. Proc.\", regexFragment: \"Code\\\\s+Civ\\\\.?\\\\s+Proc\\\\.?\" },\n { canonical: \"Code Crim. Proc.\", regexFragment: \"Code\\\\s+Crim\\\\.?\\\\s+Proc\\\\.?\" },\n\n // Two-word \"<Subject> & <Subject> Code\" / \"<Subject> Code\" forms\n { canonical: \"Bus. & Prof. Code\", regexFragment: \"Bus\\\\.?\\\\s*&\\\\s*Prof\\\\.?\\\\s+Code\" },\n { canonical: \"Welf. & Inst. Code\", regexFragment: \"Welf\\\\.?\\\\s*&\\\\s*Inst\\\\.?\\\\s+Code\" },\n { canonical: \"Health & Safety Code\", regexFragment: \"Health\\\\s*&\\\\s*Safety\\\\s+Code\" },\n { canonical: \"Fish & Game Code\", regexFragment: \"Fish\\\\s*&\\\\s*Game\\\\s+Code\" },\n { canonical: \"Food & Agric. Code\", regexFragment: \"Food\\\\s*&\\\\s*Agric\\\\.?\\\\s+Code\" },\n { canonical: \"Harb. & Nav. Code\", regexFragment: \"Harb\\\\.?\\\\s*&\\\\s*Nav\\\\.?\\\\s+Code\" },\n { canonical: \"Mil. & Vet. Code\", regexFragment: \"Mil\\\\.?\\\\s*&\\\\s*Vet\\\\.?\\\\s+Code\" },\n { canonical: \"Rev. & Tax. Code\", regexFragment: \"Rev\\\\.?\\\\s*&\\\\s*Tax\\\\.?\\\\s+Code\" },\n { canonical: \"Sts. & Hy. Code\", regexFragment: \"Sts\\\\.?\\\\s*&\\\\s*Hy\\\\.?\\\\s+Code\" },\n\n // Two-word abbreviated \"<Subject>. <Type>. Code\"\n { canonical: \"Pub. Util. Code\", regexFragment: \"Pub\\\\.?\\\\s+Util\\\\.?\\\\s+Code\" },\n { canonical: \"Pub. Cont. Code\", regexFragment: \"Pub\\\\.?\\\\s+Cont\\\\.?\\\\s+Code\" },\n { canonical: \"Pub. Resources Code\", regexFragment: \"Pub\\\\.?\\\\s+Resources\\\\s+Code\" },\n { canonical: \"Unemp. Ins. Code\", regexFragment: \"Unemp\\\\.?\\\\s+Ins\\\\.?\\\\s+Code\" },\n\n // Single-subject \"<Subject>. Code\" forms — alphabetical\n { canonical: \"Civ. Code\", regexFragment: \"Civ\\\\.?\\\\s+Code\" },\n { canonical: \"Corp. Code\", regexFragment: \"Corp\\\\.?\\\\s+Code\" },\n { canonical: \"Educ. Code\", regexFragment: \"Educ\\\\.?\\\\s+Code\" },\n { canonical: \"Elec. Code\", regexFragment: \"Elec\\\\.?\\\\s+Code\" },\n { canonical: \"Evid. Code\", regexFragment: \"Evid\\\\.?\\\\s+Code\" },\n { canonical: \"Fam. Code\", regexFragment: \"Fam\\\\.?\\\\s+Code\" },\n { canonical: \"Gov. Code\", regexFragment: \"Gov\\\\.?\\\\s+Code\" },\n { canonical: \"Ins. Code\", regexFragment: \"Ins\\\\.?\\\\s+Code\" },\n { canonical: \"Lab. Code\", regexFragment: \"Lab\\\\.?\\\\s+Code\" },\n { canonical: \"Pen. Code\", regexFragment: \"Pen\\\\.?\\\\s+Code\" },\n { canonical: \"Prob. Code\", regexFragment: \"Prob\\\\.?\\\\s+Code\" },\n { canonical: \"Veh. Code\", regexFragment: \"Veh\\\\.?\\\\s+Code\" },\n { canonical: \"Water Code\", regexFragment: \"Water\\\\s+Code\" },\n]\n\n/**\n * Build the bare-code tokenizer regex from the alternation above.\n *\n * Capture groups:\n * (1) bare code name (matched alternative)\n * (2) section body (digits + optional alphanumeric / subsections / et seq.)\n *\n * Alternation is sorted by regex length descending so longer-prefix codes\n * (`Code Civ. Proc.`, `Welf. & Inst. Code`) match before shorter ones.\n */\nexport function buildCaBareCodeRegex(): RegExp {\n const fragments = [...caBareCodeEntries]\n .sort((a, b) => b.regexFragment.length - a.regexFragment.length)\n .map((e) => e.regexFragment)\n const alternation = fragments.join(\"|\")\n return new RegExp(\n `\\\\b(${alternation})\\\\s*,?\\\\s*§§?\\\\s*(\\\\d+(?:[A-Za-z0-9:/-]|\\\\.(?=[A-Za-z0-9]))*(?:\\\\([^)]*\\\\))*(?:\\\\s*et\\\\s+seq\\\\.?)?)`,\n \"g\",\n )\n}\n\n/**\n * Find the canonical CA bare-code name from a raw token match.\n * Normalizes whitespace/period variation back to the canonical string\n * so the StatuteCitation `code` field is stable across input variations.\n */\nexport function findCaBareCode(rawText: string): string | undefined {\n const normalized = rawText.replace(/\\s+/g, \" \").trim()\n for (const entry of caBareCodeEntries) {\n const fragmentRe = new RegExp(`^${entry.regexFragment}$`, \"i\")\n if (fragmentRe.test(normalized)) return entry.canonical\n }\n return undefined\n}\n","/**\n * California Bare-Code Statute Extraction (#296)\n *\n * Parses tokenized citations to California codes that lack the `Cal.`\n * jurisdiction prefix — `Pen. Code § 148`, `Code Civ. Proc., § 1021.5`,\n * `Bus. & Prof. Code § 17200`. The fully-qualified form (`Cal. Penal\n * Code § 148`) is handled by `extractNamedCode`; this extractor\n * recognizes the bare form via the closed alternation defined in\n * `src/data/caBareCodes.ts`.\n *\n * All matches produce `jurisdiction: \"CA\"`.\n *\n * @module extract/statutes/extractCaBareCode\n */\n\nimport { findCaBareCode } from \"@/data/caBareCodes\"\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\n/** Match shape: <bare code name> [,] § <body>. Indices flag enables span computation. */\nconst CA_BARE_CODE_RE =\n /^(.+?)\\s*,?\\s*§§?\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)$/d\n\nexport function extractCaBareCode(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n // The tokenizer's closed-alternation regex guarantees a match here; the\n // extractor's regex is structurally equivalent to that tokenizer pattern,\n // so `match` is always non-null for tokens routed to this extractor.\n const match = CA_BARE_CODE_RE.exec(text)!\n const rawCodeText = match[1].trim()\n const rawBody = match[2]\n\n // Normalize back to canonical bare-code form (\"Pen. Code\", \"Code Civ. Proc.\").\n // `findCaBareCode` is guaranteed to hit because the tokenizer only emits\n // tokens whose code text matched one of the canonical alternations.\n const code = findCaBareCode(rawCodeText)!\n\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match?.indices) {\n spans = {}\n if (match.indices[1])\n spans.code = spanFromGroupIndex(span.cleanStart, match.indices[1], transformationMap)\n if (match.indices[2] && section) {\n const bodyStart = match.indices[2][0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n // Confidence: bare-code matches come from a closed alternation, so the\n // jurisdiction inference is reliable. Match the existing named-code\n // baseline (0.95 when a known code resolves) and bump for subsection.\n let confidence = 0.95\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code,\n section,\n subsection,\n pincite: subsection,\n jurisdiction: \"CA\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Chapter-Act Statute Extraction (Family 4)\n *\n * Parses Illinois Compiled Statutes (ILCS) citations with the unique\n * chapter/act/section format: \"735 ILCS 5/2-1001\"\n *\n * @module extract/statutes/extractChapterAct\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\n/**\n * Parse chapter-act token: chapter + ILCS + act/section.\n *\n * Section body uses the period-followed-by-alphanumeric guard from #283:\n * a trailing sentence period is not absorbed (`5 ILCS 100/1-1.` → `1-1`,\n * not `1-1.`; #331). The body must mirror the tokenizer regex in\n * `statutePatterns.ts` so that the extractor's anchored re-match consumes\n * the same span the tokenizer captured.\n */\nconst CHAPTER_ACT_RE =\n /^(\\d+)\\s+(?:ILCS|Ill\\.?\\s*Comp\\.?\\s*Stat\\.?)\\s*(?:Ann\\.?\\s+)?(\\d+)\\/(\\d+(?:[A-Za-z0-9:-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)$/d\n\nexport function extractChapterAct(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = CHAPTER_ACT_RE.exec(text)\n\n let title: number | undefined // chapter number\n let code: string // act number\n let rawBody: string\n\n if (match) {\n title = Number.parseInt(match[1], 10) // chapter (e.g., 735)\n code = match[2] // act (e.g., 5)\n rawBody = match[3] // section (e.g., 2-1001)\n } else {\n code = text\n rawBody = \"\"\n }\n\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Use section without trailing sentence punctuation for span boundary.\n // Note: section comes from parseBody() which strips et seq. and splits\n // subsections — the leading text still matches the raw match position.\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n\n let spans: StatuteComponentSpans | undefined\n if (match?.indices) {\n spans = {}\n if (match.indices[1]) spans.title = spanFromGroupIndex(span.cleanStart, match.indices[1], transformationMap)\n if (match.indices[2]) spans.code = spanFromGroupIndex(span.cleanStart, match.indices[2], transformationMap)\n if (match.indices[3] && section) {\n const bodyStart = match.indices[3][0]\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n // Title (chapter) is always present on a successful ILCS match — no bonus needed.\n // Only subsection presence provides a confidence boost.\n let confidence = match ? 0.95 : 0.3\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n title,\n code,\n section,\n subsection,\n pincite: subsection,\n jurisdiction: match ? \"IL\" : undefined,\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Colorado Revised Statutes prose form (pre-1973 and modern)\n *\n * Parses citations in the form `Section 148-21-34, Colorado Revised Statutes\n * 1963`, where the section comes BEFORE the code name. Pre-1973 Colorado\n * used a chapter-article-section numbering scheme (`148-21-34` = chapter\n * 148, article 21, section 34); the structured chapter/article fields are\n * not surfaced separately — the full section body goes on `section`.\n *\n * `code` carries the full code name including the year suffix when present\n * (`Colorado Revised Statutes 1963`), so consumers can distinguish editions\n * without looking at `year` (which is reserved for trailing parenthetical\n * edition years). #352\n *\n * @module extract/statutes/extractColoradoProse\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\nconst COLORADO_PROSE_RE =\n /^[Ss]ection\\s+(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*),?\\s+(Colo(?:rado)?\\.?\\s+Rev(?:ised)?\\.?\\s+Stat(?:utes)?\\.?(?:\\s+Ann(?:otated)?\\.?)?)(?:\\s+(19\\d{2}))?$/d\n\nexport function extractColoradoProse(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = COLORADO_PROSE_RE.exec(text)!\n\n const rawBody = match[1]\n const codeName = match[2].replace(/\\s+/g, \" \").trim()\n const editionYear = match[3]\n // `code` preserves the year suffix as part of the name: `Colorado Revised\n // Statutes 1963`. The bare modern form remains `Colorado Revised Statutes`.\n const code = editionYear ? `${codeName} ${editionYear}` : codeName\n\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n const sectionIdx = match.indices[1]\n if (sectionIdx && section) {\n const bodyStart = sectionIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n if (match.indices[2]) {\n spans.code = spanFromGroupIndex(span.cleanStart, match.indices[2], transformationMap)\n }\n }\n\n // Confidence: 0.9 baseline (the closed prose shape is unambiguous); +0.05\n // when a subsection is captured.\n let confidence = 0.9\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code,\n section,\n subsection,\n pincite: subsection,\n jurisdiction: \"CO\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Federal Statute Extraction (USC + CFR)\n *\n * Parses tokenized federal citations to extract title, code, section,\n * subsections, jurisdiction, and et seq. indicators.\n *\n * @module extract/statutes/extractFederal\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\n/** Regex to parse federal token: title + code + § + body */\nconst FEDERAL_SECTION_RE = /^(\\d+)\\s+(\\S+(?:\\.\\S+)*)\\s*§§?\\s*(.+)$/d\n/** Regex to parse federal token: title + code + Part + body */\nconst FEDERAL_PART_RE = /^(\\d+)\\s+(\\S+(?:\\.\\S+)*)\\s+(?:Part|pt\\.)\\s+(.+)$/d\n\n/**\n * Extract a federal statute citation (USC or CFR) from a tokenized match.\n */\nexport function extractFederal(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n\n // Try § form first, then Part form\n const bodyMatch = FEDERAL_SECTION_RE.exec(text) ?? FEDERAL_PART_RE.exec(text)\n\n let title: number | undefined\n let code: string\n let rawBody: string\n\n if (bodyMatch) {\n title = Number.parseInt(bodyMatch[1], 10)\n code = bodyMatch[2]\n rawBody = bodyMatch[3]\n } else {\n // Fallback for edge cases\n code = token.patternId === \"cfr\" ? \"C.F.R.\" : \"U.S.C.\"\n rawBody = text\n title = undefined\n }\n\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n // Translate positions\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (bodyMatch?.indices) {\n spans = {}\n if (bodyMatch.indices[1]) spans.title = spanFromGroupIndex(span.cleanStart, bodyMatch.indices[1], transformationMap)\n if (bodyMatch.indices[2]) spans.code = spanFromGroupIndex(span.cleanStart, bodyMatch.indices[2], transformationMap)\n if (bodyMatch.indices[3] && section) {\n const bodyStart = bodyMatch.indices[3][0]\n // Use section without trailing sentence punctuation for span boundary.\n // Note: section comes from parseBody() which strips et seq. and splits\n // subsections — the leading text still matches the raw match position.\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n // Confidence: known federal code + § = 0.95 base\n let confidence = 0.95\n if (title !== undefined) confidence += 0.05\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n title,\n code,\n section,\n subsection,\n pincite: subsection,\n jurisdiction: \"US\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Illinois Revised Statutes Extraction (pre-1993 format)\n *\n * Parses citations to Illinois Revised Statutes, the dominant pre-1993\n * Illinois statutory citation form:\n *\n * Ill. Rev. Stat. 1985, ch. 40, par. 504(a)\n * Ill. Rev. Stat. 1987, ch. 85, pars. 8-102, 8-103\n * Ill.Rev.Stat. 1985, Ch. 127, par. 780.04.\n *\n * Modern Illinois opinions continue to cite ILRS when referencing the\n * historical version of a statute. Companion to `extractChapterAct` (which\n * handles the modern post-1993 ILCS form). #330\n *\n * @module extract/statutes/extractIllRevStat\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\nconst ILL_REV_STAT_RE =\n /^Ill\\.?\\s*Rev\\.?\\s*Stat\\.?,?\\s+(\\d{4}),?\\s+[Cc]h\\.\\s+(\\d+[A-Z]?),?\\s+pars?\\.\\s+(\\d+(?:[A-Za-z0-9:-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)$/d\n\nexport function extractIllRevStat(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n // The tokenizer's regex guarantees a match here — same shape as the\n // extractor's regex. Use non-null assertion to keep the code path tight.\n const match = ILL_REV_STAT_RE.exec(text)!\n const year = Number.parseInt(match[1], 10)\n const chapterRaw = match[2]\n // Chapter can carry a letter suffix (`110A`). Use only the digit-prefix for\n // the numeric `title` field; the full chapter string is preserved in\n // `matchedText`.\n const title = Number.parseInt(chapterRaw, 10)\n const rawBody = match[3]\n\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n if (match.indices[2])\n spans.title = spanFromGroupIndex(span.cleanStart, match.indices[2], transformationMap)\n if (match.indices[3] && section) {\n const bodyStart = match.indices[3][0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n // Confidence: 0.95 baseline (closed-shape Ill. Rev. Stat. matches are\n // unambiguous); +0.05 when a subsection is captured.\n let confidence = 0.95\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n title,\n code: \"Ill. Rev. Stat.\",\n section,\n subsection,\n pincite: subsection,\n year,\n jurisdiction: \"IL\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Named-Code State Statute Extraction (Family 4)\n *\n * Parses tokenized citations from states that identify their code by name\n * in the citation (e.g., \"N.Y. Penal Law § 120.05\", \"Cal. Civ. Proc. Code § 437c\").\n *\n * Handles two patternIds:\n * - \"named-code\" — NY, CA, TX, MD, VA, AL citations (prefix + code name + §)\n * - \"mass-chapter\" — MA citations (corpus + ch. + chapter, § section)\n *\n * Jurisdictions: NY, CA, TX, MD, VA, AL, MA (7 total)\n *\n * @module extract/statutes/extractNamedCode\n */\n\nimport { findNamedCode } from \"@/data/knownCodes\"\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\n/** Match named-code token: jurisdiction prefix + code name + § + body */\nconst NAMED_CODE_RE =\n /^(N\\.?\\s*Y\\.?|Cal(?:ifornia)?\\.?|Tex(?:as)?\\.?|Md\\.?|Va\\.?|Ala(?:bama)?\\.?)\\s+(.*?)\\s*§§?\\s*(.+)$/sd\n\n/** Match mass-chapter token: corpus abbreviation + ch./c. + chapter + § + section */\nconst MASS_CHAPTER_RE = /^(.*?)\\s+(?:ch\\.?|c\\.?)\\s*(\\w+),?\\s*§\\s*(.+)$/d\n\n/** Map normalized jurisdiction prefixes to 2-letter state codes */\nconst PREFIX_MAP: Record<string, string> = {\n \"n.y.\": \"NY\",\n \"n.y\": \"NY\",\n ny: \"NY\",\n \"cal.\": \"CA\",\n cal: \"CA\",\n \"california.\": \"CA\",\n california: \"CA\",\n \"tex.\": \"TX\",\n tex: \"TX\",\n \"texas.\": \"TX\",\n texas: \"TX\",\n \"md.\": \"MD\",\n md: \"MD\",\n \"va.\": \"VA\",\n va: \"VA\",\n \"ala.\": \"AL\",\n ala: \"AL\",\n \"alabama.\": \"AL\",\n alabama: \"AL\",\n}\n\n/** Normalize a jurisdiction prefix string to a 2-letter state code */\nfunction resolveJurisdiction(prefix: string): string | undefined {\n return PREFIX_MAP[prefix.toLowerCase().replace(/\\s+/g, \"\")]\n}\n\n/**\n * Strip common trailing/leading suffixes from code name text to produce a\n * lookup key for the namedCodes registry.\n *\n * Examples:\n * \"Penal Law\" → \"Penal\"\n * \"Penal Code\" → \"Penal\"\n * \"Civ. Proc. Code\" → \"Civ. Proc.\"\n * \"Code Ann., Crim. Law\" → \"Crim. Law\" (MD \"Code Ann.,\" prefix stripped)\n * \"Code, Ins.\" → \"Ins.\" (MD \"Code,\" prefix stripped)\n * \"Code Ann.\" → \"Code\" (VA/AL trailing Ann. stripped)\n * \"Code\" → \"Code\" (VA/AL — matches pattern directly)\n * \"C.P.L.R.\" → \"C.P.L.R.\" (no suffixes — passed through)\n */\nfunction cleanCodeName(raw: string): string {\n return (\n raw\n // MD: \"Code Ann., Crim. Law\" → \"Crim. Law\"\n .replace(/^\\s*Code\\s+Ann\\.\\s*,\\s*/i, \"\")\n // MD: \"Code, Ins.\" → \"Ins.\"\n .replace(/^\\s*Code\\s*,\\s*/i, \"\")\n // Trailing \" Code\" only (e.g., \"Penal Code\" → \"Penal\", \"Civ. Proc. Code\" → \"Civ. Proc.\")\n // Do NOT strip \" Law\" — MD article names contain \"Law\" (e.g., \"Crim. Law\", \"Criminal Law\")\n // and NY \"Penal Law\" → \"Penal Law\" still matches registry via startsWith(\"Penal\")\n .replace(/\\s+Code\\s*$/i, \"\")\n // Trailing \" Ann.\" (e.g., \"Code Ann.\" → \"Code\" after prior rules skip)\n .replace(/\\s+Ann\\.?\\s*$/i, \"\")\n // Trailing comma/space artifacts\n .replace(/,\\s*$/, \"\")\n .trim()\n )\n}\n\n/**\n * Extract a statute citation from a \"named-code\" or \"mass-chapter\" token.\n *\n * Named-code: \"Cal. Penal Code § 187(a)\" → jurisdiction=CA, code=\"Penal\", section=\"187\", subsection=\"(a)\"\n * Mass-chapter: \"Mass. Gen. Laws ch. 93A, § 2\" → jurisdiction=MA, code=\"93A\", section=\"2\"\n */\nexport function extractNamedCode(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n\n let jurisdiction: string | undefined\n let code: string\n let rawBody: string\n let massMatch: RegExpExecArray | null = null\n let namedMatch: RegExpExecArray | null = null\n\n if (token.patternId === \"mass-chapter\") {\n massMatch = MASS_CHAPTER_RE.exec(text)\n if (massMatch) {\n jurisdiction = \"MA\"\n code = massMatch[2] // chapter number (e.g., \"93A\")\n rawBody = massMatch[3]\n } else {\n code = text\n rawBody = \"\"\n }\n } else {\n // named-code: \"[State prefix] [Code Name] § [body]\"\n namedMatch = NAMED_CODE_RE.exec(text)\n if (namedMatch) {\n jurisdiction = resolveJurisdiction(namedMatch[1])\n const rawCodeName = namedMatch[2]\n const cleaned = cleanCodeName(rawCodeName)\n\n if (jurisdiction) {\n // Look up in registry — use cleaned name as the lookup key\n const entry = findNamedCode(jurisdiction, cleaned)\n // Store the cleaned name (e.g., \"Penal\" not \"Penal Code\"); fall back to raw if no registry hit\n code = entry ? cleaned : rawCodeName.trim()\n } else {\n code = rawCodeName.trim()\n }\n\n rawBody = namedMatch[3]\n } else {\n // Unparseable token — graceful fallback\n code = text\n rawBody = \"\"\n }\n }\n\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Use section without trailing sentence punctuation for span boundary.\n // Note: section comes from parseBody() which strips et seq. and splits\n // subsections — the leading text still matches the raw match position.\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n\n let spans: StatuteComponentSpans | undefined\n if (massMatch?.indices) {\n spans = {}\n if (massMatch.indices[2]) spans.code = spanFromGroupIndex(span.cleanStart, massMatch.indices[2], transformationMap)\n if (massMatch.indices[3] && section) {\n const bodyStart = massMatch.indices[3][0]\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n } else if (namedMatch?.indices) {\n spans = {}\n if (namedMatch.indices[2]) spans.code = spanFromGroupIndex(span.cleanStart, namedMatch.indices[2], transformationMap)\n if (namedMatch.indices[3] && section) {\n const bodyStart = namedMatch.indices[3][0]\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n // Confidence: named-code patterns always require §, so known jurisdiction → 0.95 base\n let confidence = jurisdiction ? 0.95 : 0.5\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code,\n section,\n subsection,\n pincite: subsection,\n jurisdiction,\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Prose-form Statute Extraction\n *\n * Parses natural language references like \"section 1983 of title 42\"\n * into structured StatuteCitation objects.\n *\n * @module extract/statutes/extractProse\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\n\n/** Parse \"section X(subsections) of title Y\" */\nconst PROSE_RE = /[Ss]ection\\s+(\\d+[A-Za-z0-9-]*)((?:\\([^)]*\\))*)\\s+of\\s+title\\s+(\\d+)/d\n\n/**\n * Extract a prose-form statute citation.\n * Currently handles federal \"section X of title Y\" form.\n */\nexport function extractProse(token: Token, transformationMap: TransformationMap): StatuteCitation {\n const { text, span } = token\n\n const match = PROSE_RE.exec(text)\n\n let section: string\n let subsection: string | undefined\n let title: number | undefined\n\n if (match) {\n section = match[1]\n subsection = match[2] || undefined\n title = Number.parseInt(match[3], 10)\n } else {\n section = text\n }\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match?.indices) {\n spans = {}\n if (match.indices[1]) spans.section = spanFromGroupIndex(span.cleanStart, match.indices[1], transformationMap)\n if (match.indices[3]) spans.title = spanFromGroupIndex(span.cleanStart, match.indices[3], transformationMap)\n if (match.indices[2] && subsection) {\n spans.subsection = spanFromGroupIndex(span.cleanStart, match.indices[2], transformationMap)\n }\n }\n\n let confidence = 0.85\n if (title !== undefined) confidence += 0.05\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n title,\n code: \"U.S.C.\",\n section,\n subsection,\n pincite: subsection,\n jurisdiction: \"US\",\n spans,\n }\n}\n","/**\n * Statute Citation Extraction — Dispatcher\n *\n * Routes statute tokens to family-specific extractors based on patternId.\n * This is the entry point called by extractCitations.ts (line 234).\n *\n * Family dispatch:\n * - \"usc\", \"cfr\" → extractFederal\n * - \"prose\" → extractProse\n * - \"abbreviated-code\" → extractAbbreviated\n * - \"chapter-act\" → extractChapterAct\n * - unknown → legacy inline parser (safety net for unknown patternIds)\n *\n * @module extract/extractStatute\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport { resolveOriginalSpan, type TransformationMap } from \"@/types/span\"\nimport { extractAbbreviated } from \"./statutes/extractAbbreviated\"\nimport { extractAlaCode1940 } from \"./statutes/extractAlaCode1940\"\nimport { extractCaBareCode } from \"./statutes/extractCaBareCode\"\nimport { extractChapterAct } from \"./statutes/extractChapterAct\"\nimport { extractColoradoProse } from \"./statutes/extractColoradoProse\"\nimport { extractFederal } from \"./statutes/extractFederal\"\nimport { extractIllRevStat } from \"./statutes/extractIllRevStat\"\nimport { extractNamedCode } from \"./statutes/extractNamedCode\"\nimport { extractProse } from \"./statutes/extractProse\"\n\n/**\n * Legacy inline parser for unknown patterns.\n * Safety net for any patternId not explicitly handled by the dispatcher.\n */\nfunction extractLegacy(token: Token, transformationMap: TransformationMap): StatuteCitation {\n const { text, span } = token\n\n const statuteRegex = /^(?:(\\d+)\\s+)?([A-Za-z.\\s]+?)\\s*§+\\s*(\\d+[A-Za-z0-9-]*)/\n const match = statuteRegex.exec(text)\n\n // Graceful fallback for unparseable tokens — return low-confidence citation\n // rather than throwing (spec: \"Unknown codes produce citations with low confidence\")\n if (!match) {\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence: 0.3,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: text,\n section: \"\",\n }\n }\n\n const title = match[1] ? Number.parseInt(match[1], 10) : undefined\n const code = match[2].trim()\n const section = match[3]\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let confidence = 0.5\n const knownCodes = [\n \"U.S.C.\",\n \"C.F.R.\",\n \"Cal. Civ. Code\",\n \"Cal. Penal Code\",\n \"N.Y. Civ. Prac. L. & R.\",\n \"Tex. Civ. Prac. & Rem. Code\",\n ]\n\n if (knownCodes.some((c) => code.includes(c))) {\n confidence += 0.3\n }\n\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n title,\n code,\n section,\n }\n}\n\n/**\n * Extracts statute citation metadata from a tokenized citation.\n * Dispatches to family-specific extractors based on patternId.\n */\nexport function extractStatute(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n switch (token.patternId) {\n case \"usc\":\n case \"cfr\":\n return extractFederal(token, transformationMap)\n case \"prose\":\n return extractProse(token, transformationMap)\n case \"abbreviated-code\":\n return extractAbbreviated(token, transformationMap)\n case \"ca-bare-code\":\n return extractCaBareCode(token, transformationMap)\n case \"named-code\":\n case \"mass-chapter\":\n return extractNamedCode(token, transformationMap)\n case \"chapter-act\":\n return extractChapterAct(token, transformationMap)\n case \"ill-rev-stat\":\n return extractIllRevStat(token, transformationMap)\n case \"ala-code-prefix\":\n case \"ala-title-trailer\":\n case \"ala-tit-bare\":\n return extractAlaCode1940(token, transformationMap)\n case \"colorado-prose\":\n return extractColoradoProse(token, transformationMap)\n default:\n // unknown patterns use legacy parser\n return extractLegacy(token, transformationMap)\n }\n}\n","/**\n * Statutes at Large Citation Extractor\n *\n * Extracts session law citations from the Statutes at Large (e.g., \"124 Stat. 119\").\n * These are chronological compilations of federal laws, distinct from both\n * codified statutes (U.S.C.) and case reporters.\n *\n * Format: volume Stat. page [(year)]\n *\n * @module extract/extractStatutesAtLarge\n */\n\nimport type { Token } from \"@/tokenize/tokenizer\"\nimport type { StatutesAtLargeCitation } from \"@/types/citation\"\nimport type { StatutesAtLargeComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\n\nexport function extractStatutesAtLarge(\n token: Token,\n transformationMap: TransformationMap,\n): StatutesAtLargeCitation {\n const { text, span } = token\n\n // Parse volume-Stat.-page\n const statRegex = /^(\\d+(?:-\\d+)?)\\s+Stat\\.\\s+(\\d+)/d\n const match = statRegex.exec(text)\n\n if (!match) {\n throw new Error(`Failed to parse Statutes at Large citation: ${text}`)\n }\n\n const rawVolume = match[1]\n const volume = /^\\d+$/.test(rawVolume) ? Number.parseInt(rawVolume, 10) : rawVolume\n const page = Number.parseInt(match[2], 10)\n\n let spans: StatutesAtLargeComponentSpans | undefined\n if (match.indices) {\n spans = {\n volume: spanFromGroupIndex(span.cleanStart, match.indices[1]!, transformationMap),\n page: spanFromGroupIndex(span.cleanStart, match.indices[2]!, transformationMap),\n }\n }\n\n // Extract optional year in parentheses\n const yearRegex = /\\((?:.*?\\s)?(\\d{4})\\)/\n const yearMatch = yearRegex.exec(text)\n const year = yearMatch ? Number.parseInt(yearMatch[1], 10) : undefined\n\n // Translate positions from clean → original\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Confidence: 0.9 (Statutes at Large format is standardized)\n const confidence = 0.9\n\n return {\n type: \"statutesAtLarge\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n volume,\n page,\n year,\n spans,\n }\n}\n","/**\n * Case Citation Regex Patterns\n *\n * These patterns are designed for tokenization (broad matching) not extraction.\n * They identify potential case citations in text for the tokenizer (Plan 3).\n * Metadata parsing and validation against reporters-db happens in Phase 2 Plan 5 (extraction layer).\n *\n * Pattern Design Principles (from RESEARCH.md):\n * - Use \\b word boundaries to avoid matching \"F.\" in \"F.B.I.\"\n * - Avoid nested quantifiers: (a+)+ causes ReDoS\n * - Keep patterns simple: tokenization only needs to find candidates\n * - Use global flag /g for matchAll()\n */\n\nimport type { FullCitationType } from \"@/types/citation\"\n\nexport interface Pattern {\n id: string\n regex: RegExp\n description: string\n type: FullCitationType\n}\n\nexport const casePatterns: Pattern[] = [\n {\n id: \"federal-reporter\",\n // Edition suffix accepts any ordinal (\"2d\", \"3d\", or generic \"Nth\") so the\n // pattern survives the eventual rollout of F.5th / F.6th / F.Supp.Nth (#234).\n // F.Supp.* and F.App'x must come before the generic F.* alternative so the\n // longer prefixes win during alternation.\n regex:\n /\\b(\\d+(?:-\\d+)?)\\s+(F\\.\\s?Supp\\.(?:\\s?(?:\\d+(?:st|nd|rd|th)|2d|3d))?|F\\.\\s?App'x|F\\.(?:\\d+(?:st|nd|rd|th)|2d|3d)?)\\s+(\\d+|_{3,}|-{3,})(?=\\s|$|\\(|,|;|\\.)/g,\n description: \"Federal Reporter (F., F.2d, F.3d, F.Nth, F.Supp., F.App'x, etc.)\",\n type: \"case\",\n },\n {\n id: \"supreme-court\",\n // L.Ed. edition suffix accepts any ordinal so a future L.Ed.3d edition does\n // not silently fall through to the state-reporter fallback (#234).\n regex:\n /\\b(\\d+(?:-\\d+)?)\\s+(U\\.\\s?S\\.|S\\.\\s?Ct\\.|L\\.\\s?Ed\\.(?:\\s?(?:\\d+(?:st|nd|rd|th)|2d|3d))?)\\s+(?:\\(\\d+\\s+[A-Z][A-Za-z.]+\\)\\s+)?(\\d+|_{3,}|-{3,})(?=\\s|$|\\(|,|;|\\.)/g,\n description:\n \"U.S. Supreme Court reporters (with optional nominative reporter parenthetical)\",\n type: \"case\",\n },\n {\n id: \"state-reporter\",\n // Character class admits `&` so reporters with ampersands tokenize correctly\n // (e.g., `I&N Dec.` and `I. & N. Dec.` for BIA immigration decisions — #244).\n // Apostrophe `'` is admitted for reporters like `F. App'x` already covered by\n // federal-reporter; including it here is harmless and future-proofs other\n // possessive forms. Trailing lookahead also accepts `[` (NY Slip Op `[U]`\n // markers — #231) and `]` (California Style Manual bracketed parallel\n // cites like `[266 Cal.Rptr. 569]` — #237). Negative lookahead on the\n // reporter body rejects ` at ` so `18 Cal.4th at p. 717` (CSM short-form,\n // #236) doesn't absorb `at p.` into the reporter; the short-form pattern\n // handles it instead.\n //\n // ` R.\\s+\\d` guard (#332): Illinois Supreme Court Rules cite as\n // `177 Ill. 2d R. 234` (volume + reporter + `R. ruleNum`), which the lazy\n // reporter capture used to absorb as `Ill. 2d R.` with `page=234`,\n // emitting a phantom case citation. The lookahead stops the lazy match\n // before it consumes ` R.` when a digit follows — leaving the input\n // untokenized rather than misclassified. A typed rule citation is out\n // of scope; the goal here is to suppress the false positive.\n regex:\n /\\b(\\d+(?:-\\d+)?)\\s+([A-Z](?:(?! L\\.[JQR\\s])(?! R\\.\\s+\\d)(?!\\s+vs?\\.\\s)(?!\\s+at\\s)[A-Za-z.\\d\\s&'])+?)\\s+(\\d+|_{3,}|-{3,})(?=\\s|$|\\(|,|;|\\.|\\[|\\])/g,\n description:\n 'State reporters (broad pattern allowing multi-word reporters with & and \\', excludes journal patterns with \" L.J/Q/Rev\", phantom matches across \" v. \"/\" vs. \", CSM \" at \" short-form boundaries, and Illinois \" R. N\" rule-marker boundaries, validated against reporters-db in Phase 3)',\n type: \"case\",\n },\n]\n","/**\n * Docket-Number Citation Patterns\n *\n * Patterns for case citations identified by docket / slip-opinion number\n * rather than a traditional reporter assignment. Common shapes:\n *\n * - NY Court of Appeals slip ops: `Party v. Party, No. 51 (N.Y. 2023)`\n * - Federal district court pre-reporter: `Smith v. Jones, No. 12-3456 (S.D.N.Y. 2024)`\n *\n * Disambiguation: a bare `No. 51 (N.Y. 2023)` is too generic to extract\n * without strong context. The pattern matches the `No. <docket> (<court> <year>)`\n * core; the extractor enforces a case-name anchor and only emits a citation\n * when a `Party v. Party,` (or `In re Party,`) prefix is found.\n */\n\nimport type { Pattern } from \"./casePatterns\"\n\nexport const docketPatterns: Pattern[] = [\n {\n id: \"docket-paren-court-year\",\n // Match: \"No. <docket-number> (<court...> <year>)\"\n // docket-number: digits, optionally hyphenated (51, 12-3456, 22-cv-1234)\n // parenthetical: anything (lazy) ending with a 4-digit year\n // The `\\bNo\\.` anchor + space-separated paren keep this narrow without a\n // case-name lookbehind — case-name validation lives in the extractor.\n regex: /\\bNo\\.\\s+([\\d]+(?:-[\\w\\d]+)*)\\s+\\(([^)]+\\s(\\d{4}))\\)/g,\n description: 'Docket-number citation: \"Party v. Party, No. <docket> (<court> <year>)\"',\n type: \"docket\",\n },\n]\n","/**\n * Journal Citation Regex Patterns\n *\n * Patterns for law review and journal citations.\n * These are intentionally broad for tokenization - validation against\n * journals-db happens in Phase 3 (extraction layer).\n *\n * Pattern Design:\n * - Matches volume-journal-page format\n * - Broad journal name matching (validated later)\n * - Simple structure to avoid ReDoS\n */\n\nimport type { Pattern } from \"./casePatterns\"\n\nexport const journalPatterns: Pattern[] = [\n {\n id: \"law-review\",\n regex: /\\b(\\d+(?:-\\d+)?)\\s+([A-Z](?:(?!\\s+vs?\\.\\s)(?!\\s+at\\s+\\d)[A-Za-z.\\s])+)\\s+(\\d+)\\b/g,\n description:\n 'Law review citations (e.g., \"120 Harv. L. Rev. 500\"), validated against journals-db in Phase 3. Negative lookaheads exclude \" v. \"/\" vs. \" (so a party-name run isn\\'t mis-captured as a journal) and \" at <digit>\" (so a short-form pincite like \"554 U.S. at 621\" isn\\'t mis-captured as a journal).',\n type: \"journal\",\n },\n]\n","/**\n * Neutral and Online Citation Regex Patterns\n *\n * Patterns for WestLaw, LexisNexis, public laws, and Federal Register citations.\n * These have predictable formats and don't require external validation.\n *\n * Pattern Design:\n * - Matches year-database-number format for online citations\n * - Matches Pub. L. No. format for public laws\n * - Matches volume-Fed. Reg.-page for Federal Register\n * - Simple structure to avoid ReDoS\n */\n\nimport type { Pattern } from \"./casePatterns\"\n\nexport const neutralPatterns: Pattern[] = [\n {\n // Mississippi 4-segment form: year-caseType-number-appellateTrack. Listed\n // before the 3-segment hyphenated pattern so it wins on the longer match\n // (e.g., \"2010-CT-01234-SCT\"). (#233)\n id: \"state-vendor-neutral-hyphenated-ms\",\n regex: /\\b(\\d{4})-([A-Z]+)-(\\d+)-([A-Z]+)\\b/g,\n description:\n 'Mississippi 4-segment vendor-neutral (e.g., \"2010-CT-01234-SCT\", \"2015-CA-00567-COA\")',\n type: \"neutral\",\n },\n {\n // 3-segment hyphenated form used by NM (NMSC, NMCA, NMCERT), Ohio\n // (mixed-case \"Ohio\" token), and NC (NCSC, NCCOA). The court token starts\n // with an uppercase letter and may contain lowercase (so the Ohio token\n // matches). (#233)\n id: \"state-vendor-neutral-hyphenated\",\n regex: /\\b(\\d{4})-([A-Z][A-Za-z]+)-(\\d+)\\b/g,\n description:\n 'Hyphenated vendor-neutral (e.g., \"2010-NMSC-007\", \"2024-Ohio-764\", \"2020-NCSC-118\")',\n type: \"neutral\",\n },\n {\n // Multi-word neutral courts (#230). Alternation order matters — longer,\n // more specific patterns must precede the bare `[A-Z]{2}` fallback so the\n // regex prefers the more specific match:\n // - `IL App (Nst)` — Illinois Rule 23 form with district parenthetical\n // (districts 1st / 2d / 3d / 4th / 5th)\n // - `OK CIV APP|CR|AG` — Oklahoma multi-word courts\n // - `[A-Z]{2}(?:\\s+App\\.?)?` — existing single-word + optional App fallback\n // The trailing `(-U)?` captures Illinois Rule 23 unpublished marker; the\n // extractor consumes it into the `unpublished` flag and strips it from\n // `documentNumber`.\n id: \"state-vendor-neutral\",\n regex:\n /\\b(\\d{4})\\s+(IL\\s+App\\s+\\(\\d+(?:st|nd|rd|th|d)\\)|OK\\s+(?:CIV\\s+APP|CR|AG)|[A-Z]{2}(?:\\s+App\\.?)?)\\s+(\\d+(?:-U)?)\\b/g,\n description:\n 'State vendor-neutral citations (e.g., \"2007 UT 49\", \"2017 WI 17\", \"2013 IL 112116\", \"2011 IL App (1st) 101234\", \"2020 OK CIV APP 67\", \"2020 IL App (2d) 190123-U\")',\n type: \"neutral\",\n },\n {\n id: \"westlaw\",\n regex: /\\b(\\d{4})\\s+WL\\s+(\\d+)\\b/g,\n description: 'WestLaw citations (e.g., \"2021 WL 123456\")',\n type: \"neutral\",\n },\n {\n // Generalized to accept any uppercase-prefixed court abbreviation before\n // LEXIS so state variants (Cal. LEXIS, Tex. App. LEXIS, N.Y. Misc. LEXIS,\n // Ill. App. LEXIS, etc.) tokenize alongside the federal U.S. forms (#228).\n // The non-greedy `[A-Z][A-Za-z.\\s]+?` is bounded by the literal `\\s+LEXIS`\n // that follows it, so it can't run away.\n id: \"lexis\",\n regex: /\\b(\\d{4})\\s+[A-Z][A-Za-z.\\s]+?\\s+LEXIS\\s+(\\d+)\\b/g,\n description:\n 'LexisNexis citations (federal: \"2021 U.S. LEXIS 5000\", \"2021 U.S. App. LEXIS 12345\"; state: \"2020 Cal. LEXIS 1000\", \"2020 Tex. App. LEXIS 5000\")',\n type: \"neutral\",\n },\n {\n id: \"public-law\",\n regex: /\\bPub\\.\\s?L\\.(?:\\s?No\\.)?\\s?(\\d+-\\d+)\\b/g,\n description: 'Public Law citations (e.g., \"Pub. L. No. 117-58\" or \"Pub. L. 116-283\")',\n type: \"publicLaw\",\n },\n {\n id: \"federal-register\",\n regex: /\\b(\\d+(?:-\\d+)?)\\s+Fed\\.\\s?Reg\\.\\s+(\\d+)\\b/g,\n description: 'Federal Register citations (e.g., \"86 Fed. Reg. 12345\")',\n type: \"federalRegister\",\n },\n {\n id: \"statutes-at-large\",\n regex: /\\b(\\d+(?:-\\d+)?)\\s+Stat\\.\\s+(\\d+)\\b/g,\n description: 'Statutes at Large citations (e.g., \"124 Stat. 119\")',\n type: \"statutesAtLarge\",\n },\n {\n id: \"compact-law-review\",\n regex: /\\b(\\d+(?:-\\d+)?)\\s+([A-Z][A-Za-z.]+L\\.(?:Rev|J|Q)\\.)\\s+(\\d+)\\b/g,\n description: 'Compact law review citations without spaces (e.g., \"93 Harv.L.Rev. 752\")',\n type: \"journal\",\n },\n]\n","/**\n * Short-form Citation Regex Patterns\n *\n * Patterns for Id., Ibid., supra, and short-form case citations.\n * These refer to earlier citations in the document.\n *\n * Pattern Design:\n * - Simple structure to avoid ReDoS (no nested quantifiers)\n * - Broad matching for tokenization; validation happens in extraction layer\n * - Word boundaries to prevent false positives (e.g., \"Idaho\" vs \"Id.\")\n */\n\nimport type { Pattern } from \"./casePatterns\"\n\n/** Id. with optional pincite: \"Id.\" or \"Id. at 253\" or \"Id., at 253\" or\n * \"Id. ¶ 12\" (#204).\n *\n * Punctuation tolerance (#305):\n * - Optional space before the period — `Id .` / `Ibid .` (OCR + older\n * typesetting).\n * - Comma instead of period — `Id, at 1483` — only when immediately\n * followed by `at` so bare `Id,` in prose (\"his Id, but...\") is not\n * misread as a citation.\n *\n * Pincite captures an optional \"*\" prefix for star-pagination (NY Slip Op,\n * Westlaw, Lexis; see #191), an optional trailing \" n.14\" / \" nn.14-15\"\n * footnote suffix (see #202), an optional `p.` / `pp.` prefix for\n * California Style Manual form (see #236), and `¶` / `¶¶` / `para.` /\n * `paras.` paragraph markers (#204). When the pincite is a paragraph form,\n * `at` is optional — `Id. ¶ 12` and `Id. at ¶ 12` both match. */\nexport const ID_PATTERN: RegExp =\n /(?:^|(?<=\\s)|(?<=[\"'(\\[—]))\\b[Ii]d(?:\\s*\\.|\\s*,(?=\\s+at\\s))(?:(?:,\\s+|,?\\s+(?:at\\s+(?:pp?\\.\\s*)?|(?=¶|paras?\\.?\\b)))(¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?|\\*?\\d+(?:\\s*[-–]\\s*\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?))?/g\n\n/** Ibid. with optional pincite (less common variant). Paragraph forms (#204)\n * follow the same convention as Id. Optional space before the period (#305). */\nexport const IBID_PATTERN: RegExp =\n /(?:^|(?<=\\s)|(?<=[\"'(\\[—]))\\b[Ii]bid\\s*\\.(?:(?:,\\s+|,?\\s+(?:at\\s+(?:pp?\\.\\s*)?|(?=¶|paras?\\.?\\b)))(¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?|\\*?\\d+(?:\\s*[-–]\\s*\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?))?/g\n\n/**\n * Supra with party name and optional pincite.\n * Pattern: word(s), supra [note N] [, at page]\n * Captures: (1) party name, (2) note number (if any), (3) pincite\n *\n * Party-name capture (#301): continuation accepts `\\s+v\\.?\\s+` (v.),\n * `\\s+&\\s+` (ampersand-joined parties — `Walker & Horwich, supra`),\n * `,\\s+` (corporate-suffix continuation — `Thorn Americas, Inc., supra`),\n * and plain whitespace (multi-word names). Each continuation requires a\n * capital-letter follow-on, so `, supra` (lowercase `s`) still terminates\n * the name. NOTE: `In re` prefix is NOT included here — the resolver's\n * BKTree matches full-cite party names that don't carry the prefix\n * (#216 / #21), so a supra with `In re X` won't match a full cite\n * indexed as `X`. Handling that gap requires resolver-side normalization,\n * which is intentionally out of scope for #301.\n *\n * Pincite accepts optional \"*\" prefix for star-pagination (#191), an optional\n * range end (#236), an optional trailing footnote suffix (#202), an optional\n * `p.` / `pp.` prefix for California Style Manual form (#236), and `¶` /\n * `¶¶` / `para.` / `paras.` paragraph markers (#204). When the pincite is a\n * paragraph form, `at` is optional.\n */\nexport const SUPRA_PATTERN: RegExp =\n /\\b([A-Z][a-zA-Z''\\-]+\\.?(?:(?:\\s+v\\.?\\s+|\\s+&\\s+|,\\s+|\\s+)[A-Z][a-zA-Z''\\-]+\\.?)*)\\s*,?\\s+supra(?:\\s+note\\s+(\\d+))?(?:(?:,\\s+|,?\\s+(?:at\\s+(?:pp?\\.\\s*)?|(?=¶|paras?\\.?\\b)))(¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?|\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?))?/g\n\n/**\n * Standalone supra without party name (common in footnotes).\n * Matches: \"supra note 12\", \"supra at 15\", \"supra § 3\", \"supra Part II\"\n * Requires \"note\", \"at\", \"§\", \"Part\", or \"p.\" after supra to avoid matching\n * the word \"supra\" in prose. Preceded by whitespace, start, or signal words.\n * Captures: (1) note number (if any), (2) pincite (with optional \"*\" prefix,\n * #191, optional range end / `p.`/`pp.` prefix #236, optional trailing\n * footnote suffix #202, and `¶`/`para.` paragraph markers #204).\n */\nexport const STANDALONE_SUPRA_PATTERN: RegExp =\n /(?:^|(?<=\\s)|(?<=[;.]))supra(?:\\s+note\\s+(\\d+)(?:,?\\s+(?:at\\s+(?:pp?\\.\\s*)?|(?=¶|paras?\\.?\\b))(¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?|\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?))?|\\s+(?:at\\s+(?:pp?\\.\\s*)?|(?=¶|paras?\\.?\\b))(¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?|\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?)|\\s+(?:§+|Part|p\\.)\\s*\\S+)/g\n\n/**\n * Bracketed supra forms (#306) — `[supra]`, `[supra, 705]`, `[supra at 78-82]`,\n * and `State v. Jarzbek, [supra, 705]`. Connecticut Supreme/Appellate use\n * brackets around the supra token when it appears inside a string-cite or\n * quotation. The comma-pincite form `[supra, 705]` accepts NO `at` before\n * the page — that's the Connecticut convention.\n *\n * Captures: (1) party name (optional; undefined for bare standalone form),\n * (2) pincite (optional, accepts comma-form `, N` or `at N` shape with\n * optional range `N-M`).\n */\nexport const BRACKETED_SUPRA_PATTERN: RegExp =\n /(?:\\b([A-Z][a-zA-Z''\\-]+\\.?(?:(?:\\s+v\\.?\\s+|\\s+&\\s+|,\\s+|\\s+)[A-Z][a-zA-Z''\\-]+\\.?)*)\\s*,?\\s+)?\\[supra(?:(?:,\\s+|\\s+at\\s+(?:pp?\\.\\s*)?)(\\d+(?:[-–—]\\d+)?))?\\]/g\n\n/**\n * Short-form case: [Party,] volume reporter [,] at page\n * Pattern: optional Party name, then number space abbreviation [, ] at space number.\n * Simplified detection; full parsing in extraction layer.\n * Supports reporters with 1-2 letter ordinal suffixes (e.g., F.4th, Cal.4th).\n * Handles SCOTUS/federal comma-before-at: \"597 U.S., at 721\", \"116 F.4th, at 1193\".\n * Pincite accepts optional \"*\" prefix for star-pagination (#191), an optional\n * range end \"462-65\" / \"462-*65\" (#201), an optional trailing footnote suffix\n * \" n.14\" / \" nn.14-15\" (#202), an optional `p.` / `pp.` prefix for\n * California Style Manual form (`18 Cal.4th at p. 717`; see #236), and `¶` /\n * `¶¶` / `para.` / `paras.` paragraph markers (#204).\n *\n * Optional leading party-name group captures Bluebook back-references like\n * `Smith, 500 F.2d at 125` so the resolver can disambiguate when multiple\n * full citations share the same volume+reporter (#278). Group order:\n * 1: party name (optional)\n * 2: volume\n * 3: reporter\n * 4: pincite\n *\n * Pincite prefix also tolerates the spelled-out `page` / `pages` form\n * (`281 Ala. at page 322`, `38 Ala.App. at pages 186`) common in Alabama\n * appellate writing (#344). Without this, the input slipped past the\n * short-form-case pattern and was misclassified as a journal citation by\n * a later pattern.\n */\nexport const SHORT_FORM_CASE_PATTERN: RegExp =\n /\\b(?:([A-Z][a-zA-Z''\\-]+\\.?(?:(?:\\s+v\\.?\\s+|\\s+&\\s+|,\\s+|\\s+)[A-Z][a-zA-Z''\\-]+\\.?)*),\\s+)?(\\d+(?:-\\d+)?)\\s+([A-Z][A-Za-z.''\\s]+?(?:\\d[a-z]{1,2})?)\\s*,?\\s+at\\s+(?:pp?\\.\\s*|pages?\\s+)?(\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?|¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?)\\b/g\n\n/** All short-form patterns for tokenization */\nexport const SHORT_FORM_PATTERNS: readonly RegExp[] = [\n ID_PATTERN,\n IBID_PATTERN,\n BRACKETED_SUPRA_PATTERN,\n SUPRA_PATTERN,\n STANDALONE_SUPRA_PATTERN,\n SHORT_FORM_CASE_PATTERN,\n] as const\n\n/** Pattern objects for consistency with other pattern modules */\nexport const shortFormPatterns: Pattern[] = [\n {\n id: \"id\",\n regex: ID_PATTERN,\n description: 'Id. citations (e.g., \"Id.\" or \"Id. at 253\")',\n type: \"case\", // Will be typed as 'id' in extraction layer\n },\n {\n id: \"ibid\",\n regex: IBID_PATTERN,\n description: 'Ibid. citations (e.g., \"Ibid.\" or \"Ibid. at 125\")',\n type: \"case\", // Will be typed as 'id' in extraction layer\n },\n {\n id: \"supra\",\n regex: BRACKETED_SUPRA_PATTERN,\n description:\n 'Bracketed supra citations (e.g., \"State v. Jarzbek, [supra, 705]\" — Connecticut style; #306)',\n type: \"case\", // Will be typed as 'supra' in extraction layer\n },\n {\n id: \"supra\",\n regex: SUPRA_PATTERN,\n description: 'Supra citations (e.g., \"Smith, supra\" or \"Smith, supra, at 460\")',\n type: \"case\", // Will be typed as 'supra' in extraction layer\n },\n {\n id: \"supra\",\n regex: STANDALONE_SUPRA_PATTERN,\n description: 'Standalone supra (e.g., \"supra note 12\" or \"supra at 15\")',\n type: \"case\", // Will be typed as 'supra' in extraction layer\n },\n {\n id: \"shortFormCase\",\n regex: SHORT_FORM_CASE_PATTERN,\n description: 'Short-form case citations (e.g., \"500 F.2d at 125\")',\n type: \"case\", // Will be typed as 'shortFormCase' in extraction layer\n },\n]\n","/**\n * Statute Citation Regex Patterns\n *\n * Patterns for federal (USC, CFR), state, and prose-form statute citations.\n * Intentionally broad for tokenization — extraction layer validates and\n * routes to jurisdiction-specific extractors.\n *\n * Pattern families (spec Section 2):\n * - Federal: usc, cfr (enhanced with subsections, et seq., §§)\n * - Prose: \"section X of title Y\"\n * - Illinois: chapter-act (ILCS chapter/act/section format)\n */\n\nimport { buildCaBareCodeRegex } from \"@/data/caBareCodes\"\nimport { buildAbbreviatedCodeRegex } from \"@/data/stateStatutes\"\nimport type { Pattern } from \"./casePatterns\"\n\nexport const statutePatterns: Pattern[] = [\n {\n id: \"usc\",\n regex:\n /\\b(\\d+)\\s+(?:U\\.S\\.C\\.?|USC)\\s*§§?\\s*(\\d+[A-Za-z0-9-]*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)/g,\n description:\n 'U.S. Code citations with optional subsections and et seq. (e.g., \"42 U.S.C. § 1983(a)(1) et seq.\")',\n type: \"statute\",\n },\n {\n id: \"cfr\",\n regex:\n /\\b(\\d+)\\s+C\\.?F\\.?R\\.?\\s*(?:(?:Part|pt\\.)\\s+|§§?\\s*)(\\d+(?:\\.\\d+)?[A-Za-z0-9-]*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)/g,\n description:\n 'Code of Federal Regulations with Part or §, subsections, et seq. (e.g., \"12 C.F.R. Part 226\", \"40 C.F.R. § 122.26(b)(14)\")',\n type: \"statute\",\n },\n {\n id: \"prose\",\n regex: /\\b[Ss]ection\\s+(\\d+[A-Za-z0-9-]*(?:\\([^)]*\\))*)\\s+of\\s+title\\s+(\\d+)\\b/g,\n description:\n 'Prose-form federal citations (e.g., \"section 1983 of title 42\"). Note: MD-style \"section X of the Y Article\" deferred to PR 3.',\n type: \"statute\",\n },\n {\n id: \"named-code\",\n // Matches: [State abbrev]. [Code/Law Name] § [section]\n // Captures: (1) jurisdiction prefix, (2) code name text, (3) section+subsections+et seq\n //\n // Code-name body (#328): each word must be capitalized — real code names\n // are title-case sequences like `Penal Code`, `Civ. Prac. & Rem. Code Ann.`,\n // `Insurance Law`. The previous broad `[A-Za-z.&',\\s]+?` accepted lowercase\n // prose, so when the input had a stray earlier `California` followed by\n // sentence prose then `California Penal Code § 549`, the regex absorbed\n // the entire intervening clause into the code-name span.\n //\n // Section body: period only allowed when followed by alphanumeric, so a\n // trailing sentence period is not absorbed (#283).\n regex:\n /\\b(N\\.?\\s*Y\\.?|Cal(?:ifornia)?\\.?|Tex(?:as)?\\.?|Md\\.?|(?<!W\\.?\\s?)Va\\.?|Ala(?:bama)?\\.?)\\s+([A-Z][A-Za-z.&']*(?:(?:\\s+|,\\s+)(?:&|[A-Z][A-Za-z.&']*))*)\\s*§§?\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)/g,\n description:\n \"Named-code state citations (NY, CA, TX, MD, VA, AL) with jurisdiction prefix + code name + §\",\n type: \"statute\",\n },\n {\n id: \"mass-chapter\",\n // Matches: Mass. Gen. Laws ch. X, § Y / M.G.L.A. c. X, § Y / G.L. c. X, § Y / A.L.M. c. X, § Y\n // Section body: period only allowed when followed by alphanumeric, so a\n // trailing sentence period is not absorbed (#283).\n regex:\n /\\b(Mass\\.?\\s*Gen\\.?\\s*Laws|M\\.?G\\.?L\\.?A?\\.?|A\\.?L\\.?M\\.?|G\\.?\\s*L\\.?)\\s+(?:ch\\.?|c\\.?)\\s*(\\w+),?\\s*§\\s*(\\w+(?:[\\w/-]|\\.(?=\\w))*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)/g,\n description: 'Massachusetts chapter-based citations (e.g., \"Mass. Gen. Laws ch. 93A, § 2\")',\n type: \"statute\",\n },\n {\n id: \"chapter-act\",\n // IL: \"735 ILCS 5/2-1001\" or \"735 Ill. Comp. Stat. 5/2-1001\"\n // Captures: (1) chapter, (2) act, (3) section+subsections+et seq\n //\n // Section body: digits then alphanumeric/colon/slash/hyphen OR\n // period-followed-by-alphanumeric (lookahead). The period guard\n // prevents sentence-ending punctuation from being absorbed into\n // the section field (`5 ILCS 100/1-1.` → section \"1-1\", not \"1-1.\";\n // #283 / #331).\n regex:\n /\\b(\\d+)\\s+(?:ILCS|Ill\\.?\\s*Comp\\.?\\s*Stat\\.?)\\s*(?:Ann\\.?\\s+)?(\\d+)\\/(\\d+(?:[A-Za-z0-9:-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)/g,\n description: 'Illinois Compiled Statutes chapter-act citations (e.g., \"735 ILCS 5/2-1001\")',\n type: \"statute\",\n },\n {\n id: \"ill-rev-stat\",\n // Pre-1993 Illinois Revised Statutes (#330): `Ill. Rev. Stat. YYYY, ch. N,\n // par. N.N(N)`. Modern Illinois opinions still use this form when\n // referencing the historical version of a statute.\n //\n // Tolerance: spaced/no-space (`Ill. Rev. Stat.` / `Ill.Rev.Stat.`),\n // capitalized/lowercase `[Cc]h.`, singular/plural `pars?.`, optional\n // commas after `Stat.` and after the chapter number.\n //\n // Captures: (1) year-of-edition, (2) chapter (incl. letter suffix `110A`),\n // (3) paragraph body (subparagraphs + et seq.).\n regex:\n /\\bIll\\.?\\s*Rev\\.?\\s*Stat\\.?,?\\s+(\\d{4}),?\\s+[Cc]h\\.\\s+(\\d+[A-Z]?),?\\s+pars?\\.\\s+(\\d+(?:[A-Za-z0-9:-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)/g,\n description:\n \"Illinois Revised Statutes (pre-1993): Ill. Rev. Stat. YYYY, ch. N, par. N\",\n type: \"statute\",\n },\n {\n // Pre-1973 Colorado Revised Statutes (prose form): `Section 148-21-34,\n // Colorado Revised Statutes 1963` / `Section 13-25-126, Colo. Rev. Stat.\n // 1973`. Pre-1973 Colorado used a chapter-article-section numbering\n // scheme that surfaces here as the section body (`148-21-34`). The\n // section comes BEFORE the code name — opposite of the canonical\n // `<code> § <section>` shape — so this needs its own pattern.\n //\n // Listed BEFORE `abbreviated-code` so the prose-form container wins span\n // dedup over the abbreviated-code match (which would otherwise consume\n // the trailing `Colorado Revised Statutes 1963` and treat `1963` as the\n // section, producing a duplicate citation). #352\n //\n // Captures: (1) section body, (2) optional edition year (1963/1973).\n id: \"colorado-prose\",\n regex:\n /\\b[Ss]ection\\s+(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*),?\\s+Colo(?:rado)?\\.?\\s+Rev(?:ised)?\\.?\\s+Stat(?:utes)?\\.?(?:\\s+Ann(?:otated)?\\.?)?(?:\\s+(19\\d{2}))?/g,\n description:\n 'Pre-1973 Colorado prose form: \"Section 148-21-34, Colorado Revised Statutes 1963\" — #352',\n type: \"statute\",\n },\n {\n id: \"abbreviated-code\",\n regex: buildAbbreviatedCodeRegex(),\n description: \"Abbreviated state code citations for all US jurisdictions\",\n type: \"statute\",\n },\n {\n id: \"ca-bare-code\",\n regex: buildCaBareCodeRegex(),\n description:\n 'California bare-code citations (#296) — `Pen. Code § 148`, `Code Civ. Proc., § 1021.5`, `Bus. & Prof. Code § 17200` (no \"Cal.\" prefix; common in single-jurisdiction California practice).',\n type: \"statute\",\n },\n {\n // Pre-1975 Alabama Code (Code-prefix form): `Code 1940, T. 15, § 389`.\n // The leading `Code [of Alabama,] 1940` is an unambiguous Alabama signal,\n // so the title body uses `T.` / `Tit.` / `Title` interchangeably. The\n // section uses the period-followed-by-alphanumeric guard from #283.\n // Captures: (1) chapter (title), (2) section body. Year is hardcoded to\n // 1940 in the extractor (the prefix asserts 1940). #343\n id: \"ala-code-prefix\",\n regex:\n /\\bCode(?:\\s+of\\s+Alabama)?,?\\s+1940,?\\s+T(?:itle|it)?\\.\\s+(\\d+),?\\s+§\\s+(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*)/g,\n description:\n 'Alabama Code 1940 (Code-prefix form, pre-1975): \"Code 1940, T. 15, § 389\" / \"Code of Alabama 1940, T. NN, § NNN\" — #343',\n type: \"statute\",\n },\n {\n // Pre-1975 Alabama Code (Title-first with mandatory Code trailer):\n // `Title 26, Section 214, Code of Alabama 1940, as Recompiled 1958`.\n // Requires the trailing `Code [of Alabama] YYYY` clause so the spelled-out\n // `Title NN` form doesn't false-positive on bare prose like USC's\n // `Title 18, § 1001`. Captures: (1) title, (2) section, (3) edition year,\n // (4) optional recompilation year.\n id: \"ala-title-trailer\",\n regex:\n /\\bTitle\\s+(\\d+),?\\s+(?:§|Sec(?:tion)?s?\\.?)\\s+(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*),?\\s+Code(?:\\s+of\\s+Alabama)?,?\\s+(\\d{4})(?:,?\\s+(?:as\\s+)?[Rr]ecompiled\\s+(\\d{4}))?/g,\n description:\n 'Alabama Code (Title-first with Code trailer): \"Title 26, Section 214, Code of Alabama 1940, as Recompiled 1958\" — #343',\n type: \"statute\",\n },\n {\n // Pre-1975 Alabama Code (abbreviated bare form): `Tit. 52, § 361`.\n // The `Tit.` abbreviation is itself an Alabama signal — USC and other\n // federal codes spell out `Title` instead. Optional Code trailer captures\n // year and recompilation when present. Captures: (1) title, (2) section,\n // (3) optional edition year, (4) optional recompilation year.\n id: \"ala-tit-bare\",\n regex:\n /\\bTit\\.\\s+(\\d+),?\\s+§\\s+(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*)(?:,?\\s+Code(?:\\s+of\\s+Alabama)?,?\\s+(\\d{4})(?:,?\\s+(?:as\\s+)?[Rr]ecompiled\\s+(\\d{4}))?)?/g,\n description:\n 'Alabama Code (abbreviated `Tit.` form): \"Tit. 52, § 361\" — #343',\n type: \"statute\",\n },\n]\n","/**\n * Tokenization Layer for Citation Extraction\n *\n * Applies regex patterns to cleaned text to produce citation candidate tokens.\n * This is the second stage of the parsing pipeline:\n * 1. Clean text (remove HTML, normalize Unicode)\n * 2. Tokenize (apply patterns to find candidates) ← THIS MODULE\n * 3. Extract (parse metadata, validate against reporters-db)\n *\n * Tokenization is intentionally broad - it finds potential citations without\n * validating them. The extraction layer (Plan 5) validates tokens against\n * reporters-db and parses metadata.\n *\n * @module tokenize\n */\n\nimport type { Pattern } from \"@/patterns\"\nimport { casePatterns, journalPatterns, neutralPatterns, statutePatterns } from \"@/patterns\"\nimport { shortFormPatterns } from \"@/patterns/shortForm\"\nimport type { Span } from \"@/types/span\"\n\n/**\n * A token representing a potential citation found in cleaned text.\n *\n * Tokens are produced by applying regex patterns to cleaned text.\n * They include matched text, position in cleaned text, and pattern metadata\n * for use in the extraction layer.\n */\nexport interface Token {\n /** Matched text from input */\n text: string\n\n /** Position in cleaned text (cleanStart/cleanEnd only, no original positions yet) */\n span: Pick<Span, \"cleanStart\" | \"cleanEnd\">\n\n /** Pattern type that matched this token */\n type: Pattern[\"type\"]\n\n /** Pattern ID that matched this token */\n patternId: string\n}\n\n/**\n * Tokenizes cleaned text by applying regex patterns to find citation candidates.\n *\n * For each pattern in the patterns array:\n * 1. Apply pattern.regex.matchAll(cleanedText)\n * 2. Create Token for each match with position, text, and pattern metadata\n * 3. Collect all tokens from all patterns\n * 4. Sort by cleanStart position (ascending)\n *\n * Timeout protection: If a pattern throws (e.g., ReDoS), skip it and continue\n * with remaining patterns. Logs warning to console.\n *\n * Note: This function is synchronous because regex matching is inherently\n * synchronous. This enables both sync (extractCitations) and async\n * (extractCitationsAsync) APIs in Plan 6.\n *\n * @param cleanedText - Text that has been cleaned by cleanText() from Plan 1\n * @param patterns - Regex patterns to apply (defaults to all patterns from Plan 2)\n * @returns Array of tokens sorted by position (cleanStart ascending)\n *\n * @example\n * ```typescript\n * import { tokenize } from '@/tokenize'\n * import { cleanText } from '@/clean'\n *\n * const original = \"See Smith v. Doe, 500 F.2d 123 (9th Cir. 2020)\"\n * const { cleanedText } = cleanText(original)\n * const tokens = tokenize(cleanedText)\n * // tokens[0] = {\n * // text: \"500 F.2d 123\",\n * // span: { cleanStart: 18, cleanEnd: 30 },\n * // type: \"case\",\n * // patternId: \"federal-reporter\"\n * // }\n * ```\n */\nexport function tokenize(\n cleanedText: string,\n patterns: Pattern[] = [\n ...casePatterns,\n ...statutePatterns,\n ...journalPatterns,\n ...neutralPatterns,\n ...shortFormPatterns,\n ],\n): Token[] {\n const tokens: Token[] = []\n\n for (const pattern of patterns) {\n try {\n // Apply pattern to cleaned text\n const matches = cleanedText.matchAll(pattern.regex)\n\n for (const match of matches) {\n // Create token from match\n tokens.push({\n text: match[0],\n span: {\n cleanStart: match.index!,\n cleanEnd: match.index! + match[0].length,\n },\n type: pattern.type,\n patternId: pattern.id,\n })\n }\n } catch (error) {\n // Timeout protection: If pattern throws (ReDoS, etc.), skip it\n console.warn(\n `Pattern ${pattern.id} threw error, skipping:`,\n error instanceof Error ? error.message : String(error),\n )\n }\n }\n\n // Sort tokens by position (cleanStart ascending)\n tokens.sort((a, b) => a.span.cleanStart - b.span.cleanStart)\n\n return tokens\n}\n","/**\n * BK-Tree (Burkhard-Keller Tree)\n *\n * A metric tree that indexes strings by pairwise edit distance, enabling\n * threshold queries that prune dissimilar candidates via triangle inequality.\n * Used internally for supra citation party name matching.\n */\n\ninterface BKTreeNode {\n key: string\n insertionOrder: number\n children: Map<number, BKTreeNode>\n}\n\nexport interface BKQueryResult {\n key: string\n distance: number\n insertionOrder: number\n}\n\n/**\n * A BK-Tree for approximate string matching using a metric distance function.\n *\n * @param distanceFn - A metric distance function (must satisfy triangle inequality).\n * Accepts an optional third parameter `maxDistance` for early termination.\n */\nexport class BKTree {\n private root: BKTreeNode | null = null\n private nextOrder = 0\n private readonly distanceFn: (a: string, b: string, maxDistance?: number) => number\n\n constructor(distanceFn: (a: string, b: string, maxDistance?: number) => number) {\n this.distanceFn = distanceFn\n }\n\n /**\n * Insert a key into the tree. Duplicate keys are ignored (first insertion wins).\n */\n insert(key: string): void {\n const node: BKTreeNode = {\n key,\n insertionOrder: this.nextOrder++,\n children: new Map(),\n }\n\n if (this.root === null) {\n this.root = node\n return\n }\n\n let current = this.root\n while (true) {\n const d = this.distanceFn(key, current.key)\n if (d === 0) return // duplicate key, keep first\n const child = current.children.get(d)\n if (child) {\n current = child\n } else {\n current.children.set(d, node)\n return\n }\n }\n }\n\n /**\n * Find all keys within `maxDistance` of the query key.\n *\n * Uses triangle inequality to prune branches: if d(query, node) = k,\n * only children at distances in [k - maxDistance, k + maxDistance] can\n * possibly contain matches.\n *\n * Results are sorted by distance (ascending), then insertion order (ascending).\n */\n query(queryKey: string, maxDistance: number): BKQueryResult[] {\n if (this.root === null) return []\n\n const results: BKQueryResult[] = []\n const stack: BKTreeNode[] = [this.root]\n\n let node: BKTreeNode | undefined\n while ((node = stack.pop())) {\n // Compute exact distance — early termination is NOT safe here because\n // the BK-Tree needs the true distance for triangle inequality pruning.\n // A truncated distance shifts the child exploration range and causes false negatives.\n const d = this.distanceFn(queryKey, node.key)\n\n if (d <= maxDistance) {\n results.push({ key: node.key, distance: d, insertionOrder: node.insertionOrder })\n }\n\n // Triangle inequality pruning\n const lo = d - maxDistance\n const hi = d + maxDistance\n for (const [childDist, childNode] of node.children) {\n if (childDist >= lo && childDist <= hi) {\n stack.push(childNode)\n }\n }\n }\n\n results.sort((a, b) => a.distance - b.distance || a.insertionOrder - b.insertionOrder)\n return results\n }\n}\n","/**\n * Levenshtein Distance\n *\n * Calculates edit distance between strings for fuzzy party name matching\n * in supra citation resolution.\n *\n * Uses dynamic programming for O(m*n) time complexity.\n */\n\n/**\n * Calculates Levenshtein distance (edit distance) between two strings.\n *\n * Uses a space-optimized rolling two-row DP approach: only the previous and\n * current rows are kept in memory (O(min(m,n)) space instead of O(m*n)).\n *\n * @param a - First string\n * @param b - Second string\n * @param maxDistance - Optional threshold for early termination. When provided,\n * the function returns `maxDistance + 1` as soon as it determines the true\n * distance must exceed the threshold. This avoids unnecessary computation\n * for obviously dissimilar strings.\n * @returns Exact edit distance if ≤ maxDistance, otherwise maxDistance + 1\n */\nexport function levenshteinDistance(a: string, b: string, maxDistance: number = Infinity): number {\n if (a.length === 0) return Math.min(b.length, maxDistance + 1)\n if (b.length === 0) return Math.min(a.length, maxDistance + 1)\n\n // Ensure short is the shorter string so rows are min(m,n) long\n const short = a.length <= b.length ? a : b\n const long = a.length <= b.length ? b : a\n\n const cols = short.length\n let prev = Array.from({ length: cols + 1 }, (_, k) => k) // base-case row\n let curr = new Array<number>(cols + 1)\n\n for (let i = 1; i <= long.length; i++) {\n curr[0] = i\n let rowMin = i // curr[0] is always i\n\n for (let j = 1; j <= cols; j++) {\n if (long[i - 1] === short[j - 1]) {\n curr[j] = prev[j - 1]\n } else {\n curr[j] = 1 + Math.min(prev[j], curr[j - 1], prev[j - 1])\n }\n if (curr[j] < rowMin) rowMin = curr[j]\n }\n\n // Early termination: row minimums are non-decreasing, so if the\n // cheapest cell already exceeds the threshold, no future row can help\n if (rowMin > maxDistance) return maxDistance + 1\n\n const swap = prev\n prev = curr\n curr = swap\n }\n\n return prev[cols]\n}\n\n/**\n * Calculates normalized Levenshtein similarity (0-1 scale).\n *\n * Returns similarity score where:\n * - 1.0 = identical strings\n * - 0.0 = completely different\n *\n * Comparison is case-insensitive.\n *\n * @param a - First string\n * @param b - Second string\n * @returns Similarity score from 0 to 1\n */\nexport function normalizedLevenshteinDistance(a: string, b: string): number {\n // Normalize to lowercase for case-insensitive comparison\n const lowerA = a.toLowerCase()\n const lowerB = b.toLowerCase()\n\n // Calculate raw edit distance\n const distance = levenshteinDistance(lowerA, lowerB)\n\n // Normalize by max length\n const maxLength = Math.max(lowerA.length, lowerB.length)\n if (maxLength === 0) return 1.0 // Both empty strings\n\n // Convert distance to similarity: 1 - (distance / maxLength)\n return 1 - distance / maxLength\n}\n","/**\n * Scope Boundary Detection\n *\n * Detects paragraph/section boundaries in text and validates whether\n * an antecedent citation is within the resolution scope.\n */\n\nimport type { FootnoteMap } from \"../footnotes/types\"\nimport type { Citation } from \"../types/citation\"\nimport type { ScopeStrategy } from \"./types\"\n\n/**\n * Binary search returning the insertion point for `value` in sorted `arr`.\n * Returns the smallest index i such that arr[i] > value (or arr.length if none).\n */\nfunction bisectRight(arr: number[], value: number): number {\n let lo = 0\n let hi = arr.length\n while (lo < hi) {\n const mid = (lo + hi) >>> 1\n if (arr[mid] <= value) lo = mid + 1\n else hi = mid\n }\n return lo\n}\n\n/**\n * Detects paragraph boundaries from text and assigns each citation to a paragraph.\n *\n * @param text - Original document text\n * @param citations - Extracted citations with position spans\n * @param boundaryPattern - Regex pattern to detect boundaries (default: /\\n\\n+/)\n * @returns Map of citation index to paragraph number (0-based)\n */\nexport function detectParagraphBoundaries(\n text: string,\n citations: Citation[],\n boundaryPattern: RegExp = /\\n\\n+/g,\n): Map<number, number> {\n const paragraphMap = new Map<number, number>()\n\n // Find all paragraph boundaries (positions in text)\n const boundaries: number[] = [0] // Start of document is first boundary\n let match: RegExpExecArray | null\n\n while ((match = boundaryPattern.exec(text)) !== null) {\n // Boundary is at end of match (start of next paragraph)\n boundaries.push(match.index + match[0].length)\n }\n\n boundaries.push(text.length) // End of document\n\n // Assign each citation to a paragraph\n for (let i = 0; i < citations.length; i++) {\n const citation = citations[i]\n const citationStart = citation.span.originalStart\n\n paragraphMap.set(i, bisectRight(boundaries, citationStart) - 1)\n }\n\n return paragraphMap\n}\n\n/**\n * Build a scope map from footnote zones.\n * Zone 0 = body text, Zone N = footnote N.\n *\n * The footnoteMap must be in the same coordinate space as the citation spans\n * being looked up. When called from extractCitations, both are in clean-text\n * coordinates (zones mapped through TransformationMap, spans set during extraction).\n */\nexport function buildFootnoteScopes(\n citations: Citation[],\n footnoteMap: FootnoteMap,\n): Map<number, number> {\n const scopeMap = new Map<number, number>()\n\n for (let i = 0; i < citations.length; i++) {\n const pos = citations[i].span.cleanStart\n\n let zoneId = 0\n let lo = 0\n let hi = footnoteMap.length - 1\n\n while (lo <= hi) {\n const mid = (lo + hi) >>> 1\n const zone = footnoteMap[mid]\n\n if (pos < zone.start) {\n hi = mid - 1\n } else if (pos >= zone.end) {\n lo = mid + 1\n } else {\n zoneId = zone.footnoteNumber\n break\n }\n }\n\n scopeMap.set(i, zoneId)\n }\n\n return scopeMap\n}\n\n/**\n * Checks if an antecedent citation is within resolution scope.\n *\n * @param antecedentIndex - Index of the antecedent citation\n * @param currentIndex - Index of current citation being resolved\n * @param paragraphMap - Map of citation index to paragraph/zone number\n * @param strategy - Scope boundary strategy\n * @param allowCrossZone - If true (footnote strategy), allow resolution from footnote to body (zone 0)\n * @returns true if antecedent is within scope, false otherwise\n */\nexport function isWithinBoundary(\n antecedentIndex: number,\n currentIndex: number,\n paragraphMap: Map<number, number>,\n strategy: ScopeStrategy,\n allowCrossZone = false,\n): boolean {\n if (strategy === \"none\") {\n // No boundary restriction - can resolve across entire document\n return true\n }\n\n // Get scope numbers for both citations\n const antecedentScope = paragraphMap.get(antecedentIndex)\n const currentScope = paragraphMap.get(currentIndex)\n\n // If either is undefined, default to allowing resolution\n if (antecedentScope === undefined || currentScope === undefined) {\n return true\n }\n\n if (antecedentScope === currentScope) {\n return true\n }\n\n // Cross-zone: footnote strategy allows supra/shortFormCase to reach body\n if (strategy === \"footnote\" && allowCrossZone && antecedentScope === 0) {\n return true\n }\n\n // For paragraph/section strategies, citations must be in same boundary.\n // (section currently behaves the same as paragraph — future enhancement.)\n return false\n}\n","/**\n * Document-Scoped Citation Resolver\n *\n * Resolves short-form citations (Id./supra/short-form case) to their full antecedent citations\n * by maintaining resolution context and enforcing scope boundaries.\n *\n * Resolution rules:\n * - Id. resolves to the most recently cited authority (within scope)\n * - Supra resolves to full citation with matching party name (within scope)\n * - Short-form case resolves to full case with matching volume/reporter (within scope)\n */\n\nimport type {\n Citation,\n FullCaseCitation,\n IdCitation,\n ShortFormCaseCitation,\n SupraCitation,\n} from \"../types/citation\"\nimport { isFullCitation } from \"../types/guards\"\nimport type { Span } from \"../types/span\"\nimport { BKTree } from \"./bkTree\"\nimport { levenshteinDistance } from \"./levenshtein\"\nimport { buildFootnoteScopes, detectParagraphBoundaries, isWithinBoundary } from \"./scopeBoundary\"\nimport type {\n ResolutionContext,\n ResolutionOptions,\n ResolutionResult,\n ResolvedCitation,\n} from \"./types\"\n\n/**\n * Returns the citation's `fullSpan` if it has one. Only `case` and `docket`\n * citations carry `fullSpan` (set during case-name backward search). Other\n * full citation types (statute, journal, neutral, etc.) don't have a\n * case-name span concept and never participate in parenthetical-child checks.\n */\nfunction getFullSpan(citation: Citation): Span | undefined {\n if (citation.type === \"case\" || citation.type === \"docket\") {\n return citation.fullSpan\n }\n return undefined\n}\n\n/**\n * Document-scoped resolver that processes citations sequentially\n * and resolves short-form citations to their antecedents.\n */\nexport class DocumentResolver {\n private readonly citations: Citation[]\n private readonly text: string\n private readonly options: Required<Omit<ResolutionOptions, \"footnoteMap\">> & {\n footnoteMap: ResolutionOptions[\"footnoteMap\"]\n }\n private readonly context: ResolutionContext\n private readonly partyNameTree: BKTree\n\n /**\n * Creates a new DocumentResolver.\n *\n * @param citations - All citations in document (in order of appearance)\n * @param text - Original document text\n * @param options - Resolution options\n */\n constructor(citations: Citation[], text: string, options: ResolutionOptions = {}) {\n this.citations = citations\n this.text = text\n\n // Apply defaults to options\n this.options = {\n scopeStrategy: options.scopeStrategy ?? \"none\",\n autoDetectParagraphs: options.autoDetectParagraphs ?? true,\n paragraphBoundaryPattern: options.paragraphBoundaryPattern ?? /\\n\\n+/g,\n fuzzyPartyMatching: options.fuzzyPartyMatching ?? true,\n partyMatchThreshold: options.partyMatchThreshold ?? 0.8,\n reportUnresolved: options.reportUnresolved ?? true,\n footnoteMap: options.footnoteMap,\n }\n\n this.partyNameTree = new BKTree(levenshteinDistance)\n\n // Initialize resolution context\n this.context = {\n citationIndex: 0,\n allCitations: citations,\n lastResolvedIndex: undefined,\n fullCitationHistory: new Map(),\n paragraphMap: new Map(),\n }\n\n // Detect paragraph boundaries if enabled\n if (this.options.autoDetectParagraphs) {\n this.context.paragraphMap = detectParagraphBoundaries(\n text,\n citations,\n this.options.paragraphBoundaryPattern,\n )\n }\n\n // Override with footnote scopes when available\n if (this.options.scopeStrategy === \"footnote\" && this.options.footnoteMap) {\n this.context.paragraphMap = buildFootnoteScopes(citations, this.options.footnoteMap)\n }\n }\n\n /**\n * Resolves all citations in the document.\n *\n * @returns Array of citations with resolution metadata\n */\n resolve(): ResolvedCitation[] {\n const resolved: ResolvedCitation[] = []\n\n for (let i = 0; i < this.citations.length; i++) {\n this.context.citationIndex = i\n const citation = this.citations[i]\n\n // Resolve based on citation type\n let resolution: ResolutionResult | undefined\n\n switch (citation.type) {\n case \"id\":\n resolution = this.resolveId(citation)\n break\n case \"supra\":\n resolution = this.resolveSupra(citation)\n break\n case \"shortFormCase\":\n resolution = this.resolveShortFormCase(citation)\n break\n default:\n // Full citation - update context for future resolutions.\n if (isFullCitation(citation)) {\n // Bluebook Rule 4.1: Id. refers to the immediately preceding\n // *cited authority*. A full citation parsed inside another\n // citation's explanatory parenthetical (e.g. \"(citing X)\" or\n // \"(quoting Y)\") is a sub-reference within the parent's\n // citation, not the cited authority of that sentence — so it\n // must not become Id.'s default antecedent. Detect this by\n // checking whether the current cite's span lies within an\n // earlier full cite's fullSpan. We still track it for\n // supra/short-form resolution.\n const isParentheticalChild = resolved.some((prior) => {\n const priorFullSpan = getFullSpan(prior)\n if (!priorFullSpan) return false\n return (\n priorFullSpan.cleanStart <= citation.span.cleanStart &&\n priorFullSpan.cleanEnd >= citation.span.cleanEnd\n )\n })\n if (!isParentheticalChild) {\n this.context.lastResolvedIndex = i\n }\n this.trackFullCitation(citation, i)\n }\n break\n }\n\n // After resolving a short-form citation, update lastResolvedIndex\n // to the full citation it resolved to (transitive resolution).\n // If resolution failed, lastResolvedIndex is NOT updated --\n // a subsequent Id. will also fail (matching Python eyecite behavior).\n if (resolution?.resolvedTo !== undefined) {\n this.context.lastResolvedIndex = resolution.resolvedTo\n }\n\n // Add citation with resolution metadata\n // Type assertion is safe: runtime logic only sets resolution on short-form citations\n resolved.push({\n ...citation,\n resolution,\n } as ResolvedCitation)\n }\n\n return resolved\n }\n\n /**\n * Resolves Id. citation to the most recently cited authority.\n * Uses lastResolvedIndex which tracks the most recent successfully\n * resolved citation (full, short-form, supra, or Id.).\n */\n private resolveId(_citation: IdCitation): ResolutionResult | undefined {\n const currentIndex = this.context.citationIndex\n const antecedentIndex = this.context.lastResolvedIndex\n\n // No preceding citation has been resolved yet\n if (antecedentIndex === undefined) {\n return this.createFailureResult(\"No preceding citation found\")\n }\n\n // Check scope boundary\n if (!this.isWithinScope(antecedentIndex, currentIndex)) {\n return this.createFailureResult(\"Antecedent citation outside scope boundary\")\n }\n\n return {\n resolvedTo: antecedentIndex,\n confidence: 1.0, // Id. resolution is unambiguous when successful\n }\n }\n\n /**\n * Resolves supra citation by matching party name.\n */\n private resolveSupra(citation: SupraCitation): ResolutionResult | undefined {\n if (!citation.partyName) return undefined // Standalone supra — cannot resolve by party name\n const currentIndex = this.context.citationIndex\n const targetPartyName = this.normalizePartyName(citation.partyName)\n\n // Query BK-Tree for candidates within distance threshold, then filter by scope\n const queryLen = targetPartyName.length\n const threshold = this.options.partyMatchThreshold\n // Safe upper bound: guarantees no match with similarity >= threshold is missed\n const maxDistance = queryLen === 0 ? 0 : Math.ceil((queryLen * (1 - threshold)) / threshold)\n const candidates = this.partyNameTree.query(targetPartyName, maxDistance)\n\n // Sort by insertion order to match Map iteration behavior (first-inserted wins on ties)\n candidates.sort((a, b) => a.insertionOrder - b.insertionOrder)\n\n let bestMatch: { index: number; similarity: number } | undefined\n\n for (const candidate of candidates) {\n const citationIndex = this.context.fullCitationHistory.get(candidate.key)\n if (citationIndex === undefined) continue\n\n // Check scope boundary (supra allows cross-zone: footnote -> body)\n if (!this.isWithinScope(citationIndex, currentIndex, true)) continue\n\n // Convert distance to normalized similarity\n const maxLen = Math.max(queryLen, candidate.key.length)\n const similarity = maxLen === 0 ? 1.0 : 1 - candidate.distance / maxLen\n\n // Update best match if this is better (strict > preserves first-wins tie-breaking)\n if (!bestMatch || similarity > bestMatch.similarity) {\n bestMatch = { index: citationIndex, similarity }\n }\n }\n\n // Check if we found a match above threshold\n if (!bestMatch) {\n return this.createFailureResult(\"No full citation found in scope\")\n }\n\n if (bestMatch.similarity < this.options.partyMatchThreshold) {\n return this.createFailureResult(\n `Party name similarity ${bestMatch.similarity.toFixed(2)} below threshold ${this.options.partyMatchThreshold}`,\n )\n }\n\n // Return successful resolution with confidence based on similarity\n const warnings: string[] = []\n if (bestMatch.similarity < 1.0) {\n warnings.push(`Fuzzy match: similarity ${bestMatch.similarity.toFixed(2)}`)\n }\n\n return {\n resolvedTo: bestMatch.index,\n confidence: bestMatch.similarity,\n warnings: warnings.length > 0 ? warnings : undefined,\n }\n }\n\n /**\n * Resolves short-form case citation by matching volume/reporter, with\n * party-name disambiguation when the short-form includes a back-reference\n * name (#278).\n *\n * Algorithm:\n * 1. Collect all backward candidates with matching volume + normalized\n * reporter that are in-scope.\n * 2. If the short-form has `partyNameNormalized`: prefer the candidate\n * whose plaintiff/defendant matches (substring containment in either\n * direction handles abbreviations: `\"Smith\" ⊂ \"Smith\"` or\n * `\"Smith\"` in `\"Smith, Inc.\"`). Tie-break by recency.\n * 3. If no candidate matches the party name (or no party name on the\n * short-form): fall back to recency.\n */\n private resolveShortFormCase(citation: ShortFormCaseCitation): ResolutionResult | undefined {\n const currentIndex = this.context.citationIndex\n const targetReporter = this.normalizeReporter(citation.reporter)\n const targetParty = citation.partyNameNormalized\n\n // Collect all backward candidates (most recent first) that match\n // vol+reporter AND are in-scope.\n const candidates: number[] = []\n for (let i = currentIndex - 1; i >= 0; i--) {\n const candidate = this.citations[i]\n if (candidate.type !== \"case\") continue\n if (candidate.volume !== citation.volume) continue\n if (this.normalizeReporter(candidate.reporter) !== targetReporter) continue\n if (!this.isWithinScope(i, currentIndex, true)) continue\n candidates.push(i)\n }\n\n if (candidates.length === 0) {\n return this.createFailureResult(\"No matching full case citation found\")\n }\n\n // With a party name, prefer the candidate whose plaintiff or defendant\n // normalized name contains (or is contained by) the short-form's party\n // name. Substring containment in either direction tolerates common\n // abbreviation patterns: short-form `Smith` matches full `Smith, Inc.`\n // and vice versa. Recency breaks ties.\n if (targetParty) {\n const namedMatch = candidates.find((idx) => {\n const c = this.citations[idx]\n if (c.type !== \"case\") return false\n const plaintiff = c.plaintiffNormalized\n const defendant = c.defendantNormalized\n const hit = (name: string | undefined) =>\n name !== undefined &&\n (name === targetParty ||\n name.includes(targetParty) ||\n targetParty.includes(name))\n return hit(plaintiff) || hit(defendant)\n })\n if (namedMatch !== undefined) {\n return {\n resolvedTo: namedMatch,\n confidence: 0.98, // Higher than bare vol+reporter — party-name disambiguation tightens.\n }\n }\n }\n\n // No party name (or no name match): pick most recent candidate.\n return {\n resolvedTo: candidates[0],\n confidence: 0.95,\n }\n }\n\n /**\n * Tracks a full citation in the resolution history.\n * Extracts party name for supra resolution.\n * Uses extracted party names (Phase 7) when available, falls back to backward search.\n */\n private trackFullCitation(citation: Citation, index: number): void {\n // Only case citations have party names for supra resolution\n if (citation.type === \"case\") {\n // Phase 7: Use extracted party names when available\n // Defendant name stored first (preferred for Bluebook-style supra matching)\n if (citation.defendantNormalized) {\n this.context.fullCitationHistory.set(citation.defendantNormalized, index)\n this.partyNameTree.insert(citation.defendantNormalized)\n }\n if (citation.plaintiffNormalized) {\n this.context.fullCitationHistory.set(citation.plaintiffNormalized, index)\n this.partyNameTree.insert(citation.plaintiffNormalized)\n }\n\n // Fallback: backward search from text (pre-Phase 7 compatibility)\n if (!citation.plaintiffNormalized && !citation.defendantNormalized) {\n const partyName = this.extractPartyName(citation)\n if (partyName) {\n const normalized = this.normalizePartyName(partyName)\n this.context.fullCitationHistory.set(normalized, index)\n this.partyNameTree.insert(normalized)\n }\n }\n }\n }\n\n /**\n * Extracts party name from full case citation text.\n * Handles \"Party v. Party\" format by looking at text before citation span.\n */\n private extractPartyName(citation: FullCaseCitation): string | undefined {\n // Look at text before citation span to find party names\n // Case citations typically appear as: \"Smith v. Jones, 100 F.2d 10\"\n // But tokenizer only captures \"100 F.2d 10\" - we need to look backwards in text\n\n const citationStart = citation.span.originalStart\n // Look backwards up to 100 characters for party name\n const lookbackStart = Math.max(0, citationStart - 100)\n const beforeText = this.text.substring(lookbackStart, citationStart)\n\n // Match pattern: \"FirstParty v. SecondParty, \" before the citation\n // Capture the first party name (handles single-letter party names like \"A\" or \"B\")\n const vMatch = beforeText.match(\n /([A-Z][a-zA-Z]*(?:\\s+[A-Z][a-zA-Z]*)*)\\s+v\\.?\\s+[A-Z][a-zA-Z]*(?:\\s+[A-Z][a-zA-Z]*)*,\\s*$/,\n )\n if (vMatch) {\n return this.stripSignalWords(vMatch[1].trim())\n }\n\n // Fallback: try to find any capitalized word(s) before comma\n const beforeComma = beforeText.match(/([A-Z][a-zA-Z]*(?:\\s+[A-Z][a-zA-Z]*)*),\\s*$/)\n if (beforeComma) {\n return this.stripSignalWords(beforeComma[1].trim())\n }\n return undefined\n }\n\n /**\n * Strips citation signal words that may precede party names.\n * E.g., \"In Smith\" → \"Smith\", \"See Also Jones\" → \"Jones\"\n * Preserves \"In re\" which is a case name format, not a signal word.\n */\n private stripSignalWords(name: string): string {\n const stripped = name\n .replace(/^(?:In(?!\\s+re\\b)|See(?:\\s+[Aa]lso)?|Compare|But(?:\\s+[Ss]ee)?|Cf\\.?|Also)\\s+/i, \"\")\n .trim()\n // Only return stripped version if something remains\n return stripped.length > 0 ? stripped : name\n }\n\n /**\n * Normalizes party name for matching.\n */\n private normalizePartyName(name: string): string {\n return name\n .toLowerCase()\n .replace(/\\s+/g, \" \") // Normalize whitespace\n .trim()\n }\n\n /**\n * Normalizes reporter abbreviation for matching.\n */\n private normalizeReporter(reporter: string): string {\n return reporter\n .toLowerCase()\n .replace(/\\s+/g, \"\") // Remove spaces (F.2d vs F. 2d)\n .replace(/\\./g, \"\") // Remove periods\n }\n\n /**\n * Checks if antecedent citation is within scope boundary.\n */\n private isWithinScope(\n antecedentIndex: number,\n currentIndex: number,\n allowCrossZone = false,\n ): boolean {\n return isWithinBoundary(\n antecedentIndex,\n currentIndex,\n this.context.paragraphMap,\n this.options.scopeStrategy,\n allowCrossZone,\n )\n }\n\n /**\n * Creates a failure result for unresolved citations.\n */\n private createFailureResult(reason: string): ResolutionResult | undefined {\n if (this.options.reportUnresolved) {\n return {\n resolvedTo: undefined,\n failureReason: reason,\n confidence: 0.0,\n }\n }\n return undefined\n }\n}\n","/**\n * Citation Resolution\n *\n * Resolves short-form citations (Id./supra/short-form case) to their full antecedents.\n *\n * @example\n * ```ts\n * import { resolveCitations } from 'eyecite-ts/resolve'\n * import { extractCitations } from 'eyecite-ts'\n *\n * const text = 'See Smith v. Jones, 500 F.2d 100 (1974). Id. at 105.'\n * const citations = extractCitations(text)\n * const resolved = resolveCitations(citations, text)\n *\n * // resolved[1] is Id. citation with resolution.resolvedTo = 0\n * console.log(resolved[1].resolution?.resolvedTo) // 0 (points to Smith v. Jones)\n * ```\n */\n\nimport type { Citation } from \"../types/citation\"\nimport { DocumentResolver } from \"./DocumentResolver\"\nimport type { ResolutionOptions, ResolvedCitation } from \"./types\"\n\n/**\n * Resolves short-form citations to their full antecedents.\n *\n * Convenience wrapper around DocumentResolver that handles common use cases.\n *\n * @param citations - Extracted citations in order of appearance\n * @param text - Original document text\n * @param options - Resolution options\n * @returns Citations with resolution metadata\n */\nexport function resolveCitations(\n citations: Citation[],\n text: string,\n options?: ResolutionOptions,\n): ResolvedCitation[] {\n const resolver = new DocumentResolver(citations, text, options)\n return resolver.resolve()\n}\n\n// Re-export core types and classes\nexport { DocumentResolver } from \"./DocumentResolver\"\nexport type {\n ResolutionOptions,\n ResolutionResult,\n ResolvedCitation,\n ScopeStrategy,\n} from \"./types\"\n","/**\n * Parallel Citation Detection\n *\n * Detects parallel citation groups (same case in multiple reporters) using\n * comma-separated case citations sharing a closing parenthetical.\n *\n * Detection happens after tokenization and deduplication, before extraction\n * in the main extractCitations pipeline.\n *\n * @module extract/detectParallel\n */\n\nimport type { Token } from \"@/tokenize/tokenizer\"\n\n/**\n * Maximum characters allowed between end of comma and start of next citation.\n * Bluebook standard uses tight spacing: \"500 F.2d 123, 200 F. Supp. 456\"\n */\nconst MAX_PROXIMITY = 5\n\n/**\n * Maximum total gap (chars) between end of one citation and start of next\n * to even consider them as parallel candidates. Beyond this distance, we can\n * skip all other checks (comma, parenthetical, etc.) for performance.\n * Includes comma, spaces, and potential pincite: \", 125, \" = ~10 chars\n */\nconst MAX_GAP_FOR_PARALLEL = 20\n\n/**\n * Detect parallel citation groups from tokenized citations.\n *\n * Returns a map of primary citation index to array of secondary citation indices.\n * Parallel citations are comma-separated case citations sharing a parenthetical.\n *\n * Detection algorithm:\n * 1. Iterate tokens with lookahead (i, i+1, i+2...)\n * 2. Check if token[i] and token[i+1] are both case citations\n * 3. Check if comma separates them (within MAX_PROXIMITY chars)\n * 4. Check if both citations share a closing parenthetical (via cleaned text)\n * 5. If all conditions met, add to parallel group\n * 6. Continue for chain (i+1, i+2, i+3...) until no more matches\n *\n * @param tokens - Tokenized citations (after deduplication)\n * @param cleanedText - Cleaned text to check for commas and parentheticals\n * @returns Map of primary index to array of secondary indices\n *\n * @example\n * ```typescript\n * const tokens = [\n * { text: \"410 U.S. 113\", span: { cleanStart: 0, cleanEnd: 12 }, type: \"case\" },\n * { text: \"93 S. Ct. 705\", span: { cleanStart: 14, cleanEnd: 27 }, type: \"case\" }\n * ]\n * const cleaned = \"410 U.S. 113, 93 S. Ct. 705 (1973)\"\n * const result = detectParallelCitations(tokens, cleaned)\n * // result = Map { 0 => [1] }\n * ```\n */\nexport function detectParallelCitations(tokens: Token[], cleanedText = \"\"): Map<number, number[]> {\n const parallelGroups = new Map<number, number[]>()\n\n // Edge cases: empty array or no text\n if (tokens.length === 0 || cleanedText === \"\") {\n return parallelGroups\n }\n\n // Track which tokens are already in a parallel group (as secondary)\n const usedAsSecondary = new Set<number>()\n\n for (let i = 0; i < tokens.length; i++) {\n const primary = tokens[i]\n\n // Skip if not a case citation\n if (primary.type !== \"case\") {\n continue\n }\n\n // Skip if already used as secondary in another group\n if (usedAsSecondary.has(i)) {\n continue\n }\n\n const secondaryIndices: number[] = []\n\n // Look ahead for potential secondary citations\n // Chain detection: \"A, B, C (year)\" where A is primary, B and C are secondaries\n for (let j = i + 1; j < tokens.length; j++) {\n const secondary = tokens[j]\n\n // Only case citations can be parallel\n if (secondary.type !== \"case\") {\n break // Stop looking once we hit non-case citation\n }\n\n // Check proximity: comma should be right after primary (or previous secondary in chain)\n const prevToken = j === i + 1 ? primary : tokens[j - 1]\n const gapStart = prevToken.span.cleanEnd\n const gapEnd = secondary.span.cleanStart\n\n // Early exit: If gap is too large, no need to check comma/parenthetical\n // This optimization reduces O(n²) to O(n×k) where k is avg tokens within MAX_GAP\n const gapSize = gapEnd - gapStart\n if (gapSize > MAX_GAP_FOR_PARALLEL) {\n break // Too far apart to be parallel, stop looking\n }\n\n // Extract the gap text between citations\n const gapText = cleanedText.substring(gapStart, gapEnd)\n\n // California Style Manual bracket form (#237): the parallel citation\n // is wrapped in brackets — `<primary> (<year>) [<secondary>]`. Check\n // this BEFORE the comma-requirement gate so we don't reject CA parallels.\n const inBracket =\n gapText.includes(\"[\") &&\n cleanedText[secondary.span.cleanEnd] === \"]\"\n if (inBracket) {\n secondaryIndices.push(j)\n usedAsSecondary.add(j)\n // CA brackets always close after a single parallel cite — chain ends here.\n break\n }\n\n // Bluebook requires comma separator for parallel citations\n if (!gapText.includes(\",\")) {\n break // No comma = not parallel, stop looking\n }\n\n // Check proximity: distance from comma to next citation start\n // MAX_PROXIMITY enforces tight spacing: \"A, B\" not \"A, B\"\n const commaIndex = gapText.indexOf(\",\")\n const distanceAfterComma = gapText.length - commaIndex - 1\n\n if (distanceAfterComma > MAX_PROXIMITY) {\n break // Too far apart, stop looking\n }\n\n // Check for shared parenthetical\n // Both citations must share the SAME closing parenthetical\n // Reject: \"A (1970), B (1971)\" - separate parens = different cases\n // Accept: \"A, B (1970)\" - shared paren = parallel citations\n const textBetween = cleanedText.substring(primary.span.cleanEnd, secondary.span.cleanEnd)\n if (textBetween.includes(\")\")) {\n break // Separate parentheticals = not parallel, stop looking\n }\n\n // Check that there IS a parenthetical after the secondary citation\n if (!hasSharedParenthetical(cleanedText, secondary.span.cleanEnd)) {\n break // No shared parenthetical, stop looking\n }\n\n // All conditions met - this is a parallel citation\n secondaryIndices.push(j)\n usedAsSecondary.add(j)\n }\n\n // If we found any secondary citations, record the group\n if (secondaryIndices.length > 0) {\n parallelGroups.set(i, secondaryIndices)\n }\n }\n\n return parallelGroups\n}\n\n/**\n * Check if there's a closing parenthetical after the given position.\n *\n * This is a simple heuristic: look for \"(...)\" pattern within reasonable distance.\n * Full parenthetical parsing happens in extractCase, this just validates presence.\n *\n * @param cleanedText - Cleaned text\n * @param position - Position to start searching from\n * @returns true if closing parenthetical found\n */\nfunction hasSharedParenthetical(cleanedText: string, position: number): boolean {\n // Look ahead up to 200 characters for opening parenthesis\n const searchText = cleanedText.substring(position, position + 200)\n\n // Find opening parenthesis\n const openIndex = searchText.indexOf(\"(\")\n if (openIndex === -1) {\n return false\n }\n\n // Find matching closing parenthesis (simple depth tracking)\n let depth = 0\n for (let i = openIndex; i < searchText.length; i++) {\n if (searchText[i] === \"(\") {\n depth++\n } else if (searchText[i] === \")\") {\n depth--\n if (depth === 0) {\n // Found matching closing parenthesis\n return true\n }\n }\n }\n\n return false\n}\n","/**\n * String Citation Detection\n *\n * Detects semicolon-separated string citation groups (multiple authorities\n * supporting the same proposition). Runs as a post-extract phase after\n * individual citation extraction and subsequent history linking.\n *\n * @module extract/detectStringCites\n */\n\nimport type { Citation, CitationSignal, FullCaseCitation } from \"@/types/citation\"\n\n/**\n * Signal words recognized between string citation members (case-insensitive).\n * Longer patterns first so \"see also\" matches before \"see\".\n *\n * Each entry also carries a pre-built `endRegex` for matching the signal at the\n * end of preceding text (used in leading-signal detection). These are built once\n * at module load to avoid reconstructing RegExp objects inside hot loops.\n */\nconst SIGNAL_PATTERNS: ReadonlyArray<{\n regex: RegExp\n endRegex: RegExp\n signal: CitationSignal\n}> = buildSignalPatterns()\n\nfunction buildSignalPatterns() {\n // Combined `, e.g.` forms (Bluebook Rule 1.3) come BEFORE their bare-signal\n // counterparts so the alternation prefers the more specific match. The\n // trailing `,?` after `e\\.g\\.` allows the optional comma that typically\n // separates the signal from the citation (e.g., \"See, e.g., Smith v. Jones\").\n const raw: ReadonlyArray<{ regex: RegExp; signal: CitationSignal }> = [\n { regex: /^but\\s+cf\\.,\\s+e\\.g\\.,?(?=\\s|$)/i, signal: \"but cf., e.g.\" },\n { regex: /^see\\s+also,\\s+e\\.g\\.,?(?=\\s|$)/i, signal: \"see also, e.g.\" },\n { regex: /^but\\s+see,\\s+e\\.g\\.,?(?=\\s|$)/i, signal: \"but see, e.g.\" },\n { regex: /^cf\\.,\\s+e\\.g\\.,?(?=\\s|$)/i, signal: \"cf., e.g.\" },\n { regex: /^see,\\s+e\\.g\\.,?(?=\\s|$)/i, signal: \"see, e.g.\" },\n { regex: /^see\\s+generally\\b/i, signal: \"see generally\" },\n { regex: /^see\\s+also\\b/i, signal: \"see also\" },\n { regex: /^but\\s+see\\b/i, signal: \"but see\" },\n { regex: /^but\\s+cf\\.?(?=\\s|$)/i, signal: \"but cf\" },\n { regex: /^compare\\b/i, signal: \"compare\" },\n { regex: /^accord\\b/i, signal: \"accord\" },\n { regex: /^contra\\b/i, signal: \"contra\" },\n { regex: /^see\\b/i, signal: \"see\" },\n { regex: /^cf\\.?(?=\\s|$)/i, signal: \"cf\" },\n { regex: /^e\\.g\\.,?(?=\\s|$)/i, signal: \"e.g.\" },\n ]\n return raw.map(({ regex, signal }) => ({\n regex,\n // Matches the signal at the end of a string (for leading-signal lookback).\n // Replaces the leading `^` anchor with a negative lookbehind for word chars.\n endRegex: new RegExp(`${regex.source.replace(/^\\^/, \"(?<![a-z])\")}\\\\s*$`, regex.flags),\n signal,\n }))\n}\n\n/**\n * Get the end position of a citation's full extent in cleaned text.\n * Uses fullSpan if available on any citation type (currently only case\n * citations carry fullSpan, but this is future-proof for other types).\n */\nfunction getCitationEnd(c: Citation): number {\n const fullSpan = \"fullSpan\" in c ? (c as FullCaseCitation).fullSpan : undefined\n return fullSpan ? fullSpan.cleanEnd : c.span.cleanEnd\n}\n\n/**\n * Get the start position of a citation's full extent in cleaned text.\n * Uses fullSpan if available on any citation type.\n */\nfunction getCitationStart(c: Citation): number {\n const fullSpan = \"fullSpan\" in c ? (c as FullCaseCitation).fullSpan : undefined\n return fullSpan ? fullSpan.cleanStart : c.span.cleanStart\n}\n\n/** Set a signal on a citation without triggering type errors on the union. */\nfunction setSignal(c: Citation, sig: CitationSignal): void {\n ;(c as { signal?: CitationSignal }).signal = sig\n}\n\n/**\n * Parse a recognized signal word from text.\n * Returns the normalized signal and the length of the match, or undefined.\n */\nfunction parseSignal(text: string): { signal: CitationSignal; length: number } | undefined {\n const trimmed = text.trimStart()\n for (const { regex, signal } of SIGNAL_PATTERNS) {\n const match = regex.exec(trimmed)\n if (match) {\n return { signal, length: match[0].length }\n }\n }\n return undefined\n}\n\n/**\n * Check if the gap text between two citations is a valid string cite separator.\n *\n * Valid gaps contain only: whitespace, a single semicolon, and optionally a\n * recognized signal word. Returns the parsed signal if present.\n *\n * @returns Object with `valid` flag and optional `signal` if a mid-group signal was found\n */\nfunction analyzeGap(gapText: string): { valid: boolean; signal?: CitationSignal } {\n // Must contain a semicolon\n const semiIndex = gapText.indexOf(\";\")\n if (semiIndex === -1) return { valid: false }\n\n // Text before semicolon must be only whitespace\n const before = gapText.substring(0, semiIndex).trim()\n if (before !== \"\") return { valid: false }\n\n // Text after semicolon: optional whitespace + optional signal word + optional whitespace\n const after = gapText.substring(semiIndex + 1).trim()\n\n // Empty after semicolon (just whitespace) — valid, no signal\n if (after === \"\") return { valid: true }\n\n // Try to parse a signal word\n const signalResult = parseSignal(after)\n if (signalResult) {\n // Everything after the signal must be whitespace\n const remainder = after.substring(signalResult.length).trim()\n if (remainder === \"\") return { valid: true, signal: signalResult.signal }\n }\n\n // Non-signal text after semicolon — not a valid string cite gap\n return { valid: false }\n}\n\n/**\n * Detect string citation groups from extracted citations.\n *\n * Walks adjacent citations in document order, examines the gap text between\n * them, and groups citations separated by semicolons (with optional signal\n * words). Mutates citations in place to set grouping fields.\n *\n * Must run AFTER subsequent history linking (needs `subsequentHistoryOf` to\n * exclude history citations) and AFTER parallel detection.\n *\n * @param citations - Extracted citations sorted by span.cleanStart (document order)\n * @param cleanedText - Cleaned text used for gap analysis\n */\nexport function detectStringCitations(citations: Citation[], cleanedText: string): void {\n if (citations.length < 2) return\n\n // Build groups as arrays of citation indices\n const groups: number[][] = []\n let currentGroup: number[] = []\n\n for (let i = 0; i < citations.length - 1; i++) {\n const current = citations[i]\n const next = citations[i + 1]\n\n // Skip if next citation is a subsequent history entry\n if (next.type === \"case\" && (next as FullCaseCitation).subsequentHistoryOf) {\n // Finalize current group if any\n if (currentGroup.length >= 2) {\n groups.push(currentGroup)\n }\n currentGroup = []\n continue\n }\n\n // Skip if current citation is a subsequent history entry\n if (current.type === \"case\" && (current as FullCaseCitation).subsequentHistoryOf) {\n continue\n }\n\n // Extract gap text between end of current's full extent and start of next's full extent\n const gapStart = getCitationEnd(current)\n const gapEnd = getCitationStart(next)\n\n // Guard against overlapping or adjacent spans with no gap\n if (gapEnd <= gapStart) {\n if (currentGroup.length >= 2) {\n groups.push(currentGroup)\n }\n currentGroup = []\n continue\n }\n\n const gapText = cleanedText.substring(gapStart, gapEnd)\n const analysis = analyzeGap(gapText)\n\n if (analysis.valid) {\n // Start a new group with the current citation, or continue the existing one\n if (currentGroup.length === 0) {\n currentGroup.push(i)\n }\n // Always add the next citation to the group\n currentGroup.push(i + 1)\n // Set mid-group signal on next citation if found and not already set\n if (analysis.signal && !next.signal) {\n setSignal(next, analysis.signal)\n }\n } else {\n // Group breaks — finalize current group if any\n if (currentGroup.length >= 2) {\n groups.push(currentGroup)\n }\n currentGroup = []\n }\n }\n\n // Finalize any remaining group\n if (currentGroup.length >= 2) {\n groups.push(currentGroup)\n }\n\n // Assign group metadata\n for (let g = 0; g < groups.length; g++) {\n const group = groups[g]\n if (group.length < 2) continue\n\n // Group IDs are sequential per extractCitations() call. If citations from\n // multiple documents are merged downstream, IDs may collide — callers\n // should namespace or regenerate IDs in that scenario.\n const groupId = `sc-${g}`\n for (let idx = 0; idx < group.length; idx++) {\n const citIndex = group[idx]\n const cit = citations[citIndex]\n cit.stringCitationGroupId = groupId\n cit.stringCitationIndex = idx\n cit.stringCitationGroupSize = group.length\n }\n }\n\n // Detect leading signal for first member of each group.\n // (The broader detectLeadingSignals pass below also covers these, but\n // running here first ensures group-first citations get their signal before\n // the general pass skips them as \"already set\".)\n for (const group of groups) {\n if (group.length < 2) continue\n const first = citations[group[0]]\n if (first.signal) continue // Already set (e.g., by extractCase)\n\n // Look backward from citation start for a signal word\n const searchStart = Math.max(0, getCitationStart(first) - 60)\n const precedingText = cleanedText.substring(searchStart, getCitationStart(first)).trim()\n\n // Check if preceding text ends with a signal word (uses pre-built endRegex)\n for (const { endRegex, signal } of SIGNAL_PATTERNS) {\n if (endRegex.test(precedingText)) {\n setSignal(first, signal)\n break\n }\n }\n }\n}\n\n/**\n * Detect leading introductory signals for ALL citations that don't already\n * have one. Scans backward from each citation's start position to find\n * Bluebook signal words (See, But see, Cf., Accord, See also, etc.).\n *\n * Should run AFTER detectStringCitations (which sets signals for string cite\n * group members) so we skip citations that already have signals.\n *\n * @param citations - Extracted citations sorted by span.cleanStart\n * @param cleanedText - Cleaned text used for lookback\n */\nexport function detectLeadingSignals(citations: Citation[], cleanedText: string): void {\n for (let i = 0; i < citations.length; i++) {\n const c = citations[i]\n // Skip if already has a signal from STRING CITE detection (those are scoped\n // and reliable). Do NOT skip signals set by case name extraction — those\n // come from greedy backward search and may be wrong (e.g., cite3 picks up\n // \"See\" from cite1's case name because fullSpan extends too far back).\n if (c.signal && c.stringCitationGroupId) continue\n\n // Determine the search window: from the end of the previous citation (or\n // start of text) to this citation's span.cleanStart. This scopes the\n // search to just the gap between citations.\n const prevEnd = i > 0 ? getCitationEnd(citations[i - 1]) : 0\n const citStart = c.span.cleanStart\n if (citStart <= prevEnd) continue\n\n const gapText = cleanedText.substring(prevEnd, citStart)\n\n // Find the LAST signal word in the gap text (closest to the citation).\n // Use the endRegex patterns which match at the END of a string.\n // We trim the gap text to remove trailing whitespace/punctuation after\n // the signal word (e.g., \"See Smith v. Jones, \" → check if \"See\" is\n // near the start, but we need to find it even with case name after it).\n //\n // Strategy: progressively trim from the end and check for endRegex match.\n // More efficient: use the start-anchored regex on each \"sentence\" in the gap.\n // Simplest correct approach: search for signal words as standalone tokens.\n\n // Find ALL signal matches in the gap, then pick the best one:\n // closest to the citation (highest end position), with ties broken\n // by longest match (so \"but see\" beats \"see\" when they overlap).\n const matches: Array<{ signal: CitationSignal; start: number; end: number }> = []\n\n for (const { signal } of SIGNAL_PATTERNS) {\n const escaped = signal.replace(/\\./g, \"\\\\.\").replace(/\\s+/g, \"\\\\s+\")\n const pattern = new RegExp(`(?<![a-zA-Z])(${escaped})(?![a-zA-Z])`, \"gi\")\n let match: RegExpExecArray | null\n while ((match = pattern.exec(gapText)) !== null) {\n matches.push({ signal, start: match.index, end: match.index + match[0].length })\n }\n }\n\n if (matches.length === 0) continue\n\n // Sort by: (1) end position descending (closest to citation), then\n // (2) length descending (prefer longer/more-specific signals).\n matches.sort((a, b) => {\n const endDiff = b.end - a.end\n if (endDiff !== 0) return endDiff\n return (b.end - b.start) - (a.end - a.start)\n })\n\n // The best match is the one closest to the citation. But if a shorter\n // signal (e.g., \"see\") is a substring of a longer signal (\"but see\")\n // that starts just before it, prefer the longer one.\n let best = matches[0]\n for (const m of matches) {\n // If this match fully contains the current best (or starts within\n // a few chars), it's the more specific signal.\n if (m.start <= best.start && m.end >= best.end) {\n best = m\n }\n }\n\n // Reject signals followed by lowercase prose (#304). `Contra plaintiff's\n // argument, Smith v. Jones, ...` matches `Contra` as a signal, but the\n // following `plaintiff's` is prose, not a citation-introducer context.\n // Real Bluebook signals are followed by a case-name (capital-letter-led),\n // a comma+capital, or directly by the citation core. Multi-word signal\n // forms (`see also`, `but see`, `see, e.g.`) are already captured as\n // complete units by SIGNAL_PATTERNS, so the post-signal text should\n // always begin with case-name context.\n const afterSignal = gapText.substring(best.end).replace(/^[\\s,]+/, \"\")\n if (afterSignal.length > 0) {\n const firstChar = afterSignal[0]\n // Lowercase next char → signal is part of sentence prose, not a\n // citation introducer. Reject. (Digits start citation tokens like\n // `id.`/short-form, but those are already consumed before this gap.)\n if (firstChar >= \"a\" && firstChar <= \"z\") continue\n }\n\n setSignal(c, best.signal)\n }\n}\n","/**\n * False Positive Citation Filtering\n *\n * Flags or removes citations that are likely false positives:\n * - Non-US reporter abbreviations (international, UK, European, historical English)\n * - Citations with years predating US legal reporting (before 1750)\n *\n * Runs as a post-extraction phase (Step 4.9) after string citation grouping.\n * Uses a lightweight static blocklist, enhanced with reporters-db validation\n * when loaded (for single-digit-volume paragraph/footnote marker detection).\n *\n * @module extract/filterFalsePositives\n */\n\nimport type {\n Citation,\n FederalRegisterCitation,\n FullCaseCitation,\n JournalCitation,\n ShortFormCaseCitation,\n StatutesAtLargeCitation,\n Warning,\n} from \"@/types/citation\"\nimport { getReportersSync } from \"@/data/reportersCache\"\n\n/** Year threshold: US legal reporting starts ~1790 (Dallas Reports). 1750 gives headroom. */\nconst MIN_PLAUSIBLE_YEAR = 1750\n\n/** Confidence floor for flagged citations in penalize mode. */\nconst FLAGGED_CONFIDENCE = 0.1\n\n/**\n * Static blocklist of known non-US reporter abbreviations (lowercase, trimmed).\n *\n * International tribunals/treaties:\n * I.C.J., U.N.T.S., I.L.M., I.L.R., P.C.I.J.\n * UK reporters:\n * A.C., W.L.R., All E.R., Q.B., K.B., Ch., Co. Rep.\n * European:\n * E.C.R., E.H.R.R., C.M.L.R.\n * Historical English:\n * Edw. (standalone — \"Edw. Ch.\" is a valid US reporter)\n */\nconst BLOCKED_REPORTERS: ReadonlySet<string> = new Set([\n // International\n \"i.c.j.\",\n \"u.n.t.s.\",\n \"i.l.m.\",\n \"i.l.r.\",\n \"p.c.i.j.\",\n // UK\n \"a.c.\",\n \"w.l.r.\",\n \"all e.r.\",\n \"q.b.\",\n \"k.b.\",\n \"ch.\",\n \"co. rep.\",\n // European\n \"e.c.r.\",\n \"e.h.r.r.\",\n \"c.m.l.r.\",\n // Historical English\n \"edw.\",\n])\n\n/**\n * Get the reporter string to check against the blocklist.\n * Returns undefined for citation types that don't have a reporter,\n * or for short-form types (id, supra, shortFormCase) which inherit\n * their reporter from an antecedent — filtering the antecedent is sufficient.\n */\nfunction getReporter(citation: Citation): string | undefined {\n if (citation.type === \"case\") return (citation as FullCaseCitation).reporter\n if (citation.type === \"shortFormCase\") return (citation as ShortFormCaseCitation).reporter\n if (citation.type === \"journal\") return (citation as JournalCitation).abbreviation\n return undefined\n}\n\n/**\n * Get the year from a citation, if present.\n * Returns undefined for citation types without a year field.\n */\nfunction getYear(citation: Citation): number | undefined {\n switch (citation.type) {\n case \"case\":\n return (citation as FullCaseCitation).year\n case \"journal\":\n return (citation as JournalCitation).year\n case \"federalRegister\":\n return (citation as FederalRegisterCitation).year\n case \"statutesAtLarge\":\n return (citation as StatutesAtLargeCitation).year\n default:\n return undefined\n }\n}\n\n/**\n * Words that should never appear as standalone tokens in a reporter string.\n * These appear in English prose (e.g., \"the District 2 Court dismissed\") and get\n * falsely matched by the broad state-reporter regex.\n * Note: English-only — extend if the library is used on non-English legal text.\n */\nconst REPORTER_BLOCKLIST_WORDS: ReadonlySet<string> = new Set([\n \"court\",\n \"rule\",\n \"section\",\n \"chapter\",\n \"article\",\n \"part\",\n \"title\",\n \"paragraph\",\n \"clause\",\n \"amendment\",\n \"dismissed\",\n \"granted\",\n \"denied\",\n \"filed\",\n \"argued\",\n])\n\n/**\n * Month names matched as `reporter` on date-shaped sequences like `8 April 1988`\n * (day-first European-style dates) where the state-reporter tokenizer's broad\n * `<volume> <Word> <page>` pattern captures the day, month name, and year as a\n * phantom case citation. #302\n */\nconst MONTH_NAMES: ReadonlySet<string> = new Set([\n \"january\",\n \"february\",\n \"march\",\n \"april\",\n \"may\",\n \"june\",\n \"july\",\n \"august\",\n \"september\",\n \"october\",\n \"november\",\n \"december\",\n])\n\n/** Earliest plausible year for a citation's reporting date. Anything below this\n * is almost certainly a false positive (most likely a date misparse). */\nconst MIN_PLAUSIBLE_REPORT_YEAR = 1700\n/** Latest plausible year — current year plus a small buffer for not-yet-reported\n * cases / advance sheets. */\nconst MAX_PLAUSIBLE_REPORT_YEAR = new Date().getFullYear() + 5\n/** Maximum day-of-month for the date-shape filter. */\nconst MAX_DAY_OF_MONTH = 31\n\n/**\n * Date misparse: `<day> <Month> <year>` matched as case citation (#302).\n *\n * The state-reporter regex captures `8 April 1988` as\n * `volume=8, reporter=\"April\", page=1988`. Real reporters never use month names,\n * so any cite whose reporter is a month name AND whose volume/page shape look\n * like day+year is a false positive — rejected unconditionally regardless of\n * the caller's `filterFalsePositives` opt-in.\n */\nfunction isMonthNameDateMisparse(citation: Citation): boolean {\n if (citation.type !== \"case\" && citation.type !== \"shortFormCase\") return false\n const c = citation as FullCaseCitation | ShortFormCaseCitation\n if (!c.reporter) return false\n if (!MONTH_NAMES.has(c.reporter.toLowerCase().trim())) return false\n const vol = typeof c.volume === \"number\" ? c.volume : Number.parseInt(String(c.volume), 10)\n if (Number.isNaN(vol) || vol < 1 || vol > MAX_DAY_OF_MONTH) return false\n const page = typeof c.page === \"number\" ? c.page : Number.parseInt(String(c.page), 10)\n if (Number.isNaN(page)) return false\n return page >= MIN_PLAUSIBLE_REPORT_YEAR && page <= MAX_PLAUSIBLE_REPORT_YEAR\n}\n\n/** Maximum length for a reporter string without periods.\n * Real period-less reporters (e.g., \"Cal\", \"Wis\", \"Mass\") are short.\n * Prose false positives (\"Court dismissed the complaint...\") are long.\n * Threshold of 12 accommodates the longest known period-less reporters\n * (e.g., \"Mass App Ct\" at 11 chars). Raise if new reporters exceed this. */\nconst MAX_PERIODLESS_REPORTER_LENGTH = 12\n\n/**\n * Check if a reporter string looks implausible (prose text matched as reporter).\n * Real reporters contain periods (F.2d, N.W.2d, So. 2d) or are very short (Cal, Wis).\n */\nfunction isImplausibleReporter(reporter: string): boolean {\n const words = reporter.toLowerCase().split(/\\s+/)\n if (words.some((w) => REPORTER_BLOCKLIST_WORDS.has(w))) return true\n if (!reporter.includes(\".\") && reporter.length > MAX_PERIODLESS_REPORTER_LENGTH) return true\n return false\n}\n\n/**\n * Words that appear in prose false positives for single-digit-volume citations\n * but never in legitimate reporter abbreviations (verified against reporters-db).\n * Used as fallback when reporters-db is not loaded.\n */\nconst SINGLE_DIGIT_PROSE_WORDS: ReadonlySet<string> = new Set([\n // Prepositions / conjunctions / articles\n \"the\", \"a\", \"an\", \"in\", \"on\", \"at\", \"but\", \"and\", \"for\", \"by\", \"to\",\n \"with\", \"from\", \"as\", \"if\", \"so\", \"nor\", \"yet\", \"not\", \"no\", \"then\",\n \"when\", \"where\", \"who\", \"what\", \"how\", \"that\", \"this\", \"these\", \"those\",\n // Pronouns\n \"he\", \"she\", \"it\", \"they\", \"we\", \"his\", \"her\", \"its\", \"their\", \"our\",\n // Common verbs\n \"was\", \"were\", \"is\", \"are\", \"has\", \"had\", \"been\", \"being\",\n \"did\", \"does\", \"do\", \"may\", \"shall\", \"will\", \"would\", \"could\", \"should\",\n \"held\", \"said\", \"found\", \"made\", \"took\", \"gave\", \"see\", \"also\",\n // Month names (after HTML stripping, \"¶2 In July 2016\" → \"2 In July 2016\")\n \"january\", \"february\", \"march\", \"april\", \"june\",\n \"july\", \"august\", \"september\", \"october\", \"november\", \"december\",\n])\n\n/** Maximum plausible volume number for US reporters.\n * The most prolific reporters (F. Supp. 3d, F.3d) have volumes in the\n * low-to-mid hundreds. 2000 gives generous headroom while still catching\n * zip codes (5-digit numbers like 20006) and other non-citation numbers. */\nconst MAX_PLAUSIBLE_VOLUME = 2000\n\n/** Docket number pattern: 1-2 digit prefix + hyphen + 4+ digit suffix.\n * E.g., \"24-30706\", \"23-12345\". Real hyphenated citation volumes look\n * like \"1984-1\" (4-digit year + short index). */\nconst DOCKET_VOLUME_REGEX = /^\\d{1,2}-\\d{4,}$/\n\n/**\n * Check if a case citation has an implausibly large volume number.\n * US reporter volumes rarely exceed ~1000. 5-digit volumes are typically\n * zip codes (e.g., \"DC 20006 Counsel for Appellants 20004\").\n */\nfunction isImplausibleVolume(citation: Citation): boolean {\n if (citation.type !== \"case\" && citation.type !== \"shortFormCase\") return false\n const caseCit = citation as FullCaseCitation | ShortFormCaseCitation\n // Only check purely numeric volumes; hyphenated volumes (strings) are\n // handled by isDocketNumberVolume\n if (typeof caseCit.volume !== \"number\") return false\n return caseCit.volume > MAX_PLAUSIBLE_VOLUME\n}\n\n/**\n * Check if a hyphenated volume matches a docket-number pattern.\n * Docket numbers have a short prefix and long suffix (e.g., \"24-30706\").\n * Real hyphenated citation volumes have the opposite shape (e.g., \"1984-1\").\n */\nfunction isDocketNumberVolume(citation: Citation): boolean {\n if (citation.type !== \"case\" && citation.type !== \"shortFormCase\") return false\n const caseCit = citation as FullCaseCitation | ShortFormCaseCitation\n const vol = String(caseCit.volume)\n return DOCKET_VOLUME_REGEX.test(vol)\n}\n\n/**\n * Check if a case citation with small volume (1–20) is likely a\n * paragraph/footnote marker misidentified as a citation.\n *\n * After HTML stripping, paragraph markers like \"¶2\" become bare \"2\", which the\n * broad state-reporter regex matches as volume + prose-as-reporter + next-number-as-page.\n *\n * For reporters WITHOUT periods: validates against reporters-db when loaded\n * (precise, zero false negatives), falls back to prose-word blocklist.\n *\n * For reporters WITH periods: also validates against reporters-db, since\n * non-reporter abbreviations like \"R. Civ. P.\" and \"Fed. R. Civ. P.\" contain\n * periods but are not real reporters.\n */\nfunction isSuspiciousSmallVolume(citation: Citation): boolean {\n if (citation.type !== \"case\" && citation.type !== \"shortFormCase\") return false\n const caseCit = citation as FullCaseCitation | ShortFormCaseCitation\n const vol =\n typeof caseCit.volume === \"number\"\n ? caseCit.volume\n : Number.parseInt(String(caseCit.volume), 10)\n if (Number.isNaN(vol) || vol < 1 || vol > 20) return false\n\n const reporter = caseCit.reporter\n if (!reporter) return false\n\n // Primary: check reporters-db if loaded (works for all reporters)\n const db = getReportersSync()\n if (db) {\n const matches = db.byAbbreviation.get(reporter.toLowerCase()) ?? []\n return matches.length === 0\n }\n\n // Fallback when reporters-db not loaded:\n // Period-containing reporters are more likely real (F.2d, Cal., Ohio St.)\n // but we can't validate without the db, so let them through\n if (reporter.includes(\".\")) return false\n\n // For period-less reporters, use expanded prose-word heuristic\n const words = reporter.toLowerCase().split(/\\s+/)\n return words.some((w) => SINGLE_DIGIT_PROSE_WORDS.has(w))\n}\n\n/**\n * Check if a citation is a likely false positive (short-circuit, no allocations).\n */\nfunction isFalsePositive(citation: Citation): boolean {\n const reporter = getReporter(citation)\n if (reporter && BLOCKED_REPORTERS.has(reporter.toLowerCase().trim())) return true\n if (reporter && (citation.type === \"case\" || citation.type === \"shortFormCase\") && isImplausibleReporter(reporter)) return true\n if (isImplausibleVolume(citation)) return true\n if (isDocketNumberVolume(citation)) return true\n if (isSuspiciousSmallVolume(citation)) return true\n\n const year = getYear(citation)\n if (year !== undefined && year < MIN_PLAUSIBLE_YEAR) return true\n\n return false\n}\n\n/**\n * Collect all false positive reasons for a citation.\n * Returns an empty array if the citation is clean.\n * Only called in penalize mode where we need the reason strings for warnings.\n */\nfunction collectFalsePositiveReasons(citation: Citation): string[] {\n const reasons: string[] = []\n\n const reporter = getReporter(citation)\n if (reporter) {\n const normalized = reporter.toLowerCase().trim()\n if (BLOCKED_REPORTERS.has(normalized)) {\n reasons.push(`Reporter \"${reporter}\" is a known non-US source`)\n }\n if ((citation.type === \"case\" || citation.type === \"shortFormCase\") && isImplausibleReporter(reporter)) {\n reasons.push(`Reporter \"${reporter}\" contains prose words or is implausibly long`)\n }\n }\n\n if (isImplausibleVolume(citation)) {\n const caseCit = citation as FullCaseCitation | ShortFormCaseCitation\n reasons.push(\n `Volume ${caseCit.volume} exceeds maximum plausible volume (${MAX_PLAUSIBLE_VOLUME}) — likely a zip code or other number`,\n )\n }\n\n if (isDocketNumberVolume(citation)) {\n const caseCit = citation as FullCaseCitation | ShortFormCaseCitation\n reasons.push(\n `Hyphenated volume \"${caseCit.volume}\" matches docket number pattern — likely a case number, not a citation volume`,\n )\n }\n\n if (isSuspiciousSmallVolume(citation)) {\n const caseCit = citation as FullCaseCitation | ShortFormCaseCitation\n reasons.push(\n `Small volume (${caseCit.volume}) with unrecognized reporter \"${caseCit.reporter}\" — likely a paragraph or footnote marker`,\n )\n }\n\n const year = getYear(citation)\n if (year !== undefined && year < MIN_PLAUSIBLE_YEAR) {\n reasons.push(`Year ${year} predates US legal reporting (threshold: ${MIN_PLAUSIBLE_YEAR})`)\n }\n\n return reasons\n}\n\n/**\n * Apply false positive filters to extracted citations.\n *\n * @param citations - Extracted citations (may be mutated in penalize mode)\n * @param remove - If true, remove flagged citations. If false, penalize confidence + add warning.\n * @returns Filtered array (same reference if remove=false, new array if remove=true and items removed)\n */\nexport function applyFalsePositiveFilters(citations: Citation[], remove: boolean): Citation[] {\n // Hard-reject pass: unconditionally drop unambiguous garbage like\n // `<day> <Month> <year>` date misparses (#302). These are never legitimate\n // citations under any policy, so they should not survive even when the\n // caller asked for soft-flag mode.\n const hardFiltered = citations.filter((c) => !isMonthNameDateMisparse(c))\n\n if (remove) {\n return hardFiltered.filter((c) => !isFalsePositive(c))\n }\n\n for (const citation of hardFiltered) {\n // Skip if already penalized (idempotency guard)\n if (citation.confidence === FLAGGED_CONFIDENCE && citation.warnings?.length) continue\n\n const reasons = collectFalsePositiveReasons(citation)\n if (reasons.length > 0) {\n citation.confidence = FLAGGED_CONFIDENCE\n const warnings: Warning[] = reasons.map((message) => ({\n level: \"warning\" as const,\n message,\n position: { start: citation.span.originalStart, end: citation.span.originalEnd },\n }))\n citation.warnings = [...(citation.warnings || []), ...warnings]\n }\n }\n\n return hardFiltered\n}\n","/**\n * Main Citation Extraction Pipeline\n *\n * Orchestrates the complete citation extraction flow:\n * 1. Clean text (remove HTML, normalize Unicode)\n * 2. Tokenize (apply patterns to find candidates)\n * 3. Extract (parse metadata from tokens)\n *\n * This is the primary public API for citation extraction.\n *\n * @module extract/extractCitations\n */\n\nimport { cleanText } from \"@/clean\"\nimport { UnionFind } from \"@/extract/unionFind\"\nimport { detectFootnotes } from \"@/footnotes/detectFootnotes\"\nimport { mapFootnoteZones } from \"@/footnotes/mapZones\"\nimport { tagCitationsWithFootnotes } from \"@/footnotes/tagging\"\nimport type { FootnoteMap } from \"@/footnotes/types\"\nimport {\n extractCase,\n extractConstitutional,\n extractDocket,\n extractFederalRegister,\n extractJournal,\n extractNeutral,\n extractPublicLaw,\n extractStatute,\n extractStatutesAtLarge,\n} from \"@/extract\"\nimport type { Pattern } from \"@/patterns\"\nimport {\n casePatterns,\n constitutionalPatterns,\n docketPatterns,\n journalPatterns,\n neutralPatterns,\n shortFormPatterns,\n statutePatterns,\n} from \"@/patterns\"\nimport { tokenize } from \"@/tokenize\"\nimport type { Citation, HistorySignal } from \"@/types/citation\"\nimport { resolveCitations } from \"../resolve\"\nimport type { ResolutionOptions, ResolvedCitation } from \"../resolve/types\"\nimport { detectParallelCitations } from \"./detectParallel\"\nimport { detectStringCitations, detectLeadingSignals } from \"./detectStringCites\"\nimport { extractId, extractShortFormCase, extractSupra } from \"./extractShortForms\"\nimport { applyFalsePositiveFilters } from \"./filterFalsePositives\"\n\n/**\n * Regex to parse \"volume reporter page\" from a citation token's text.\n * Used to build groupId and parallelCitations metadata for parallel citation groups.\n */\nconst CITATION_PARTS_RE = /^(\\S+)\\s+(.+)\\s+(\\d+)$/\n\n/**\n * Options for customizing citation extraction behavior.\n */\nexport interface ExtractOptions {\n /**\n * Custom text cleaners (overrides defaults).\n *\n * If provided, these cleaners replace the default pipeline:\n * [stripHtmlTags, normalizeWhitespace, normalizeUnicode, fixSmartQuotes]\n *\n * @example\n * ```typescript\n * // Use only HTML stripping, skip Unicode normalization\n * const citations = extractCitations(text, {\n * cleaners: [stripHtmlTags]\n * })\n * ```\n */\n cleaners?: Array<(text: string) => string>\n\n /**\n * Custom regex patterns (overrides defaults).\n *\n * If provided, these patterns replace the default pattern set:\n * [casePatterns, statutePatterns, journalPatterns, neutralPatterns, shortFormPatterns]\n *\n * @example\n * ```typescript\n * // Extract only case citations\n * const citations = extractCitations(text, {\n * patterns: casePatterns\n * })\n * ```\n */\n patterns?: Pattern[]\n\n /**\n * Resolve short-form citations to their full antecedents (default: false).\n *\n * If true, returns ResolvedCitation[] with resolution metadata for short-form citations\n * (Id., supra, short-form case). Full citations are unchanged.\n *\n * @example\n * ```typescript\n * const text = \"Smith v. Jones, 500 F.2d 100 (1974). Id. at 105.\"\n * const citations = extractCitations(text, { resolve: true })\n * // citations[1].resolution.resolvedTo === 0 (points to Smith v. Jones)\n * ```\n */\n resolve?: boolean\n\n /**\n * Options for citation resolution (only used if resolve: true).\n *\n * @example\n * ```typescript\n * const citations = extractCitations(text, {\n * resolve: true,\n * resolutionOptions: {\n * scopeStrategy: 'paragraph',\n * fuzzyPartyMatching: true\n * }\n * })\n * ```\n */\n resolutionOptions?: ResolutionOptions\n\n /**\n * Remove citations flagged as likely false positives (default: false).\n *\n * When false (default), flagged citations get reduced confidence (0.1) and a warning.\n * When true, flagged citations are removed from results entirely.\n *\n * False positive detection uses:\n * - A static blocklist of known non-US reporter abbreviations (international, UK, European)\n * - A year plausibility heuristic (years before 1750 predate US legal reporting)\n *\n * @example\n * ```typescript\n * // Remove false positives from results\n * const citations = extractCitations(text, { filterFalsePositives: true })\n * ```\n */\n filterFalsePositives?: boolean\n\n /** Detect footnote zones and annotate citations with inFootnote/footnoteNumber (default: false) */\n detectFootnotes?: boolean\n}\n\n/**\n * Extracts legal citations from text using the full parsing pipeline.\n *\n * Pipeline flow:\n * 1. **Clean:** Remove HTML tags, normalize Unicode, fix smart quotes\n * 2. **Tokenize:** Apply regex patterns to find citation candidates\n * 3. **Extract:** Parse metadata (volume, reporter, page, etc.)\n * 4. **Translate:** Map positions from cleaned text back to original text\n *\n * This function is synchronous because all stages (cleaning, tokenization,\n * extraction) are synchronous. For async operations (e.g., future reporters-db\n * lookups), use extractCitationsAsync().\n *\n * Position tracking:\n * - TransformationMap is built during cleaning\n * - Tokens contain positions in cleaned text (cleanStart/cleanEnd)\n * - Extraction translates cleaned positions → original positions\n * - Final citations have originalStart/originalEnd pointing to input text\n *\n * Warnings from cleaning layer are attached to all extracted citations.\n *\n * @param text - Raw text to extract citations from (may contain HTML, Unicode)\n * @param options - Optional customization (cleaners, patterns)\n * @returns Array of citations with parsed metadata and accurate positions\n *\n * @example\n * ```typescript\n * const text = \"See Smith v. Doe, 500 F.2d 123 (9th Cir. 2020)\"\n * const citations = extractCitations(text)\n * // citations[0] = {\n * // type: \"case\",\n * // volume: 500,\n * // reporter: \"F.2d\",\n * // page: 123,\n * // court: \"9th Cir.\",\n * // year: 2020,\n * // span: { originalStart: 18, originalEnd: 30, ... }\n * // }\n * ```\n *\n * @example\n * ```typescript\n * // Extract from HTML\n * const html = \"<p>In <b>Smith</b>, 500 F.2d 123, the court held...</p>\"\n * const citations = extractCitations(html)\n * // HTML is stripped, positions point to original HTML\n * ```\n *\n * @example\n * ```typescript\n * // Extract multiple citation types\n * const text = \"See 42 U.S.C. § 1983; Smith, 500 F.2d 123; 123 Harv. L. Rev. 456\"\n * const citations = extractCitations(text)\n * // citations[0].type === \"statute\"\n * // citations[1].type === \"case\"\n * // citations[2].type === \"journal\"\n * ```\n */\nexport function extractCitations(\n text: string,\n options: ExtractOptions & { resolve: true },\n): ResolvedCitation[]\nexport function extractCitations(text: string, options?: ExtractOptions): Citation[]\nexport function extractCitations(\n text: string,\n options?: ExtractOptions,\n): Citation[] | ResolvedCitation[] {\n const startTime = performance.now()\n\n // Step 1: Clean text\n const { cleaned, transformationMap, warnings } = cleanText(text, options?.cleaners)\n\n // Step 1.5: Detect footnote zones (opt-in)\n let cleanFootnoteMap: FootnoteMap | undefined\n if (options?.detectFootnotes) {\n const rawZones = detectFootnotes(text)\n if (rawZones.length > 0) {\n cleanFootnoteMap = mapFootnoteZones(rawZones, transformationMap)\n }\n }\n\n // Step 2: Tokenize (synchronous)\n // Note: Pattern order matters for deduplication - more specific patterns first\n const allPatterns = options?.patterns || [\n ...neutralPatterns, // Most specific (year-based format)\n ...docketPatterns, // Docket-number citations (anchored by \"No. \")\n ...shortFormPatterns, // Short-form (requires \" at \" keyword)\n ...casePatterns, // Case citations (reporter-specific)\n ...constitutionalPatterns, // Constitutional citations (more specific than statutes)\n ...statutePatterns, // Statutes (code-specific)\n ...journalPatterns, // Least specific (broad pattern)\n ]\n const tokens = tokenize(cleaned, allPatterns)\n\n // Step 3: Deduplicate overlapping tokens via priority-aware subsumption.\n //\n // Multiple patterns may match the same or overlapping text:\n // (a) Exact-span duplicates — e.g., `100 F.3d 456` matches both\n // `federal-reporter` and `state-reporter`. Keep the higher-priority\n // one (earlier in the pattern list = more specific).\n // (b) Subsumed matches — a token whose span is a strict subset of\n // another token's span, and the containing token comes from a\n // more-or-equally-specific pattern. Canonical failure: `state-reporter`\n // or `law-review` treating `F.3d at` / `U.S. at` as a reporter/journal\n // name inside a `shortFormCase` cite (see #207, #209). Drop the\n // subsumed token only when the container is at least as specific —\n // otherwise we'd swallow legitimate shorter matches (e.g., a\n // `state-constitution` token that sits inside a broader `named-code`\n // match for \"Cal. Const. art. I, § 7.\").\n //\n // Priority = first occurrence index in `allPatterns`. Duplicate patternIds\n // (e.g., \"supra\") share the earliest index, which is fine — they form a\n // single priority bucket.\n const priorityByPatternId = new Map<string, number>()\n for (let i = 0; i < allPatterns.length; i++) {\n const id = allPatterns[i].id\n if (!priorityByPatternId.has(id)) priorityByPatternId.set(id, i)\n }\n const priorityOf = (t: (typeof tokens)[number]): number =>\n priorityByPatternId.get(t.patternId) ?? Number.POSITIVE_INFINITY\n\n // Sort by (cleanStart asc, cleanEnd desc, priority asc) so containers come\n // before their contained tokens at each start position, and within any\n // (start, end) bucket the higher-priority token comes first.\n const sortedTokens = [...tokens].sort(\n (a, b) =>\n a.span.cleanStart - b.span.cleanStart ||\n b.span.cleanEnd - a.span.cleanEnd ||\n priorityOf(a) - priorityOf(b),\n )\n const deduplicatedTokens: typeof tokens = []\n for (const token of sortedTokens) {\n let subsumed = false\n for (const kept of deduplicatedTokens) {\n const contains =\n kept.span.cleanStart <= token.span.cleanStart && kept.span.cleanEnd >= token.span.cleanEnd\n if (!contains) continue\n if (priorityOf(kept) > priorityOf(token)) continue // kept is less specific, don't let it swallow\n // `kept` is at least as specific AND contains this token. Drop `token`\n // if this is a strict containment or an equal-span lower-priority duplicate.\n if (\n kept.span.cleanStart < token.span.cleanStart ||\n kept.span.cleanEnd > token.span.cleanEnd ||\n priorityOf(kept) < priorityOf(token)\n ) {\n subsumed = true\n break\n }\n }\n if (!subsumed) deduplicatedTokens.push(token)\n }\n\n // Step 3.5: Detect parallel citation groups\n // Map of primary token index -> array of secondary token indices\n const parallelGroups = detectParallelCitations(deduplicatedTokens, cleaned)\n\n // Build reverse-lookup: secondary index -> primary index (O(1) instead of O(N×M))\n const secondaryToGroup = new Map<number, number>()\n for (const [primary, secondaries] of parallelGroups.entries()) {\n for (const s of secondaries) secondaryToGroup.set(s, primary)\n }\n\n // Span list for all case-shape tokens. Passed to extractCase so the per-cite\n // pincite/year/caseName logic can see what's adjacent and avoid scanning\n // INTO neighbor citations (parallel-cite chains share a trailing year paren\n // and the case-name backward walk for a parallel cite must stop at the\n // prior cite's end).\n const caseTokenSpans = deduplicatedTokens\n .filter((t) => t.type === \"case\")\n .map((t) => ({ cleanStart: t.span.cleanStart, cleanEnd: t.span.cleanEnd }))\n\n // Step 4: Extract citations from deduplicated tokens\n const citations: Citation[] = []\n for (let i = 0; i < deduplicatedTokens.length; i++) {\n const token = deduplicatedTokens[i]\n let citation: Citation\n\n switch (token.type) {\n case \"case\":\n // Check pattern ID to distinguish short-form from full citations\n if (token.patternId === \"id\" || token.patternId === \"ibid\") {\n citation = extractId(token, transformationMap, cleaned)\n } else if (token.patternId === \"supra\") {\n citation = extractSupra(token, transformationMap, cleaned)\n } else if (token.patternId === \"shortFormCase\") {\n citation = extractShortFormCase(token, transformationMap, cleaned)\n } else {\n citation = extractCase(\n token,\n transformationMap,\n cleaned,\n text,\n caseTokenSpans,\n )\n }\n break\n case \"docket\": {\n // Docket extractor returns undefined when no case-name anchor is\n // found — the bare \"No. <N> (<court> <year>)\" shape is too generic\n // to surface without context.\n const result = extractDocket(token, transformationMap, cleaned, text)\n if (!result) continue\n citation = result\n break\n }\n case \"statute\":\n citation = extractStatute(token, transformationMap)\n break\n case \"journal\":\n citation = extractJournal(token, transformationMap, cleaned)\n break\n case \"neutral\":\n citation = extractNeutral(token, transformationMap, cleaned)\n break\n case \"publicLaw\":\n citation = extractPublicLaw(token, transformationMap)\n break\n case \"federalRegister\":\n citation = extractFederalRegister(token, transformationMap)\n break\n case \"statutesAtLarge\":\n citation = extractStatutesAtLarge(token, transformationMap)\n break\n case \"constitutional\":\n citation = extractConstitutional(token, transformationMap)\n break\n default:\n // Unknown type - skip\n continue\n }\n\n // Attach cleaning warnings to citation if any\n if (warnings.length > 0) {\n citation.warnings = [...(citation.warnings || []), ...warnings]\n }\n\n // Update processing time\n citation.processTimeMs = performance.now() - startTime\n\n // Populate parallel citation metadata (Phase 8)\n if (citation.type === \"case\") {\n const isPrimary = parallelGroups.has(i)\n const isSecondary = secondaryToGroup.has(i)\n\n if (isPrimary || isSecondary) {\n const primaryIndex = isSecondary ? (secondaryToGroup.get(i) ?? i) : i\n const primaryToken = deduplicatedTokens[primaryIndex]\n const match = CITATION_PARTS_RE.exec(primaryToken.text)\n if (match) {\n const [, volume, reporter, page] = match\n citation.groupId = `${volume}-${reporter.replace(/\\s+/g, \".\")}-${page}`\n\n if (isPrimary) {\n const secondaryIndices = parallelGroups.get(i) ?? []\n citation.parallelCitations = secondaryIndices.map((secIdx) => {\n const secToken = deduplicatedTokens[secIdx]\n const secMatch = CITATION_PARTS_RE.exec(secToken.text)\n if (secMatch) {\n const [, secVol, secRep, secPage] = secMatch\n return {\n volume: /^\\d+$/.test(secVol) ? Number.parseInt(secVol, 10) : secVol,\n reporter: secRep,\n page: Number.parseInt(secPage, 10),\n }\n }\n return { volume: 0, reporter: \"\", page: 0 }\n })\n }\n }\n }\n }\n\n citations.push(citation)\n }\n\n // Step 4.5: Link subsequent history citations using Union-Find.\n // Three-phase approach: match signals → union chains → aggregate entries.\n // Invariant: citations are in text order (guaranteed by token-order processing above).\n linkSubsequentHistory(citations)\n\n // Step 4.55: Inherit case name from chain root for subsequent-history children (#224).\n // Per Bluebook 10.7, all citations in a history chain reference one case.\n // Without this, extractCaseName scans back from the child, captures the\n // parent cite + connector (\"Smith v. Doe, 100 F.3d 200 (...), aff'd\"),\n // and produces a nonsense caseName.\n inheritSubsequentHistoryCaseName(citations)\n\n // Step 4.6: Propagate caseName from the primary onto each parallel-cite\n // secondary (#282). Detection in step 4 sets the shared `groupId` and\n // populates `parallelCitations` on the primary; this pass fills in the\n // shared caption fields on secondaries that have no caseName of their own.\n // Runs AFTER 4.55 so a primary that inherited from a history chain root\n // still propagates that inherited caption to its parallels.\n inheritParallelCaseName(citations)\n\n // Step 4.65: Attach year-of-edition / publisher from a trailing parenthetical\n // to statute citations (#285). E.g. `HRS § 91-14(a) (1985)` → year=1985;\n // `28 U.S.C. § 1331 (West 2018)` → publisher=\"West\", year=2018.\n attachStatuteYearParen(citations, cleaned)\n\n // Step 4.75: Detect string citation groups (semicolon-separated)\n detectStringCitations(citations, cleaned)\n\n // Step 4.8: Detect leading introductory signals for all citations.\n // Runs after string cite detection (which sets mid-group signals) so we\n // only scan backward for citations that still lack a signal.\n detectLeadingSignals(citations, cleaned)\n\n // Step 4.9: Apply false positive filters (blocklist + year heuristic)\n const filtered = applyFalsePositiveFilters(citations, options?.filterFalsePositives ?? false)\n\n // Step 4.95: Tag citations with footnote metadata\n if (cleanFootnoteMap) {\n tagCitationsWithFootnotes(filtered, cleanFootnoteMap)\n }\n\n // Step 5: Resolve short-form citations if requested\n if (options?.resolve) {\n const resolutionOpts = cleanFootnoteMap\n ? { ...options.resolutionOptions, footnoteMap: cleanFootnoteMap }\n : options.resolutionOptions\n return resolveCitations(filtered, text, resolutionOpts)\n }\n\n return filtered\n}\n\n/**\n * Asynchronous version of extractCitations().\n *\n * Currently wraps the synchronous extractCitations() function. This API\n * exists for future extensibility when async operations are added:\n * - Async reporters-db lookups (Phase 3)\n * - Async resolution/annotation services\n * - Web Workers for parallel processing\n *\n * For now, this function immediately resolves with the same results as\n * the synchronous version.\n *\n * @param text - Raw text to extract citations from\n * @param options - Optional customization (cleaners, patterns, resolve)\n * @returns Promise resolving to array of citations (or ResolvedCitation[] if resolve: true)\n *\n * @example\n * ```typescript\n * const citations = await extractCitationsAsync(text, { resolve: true })\n * // Returns ResolvedCitation[] with resolution metadata\n * ```\n */\nexport async function extractCitationsAsync(\n text: string,\n options: ExtractOptions & { resolve: true },\n): Promise<ResolvedCitation[]>\nexport async function extractCitationsAsync(\n text: string,\n options?: ExtractOptions,\n): Promise<Citation[]>\nexport async function extractCitationsAsync(\n text: string,\n options?: ExtractOptions,\n): Promise<Citation[] | ResolvedCitation[]> {\n // Async wrapper for future extensibility (e.g., async reporters-db lookup)\n // For MVP, wraps synchronous extractCitations\n return extractCitations(text, options)\n}\n\n/**\n * Link subsequent history citations using a three-phase Union-Find approach.\n * Replaces the old mutation-during-iteration pattern with cleanly separated phases.\n */\nfunction linkSubsequentHistory(citations: Citation[]): void {\n // Phase 1: Signal matching — collect (parent, child) pairs without mutating citations.\n // Also record each child's signal text for back-pointer assignment in Phase 3.\n const pairs: Array<{ parentIdx: number; childIdx: number; signal: HistorySignal }> = []\n\n for (let i = 0; i < citations.length; i++) {\n const parent = citations[i]\n if (parent.type !== \"case\" || !parent.subsequentHistoryEntries) continue\n\n const entries = parent.subsequentHistoryEntries\n let entryIdx = 0\n\n for (let j = i + 1; j < citations.length && entryIdx < entries.length; j++) {\n const child = citations[j]\n if (child.type !== \"case\") continue\n\n const signalEnd = entries[entryIdx].signalSpan.cleanEnd\n if (child.span.cleanStart >= signalEnd) {\n pairs.push({ parentIdx: i, childIdx: j, signal: entries[entryIdx].signal })\n entryIdx++\n }\n }\n }\n\n if (pairs.length === 0) return\n\n // Phase 2: Union — build connected components from parent-child pairs.\n const uf = new UnionFind(citations.length)\n for (const pair of pairs) {\n uf.union(pair.parentIdx, pair.childIdx)\n }\n\n // Build lookup: childIdx → signal (for back-pointer assignment)\n const childSignals = new Map<number, HistorySignal>()\n for (const pair of pairs) {\n childSignals.set(pair.childIdx, pair.signal)\n }\n\n // Phase 3: Aggregation — set back-pointers and collect entries onto chain roots.\n for (const [root, members] of uf.components()) {\n if (members.length === 1) continue\n\n const rootCitation = citations[root]\n if (rootCitation.type !== \"case\") continue\n\n const allEntries = [...(rootCitation.subsequentHistoryEntries ?? [])]\n\n for (const memberIdx of members) {\n if (memberIdx === root) continue\n\n const member = citations[memberIdx]\n if (member.type !== \"case\") continue\n\n // Set back-pointer to chain root.\n // Signal is guaranteed to exist: every non-root member was recorded as a\n // child in Phase 1, which always stores the signal in childSignals.\n const signal = childSignals.get(memberIdx)\n if (!signal) continue\n member.subsequentHistoryOf = { index: root, signal }\n\n // Aggregate entries from non-root members onto the root\n if (member.subsequentHistoryEntries) {\n for (const entry of member.subsequentHistoryEntries) {\n allEntries.push({ ...entry, order: allEntries.length })\n }\n member.subsequentHistoryEntries = undefined\n }\n }\n\n rootCitation.subsequentHistoryEntries = allEntries\n }\n}\n\n/**\n * Inherit case name fields from the chain root for subsequent-history children.\n *\n * In a chain like `<full cite A>, modified on other grounds, <full cite B>`,\n * citation B has no preceding case-name string in the document — it implicitly\n * shares A's case name (Bluebook Rule 10.7). The default case-name scanner\n * walks left from B and captures all of A's text plus the history connector.\n *\n * After `linkSubsequentHistory` has set `subsequentHistoryOf` back-pointers,\n * this pass overwrites the child's case-name fields with the chain root's,\n * clears component spans (the child has no anchor), and trims `fullSpan`\n * back to the child's own citation core.\n *\n * Closes #224.\n */\nfunction inheritSubsequentHistoryCaseName(citations: Citation[]): void {\n for (const child of citations) {\n if (child.type !== \"case\") continue\n if (!child.subsequentHistoryOf) continue\n const parent = citations[child.subsequentHistoryOf.index]\n if (!parent || parent.type !== \"case\") continue\n if (!parent.caseName) continue\n\n child.caseName = parent.caseName\n child.plaintiff = parent.plaintiff\n child.defendant = parent.defendant\n child.plaintiffNormalized = parent.plaintiffNormalized\n child.defendantNormalized = parent.defendantNormalized\n child.proceduralPrefix = parent.proceduralPrefix\n\n if (child.spans) {\n child.spans.caseName = undefined\n child.spans.plaintiff = undefined\n child.spans.defendant = undefined\n }\n\n // Trim fullSpan to the child's own citation core. extractCaseName had\n // anchored fullSpan at the parent's case name; that's not the child's\n // text. Keep the original cleanEnd/originalEnd (parenthetical end).\n if (child.fullSpan) {\n child.fullSpan = {\n cleanStart: child.span.cleanStart,\n cleanEnd: child.fullSpan.cleanEnd,\n originalStart: child.span.originalStart,\n originalEnd: child.fullSpan.originalEnd,\n }\n }\n }\n}\n\n/**\n * Propagate caseName fields from the primary onto each parallel-cite secondary.\n *\n * A parallel-citation group (`Roe v. Wade, 410 U.S. 113, 93 S. Ct. 705, 35 L. Ed. 2d 147\n * (1973)`) shares one caption across multiple reporter citations. Step 4 in the main\n * pipeline assigns the shared `groupId` to every cite in the group and populates\n * `parallelCitations` on the primary, but only the primary's per-cite case-name\n * extraction captures `Roe v. Wade` — secondaries land with `caseName === undefined`.\n *\n * The primary in a group is the cite that has a non-undefined `caseName` (it appears\n * first in document order and so is the only one the backward case-name scan can anchor\n * on without breaking the parallel-cite disambiguation from #281). This pass joins on\n * `groupId`, takes the first cite per group that has a `caseName`, and copies the\n * shared caption fields onto every other cite in the same group.\n *\n * Closes #282.\n */\n/**\n * Statute year-of-edition parenthetical regex (#285 + #349).\n *\n * Matches a parenthetical that follows a statute citation core. Anchored at\n * the start of the lookahead text (we substring from `span.cleanEnd`) and\n * allows whitespace, an optional comma + whitespace, and an optional\n * `, at <pincite>` intervening text. The parenthetical body is one of:\n *\n * - `(YYYY)` — bare year\n * - `(Publisher YYYY)` — publisher-first (`(West 2018)`,\n * `(Lexis Nexis 2019)`)\n * - `(Label. YYYY)` — edition-label-first (`(Repl. 1996)`,\n * `(Supp. 1985)`, `(Cum. Supp. 1985)`)\n * - `(YYYY Label.)` — year-first edition label (`(1969 Supp.)`,\n * `(1985 Cum. Supp.)`)\n *\n * Subsection parens like `(a)` and `(1)` don't match — the body must contain\n * a 4-digit year. The pre/post token (group 1 / group 3) admits a trailing\n * `.` and an optional second capitalized word so `Cum. Supp.` and\n * `Lexis Nexis` both flow through the same regex.\n */\nconst STATUTE_YEAR_PAREN_REGEX =\n /^\\s*(?:,\\s*(?:at\\s+)?\\d+(?:-\\d+)?\\s*)?\\(\\s*([A-Z][A-Za-z]+\\.?(?:\\s+[A-Z][A-Za-z]+\\.?)?)?\\s*(\\d{4})\\s*([A-Z][A-Za-z]+\\.?(?:\\s+[A-Z][A-Za-z]+\\.?)?)?\\s*\\)/\n\n/**\n * Edition-label set — captured tokens that should populate `editionLabel`\n * rather than `publisher`. `Repl.` = replacement volume, `Supp.` = supplement,\n * `Cum. Supp.` = cumulative supplement. #349\n */\nconst EDITION_LABEL_REGEX = /^(?:Repl|Supp|Cum\\.?\\s*Supp)\\.?$/i\n\n/**\n * Attach `year` (and optional `publisher` / `editionLabel`) to statute\n * citations whose citation core is immediately followed by a year-of-edition\n * parenthetical.\n *\n * `HRS § 91-14(a) (1985)`, `42 U.S.C. § 1983 (1976)`,\n * `28 U.S.C. § 1331 (West 2018)`, and `Ark. Code Ann. § 11-9-514(a)(1)\n * (Repl. 1996)` are all common code-edition forms. The tokenizer captures\n * only the citation core; this post-pass scans forward from `span.cleanEnd`\n * for the year-paren and routes the non-year token to `publisher` or\n * `editionLabel` depending on whether it's a replacement/supplement marker.\n *\n * Closes #285. Extended for `Repl.` / `Supp.` / `Cum. Supp.` in #349.\n */\nfunction attachStatuteYearParen(citations: Citation[], cleaned: string): void {\n for (const cite of citations) {\n if (cite.type !== \"statute\") continue\n if (cite.year !== undefined) continue\n const after = cleaned.slice(cite.span.cleanEnd)\n const match = STATUTE_YEAR_PAREN_REGEX.exec(after)\n if (!match) continue\n const [, prefixToken, yearStr, suffixToken] = match\n cite.year = Number.parseInt(yearStr, 10)\n // Route the non-year token: edition label vs publisher. Either slot may\n // carry it (publisher conventionally precedes the year; edition labels\n // appear on either side).\n const token = prefixToken ?? suffixToken\n if (!token) continue\n if (EDITION_LABEL_REGEX.test(token)) {\n // Normalize spacing inside `Cum. Supp.` to a single space.\n cite.editionLabel = token.replace(/\\s+/g, \" \").trim()\n } else {\n cite.publisher = token\n }\n }\n}\n\nfunction inheritParallelCaseName(citations: Citation[]): void {\n const primaryByGroup = new Map<string, number>()\n for (let i = 0; i < citations.length; i++) {\n const c = citations[i]\n if (c.type !== \"case\") continue\n if (!c.groupId || !c.caseName) continue\n if (!primaryByGroup.has(c.groupId)) primaryByGroup.set(c.groupId, i)\n }\n\n for (const secondary of citations) {\n if (secondary.type !== \"case\") continue\n if (!secondary.groupId) continue\n if (secondary.caseName) continue\n const primaryIdx = primaryByGroup.get(secondary.groupId)\n if (primaryIdx === undefined) continue\n const primary = citations[primaryIdx]\n if (!primary || primary.type !== \"case\") continue\n\n secondary.caseName = primary.caseName\n secondary.plaintiff = primary.plaintiff\n secondary.defendant = primary.defendant\n secondary.plaintiffNormalized = primary.plaintiffNormalized\n secondary.defendantNormalized = primary.defendantNormalized\n secondary.proceduralPrefix = primary.proceduralPrefix\n }\n}\n"],"mappings":"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CE,oJACF,YACE,2RACF,KAAM,OACP,CACF,CCtDY,GAA4B,CACvC,CACE,GAAI,0BAMJ,MAAO,wDACP,YAAa,0EACb,KAAM,SACP,CACF,CCdY,GAA6B,CACxC,CACE,GAAI,aACJ,MAAO,oFACP,YACE,uSACF,KAAM,UACP,CACF,CCRY,GAA6B,CACxC,CAIE,GAAI,qCACJ,MAAO,uCACP,YACE,wFACF,KAAM,UACP,CACD,CAKE,GAAI,kCACJ,MAAO,sCACP,YACE,sFACF,KAAM,UACP,CACD,CAWE,GAAI,uBACJ,MACE,sHACF,YACE,qKACF,KAAM,UACP,CACD,CACE,GAAI,UACJ,MAAO,4BACP,YAAa,6CACb,KAAM,UACP,CACD,CAME,GAAI,QACJ,MAAO,oDACP,YACE,mJACF,KAAM,UACP,CACD,CACE,GAAI,aACJ,MAAO,2CACP,YAAa,yEACb,KAAM,YACP,CACD,CACE,GAAI,mBACJ,MAAO,8CACP,YAAa,0DACb,KAAM,kBACP,CACD,CACE,GAAI,oBACJ,MAAO,uCACP,YAAa,sDACb,KAAM,kBACP,CACD,CACE,GAAI,qBACJ,MAAO,kEACP,YAAa,2EACb,KAAM,UACP,CACF,CCgCY,GAA+B,CAC1C,CACE,GAAI,KACJ,MArGF,wPAsGE,YAAa,8CACb,KAAM,OACP,CACD,CACE,GAAI,OACJ,MAtGF,sOAuGE,YAAa,oDACb,KAAM,OACP,CACD,CACE,GAAI,QACJ,MAzDF,iKA0DE,YACE,+FACF,KAAM,OACP,CACD,CACE,GAAI,QACJ,MA1FF,2SA2FE,YAAa,mEACb,KAAM,OACP,CACD,CACE,GAAI,QACJ,MApFF,4ZAqFE,YAAa,4DACb,KAAM,OACP,CACD,CACE,GAAI,gBACJ,MA/CF,sTAgDE,YAAa,sDACb,KAAM,OACP,CACF,CCtJY,GAA6B,CACxC,CACE,GAAI,MACJ,MACE,4FACF,YACE,qGACF,KAAM,UACP,CACD,CACE,GAAI,MACJ,MACE,qHACF,YACE,6HACF,KAAM,UACP,CACD,CACE,GAAI,QACJ,MAAO,0EACP,YACE,iIACF,KAAM,UACP,CACD,CACE,GAAI,aAaJ,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"}
1
+ {"version":3,"file":"index.cjs","names":["getReportersSync","findAbbreviatedCode","findNamedCode","buildAbbreviatedCodeRegex","getReportersSync"],"sources":["../src/types/guards.ts","../src/types/span.ts","../src/clean/cleaners.ts","../src/clean/segmentMap.ts","../src/clean/cleanText.ts","../src/extract/unionFind.ts","../src/footnotes/htmlDetector.ts","../src/footnotes/textDetector.ts","../src/footnotes/detectFootnotes.ts","../src/footnotes/mapZones.ts","../src/footnotes/tagging.ts","../src/extract/dates.ts","../src/extract/courtInference.ts","../src/extract/pincite.ts","../src/extract/courtNormalization.ts","../src/extract/extractCase.ts","../src/patterns/constitutionalPatterns.ts","../src/extract/extractConstitutional.ts","../src/extract/extractDocket.ts","../src/extract/extractFederalRegister.ts","../src/extract/extractJournal.ts","../src/extract/extractNeutral.ts","../src/extract/extractPublicLaw.ts","../src/extract/extractShortForms.ts","../src/extract/statutes/parseBody.ts","../src/extract/statutes/extractAbbreviated.ts","../src/extract/statutes/extractAlaCode1940.ts","../src/data/caBareCodes.ts","../src/extract/statutes/extractCaBareCode.ts","../src/extract/statutes/extractChapterAct.ts","../src/extract/statutes/extractColoradoProse.ts","../src/extract/statutes/extractFederal.ts","../src/extract/statutes/extractFloridaStatute.ts","../src/extract/statutes/extractGaPre1983.ts","../src/extract/statutes/extractIcYearEdition.ts","../src/extract/statutes/extractIdahoPostfix.ts","../src/extract/statutes/extractIllRevStat.ts","../src/extract/statutes/extractIrc.ts","../src/extract/statutes/extractKsaYearEdition.ts","../src/extract/statutes/extractMcaPostfix.ts","../src/extract/statutes/extractMdArticleLetter.ts","../src/extract/statutes/extractMinnStYearEdition.ts","../src/extract/statutes/extractNamedCode.ts","../src/extract/statutes/extractNmBareSection.ts","../src/extract/statutes/extractNyBareLaw.ts","../src/extract/statutes/extractOhChapter.ts","../src/extract/statutes/extractOrsChapter.ts","../src/extract/statutes/extractProse.ts","../src/extract/statutes/extractRlh.ts","../src/extract/statutes/extractRrs1943.ts","../src/extract/statutes/extractRigl1956.ts","../src/extract/statutes/extractRsaChapter.ts","../src/extract/statutes/extractRcwChapterPostfix.ts","../src/extract/statutes/extractTcaPostfix.ts","../src/extract/statutes/extractVaBareCode.ts","../src/extract/statutes/extractWiStatsPostfix.ts","../src/extract/statutes/extractWvCode1931.ts","../src/extract/extractStatute.ts","../src/extract/extractStatutesAtLarge.ts","../src/patterns/casePatterns.ts","../src/patterns/docketPatterns.ts","../src/patterns/journalPatterns.ts","../src/patterns/neutralPatterns.ts","../src/patterns/shortForm.ts","../src/patterns/statutePatterns.ts","../src/tokenize/tokenizer.ts","../src/resolve/bkTree.ts","../src/resolve/levenshtein.ts","../src/resolve/scopeBoundary.ts","../src/resolve/DocumentResolver.ts","../src/resolve/index.ts","../src/extract/detectParallel.ts","../src/extract/detectStringCites.ts","../src/extract/filterFalsePositives.ts","../src/extract/extractCitations.ts"],"sourcesContent":["import type {\n Citation,\n CitationOfType,\n CitationType,\n FullCaseCitation,\n FullCitation,\n ShortFormCitation,\n} from \"./citation\"\n\n/**\n * Type guard: narrows Citation to a full citation (case, statute, journal, neutral, publicLaw, federalRegister, statutesAtLarge, constitutional).\n */\nexport function isFullCitation(citation: Citation): citation is FullCitation {\n return (\n citation.type === \"case\" ||\n citation.type === \"docket\" ||\n citation.type === \"statute\" ||\n citation.type === \"journal\" ||\n citation.type === \"neutral\" ||\n citation.type === \"publicLaw\" ||\n citation.type === \"federalRegister\" ||\n citation.type === \"statutesAtLarge\" ||\n citation.type === \"constitutional\"\n )\n}\n\n/**\n * Type guard: narrows Citation to a short-form citation (id, supra, shortFormCase).\n */\nexport function isShortFormCitation(citation: Citation): citation is ShortFormCitation {\n return citation.type === \"id\" || citation.type === \"supra\" || citation.type === \"shortFormCase\"\n}\n\n/**\n * Type guard: narrows Citation to a full case citation.\n */\nexport function isCaseCitation(citation: Citation): citation is FullCaseCitation {\n return citation.type === \"case\"\n}\n\n/**\n * Generic type guard that narrows a Citation to a specific type.\n * Useful when the target type is dynamic or generic.\n */\nexport function isCitationType<T extends CitationType>(\n citation: Citation,\n type: T,\n): citation is CitationOfType<T> {\n return citation.type === type\n}\n\n/**\n * Exhaustiveness helper for switch statements on discriminated unions.\n *\n * Place in the `default` branch to get a compile-time error if a new\n * variant is added but not handled.\n *\n * @example\n * ```typescript\n * switch (citation.type) {\n * case 'case': ...\n * case 'statute': ...\n * // If you forget a variant, TypeScript errors here:\n * default: assertUnreachable(citation.type)\n * }\n * ```\n */\nexport function assertUnreachable(x: never): never {\n throw new Error(`Unexpected value: ${x}`)\n}\n","/**\n * Represents a text span with positions tracked through transformations.\n *\n * During text cleaning (HTML removal, whitespace normalization), positions\n * shift. Span tracks BOTH cleaned positions (for parsing) and original\n * positions (for user-facing results).\n *\n * @example\n * const original = \"Smith v. Doe, 500 F.2d 123 (2020)\"\n * // After cleaning, positions may shift\n * const span: Span = {\n * cleanStart: 14, // Position in cleaned text\n * cleanEnd: 27,\n * originalStart: 14, // Position in original text\n * originalEnd: 27\n * }\n */\nexport interface Span {\n /** Start position in cleaned/tokenized text (used during parsing) */\n cleanStart: number\n\n /** End position in cleaned/tokenized text (used during parsing) */\n cleanEnd: number\n\n /** Start position in original input text (returned to user) */\n originalStart: number\n\n /** End position in original input text (returned to user) */\n originalEnd: number\n}\n\n/**\n * Maps positions between cleaned and original text.\n *\n * Built during text transformation to track how character positions shift\n * when HTML entities are removed, whitespace is normalized, etc.\n */\nexport interface TransformationMap {\n /** Maps cleaned text position to original text position */\n cleanToOriginal: Map<number, number>\n\n /** Maps original text position to cleaned text position */\n originalToClean: Map<number, number>\n\n /** Compressed segment-based clean→original mapping for O(log k) lookup */\n cleanToOriginalSegments?: import(\"../clean/segmentMap\").SegmentMap\n}\n\n/**\n * Build a Span for a regex capture group using match.indices (ES2022 `d` flag).\n *\n * Requires the regex to have the `d` flag so match.indices is populated.\n * The indices are relative to the token text — tokenCleanStart translates\n * them to document-level clean-text positions, then resolveOriginalSpan\n * maps to original positions via TransformationMap.\n *\n * @param tokenCleanStart - The token's cleanStart position in the document\n * @param indices - match.indices[n] for the capture group: [start, end]\n * @param map - TransformationMap for clean→original resolution\n * @returns Span with both clean and original coordinates\n */\nexport function spanFromGroupIndex(\n tokenCleanStart: number,\n indices: [number, number],\n map: TransformationMap,\n): Span {\n const cleanStart = tokenCleanStart + indices[0]\n const cleanEnd = tokenCleanStart + indices[1]\n const { originalStart, originalEnd } = resolveOriginalSpan(\n { cleanStart, cleanEnd },\n map,\n )\n return { cleanStart, cleanEnd, originalStart, originalEnd }\n}\n\n/** Translate clean-text span positions back to original-text positions. */\nexport function resolveOriginalSpan(\n span: { cleanStart: number; cleanEnd: number },\n map: TransformationMap,\n): { originalStart: number; originalEnd: number } {\n // Prefer segment map (binary search) when available\n if (map.cleanToOriginalSegments) {\n return {\n originalStart: map.cleanToOriginalSegments.lookup(span.cleanStart),\n originalEnd: map.cleanToOriginalSegments.lookup(span.cleanEnd),\n }\n }\n return {\n originalStart: map.cleanToOriginal.get(span.cleanStart) ?? span.cleanStart,\n originalEnd: map.cleanToOriginal.get(span.cleanEnd) ?? span.cleanEnd,\n }\n}\n","/**\n * Built-in text cleaner functions for preprocessing legal documents.\n *\n * Each cleaner is a simple transformation: (text: string) => string\n * Cleaners can be composed via the cleanText() pipeline.\n */\n\n/**\n * Remove all HTML tags from text.\n *\n * @example\n * stripHtmlTags(\"Smith v. <b>Doe</b>, 500 F.2d 123\")\n * // => \"Smith v. Doe, 500 F.2d 123\"\n */\nexport function stripHtmlTags(text: string): string {\n return text.replace(/<[^>]+>/g, \"\")\n}\n\n/**\n * Rejoin words split across line breaks by a hyphen.\n *\n * Court opinions often wrap long words (party names, reporter abbreviations)\n * with a hyphen at the line break: \"Dil-\\nlinger\" or \"F. Sup-\\np. 3d\".\n * This cleaner removes the hyphen + line break to restore the original word.\n *\n * Must run before normalizeWhitespace (which converts \\n to spaces, leaving\n * \"Dil- linger\" instead of \"Dillinger\").\n *\n * @example\n * rejoinHyphenatedWords(\"Dil-\\nlinger V, 672 F. Supp. 3d\")\n * // => \"Dillinger V, 672 F. Supp. 3d\"\n *\n * @example\n * rejoinHyphenatedWords(\"F. Sup-\\np. 3d 100\")\n * // => \"F. Supp. 3d 100\"\n */\nexport function rejoinHyphenatedWords(text: string): string {\n return text.replace(/(\\w)-\\s*[\\n\\r]+\\s*(\\w)/g, \"$1$2\")\n}\n\n/**\n * Replace each whitespace character (tab, newline, etc.) with a regular space.\n * Does NOT collapse consecutive spaces — that's a separate step so the position\n * mapper can handle each transformation type correctly (same-length replacement\n * vs. length-reducing collapse).\n *\n * @example\n * replaceWhitespace(\"Smith\\tv.\\nDoe\")\n * // => \"Smith v. Doe\"\n */\nexport function replaceWhitespace(text: string): string {\n return text.replace(/[\\t\\n\\r\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g, \" \")\n}\n\n/**\n * Collapse runs of multiple spaces into a single space.\n *\n * @example\n * collapseSpaces(\"Smith v. Doe, 500 F.2d 123\")\n * // => \"Smith v. Doe, 500 F.2d 123\"\n */\nexport function collapseSpaces(text: string): string {\n return text.replace(/ {2,}/g, \" \")\n}\n\n/**\n * Normalize whitespace: convert tabs/newlines to spaces, collapse multiple spaces.\n * Kept for backwards compatibility — new pipeline uses replaceWhitespace + collapseSpaces.\n *\n * @example\n * normalizeWhitespace(\"Smith v. Doe, 500 F.2d 123\")\n * // => \"Smith v. Doe, 500 F.2d 123\"\n */\nexport function normalizeWhitespace(text: string): string {\n return text.replace(/[\\t\\n\\r\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]+/g, \" \").replace(/ {2,}/g, \" \")\n}\n\n/**\n * Apply Unicode NFKC normalization (ligatures → separate chars).\n *\n * @example\n * normalizeUnicode(\"Smith v. Doe, 500 F.2d 123\") // with ligature \"fi\"\n * // => \"Smith v. Doe, 500 F.2d 123\" // normalized\n */\nexport function normalizeUnicode(text: string): string {\n return text.normalize(\"NFKC\")\n}\n\n/**\n * Replace curly quotes and apostrophes with straight quotes.\n *\n * @example\n * fixSmartQuotes(\"\"Smith\" v. 'Doe', 500 F.2d 123\")\n * // => \"\\\"Smith\\\" v. 'Doe', 500 F.2d 123\"\n */\nexport function fixSmartQuotes(text: string): string {\n return text\n .replace(/[\\u201C\\u201D]/g, '\"') // curly double quotes\n .replace(/[\\u2018\\u2019]/g, \"'\") // curly single quotes/apostrophes\n}\n\n/**\n * Remove underscore OCR artifacts (common in scanned documents).\n *\n * @example\n * removeOcrArtifacts(\"Smith v. Doe, 500 F._2d 123\")\n * // => \"Smith v. Doe, 500 F.2d 123\"\n */\nexport function removeOcrArtifacts(text: string): string {\n return text.replace(/_/g, \"\")\n}\n\n/**\n * Normalize Unicode dashes to ASCII equivalents.\n *\n * En-dash (U+2013) maps to a single hyphen (page ranges like 105–107).\n *\n * Em-dash (U+2014) is context-aware: between word characters (Illinois\n * Revised Statutes paragraph subdivisions like `par. 13—214`,\n * docket-number separators like `No. 84—C—4508`, page-range pincites\n * like `875—877`) it maps to a single hyphen; standalone (the\n * `500 F.4th — (2024)` blank-page placeholder) it maps to triple\n * hyphen so the existing `-{3,}` blank-page pattern still matches.\n *\n * The in-word substitution runs first with zero-width\n * lookbehind/lookahead so adjacent em-dashes (`84—C—4508`) are both\n * rewritten in one pass and don't fall through to the blank-page rule\n * (#333).\n *\n * @example\n * normalizeDashes(\"500 F.2d 123, 125–130\") // en-dash in range\n * // => \"500 F.2d 123, 125-130\"\n *\n * @example\n * normalizeDashes(\"par. 13—214(a)\") // in-word em-dash (#333)\n * // => \"par. 13-214(a)\"\n *\n * @example\n * normalizeDashes(\"No. 84—C—4508\") // docket separator (#333)\n * // => \"No. 84-C-4508\"\n *\n * @example\n * normalizeDashes(\"500 F.4th — (2024)\") // em-dash blank page\n * // => \"500 F.4th --- (2024)\"\n */\nexport function normalizeDashes(text: string): string {\n return text\n .replace(/(?<=\\w)[\\u2014\\u2015](?=\\w)/g, \"-\") // in-word em-dash \\u2192 hyphen (#333)\n .replace(/[\\u2014\\u2015]/g, \"---\") // standalone em-dash, horizontal bar → triple hyphen\n .replace(/[\\u2010\\u2012\\u2013]/g, \"-\") // hyphen, figure dash, en-dash → hyphen\n}\n\n/**\n * Decode common HTML entities relevant to legal text.\n *\n * Handles named entities (&sect;, &para;, &amp;, &nbsp;) and numeric entities\n * (&#NNN; and &#xHHH;). Should be called after stripHtmlTags to decode any\n * remaining entities.\n *\n * @example\n * decodeHtmlEntities(\"42 U.S.C. &sect; 1983\")\n * // => \"42 U.S.C. § 1983\"\n *\n * @example\n * decodeHtmlEntities(\"Smith &amp; Jones, 500 F.2d 123\")\n * // => \"Smith & Jones, 500 F.2d 123\"\n */\nexport function decodeHtmlEntities(text: string): string {\n return (\n text\n // Named entities\n .replace(/&sect;/gi, \"§\")\n .replace(/&para;/gi, \"¶\")\n .replace(/&amp;/gi, \"&\")\n .replace(/&nbsp;/gi, \" \")\n .replace(/&lt;/gi, \"<\")\n .replace(/&gt;/gi, \">\")\n .replace(/&quot;/gi, '\"')\n .replace(/&apos;/gi, \"'\")\n // Numeric entities - decimal\n .replace(/&#(\\d+);/g, (_match, dec) => {\n const code = Number.parseInt(dec, 10)\n return Number.isNaN(code) ? _match : String.fromCharCode(code)\n })\n // Numeric entities - hexadecimal\n .replace(/&#x([0-9a-fA-F]+);/g, (_match, hex) => {\n const code = Number.parseInt(hex, 16)\n return Number.isNaN(code) ? _match : String.fromCharCode(code)\n })\n )\n}\n\n/**\n * Normalize spacing in reporter abbreviations.\n *\n * Collapses \"letter. space\" sequences common in legal reporter abbreviations\n * where the space is inconsistent (e.g., OCR or copy-paste artifacts).\n *\n * @example\n * normalizeReporterSpacing(\"550 U. S. 544\") // => \"550 U.S. 544\"\n * normalizeReporterSpacing(\"500 F. 2d 123\") // => \"500 F.2d 123\"\n * normalizeReporterSpacing(\"127 S. Ct. 1955\") // => \"127 S.Ct. 1955\"\n */\nexport function normalizeReporterSpacing(text: string): string {\n // Targeted approach: collapse spacing in known reporter abbreviation patterns,\n // then apply a general ordinal-suffix rule. This avoids affecting non-reporter\n // abbreviations like \"L. Rev.\" or \"L. J.\" in journal citations.\n let result = text\n\n // Three-letter code abbreviations (U.S.C., C.F.R.) — must run BEFORE the\n // two-letter rules below so the full 3-letter shape collapses in one pass\n // regardless of spacing pattern (`U. S. C.` / `U.S. C.` / `U. S.C.`). #284\n result = result.replace(/\\bU\\.\\s*S\\.\\s*C\\./g, \"U.S.C.\")\n result = result.replace(/\\bC\\.\\s*F\\.\\s*R\\./g, \"C.F.R.\")\n\n // Specific reporter abbreviation collapses\n result = result.replace(/\\bU\\.\\s+S\\./g, \"U.S.\")\n result = result.replace(/\\bS\\.\\s+Ct\\./g, \"S.Ct.\")\n result = result.replace(/\\bL\\.\\s+Ed\\./g, \"L.Ed.\")\n result = result.replace(/\\bF\\.\\s+Supp\\./g, \"F.Supp.\")\n result = result.replace(/\\bF\\.\\s+(\\d+[a-z]+)/g, \"F.$1\")\n\n // General ordinal-suffix collapse: \"Supp. 2d\" → \"Supp.2d\", \"Ed. 2d\" → \"Ed.2d\",\n // \"St. 3d\" → \"St.3d\", \"So. 2d\" → \"So.2d\", \"Wis. 2d\" → \"Wis.2d\"\n result = result.replace(/([A-Za-z])\\.\\s+(\\d+[a-z]+)/g, \"$1.$2\")\n\n return result\n}\n\n/**\n * Normalize typographical symbols and strip zero-width characters.\n *\n * Handles prime marks (common OCR substitution for apostrophes) and invisible\n * Unicode characters that can silently break regex pattern matching.\n *\n * @example\n * normalizeTypography(\"Doe\\u2032s case\") // prime mark\n * // => \"Doe's case\"\n *\n * @example\n * normalizeTypography(\"500\\u200BF.2d\") // zero-width space\n * // => \"500F.2d\"\n */\nexport function normalizeTypography(text: string): string {\n return text\n .replace(/[\\u2032\\u2035]/g, \"'\") // prime, reversed prime → apostrophe\n .replace(/\\u200B|\\u200C|\\u200D|\\u2060|\\uFEFF/g, \"\") // zero-width chars\n}\n\n/**\n * Strip diacritical marks from text (opt-in OCR cleaner).\n *\n * Uses Unicode NFD decomposition to separate base characters from combining\n * marks, then strips the marks. Useful for OCR'd legal documents where\n * accented characters are artifacts of misrecognition.\n *\n * NOT included in the default pipeline — call explicitly or pass in cleaners array.\n *\n * @example\n * stripDiacritics(\"Hernández v. García\")\n * // => \"Hernandez v. Garcia\"\n */\nexport function stripDiacritics(text: string): string {\n return text.normalize(\"NFD\").replace(/[\\u0300-\\u036F]/g, \"\")\n}\n","/**\n * Segment-based position mapping.\n *\n * Compresses a per-character position map into contiguous segments where the\n * offset between clean and original coordinates is constant. Lookups use\n * binary search (O(log k) where k = number of segments, typically 50-200).\n */\n\nexport interface Segment {\n /** Start position in clean text */\n cleanPos: number\n /** Corresponding start position in original text */\n origPos: number\n /** Number of positions covered by this segment */\n len: number\n}\n\nexport class SegmentMap {\n readonly segments: readonly Segment[]\n\n constructor(segments: Segment[]) {\n this.segments = segments\n }\n\n /**\n * Create an identity map (clean position === original position).\n */\n static identity(length: number): SegmentMap {\n return new SegmentMap([{ cleanPos: 0, origPos: 0, len: length + 1 }])\n }\n\n /**\n * Compress a per-position Map into a SegmentMap.\n * Adjacent entries with the same offset (origPos - cleanPos) are merged\n * into a single segment.\n */\n static fromMap(map: Map<number, number>): SegmentMap {\n if (map.size === 0) return new SegmentMap([])\n\n // Sort entries by clean position (Map iteration order may not be sorted)\n const entries = [...map.entries()].sort((a, b) => a[0] - b[0])\n\n const segments: Segment[] = []\n let segCleanStart = entries[0][0]\n let segOrigStart = entries[0][1]\n let segLen = 1\n\n for (let i = 1; i < entries.length; i++) {\n const [cleanPos, origPos] = entries[i]\n const expectedCleanPos = segCleanStart + segLen\n const expectedOrigPos = segOrigStart + segLen\n\n if (cleanPos === expectedCleanPos && origPos === expectedOrigPos) {\n segLen++\n } else {\n segments.push({ cleanPos: segCleanStart, origPos: segOrigStart, len: segLen })\n segCleanStart = cleanPos\n segOrigStart = origPos\n segLen = 1\n }\n }\n segments.push({ cleanPos: segCleanStart, origPos: segOrigStart, len: segLen })\n\n return new SegmentMap(segments)\n }\n\n /**\n * Look up the original position for a clean-text position.\n * Uses binary search on sorted segments.\n */\n lookup(cleanPos: number): number {\n const segs = this.segments\n if (segs.length === 0) return cleanPos\n\n let lo = 0\n let hi = segs.length - 1\n\n while (lo <= hi) {\n const mid = (lo + hi) >>> 1\n const seg = segs[mid]\n\n if (cleanPos < seg.cleanPos) {\n hi = mid - 1\n } else if (cleanPos >= seg.cleanPos + seg.len) {\n lo = mid + 1\n } else {\n return seg.origPos + (cleanPos - seg.cleanPos)\n }\n }\n\n // Position beyond all segments: extrapolate from last segment\n const last = segs[segs.length - 1]\n return last.origPos + (cleanPos - last.cleanPos)\n }\n}\n","import type { Warning } from \"../types/citation\"\nimport type { TransformationMap } from \"../types/span\"\nimport { SegmentMap } from \"./segmentMap\"\nimport {\n collapseSpaces,\n decodeHtmlEntities,\n fixSmartQuotes,\n normalizeDashes,\n normalizeReporterSpacing,\n normalizeTypography,\n normalizeUnicode,\n rejoinHyphenatedWords,\n replaceWhitespace,\n stripHtmlTags,\n} from \"./cleaners\"\n\n/**\n * Result of text cleaning operation.\n */\nexport interface CleanTextResult {\n /** Cleaned text after all transformations */\n cleaned: string\n\n /** Position mappings between cleaned and original text */\n transformationMap: TransformationMap\n\n /** Warnings generated during cleaning (currently unused) */\n warnings: Warning[]\n}\n\n/**\n * Clean text using a pipeline of transformation functions.\n *\n * Applies cleaners sequentially while maintaining accurate position mappings\n * between the original and cleaned text. This enables citation extraction from\n * cleaned text while reporting positions in the original text.\n *\n * @param original - Original input text\n * @param cleaners - Array of cleaner functions to apply (default: stripHtmlTags, decodeHtmlEntities, normalizeWhitespace, normalizeUnicode, normalizeDashes, fixSmartQuotes, normalizeTypography, normalizeReporterSpacing)\n * @returns Cleaned text with position mappings and warnings\n *\n * @example\n * const result = cleanText(\"Smith v. <b>Doe</b>, 500 F.2d 123\")\n * // result.cleaned: \"Smith v. Doe, 500 F.2d 123\"\n * // result.transformationMap tracks position shifts from HTML removal\n */\nexport function cleanText(\n original: string,\n cleaners: Array<(text: string) => string> = [\n stripHtmlTags,\n decodeHtmlEntities,\n rejoinHyphenatedWords,\n replaceWhitespace,\n collapseSpaces,\n normalizeUnicode,\n normalizeDashes,\n fixSmartQuotes,\n normalizeTypography,\n normalizeReporterSpacing,\n ],\n): CleanTextResult {\n // Initialize 1:1 position mapping\n let currentText = original\n let cleanToOriginal = new Map<number, number>()\n let originalToClean = new Map<number, number>()\n\n // Identity mapping: cleanToOriginal[i] = i, originalToClean[i] = i\n for (let i = 0; i <= original.length; i++) {\n cleanToOriginal.set(i, i)\n originalToClean.set(i, i)\n }\n\n // Apply each cleaner sequentially, rebuilding position maps\n for (const cleaner of cleaners) {\n const beforeText = currentText\n const afterText = cleaner(currentText)\n\n if (beforeText !== afterText) {\n // Text changed - rebuild position maps\n const { newCleanToOriginal, newOriginalToClean } = rebuildPositionMaps(\n beforeText,\n afterText,\n cleanToOriginal,\n originalToClean,\n )\n\n cleanToOriginal = newCleanToOriginal\n originalToClean = newOriginalToClean\n currentText = afterText\n }\n }\n\n const transformationMap: TransformationMap = {\n cleanToOriginal,\n originalToClean,\n cleanToOriginalSegments: SegmentMap.fromMap(cleanToOriginal),\n }\n\n return {\n cleaned: currentText,\n transformationMap,\n warnings: [],\n }\n}\n\n/**\n * Rebuild position maps after a text transformation.\n *\n * Uses a simplified algorithm that scans through both strings, matching\n * characters where possible and tracking the offset accumulation.\n *\n * @param beforeText - Text before transformation\n * @param afterText - Text after transformation\n * @param oldCleanToOriginal - Previous clean-to-original mapping\n * @param oldOriginalToClean - Previous original-to-clean mapping\n * @returns New position maps\n */\nfunction rebuildPositionMaps(\n beforeText: string,\n afterText: string,\n oldCleanToOriginal: Map<number, number>,\n _oldOriginalToClean: Map<number, number>,\n): {\n newCleanToOriginal: Map<number, number>\n newOriginalToClean: Map<number, number>\n} {\n const newCleanToOriginal = new Map<number, number>()\n const newOriginalToClean = new Map<number, number>()\n\n let beforeIdx = 0\n let afterIdx = 0\n\n // Scan through both strings, matching characters where possible\n while (beforeIdx <= beforeText.length || afterIdx <= afterText.length) {\n // Both at end\n if (beforeIdx >= beforeText.length && afterIdx >= afterText.length) {\n const originalPos = oldCleanToOriginal.get(beforeIdx) ?? beforeIdx\n newCleanToOriginal.set(afterIdx, originalPos)\n newOriginalToClean.set(originalPos, afterIdx)\n break\n }\n\n // Before text exhausted (expansion case)\n if (beforeIdx >= beforeText.length) {\n const originalPos = oldCleanToOriginal.get(beforeIdx) ?? beforeIdx\n newCleanToOriginal.set(afterIdx, originalPos)\n afterIdx++\n continue\n }\n\n // After text exhausted (removal case)\n if (afterIdx >= afterText.length) {\n const originalPos = oldCleanToOriginal.get(beforeIdx) ?? beforeIdx\n newOriginalToClean.set(originalPos, afterIdx)\n beforeIdx++\n continue\n }\n\n // Characters match - carry forward the mapping\n if (beforeText[beforeIdx] === afterText[afterIdx]) {\n const originalPos = oldCleanToOriginal.get(beforeIdx) ?? beforeIdx\n newCleanToOriginal.set(afterIdx, originalPos)\n newOriginalToClean.set(originalPos, afterIdx)\n beforeIdx++\n afterIdx++\n } else {\n // Characters differ - need to determine if this is insertion/deletion/replacement\n\n // If remaining lengths are equal, every mismatch is a pure character\n // replacement (no insertions or deletions from this point on).\n // This prevents the lookahead from misinterpreting replacements like \\n→' '\n // as multi-char deletions when the replacement char appears later in the text.\n if (beforeText.length - beforeIdx === afterText.length - afterIdx) {\n const originalPos = oldCleanToOriginal.get(beforeIdx) ?? beforeIdx\n newCleanToOriginal.set(afterIdx, originalPos)\n newOriginalToClean.set(originalPos, afterIdx)\n beforeIdx++\n afterIdx++\n continue\n }\n\n // Look ahead to find next match\n let foundMatch = false\n // Lookahead must span the largest single deletion (e.g., a long HTML tag\n // like <span class=\"citation\" data-id=\"1\"> is 35+ chars). A fixed 20\n // caused Issue #154: tags longer than the window produced corrupted\n // position mappings, collapsing many clean positions to a single\n // original position. Scale to the length delta with a reasonable floor.\n const maxLookAhead = Math.max(40, Math.abs(beforeText.length - afterText.length) + 10)\n\n // Find the closest CONFIRMED match in both directions simultaneously.\n // A \"confirmed\" match requires that at least CONFIRM_LEN characters\n // after the match point also align. This prevents greedy false matches\n // (Issue #161) where, e.g., normalizeDashes expands \"—\" → \"---\" and the\n // deletion lookahead grabs a \"-\" from a nearby page range instead.\n const CONFIRM_LEN = 3\n let bestDelLA = -1\n let bestInsLA = -1\n\n for (let la = 1; la <= maxLookAhead; la++) {\n // Check deletion direction (skipping chars in before)\n if (bestDelLA < 0 && beforeIdx + la < beforeText.length) {\n if (beforeText[beforeIdx + la] === afterText[afterIdx]) {\n let ok = true\n for (let c = 1; c < CONFIRM_LEN; c++) {\n const bi = beforeIdx + la + c\n const ai = afterIdx + c\n if (bi >= beforeText.length || ai >= afterText.length) break\n if (beforeText[bi] !== afterText[ai]) { ok = false; break }\n }\n if (ok) bestDelLA = la\n }\n }\n\n // Check insertion direction (skipping chars in after)\n if (bestInsLA < 0 && afterIdx + la < afterText.length) {\n if (beforeText[beforeIdx] === afterText[afterIdx + la]) {\n let ok = true\n for (let c = 1; c < CONFIRM_LEN; c++) {\n const bi = beforeIdx + c\n const ai = afterIdx + la + c\n if (bi >= beforeText.length || ai >= afterText.length) break\n if (beforeText[bi] !== afterText[ai]) { ok = false; break }\n }\n if (ok) bestInsLA = la\n }\n }\n\n // Stop early if we found matches in both directions\n if (bestDelLA >= 0 && bestInsLA >= 0) break\n }\n\n // Pick the shorter confirmed match (prefer smaller displacement)\n if (bestDelLA >= 0 && (bestInsLA < 0 || bestDelLA <= bestInsLA)) {\n // Deletion: chars before[beforeIdx .. beforeIdx+bestDelLA-1] were removed\n for (let i = 0; i < bestDelLA; i++) {\n const originalPos = oldCleanToOriginal.get(beforeIdx + i) ?? beforeIdx + i\n newOriginalToClean.set(originalPos, afterIdx)\n }\n beforeIdx += bestDelLA\n foundMatch = true\n } else if (bestInsLA >= 0) {\n // Insertion: chars after[afterIdx .. afterIdx+bestInsLA-1] are new\n const originalPos = oldCleanToOriginal.get(beforeIdx) ?? beforeIdx\n for (let i = 0; i < bestInsLA; i++) {\n newCleanToOriginal.set(afterIdx + i, originalPos)\n }\n afterIdx += bestInsLA\n foundMatch = true\n }\n\n if (foundMatch) continue\n\n // No match found within lookahead - treat as replacement\n const originalPos = oldCleanToOriginal.get(beforeIdx) ?? beforeIdx\n newCleanToOriginal.set(afterIdx, originalPos)\n newOriginalToClean.set(originalPos, afterIdx)\n beforeIdx++\n afterIdx++\n }\n }\n\n return { newCleanToOriginal, newOriginalToClean }\n}\n","/**\n * Union-Find (Disjoint-Set Forest)\n *\n * Tracks connected components for subsequent history chain linking.\n * Uses path halving and union by rank with lower-index-wins tie-breaking.\n */\n\nexport class UnionFind {\n private readonly parent: number[]\n private readonly rank: number[]\n\n constructor(n: number) {\n this.parent = Array.from({ length: n }, (_, i) => i)\n this.rank = new Array<number>(n).fill(0)\n }\n\n /** Find the root (canonical representative) of the set containing x. */\n find(x: number): number {\n let current = x\n while (this.parent[current] !== current) {\n this.parent[current] = this.parent[this.parent[current]] // path halving\n current = this.parent[current]\n }\n return current\n }\n\n /** Merge the sets containing x and y. Lower index becomes root. */\n union(x: number, y: number): void {\n let rootX = this.find(x)\n let rootY = this.find(y)\n if (rootX === rootY) return\n\n // Lower index is always the canonical representative\n if (rootX > rootY) {\n const tmp = rootX\n rootX = rootY\n rootY = tmp\n }\n this.parent[rootY] = rootX\n if (this.rank[rootX] === this.rank[rootY]) this.rank[rootX]++\n }\n\n /** Check if x and y are in the same set. */\n connected(x: number, y: number): boolean {\n return this.find(x) === this.find(y)\n }\n\n /** Return all connected components as a map from root → sorted member indices. */\n components(): Map<number, number[]> {\n const result = new Map<number, number[]>()\n for (let i = 0; i < this.parent.length; i++) {\n const root = this.find(i)\n let members = result.get(root)\n if (!members) {\n members = []\n result.set(root, members)\n }\n members.push(i)\n }\n return result\n }\n}\n","import type { FootnoteMap, FootnoteZone } from \"./types\"\n\n/**\n * Source pattern for opening tags of footnote container elements.\n * Matches: <footnote ...>, <fn ...>, <div class=\"footnote\" ...>,\n * <div id=\"fn1\" ...>, <aside class=\"footnote\" ...>, etc.\n *\n * Created as a fresh RegExp per call to avoid shared mutable lastIndex state.\n */\nconst FOOTNOTE_OPEN_SRC =\n /<(footnote|fn)\\b[^>]*>|<(div|aside|section|p|span)\\b[^>]*(?:class\\s*=\\s*[\"'][^\"']*\\bfootnote\\b[^\"']*[\"']|id\\s*=\\s*[\"'](?:fn|footnote)\\d*[\"'])[^>]*>/gi.source\n\n/**\n * Extract a footnote number from an HTML tag's attributes or from leading content.\n *\n * Priority: label attr > id digits > content leading digits > sequential fallback.\n */\nfunction extractFootnoteNumber(tag: string, content: string, sequentialIndex: number): number {\n // Try label=\"N\" attribute\n const labelMatch = /\\blabel\\s*=\\s*[\"'](\\d+)[\"']/.exec(tag)\n if (labelMatch) return Number.parseInt(labelMatch[1], 10)\n\n // Try id=\"fn3\" or id=\"footnote3\" attribute\n const idMatch = /\\bid\\s*=\\s*[\"'](?:fn|footnote)(\\d+)[\"']/.exec(tag)\n if (idMatch) return Number.parseInt(idMatch[1], 10)\n\n // Strip HTML tags from content before checking for leading digits\n const textContent = content.replace(/<[^>]*>/g, \"\")\n\n // Try leading digit in text content\n const contentMatch = /^\\s*(\\d+)[.\\s):]/.exec(textContent)\n if (contentMatch) return Number.parseInt(contentMatch[1], 10)\n\n // Fallback: sequential\n return sequentialIndex + 1\n}\n\n/**\n * Result of closing-tag search: the position where inner content ends\n * (start of `</tag>`) and the position after the closing tag.\n */\ninterface ClosingTagResult {\n /** Index of the `<` in `</tagName>` — marks the end of inner content */\n contentEnd: number\n /** Index of the character after `</tagName>` — marks the end of the element */\n tagEnd: number\n}\n\n/**\n * Find the matching closing tag for a given element, handling nesting.\n *\n * @returns Positions of the closing tag, or null if unmatched.\n */\nfunction findClosingTag(\n html: string,\n tagName: string,\n startAfterOpen: number,\n): ClosingTagResult | null {\n const openPattern = new RegExp(`<${tagName}\\\\b[^>]*>`, \"gi\")\n const closePattern = new RegExp(`</${tagName}\\\\s*>`, \"gi\")\n\n openPattern.lastIndex = startAfterOpen\n closePattern.lastIndex = startAfterOpen\n\n let depth = 1\n\n while (depth > 0) {\n const nextOpen = openPattern.exec(html)\n const nextClose = closePattern.exec(html)\n\n if (!nextClose) return null\n\n if (nextOpen && nextOpen.index < nextClose.index) {\n depth++\n closePattern.lastIndex = nextOpen.index + nextOpen[0].length\n } else {\n depth--\n if (depth === 0) {\n return { contentEnd: nextClose.index, tagEnd: nextClose.index + nextClose[0].length }\n }\n openPattern.lastIndex = nextClose.index + nextClose[0].length\n }\n }\n\n return null\n}\n\n/**\n * Detect footnote zones from HTML structural elements.\n *\n * Uses regex-based tag scanning (no DOM dependency) to find footnote\n * containers and record their content ranges.\n *\n * @param html - Raw HTML text\n * @returns FootnoteMap with zones in raw-text coordinates, sorted by start position\n */\nexport function detectHtmlFootnotes(html: string): FootnoteMap {\n const zones: FootnoteZone[] = []\n let match: RegExpExecArray | null\n\n // Fresh regex per call to avoid shared mutable lastIndex state\n const footnoteOpenRe = new RegExp(FOOTNOTE_OPEN_SRC, \"gi\")\n\n while ((match = footnoteOpenRe.exec(html)) !== null) {\n const openTag = match[0]\n const openTagStart = match.index\n const contentStart = openTagStart + openTag.length\n\n const tagName = match[1] || match[2]\n\n const closing = findClosingTag(html, tagName, contentStart)\n if (!closing) continue\n\n const content = html.slice(contentStart, closing.contentEnd)\n const footnoteNumber = extractFootnoteNumber(openTag, content, zones.length)\n\n zones.push({\n start: contentStart,\n end: closing.contentEnd,\n footnoteNumber,\n })\n\n footnoteOpenRe.lastIndex = closing.tagEnd\n }\n\n return zones.sort((a, b) => a.start - b.start)\n}\n","import type { FootnoteMap } from \"./types\"\n\n/** Separator line pattern: 5+ dashes or underscores on their own line. */\nconst SEPARATOR_RE = /^\\s*[-_]{5,}\\s*$/m\n\n/**\n * Source pattern for footnote markers at line start.\n * Captures the footnote number from whichever group matches.\n * Created as a fresh RegExp per call to avoid shared mutable lastIndex state.\n */\nconst MARKER_SRC =\n /^\\s*(?:FN\\s*(\\d+)[.\\s:)]|\\[(\\d+)\\]\\s|n\\.\\s*(\\d+)\\s|(\\d+)\\.\\s)/gm.source\n\n/**\n * Detect footnote zones in plain text using separator + marker heuristics.\n *\n * Strategy: find a separator line, then parse numbered markers in the text\n * that follows. Each footnote zone extends from its marker to the start\n * of the next marker (or end of text).\n *\n * @param text - Raw text (not cleaned -- needs newlines intact)\n * @returns FootnoteMap with zones in input-text coordinates, sorted by start position\n */\nexport function detectTextFootnotes(text: string): FootnoteMap {\n const sepMatch = SEPARATOR_RE.exec(text)\n if (!sepMatch) return []\n\n const sectionOffset = sepMatch.index + sepMatch[0].length\n\n const footnoteSection = text.slice(sectionOffset)\n\n // Fresh regex per call to avoid shared mutable lastIndex state\n const markerRe = new RegExp(MARKER_SRC, \"gm\")\n const markers: { index: number; footnoteNumber: number }[] = []\n let match: RegExpExecArray | null\n\n while ((match = markerRe.exec(footnoteSection)) !== null) {\n const numStr = match[1] || match[2] || match[3] || match[4]\n if (!numStr) continue\n markers.push({\n index: match.index + sectionOffset,\n footnoteNumber: Number.parseInt(numStr, 10),\n })\n }\n\n if (markers.length === 0) return []\n\n const zones: FootnoteMap = []\n for (let i = 0; i < markers.length; i++) {\n const start = markers[i].index\n const end = i + 1 < markers.length ? markers[i + 1].index : text.length\n\n zones.push({\n start,\n end,\n footnoteNumber: markers[i].footnoteNumber,\n })\n }\n\n return zones\n}\n","import { detectHtmlFootnotes } from \"./htmlDetector\"\nimport { detectTextFootnotes } from \"./textDetector\"\nimport type { FootnoteMap } from \"./types\"\n\nconst HAS_HTML_RE = /<[^>]+>/\n\n/**\n * Detect footnote zones in text (HTML or plain text).\n *\n * Strategy: if the input contains HTML tags, try HTML structural detection\n * first. If that yields no results (HTML without footnote elements), fall\n * back to plain-text heuristic detection. For non-HTML input, use plain-text\n * detection directly.\n *\n * @param text - Raw input text (HTML or plain text)\n * @returns FootnoteMap with zones in input-text coordinates, sorted by start\n */\nexport function detectFootnotes(text: string): FootnoteMap {\n if (HAS_HTML_RE.test(text)) {\n const htmlZones = detectHtmlFootnotes(text)\n if (htmlZones.length > 0) return htmlZones\n }\n\n return detectTextFootnotes(text)\n}\n","import type { TransformationMap } from \"@/types/span\"\nimport type { FootnoteMap } from \"./types\"\n\n/**\n * Search nearby positions in the map to find the closest mapped coordinate.\n * This handles cases where a zone boundary falls on a character that was\n * removed during cleaning (e.g., an HTML tag boundary).\n *\n * For zone starts, search forward (the first surviving character after the boundary).\n * For zone ends, search backward (the last surviving character before the boundary).\n *\n * @param pos - Original-text position to look up\n * @param originalToClean - Position mapping from TransformationMap\n * @param direction - \"forward\" for zone starts, \"backward\" for zone ends\n * @param maxSearch - Maximum positions to scan (matches cleanText maxLookAhead)\n * @returns Mapped clean-text position, or undefined if nothing found within range\n */\nfunction findNearestCleanPosition(\n pos: number,\n originalToClean: Map<number, number>,\n direction: \"forward\" | \"backward\",\n maxSearch = 20,\n): number | undefined {\n for (let offset = 1; offset <= maxSearch; offset++) {\n const candidate = direction === \"forward\" ? pos + offset : pos - offset\n const mapped = originalToClean.get(candidate)\n if (mapped !== undefined) return mapped\n }\n return undefined\n}\n\n/**\n * Map FootnoteMap zones from raw-text coordinates to clean-text coordinates.\n *\n * Uses TransformationMap.originalToClean to translate each zone's start/end.\n * When an exact position isn't in the map (e.g., it fell on a stripped HTML tag),\n * scans nearby positions: forward for zone starts, backward for zone ends.\n *\n * @param zones - FootnoteMap in raw-text coordinates\n * @param map - TransformationMap from cleanText()\n * @returns FootnoteMap in clean-text coordinates\n */\nexport function mapFootnoteZones(zones: FootnoteMap, map: TransformationMap): FootnoteMap {\n if (zones.length === 0) return []\n\n return zones.map((zone) => ({\n start:\n map.originalToClean.get(zone.start) ??\n findNearestCleanPosition(zone.start, map.originalToClean, \"forward\") ??\n zone.start,\n end:\n map.originalToClean.get(zone.end) ??\n findNearestCleanPosition(zone.end, map.originalToClean, \"backward\") ??\n zone.end,\n footnoteNumber: zone.footnoteNumber,\n }))\n}\n","import type { Citation } from \"@/types/citation\"\nimport type { FootnoteMap } from \"./types\"\n\n/**\n * Tag citations with footnote metadata by looking up each citation's\n * clean-text span position in the footnote zone map.\n *\n * Uses binary search on the sorted FootnoteMap for O(log n) lookup per citation.\n * Mutates citations in place.\n *\n * @param citations - Citations to tag (mutated in place)\n * @param footnoteMap - Footnote zones in clean-text coordinates, sorted by start\n */\nexport function tagCitationsWithFootnotes(\n citations: Citation[],\n footnoteMap: FootnoteMap,\n): void {\n if (footnoteMap.length === 0) return\n\n for (const citation of citations) {\n const pos = citation.span.cleanStart\n\n let lo = 0\n let hi = footnoteMap.length - 1\n\n while (lo <= hi) {\n const mid = (lo + hi) >>> 1\n const zone = footnoteMap[mid]\n\n if (pos < zone.start) {\n hi = mid - 1\n } else if (pos >= zone.end) {\n lo = mid + 1\n } else {\n citation.inFootnote = true\n citation.footnoteNumber = zone.footnoteNumber\n break\n }\n }\n }\n}\n","/**\n * Date Parsing Utilities for Legal Citations\n *\n * Parses dates from parentheticals in legal citations. Supports three formats:\n * 1. Abbreviated month: \"Jan. 15, 2020\"\n * 2. Full month: \"January 15, 2020\"\n * 3. Numeric US: \"1/15/2020\"\n * 4. Year-only: \"2020\"\n *\n * @module extract/dates\n */\n\n/**\n * Structured date components.\n * Month and day are optional to support year-only dates.\n */\nexport interface ParsedDate {\n year: number\n month?: number\n day?: number\n}\n\n/**\n * Date in both ISO string and structured format.\n */\nexport interface StructuredDate {\n /** ISO 8601 format: YYYY-MM-DD, YYYY-MM, or YYYY */\n iso: string\n /** Structured date components */\n parsed: ParsedDate\n}\n\n/**\n * Month name/abbreviation to numeric value (1-12).\n * Includes both 3-letter and 4-letter (Sept) abbreviations.\n */\nconst MONTH_MAP: Record<string, number> = {\n jan: 1,\n january: 1,\n feb: 2,\n february: 2,\n mar: 3,\n march: 3,\n apr: 4,\n april: 4,\n may: 5,\n jun: 6,\n june: 6,\n jul: 7,\n july: 7,\n aug: 8,\n august: 8,\n sep: 9,\n sept: 9,\n september: 9,\n oct: 10,\n october: 10,\n nov: 11,\n november: 11,\n dec: 12,\n december: 12,\n}\n\n/**\n * Parse a month name or abbreviation to numeric value (1-12).\n *\n * @param monthStr - Month name or abbreviation (e.g., \"Jan\", \"January\", \"Sept.\")\n * @returns Numeric month (1-12)\n * @throws Error if month name is not recognized\n *\n * @example\n * ```typescript\n * parseMonth(\"Jan\") // 1\n * parseMonth(\"Sept.\") // 9\n * parseMonth(\"December\") // 12\n * ```\n */\nexport function parseMonth(monthStr: string): number {\n // Normalize: lowercase, strip trailing period\n const normalized = monthStr.toLowerCase().replace(/\\.$/, \"\")\n const month = MONTH_MAP[normalized]\n\n if (month === undefined) {\n throw new Error(`Invalid month name: ${monthStr}`)\n }\n\n return month\n}\n\n/**\n * Convert structured date components to ISO 8601 string.\n * Handles full dates, month+year, and year-only formats.\n *\n * @param parsed - Structured date components\n * @returns ISO 8601 string (YYYY-MM-DD, YYYY-MM, or YYYY)\n *\n * @example\n * ```typescript\n * toIsoDate({ year: 2020, month: 1, day: 15 }) // \"2020-01-15\"\n * toIsoDate({ year: 2020, month: 1 }) // \"2020-01\"\n * toIsoDate({ year: 2020 }) // \"2020\"\n * ```\n */\nexport function toIsoDate(parsed: ParsedDate): string {\n const { year, month, day } = parsed\n\n if (month !== undefined && day !== undefined) {\n // Full date: YYYY-MM-DD with zero-padding\n const monthStr = String(month).padStart(2, \"0\")\n const dayStr = String(day).padStart(2, \"0\")\n return `${year}-${monthStr}-${dayStr}`\n }\n\n if (month !== undefined) {\n // Month+year: YYYY-MM with zero-padding\n const monthStr = String(month).padStart(2, \"0\")\n return `${year}-${monthStr}`\n }\n\n // Year-only: YYYY\n return String(year)\n}\n\n/**\n * Parse a date string into structured format.\n * Tries multiple formats in order:\n * 1. Abbreviated month (Jan. 15, 2020)\n * 2. Full month (January 15, 2020)\n * 3. Numeric US format (1/15/2020)\n * 4. Year-only (2020)\n *\n * @param dateStr - Date string in any supported format\n * @returns Structured date with ISO string, or undefined if no match\n *\n * @example\n * ```typescript\n * parseDate(\"Jan. 15, 2020\") // { iso: \"2020-01-15\", parsed: { year: 2020, month: 1, day: 15 } }\n * parseDate(\"January 15, 2020\") // { iso: \"2020-01-15\", parsed: { year: 2020, month: 1, day: 15 } }\n * parseDate(\"1/15/2020\") // { iso: \"2020-01-15\", parsed: { year: 2020, month: 1, day: 15 } }\n * parseDate(\"2020\") // { iso: \"2020\", parsed: { year: 2020 } }\n * parseDate(\"no date\") // undefined\n * ```\n */\nexport function parseDate(dateStr: string): StructuredDate | undefined {\n // Try abbreviated month format: Jan. 15, 2020 or Feb 9, 2015\n const abbrMatch = dateStr.match(\n /\\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)\\.?\\s+(\\d{1,2}),?\\s+(\\d{4})\\b/i,\n )\n if (abbrMatch) {\n const month = parseMonth(abbrMatch[1])\n const day = Number.parseInt(abbrMatch[2], 10)\n const year = Number.parseInt(abbrMatch[3], 10)\n const parsed = { year, month, day }\n return { iso: toIsoDate(parsed), parsed }\n }\n\n // Try full month format: January 15, 2020\n const fullMatch = dateStr.match(\n /\\b(January|February|March|April|May|June|July|August|September|October|November|December)\\s+(\\d{1,2}),?\\s+(\\d{4})\\b/i,\n )\n if (fullMatch) {\n const month = parseMonth(fullMatch[1])\n const day = Number.parseInt(fullMatch[2], 10)\n const year = Number.parseInt(fullMatch[3], 10)\n const parsed = { year, month, day }\n return { iso: toIsoDate(parsed), parsed }\n }\n\n // Try numeric US format: 1/15/2020 (full year) or 10/3/07 (two-digit year,\n // Louisiana docket-prefix and other regional shorthand; #232). Two-digit\n // years pivot at 50: 00-50 → 21st century, 51-99 → 20th century.\n const numericMatch = dateStr.match(/\\b(\\d{1,2})\\/(\\d{1,2})\\/(\\d{2}|\\d{4})\\b/)\n if (numericMatch) {\n const month = Number.parseInt(numericMatch[1], 10)\n const day = Number.parseInt(numericMatch[2], 10)\n const rawYear = numericMatch[3]\n let year = Number.parseInt(rawYear, 10)\n if (rawYear.length === 2) {\n year = year <= 50 ? 2000 + year : 1900 + year\n }\n const parsed = { year, month, day }\n return { iso: toIsoDate(parsed), parsed }\n }\n\n // Try year-only: 2020\n const yearMatch = dateStr.match(/\\b(\\d{4})\\b/)\n if (yearMatch) {\n const year = Number.parseInt(yearMatch[1], 10)\n const parsed = { year }\n return { iso: toIsoDate(parsed), parsed }\n }\n\n // No match\n return undefined\n}\n","/**\n * Court Inference from Reporter Series\n *\n * Infers court level and jurisdiction from reporter abbreviation using a\n * curated static lookup table.\n *\n * Design decision: This uses a hand-curated table rather than parsing\n * mlz_jurisdiction from the reporter DB. Using the reporter DB would make\n * it a hard dependency of core extraction, defeating the lazy-loading\n * architecture where eyecite-ts/data is a separate entry point for\n * tree-shaking. A curated table keeps court inference zero-dependency\n * and fast. Full reporter DB coverage can be added later as an opt-in\n * function in the eyecite-ts/data entry point.\n *\n * @module extract/courtInference\n */\n\nimport type { CourtInference } from \"@/types/citation\"\n\n/** Helper to reduce repetition when building the lookup table. */\nfunction federal(level: CourtInference[\"level\"]): CourtInference {\n return { level, jurisdiction: \"federal\", confidence: 1.0 }\n}\n\nfunction state(level: CourtInference[\"level\"], st: string): CourtInference {\n return { level, jurisdiction: \"state\", state: st, confidence: 1.0 }\n}\n\nfunction regional(level: CourtInference[\"level\"]): CourtInference {\n return { level, jurisdiction: \"state\", confidence: 0.7 }\n}\n\n/**\n * Curated reporter → court inference mapping.\n *\n * Covers ~80 reporter abbreviations (including spacing variants). Unknown\n * reporters return undefined from inferCourtFromReporter() — no guessing.\n */\nconst REPORTER_COURT_MAP = new Map<string, CourtInference>([\n // ── Federal Supreme ──────────────────────────────────────────────\n // After normalizeReporterSpacing, reporters arrive collapsed (e.g. \"S.Ct.\").\n // Spaced forms kept for direct-call / pre-cleaned paths.\n [\"U.S.\", federal(\"supreme\")],\n [\"S.Ct.\", federal(\"supreme\")],\n [\"L.Ed.\", federal(\"supreme\")],\n [\"L.Ed.2d\", federal(\"supreme\")],\n [\"S. Ct.\", federal(\"supreme\")],\n [\"L. Ed.\", federal(\"supreme\")],\n [\"L. Ed. 2d\", federal(\"supreme\")],\n [\"U. S.\", federal(\"supreme\")],\n [\"L.Ed. 2d\", federal(\"supreme\")],\n [\"L. Ed.2d\", federal(\"supreme\")],\n\n // ── Federal Appellate ────────────────────────────────────────────\n [\"F.\", federal(\"appellate\")],\n [\"F.2d\", federal(\"appellate\")],\n [\"F.3d\", federal(\"appellate\")],\n [\"F.4th\", federal(\"appellate\")],\n [\"F. App'x\", federal(\"appellate\")],\n\n // ── Federal Trial ────────────────────────────────────────────────\n [\"F.Supp.\", federal(\"trial\")],\n [\"F.Supp.2d\", federal(\"trial\")],\n [\"F.Supp.3d\", federal(\"trial\")],\n [\"F.Supp.4th\", federal(\"trial\")],\n [\"F. Supp.\", federal(\"trial\")],\n [\"F. Supp. 2d\", federal(\"trial\")],\n [\"F. Supp. 3d\", federal(\"trial\")],\n [\"F. Supp. 4th\", federal(\"trial\")],\n [\"F.R.D.\", federal(\"trial\")],\n [\"B.R.\", federal(\"trial\")],\n\n // ── California ───────────────────────────────────────────────────\n [\"Cal.\", state(\"supreme\", \"CA\")],\n [\"Cal.2d\", state(\"supreme\", \"CA\")],\n [\"Cal.3d\", state(\"supreme\", \"CA\")],\n [\"Cal.4th\", state(\"supreme\", \"CA\")],\n [\"Cal.5th\", state(\"supreme\", \"CA\")],\n [\"Cal.App.\", state(\"appellate\", \"CA\")],\n [\"Cal.App.2d\", state(\"appellate\", \"CA\")],\n [\"Cal.App.3d\", state(\"appellate\", \"CA\")],\n [\"Cal.App.4th\", state(\"appellate\", \"CA\")],\n [\"Cal.App.5th\", state(\"appellate\", \"CA\")],\n [\"Cal.Rptr.\", state(\"unknown\", \"CA\")],\n [\"Cal.Rptr.2d\", state(\"unknown\", \"CA\")],\n [\"Cal.Rptr.3d\", state(\"unknown\", \"CA\")],\n\n // ── New York ─────────────────────────────────────────────────────\n [\"N.Y.\", state(\"supreme\", \"NY\")],\n [\"N.Y.2d\", state(\"supreme\", \"NY\")],\n [\"N.Y.3d\", state(\"supreme\", \"NY\")],\n [\"A.D.\", state(\"appellate\", \"NY\")],\n [\"A.D.2d\", state(\"appellate\", \"NY\")],\n [\"A.D.3d\", state(\"appellate\", \"NY\")],\n [\"Misc.\", state(\"trial\", \"NY\")],\n [\"Misc.2d\", state(\"trial\", \"NY\")],\n [\"Misc.3d\", state(\"trial\", \"NY\")],\n [\"N.Y.S.\", state(\"unknown\", \"NY\")],\n [\"N.Y.S.2d\", state(\"unknown\", \"NY\")],\n [\"N.Y.S.3d\", state(\"unknown\", \"NY\")],\n\n // ── Illinois ─────────────────────────────────────────────────────\n [\"Ill.\", state(\"supreme\", \"IL\")],\n [\"Ill.2d\", state(\"supreme\", \"IL\")],\n [\"Ill.App.\", state(\"appellate\", \"IL\")],\n [\"Ill.App.2d\", state(\"appellate\", \"IL\")],\n [\"Ill.App.3d\", state(\"appellate\", \"IL\")],\n [\"Ill.Dec.\", state(\"unknown\", \"IL\")],\n\n // ── Ohio ────────────────────────────────────────────────────────\n [\"Ohio St.\", state(\"supreme\", \"OH\")],\n [\"Ohio St.2d\", state(\"supreme\", \"OH\")],\n [\"Ohio St.3d\", state(\"supreme\", \"OH\")],\n [\"Ohio App.3d\", state(\"appellate\", \"OH\")],\n\n // ── Pennsylvania ────────────────────────────────────────────────\n [\"Pa.\", state(\"supreme\", \"PA\")],\n [\"Pa. Super.\", state(\"appellate\", \"PA\")],\n\n // ── Texas ───────────────────────────────────────────────────────\n [\"Tex.\", state(\"supreme\", \"TX\")],\n\n // ── Florida ─────────────────────────────────────────────────────\n [\"Fla.\", state(\"supreme\", \"FL\")],\n\n // ── Massachusetts ───────────────────────────────────────────────\n [\"Mass.\", state(\"supreme\", \"MA\")],\n [\"Mass. App. Ct.\", state(\"appellate\", \"MA\")],\n\n // ── Regional (multi-state, no state field) ───────────────────────\n // Level is \"unknown\" because regional reporters carry both supreme\n // and appellate court opinions (e.g., A.3d includes MD Court of\n // Appeals decisions). The lower confidence already signals ambiguity.\n [\"A.\", regional(\"unknown\")],\n [\"A.2d\", regional(\"unknown\")],\n [\"A.3d\", regional(\"unknown\")],\n [\"S.E.\", regional(\"unknown\")],\n [\"S.E.2d\", regional(\"unknown\")],\n [\"S.E.3d\", regional(\"unknown\")],\n [\"S.W.\", regional(\"unknown\")],\n [\"S.W.2d\", regional(\"unknown\")],\n [\"S.W.3d\", regional(\"unknown\")],\n [\"N.E.\", regional(\"unknown\")],\n [\"N.E.2d\", regional(\"unknown\")],\n [\"N.E.3d\", regional(\"unknown\")],\n [\"N.W.\", regional(\"unknown\")],\n [\"N.W.2d\", regional(\"unknown\")],\n [\"N.W.3d\", regional(\"unknown\")],\n [\"So.\", regional(\"unknown\")],\n [\"So.2d\", regional(\"unknown\")],\n [\"So.3d\", regional(\"unknown\")],\n [\"P.\", regional(\"unknown\")],\n [\"P.2d\", regional(\"unknown\")],\n [\"P.3d\", regional(\"unknown\")],\n])\n\n/**\n * Infer court level and jurisdiction from a reporter abbreviation.\n *\n * @param reporter - Reporter abbreviation (e.g., \"F.3d\", \"Cal.App.5th\")\n * @returns CourtInference if reporter is in the curated table, undefined otherwise\n */\nexport function inferCourtFromReporter(reporter: string): CourtInference | undefined {\n return REPORTER_COURT_MAP.get(reporter)\n}\n","/**\n * Structured pincite information parsed from citation text.\n *\n * `page` and `paragraph` are mutually exclusive — a pincite is either a page\n * reference (the common case) or a paragraph reference (#204; common in\n * NY Slip Op, Canadian neutrals, and other paragraph-numbered sources). The\n * top-level convenience `pincite: number` field on the citation continues to\n * mirror `page` only; paragraph consumers read `paragraph` / `endParagraph`\n * from this struct directly.\n */\nexport interface PinciteInfo {\n /** Primary page number. Undefined when the pincite is paragraph-only (#204). */\n page?: number\n /** End page for ranges: \"570-75\" → 575 */\n endPage?: number\n /** Footnote number: \"570 n.3\" → 3. For multi-footnote refs (\"nn.3-5\"), the\n * first note; see `footnoteEnd` for the range end. */\n footnote?: number\n /** End footnote for multi-note refs: \"570 nn.3-5\" → 5 */\n footnoteEnd?: number\n /** True if this is a page or paragraph range */\n isRange: boolean\n /** True when the pincite uses star-pagination (e.g., \"*2\"), denoting a\n * slip-opinion page or unreported-decision page rather than a reporter page.\n * Common on NY Slip Op, Westlaw, and Lexis citations. */\n starPage?: boolean\n /** Paragraph number for `¶ N` / `para. N` pincites (#204). */\n paragraph?: number\n /** End paragraph for `¶¶ N-M` / `paras. N-M` pincites (#204). */\n endParagraph?: number\n /** Additional discrete pincites following the primary one (#247). E.g.,\n * `410 U.S. 113, 115, 153` → first pincite is page=115, additionalPincites\n * is `[{ page: 153, ... }]`. Each entry is a full `PinciteInfo` so ranges\n * / footnotes / star-pages inside the comma chain are preserved\n * (`115, 105-110` → additional has `endPage` set). The top-level\n * convenience `pincite: number` field on the citation continues to mirror\n * only the primary pincite; consumers needing all pincites read this array. */\n additionalPincites?: PinciteInfo[]\n /** Original text before parsing */\n raw: string\n}\n\n/** Paragraph-marker prefix: `¶`, `¶¶`, `para.`, `paras.` with optional leading\n * `at`. Routes the rest of the string into paragraph parsing. (#204) */\nconst PARA_PREFIX_REGEX = /^(?:at\\s+)?(?:¶¶?|paras?\\.?)\\s*/i\n\n/** Body of a paragraph pincite once the marker has been consumed: `N` or `N-M`. */\nconst PARA_NUM_REGEX = /^(\\d+)(?:\\s*[-–—]\\s*(\\d+))?\\s*$/\n\n/** Matches: optional \"at \", optional \"*\" (star pagination), digits, optional\n * \"-/–/—[*]digits\", optional \"n./nn./note digits\" with optional range end.\n * The footnote separator accepts a comma+space variant (`, fn. 3` — common\n * in California opinions, #311) in addition to the canonical whitespace\n * separator. `fn` / `fns` are recognized alongside `n` / `nn` / `note`. */\nconst PINCITE_PARSE_REGEX =\n /^(?:at\\s+)?(\\*?)(\\d+)(?:[-–—]\\*?(\\d+))?(?:\\s*,)?\\s*(?:(?:nn?|fns?|note)\\s*\\.?\\s*(\\d+)(?:[-–—](\\d+))?)?$/i\n\n/**\n * Parse a pincite string into structured components.\n *\n * Handles simple pages, ranges (with abbreviated end pages),\n * footnote references, and \"at\" prefixes.\n *\n * @example\n * parsePincite(\"570\") // { page: 570, isRange: false, raw: \"570\" }\n * parsePincite(\"570-75\") // { page: 570, endPage: 575, isRange: true, raw: \"570-75\" }\n * parsePincite(\"570 n.3\") // { page: 570, footnote: 3, isRange: false, raw: \"570 n.3\" }\n *\n * @returns Parsed pincite info, or null if unparseable\n */\nexport function parsePincite(raw: string): PinciteInfo | null {\n const trimmed = raw.trim()\n if (!trimmed) return null\n\n // Paragraph-marker pincite (`¶ 12`, `¶¶ 12-14`, `para. 12`, `paras. 12-14`).\n // Checked first because the page parser would reject these forms anyway. (#204)\n const paraPrefix = PARA_PREFIX_REGEX.exec(trimmed)\n if (paraPrefix) {\n const rest = trimmed.substring(paraPrefix[0].length)\n const numMatch = PARA_NUM_REGEX.exec(rest)\n if (numMatch) {\n const paragraph = Number.parseInt(numMatch[1], 10)\n const endParagraph = numMatch[2]\n ? Number.parseInt(numMatch[2], 10)\n : undefined\n const result: PinciteInfo = {\n paragraph,\n isRange: endParagraph !== undefined,\n raw: trimmed,\n }\n if (endParagraph !== undefined) result.endParagraph = endParagraph\n return result\n }\n // Falls through to page parsing if the body isn't a clean number — defensive.\n }\n\n const match = PINCITE_PARSE_REGEX.exec(trimmed)\n if (!match) return null\n\n const starPrefix = match[1]\n const pageRaw = match[2]\n const endRaw = match[3]\n const footnoteRaw = match[4]\n const footnoteEndRaw = match[5]\n const page = Number.parseInt(pageRaw, 10)\n\n let endPage: number | undefined\n let isRange = false\n\n if (endRaw) {\n isRange = true\n const endNum = Number.parseInt(endRaw, 10)\n // Handle abbreviated end pages: \"570-75\" means 575\n if (endRaw.length < pageRaw.length) {\n const prefix = pageRaw.slice(0, pageRaw.length - endRaw.length)\n endPage = Number.parseInt(prefix + endRaw, 10)\n } else {\n endPage = endNum\n }\n }\n\n const footnote = footnoteRaw ? Number.parseInt(footnoteRaw, 10) : undefined\n const footnoteEnd = footnoteEndRaw ? Number.parseInt(footnoteEndRaw, 10) : undefined\n\n const result: PinciteInfo = { page, isRange, raw: trimmed }\n if (endPage !== undefined) result.endPage = endPage\n if (footnote !== undefined) result.footnote = footnote\n if (footnoteEnd !== undefined) result.footnoteEnd = footnoteEnd\n if (starPrefix === \"*\") result.starPage = true\n\n return result\n}\n","/**\n * Normalize a court string extracted from a citation parenthetical.\n *\n * - Collapses spaces after periods: \"S.D. N.Y.\" → \"S.D.N.Y.\"\n * - Ensures trailing period on abbreviated forms: \"2d Cir\" → \"2d Cir.\"\n * - Returns undefined for empty/undefined input\n *\n * @example\n * normalizeCourt(\"S.D. N.Y.\") // \"S.D.N.Y.\"\n * normalizeCourt(\"2d Cir\") // \"2d Cir.\"\n * normalizeCourt(\"U.S.\") // \"U.S.\"\n */\nexport function normalizeCourt(court: string | undefined): string | undefined {\n if (!court || !court.trim()) return undefined\n\n let normalized = court.trim()\n\n // Collapse spaces after periods before letters: \"S.D. N.Y.\" → \"S.D.N.Y.\", \"D. del.\" → \"D.del.\"\n normalized = normalized.replace(/\\.\\s+(?=[A-Za-z])/g, \".\")\n\n // Ensure trailing period on abbreviated forms that end with a letter\n // Only add period when the string contains a period (abbreviation) or\n // starts with ordinal+word (e.g., \"2d Cir\", \"9th Cir\")\n if (\n /[A-Za-z]$/.test(normalized) &&\n (/\\./.test(normalized) || /^\\d+\\w*\\s+[A-Z]/i.test(normalized))\n ) {\n normalized += \".\"\n }\n\n return normalized\n}\n","/**\n * Case Citation Extraction\n *\n * Parses tokenized case citations to extract volume, reporter, page, and\n * optional metadata (pincite, court, year). This is the third stage of\n * the parsing pipeline:\n * 1. Clean text (remove HTML, normalize Unicode)\n * 2. Tokenize (apply patterns to find candidates)\n * 3. Extract (parse metadata, validate) ← THIS MODULE\n *\n * Extraction parses structured data from token text. Validation against\n * reporters-db happens in Phase 3 (resolution layer).\n *\n * @module extract/extractCase\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type {\n CitationSignal,\n FullCaseCitation,\n HistorySignal,\n Parenthetical,\n ParentheticalType,\n SubsequentHistoryEntry,\n} from \"@/types/citation\"\nimport {\n resolveOriginalSpan,\n spanFromGroupIndex,\n type Span,\n type TransformationMap,\n} from \"@/types/span\"\nimport type { CaseComponentSpans } from \"@/types/componentSpans\"\nimport { parseDate, type StructuredDate } from \"./dates\"\nimport { getReportersSync } from \"@/data/reportersCache\"\nimport { inferCourtFromReporter } from \"./courtInference\"\nimport { parsePincite, type PinciteInfo } from \"./pincite\"\nimport { normalizeCourt } from \"./courtNormalization\"\n\n/** Valid CitationSignal values for safe validation after regex capture + normalization. */\nconst VALID_SIGNALS = new Set([\n \"see\",\n \"see also\",\n \"see generally\",\n \"cf\",\n \"but see\",\n \"but cf\",\n \"compare\",\n \"accord\",\n \"contra\",\n // Combined `, e.g.` forms (Bluebook Rule 1.3) — must be matched by SIGNAL_PATTERNS\n // in detectStringCites.ts before the bare-signal forms (#239).\n \"e.g.\",\n \"see, e.g.\",\n \"see also, e.g.\",\n \"but see, e.g.\",\n \"cf., e.g.\",\n \"but cf., e.g.\",\n])\n\n/**\n * Regex matching any VALID_SIGNALS entry at the start of a string, followed by whitespace.\n * Derived from VALID_SIGNALS to ensure a single source of truth.\n * Multi-word signals are listed first so \"See also\" matches before \"See\".\n * The trailing `,?` accommodates combined `, e.g.` signals (Bluebook Rule 1.3)\n * whose source-text form has a trailing comma between the signal and citation.\n */\nconst SIGNAL_STRIP_REGEX = (() => {\n const sorted = [...VALID_SIGNALS].sort((a, b) => b.length - a.length)\n const alternatives = sorted.map((s) => s.replace(/\\s+/g, \"\\\\s+\").replace(/\\./g, \"\\\\.\"))\n return new RegExp(`^(${alternatives.join(\"|\")}),?\\\\s+`, \"i\")\n})()\n\n/** Parse a volume string as number when purely numeric, string when hyphenated */\nfunction parseVolume(raw: string): number | string {\n const num = Number.parseInt(raw, 10)\n return String(num) === raw ? num : raw\n}\n\n/** Month abbreviations and full names found in legal citation parentheticals */\nconst MONTH_PATTERN =\n /(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec|January|February|March|April|May|June|July|August|September|October|November|December)\\.?/\n\n// ============================================================================\n// Compiled regex patterns for performance (hoisted to module level)\n// ============================================================================\n\n/** Cached current year to avoid Date allocation per extraction call. */\nconst CURRENT_YEAR = new Date().getFullYear()\n\n/** Common US reporters for confidence boost. Exact match to avoid substring false positives.\n * Shared across extractCase and extractShortForms.\n *\n * Future editions are pre-registered defensively (#234) so the eventual rollout\n * of F.5th / N.E.4th / etc. does not silently regress confidence scores. The\n * generalized federal-reporter regex captures these formats; this set ensures\n * they earn the +0.3 reporter-match boost out of the box. */\nexport const COMMON_REPORTERS: ReadonlySet<string> = new Set([\n \"F.\",\n \"F.2d\",\n \"F.3d\",\n \"F.4th\",\n \"F.5th\",\n \"F.6th\",\n \"F.7th\",\n \"U.S.\",\n \"S. Ct.\",\n \"L. Ed.\",\n \"L. Ed. 2d\",\n \"L. Ed. 3d\",\n \"P.\",\n \"P.2d\",\n \"P.3d\",\n \"P.4th\",\n \"A.\",\n \"A.2d\",\n \"A.3d\",\n \"A.4th\",\n \"N.E.\",\n \"N.E.2d\",\n \"N.E.3d\",\n \"N.E.4th\",\n \"N.W.\",\n \"N.W.2d\",\n \"N.W.3d\",\n \"S.E.\",\n \"S.E.2d\",\n \"S.E.3d\",\n \"S.W.\",\n \"S.W.2d\",\n \"S.W.3d\",\n \"S.W.4th\",\n \"So.\",\n \"So. 2d\",\n \"So. 3d\",\n \"So. 4th\",\n \"F. Supp.\",\n \"F. Supp. 2d\",\n \"F. Supp. 3d\",\n \"F. Supp. 4th\",\n \"F. Supp. 5th\",\n \"F. Supp. 6th\",\n \"F. App'x\",\n])\n\n/** Matches volume-reporter-page format in citation core, with optional nominative reporter parenthetical.\n * Reporter character class includes `&` so the BIA `I&N Dec.` / `I. & N. Dec.`\n * variants parse correctly (#244). */\nconst VOLUME_REPORTER_PAGE_REGEX =\n /^(\\d+(?:-\\d+)?)\\s+([A-Za-z0-9.\\s'&]+)\\s+(?:\\((\\d+)\\s+([A-Z][A-Za-z.]+)\\)\\s+)?(\\d+|_{3,}|-{3,})/d\n\n/** Detects blank page placeholders (3+ underscores or dashes) */\nconst BLANK_PAGE_REGEX = /^[_-]{3,}$/\n\n/** Extracts pincite (page reference after comma). Accepts optional \"at \"\n * keyword, optional \"*\" prefix for star-pagination (NY Slip Op, Westlaw,\n * Lexis, and other slip-opinion citations; see #191), and an optional\n * trailing footnote suffix \" n.14\" / \" nn.14-15\" (see #202). */\nconst PINCITE_REGEX =\n /,\\s*(?:at\\s+)?(\\*?\\d+(?:-\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?)/d\n\n/** Matches parenthetical content */\nconst PAREN_REGEX = /\\(([^)]+)\\)/\n\n/** Look-ahead pattern for parenthetical after token. Skips pincite text\n * (including star-pagination) before the court/year parenthetical. */\nconst LOOKAHEAD_PAREN_REGEX =\n /^(?:,\\s*(?:at\\s+)?\\*?\\d+(?:-\\d+)?)*(?:\\s+(?:n|note)\\s*\\.?\\s*\\d+)?\\s*\\(([^)]+)\\)/\n\n/** Extracts pincite from look-ahead text.\n * Accepts five prefix forms:\n * - \", 125\" (comma-separated, numeric)\n * - \", at *1\" (comma + \"at\" keyword; common with star-pagination)\n * - \" at *2\" (whitespace + \"at\" keyword; NY Slip Op repeat form)\n * - \", at p. 115\" (CSM form with `p.` / `pp.` prefix; #236)\n * - \", ¶ 12\" (paragraph-marker form; #204)\n * The \"*\" prefix marks star-pagination (#191); a trailing \" n.14\" /\n * \" nn.14-15\" footnote suffix is captured when present (#202). Paragraph\n * forms (`¶ N` / `¶¶ N-M` / `para. N` / `paras. N-M`) are accepted in the\n * capture; `parsePincite` routes them to the `paragraph` field (#204). */\n// Parallel-cite disambiguation: a real pincite is bounded by end-of-string,\n// sentence punctuation, a paren or bracket close, or whitespace NOT followed\n// by a capital letter (which would start a parallel cite's reporter token,\n// e.g., `, 198 A. 154` or `, 93 S. Ct. 705`). The anchored positive lookahead\n// prevents regex backtracking into shorter digit prefixes.\n//\n// Footnote suffix (#311): the suffix-bearing forms `n.3`, `note 3` accept\n// either `\\s+` (the original `768 n.3` form) or `,\\s+` (the California\n// `768, fn. 3` form). `fn` / `fns` are added to the alternation alongside\n// `n` / `nn` / `note`.\nconst LOOKAHEAD_PINCITE_REGEX =\n /^(?:\\s+at\\s+(?:pp?\\.\\s*)?|,\\s*(?:at\\s+(?:pp?\\.\\s*)?)?)(\\*?\\d+(?:-\\d+)?(?:(?:\\s+|,\\s+)(?:nn?|fns?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?|¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?)(?=$|[.,;)(\\]]|\\s(?![A-Z]))/d\n\n/** Citation boundary pattern (digit-period-space) */\nconst CITATION_BOUNDARY_REGEX = /\\d\\.\\s+/g\n\n/** Whitespace/comma skip pattern for parenthetical scanning */\nconst PAREN_SKIP_REGEX = /[\\s,]/\n\n/** Additional discrete pincite (`, NNN` continuation) after the primary\n * pincite has been consumed (#247). Matches a comma + optional whitespace\n * followed by a pincite body. Used in a loop after `LOOKAHEAD_PINCITE_REGEX`\n * to collect `115, 153, 200` chains.\n *\n * Excludes paragraph forms (`¶ 12` mixed with page numbers is exceedingly\n * rare and would conflict with the citation core's lookahead boundary). */\n// Parallel-cite disambiguation: tighten the trailing whitespace branch to\n// reject `\\s+[A-Z]` (a parallel-cite reporter token). Allow bracket close\n// `]` as a terminator so bracketed parallel pincites still capture.\nconst ADDITIONAL_PINCITE_REGEX =\n /^,\\s*(\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?)(?=$|[.,;)(\\]]|\\s(?![A-Z]))/\n\n/** Pincite text that appears between core citation and parentheticals.\n * Matches: comma-separated page numbers/ranges and optional note refs.\n * E.g., \", 199 n.2\", \", 999-1000\", \", 130 n.5\", \", at p. 115\" (CSM, #236),\n * \", ¶ 12\" / \", paras. 12-14\" (paragraph form, #204).\n * The outer `+` is intentionally greedy to handle multi-pincite citations\n * (e.g., \", 199, 205, 210\"). Safe because the scan window is bounded by maxLookahead. */\nconst PINCITE_SKIP_REGEX =\n /^(?:,\\s*(?:(?:at\\s+(?:pp?\\.\\s*)?)?\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:n|note)\\s*\\.?\\s*\\d+)?|(?:at\\s+)?(?:¶¶?|paras?\\.?)\\s*\\d+(?:[-–—]\\d+)?))+/\n\n/**\n * Signal normalization table. Longer patterns first so \"aff'd on other grounds\"\n * matches before \"aff'd\". Each entry: [regex, normalized HistorySignal].\n */\nconst SIGNAL_TABLE: ReadonlyArray<readonly [RegExp, HistorySignal]> = [\n // affirmed (longer variants first)\n [/^aff'?d\\s+on\\s+other\\s+grounds\\b/i, \"affirmed\"],\n [/^affirmed\\s+on\\s+other\\s+grounds\\b/i, \"affirmed\"],\n [/^aff'?d\\b/i, \"affirmed\"],\n [/^affirmed\\b/i, \"affirmed\"],\n // reversed\n [/^rev'?d\\s+and\\s+remanded\\b/i, \"reversed\"],\n [/^rev'?d\\s+on\\s+other\\s+grounds\\b/i, \"reversed\"],\n [/^reversed\\s+and\\s+remanded\\b/i, \"reversed\"],\n [/^rev'?d\\b/i, \"reversed\"],\n [/^reversed\\b/i, \"reversed\"],\n // cert denied\n [/^certiorari\\s+denied\\b/i, \"cert_denied\"],\n [/^cert\\.\\s*den(ied|\\.)(?=[\\s,;(]|$)/i, \"cert_denied\"],\n // cert granted\n [/^certiorari\\s+granted\\b/i, \"cert_granted\"],\n [/^cert\\.\\s*granted\\b/i, \"cert_granted\"],\n // overruled\n [/^overruled\\s+by\\b/i, \"overruled\"],\n [/^overruled\\s+in\\b/i, \"overruled\"],\n [/^overruling\\b/i, \"overruled\"],\n [/^overruled\\b/i, \"overruled\"],\n // vacated\n [/^vacated\\s+by\\b/i, \"vacated\"],\n [/^vacated\\b/i, \"vacated\"],\n // remanded\n [/^remanded\\s+for\\s+reconsideration\\b/i, \"remanded\"],\n [/^remanded\\b/i, \"remanded\"],\n // modified\n [/^modified\\s+by\\b/i, \"modified\"],\n [/^modified\\b/i, \"modified\"],\n // abrogated\n [/^abrogated\\s+by\\b/i, \"abrogated\"],\n [/^abrogated\\s+in\\b/i, \"abrogated\"],\n [/^abrogated\\b/i, \"abrogated\"],\n // additional signals — CA-specific \"superseded by grant of review\" precedes\n // the bare \"superseded by\" so alternation prefers the more specific match.\n [/^superseded\\s+by\\s+grant\\s+of\\s+review\\b/i, \"superseded_by_grant_of_review\"],\n [/^superseded\\s+by\\b/i, \"superseded\"],\n [/^superseded\\b/i, \"superseded\"],\n // CA-specific \"disapproved on other grounds\" precedes the bare/of forms\n // so alternation prefers the more specific match (#238).\n [/^disapproved\\s+on\\s+other\\s+grounds\\b/i, \"disapproved_other_grounds\"],\n [/^disapproved\\s+of\\b/i, \"disapproved\"],\n [/^disapproved\\b/i, \"disapproved\"],\n [/^questioned\\s+by\\b/i, \"questioned\"],\n [/^questioned\\b/i, \"questioned\"],\n [/^distinguished\\s+by\\b/i, \"distinguished\"],\n [/^distinguished\\b/i, \"distinguished\"],\n [/^withdrawn\\b/i, \"withdrawn\"],\n [/^reinstated\\b/i, \"reinstated\"],\n // Federal rehearing history (#246). `as modified on denial of rehearing`\n // (CA compound, listed later) anchors on `^as modified` so the bare\n // `reh'g denied` / `rehearing denied` entries here do not conflict.\n [/^reh'?g\\s+denied\\b/i, \"rehearing_denied\"],\n [/^rehearing\\s+denied\\b/i, \"rehearing_denied\"],\n [/^reh'?g\\s+granted\\b/i, \"rehearing_granted\"],\n [/^rehearing\\s+granted\\b/i, \"rehearing_granted\"],\n // Texas writ-of-error history (Tex. R. App. P. 47.7, pre-Sept. 1997).\n // Longer disposition modifiers must precede the bare forms so alternation\n // picks the more specific match (#229).\n [/^writ\\s+ref'?d\\s+n\\.r\\.e\\./i, \"writ_refused\"],\n [/^writ\\s+ref'?d\\s+w\\.m\\.j\\./i, \"writ_refused\"],\n [/^writ\\s+ref'?d\\b/i, \"writ_refused\"],\n [/^writ\\s+dism'?d\\s+w\\.o\\.j\\./i, \"writ_dismissed\"],\n [/^writ\\s+dism'?d\\b/i, \"writ_dismissed\"],\n [/^writ\\s+denied\\b/i, \"writ_denied\"],\n [/^writ\\s+granted\\b/i, \"writ_granted\"],\n [/^no\\s+writ\\b/i, \"no_writ\"],\n // Texas petition history (post-Sept. 1997).\n [/^pet\\.\\s+ref'?d\\b/i, \"pet_refused\"],\n [/^pet\\.\\s+denied\\b/i, \"pet_denied\"],\n [/^pet\\.\\s+dism'?d\\b/i, \"pet_dismissed\"],\n [/^pet\\.\\s+granted\\b/i, \"pet_granted\"],\n [/^pet\\.\\s+filed\\b/i, \"pet_filed\"],\n [/^no\\s+pet\\.\\s+h\\./i, \"no_pet\"],\n [/^no\\s+pet\\./i, \"no_pet\"],\n // California Supreme Court review history (#238). Bluebook T8 only covers\n // federal cert. denied/granted — these CA-specific forms appear in Cal.,\n // Cal.App., and federal opinions citing CA cases.\n [/^review\\s+den(?:ied|\\.)/i, \"review_denied\"],\n [/^review\\s+granted\\b/i, \"review_granted\"],\n [/^opinion\\s+vacated\\b/i, \"opinion_vacated\"],\n // CA Tier 1 research additions (2026-05-11). Longer disposition modifiers\n // precede the bare forms so alternation prefers the more specific match.\n // (`superseded_by_grant_of_review` is placed earlier in SIGNAL_TABLE next to\n // the bare `superseded by` entry — see comment there.)\n [/^petition\\s+for\\s+review\\s+filed\\b/i, \"petition_for_review_filed\"],\n [/^petition\\s+for\\s+review\\s+granted\\b/i, \"petition_for_review_granted\"],\n [/^petition\\s+for\\s+review\\s+denied\\b/i, \"petition_for_review_denied\"],\n [/^as\\s+modified\\s+on\\s+denial\\s+of\\s+rehearing\\b/i, \"modified_on_denial_of_rehearing\"],\n // Depublication signals — order: longest-first\n [/^ordered\\s+not\\s+pub\\.?/i, \"not_published\"],\n [/^not\\s+for\\s+publication\\b/i, \"not_published\"],\n [/^nonpubl?\\.?\\s+opn\\.?/i, \"not_published\"],\n]\n\n/**\n * Match a string against SIGNAL_TABLE and return the normalized signal + match length.\n * Returns undefined if the string doesn't start with a known signal.\n */\nfunction normalizeSignal(raw: string): { signal: HistorySignal; matchLength: number } | undefined {\n for (const [regex, signal] of SIGNAL_TABLE) {\n const match = regex.exec(raw)\n if (match) {\n return { signal, matchLength: match[0].length }\n }\n }\n return undefined\n}\n\n/** Signal words that identify explanatory parentheticals */\nconst SIGNAL_WORDS: ReadonlySet<string> = new Set([\n \"holding\",\n \"finding\",\n \"stating\",\n \"noting\",\n \"explaining\",\n \"quoting\",\n \"citing\",\n \"discussing\",\n \"describing\",\n \"recognizing\",\n \"applying\",\n \"rejecting\",\n \"adopting\",\n \"requiring\",\n])\n\n/** Type guard: validates a string is a known signal word */\nfunction isSignalWord(word: string): word is ParentheticalType {\n return SIGNAL_WORDS.has(word)\n}\n\n/** Matches a leading word (used to extract signal word candidate) */\nconst LEADING_WORD_REGEX = /^([a-z]+)\\b/i\n\n/** Standard \"v.\" or \"vs.\" case name format.\n *\n * The trailing alternation accepts either a comma (Bluebook form:\n * `Smith v. Jones, 50 Cal.3d 100 (Cal. 1990)`) or a year paren (California\n * Style Manual year-first form: `Smith v. Jones (2d Cir. 2005) 396 F.3d 96`\n * / `Smith v. Jones (1990) 50 Cal.3d 100`). The CSM paren may carry an\n * optional court abbreviation before the year — `(2d Cir. 2005)`,\n * `(N.Y. 1991)` — which the caller routes to `precedingDocketMeta.court`.\n * The court text must contain a period so loose forms like `(March 1991)`\n * don't get misread as courts (Bluebook T7 court abbreviations all contain\n * at least one period). Capture group 3 = court (optional), 4 = year.\n * The `d` flag enables `match.indices` so the caller can compute a year\n * span. See #19, #293. */\nconst V_CASE_NAME_REGEX =\n /([A-Z][A-Za-z0-9\\s.,'&()/-]+?)\\s+v(?:s)?\\.?\\s+([A-Za-z0-9\\s.,'&()/-]+?)\\s*(?:,|\\((?:([^)]*?\\.[^)]*?)\\s+)?(\\d{4})\\))\\s*$/d\n\n/** Procedural prefix case name format.\n * Longer prefixes listed first so the alternation prefers the longer match\n * (e.g., `In the Matter of the Liquidation of X` beats `In the Matter of X`,\n * `In re Marriage of X` beats `In re X`, `Commonwealth of Puerto Rico ex rel.`\n * beats `Commonwealth ex rel.`). See #193, #242, and the six 2026-05-11\n * procedural-prefix research dispatches in `docs/research/`.\n *\n * The trailing alternation matches either `,` (Bluebook) or\n * `((<court>)? <year>)` (CSM year-first form, #19 / #293). Captures:\n * 1: prefix word, 2: party body, 3: court (optional), 4: year.\n * The court text must contain a period so loose forms like `(March 1991)`\n * don't get misread as courts. The `d` flag enables `match.indices`. */\nconst PROCEDURAL_PREFIX_REGEX =\n /\\b(In\\s+the\\s+Matter\\s+of\\s+the\\s+Liquidation\\s+of|In\\s+the\\s+Matter\\s+of\\s+the\\s+Rehabilitation\\s+of|In\\s+the\\s+Matter\\s+of\\s+the\\s+Receivership\\s+of|In\\s+the\\s+Matter\\s+of\\s+the\\s+Extradition\\s+of|In\\s+the\\s+Matter\\s+of\\s+the\\s+Application\\s+of|In\\s+the\\s+Matter\\s+of\\s+the\\s+Welfare\\s+of|In\\s+the\\s+Matter\\s+of|In\\s+re\\s+Petition\\s+for\\s+Naturalization\\s+of|In\\s+re\\s+Termination\\s+of\\s+Parental\\s+Rights\\s+as\\s+to|In\\s+re\\s+Termination\\s+of\\s+Parental\\s+Rights\\s+to|In\\s+re\\s+Termination\\s+of\\s+Parental\\s+Rights\\s+of|In\\s+re\\s+Marriage\\s+of|In\\s+re\\s+Liquidation\\s+of|In\\s+re\\s+Rehabilitation\\s+of|In\\s+re\\s+Receivership\\s+of|In\\s+re\\s+Naturalization\\s+of|In\\s+re\\s+Extradition\\s+of|In\\s+re\\s+Application\\s+of|In\\s+re\\s+Welfare\\s+of|In\\s+re\\s+Dependency\\s+of|In\\s+re\\s+Paternity\\s+of|In\\s+re\\s+Parentage\\s+of|In\\s+re\\s+Conservatorship\\s+of|In\\s+re\\s+Guardianship\\s+of|In\\s+re\\s+Adoption\\s+of|In\\s+the\\s+Interest\\s+of|Matter\\s+of\\s+Liquidation\\s+of|Matter\\s+of\\s+Rehabilitation\\s+of|Commonwealth\\s+of\\s+Puerto\\s+Rico\\s+ex\\s+rel\\.|Government\\s+of\\s+the\\s+Virgin\\s+Islands\\s+ex\\s+rel\\.|Commonwealth\\s+ex\\s+rel\\.|Petition\\s+for\\s+Naturalization\\s+of|People\\s+ex\\s+rel\\.|District\\s+of\\s+Columbia\\s+ex\\s+rel\\.|Conservatorship\\s+of\\s+the\\s+Person\\s+and\\s+Estate\\s+of|Conservatorship\\s+of\\s+the\\s+Person\\s+of|Conservatorship\\s+of\\s+the\\s+Estate\\s+of|Inquiry\\s+Concerning\\s+Judge|Appeal\\s+of|Care\\s+and\\s+Protection\\s+of|Succession\\s+of|In re|Ex parte|Matter of|Estate of|State ex rel\\.|United States ex rel\\.|Application of|On Petition of|Petition of|Adoption of|Conservatorship of|Guardianship of)\\s+([A-Za-z0-9\\s.,'&()/-]+?)\\s*(?:,|\\((?:([^)]*?\\.[^)]*?)\\s+)?(\\d{4})\\))\\s*$/id\n\n/**\n * Lowercase words that legitimately appear in legal party names.\n * Articles, prepositions, and legal connectors (e.g., \"of\", \"the\", \"ex\", \"rel\").\n * Used to distinguish real party names from sentence context captured by the regex.\n *\n * Note: \"in\" is intentionally NOT a connector. It's overwhelmingly a prose\n * preposition (\"the holding in\", \"the rule announced in\") rather than a\n * party-name internal token. Treating \"in\" as a connector lets lead-in\n * clauses bleed into the captured plaintiff (#223). Procedural \"In re\"\n * captions go through PROCEDURAL_PREFIX_REGEX instead.\n */\nconst PARTY_NAME_CONNECTORS = new Set([\n \"of\",\n \"the\",\n \"and\",\n \"for\",\n \"on\",\n \"by\",\n \"a\",\n \"an\",\n \"to\",\n \"at\",\n \"as\",\n \"de\",\n \"la\",\n \"el\",\n \"del\",\n \"von\",\n \"van\",\n \"ex\",\n \"rel\",\n \"et\",\n \"al\",\n \"d\",\n \"or\",\n])\n\n/**\n * Internal qualifier markers that appear inside legitimate party names\n * (e.g., \"Smith d/b/a Old Bob's Diner v. Jones\", \"Jones aka Johnson v. Smith\").\n * When such a marker is present, the plaintiff is correctly anchored at its\n * first word — even if that word is followed by lowercase non-connector\n * tokens. Without this signal, the firstWordIsProperName guard incorrectly\n * preserves lead-in prose (#223).\n */\nconst INTERNAL_QUALIFIER_REGEX = /\\b(?:d\\/?b\\/?a|a\\/?k\\/?a|f\\/?k\\/?a|n\\/?k\\/?a)\\b/i\n\n/**\n * Check whether a string looks like a legal party name vs. sentence context.\n *\n * Valid party names consist of capitalized words and legal connectors:\n * \"Smith\" ✓, \"United States\" ✓, \"People of the State of New York\" ✓\n *\n * Sentence context contains lowercase non-connector words (verbs, nouns):\n * \"The court cited Smith\" ✗ (\"court\", \"cited\" are not connectors)\n */\nfunction isLikelyPartyName(name: string): boolean {\n const words = name.split(/\\s+/)\n // Reject names whose first word is a sentence-initial transition word\n // (`Invoking Younger`, `Citing Pederson`, `Under People`). These pass\n // the all-capitalized-words check below because every word starts capital,\n // but the first word is prose, not a party name. (#323)\n const firstWord = words[0] ?? \"\"\n const firstWordClean = firstWord.toLowerCase().replace(/[.,']+$/, \"\")\n if (SENTENCE_INITIAL_WORDS.has(firstWordClean)) return false\n for (const word of words) {\n if (!word) continue\n // Standalone ampersand is ubiquitous in corporate captions\n // (\"Smith & Jones\", \"Goldman, Sachs & Co.\").\n if (word === \"&\") continue\n // Strip trailing punctuation for comparison (handles \"Inc.\", \"Corp.,\")\n const clean = word.toLowerCase().replace(/[.,']+$/, \"\")\n if (PARTY_NAME_CONNECTORS.has(clean)) continue\n if (/^[A-Z]/.test(word)) continue\n // Numeric words are valid in party names (e.g., \"Doe No. 2\", \"Route 66\")\n if (/^\\d/.test(word)) continue\n // Lowercase non-connector word → not a party name\n return false\n }\n return true\n}\n\n/**\n * Capitalized words that are never proper names — only uppercase because they're\n * sentence-initial. Prevents the firstWordIsProperName guard from treating\n * \"This landmark decision...\" or \"Those cases...\" as party-name-anchored text.\n *\n * Includes citation-introducing transition words (#323): `Under`, `Invoking`,\n * `Citing`, `Following`, `Unlike`, `Whereas`, `Pursuant`, `Applying`. These\n * appear at the start of sentences that introduce a citation and get\n * incorrectly captured as part of the plaintiff name by V_CASE_NAME_REGEX's\n * greedy lookback.\n */\nconst SENTENCE_INITIAL_WORDS = new Set([\n \"this\",\n \"that\",\n \"these\",\n \"those\",\n \"here\",\n \"there\",\n \"such\",\n \"its\",\n \"his\",\n \"her\",\n \"their\",\n \"our\",\n // Citation-introducing transition words (#323)\n \"under\",\n \"invoking\",\n \"citing\",\n \"following\",\n \"unlike\",\n \"whereas\",\n \"pursuant\",\n \"applying\",\n])\n\n/**\n * Strips date components (month, day, year) from parenthetical content\n * to isolate the court abbreviation.\n * E.g., \"2d Cir. Jan. 15, 2020\" → \"2d Cir.\"\n * \"C.D. Cal. Feb. 9, 2015\" → \"C.D. Cal.\"\n * \"D. Mass. Mar. 2020\" → \"D. Mass.\"\n * \"D. Mass. 1/15/2020\" → \"D. Mass.\"\n */\nfunction stripDateFromCourt(content: string): string | undefined {\n // Strip trailing numeric date format first (1/15/2020)\n let court = content.replace(/\\s*\\d{1,2}\\/\\d{1,2}\\/\\d{4}\\s*$/, \"\").trim()\n // Strip trailing year\n court = court.replace(/\\s*\\d{4}\\s*$/, \"\").trim()\n // Strip trailing date components: optional day+comma, month abbreviation or full name\n court = court.replace(/\\s*,?\\s*\\d{1,2}\\s*,?\\s*$/, \"\").trim()\n court = court.replace(new RegExp(`\\\\s*${MONTH_PATTERN.source}\\\\s*$`, \"i\"), \"\").trim()\n // Strip any trailing commas left over\n court = court.replace(/,\\s*$/, \"\").trim()\n return court && /[A-Za-z]/.test(court) ? court : undefined\n}\n\n// ============================================================================\n// Case-name boundary detection: abbreviation set + heuristics\n// ============================================================================\n\n/**\n * Comprehensive set of legal abbreviation stems (lowercase, without trailing period)\n * used to distinguish abbreviation periods from sentence-ending periods during\n * backward case-name scanning.\n *\n * Sources: Bluebook T6 (case name abbreviations), T7 (court abbreviations),\n * T10 (geographic abbreviations), plus common titles and corporate suffixes.\n */\nconst CASE_NAME_ABBREVS: ReadonlySet<string> = new Set([\n // ── Bluebook T6: Case name and institutional abbreviations ──\n \"acad\",\n \"acct\",\n \"accts\",\n \"admin\",\n \"adm\",\n \"advert\",\n \"advoc\",\n \"aff\",\n \"affs\",\n \"afr\",\n \"agric\",\n \"all\",\n \"alt\",\n \"am\",\n \"ann\",\n \"app\",\n \"arb\",\n \"assoc\",\n \"assocs\",\n \"atl\",\n \"auth\",\n \"auto\",\n \"ave\",\n \"bankr\",\n \"behav\",\n \"bd\",\n \"bor\",\n \"brit\",\n \"broad\",\n \"bhd\",\n \"bros\",\n \"bldg\",\n \"bull\",\n \"bus\",\n \"can\",\n \"cap\",\n \"cas\",\n \"cath\",\n \"ctr\",\n \"ctrs\",\n \"cent\",\n \"chem\",\n \"child\",\n \"chron\",\n \"coal\",\n \"coll\",\n \"com\",\n \"comm\",\n \"compar\",\n \"comp\",\n \"comput\",\n \"condo\",\n \"conf\",\n \"cong\",\n \"consol\",\n \"const\",\n \"constr\",\n \"cont\",\n \"coop\",\n \"corp\",\n \"corps\",\n \"corr\",\n \"cosm\",\n \"couns\",\n \"cntys\",\n \"cnty\",\n \"crim\",\n \"def\",\n \"delinq\",\n \"det\",\n \"dev\",\n \"dig\",\n \"dir\",\n \"disc\",\n \"disp\",\n \"distrib\",\n \"dist\",\n \"div\",\n \"econ\",\n \"educ\",\n \"elec\",\n \"emp\",\n \"eng\",\n \"enter\",\n \"enters\", // Enters. (Enterprises, plural of Bluebook T6 \"Enter.\") — common in NY/4th Dep't captions (\"Fields Enters. Inc.\"). #288 surfaced this gap.\n \"ent\",\n \"equal\",\n \"equip\",\n \"est\",\n \"eur\",\n \"exam\",\n \"exch\",\n \"exec\",\n \"expl\",\n \"exp\",\n \"fac\",\n \"fam\",\n \"fams\",\n \"fed\",\n \"fid\",\n \"fin\",\n \"found\",\n \"gen\",\n \"glob\",\n \"grp\",\n \"guar\",\n \"hist\",\n \"hosp\",\n \"hous\",\n \"hum\",\n \"immigr\",\n \"imp\",\n \"inc\",\n \"indem\",\n \"indep\",\n \"indus\",\n \"info\",\n \"inj\",\n \"inst\",\n \"ins\",\n \"intell\",\n \"intel\",\n \"int\",\n \"inv\",\n \"invs\",\n \"jurid\",\n \"just\",\n \"juv\",\n \"lab\",\n \"law\",\n \"liab\",\n \"ltd\",\n \"loc\",\n \"mach\",\n \"mag\",\n \"maint\",\n \"mgmt\",\n \"mgt\",\n \"mfr\",\n \"mfrs\",\n \"mfg\",\n \"mar\",\n \"mkt\",\n \"mktg\",\n \"matrim\",\n \"mech\",\n \"med\",\n \"merch\",\n \"metro\",\n \"min\",\n \"misc\",\n \"mod\",\n \"mortg\",\n \"mun\",\n \"mut\",\n \"nat\",\n \"negl\",\n \"negot\",\n \"nw\",\n \"no\",\n \"nos\",\n \"off\",\n \"org\",\n \"orgs\",\n \"pac\",\n \"pat\",\n \"pers\",\n \"pharm\",\n \"phil\",\n \"plan\",\n \"pol\",\n \"prac\",\n \"pres\",\n \"priv\",\n \"prob\",\n \"proc\",\n \"prod\",\n \"pro\",\n \"prop\",\n \"psych\",\n \"pub\",\n \"rec\",\n \"reg\",\n \"regul\",\n \"rehab\",\n \"rel\",\n \"rels\",\n \"rep\",\n \"reprod\",\n \"rsch\",\n \"rsrv\",\n \"resol\",\n \"res\",\n \"resp\",\n \"rest\",\n \"ret\",\n \"rd\",\n \"sav\",\n \"sch\",\n \"schs\",\n \"sci\",\n \"sec\",\n \"serv\",\n \"servs\",\n \"sess\",\n \"soc\",\n \"solic\",\n \"spec\",\n \"stat\",\n \"subcomm\",\n \"sur\",\n \"surv\",\n \"sys\",\n \"tchr\",\n \"tech\",\n \"telecomm\",\n \"tel\",\n \"temp\",\n \"twp\",\n \"transcon\",\n \"transp\",\n \"treas\",\n \"tr\",\n \"trs\",\n \"tpk\",\n \"unemplmt\",\n \"unif\",\n \"univ\",\n \"urb\",\n \"util\",\n \"veh\",\n \"vehs\",\n \"vill\",\n \"voc\",\n \"whse\",\n \"whol\",\n \"litig\",\n // ── T6: Directional abbreviations ──\n \"n\",\n \"s\",\n \"e\",\n \"w\",\n \"m\",\n \"ne\",\n \"se\",\n \"sw\",\n // ── T6/T10: Geographic features and street types ──\n // Appear mid-party-name as \"Long Is.\", \"Mt. Sinai\", \"Ft. Worth\", \"Stony Pt.\",\n // \"Route 66\" (Rt.), \"St. Paul\" / \"Main St.\", \"Wilshire Blvd.\", \"Times Sq.\",\n // \"Pacific Hwy.\", \"Grand Central Pkwy.\", \"Washington Hts.\". Without these,\n // the backward scanner treats \"Is. R\" / \"Mt. S\" as sentence boundaries and\n // truncates the case name. See #188.\n \"is\",\n \"mt\",\n \"ft\",\n \"pt\",\n \"rt\",\n \"st\",\n \"blvd\",\n \"sq\",\n \"hwy\",\n \"pkwy\",\n \"hts\",\n // ── T7: Court abbreviations ──\n \"v\",\n \"vs\",\n \"ct\",\n \"cir\",\n \"supp\",\n \"cl\",\n \"jud\",\n \"super\",\n \"sup\",\n \"magis\",\n \"mil\",\n \"terr\",\n // ── T10: US state abbreviations ──\n \"ala\",\n \"ariz\",\n \"ark\",\n \"cal\",\n \"colo\",\n \"conn\",\n \"del\",\n \"fla\",\n \"ga\",\n \"haw\",\n \"ida\",\n \"ill\",\n \"ind\",\n \"kan\",\n \"ky\",\n \"la\",\n \"me\",\n \"md\",\n \"mass\",\n \"mich\",\n \"minn\",\n \"miss\",\n \"mo\",\n \"mont\",\n \"neb\",\n \"nev\",\n \"okla\",\n \"or\",\n \"pa\",\n \"tenn\",\n \"tex\",\n \"vt\",\n \"va\",\n \"wash\",\n \"wis\",\n \"wyo\",\n // ── Titles and honorifics ──\n \"mr\",\n \"mrs\",\n \"ms\",\n \"dr\",\n \"jr\",\n \"sr\",\n \"prof\",\n \"rev\",\n \"hon\",\n \"sgt\",\n \"capt\",\n \"col\",\n \"lt\",\n // ── Other common legal abbreviations ──\n \"ed\",\n \"op\",\n \"ad\",\n \"dep\",\n \"ass\",\n \"ry\",\n // ── reporters-db alignment (Bluebook T6-derived, 19th ed) ──\n // Period-form abbreviations. Source: freelawproject/reporters-db\n // data/case_name_abbreviations.json. `co` (Co./Company) was the most\n // impactful gap — \"Smith & Co. United States Corp.\" was truncated to\n // just \"United States Corp.\" because the sentence-boundary scan fired\n // on \"Co. U\".\n \"co\",\n \"cmty\",\n \"cty\",\n \"envtl\",\n \"gend\",\n \"par\",\n \"prot\",\n \"ref\",\n \"sol\",\n \"adver\",\n // Apostrophe-form abbreviations. Stored as pure-letter stems because\n // isLikelyAbbreviationPeriod now strips all apostrophes/periods before\n // set lookup. These appear in nearly every NY appellate citation\n // (\"2d Dep't\", \"Nat'l\", \"Int'l\", \"Ass'n\", \"Gov't\", etc.).\n \"admr\",\n \"admx\",\n \"assn\",\n \"commcn\",\n \"commn\",\n \"commr\",\n \"contl\",\n \"dept\",\n \"empr\",\n \"empt\",\n \"engg\",\n \"engr\",\n \"entmt\",\n \"envt\",\n \"examr\",\n \"exr\",\n \"exx\",\n \"fedn\",\n \"govt\",\n \"intl\",\n \"invr\",\n \"meml\",\n \"natl\",\n \"profl\",\n \"pship\",\n \"publg\",\n \"publn\",\n \"regl\",\n \"secy\",\n \"sholder\",\n \"socy\",\n // ── Cornell § 4-100 / state-practice gaps not in Bluebook T6 source ──\n // Used in real case captions across multiple jurisdictions:\n // - \"Tp.\" (NJ alternative to Bluebook \"Twp.\" Township) —\n // \"Parsippany-Troy Hills Tp. Council\", \"Bernards Tp. v. ...\"\n // - \"Vil.\" (NY single-L variant of Bluebook \"Vill.\" Village) — #288\n // NY Reporter / Slip Opinion captions, esp. 4th Dep't:\n // \"Bristol Harbour Vil. Assn., Inc.\", \"Smithtown Vil. Bd.\"\n // - \"Tax'n\" (Taxation) — \"Dep't of Tax'n v. ...\"\n // - \"Enf't\" (Enforcement) — \"Drug Enf't Admin. v. ...\"\n // - \"Rts.\" (Rights) — \"Human Rts. Watch v. ...\", \"Civ. Rts. Div.\"\n \"tp\",\n \"vil\",\n \"taxn\",\n \"enft\",\n \"rts\",\n // ── 2026-05-10 jurisdiction-survey additions ──\n // Cross-agent research canvassing 15 jurisdictional clusters (NY/NJ, PA/DE/\n // MD/DC/WV, New England, CA, TX/OK, Southeast, Deep South, Great Lakes,\n // Western/Pacific, federal courts, federal specialty courts, govt agencies +\n // corporate entity forms, ALWD + Bluebook 21st + reporters-db sweep, and\n // foreign/tribal/territorial) plus a parser-quirks audit. Reports retained\n // in docs/research/2026-05-10-citation-abbrevs-*.md.\n //\n // Universal apostrophe-form + Bluebook BT1.2 party designations:\n \"atty\", // Att'y / Att'y Gen. — 32k+ corpus matches; every state + federal AG case\n \"attys\", // Att'ys (plural)\n \"petr\", // Pet'r — Bluebook 21st BT1.2 (habeas, immigration, PTAB captions)\n \"respt\", // Resp't — Bluebook 21st BT1.2 counterpart to Pet'r\n \"commrs\", // Comm'rs (plural of existing commr) — \"Bd. of Cnty. Comm'rs\"\n // Plurals of existing singular stems (modern LLC-era captions):\n \"hldgs\", // Hldgs. (Holdings) — DE Chancery, NY 1st Dep't, GA LLC\n \"hldg\", // Hldg. (singular)\n \"props\", // Props. — Lanvale Props. LLC (NC), Ryan Jackson Props.\n \"prods\", // Prods. (Products plural) — product-liability captions nationwide\n \"ents\", // Ents. (Enterprises plural) — \"NC Ents., L.L.C.\"\n \"invests\", //Invests. — Ohio \"A.A.A. Invests. v. Columbus\"\n \"scis\", // Scis. (Sciences plural)\n \"emps\", // Emps. — \"Okla. Pub. Emps. Ret. Sys.\", \"Pub. Emps. Rel. Comm'n\"\n \"sols\", // Sols. (Solutions plural) — modern LLC captions \"Med-Care Sols., LLC\"\n \"corrs\", // Corrs. (Corrections plural) — \"Ark. Bd. of Corrs.\"\n \"telecomms\", //Telecomms. (plural) — \"BellSouth Telecomms., Inc.\"\n \"examrs\", // Exam'rs (Examiners plural) — \"Med. Exam'rs Comm'n\", \"Bar Exam'rs\"\n \"cmtys\", // Cmtys. (Communities plural) — \"Fla. Cmtys. Tr.\"\n \"colls\", // Colls. (Colleges plural) — \"State Bd. of Cmty. Colls.\"\n \"cts\", // Cts. (Courts plural) — \"Off. of the St. Cts. Admin'r\"\n \"amends\", // Amends. (Amendments plural)\n // Standard institutional / agency abbreviations:\n \"civ\", // Civ. (Civil) — Ala. Civ. App., Civ. Rts. Div., Civ. Liberties Union\n \"enf\", // Enf. (Enforcement, distinct from existing enft) — \"Drug Enf. Admin.\"\n \"advis\", // Advis. (Advisory) — \"Advis. Council/Comm.\"\n \"utils\", // Utils. — \"Utils. Comm'n\", \"Pub. Utils. Comm'n\"\n \"lic\", // Lic. (License) — \"Bd. of License Comm'rs\" (Tiverton, 469 U.S. 238)\n \"bur\", // Bur. (Bureau) — \"Bur. of Driver Lic.\", \"Bur. of Land Mgmt.\"\n \"insp\", // Insp. (Inspection) — \"Bd. of Lic. & Insp. Review\"\n \"conserv\", //Conserv. (Conservation) — Bluebook 21st; 1.5k corpus matches\n \"retire\", // Retire. (Retirement) — \"W. Va. Consol. Pub. Retire. Bd.\" (distinct from ret)\n \"discipl\", //Discipl. (Disciplinary) — \"Lawyer Disciplinary Bd.\"\n \"supers\", // Supers. (Supervisors) — PA \"Twp. Bd. of Supers.\" (hundreds of captions)\n \"edn\", // Edn. (Ohio variant of Educ.) — \"Bd. of Edn.\"\n \"coun\", // Coun. (Council) — NLRB \"Dist. Council 9\", distinct from couns (Counsel)\n \"stds\", // Stds. (Standards) — \"Crim. Just. Stds. & Training Comm'n\"\n \"procs\", // Procs. (Procedures)\n \"quals\", // Quals. (Qualifications) — \"Jud. Quals. Comm'n\"\n // Regional / state-specific:\n \"boro\", // NJ \"Boro.\" — alternative long form to existing \"Bor.\" (Borough)\n \"commw\", // Commw. — PA Commonwealth Court (\"Pa. Commw. Ct.\")\n \"adv\", // Adv. (Advance) — NV \"Nev., Adv. Op.\" form\n \"comn\", // Com'n — Hawaii single-m variant of Comm'n\n \"irrig\", // Irrig. (Irrigation) — ID/WY/WA \"Pioneer Irrig. Dist.\"\n \"reclam\", // Reclam. (Reclamation) — federal-project captions\n \"rptr\", // Rptr. — CA \"Cal.Rptr.\" nested in bracketed parallel cites\n \"vet\", // Vet. (Veterans) — \"Vet. App.\", \"Sec'y of Vet. Aff.\"\n \"trib\", // Trib. — Tribune (Bluebook 21st T6) + Tribal Ct.\n \"adj\", // Adj. — Adjustment (VT/NH \"Zoning Bd. of Adj.\") + Adjudicatory (FL)\n \"vol\", // Vol. (Volunteer) — PA \"Univ. Vol. Fire Dept.\"; volume cites are pre-digit\n // Corporate entity forms:\n \"pty\", // Pty. — Australian \"Pty. Ltd.\"\n // Bluebook 21st ed. (2020) T6 / T13.2 merger additions:\n \"poly\", // Pol'y (Policy)\n \"stud\", // Stud. (Studies)\n \"libr\", // Libr. (Library)\n \"refin\", // Refin. (Refining) — distinct from existing ref (Referee/Reference)\n \"socio\", // Socio. (Sociology) — distinct from existing soc (Social)\n \"laby\", // Lab'y (Laboratory) — distinct from existing lab (Labor)\n \"naty\", // Nat'y (Nationality)\n \"wkly\", // Wkly. (Weekly)\n \"appx\", // App'x (Appendix) — \"F. App'x\" reporter\n // Plains + Upper Midwest (re-dispatch agent, report retained):\n \"comr\", // Comr. — Nebraska apostrophe-dropping single-m variant of Comm'r\n \"comrs\", // Comrs. — NE plural variant; \"Cherry Cty. Bd. of Comrs.\"\n \"reins\", // Reins. — Bluebook T6; \"Grinnell Mut. Reins. Co.\" (ND insurance)\n])\n\n/**\n * Detect whether a period at `dotIndex` in `text` is likely an abbreviation\n * rather than a sentence boundary.\n *\n * Three-tier check:\n * 1. Word stem is in the comprehensive CASE_NAME_ABBREVS set\n * 2. Single uppercase letter (initial: A., B., J., N.)\n * 3. Word contains internal periods (dotted initialism: N.Y., U.S., D.C.)\n */\nfunction isLikelyAbbreviationPeriod(text: string, dotIndex: number): boolean {\n // Walk backward from the period to find the word\n let start = dotIndex\n while (start > 0 && /[-A-Za-z.']/.test(text[start - 1])) {\n start--\n }\n const word = text.substring(start, dotIndex)\n if (!word) return false\n\n // Strip ALL periods and apostrophes for set lookup. This normalizes\n // apostrophe-form abbreviations (\"Ass'n\" → \"assn\", \"Dep't\" → \"dept\",\n // \"Nat'l\" → \"natl\") so the set can store pure-letter stems.\n const stem = word.replace(/['.]/g, \"\").toLowerCase()\n\n // Tier 1: Known legal abbreviation\n if (CASE_NAME_ABBREVS.has(stem)) return true\n\n // Tier 2: Single uppercase letter (initial)\n if (stem.length === 1 && /[a-z]/i.test(stem)) return true\n\n // Tier 3: Contains internal periods (dotted initialism like N.Y, U.S, D.C)\n if (/\\.[A-Za-z]/.test(word)) return true\n\n return false\n}\n\n/** Hard boundary: Id. citation marker — the scan must not cross this.\n * Case-sensitive: Bluebook convention is always capitalized \"Id.\" */\nconst ID_BOUNDARY_REGEX = /\\bId\\.\\s+/g\n\n/** Hard boundary: parenthetical signal words that introduce nested citations.\n * Matches opening paren + optional space + signal word (+ optional \", e.g.,\")\n * + whitespace.\n *\n * E.g., \"(quoting \", \"(citing \", \"(cited in \", \"(quoted in \", \"(accord \",\n * \"(citing, e.g., \". The optional `, e.g.[,]` tail handles the common form\n * where a citing parenthetical introduces multiple authorities. See #187. */\nconst PAREN_SIGNAL_BOUNDARY_REGEX =\n /\\(\\s*(?:quoting|citing|cited\\s+in|quoted\\s+in|accord|discussing|noting|explaining|describing|recognizing|applying|rejecting|adopting|requiring|overruling|overruled\\s+by|abrogated\\s+by)(?:,\\s*e\\.g\\.,?)?\\s+/gi\n\n/** Sentence boundary: closing paren or period, followed by space + uppercase\n * letter or open-paren. The `(` lookahead handles parenthesized citations\n * inside running prose — `... discretion. (Burquet v. Brumbaugh, ...)` —\n * where the citation envelope opens with `(` immediately after the\n * sentence-ending period. Without it, the case-name backward walk crosses\n * the boundary and absorbs the entire preceding sentence into the\n * plaintiff field. #323 */\nconst SENTENCE_BOUNDARY_REGEX = /[.)]\\s+(?=[A-Z(])/g\n\n/** Louisiana docket-prefix boundary (#232). Matches the Louisiana citation\n * shape `NN-NNNN (La. ... M/D/YY)` or `YYYY-K-NNNN (La. ... M/D/YY)` that\n * precedes the parallel `So. 2d` / `So. 3d` reporter citation. The capture\n * groups expose the court (group 2) and the date string (group 3) so the\n * trailing reporter citation can inherit the metadata. Includes an optional\n * `, p. N` pincite segment commonly present in LA practice.\n *\n * The trailing `,` + whitespace is consumed so that everything BEFORE this\n * pattern is the caption. */\nconst LA_DOCKET_BOUNDARY_REGEX =\n /,?\\s*(\\d{2,4}-[A-Z\\d-]+)(?:,\\s*p\\.\\s*\\d+)?\\s*\\((La\\.[^)]*?)\\s+(\\d{1,2}\\/\\d{1,2}\\/\\d{2,4})\\),\\s*/g\n\n/**\n * Extract case name via backward search from citation core.\n * Looks for \"v.\" pattern or procedural prefixes (In re, Ex parte, Matter of).\n *\n * @param cleanedText - Full cleaned text\n * @param coreStart - Position where citation core begins (volume start)\n * @param maxLookback - Maximum characters to search backward (default 150)\n * @param options - Optional original text + transformationMap to detect\n * paragraph-break boundaries that the cleaner has collapsed (#221).\n * @returns Case name and start position, or undefined if not found\n *\n * @example\n * ```typescript\n * extractCaseName(text, 20, 150)\n * // Returns: { caseName: \"Smith v. Jones\", nameStart: 0 }\n * ```\n */\nexport function extractCaseName(\n cleanedText: string,\n coreStart: number,\n maxLookback = 150,\n options?: { originalText?: string; transformationMap?: TransformationMap },\n):\n | {\n caseName: string\n nameStart: number\n /** Year captured from CSM year-first form (`In re K.F. (2009)`). */\n year?: number\n /** Clean-coordinate position of the year digits (excluding parens). */\n yearStart?: number\n /** Clean-coordinate position after the year digits. */\n yearEnd?: number\n /** Metadata recovered from a Louisiana docket-prefix paren that sits\n * between the caption and the citation core (#232). Applied by the\n * caller as fallback for `year` / `court` / `date` when the citation's\n * own trailing paren is absent. */\n precedingDocketMeta?: {\n court: string\n year: number\n date: StructuredDate\n }\n }\n | undefined {\n const searchStart = Math.max(0, coreStart - maxLookback)\n let precedingText = cleanedText.substring(searchStart, coreStart)\n let adjustedSearchStart = searchStart\n\n // Split at last boundary to avoid crossing citation/sentence boundaries.\n // We check five boundary types:\n // 1. Citation boundary: digit-period-space (e.g., \"10. \" from a previous cite's page number)\n // 2. Id. boundary: \"Id. \" short-form citation marker (#182)\n // 3. Parenthetical signal boundary: \"(quoting \", \"(citing \", \"(cited in \" (#182)\n // 4. Sentence boundary: period/paren + space + uppercase, skipped when the\n // word before the period is a legal abbreviation (Bluebook T6/T10/T7)\n // 5. Paragraph boundary: \\n\\s*\\n in the original text, recovered via\n // transformationMap because the cleaner collapses newlines to spaces (#221)\n let lastBoundaryIndex = -1\n let match: RegExpExecArray | null\n\n // Check paragraph boundaries via original text (#221).\n // The default cleaner pipeline replaces \\n with space, so paragraph breaks\n // are invisible in cleanedText. Recover them from originalText by mapping\n // the search window back to original coordinates.\n if (options?.originalText && options.transformationMap) {\n const { originalText, transformationMap } = options\n const searchOriginalStart =\n transformationMap.cleanToOriginal.get(searchStart) ?? searchStart\n const coreOriginalStart =\n transformationMap.cleanToOriginal.get(coreStart) ?? coreStart\n if (coreOriginalStart > searchOriginalStart) {\n const originalWindow = originalText.substring(searchOriginalStart, coreOriginalStart)\n const paragraphBreakRegex = /\\n[ \\t\\r]*\\n/g\n let pMatch: RegExpExecArray | null\n while ((pMatch = paragraphBreakRegex.exec(originalWindow)) !== null) {\n const breakOriginalEnd = searchOriginalStart + pMatch.index + pMatch[0].length\n // Find the clean position immediately at/after the paragraph break.\n // The break itself collapses to a space; the next non-whitespace char\n // is the start of the new paragraph in cleanedText.\n let cleanPos: number | undefined\n for (let off = 0; off < 10; off++) {\n cleanPos = transformationMap.originalToClean.get(breakOriginalEnd + off)\n if (cleanPos !== undefined) break\n }\n if (cleanPos !== undefined && cleanPos >= searchStart && cleanPos <= coreStart) {\n const relIndex = cleanPos - searchStart\n if (relIndex > lastBoundaryIndex) {\n lastBoundaryIndex = relIndex\n }\n }\n }\n }\n }\n\n // Check citation boundaries (digit-period-space)\n CITATION_BOUNDARY_REGEX.lastIndex = 0\n while ((match = CITATION_BOUNDARY_REGEX.exec(precedingText)) !== null) {\n const boundaryEnd = match.index + match[0].length\n if (boundaryEnd > lastBoundaryIndex) {\n lastBoundaryIndex = boundaryEnd\n }\n }\n\n // Check Id. boundaries (#182)\n ID_BOUNDARY_REGEX.lastIndex = 0\n while ((match = ID_BOUNDARY_REGEX.exec(precedingText)) !== null) {\n const boundaryEnd = match.index + match[0].length\n if (boundaryEnd > lastBoundaryIndex) {\n lastBoundaryIndex = boundaryEnd\n }\n }\n\n // Check parenthetical signal boundaries (#182)\n PAREN_SIGNAL_BOUNDARY_REGEX.lastIndex = 0\n while ((match = PAREN_SIGNAL_BOUNDARY_REGEX.exec(precedingText)) !== null) {\n const boundaryEnd = match.index + match[0].length\n if (boundaryEnd > lastBoundaryIndex) {\n lastBoundaryIndex = boundaryEnd\n }\n }\n\n // Louisiana docket-prefix segments (#232) sit *between* the caption and\n // the trailing reporter citation: `Smith v. Jones, 07-393, p. 2 (La. App.\n // 3d Cir. 10/3/07), 966 So. 2d 1127`. Unlike sentence / Id. / paren-signal\n // boundaries, the segment is INTERIOR — stripping it from `precedingText`\n // preserves the caption to its left. Capture the docket paren's court +\n // date for metadata transfer onto the trailing reporter citation.\n let precedingDocketMeta:\n | { court: string; year: number; date: StructuredDate }\n | undefined\n LA_DOCKET_BOUNDARY_REGEX.lastIndex = 0\n const laDocketMatch = LA_DOCKET_BOUNDARY_REGEX.exec(precedingText)\n if (laDocketMatch) {\n const dateStr = laDocketMatch[3]\n const date = parseDate(dateStr)\n if (date) {\n precedingDocketMeta = {\n court: laDocketMatch[2].trim(),\n year: date.parsed.year,\n date,\n }\n }\n // Excise the docket segment, leaving just the trailing \", \" so the\n // V_CASE_NAME_REGEX still sees a comma-terminated caption to its left.\n precedingText =\n precedingText.substring(0, laDocketMatch.index) +\n \", \" +\n precedingText.substring(laDocketMatch.index + laDocketMatch[0].length)\n }\n\n // Check sentence boundaries: \"). \" or \". \" followed by uppercase letter.\n // Skip when the period belongs to a legal abbreviation (comprehensive T6/T10/T7 check).\n SENTENCE_BOUNDARY_REGEX.lastIndex = 0\n while ((match = SENTENCE_BOUNDARY_REGEX.exec(precedingText)) !== null) {\n // Only check abbreviation for period boundaries, not close-paren boundaries\n if (\n precedingText[match.index] === \".\" &&\n isLikelyAbbreviationPeriod(precedingText, match.index)\n ) {\n continue\n }\n const boundaryEnd = match.index + match[0].length\n if (boundaryEnd > lastBoundaryIndex) {\n lastBoundaryIndex = boundaryEnd\n }\n }\n\n if (lastBoundaryIndex !== -1) {\n precedingText = precedingText.substring(lastBoundaryIndex)\n adjustedSearchStart = searchStart + lastBoundaryIndex\n }\n\n // Priority 1: Standard \"v.\" or \"vs.\" format with comma before citation\n // Match party names with letters, numbers (for \"Doe No. 2\"), periods, apostrophes, ampersands, hyphens, slashes\n const vMatch = V_CASE_NAME_REGEX.exec(precedingText)\n if (vMatch) {\n // Check for semicolon in matched text (multi-citation separator)\n if (!vMatch[0].includes(\";\")) {\n let plaintiff = vMatch[1].trim()\n let trimOffset = 0\n\n // Validate plaintiff: real party names are capitalized words + legal connectors.\n // If the plaintiff contains lowercase non-connector words (e.g., \"The court cited Smith\"),\n // it captured sentence context. Trim from the left to the first valid party name start.\n //\n // The firstWordIsProperName guard preserves the original plaintiff when the\n // first word is a real party name and the lowercase content is an internal\n // qualifier (\"Smith d/b/a Old Bob's Diner\"). Without an internal-qualifier\n // marker, a capitalized first word alone is NOT enough to suppress trimming —\n // sentence-initial prepositions like \"Under\", \"Pursuant\", \"Following\" would\n // otherwise be preserved as if they were proper nouns (#223).\n if (!isLikelyPartyName(plaintiff)) {\n const words = plaintiff.split(/\\s+/)\n const firstWord = words[0] ?? \"\"\n const firstWordClean = firstWord.toLowerCase().replace(/[.,']+$/, \"\")\n const firstWordIsProperName =\n /^[A-Z]/.test(firstWord) &&\n !PARTY_NAME_CONNECTORS.has(firstWordClean) &&\n !SENTENCE_INITIAL_WORDS.has(firstWordClean) &&\n INTERNAL_QUALIFIER_REGEX.test(plaintiff)\n if (!firstWordIsProperName) {\n // Check if the prefix starts with a signal word (See, See also, But see, etc.).\n // If so, keep it — extractPartyNames handles signal stripping downstream.\n const signalMatch = SIGNAL_STRIP_REGEX.exec(plaintiff)\n if (!signalMatch) {\n for (let i = 1; i < words.length; i++) {\n const candidate = words.slice(i).join(\" \")\n if (/^[A-Z]/.test(candidate) && isLikelyPartyName(candidate)) {\n // Compute offset from word positions rather than indexOf,\n // which could match the wrong position if a word repeats.\n const prefix = words.slice(0, i).join(\" \")\n trimOffset = prefix.length + 1\n plaintiff = candidate\n break\n }\n }\n }\n }\n }\n\n // Detect consolidated captions: vMatch[0] contains 2+ \"v.\" anchors.\n // The non-greedy regex defendant (group 2) is anchored at the trailing\n // \",$\" and so absorbs downstream comma-separated caption segments\n // including their own \"v.\" anchors (#222). Recovery: truncate the\n // defendant at its first comma to keep just the first \"X v. Y\" pair.\n const vAnchorMatches = vMatch[0].match(/\\bv(?:s)?\\.\\s/g)\n let defendantText = vMatch[2].trim()\n if (vAnchorMatches && vAnchorMatches.length >= 2) {\n const firstCommaInDefendant = vMatch[2].indexOf(\",\")\n if (firstCommaInDefendant !== -1) {\n defendantText = vMatch[2].substring(0, firstCommaInDefendant).trim()\n }\n }\n\n // Preserve the source's `v` punctuation form in `caseName`. New York\n // courts use `v` (no period); federal/most state courts use `v.`. The\n // existing V_CASE_NAME_REGEX accepts both via `v(?:s)?\\.?` — extract\n // whichever form actually appears in the matched text so the\n // assembled caseName is faithful to the source. #326\n const sepMatch = /\\bvs?\\.?(?=\\s)/.exec(vMatch[0])\n const sep = sepMatch?.[0] ?? \"v.\"\n\n const caseName = `${plaintiff} ${sep} ${defendantText}`\n const nameStart = adjustedSearchStart + vMatch.index + trimOffset\n // vMatch[3] = optional court text from the CSM year-first paren\n // (`Smith v. Jones (2d Cir. 2005)` — #293); vMatch[4] = the year\n // (`Smith v. Jones (1990)` — #19). Bluebook form leaves both undefined.\n // vMatch.indices[4] (enabled by `d` flag) gives the year position;\n // translate to cleanedText coordinates.\n const courtFromCsm = vMatch[3]?.trim()\n const year = vMatch[4] ? Number.parseInt(vMatch[4], 10) : undefined\n let yearStart: number | undefined\n let yearEnd: number | undefined\n if (year !== undefined && vMatch.indices?.[4]) {\n yearStart = adjustedSearchStart + vMatch.indices[4][0]\n yearEnd = adjustedSearchStart + vMatch.indices[4][1]\n }\n // CSM `(court year)` form (#293): synthesize a precedingDocketMeta so\n // the existing consumer at extractCase line ~2502 propagates court,\n // year, and date onto the citation. Skip when only year is present\n // (year-only handled by the dedicated `year`/`yearStart`/`yearEnd`\n // fields above).\n let csmDocketMeta = precedingDocketMeta\n if (!csmDocketMeta && courtFromCsm && year !== undefined && vMatch[4]) {\n csmDocketMeta = {\n court: courtFromCsm,\n year,\n date: { iso: vMatch[4], parsed: { year } },\n }\n }\n return {\n caseName,\n nameStart,\n year,\n yearStart,\n yearEnd,\n precedingDocketMeta: csmDocketMeta,\n }\n }\n }\n\n // Priority 2: Procedural prefixes (including Estate of, In the Matter of)\n const procMatch = PROCEDURAL_PREFIX_REGEX.exec(precedingText)\n if (procMatch) {\n // Check for semicolon in matched text (multi-citation separator)\n if (!procMatch[0].includes(\";\")) {\n const caseName = `${procMatch[1]} ${procMatch[2].trim()}`\n const nameStart = adjustedSearchStart + procMatch.index\n // procMatch[3] = optional court text from the CSM year-first paren\n // (`In re Cellphone (9th Cir. 2014)` — #293); procMatch[4] = the year\n // (`In re K.F. (2009)` — #19). Bluebook form leaves both undefined.\n const courtFromCsm = procMatch[3]?.trim()\n const year = procMatch[4] ? Number.parseInt(procMatch[4], 10) : undefined\n let yearStart: number | undefined\n let yearEnd: number | undefined\n if (year !== undefined && procMatch.indices?.[4]) {\n yearStart = adjustedSearchStart + procMatch.indices[4][0]\n yearEnd = adjustedSearchStart + procMatch.indices[4][1]\n }\n let csmDocketMeta = precedingDocketMeta\n if (!csmDocketMeta && courtFromCsm && year !== undefined && procMatch[4]) {\n csmDocketMeta = {\n court: courtFromCsm,\n year,\n date: { iso: procMatch[4], parsed: { year } },\n }\n }\n return {\n caseName,\n nameStart,\n year,\n yearStart,\n yearEnd,\n precedingDocketMeta: csmDocketMeta,\n }\n }\n }\n\n // Priority 3: Generic single-party caption (#193).\n //\n // V. and procedural-prefix scans failed. The precedingText is already\n // bounded by sentence/citation/paren-signal boundaries, so whatever\n // remains — typically a capitalized-words-only caption ending at \", \" —\n // is the caption candidate. Strip any leading signal word (See, cf., etc.)\n // and validate via isLikelyPartyName to filter out sentence prose.\n //\n // Handles single-party corporate captions like \"Board of Mgrs. of X\",\n // \"Board of Directors of X\", and unrecognized organizational prefixes\n // that don't fit PROCEDURAL_PREFIX_REGEX.\n const commaStrippedBody = precedingText.replace(/,\\s*$/, \"\")\n const leadingWsLen = commaStrippedBody.length - commaStrippedBody.trimStart().length\n let captionBody = commaStrippedBody.substring(leadingWsLen)\n let signalStripLen = 0\n const sigStripMatch = SIGNAL_STRIP_REGEX.exec(captionBody)\n if (sigStripMatch) {\n signalStripLen = sigStripMatch[0].length\n captionBody = captionBody.substring(signalStripLen)\n }\n const caption = captionBody.trim()\n\n if (caption.length > 0 && isLikelyPartyName(caption)) {\n const firstWord = caption.split(/\\s+/)[0] ?? \"\"\n const firstWordClean = firstWord.toLowerCase().replace(/[.,']+$/, \"\")\n if (!SENTENCE_INITIAL_WORDS.has(firstWordClean)) {\n // Skip multi-citation strings (joined by semicolons)\n if (!caption.includes(\";\")) {\n const nameStart = adjustedSearchStart + leadingWsLen + signalStripLen\n return { caseName: caption, nameStart, precedingDocketMeta }\n }\n }\n }\n\n return undefined\n}\n\n/** A raw parenthetical block extracted from text */\ninterface RawParenthetical {\n /** Content between the parentheses (excluding parens themselves) */\n text: string\n /** Position of opening '(' in the text */\n start: number\n /** Position after closing ')' in the text (exclusive) */\n end: number\n}\n\n/** A subsequent history signal found between parenthetical groups */\ninterface RawSignal {\n /** Raw signal text (e.g., \"aff'd\", \"cert. denied\") */\n text: string\n /** Normalized signal classification */\n normalized: HistorySignal\n /** Position of signal start in the text */\n start: number\n /** Position after signal end (exclusive) */\n end: number\n}\n\n/** Result of collecting parentheticals with signal awareness */\ninterface CollectedParentheticals {\n /** All parenthetical blocks in order */\n parens: RawParenthetical[]\n /** Signals found between groups, each paired with the index of the next paren */\n signals: Array<{ signal: RawSignal; nextParenIndex: number }>\n}\n\n/**\n * Collect all top-level parenthetical blocks starting from a position.\n * Uses depth tracking to handle nested parens. Continues scanning through\n * chained parentheticals and subsequent history signals.\n *\n * @param text - Full text to scan\n * @param startPos - Position to start scanning (typically after citation core)\n * @param maxLookahead - Maximum characters to scan forward (default 500)\n * @returns Collected parentheticals with associated signals\n */\nfunction collectParentheticals(\n text: string,\n startPos: number,\n maxLookahead = 500,\n): CollectedParentheticals {\n const parens: RawParenthetical[] = []\n const signals: CollectedParentheticals[\"signals\"] = []\n let pos = startPos\n const endLimit = Math.min(text.length, startPos + maxLookahead)\n let pendingSignal: RawSignal | undefined\n\n // Skip past any pincite text between core citation and parentheticals.\n // E.g., \", 199 n.2\" in \"982 N.W.2d 189, 199 n.2 (Minn. 2022)\".\n // This must happen before the main loop because pincite text includes\n // commas and digits that would otherwise block the scanner.\n const pinciteText = text.substring(pos, endLimit)\n const pinciteSkip = PINCITE_SKIP_REGEX.exec(pinciteText)\n if (pinciteSkip) {\n pos += pinciteSkip[0].length\n }\n\n while (pos < endLimit) {\n // Skip whitespace and commas between parentheticals\n while (pos < endLimit && PAREN_SKIP_REGEX.test(text[pos])) {\n pos++\n }\n\n if (pos >= endLimit || text[pos] !== \"(\") {\n // Check for subsequent history signal before giving up.\n // Normalize in-place to avoid a second SIGNAL_TABLE scan later.\n const remainingText = text.substring(pos, endLimit)\n const normalized = normalizeSignal(remainingText)\n if (normalized) {\n // Multi-stage chain (e.g., \"review granted, opinion vacated\"): if a\n // prior signal is still pending with no following paren, flush it\n // before overwriting. Without this, only the last link of a chain\n // survives. (#238)\n if (pendingSignal) {\n signals.push({ signal: pendingSignal, nextParenIndex: -1 })\n }\n pendingSignal = {\n text: remainingText.substring(0, normalized.matchLength).replace(/\\s+$/, \"\"),\n normalized: normalized.signal,\n start: pos,\n end: pos + normalized.matchLength,\n }\n pos += normalized.matchLength\n continue\n }\n break\n }\n\n // Found opening paren — track depth to find matching close\n const parenStart = pos\n let depth = 0\n const contentStart = pos + 1\n\n while (pos < endLimit) {\n const char = text[pos]\n if (char === \"(\") {\n depth++\n } else if (char === \")\") {\n depth--\n if (depth === 0) {\n pos++ // move past closing paren\n const content = text.substring(contentStart, pos - 1).trim()\n if (content.length > 0) {\n parens.push({ text: content, start: parenStart, end: pos })\n // If there was a pending signal, associate it with this paren\n if (pendingSignal) {\n signals.push({ signal: pendingSignal, nextParenIndex: parens.length - 1 })\n pendingSignal = undefined\n }\n }\n break\n }\n }\n pos++\n }\n\n // If we never closed the paren, stop\n if (depth > 0) break\n }\n\n // Handle trailing signal with no following paren\n if (pendingSignal) {\n signals.push({ signal: pendingSignal, nextParenIndex: -1 })\n }\n\n return { parens, signals }\n}\n\n/**\n * Parse parenthetical content to extract court, year, date, and disposition.\n * Unified parser replacing the old year-only logic.\n *\n * @param content - Parenthetical content (without the parens themselves)\n * @returns Structured parenthetical data\n *\n * @example\n * ```typescript\n * parseParenthetical(\"9th Cir. 2020\")\n * // Returns: { court: \"9th Cir.\", year: 2020, date: { iso: \"2020\", parsed: { year: 2020 } } }\n *\n * parseParenthetical(\"2d Cir. Jan. 15, 2020\")\n * // Returns: { court: \"2d Cir.\", year: 2020, date: { iso: \"2020-01-15\", parsed: { year: 2020, month: 1, day: 15 } } }\n *\n * parseParenthetical(\"en banc\")\n * // Returns: { disposition: \"en banc\" }\n * ```\n */\nexport function parseParenthetical(content: string): {\n court?: string\n year?: number\n date?: StructuredDate\n disposition?: string\n /** Surname(s) of justice(s) attributed to a justice-attribution paren (#235) */\n justices?: string[]\n /** Scope qualifier for a justice-attribution paren (#235): in_judgment | in_part | from_denial */\n scope?: string\n /** Texas Greenbook writ/petition history clause inside the parenthetical (#229) */\n internalHistory?: { signal: HistorySignal; rawSignal: string; start: number; end: number }\n courtStart?: number\n courtEnd?: number\n yearStart?: number\n yearEnd?: number\n} {\n const result: {\n court?: string\n year?: number\n date?: StructuredDate\n disposition?: string\n justices?: string[]\n scope?: string\n internalHistory?: {\n signal: HistorySignal\n rawSignal: string\n start: number\n end: number\n }\n courtStart?: number\n courtEnd?: number\n yearStart?: number\n yearEnd?: number\n } = {}\n\n // Parse structured date using dates.ts\n const dateResult = parseDate(content)\n if (dateResult) {\n result.date = dateResult\n result.year = dateResult.parsed.year\n }\n\n // Texas writ/pet history: detect trailing \",\\s*<signal>\" clause after year\n // (e.g., \"Tex. App.—Dallas 2010, writ ref'd n.r.e.\"). Strip the clause from\n // the working content so stripDateFromCourt sees the conventional shape.\n let workingContent = content\n if (result.year) {\n const yearStr = String(result.year)\n const yearIdx = content.lastIndexOf(yearStr)\n if (yearIdx !== -1) {\n const afterYearStart = yearIdx + yearStr.length\n const afterYear = content.substring(afterYearStart)\n const trailing = /^\\s*,\\s*(.+?)\\s*$/.exec(afterYear)\n if (trailing) {\n const sigText = trailing[1]\n const normalized = normalizeSignal(sigText)\n if (normalized) {\n const rawSignal = sigText.substring(0, normalized.matchLength)\n // Compute the absolute offset of the signal text within the content.\n const sigOffset = content.indexOf(rawSignal, afterYearStart)\n result.internalHistory = {\n signal: normalized.signal,\n rawSignal,\n start: sigOffset !== -1 ? sigOffset : afterYearStart,\n end:\n (sigOffset !== -1 ? sigOffset : afterYearStart) + rawSignal.length,\n }\n workingContent = content.substring(0, afterYearStart)\n }\n }\n }\n }\n\n // Extract court (strips date components) — runs on workingContent so the\n // Texas trailing-history clause does not interfere with date-end detection.\n const courtResult = stripDateFromCourt(workingContent)\n if (courtResult) {\n result.court = courtResult\n const courtIdx = content.indexOf(courtResult)\n if (courtIdx !== -1) {\n result.courtStart = courtIdx\n result.courtEnd = courtIdx + courtResult.length\n }\n }\n\n // Year offset within parenthetical content\n if (result.year) {\n const yearStr = String(result.year)\n const yearIdx = content.lastIndexOf(yearStr)\n if (yearIdx !== -1) {\n result.yearStart = yearIdx\n result.yearEnd = yearIdx + yearStr.length\n }\n }\n\n // Justice-attribution parenthetical (#235). Detected BEFORE the bare\n // en banc / per curiam check so a parenthetical like\n // `Cabranes, J., dissenting from denial of rehearing en banc` doesn't\n // false-positive on the trailing `en banc` substring.\n //\n // Pattern: <Surname>(, <Surname>)*(?:,? and <Surname>)?,? (C\\.J\\.|J\\.|JJ\\.),? <role>\n const justiceMatch = /^(?<surnames>[A-Z][a-z]+(?:(?:,\\s+|\\s+and\\s+)[A-Z][a-z]+)*)\\s*,?\\s*(?<title>C\\.J\\.|J\\.|JJ\\.)\\s*,?\\s*(?<role>.+)$/.exec(\n content.trim(),\n )\n if (justiceMatch?.groups) {\n const surnameText = justiceMatch.groups.surnames\n const roleText = justiceMatch.groups.role.trim().replace(/[.,]+$/, \"\")\n const justices = surnameText\n .split(/(?:,\\s+and\\s+|,\\s+|\\s+and\\s+)/)\n .map((s) => s.trim())\n .filter(Boolean)\n result.justices = justices\n\n // Classify the role into a disposition + optional scope.\n const lower = roleText.toLowerCase()\n if (/^concurring\\s+in\\s+part\\s+and\\s+dissenting\\s+in\\s+part/.test(lower)) {\n result.disposition = \"mixed\"\n result.scope = \"in_part\"\n } else if (/^concurring\\s+in\\s+the\\s+judgment/.test(lower)) {\n result.disposition = \"concurrence\"\n result.scope = \"in_judgment\"\n } else if (/^concurring\\s+in\\s+part/.test(lower)) {\n result.disposition = \"concurrence\"\n result.scope = \"in_part\"\n } else if (/^dissenting\\s+in\\s+part/.test(lower)) {\n result.disposition = \"dissent\"\n result.scope = \"in_part\"\n } else if (/^dissenting\\s+from\\s+denial\\s+of/.test(lower)) {\n result.disposition = \"dissent\"\n result.scope = \"from_denial\"\n } else if (/^concurring/.test(lower)) {\n result.disposition = \"concurrence\"\n } else if (/^dissenting/.test(lower)) {\n result.disposition = \"dissent\"\n } else if (/^joining/.test(lower)) {\n result.disposition = \"majority\"\n }\n return result\n }\n\n // Non-justice disposition parens (#235): plurality opinion, mem.,\n // unpublished table decision. Checked before en banc/per curiam.\n if (/^plurality\\s+opinion\\b/i.test(content.trim())) {\n result.disposition = \"plurality opinion\"\n return result\n }\n if (/^mem\\.\\s*$/i.test(content.trim())) {\n result.disposition = \"mem.\"\n return result\n }\n if (/^unpublished\\s+table\\s+decision\\b/i.test(content.trim())) {\n result.disposition = \"unpublished table decision\"\n return result\n }\n\n // Check for disposition (en banc / in bank / per curiam). Anchored at\n // content end (\\s*$) so a parenthetical like `Cabranes, J., dissenting from\n // denial of rehearing en banc` — caught above by the justice-attribution\n // branch — does not also trip the en-banc check via substring match (#235).\n // `(in bank)` is the California Supreme Court's equivalent of `(en banc)`\n // — added as a separate disposition value to preserve the CA distinction.\n if (/\\ben banc\\b\\s*$/i.test(content.trim())) {\n result.disposition = \"en banc\"\n } else if (/\\bin bank\\b\\s*$/i.test(content.trim())) {\n result.disposition = \"in bank\"\n } else if (/\\bper curiam\\b\\s*$/i.test(content.trim())) {\n result.disposition = \"per curiam\"\n }\n\n return result\n}\n\n/**\n * Classify a raw parenthetical block as metadata or explanatory.\n *\n * @param raw - Raw parenthetical text (content between parens)\n * @returns Classification result with kind discriminator\n */\nfunction classifyParenthetical(raw: string):\n | {\n kind: \"metadata\"\n court?: string\n year?: number\n date?: StructuredDate\n disposition?: string\n justices?: string[]\n scope?: string\n }\n | {\n kind: \"explanatory\"\n text: string\n type: ParentheticalType\n } {\n // Check for signal word first — signal-word parens are always explanatory\n const leadingMatch = LEADING_WORD_REGEX.exec(raw)\n if (leadingMatch) {\n const candidate = leadingMatch[1].toLowerCase()\n if (isSignalWord(candidate)) {\n return { kind: \"explanatory\", text: raw, type: candidate }\n }\n }\n\n // Try metadata parse: court, year, date, disposition\n // Note: \"other\"-type parens with embedded years (e.g., \"the court, in 2019, held X\")\n // will be classified as metadata. This is a known limitation — most explanatory\n // parentheticals start with a signal word and are handled above.\n // Note: meta.court alone is insufficient — stripDateFromCourt returns any\n // text with letters as a \"court\", so a standalone court-only second paren\n // like \"(9th Cir.)\" will fall through to \"other\". This is acceptable since\n // court-only parens without year/date are extremely rare in legal text.\n const meta = parseParenthetical(raw)\n if (meta.year || meta.date || meta.disposition || meta.justices) {\n return { kind: \"metadata\", ...meta }\n }\n\n // No signal word and no metadata — classify as \"other\" explanatory\n return { kind: \"explanatory\", text: raw, type: \"other\" }\n}\n\n/**\n * Normalize party name for matching by removing legal noise.\n * Normalization pipeline:\n * 1. Strip \"et al.\" (case-insensitive)\n * 2. Strip slash-aliases \"d/b/a\", \"f/k/a\", \"n/k/a\", \"a/k/a\" and everything after\n * 3. Strip \"aka\" and everything after (case-insensitive, word boundary)\n * 4. Strip trailing corporate suffixes (Inc., LLC, Corp., Ltd., Co., LLP, LP, P.C.) - iterative\n * 5. Strip leading articles (The, A, An)\n * 6. Normalize whitespace\n * 7. Trim and lowercase\n *\n * @param name - Raw party name\n * @returns Normalized party name\n *\n * @example\n * ```typescript\n * normalizePartyName(\"The Smith Corp., Inc.\") // \"smith\"\n * normalizePartyName(\"Doe et al.\") // \"doe\"\n * normalizePartyName(\"United States\") // \"united states\" (not stripped)\n * ```\n */\nfunction normalizePartyName(name: string): string {\n let normalized = name\n\n // Strip \"et al.\" (with or without period, case-insensitive)\n normalized = normalized.replace(/\\bet\\s+al\\.?/gi, \"\")\n\n // Strip slash-alias variants (\"d/b/a\", \"f/k/a\", \"n/k/a\", \"a/k/a\") and\n // everything after them. Matches the slash forms produced by Bluebook-style\n // captions; the non-slash \"aka\" form is handled below (#240).\n normalized = normalized.replace(/\\s+(?:d\\/b\\/a|[fna]\\/k\\/a)\\b.*/gi, \"\")\n\n // Strip \"aka\" and everything after it (case-insensitive, word boundary)\n normalized = normalized.replace(/\\s+aka\\b.*/gi, \"\")\n\n // Strip trailing corporate suffixes (with or without trailing period, handle comma)\n // Repeat to handle multiple suffixes like \"Corp., Inc.\"\n let prev = \"\"\n while (prev !== normalized) {\n prev = normalized\n normalized = normalized.replace(/,?\\s*(Inc|LLC|Corp|Ltd|Co|LLP|LP|P\\.C)\\.?$/gi, \"\")\n }\n\n // Strip leading articles (only at start)\n normalized = normalized.replace(/^(The|A|An)\\s+/i, \"\")\n\n // Normalize whitespace (collapse multiple spaces)\n normalized = normalized.replace(/\\s+/g, \" \")\n\n // Trim and lowercase\n return normalized.trim().toLowerCase()\n}\n\n/**\n * Extract plaintiff and defendant party names from case name.\n * Handles adversarial cases (v.) and procedural prefixes (In re, Ex parte, etc.).\n *\n * @param caseName - Case name string\n * @returns Party name data with raw and normalized fields\n *\n * @example\n * ```typescript\n * extractPartyNames(\"Smith v. Jones\")\n * // Returns: { plaintiff: \"Smith\", plaintiffNormalized: \"smith\", defendant: \"Jones\", defendantNormalized: \"jones\" }\n *\n * extractPartyNames(\"In re Smith\")\n * // Returns: { plaintiff: \"In re Smith\", plaintiffNormalized: \"smith\", proceduralPrefix: \"In re\" }\n *\n * extractPartyNames(\"People v. Smith\")\n * // Returns: { plaintiff: \"People\", plaintiffNormalized: \"people\", defendant: \"Smith\", defendantNormalized: \"smith\" }\n * ```\n */\nexport function extractPartyNames(caseName: string): {\n plaintiff?: string\n plaintiffNormalized?: string\n defendant?: string\n defendantNormalized?: string\n proceduralPrefix?: string\n signal?: CitationSignal\n /** Bankruptcy adversary admin parenthetical (#241), e.g., \"In re Hintze\". */\n adminParenthetical?: string\n} {\n let signal: CitationSignal | undefined\n // Procedural prefix patterns (anchored to start, case-insensitive).\n // Longer prefixes first so the for-loop's `prefixRegex.exec(caseName)` finds\n // the most specific match. Six 2026-05-11 cross-domain research dispatches\n // (family, probate, bankruptcy, immigration, criminal/habeas, ex rel./qui tam)\n // identified the additions; corpus-sourced examples live in\n // `docs/research/2026-05-11-procedural-prefixes-*.md`.\n const proceduralPrefixes = [\n // \"In the Matter of the X of\" cluster — must precede \"In the Matter of\"\n \"In the Matter of the Liquidation of\",\n \"In the Matter of the Rehabilitation of\",\n \"In the Matter of the Receivership of\",\n \"In the Matter of the Extradition of\",\n \"In the Matter of the Application of\",\n \"In the Matter of the Welfare of\",\n \"In the Matter of\",\n // \"In re X of\" cluster — must precede \"In re\"\n \"In re Petition for Naturalization of\",\n \"In re Termination of Parental Rights as to\",\n \"In re Termination of Parental Rights to\",\n \"In re Termination of Parental Rights of\",\n \"In re Marriage of\",\n \"In re Liquidation of\",\n \"In re Rehabilitation of\",\n \"In re Receivership of\",\n \"In re Naturalization of\",\n \"In re Extradition of\",\n \"In re Application of\",\n \"In re Welfare of\",\n \"In re Dependency of\",\n \"In re Paternity of\",\n \"In re Parentage of\",\n // CA Tier 1 — In re precision upgrades for conservatorship/guardianship/adoption\n \"In re Conservatorship of\",\n \"In re Guardianship of\",\n \"In re Adoption of\",\n \"In the Interest of\",\n \"In re\",\n \"Ex parte\",\n // \"Matter of X of\" cluster — must precede \"Matter of\"\n \"Matter of Liquidation of\",\n \"Matter of Rehabilitation of\",\n \"Matter of\",\n // Sovereign ex rel. — long forms precede short forms\n \"Commonwealth of Puerto Rico ex rel.\",\n \"Government of the Virgin Islands ex rel.\",\n \"Commonwealth ex rel.\",\n \"State ex rel.\",\n \"United States ex rel.\",\n \"People ex rel.\",\n \"District of Columbia ex rel.\",\n // Petition variants — \"Petition for Naturalization of\" precedes \"Petition of\"\n \"Petition for Naturalization of\",\n \"Application of\",\n \"On Petition of\",\n \"Petition of\",\n // Other \"X of\" forms\n \"Adoption of\",\n // CA Tier 1 — Conservatorship extended forms must precede bare \"Conservatorship of\"\n \"Conservatorship of the Person and Estate of\",\n \"Conservatorship of the Person of\",\n \"Conservatorship of the Estate of\",\n \"Conservatorship of\",\n \"Guardianship of\",\n \"Estate of\",\n // Bare forms with no \"In re\" prefix (no alternation-ordering collisions)\n \"Care and Protection of\",\n \"Succession of\",\n // CA Tier 1 — agency / discipline procedural prefixes (2026-05-11)\n \"Inquiry Concerning Judge\",\n \"Appeal of\",\n ]\n\n // Check for procedural prefix first\n for (const prefix of proceduralPrefixes) {\n const prefixRegex = new RegExp(`^(${prefix})\\\\s+(.+)$`, \"i\")\n const match = prefixRegex.exec(caseName)\n if (match) {\n const matchedPrefix = match[1]\n const subject = match[2]\n\n // Check if there's a \"v.\" after the prefix (adversarial case)\n if (/\\s+vs?\\.?\\s+/i.test(subject)) {\n // Adversarial case with procedural-looking plaintiff (e.g., \"Estate of X v. Y\")\n // Split on \"v.\"\n const vMatch = /^(.+?)\\s+vs?\\.?\\s+(.+)$/i.exec(caseName)\n if (vMatch) {\n const plaintiff = vMatch[1].trim()\n const defendant = vMatch[2].trim()\n return {\n plaintiff,\n plaintiffNormalized: normalizePartyName(plaintiff),\n defendant,\n defendantNormalized: normalizePartyName(defendant),\n }\n }\n } else {\n // Pure procedural (no \"v.\")\n return {\n plaintiff: caseName,\n plaintiffNormalized: normalizePartyName(subject),\n proceduralPrefix: matchedPrefix,\n }\n }\n }\n }\n\n // Split on \"v.\" for adversarial cases\n const vRegex = /^(.+?)\\s+vs?\\.?\\s+(.+)$/i\n const vMatch = vRegex.exec(caseName)\n if (vMatch) {\n let plaintiff = vMatch[1].trim()\n let defendant = vMatch[2].trim()\n\n // Bankruptcy adversary admin parenthetical (#241): trailing\n // `(In re <Debtor>)` immediately after the defendant identifies the\n // underlying bankruptcy debtor. Strip from defendant; expose separately\n // via `adminParenthetical`. The leading \"In re\" anchor distinguishes the\n // adversary admin form from explanatory parens which appear *after* the\n // citation core, not inside the case name.\n let adminParenthetical: string | undefined\n const adminMatch = /\\s*\\(\\s*(In\\s+re\\s+[^)]+?)\\s*\\)\\s*$/i.exec(defendant)\n if (adminMatch) {\n adminParenthetical = adminMatch[1]\n defendant = defendant.substring(0, adminMatch.index).trim()\n }\n\n // Strip signal words from plaintiff (e.g., \"See Jones\" → \"Jones\")\n // Uses SIGNAL_STRIP_REGEX derived from VALID_SIGNALS for single source of truth.\n // Also strips \"Also\" and \"In\" (not valid signals) that can precede party names.\n const signalMatch =\n plaintiff.match(SIGNAL_STRIP_REGEX) ?? plaintiff.match(/^(Also|In(?!\\s+re\\b))\\s+/i)\n if (signalMatch) {\n // Guard against false-positive signal capture from over-greedy\n // case-name extraction (#304). When the V_CASE_NAME_REGEX captures\n // sentence prose like `Contra plaintiff's argument, Bolling v. Sharpe`,\n // the leading `Contra` looks like a Bluebook signal — but the next\n // token is lowercase prose, not a capitalized party name. Only strip\n // the signal when the remainder after stripping starts with a capital\n // letter (real case-name context) so we don't manufacture phantom\n // signals from sentence-internal English.\n const remainderAfterStrip = plaintiff.substring(signalMatch[0].length).trimStart()\n const firstChar = remainderAfterStrip[0] ?? \"\"\n const remainderIsCaseNameLike = firstChar >= \"A\" && firstChar <= \"Z\"\n if (remainderIsCaseNameLike) {\n const lowered = signalMatch[1].toLowerCase()\n // Combined `, e.g.` signals end with a period that is part of the canonical\n // form (e.g., \"see, e.g.\"); strip the trailing period only if the lowered\n // form isn't itself a valid signal (handles \"Cf.\" → \"cf\" without breaking\n // \"see, e.g.\" → \"see, e.g.\").\n if (VALID_SIGNALS.has(lowered)) {\n signal = lowered as CitationSignal\n } else {\n const stripped = lowered.replace(/\\.$/, \"\")\n if (VALID_SIGNALS.has(stripped)) {\n signal = stripped as CitationSignal\n }\n }\n plaintiff = plaintiff.substring(signalMatch[0].length).trim()\n }\n }\n\n return {\n plaintiff: plaintiff || vMatch[1].trim(), // Fallback to original if strip leaves nothing\n plaintiffNormalized: normalizePartyName(plaintiff || vMatch[1].trim()),\n defendant,\n defendantNormalized: normalizePartyName(defendant),\n signal,\n ...(adminParenthetical ? { adminParenthetical } : {}),\n }\n }\n\n // No \"v.\" and no procedural prefix - no parties extracted\n return {}\n}\n\n/**\n * Extracts case citation metadata from a tokenized citation.\n *\n * Parses token text to extract:\n * - Volume: Leading digits (e.g., \"500\" from \"500 F.2d 123\")\n * - Reporter: Alphabetic abbreviation (e.g., \"F.2d\")\n * - Page: Trailing digits after reporter (e.g., \"123\")\n * - Pincite: Optional page reference after comma (e.g., \", 125\")\n * - Court: Optional court abbreviation in parentheses (e.g., \"(9th Cir.)\")\n * - Year: Optional year in parentheses (e.g., \"(2020)\")\n *\n * Confidence scoring:\n * - Base: 0.5\n * - Common reporter pattern (F., U.S., etc.): +0.3\n * - Valid year (not future): +0.2\n * - Capped at 1.0\n *\n * Position translation:\n * - Uses TransformationMap to convert clean positions → original positions\n * - cleanStart/cleanEnd from token span\n * - originalStart/originalEnd via transformationMap.cleanToOriginal\n *\n * Note: This function does NOT validate against reporters-db. That happens\n * in Phase 3 (resolution layer). Phase 2 extraction only parses structure.\n *\n * @param token - Token from tokenizer containing matched text and clean positions\n * @param transformationMap - Position mapping from clean → original text\n * @returns FullCaseCitation with parsed metadata and translated positions\n *\n * @example\n * ```typescript\n * const token = {\n * text: \"500 F.2d 123, 125\",\n * span: { cleanStart: 10, cleanEnd: 27 },\n * type: \"case\",\n * patternId: \"federal-reporter\"\n * }\n * const citation = extractCase(token, transformationMap)\n * // citation = {\n * // type: \"case\",\n * // text: \"500 F.2d 123, 125\",\n * // volume: 500,\n * // reporter: \"F.2d\",\n * // page: 123,\n * // pincite: 125,\n * // span: { cleanStart: 10, cleanEnd: 27, originalStart: 10, originalEnd: 27 },\n * // confidence: 0.8,\n * // ...\n * // }\n * ```\n */\nexport function extractCase(\n token: Token,\n transformationMap: TransformationMap,\n cleanedText?: string,\n originalText?: string,\n /** Clean-coordinate spans of sibling tokens. Used to:\n * - bound the case-name backward walk so a parallel cite's caption is\n * not absorbed into this cite's caseName,\n * - skip past a contiguous parallel-cite chain (`, 198 A. 154, 35 L.Ed.2d 147`)\n * when searching for the shared trailing year parenthetical so each\n * cite in the chain gets year/court populated. */\n siblings?: ReadonlyArray<{ cleanStart: number; cleanEnd: number }>,\n): FullCaseCitation {\n const { text, span } = token\n\n // Parse volume-reporter-page using regex\n // Pattern: volume (digits) + reporter (letters/periods/spaces/numbers) + page (digits or blank placeholder)\n // Use greedy matching for reporter to capture full abbreviation including spaces\n const match = VOLUME_REPORTER_PAGE_REGEX.exec(text)\n\n if (!match) {\n // Fallback if pattern doesn't match (shouldn't happen if tokenizer is correct)\n throw new Error(`Failed to parse case citation: ${text}`)\n }\n\n const volume = parseVolume(match[1])\n const reporter = match[2].trim()\n\n // Extract nominative reporter if present (e.g., \"1 Cranch\" from \"5 U.S. (1 Cranch) 137\")\n const nominativeVolume = match[3] ? Number.parseInt(match[3], 10) : undefined\n const nominativeReporter = match[4] || undefined\n\n // Check if page is a blank placeholder (group 5 after nominative groups)\n const pageStr = match[5]\n const isBlankPage = BLANK_PAGE_REGEX.test(pageStr)\n const page = isBlankPage ? undefined : Number.parseInt(pageStr, 10)\n const hasBlankPage = isBlankPage ? true : undefined\n\n // Extract optional pincite (page reference after comma).\n // Pattern: \", digits\" (e.g., \", 125\") or \", at *N\" (star-pagination, #191).\n // Route the numeric part through parsePincite so star-page rawText (\"*2\")\n // doesn't blow up Number.parseInt.\n const pinciteMatch = PINCITE_REGEX.exec(text)\n let pinciteInfo: PinciteInfo | undefined = pinciteMatch\n ? (parsePincite(pinciteMatch[1]) ?? undefined)\n : undefined\n let pincite = pinciteInfo?.page\n\n // Initialize component spans for core regex-extracted fields\n const spans: CaseComponentSpans = {}\n\n if (match.indices) {\n // Group 1 = volume, Group 2 = reporter, Group 5 = page\n // Groups 3, 4 are optional nominative reporter (not tracked here)\n if (match.indices[1]) {\n spans.volume = spanFromGroupIndex(span.cleanStart, match.indices[1], transformationMap)\n }\n if (match.indices[2]) {\n // Trim whitespace from reporter span to match the trimmed reporter value\n const [rStart, rEnd] = match.indices[2]\n const rawReporter = text.substring(rStart, rEnd)\n const leadTrim = rawReporter.length - rawReporter.trimStart().length\n const trailTrim = rawReporter.length - rawReporter.trimEnd().length\n spans.reporter = spanFromGroupIndex(\n span.cleanStart,\n [rStart + leadTrim, rEnd - trailTrim],\n transformationMap,\n )\n }\n if (match.indices[5]) {\n spans.page = spanFromGroupIndex(span.cleanStart, match.indices[5], transformationMap)\n }\n }\n\n // Pincite span (from the token-level pincite match)\n if (pinciteMatch?.indices?.[1]) {\n spans.pincite = spanFromGroupIndex(span.cleanStart, pinciteMatch.indices[1], transformationMap)\n }\n\n // Initialize Phase 6 fields\n let year: number | undefined\n let court: string | undefined\n let date: StructuredDate | undefined\n let disposition: string | undefined\n let justices: string[] | undefined\n let scope: string | undefined\n let caseName: string | undefined\n let fullSpan: Span | undefined\n\n // Extract parenthetical from token text\n let parentheticalContent: string | undefined\n // Shared parenResult for court/year span computation (used by both code paths)\n let metaParenResult: ReturnType<typeof parseParenthetical> | undefined\n // Whether the metadata paren was found in token text (vs lookahead)\n let metaParenFromToken = false\n // Match any parenthetical (with or without letters)\n // When a nominative reporter is present, the first paren in token text is the\n // nominative (e.g., \"(2 Black)\") — skip it so the year/court look-ahead runs.\n const parenMatch = PAREN_REGEX.exec(text)\n if (parenMatch && !nominativeVolume) {\n parentheticalContent = parenMatch[1]\n // Parse parenthetical using unified parser\n metaParenResult = parseParenthetical(parentheticalContent)\n metaParenFromToken = true\n year = metaParenResult.year\n court = metaParenResult.court\n date = metaParenResult.date\n disposition = metaParenResult.disposition\n justices = metaParenResult.justices\n scope = metaParenResult.scope\n }\n\n // NY Slip Op unpublished marker (#231): `(U)` (older) or `[U]` (newer)\n // appears immediately after the page number and must be consumed *before*\n // LOOKAHEAD_PAREN_REGEX runs, otherwise the regex captures `(U)` as the\n // court parenthetical and produces `court = \"U\"`. Detected once and used\n // both in the in-token paren path and the lookahead path.\n let unpublished = false\n if (cleanedText) {\n const afterTokenForFlag = cleanedText.substring(span.cleanEnd)\n if (/^\\s*(?:\\(U\\)|\\[U\\])/.test(afterTokenForFlag)) {\n unpublished = true\n }\n }\n\n // Parallel-cite chain skip: when this cite is followed by another citation\n // separated only by parallel-chain junk (commas, whitespace, digit/dash\n // runs for intervening pincites), the shared trailing parenthetical sits\n // AFTER the last cite in the chain — e.g., `329 Pa. 256, 198 A. 154 (1938)`\n // or `410 U.S. 113, 117, 93 S. Ct. 705 (1973)` where the `, 117,` is the\n // first cite's pincite. Compute the post-chain start position once and\n // share it between the look-ahead paren scan and `collectParentheticals`\n // so the trailing year paren is found AND fullSpan extends through it.\n let postChainStart = span.cleanEnd\n if (cleanedText && siblings && siblings.length > 0) {\n const CHAIN_BRIDGE_REGEX = /^[\\s,\\d\\-–—]*$/\n while (true) {\n const next = siblings.find(\n (s) =>\n s.cleanStart > postChainStart &&\n CHAIN_BRIDGE_REGEX.test(\n cleanedText.substring(postChainStart, s.cleanStart),\n ),\n )\n if (!next) break\n postChainStart = next.cleanEnd\n }\n }\n\n // Look ahead in cleaned text for parenthetical after the token\n // Tokenization patterns only capture volume-reporter-page, so parentheticals\n // like \"(1989)\" or \"(9th Cir. 2020)\" are not in the token text.\n if (cleanedText && !parentheticalContent) {\n // The pincite scan below must still operate on the original afterToken\n // (starting at span.cleanEnd) so `, 117` is parseable as this cite's\n // pincite; the paren scan uses the post-chain window instead.\n const afterToken = cleanedText.substring(span.cleanEnd)\n let parenAfterToken =\n postChainStart === span.cleanEnd\n ? afterToken\n : cleanedText.substring(postChainStart)\n // Consume any leading (U)/[U] marker so the real court paren is found.\n const unpubMatch = /^\\s*(?:\\(U\\)|\\[U\\])/.exec(parenAfterToken)\n if (unpubMatch) {\n parenAfterToken = parenAfterToken.substring(unpubMatch[0].length)\n }\n const lookAheadMatch = LOOKAHEAD_PAREN_REGEX.exec(parenAfterToken)\n if (lookAheadMatch) {\n parentheticalContent = lookAheadMatch[1]\n // Parse parenthetical using unified parser\n metaParenResult = parseParenthetical(parentheticalContent)\n metaParenFromToken = false\n year = metaParenResult.year\n court = metaParenResult.court\n date = metaParenResult.date\n disposition = metaParenResult.disposition\n justices = metaParenResult.justices\n scope = metaParenResult.scope\n }\n\n // Extract pincite from look-ahead independently of the parenthetical match.\n // A citation can carry a pincite without a trailing court/year parenthetical,\n // e.g. \"2020 NY Slip Op 00001 at *2.\" — the second occurrence is classified\n // as a full-case cite (because shortFormCase requires no page between reporter\n // and \"at\"), but the pincite is still meaningful data. See #191.\n if (pincite === undefined) {\n const laPinciteMatch = LOOKAHEAD_PINCITE_REGEX.exec(afterToken)\n if (laPinciteMatch) {\n if (!pinciteInfo) {\n pinciteInfo = parsePincite(laPinciteMatch[1]) ?? undefined\n }\n pincite = pinciteInfo?.page\n // Pincite span: indices are relative to afterToken (which starts at span.cleanEnd)\n if (laPinciteMatch.indices?.[1]) {\n spans.pincite = spanFromGroupIndex(\n span.cleanEnd,\n laPinciteMatch.indices[1],\n transformationMap,\n )\n }\n\n // Multiple discrete pincites (#247): continue scanning for additional\n // comma-separated pincites (`, 115, 153, 200`). Each entry is parsed\n // through `parsePincite` so range / footnote / paragraph semantics\n // inside the chain are preserved. The convenience `pincite` field\n // continues to point at the primary; consumers walk `additionalPincites`.\n if (pinciteInfo) {\n const additionalPincites: PinciteInfo[] = []\n let scanStart =\n (laPinciteMatch.index ?? 0) + laPinciteMatch[0].length\n while (scanStart < afterToken.length) {\n const remainder = afterToken.substring(scanStart)\n const addMatch = ADDITIONAL_PINCITE_REGEX.exec(remainder)\n if (!addMatch) break\n const addInfo = parsePincite(addMatch[1])\n if (!addInfo) break\n additionalPincites.push(addInfo)\n scanStart += addMatch[0].length\n }\n if (additionalPincites.length > 0) {\n pinciteInfo = { ...pinciteInfo, additionalPincites }\n }\n }\n }\n }\n }\n\n // Classify chained parentheticals: extract disposition and explanatory content\n let parentheticals: Parenthetical[] | undefined\n let allParens: RawParenthetical[] | undefined\n let collected: CollectedParentheticals | undefined\n if (cleanedText) {\n // Use postChainStart so fullSpan / chained-paren classification can see\n // the shared trailing paren that sits past a parallel-cite chain.\n collected = collectParentheticals(cleanedText, postChainStart)\n allParens = collected.parens\n // Skip first paren (already parsed above as court/year)\n const remaining = parentheticalContent ? allParens.slice(1) : allParens\n for (const raw of remaining) {\n const classified = classifyParenthetical(raw.text)\n if (classified.kind === \"metadata\") {\n // Accept court from later metadata parens if we don't have a real one.\n // The primary parse can set court to the disposition text (e.g., \"en banc\")\n // as a side effect of stripDateFromCourt, so treat that as unset.\n if (classified.court && (!court || court === disposition)) {\n court = classified.court\n }\n if (classified.year && !year) {\n year = classified.year\n date = classified.date\n }\n if (classified.disposition && !disposition) {\n disposition = classified.disposition\n }\n if (classified.justices && !justices) {\n justices = classified.justices\n }\n if (classified.scope && !scope) {\n scope = classified.scope\n }\n } else {\n parentheticals ??= []\n const parenOrig = resolveOriginalSpan(\n { cleanStart: raw.start, cleanEnd: raw.end },\n transformationMap,\n )\n parentheticals.push({\n text: classified.text,\n type: classified.type,\n span: {\n cleanStart: raw.start,\n cleanEnd: raw.end,\n originalStart: parenOrig.originalStart,\n originalEnd: parenOrig.originalEnd,\n },\n })\n }\n }\n }\n\n // Metadata parenthetical span (the first paren that yielded court/year)\n if (allParens && allParens.length > 0 && (court || year)) {\n const metaParen = parentheticalContent ? allParens[0] : undefined\n if (metaParen) {\n const metaOrig = resolveOriginalSpan(\n { cleanStart: metaParen.start, cleanEnd: metaParen.end },\n transformationMap,\n )\n spans.metadataParenthetical = {\n cleanStart: metaParen.start,\n cleanEnd: metaParen.end,\n originalStart: metaOrig.originalStart,\n originalEnd: metaOrig.originalEnd,\n }\n\n // Court and year spans from parseParenthetical content offsets.\n // The content starts at metaParen.start + 1 (past the opening \"(\").\n if (metaParenResult) {\n const contentStart = metaParen.start + 1\n if (metaParenResult.courtStart !== undefined) {\n const courtCS = contentStart + metaParenResult.courtStart\n const courtCE = contentStart + metaParenResult.courtEnd!\n const courtOrig = resolveOriginalSpan(\n { cleanStart: courtCS, cleanEnd: courtCE },\n transformationMap,\n )\n spans.court = {\n cleanStart: courtCS,\n cleanEnd: courtCE,\n originalStart: courtOrig.originalStart,\n originalEnd: courtOrig.originalEnd,\n }\n }\n if (metaParenResult.yearStart !== undefined) {\n const yearCS = contentStart + metaParenResult.yearStart\n const yearCE = contentStart + metaParenResult.yearEnd!\n const yearOrig = resolveOriginalSpan(\n { cleanStart: yearCS, cleanEnd: yearCE },\n transformationMap,\n )\n spans.year = {\n cleanStart: yearCS,\n cleanEnd: yearCE,\n originalStart: yearOrig.originalStart,\n originalEnd: yearOrig.originalEnd,\n }\n }\n }\n }\n }\n\n // Build subsequentHistoryEntries from captured signals (already normalized\n // during collection to avoid a second SIGNAL_TABLE scan).\n // Texas Greenbook writ/petition history (#229) lives *inside* the\n // court-and-year parenthetical, so it's captured by parseParenthetical's\n // `internalHistory` field rather than the between-parens collector. Emit\n // it first so it appears at order=0 in the chain — it semantically precedes\n // any later signals between separate parens.\n let subsequentHistoryEntries: SubsequentHistoryEntry[] | undefined\n if (cleanedText && metaParenResult?.internalHistory && allParens && allParens.length > 0) {\n const metaParen = parentheticalContent ? allParens[0] : undefined\n if (metaParen) {\n const contentStart = metaParen.start + 1\n const ih = metaParenResult.internalHistory\n const sigCleanStart = contentStart + ih.start\n const sigCleanEnd = contentStart + ih.end\n const { originalStart: sigOrigStart, originalEnd: sigOrigEnd } =\n resolveOriginalSpan(\n { cleanStart: sigCleanStart, cleanEnd: sigCleanEnd },\n transformationMap,\n )\n subsequentHistoryEntries ??= []\n subsequentHistoryEntries.push({\n signal: ih.signal,\n rawSignal: ih.rawSignal,\n signalSpan: {\n cleanStart: sigCleanStart,\n cleanEnd: sigCleanEnd,\n originalStart: sigOrigStart,\n originalEnd: sigOrigEnd,\n },\n order: 0,\n })\n }\n }\n if (cleanedText && collected && collected.signals.length > 0) {\n for (let i = 0; i < collected.signals.length; i++) {\n const { signal: rawSig } = collected.signals[i]\n subsequentHistoryEntries ??= []\n const { originalStart: sigOrigStart, originalEnd: sigOrigEnd } = resolveOriginalSpan(\n { cleanStart: rawSig.start, cleanEnd: rawSig.end },\n transformationMap,\n )\n subsequentHistoryEntries.push({\n signal: rawSig.normalized,\n rawSignal: rawSig.text,\n signalSpan: {\n cleanStart: rawSig.start,\n cleanEnd: rawSig.end,\n originalStart: sigOrigStart,\n originalEnd: sigOrigEnd,\n },\n order: subsequentHistoryEntries.length,\n })\n }\n }\n\n // Infer court level/jurisdiction from reporter series\n const inferredCourt = inferCourtFromReporter(reporter)\n\n // Backward compat: set court string for SCOTUS when not already extracted\n if (!court && inferredCourt?.level === \"supreme\" && inferredCourt?.jurisdiction === \"federal\") {\n court = \"scotus\"\n }\n\n // Phase 6: Extract case name via backward search.\n // Bound the lookback by the previous sibling token's end (if any) so the\n // backward walk for a parallel cite (e.g., the `198 A. 154` half of\n // `Nixon v. Nixon, 329 Pa. 256, 198 A. 154`) does not absorb the earlier\n // reporter cite into the case name.\n let caseNameLookback: number | undefined\n if (siblings && siblings.length > 0) {\n const prev = siblings\n .filter((s) => s.cleanEnd <= span.cleanStart)\n .reduce<{ cleanEnd: number } | undefined>(\n (best, s) =>\n !best || s.cleanEnd > best.cleanEnd ? s : best,\n undefined,\n )\n if (prev) {\n caseNameLookback = span.cleanStart - prev.cleanEnd\n }\n }\n let caseNameResult: ReturnType<typeof extractCaseName> | undefined\n if (cleanedText) {\n caseNameResult = extractCaseName(\n cleanedText,\n span.cleanStart,\n caseNameLookback,\n {\n originalText,\n transformationMap,\n },\n )\n if (caseNameResult) {\n caseName = caseNameResult.caseName\n\n // CSM year-first form puts the year *before* volume-reporter-page\n // (`In re K.F. (2009) 173 Cal.App.4th 655` — #19). Pick it up here when\n // there's no trailing court parenthetical to recover it from. Don't\n // overwrite a year already parsed from a trailing paren — the trailing\n // paren may also carry court information that the year-first paren lacks.\n if (caseNameResult.year && !year) {\n year = caseNameResult.year\n if (\n caseNameResult.yearStart !== undefined &&\n caseNameResult.yearEnd !== undefined &&\n !spans.year\n ) {\n const yearOrig = resolveOriginalSpan(\n {\n cleanStart: caseNameResult.yearStart,\n cleanEnd: caseNameResult.yearEnd,\n },\n transformationMap,\n )\n spans.year = {\n cleanStart: caseNameResult.yearStart,\n cleanEnd: caseNameResult.yearEnd,\n originalStart: yearOrig.originalStart,\n originalEnd: yearOrig.originalEnd,\n }\n }\n }\n\n // Louisiana docket-prefix paren metadata transfer (#232). When a Louisiana\n // citation places `NN-NNNN (La. ... M/D/YY)` between the caption and the\n // reporter, the trailing reporter citation typically carries no court\n // paren of its own — pull court/year/date from the docket paren so the\n // citation surfaces structured metadata instead of dropping it.\n if (caseNameResult.precedingDocketMeta) {\n const meta = caseNameResult.precedingDocketMeta\n if (!year) year = meta.year\n if (!court) court = meta.court\n if (!date) date = meta.date\n }\n\n // Calculate fullSpan: case name start through parenthetical end\n // Reuse allParens from classify loop to avoid scanning twice\n const parenEnd =\n allParens && allParens.length > 0 ? allParens[allParens.length - 1].end : span.cleanEnd\n const fullCleanStart = caseNameResult.nameStart\n const fullCleanEnd = parenEnd\n\n // Translate to original positions\n const fullOriginalStart =\n transformationMap.cleanToOriginal.get(fullCleanStart) ?? fullCleanStart\n const fullOriginalEnd = transformationMap.cleanToOriginal.get(fullCleanEnd) ?? fullCleanEnd\n\n fullSpan = {\n cleanStart: fullCleanStart,\n cleanEnd: fullCleanEnd,\n originalStart: fullOriginalStart,\n originalEnd: fullOriginalEnd,\n }\n\n // Case name span — computed BEFORE signal stripping rebuilds caseName\n const caseNameCleanStart = caseNameResult.nameStart\n const caseNameCleanEnd = caseNameCleanStart + caseName!.length\n const caseNameOrig = resolveOriginalSpan(\n { cleanStart: caseNameCleanStart, cleanEnd: caseNameCleanEnd },\n transformationMap,\n )\n spans.caseName = {\n cleanStart: caseNameCleanStart,\n cleanEnd: caseNameCleanEnd,\n originalStart: caseNameOrig.originalStart,\n originalEnd: caseNameOrig.originalEnd,\n }\n }\n }\n\n // Parallel-cite fullSpan fallback: when this cite is a secondary parallel\n // (no case-name extracted because the bounded lookback hits the prior\n // cite's end) AND there is a close preceding sibling indicating a parallel\n // chain, still extend fullSpan through the shared trailing paren so\n // string-citation grouping and downstream span consumers see the full\n // citation extent. The bare cite's own cleanStart anchors the lower bound.\n // Cites without a preceding sibling (e.g., a standalone `500 F.2d 123 (2020)`\n // with no caption) intentionally do not get a fullSpan — that's existing\n // contract: \"no case name → no fullSpan\".\n const hasCloseParallelPrev =\n caseNameLookback !== undefined && caseNameLookback < 30\n if (\n !fullSpan &&\n hasCloseParallelPrev &&\n allParens &&\n allParens.length > 0\n ) {\n const lastParen = allParens[allParens.length - 1]\n if (lastParen.end > span.cleanEnd) {\n const fullCleanStart = span.cleanStart\n const fullCleanEnd = lastParen.end\n fullSpan = {\n cleanStart: fullCleanStart,\n cleanEnd: fullCleanEnd,\n originalStart:\n transformationMap.cleanToOriginal.get(fullCleanStart) ??\n fullCleanStart,\n originalEnd:\n transformationMap.cleanToOriginal.get(fullCleanEnd) ?? fullCleanEnd,\n }\n }\n }\n\n // Phase 7: Extract party names from case name\n let plaintiff: string | undefined\n let plaintiffNormalized: string | undefined\n let defendant: string | undefined\n let defendantNormalized: string | undefined\n let proceduralPrefix: string | undefined\n let adminParenthetical: string | undefined\n\n let signal: CitationSignal | undefined\n if (caseName) {\n const partyResult = extractPartyNames(caseName)\n plaintiff = partyResult.plaintiff\n plaintiffNormalized = partyResult.plaintiffNormalized\n defendant = partyResult.defendant\n defendantNormalized = partyResult.defendantNormalized\n proceduralPrefix = partyResult.proceduralPrefix\n signal = partyResult.signal\n adminParenthetical = partyResult.adminParenthetical\n\n // Rebuild caseName when extractPartyNames modified the plaintiff (signal stripped,\n // \"In\"/\"Also\" prefix removed, etc.). Find the plaintiff's actual position in the\n // cleaned text to update fullSpan and caseName span. Bankruptcy admin\n // parenthetical is preserved as part of the rebuilt caseName so it remains\n // visible to consumers even though it's stripped off the `defendant` field.\n if (plaintiff && defendant) {\n const adminSuffix = adminParenthetical ? ` (${adminParenthetical})` : \"\"\n // Preserve the source's `v` punctuation form when rebuilding (#326).\n // The existing caseName already carries the right separator (set by\n // extractCaseName / V_CASE_NAME_REGEX); detect it and reuse.\n const existingSepMatch = caseName ? /\\s+(vs?\\.?)\\s+/.exec(caseName) : null\n const rebuildSep = existingSepMatch?.[1] ?? \"v.\"\n const rebuiltName = `${plaintiff} ${rebuildSep} ${defendant}${adminSuffix}`\n if (rebuiltName !== caseName && fullSpan && cleanedText) {\n caseName = rebuiltName\n\n // Advance fullSpan.cleanStart to where the plaintiff actually starts\n const prefixRegion = cleanedText.substring(fullSpan.cleanStart, span.cleanStart)\n const vSep = /\\s+vs?\\.?\\s+/i.exec(prefixRegion)\n if (vSep) {\n const beforeV = prefixRegion.substring(0, vSep.index)\n const pIdx = beforeV.lastIndexOf(plaintiff)\n if (pIdx !== -1) {\n const newCleanStart = fullSpan.cleanStart + pIdx\n const newOriginalStart =\n transformationMap.cleanToOriginal.get(newCleanStart) ?? newCleanStart\n fullSpan = { ...fullSpan, cleanStart: newCleanStart, originalStart: newOriginalStart }\n }\n }\n\n // Update caseName span to reflect the cleaned name\n if (caseNameResult) {\n const strippedCleanStart = fullSpan.cleanStart\n const strippedCleanEnd = strippedCleanStart + caseName.length\n const strippedOrig = resolveOriginalSpan(\n { cleanStart: strippedCleanStart, cleanEnd: strippedCleanEnd },\n transformationMap,\n )\n spans.caseName = {\n cleanStart: strippedCleanStart,\n cleanEnd: strippedCleanEnd,\n originalStart: strippedOrig.originalStart,\n originalEnd: strippedOrig.originalEnd,\n }\n }\n }\n }\n\n // Plaintiff and defendant spans — split the search region at the \"v.\" separator\n // so each name is only matched on the correct side, avoiding indexOf collisions\n // when a name substring appears in both halves (e.g., \"Smith v. Smith\").\n if (plaintiff && caseNameResult && cleanedText) {\n const nameAnchor = fullSpan?.cleanStart ?? caseNameResult.nameStart\n const searchRegion = cleanedText.substring(nameAnchor, span.cleanStart)\n const vSepMatch = /\\s+vs?\\.?\\s+/i.exec(searchRegion)\n if (vSepMatch) {\n // Plaintiff: search only in the region before \"v.\"\n const plaintiffRegion = searchRegion.substring(0, vSepMatch.index)\n const pIdx = plaintiffRegion.lastIndexOf(plaintiff)\n if (pIdx !== -1) {\n const pCleanStart = nameAnchor + pIdx\n const pCleanEnd = pCleanStart + plaintiff.length\n const pOrig = resolveOriginalSpan(\n { cleanStart: pCleanStart, cleanEnd: pCleanEnd },\n transformationMap,\n )\n spans.plaintiff = {\n cleanStart: pCleanStart,\n cleanEnd: pCleanEnd,\n originalStart: pOrig.originalStart,\n originalEnd: pOrig.originalEnd,\n }\n }\n // Defendant: search only in the region after \"v.\"\n if (defendant) {\n const defRegionStart = vSepMatch.index + vSepMatch[0].length\n const defendantRegion = searchRegion.substring(defRegionStart)\n const dIdx = defendantRegion.indexOf(defendant)\n if (dIdx !== -1) {\n const dCleanStart = nameAnchor + defRegionStart + dIdx\n const dCleanEnd = dCleanStart + defendant.length\n const dOrig = resolveOriginalSpan(\n { cleanStart: dCleanStart, cleanEnd: dCleanEnd },\n transformationMap,\n )\n spans.defendant = {\n cleanStart: dCleanStart,\n cleanEnd: dCleanEnd,\n originalStart: dOrig.originalStart,\n originalEnd: dOrig.originalEnd,\n }\n }\n }\n } else {\n // No \"v.\" separator — procedural prefix case (e.g., \"In re X\").\n // Plaintiff is the full case name; no defendant to locate.\n const pIdx = searchRegion.indexOf(plaintiff)\n if (pIdx !== -1) {\n const pCleanStart = nameAnchor + pIdx\n const pCleanEnd = pCleanStart + plaintiff.length\n const pOrig = resolveOriginalSpan(\n { cleanStart: pCleanStart, cleanEnd: pCleanEnd },\n transformationMap,\n )\n spans.plaintiff = {\n cleanStart: pCleanStart,\n cleanEnd: pCleanEnd,\n originalStart: pOrig.originalStart,\n originalEnd: pOrig.originalEnd,\n }\n }\n }\n }\n\n // Signal span — the signal word was part of the original case name, found\n // at caseNameResult.nameStart. After signal stripping, fullSpan.cleanStart\n // was advanced past it, so the signal occupies [nameStart, fullSpan.cleanStart).\n if (signal && fullSpan && cleanedText && caseNameResult) {\n const sigRegion = cleanedText.substring(caseNameResult.nameStart, span.cleanStart)\n const sigMatch = SIGNAL_STRIP_REGEX.exec(sigRegion)\n if (sigMatch) {\n const sigCleanStart = caseNameResult.nameStart\n const sigCleanEnd = sigCleanStart + sigMatch[1].length\n const sigOrig = resolveOriginalSpan(\n { cleanStart: sigCleanStart, cleanEnd: sigCleanEnd },\n transformationMap,\n )\n spans.signal = {\n cleanStart: sigCleanStart,\n cleanEnd: sigCleanEnd,\n originalStart: sigOrig.originalStart,\n originalEnd: sigOrig.originalEnd,\n }\n }\n }\n }\n\n // Translate positions from clean → original (citation core only - span unchanged)\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Calculate confidence score using multi-factor model.\n // Base is low — unvalidated matches are uncertain. Real signals earn confidence.\n let confidence = 0.2\n\n // Known reporter: strong signal.\n // Check reporters-db first (precise), fall back to common reporter set.\n const reportersDb = getReportersSync()\n const dbMatch = reportersDb?.byAbbreviation.get(reporter.toLowerCase())\n if (dbMatch && dbMatch.length > 0) {\n confidence += 0.3\n } else if (COMMON_REPORTERS.has(reporter)) {\n confidence += 0.3\n }\n\n // Year present and plausible: moderate signal\n if (year !== undefined) {\n if (year <= CURRENT_YEAR) {\n confidence += 0.2\n }\n }\n\n // Case name found: moderate signal\n if (caseName) {\n confidence += 0.15\n }\n\n // Court identified: confirmatory signal\n if (court) {\n confidence += 0.1\n }\n\n // Cap at 1.0 and round to avoid floating point artifacts (e.g., 0.7999...9)\n confidence = Math.round(Math.min(confidence, 1.0) * 100) / 100\n\n // Blank page citations: intentional placeholders (3+ underscores/dashes in legal\n // briefs). The pattern is very specific so they deserve at least moderate confidence,\n // but don't let them exceed the signals they actually have.\n if (hasBlankPage) {\n confidence = Math.max(confidence, 0.5)\n }\n\n return {\n type: \"case\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0, // Placeholder - timing handled by orchestration layer\n patternsChecked: 1, // Single token processed\n volume,\n reporter,\n page,\n nominativeVolume,\n nominativeReporter,\n pincite,\n pinciteInfo,\n court,\n normalizedCourt: normalizeCourt(court),\n year,\n hasBlankPage,\n date,\n fullSpan,\n caseName,\n disposition,\n parentheticals,\n subsequentHistoryEntries,\n ...(unpublished ? { unpublished: true } : {}),\n ...(justices ? { justices } : {}),\n ...(scope ? { scope } : {}),\n ...(adminParenthetical ? { adminParenthetical } : {}),\n plaintiff,\n plaintiffNormalized,\n defendant,\n defendantNormalized,\n proceduralPrefix,\n inferredCourt,\n signal,\n spans,\n }\n}\n","/**\n * Constitutional Citation Regex Patterns\n *\n * Patterns for U.S. Constitution, state constitutions, and bare \"Const.\" citations.\n * Intentionally broad for tokenization — extraction layer parses structured fields.\n *\n * Four patterns (ordered by specificity):\n * - us-constitution: \"U.S. Const. art. III, § 2\"\n * - state-constitution: \"Cal. Const. art. I, § 7\"\n * - bare-constitution: \"Const. art. I, § 8, cl. 3\"\n * - bare-article: \"Art. I, §8, cl. 3\" (requires Roman numeral + § section)\n */\n\nimport type { Pattern } from \"./casePatterns\"\n\n// Shared tail: art./amend. + numeral + optional § section + optional cl. clause\n// Roman numerals: I, II, III, IV, V, VI, VII, VIII, IX, X, XI, XII, XIII, XIV, XV, XVI, XVII, XVIII, XIX, XX, XXI, XXII, XXIII, XXIV, XXV, XXVI, XXVII\n// Also accepts Arabic numerals as fallback\nconst ARTICLE_OR_AMENDMENT = String.raw`(?:art(?:icle)?\\.?|amend(?:ment)?\\.?|amdt\\.?)\\s+([IVX]+|\\d+)`\nconst OPTIONAL_SECTION = String.raw`(?:[,;]\\s*§\\s*([\\w-]+))?`\nconst OPTIONAL_CLAUSE = String.raw`(?:[,;]\\s*cl\\.?\\s*(\\d+))?`\nconst BODY_TAIL = `${ARTICLE_OR_AMENDMENT}${OPTIONAL_SECTION}${OPTIONAL_CLAUSE}`\n\n/** Compiled body regex shared with the extractor to avoid duplicate definitions. */\nexport const CONSTITUTIONAL_BODY_RE: RegExp = new RegExp(BODY_TAIL, \"id\")\n\nexport const constitutionalPatterns: Pattern[] = [\n {\n id: \"us-constitution\",\n regex: new RegExp(\n String.raw`\\b(?:United\\s+States\\s+Constitution|U\\.?\\s*S\\.?\\s+Const\\.?),?\\s+${BODY_TAIL}`,\n \"gi\",\n ),\n description:\n 'U.S. Constitution citations (e.g., \"U.S. Const. art. III, § 2\", \"U.S. Const. amend. XIV\")',\n type: \"constitutional\",\n },\n {\n id: \"state-constitution\",\n // Separator between state abbrev and `Const.` uses `(?:\\.\\s*|\\s+)`:\n // accepts canonical `Pa. Const.`, abbreviated no-space `Pa.Const.`\n // (#329), and bare-space `Pa Const.`. The `.` branch requires a dot\n // (forces a separator), so `PaConst.` does not match — avoids false\n // positives from words that happen to start with a state stem.\n regex: new RegExp(\n String.raw`\\b(?:Ala|Alaska|Ariz|Ark|Cal(?:if)?|Colo|Conn|Del|Fla|Ga|Haw|Idaho|Ill|Ind|Iowa|Kan|Ky|La|Me|Md|Mass|Mich|Minn|Miss|Mo|Mont|Neb|Nev|N\\.?\\s*H|N\\.?\\s*J|N\\.?\\s*M|N\\.?\\s*Y|N\\.?\\s*C|N\\.?\\s*D|Ohio|Okla|Or(?:e)?|Pa|R\\.?\\s*I|S\\.?\\s*C|S\\.?\\s*D|Tenn|Tex|Utah|Vt|W\\.?\\s*Va|Va|Wash|Wis|Wyo)(?:\\.\\s*|\\s+)Const\\.?,?\\s+${BODY_TAIL}`,\n \"gi\",\n ),\n description:\n 'State constitution citations (e.g., \"Cal. Const. art. I, § 7\", \"N.Y. Const. art. VI, § 20\", \"Pa.Const. art. VIII, § 4\")',\n type: \"constitutional\",\n },\n {\n id: \"bare-constitution\",\n // \"g\" (not \"gi\") is intentional: the lookbehind uses [A-Z] which requires case sensitivity.\n // Consequence: lowercase \"const.\" is never matched — acceptable in formal legal citations.\n // Consequence: all-caps preceding words like \"THE Const.\" won't match due to [A-Z]\\s lookbehind — rare, acceptable tradeoff.\n // Known limitation: multi-character state abbreviations ending in lowercase (Alaska, Idaho, etc.)\n // bypass the lookbehind and produce a second bare match — tokenizer span dedup handles this.\n regex: new RegExp(String.raw`(?<!\\.\\s)(?<![A-Z]\\s)\\bConst\\.?,?\\s+${BODY_TAIL}`, \"g\"),\n description:\n 'Bare constitutional citations without jurisdiction prefix (e.g., \"Const. art. I, § 8, cl. 3\")',\n type: \"constitutional\",\n },\n {\n id: \"bare-article\",\n // Lowest-priority pattern: bare \"Art.\" with no \"Const.\" prefix at all.\n // Constrained to reduce false positives: Roman numerals only (no Arabic),\n // must include a § section reference, and lookbehind rejects \"Const.\" prefix\n // (already handled by higher-priority patterns). Confidence set to 0.5 in extractor.\n regex: new RegExp(\n String.raw`(?<!Const\\.?,?\\s)\\bArt\\.?\\s+[IVX]+[,;]\\s*§\\s*[\\w-]+(?:[,;]\\s*cl\\.?\\s*\\d+)?`,\n \"g\",\n ),\n description:\n 'Bare article references without \"Const.\" prefix (e.g., \"Art. I, §8, cl. 3\")',\n type: \"constitutional\",\n },\n]\n","/**\n * Constitutional Citation Extraction\n *\n * Parses tokenized constitutional citations to extract jurisdiction,\n * article/amendment, section, and clause fields.\n *\n * Dispatch by patternId:\n * - \"us-constitution\" → jurisdiction: \"US\"\n * - \"state-constitution\" → jurisdiction mapped from state abbreviation\n * - \"bare-constitution\" → jurisdiction: undefined\n *\n * @module extract/extractConstitutional\n */\n\nimport { CONSTITUTIONAL_BODY_RE } from \"@/patterns/constitutionalPatterns\"\nimport type { Token } from \"@/tokenize\"\nimport type { ConstitutionalCitation } from \"@/types/citation\"\nimport type { ConstitutionalComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\n\n/**\n * Roman numeral lookup table (I–XXVII).\n * Covers all U.S. constitutional articles (I–VII) and amendments (I–XXVII).\n */\nconst ROMAN_TO_INT: Record<string, number> = {\n I: 1,\n II: 2,\n III: 3,\n IV: 4,\n V: 5,\n VI: 6,\n VII: 7,\n VIII: 8,\n IX: 9,\n X: 10,\n XI: 11,\n XII: 12,\n XIII: 13,\n XIV: 14,\n XV: 15,\n XVI: 16,\n XVII: 17,\n XVIII: 18,\n XIX: 19,\n XX: 20,\n XXI: 21,\n XXII: 22,\n XXIII: 23,\n XXIV: 24,\n XXV: 25,\n XXVI: 26,\n XXVII: 27,\n}\n\n/** Parse a Roman numeral or Arabic number string to an integer. */\nfunction parseNumeral(raw: string): number | undefined {\n const upper = raw.toUpperCase()\n if (upper in ROMAN_TO_INT) return ROMAN_TO_INT[upper]\n const n = Number.parseInt(raw, 10)\n return Number.isNaN(n) ? undefined : n\n}\n\n/**\n * State abbreviation → 2-letter code mapping.\n * Keys are lowercase abbreviation stems (without trailing period).\n */\nconst STATE_ABBREV_TO_CODE: Record<string, string> = {\n ala: \"AL\",\n alaska: \"AK\",\n ariz: \"AZ\",\n ark: \"AR\",\n cal: \"CA\",\n calif: \"CA\",\n colo: \"CO\",\n conn: \"CT\",\n del: \"DE\",\n fla: \"FL\",\n ga: \"GA\",\n haw: \"HI\",\n idaho: \"ID\",\n ill: \"IL\",\n ind: \"IN\",\n iowa: \"IA\",\n kan: \"KS\",\n ky: \"KY\",\n la: \"LA\",\n me: \"ME\",\n md: \"MD\",\n mass: \"MA\",\n mich: \"MI\",\n minn: \"MN\",\n miss: \"MS\",\n mo: \"MO\",\n mont: \"MT\",\n neb: \"NE\",\n nev: \"NV\",\n \"n.h\": \"NH\",\n \"n.j\": \"NJ\",\n \"n.m\": \"NM\",\n \"n.y\": \"NY\",\n \"n.c\": \"NC\",\n \"n.d\": \"ND\",\n ohio: \"OH\",\n okla: \"OK\",\n or: \"OR\",\n ore: \"OR\",\n pa: \"PA\",\n \"r.i\": \"RI\",\n \"s.c\": \"SC\",\n \"s.d\": \"SD\",\n tenn: \"TN\",\n tex: \"TX\",\n utah: \"UT\",\n vt: \"VT\",\n va: \"VA\",\n wash: \"WA\",\n \"w.va\": \"WV\",\n wis: \"WI\",\n wyo: \"WY\",\n}\n\nconst IS_AMENDMENT_RE = /amend|amdt/i\n\n/**\n * Regex to extract the state abbreviation prefix from state-constitution tokens.\n *\n * Trailing `\\.?\\s*Const` (rather than `\\.?\\s+Const`) accepts both the\n * canonical spaced form (`Pa. Const.`) and the abbreviated no-space form\n * (`Pa.Const.`, `N.Y.Const.`) introduced in #329. The greedy `[A-Za-z]+`\n * still backtracks correctly so the prefix capture stops at the state\n * abbreviation rather than swallowing `Const`.\n */\nconst STATE_PREFIX_RE = /^([A-Za-z]+(?:\\.\\s*[A-Za-z]+)?(?:\\.\\s*[A-Za-z]+)?)\\.?\\s*Const/i\n\n/**\n * Resolve state abbreviation from token text to 2-letter code.\n */\nfunction resolveStateJurisdiction(text: string): string | undefined {\n const prefixMatch = STATE_PREFIX_RE.exec(text)\n if (!prefixMatch) return undefined\n\n // Normalize: collapse spaces, lowercase, remove trailing dots\n const raw = prefixMatch[1].replace(/\\s+/g, \"\").replace(/\\.$/g, \"\").toLowerCase()\n\n if (raw in STATE_ABBREV_TO_CODE) return STATE_ABBREV_TO_CODE[raw]\n\n return undefined\n}\n\n/**\n * Extract a constitutional citation from a tokenized match.\n *\n * @param token - Tokenized citation candidate from the tokenizer\n * @param transformationMap - Maps cleaned text positions to original text positions\n * @returns Parsed constitutional citation with structured fields\n */\nexport function extractConstitutional(\n token: Token,\n transformationMap: TransformationMap,\n): ConstitutionalCitation {\n const { text, span } = token\n\n const bodyMatch = CONSTITUTIONAL_BODY_RE.exec(text)\n\n let article: number | undefined\n let amendment: number | undefined\n let section: string | undefined\n let clause: number | undefined\n\n if (bodyMatch) {\n const numeral = parseNumeral(bodyMatch[1])\n\n if (IS_AMENDMENT_RE.test(bodyMatch[0])) {\n amendment = numeral\n } else {\n article = numeral\n }\n\n section = bodyMatch[2] || undefined\n clause = bodyMatch[3] ? Number.parseInt(bodyMatch[3], 10) : undefined\n }\n\n let jurisdiction: string | undefined\n switch (token.patternId) {\n case \"us-constitution\":\n jurisdiction = \"US\"\n break\n case \"state-constitution\":\n jurisdiction = resolveStateJurisdiction(text)\n break\n default:\n jurisdiction = undefined\n break\n }\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let confidence: number\n if (token.patternId === \"bare-article\") {\n confidence = 0.5\n } else if (token.patternId === \"bare-constitution\") {\n confidence = 0.7\n } else if (section) {\n confidence = 0.95\n } else {\n confidence = 0.9\n }\n\n // The section regex may greedily consume a sentence-terminating period (\"§ 1.\")\n const matchedText = text.endsWith(\".\") ? text.slice(0, -1) : text\n\n // Build component spans\n const spans: ConstitutionalComponentSpans = {}\n\n // Jurisdiction span: find the jurisdiction text in the token\n if (jurisdiction === \"US\") {\n const usIdx = text.indexOf(\"U.S.\")\n if (usIdx !== -1) {\n spans.jurisdiction = spanFromGroupIndex(span.cleanStart, [usIdx, usIdx + 4], transformationMap)\n }\n } else if (jurisdiction && token.patternId === \"state-constitution\") {\n // State prefix is at the start of the token text\n const prefixMatch = STATE_PREFIX_RE.exec(text)\n if (prefixMatch) {\n // The prefix is the abbreviation stem; add 1 for the trailing period\n const prefixEnd = prefixMatch[1].length + 1\n spans.jurisdiction = spanFromGroupIndex(span.cleanStart, [0, prefixEnd], transformationMap)\n }\n }\n\n // Body match groups for article/amendment, section, clause\n // bodyMatch.indices[n] gives positions relative to the full token text\n if (bodyMatch?.indices) {\n if (IS_AMENDMENT_RE.test(bodyMatch[0])) {\n if (bodyMatch.indices[1]) {\n spans.amendment = spanFromGroupIndex(span.cleanStart, bodyMatch.indices[1], transformationMap)\n }\n } else {\n if (bodyMatch.indices[1]) {\n spans.article = spanFromGroupIndex(span.cleanStart, bodyMatch.indices[1], transformationMap)\n }\n }\n if (bodyMatch.indices[2]) {\n spans.section = spanFromGroupIndex(span.cleanStart, bodyMatch.indices[2], transformationMap)\n }\n if (bodyMatch.indices[3]) {\n spans.clause = spanFromGroupIndex(span.cleanStart, bodyMatch.indices[3], transformationMap)\n }\n }\n\n return {\n type: \"constitutional\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText,\n processTimeMs: 0,\n patternsChecked: 1,\n jurisdiction,\n article,\n amendment,\n section,\n clause,\n spans,\n }\n}\n","/**\n * Docket-Number Citation Extraction\n *\n * Parses tokenized docket citations of the form\n * `Party v. Party, No. <docket> (<court> <year>)`\n *\n * Disambiguation strategy: a bare `No. 51 (N.Y. 2023)` is too generic to\n * extract on its own. The extractor backward-searches for a case-name\n * anchor and only emits a `DocketCitation` when one is found. Tokens\n * without a case-name anchor are silently dropped (the extractor returns\n * `undefined`).\n *\n * @module extract/extractDocket\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { DocketCitation } from \"@/types/citation\"\nimport { resolveOriginalSpan, type TransformationMap } from \"@/types/span\"\nimport { normalizeCourt } from \"./courtNormalization\"\nimport { extractCaseName, extractPartyNames, parseParenthetical } from \"./extractCase\"\n\n/**\n * Extracts a docket-number citation from a tokenized match.\n *\n * Parses token text to extract:\n * - `docketNumber`: digits with optional hyphens (e.g. \"51\", \"12-3456\")\n * - `court`, `year`, `date`: from the trailing parenthetical\n * - `caseName`, `plaintiff`, `defendant`: via backward case-name search\n *\n * Returns `undefined` when no case-name anchor is found — the bare docket\n * shape is too ambiguous to surface without context.\n *\n * Confidence: 0.7 (lower than reporter-based citations because there is no\n * reporter to validate against).\n *\n * @param token - Tokenizer output containing matched text and clean positions\n * @param transformationMap - Position mapping from clean → original text\n * @param cleanedText - Full cleaned document (needed for backward case-name search)\n * @returns DocketCitation when a case-name anchor is found, otherwise undefined\n *\n * @example\n * ```typescript\n * const text = \"IKB Int'l, S.A. v. Wells Fargo, N.A., No. 51 (N.Y. 2023).\"\n * const citation = extractDocket(token, transformationMap, text)\n * // citation = {\n * // type: \"docket\",\n * // docketNumber: \"51\",\n * // court: \"N.Y.\",\n * // year: 2023,\n * // caseName: \"IKB Int'l, S.A. v. Wells Fargo, N.A.\",\n * // ...\n * // }\n * ```\n */\nexport function extractDocket(\n token: Token,\n transformationMap: TransformationMap,\n cleanedText: string,\n originalText?: string,\n): DocketCitation | undefined {\n const { text, span } = token\n\n // Parse the token text: \"No. <docket> (<paren-content>)\"\n const tokenRegex = /\\bNo\\.\\s+([\\d]+(?:-[\\w\\d]+)*)\\s+\\(([^)]+)\\)/\n const match = tokenRegex.exec(text)\n if (!match) return undefined\n\n const docketNumber = match[1]\n const parenContent = match[2]\n\n // Backward case-name search — anchor for disambiguation. Without a\n // case-name we don't emit a citation: a bare \"No. 51 (N.Y. 2023)\" lacks\n // the context needed to be confident this is a citation at all.\n const caseNameResult = extractCaseName(cleanedText, span.cleanStart, undefined, {\n originalText,\n transformationMap,\n })\n if (!caseNameResult) return undefined\n\n // The case-name extractor's V_CASE_NAME_REGEX requires \"Party v. Party\"\n // or \"In re Party\" plus a trailing comma. Validate the result actually\n // looks like a case name (contains \"v.\" or a procedural prefix).\n const partyResult = extractPartyNames(caseNameResult.caseName)\n const hasAdversarial = partyResult.plaintiff && partyResult.defendant\n const hasProceduralPrefix = !!partyResult.proceduralPrefix\n if (!hasAdversarial && !hasProceduralPrefix) return undefined\n\n // Parse court/year/date from the parenthetical content. parseParenthetical\n // is the same helper used by extractCase, so docket citations get the\n // same court-string normalization and date handling.\n const meta = parseParenthetical(parenContent)\n\n // Resolve clean → original positions for the citation core.\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // fullSpan: case-name start through closing paren. cleanEnd is the end\n // of the matched token (after the closing `)`). When extractPartyNames\n // strips a signal word (\"See\", \"But see\", etc.) from the plaintiff,\n // advance fullCleanStart past it to mirror extractCase's behavior.\n let fullCleanStart = caseNameResult.nameStart\n if (partyResult.plaintiff && partyResult.defendant) {\n const prefixRegion = cleanedText.substring(fullCleanStart, span.cleanStart)\n const vSep = /\\s+v\\.?\\s+/i.exec(prefixRegion)\n if (vSep) {\n const beforeV = prefixRegion.substring(0, vSep.index)\n const pIdx = beforeV.lastIndexOf(partyResult.plaintiff)\n if (pIdx !== -1) fullCleanStart += pIdx\n }\n }\n const fullCleanEnd = span.cleanEnd\n const fullOriginalStart = transformationMap.cleanToOriginal.get(fullCleanStart) ?? fullCleanStart\n const fullOriginalEnd = transformationMap.cleanToOriginal.get(fullCleanEnd) ?? fullCleanEnd\n\n return {\n type: \"docket\",\n text,\n matchedText: text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence: 0.7,\n processTimeMs: 0,\n patternsChecked: 1,\n docketNumber,\n caseName:\n partyResult.plaintiff && partyResult.defendant\n ? `${partyResult.plaintiff} v. ${partyResult.defendant}`\n : caseNameResult.caseName,\n plaintiff: partyResult.plaintiff,\n defendant: partyResult.defendant,\n plaintiffNormalized: partyResult.plaintiffNormalized,\n defendantNormalized: partyResult.defendantNormalized,\n proceduralPrefix: partyResult.proceduralPrefix,\n court: meta.court,\n normalizedCourt: normalizeCourt(meta.court),\n year: meta.year,\n date: meta.date,\n fullSpan: {\n cleanStart: fullCleanStart,\n cleanEnd: fullCleanEnd,\n originalStart: fullOriginalStart,\n originalEnd: fullOriginalEnd,\n },\n }\n}\n","/**\n * Federal Register Citation Extraction\n *\n * Parses tokenized Federal Register citations to extract volume, page, and\n * optional year. Examples: \"85 Fed. Reg. 12345\", \"86 Fed. Reg. 56789 (Jan. 15, 2021)\"\n *\n * @module extract/extractFederalRegister\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { FederalRegisterCitation } from \"@/types/citation\"\nimport type { FederalRegisterComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\n\n/**\n * Extracts Federal Register citation metadata from a tokenized citation.\n *\n * Parses token text to extract:\n * - Volume: Federal Register volume number (e.g., \"85\")\n * - Page: Page number (e.g., \"12345\")\n * - Year: Optional publication year in parentheses (e.g., \"(2021)\")\n *\n * Confidence scoring:\n * - 0.9 (Federal Register format is standardized)\n *\n * @param token - Token from tokenizer containing matched text and clean positions\n * @param transformationMap - Position mapping from clean → original text\n * @returns FederalRegisterCitation with parsed metadata and translated positions\n *\n * @example\n * ```typescript\n * const token = {\n * text: \"85 Fed. Reg. 12345\",\n * span: { cleanStart: 10, cleanEnd: 28 },\n * type: \"federalRegister\",\n * patternId: \"federal-register\"\n * }\n * const citation = extractFederalRegister(token, transformationMap)\n * // citation = {\n * // type: \"federalRegister\",\n * // volume: 85,\n * // page: 12345,\n * // confidence: 0.9,\n * // ...\n * // }\n * ```\n */\nexport function extractFederalRegister(\n token: Token,\n transformationMap: TransformationMap,\n): FederalRegisterCitation {\n const { text, span } = token\n\n // Parse volume-page using regex\n // Pattern: volume (digits) + \"Fed. Reg.\" + page (digits)\n const federalRegisterRegex = /^(\\d+(?:-\\d+)?)\\s+Fed\\.\\s?Reg\\.\\s+(\\d+)/d\n const match = federalRegisterRegex.exec(text)\n\n if (!match) {\n throw new Error(`Failed to parse Federal Register citation: ${text}`)\n }\n\n const rawVolume = match[1]\n const volume = /^\\d+$/.test(rawVolume) ? Number.parseInt(rawVolume, 10) : rawVolume\n const page = Number.parseInt(match[2], 10)\n\n let spans: FederalRegisterComponentSpans | undefined\n if (match.indices) {\n spans = {\n volume: spanFromGroupIndex(span.cleanStart, match.indices[1]!, transformationMap),\n page: spanFromGroupIndex(span.cleanStart, match.indices[2]!, transformationMap),\n }\n }\n\n // Extract optional year in parentheses\n // Pattern: \"(year)\" or \"(month day, year)\"\n const yearRegex = /\\((?:.*?\\s)?(\\d{4})\\)/\n const yearMatch = yearRegex.exec(text)\n const year = yearMatch ? Number.parseInt(yearMatch[1], 10) : undefined\n\n // Translate positions from clean → original\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Confidence: 0.9 (Federal Register format is standardized)\n const confidence = 0.9\n\n return {\n type: \"federalRegister\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n volume,\n page,\n year,\n spans,\n }\n}\n","/**\n * Journal Citation Extraction\n *\n * Parses tokenized journal citations to extract volume, journal name, page,\n * and optional metadata. Examples: \"123 Harv. L. Rev. 456\", \"75 Yale L.J. 789, 791\"\n *\n * @module extract/extractJournal\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { JournalCitation } from \"@/types/citation\"\nimport type { JournalComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\n\n/**\n * Extracts journal citation metadata from a tokenized citation.\n *\n * Parses token text to extract:\n * - Volume: Leading digits (e.g., \"123\" from \"123 Harv. L. Rev. 456\")\n * - Journal: Journal abbreviation (e.g., \"Harv. L. Rev.\")\n * - Page: Starting page number (e.g., \"456\")\n * - Pincite: Optional specific page reference after comma (e.g., \", 458\")\n * - Year: Optional publication year in parenthetical (e.g., \"(2020)\")\n *\n * When `cleanedText` is provided, the extractor performs lookahead beyond the token\n * boundary to extract optional pincite and year components that the tokenizer does\n * not capture in the token text.\n *\n * Confidence scoring:\n * - Base: 0.6 (journal validation happens in Phase 3)\n *\n * Note: Author and title extraction from preceding text is not implemented\n * in Phase 2. That requires context analysis in Phase 3.\n *\n * @param token - Token from tokenizer containing matched text and clean positions\n * @param transformationMap - Position mapping from clean → original text\n * @param cleanedText - Full cleaned document text (optional; enables pincite/year lookahead)\n * @returns JournalCitation with parsed metadata and translated positions\n *\n * @example\n * ```typescript\n * const token = {\n * text: \"123 Harv. L. Rev. 456\",\n * span: { cleanStart: 10, cleanEnd: 31 },\n * type: \"journal\",\n * patternId: \"journal-standard\"\n * }\n * const citation = extractJournal(token, transformationMap)\n * // citation = {\n * // type: \"journal\",\n * // volume: 123,\n * // journal: \"Harv. L. Rev.\",\n * // abbreviation: \"Harv. L. Rev.\",\n * // page: 456,\n * // ...\n * // }\n * ```\n */\nexport function extractJournal(\n token: Token,\n transformationMap: TransformationMap,\n cleanedText?: string,\n): JournalCitation {\n const { text, span } = token\n\n // Parse volume-journal-page using regex\n // Pattern: volume (digits) + journal (letters/periods/spaces) + page (digits)\n const journalRegex = /^(\\d+(?:-\\d+)?)\\s+([A-Za-z.\\s]+?)\\s+(\\d+)/d\n const match = journalRegex.exec(text)\n\n if (!match) {\n throw new Error(`Failed to parse journal citation: ${text}`)\n }\n\n const rawVolume = match[1]\n const volume = /^\\d+$/.test(rawVolume) ? Number.parseInt(rawVolume, 10) : rawVolume\n const journal = match[2].trim()\n const page = Number.parseInt(match[3], 10)\n\n // Determine where to search for pincite/year.\n //\n // When cleanedText is available (pipeline context), we search a window starting\n // at the token end position. This handles the normal tokenizer case where the\n // tokenizer only captures the core citation (e.g., \"75 Yale L.J. 456\") and the\n // pincite/year appear immediately after in the document.\n //\n // When cleanedText is not available (manually constructed tokens in tests),\n // we fall back to the token text itself, which may contain them directly.\n const lookaheadWindow = 30\n\n // afterTokenText: text immediately after the core token match, in clean coordinates.\n // For pipeline tokens: this is a window of cleanedText starting at span.cleanEnd.\n // For manually constructed tokens (no cleanedText): the remainder of token.text after\n // the core match (text after match[0].length characters, if any).\n let afterTokenText: string\n let afterTokenCleanStart: number\n if (cleanedText !== undefined) {\n afterTokenText = cleanedText.slice(span.cleanEnd, span.cleanEnd + lookaheadWindow)\n afterTokenCleanStart = span.cleanEnd\n } else {\n // Token text may already include pincite/year (e.g., \"123 Harv. L. Rev. 456, 458\")\n // The core match ends at match[0].length within the token text.\n afterTokenText = text.slice(match[0].length)\n afterTokenCleanStart = span.cleanStart + match[0].length\n }\n\n // Build full context string (from token start) for year search\n const fullContext = cleanedText\n ? cleanedText.slice(span.cleanStart, span.cleanEnd + lookaheadWindow)\n : text\n\n // Extract optional pincite (page reference after comma) immediately after core match\n const pinciteRegex = /^,\\s*(\\d+)/d\n const pinciteMatch = pinciteRegex.exec(afterTokenText)\n const pincite = pinciteMatch ? Number.parseInt(pinciteMatch[1], 10) : undefined\n\n // Extract optional year from parenthetical (e.g., \"(2020)\") anywhere in the context\n const yearRegex = /\\((?:.*?\\s)?(\\d{4})\\)/d\n const yearMatch = yearRegex.exec(fullContext)\n const year = yearMatch ? Number.parseInt(yearMatch[1], 10) : undefined\n\n // Build component spans using match indices from `d` flag\n let spans: JournalComponentSpans | undefined\n if (match.indices) {\n spans = {\n volume: spanFromGroupIndex(span.cleanStart, match.indices[1]!, transformationMap),\n journal: spanFromGroupIndex(span.cleanStart, match.indices[2]!, transformationMap),\n page: spanFromGroupIndex(span.cleanStart, match.indices[3]!, transformationMap),\n }\n if (pinciteMatch?.indices?.[1]) {\n // pinciteMatch.indices[1] is relative to afterTokenText which starts at afterTokenCleanStart\n spans.pincite = spanFromGroupIndex(\n afterTokenCleanStart,\n pinciteMatch.indices[1],\n transformationMap,\n )\n }\n if (yearMatch?.indices?.[1]) {\n // yearMatch.indices[1] is relative to fullContext which starts at span.cleanStart\n spans.year = spanFromGroupIndex(span.cleanStart, yearMatch.indices[1], transformationMap)\n }\n }\n\n // Translate positions from clean → original\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Confidence: 0.6 base (journal validation against database happens in Phase 3)\n const confidence = 0.6\n\n return {\n type: \"journal\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n volume,\n journal,\n abbreviation: journal, // For Phase 2, abbreviation = journal name\n page,\n pincite,\n year,\n spans,\n }\n}\n","/**\n * Neutral Citation Extraction\n *\n * Parses tokenized neutral (vendor-neutral) citations to extract year, court,\n * and document number. Examples: \"2020 WL 123456\", \"2020 U.S. LEXIS 456\"\n *\n * @module extract/extractNeutral\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { NeutralCitation } from \"@/types/citation\"\nimport type { NeutralComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport type { StructuredDate } from \"./dates\"\nimport { parseParenthetical } from \"./extractCase\"\nimport { parsePincite, type PinciteInfo } from \"./pincite\"\n\n/** Matches a trailing pincite on a neutral citation. Accepts both\n * \", at *3\" (comma + \"at\" keyword) and \" at *3\" (whitespace + \"at\") forms,\n * with optional \"*\" prefix for star-pagination on both ends of a range\n * (#191, #203 — \"*3-*5\" is common on Westlaw/Lexis/NY Slip Op), and an\n * optional trailing \" n.14\" / \" nn.14-15\" footnote suffix (#202). Also\n * accepts paragraph-marker pincites `, ¶ N` / `, ¶¶ N-M` / `, paras. N-M`\n * for state neutral-cite forms like `2015-NMCA-072, ¶ 2` where the\n * paragraph numbering is the canonical pinpoint format (#311). When the\n * pincite is a paragraph form, `at` is optional. */\nconst NEUTRAL_PINCITE_LOOKAHEAD =\n /^(?:\\s+at\\s+|,\\s*(?:at\\s+(?:pp?\\.\\s*)?)?)(\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?|¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?)/d\n\n/** Trailing `(court date)` parenthetical lookahead for database cites.\n * Allows optional intervening pincite (`, at *3`) per #191. The body\n * is anything inside one set of parens. Parsing is delegated to\n * `parseParenthetical`. #294 */\nconst NEUTRAL_PAREN_LOOKAHEAD =\n /^(?:\\s*,?\\s*(?:at\\s+)?\\*?\\d+(?:[-–—]\\*?\\d+)?)?\\s*\\(([^)]+)\\)/\n\n/** Identifies whether a captured \"court\" string is actually a database\n * identifier (WL/LEXIS/BL) rather than a real jurisdictional code. #294 */\nfunction isDatabaseIdentifier(s: string): boolean {\n if (s === \"WL\" || s === \"BL\") return true\n return /\\bLEXIS\\b/.test(s)\n}\n\n/**\n * Extracts neutral citation metadata from a tokenized citation.\n *\n * Parses token text to extract:\n * - Year: 4-digit year (e.g., \"2020\")\n * - Court: Vendor identifier (e.g., \"WL\", \"U.S. LEXIS\")\n * - Document number: Unique document identifier (e.g., \"123456\")\n *\n * Confidence scoring:\n * - 1.0 (neutral format is unambiguous and standardized)\n *\n * @param token - Token from tokenizer containing matched text and clean positions\n * @param transformationMap - Position mapping from clean → original text\n * @returns NeutralCitation with parsed metadata and translated positions\n *\n * @example\n * ```typescript\n * const token = {\n * text: \"2020 WL 123456\",\n * span: { cleanStart: 10, cleanEnd: 24 },\n * type: \"neutral\",\n * patternId: \"westlaw-neutral\"\n * }\n * const citation = extractNeutral(token, transformationMap)\n * // citation = {\n * // type: \"neutral\",\n * // year: 2020,\n * // court: \"WL\",\n * // documentNumber: \"123456\",\n * // confidence: 1.0,\n * // ...\n * // }\n * ```\n */\nexport function extractNeutral(\n token: Token,\n transformationMap: TransformationMap,\n cleanedText?: string,\n): NeutralCitation {\n const { text, span } = token\n\n // Parse year-court-documentNumber. Two-step:\n // 1. Try the Mississippi 4-segment hyphenated form (#233):\n // year-caseType-number-appellateTrack, e.g., \"2010-CT-01234-SCT\".\n // Court is composed as `${caseType}-${appellateTrack}` so the single\n // `court` field preserves the full sovereign identifier.\n // 2. Try the 3-segment hyphenated form (NM/Ohio/NC) or the whitespace form.\n let year: number\n let court: string\n let documentNumber: string\n let unpublished = false\n let spans: NeutralComponentSpans | undefined\n\n const msMatch = /^(\\d{4})-([A-Z]+)-(\\d+)-([A-Z]+)$/d.exec(text)\n if (msMatch) {\n year = Number.parseInt(msMatch[1], 10)\n court = `${msMatch[2]}-${msMatch[4]}`\n documentNumber = msMatch[3]\n if (msMatch.indices) {\n const caseTypeIndices = msMatch.indices[2]!\n const trackIndices = msMatch.indices[4]!\n // Span covers the case-type token through the appellate-track token so\n // the position range reflects the combined court identifier.\n const courtIndices: [number, number] = [caseTypeIndices[0], trackIndices[1]]\n spans = {\n year: spanFromGroupIndex(span.cleanStart, msMatch.indices[1]!, transformationMap),\n court: spanFromGroupIndex(span.cleanStart, courtIndices, transformationMap),\n documentNumber: spanFromGroupIndex(\n span.cleanStart,\n msMatch.indices[3]!,\n transformationMap,\n ),\n }\n }\n } else {\n // 3-segment forms: hyphenated (NM/Ohio/NC) or whitespace (UT/WI/IL/WL).\n // Trailing `(-U)?` captures Illinois Rule 23 unpublished marker (#230);\n // the suffix is consumed but excluded from `documentNumber`.\n const neutralRegex = /^(\\d{4})[-\\s]+(.+?)[-\\s]+(\\d+)(-U)?$/d\n const match = neutralRegex.exec(text)\n if (!match) {\n throw new Error(`Failed to parse neutral citation: ${text}`)\n }\n year = Number.parseInt(match[1], 10)\n court = match[2]\n documentNumber = match[3]\n if (match[4] === \"-U\") {\n unpublished = true\n }\n if (match.indices) {\n spans = {\n year: spanFromGroupIndex(span.cleanStart, match.indices[1]!, transformationMap),\n court: spanFromGroupIndex(span.cleanStart, match.indices[2]!, transformationMap),\n documentNumber: spanFromGroupIndex(\n span.cleanStart,\n match.indices[3]!,\n transformationMap,\n ),\n }\n }\n }\n\n // Look ahead in cleaned text for a trailing pincite (e.g., \", at *3\" on\n // Westlaw and Lexis citations). See #191.\n let pincite: number | undefined\n let pinciteInfo: PinciteInfo | undefined\n if (cleanedText) {\n const afterToken = cleanedText.substring(span.cleanEnd)\n const laMatch = NEUTRAL_PINCITE_LOOKAHEAD.exec(afterToken)\n if (laMatch) {\n pinciteInfo = parsePincite(laMatch[1]) ?? undefined\n // Neutral cites in state appellate practice use paragraph pinpoints\n // (`2015-NMCA-072, ¶ 2`) rather than page numbers. Fall back to the\n // paragraph number when no page is set so the top-level `pincite`\n // field reflects the pinpoint regardless of form. #311\n pincite = pinciteInfo?.page ?? pinciteInfo?.paragraph\n // Component span for pincite (#210). Indices are relative to afterToken,\n // which starts at span.cleanEnd in cleanedText.\n if (laMatch.indices?.[1]) {\n if (!spans) spans = {}\n spans.pincite = spanFromGroupIndex(\n span.cleanEnd,\n laMatch.indices[1],\n transformationMap,\n )\n }\n }\n }\n\n // Database vs. real-court routing (#294). Tokenizer captures \"WL\" or\n // \"U.S. LEXIS\" as the middle segment, which lands here as `court`. These\n // are vendor-database identifiers, not courts — route them to `database`\n // and leave `court` undefined so downstream consumers don't treat the\n // database tag as a court abbreviation.\n let database: string | undefined\n let courtOut: string | undefined = court\n if (isDatabaseIdentifier(court)) {\n database = court\n courtOut = undefined\n // The mistakenly-captured \"court\" span is meaningless for a database tag.\n if (spans) spans.court = undefined\n }\n\n // Trailing `(court date)` parenthetical lookahead (#294). For database\n // cites the trailing paren is the only place the real court appears —\n // `2001 WL 1077846 (N.D. Cal. Sept. 4, 2001)`. Reuses parseParenthetical\n // so the same court/date parser that handles case-cite parens applies.\n let date: StructuredDate | undefined\n if (cleanedText && database) {\n const afterToken = cleanedText.substring(span.cleanEnd)\n const parenMatch = NEUTRAL_PAREN_LOOKAHEAD.exec(afterToken)\n if (parenMatch) {\n const parsed = parseParenthetical(parenMatch[1])\n if (parsed.court) courtOut = parsed.court\n if (parsed.date) {\n date = parsed.date\n // Prefer the more-precise date.parsed.year over the cite's\n // documentary year if the trailing paren disambiguates it. The\n // tokenizer's year (e.g., 2001 in \"2001 WL ...\") is always the\n // citation year and typically matches the paren — leave year alone.\n }\n }\n }\n\n // Translate positions from clean → original\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Confidence: 1.0 (neutral format is unambiguous)\n const confidence = 1.0\n\n return {\n type: \"neutral\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n year,\n court: courtOut,\n ...(database ? { database } : {}),\n documentNumber,\n ...(unpublished ? { unpublished: true } : {}),\n pincite,\n pinciteInfo,\n ...(date ? { date } : {}),\n spans,\n }\n}\n","/**\n * Public Law Citation Extraction\n *\n * Parses tokenized public law citations to extract congress number and law number.\n * Examples: \"Pub. L. No. 116-283\", \"Pub. L. 117-58\"\n *\n * @module extract/extractPublicLaw\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { PublicLawCitation } from \"@/types/citation\"\nimport type { PublicLawComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\n\n/**\n * Extracts public law citation metadata from a tokenized citation.\n *\n * Parses token text to extract:\n * - Congress: Congress number (e.g., \"116\" from \"Pub. L. No. 116-283\")\n * - Law number: Law number within that Congress (e.g., \"283\")\n *\n * Confidence scoring:\n * - 0.9 (public law format is fairly standard)\n *\n * Note: Bill title extraction from nearby text is not implemented in Phase 2.\n * That requires context analysis in Phase 3.\n *\n * @param token - Token from tokenizer containing matched text and clean positions\n * @param transformationMap - Position mapping from clean → original text\n * @returns PublicLawCitation with parsed metadata and translated positions\n *\n * @example\n * ```typescript\n * const token = {\n * text: \"Pub. L. No. 116-283\",\n * span: { cleanStart: 10, cleanEnd: 29 },\n * type: \"publicLaw\",\n * patternId: \"public-law\"\n * }\n * const citation = extractPublicLaw(token, transformationMap)\n * // citation = {\n * // type: \"publicLaw\",\n * // congress: 116,\n * // lawNumber: 283,\n * // confidence: 0.9,\n * // ...\n * // }\n * ```\n */\nexport function extractPublicLaw(\n token: Token,\n transformationMap: TransformationMap,\n): PublicLawCitation {\n const { text, span } = token\n\n // Parse congress-lawNumber using regex\n // Pattern: \"Pub. L.\" (with optional \"No.\") + congress number + \"-\" + law number\n const publicLawRegex = /Pub\\.\\s?L\\.(?:\\s?No\\.)?\\s?(\\d+)-(\\d+)/d\n const match = publicLawRegex.exec(text)\n\n if (!match) {\n throw new Error(`Failed to parse public law citation: ${text}`)\n }\n\n const congress = Number.parseInt(match[1], 10)\n const lawNumber = Number.parseInt(match[2], 10)\n\n let spans: PublicLawComponentSpans | undefined\n if (match.indices) {\n spans = {\n congress: spanFromGroupIndex(span.cleanStart, match.indices[1]!, transformationMap),\n lawNumber: spanFromGroupIndex(span.cleanStart, match.indices[2]!, transformationMap),\n }\n }\n\n // Translate positions from clean → original\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Confidence: 0.9 (public law format is fairly standard)\n const confidence = 0.9\n\n return {\n type: \"publicLaw\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n congress,\n lawNumber,\n spans,\n }\n}\n","/**\n * Short-form Citation Extraction\n *\n * Parses tokenized short-form citations (Id., supra, short-form case) to extract\n * metadata. Short-form citations refer to earlier citations in the document.\n *\n * @module extract/extractShortForms\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { IdCitation, ShortFormCaseCitation, SupraCitation } from \"@/types/citation\"\nimport type {\n IdComponentSpans,\n ShortFormCaseComponentSpans,\n SupraComponentSpans,\n} from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { COMMON_REPORTERS } from \"./extractCase\"\nimport { parsePincite, type PinciteInfo } from \"./pincite\"\n\n/**\n * Strip leading citation signals (`See`, `See also`, `Cf.`, `Compare`,\n * `Accord`, `But see`, `But cf.`, `E.g.`) and sentence-initial connectors\n * (`Also`, `Then`, `In` (but never `In re`)) from a captured supra party name.\n *\n * The `SUPRA_PATTERN` tokenizer is greedy with leading capitalized words, so\n * `See Gall, supra` produces `partyName = \"See Gall\"` and prevents the\n * resolver from matching the supra to its `Gall v. Colon-Sylvain` antecedent.\n * The `In(?!\\s+re\\b)` negative lookahead preserves `In re Smith` — only the\n * bare `In` directly preceding a proper-name party gets stripped (#216).\n *\n * The original captured name is returned unchanged when stripping would leave\n * an empty string (defensive: prevents a wholesale signal token from blanking\n * out the party name).\n */\nconst SUPRA_PARTY_PREFIX_REGEX =\n /^(?:But\\s+(?:see|cf\\.?)|See(?:\\s+also)?(?:\\s*,\\s*e\\.\\s*g\\.?)?|Compare|Cf\\.?|Accord|E\\.\\s*g\\.?|Also|In(?!\\s+re\\b)|Then)\\s+/i\n\nfunction stripSupraPartyPrefix(raw: string): string {\n const stripped = raw.replace(SUPRA_PARTY_PREFIX_REGEX, \"\").trim()\n return stripped.length > 0 ? stripped : raw\n}\n\n/**\n * Trailing-parenthetical lookahead for short-form citations (#303).\n *\n * Captures content of a single `(...)` parenthetical immediately after the\n * citation core, allowing optional whitespace/comma between. The body uses\n * `[^()]*` (no nesting) — `parenthetical` is the raw text inside one set of\n * parens. Suitable for `Id. at N (Marsh)`, `Id. (citation omitted)`,\n * `Smith, supra (holding that ...)`, `Smith, 500 F.2d at 125 (citations omitted)`.\n */\nconst TRAILING_PAREN_REGEX = /^[\\s,]*\\(([^()]*)\\)/\n\n/**\n * Scan the cleaned text after a short-form citation's span end for an\n * immediately-trailing `(...)` parenthetical. Returns the inner text\n * (excluding the parens) or `undefined` if none found. #303\n */\nfunction extractTrailingParenthetical(\n cleanedText: string | undefined,\n cleanEnd: number,\n): string | undefined {\n if (!cleanedText) return undefined\n const after = cleanedText.slice(cleanEnd)\n const m = TRAILING_PAREN_REGEX.exec(after)\n if (!m) return undefined\n const content = m[1].trim()\n return content.length > 0 ? content : undefined\n}\n\n/**\n * Extracts Id. citation metadata from a tokenized citation.\n *\n * Parses token text to extract:\n * - Pincite: Optional page reference (e.g., \"253\" from \"Id. at 253\")\n *\n * Confidence scoring:\n * - 1.0 (Id. format is unambiguous and standardized)\n *\n * @param token - Token from tokenizer containing matched text and clean positions\n * @param transformationMap - Position mapping from clean → original text\n * @returns IdCitation with parsed metadata and translated positions\n *\n * @example\n * ```typescript\n * const token = {\n * text: \"Id. at 253\",\n * span: { cleanStart: 10, cleanEnd: 20 },\n * type: \"case\",\n * patternId: \"id\"\n * }\n * const citation = extractId(token, transformationMap)\n * // citation = {\n * // type: \"id\",\n * // pincite: 253,\n * // confidence: 1.0,\n * // ...\n * // }\n * ```\n */\nexport function extractId(\n token: Token,\n transformationMap: TransformationMap,\n cleanedText?: string,\n): IdCitation {\n const { text, span } = token\n\n // Parse Id. with optional pincite.\n // Pattern: Id. or Ibid. with optional comma + \"at [page]\" (handles \"Id., at 5\").\n //\n // Punctuation tolerance (#305):\n // - Optional whitespace before the period — `Id . at 326`, `Ibid .`\n // (OCR + older typesetting).\n // - Comma instead of period — `Id, at 1483` — guarded by `(?=\\s+at\\s)`\n // so bare `Id,` in prose (\"his Id, but ...\") is not misread.\n //\n // Group layout: 1=initial char (\"I\"/\"i\"), 2=`.` when canonical form,\n // 3=`,` when typo form (mutually exclusive with 2), 4=connector before\n // pincite (`, ` Connecticut-style, `,? at`, or `,? (?=¶)`), 5=pincite.\n //\n // Connector alternation accepts three forms (#353):\n // a) `, <pincite>` — Connecticut comma-pincite (`Id., 253`)\n // b) `[, ]?at <pincite>` — Bluebook at-form, optional leading comma\n // c) `[, ]?(?=¶|para)` — paragraph marker\n //\n // Pincite accepts optional \"*\" prefix for star-pagination (#191), an optional\n // trailing footnote suffix \" n.14\" / \" nn.14-15\" (#202), an optional\n // `p.` / `pp.` prefix for CSM form (`Id. at p. 125`; see #236), and\n // `¶` / `¶¶` / `para.` / `paras.` paragraph markers (#204). When the\n // pincite is a paragraph form, `at` is optional (`Id. ¶ 12`).\n const idRegex = /([Ii])(?:d|bid)\\s*(?:(\\.)|(,)(?=\\s+at\\s))(?:(,\\s+|,?\\s+(?:at\\s+(?:pp?\\.\\s*)?|(?=¶|paras?\\.?\\b)))(\\*?\\d+(?:\\s*[-–]\\s*\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?|¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?))?/d\n const match = idRegex.exec(text)\n\n if (!match) {\n throw new Error(`Failed to parse Id. citation: ${text}`)\n }\n\n const firstChar = match[1]\n // Non-standard punctuation signals:\n // - `isTypoComma`: comma replacing the period (`Id, at 1483`) — lower confidence\n // - `hasComma`: post-period comma (`Id., at 253` or `Id., 253`) — slightly\n // lower confidence than canonical. Connector capture (group 4) starts\n // with `,` for both the post-period-comma-at form and the Connecticut\n // comma-pincite form.\n const isTypoComma = match[3] === \",\"\n const hasComma = isTypoComma || match[4]?.startsWith(\",\") === true\n const pinciteInfo: PinciteInfo | undefined = match[5]\n ? (parsePincite(match[5]) ?? undefined)\n : undefined\n const pincite = pinciteInfo?.page\n\n // Component span for pincite (#210)\n let spans: IdComponentSpans | undefined\n if (match[5] && match.indices?.[5]) {\n spans = {\n pincite: spanFromGroupIndex(span.cleanStart, match.indices[5], transformationMap),\n }\n }\n\n // Confidence scoring based on variant\n let confidence = 1.0\n const isLowercase = firstChar === \"i\"\n if (isLowercase) confidence = 0.85 // Lowercase id. is non-standard\n if (hasComma) confidence = Math.min(confidence, 0.9) // Comma variant (Id., at N)\n if (isTypoComma) confidence = Math.min(confidence, 0.7) // `Id, at N` typo (#305)\n\n // Context validation: check whether Id. appears in a citation context.\n // Real Id. citations follow sentence-ending punctuation, semicolons,\n // or paragraph breaks — not mid-sentence prose like \"The Id. card\".\n if (cleanedText && span.cleanStart > 0) {\n const preceding = cleanedText.slice(Math.max(0, span.cleanStart - 20), span.cleanStart)\n // Look for the last non-whitespace character before Id.\n const trimmed = preceding.trimEnd()\n if (trimmed.length > 0) {\n const lastChar = trimmed[trimmed.length - 1]\n // Citation contexts end with: . ; ) ] — or follow certain patterns\n const isCitationContext = /[.;)\\]—:]$/.test(trimmed)\n if (!isCitationContext) {\n // Mid-sentence Id. (e.g., \"The Id. card\") — likely not a citation\n confidence = Math.min(confidence, 0.4)\n }\n }\n }\n\n // Translate positions from clean → original\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Trailing parenthetical (#303): `Id. at 770 (Marsh)`, `Id. (citation omitted)`.\n const parenthetical = extractTrailingParenthetical(cleanedText, span.cleanEnd)\n\n return {\n type: \"id\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n pincite,\n pinciteInfo,\n ...(parenthetical ? { parenthetical } : {}),\n spans,\n }\n}\n\n/**\n * Extracts supra citation metadata from a tokenized citation.\n *\n * Parses token text to extract:\n * - Party name: Name preceding \"supra\" (e.g., \"Smith\" from \"Smith, supra\")\n * - Pincite: Optional page reference (e.g., \"460\" from \"Smith, supra, at 460\")\n *\n * Confidence scoring:\n * - 0.9 (supra format is fairly standard but party name extraction can vary)\n *\n * @param token - Token from tokenizer containing matched text and clean positions\n * @param transformationMap - Position mapping from clean → original text\n * @returns SupraCitation with parsed metadata and translated positions\n *\n * @example\n * ```typescript\n * const token = {\n * text: \"Smith, supra, at 460\",\n * span: { cleanStart: 10, cleanEnd: 30 },\n * type: \"case\",\n * patternId: \"supra\"\n * }\n * const citation = extractSupra(token, transformationMap)\n * // citation = {\n * // type: \"supra\",\n * // partyName: \"Smith\",\n * // pincite: 460,\n * // confidence: 0.9,\n * // ...\n * // }\n * ```\n */\nexport function extractSupra(\n token: Token,\n transformationMap: TransformationMap,\n cleanedText?: string,\n): SupraCitation {\n const { text, span } = token\n\n // Bracketed supra (#306): `State v. Jarzbek, [supra, 705]` /\n // `[supra at 78-82]`. Connecticut Supreme/Appellate convention. The\n // comma-pincite shape `[supra, 705]` accepts no `at` before the page.\n // When the token text matches this shape, parse it via the bracketed\n // regex; otherwise fall through to the canonical partySupraRegex.\n const bracketedSupraRegex =\n /(?:\\b([A-Z][a-zA-Z''\\-]+\\.?(?:(?:\\s+v\\.?\\s+|\\s+&\\s+|,\\s+|\\s+)[A-Z][a-zA-Z''\\-]+\\.?)*)\\s*,?\\s+)?\\[supra(?:(?:,\\s+|\\s+at\\s+(?:pp?\\.\\s*)?)(\\d+(?:[-–—]\\d+)?))?\\]/d\n const bracketedMatch = text.includes(\"[supra\") ? bracketedSupraRegex.exec(text) : null\n\n // Try party-name pattern first: \"Smith, supra [note N] [, at page]\".\n // Party-name capture mirrors SUPRA_PATTERN in src/patterns/shortForm.ts:\n // `v.` / `&` / `,` continuations (#301) so multi-word names like\n // `Thorn Americas, Inc.` and `Walker & Horwich` capture the whole\n // caption rather than just the last word. `In re` prefix is NOT included\n // — the resolver's BKTree indexes full cites without the prefix (#216 /\n // #21), and adding it here would break supra resolution for `In re X`.\n // Pincite accepts optional \"*\" prefix for star-pagination (#191), an optional\n // range end / `p.` / `pp.` prefix for CSM form (#236), an optional trailing\n // footnote suffix (#202), and `¶` / `¶¶` / `para.` / `paras.` paragraph\n // markers (#204). When the pincite is a paragraph form, `at` is optional.\n // Connector before pincite accepts the Connecticut comma-pincite form\n // (`Smith, supra, 522`) alongside the Bluebook `, at` and paragraph\n // forms (#353).\n const partySupraRegex =\n /\\b([A-Z][a-zA-Z''\\-]+\\.?(?:(?:\\s+v\\.?\\s+|\\s+&\\s+|,\\s+|\\s+)[A-Z][a-zA-Z''\\-]+\\.?)*)\\s*,?\\s+supra(?:\\s+note\\s+(\\d+))?(?:(?:,\\s+|,?\\s+(?:at\\s+(?:pp?\\.\\s*)?|(?=¶|paras?\\.?\\b)))(\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?|¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?))?/d\n const partyMatch = bracketedMatch ? null : partySupraRegex.exec(text)\n\n // Fallback: standalone supra — \"supra note N\", \"supra at N\", \"supra § N\".\n // The `at` page accepts the same `p.` / `pp.` prefix and range form (#236)\n // plus paragraph markers (#204).\n const standaloneRegex =\n /supra(?:\\s+note\\s+(\\d+)(?:,?\\s+(?:at\\s+(?:pp?\\.\\s*)?|(?=¶|paras?\\.?\\b))(\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?|¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?))?|\\s+(?:at\\s+(?:pp?\\.\\s*)?|(?=¶|paras?\\.?\\b))(\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?|¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?))?/d\n const match = bracketedMatch || partyMatch || standaloneRegex.exec(text)\n\n if (!match) {\n throw new Error(`Failed to parse supra citation: ${text}`)\n }\n\n let partyName: string | undefined\n let pinciteInfo: PinciteInfo | undefined\n let confidence: number\n let pinciteGroupIdx: number | undefined\n\n if (bracketedMatch) {\n // Bracketed form (#306): group 1 = optional party, group 2 = optional pincite.\n partyName = bracketedMatch[1] ? stripSupraPartyPrefix(bracketedMatch[1]) : undefined\n pinciteInfo = bracketedMatch[2]\n ? (parsePincite(bracketedMatch[2]) ?? undefined)\n : undefined\n confidence = partyName ? 0.9 : 0.8\n if (bracketedMatch[2]) pinciteGroupIdx = 2\n } else if (partyMatch) {\n partyName = stripSupraPartyPrefix(partyMatch[1])\n pinciteInfo = partyMatch[3]\n ? (parsePincite(partyMatch[3]) ?? undefined)\n : undefined\n confidence = 0.9\n if (partyMatch[3]) pinciteGroupIdx = 3\n } else {\n // Standalone supra — no party name\n partyName = undefined\n const noteAtPage = match[2]\n const atPage = match[3]\n const rawPin = noteAtPage ?? atPage\n pinciteInfo = rawPin ? (parsePincite(rawPin) ?? undefined) : undefined\n confidence = 0.8 // Slightly lower — standalone supra is less specific\n if (noteAtPage) pinciteGroupIdx = 2\n else if (atPage) pinciteGroupIdx = 3\n }\n\n const pincite = pinciteInfo?.page\n\n // Component span for pincite (#210)\n let spans: SupraComponentSpans | undefined\n if (pinciteGroupIdx !== undefined && match.indices?.[pinciteGroupIdx]) {\n spans = {\n pincite: spanFromGroupIndex(\n span.cleanStart,\n match.indices[pinciteGroupIdx],\n transformationMap,\n ),\n }\n }\n\n // Translate positions from clean → original\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Trailing parenthetical (#303): `Smith, supra (holding ...)`.\n const parenthetical = extractTrailingParenthetical(cleanedText, span.cleanEnd)\n\n return {\n type: \"supra\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n partyName,\n pincite,\n pinciteInfo,\n ...(parenthetical ? { parenthetical } : {}),\n spans,\n }\n}\n\n/**\n * Extracts short-form case citation metadata from a tokenized citation.\n *\n * Parses token text to extract:\n * - Volume: Volume number\n * - Reporter: Reporter abbreviation\n * - Pincite: Page reference (from \"at [page]\" pattern)\n *\n * Confidence scoring:\n * - 0.7 (short-form case citations are more ambiguous than full citations)\n *\n * @param token - Token from tokenizer containing matched text and clean positions\n * @param transformationMap - Position mapping from clean → original text\n * @returns ShortFormCaseCitation with parsed metadata and translated positions\n *\n * @example\n * ```typescript\n * const token = {\n * text: \"500 F.2d at 125\",\n * span: { cleanStart: 10, cleanEnd: 25 },\n * type: \"case\",\n * patternId: \"short-form-case\"\n * }\n * const citation = extractShortFormCase(token, transformationMap)\n * // citation = {\n * // type: \"shortFormCase\",\n * // volume: 500,\n * // reporter: \"F.2d\",\n * // pincite: 125,\n * // confidence: 0.7,\n * // ...\n * // }\n * ```\n */\nexport function extractShortFormCase(\n token: Token,\n transformationMap: TransformationMap,\n cleanedText?: string,\n): ShortFormCaseCitation {\n const { text, span } = token\n\n // Parse [Party,] volume-reporter-[,]-at-page.\n // Pattern: optional Party name then number space abbreviation [, ] at space number.\n // Supports reporters with 1-2 letter ordinal suffixes (e.g., F.4th, Cal.4th).\n // Handles comma-before-at: \"597 U.S., at 721\", \"116 F.4th, at 1193\".\n // Pincite accepts optional \"*\" prefix for star-pagination (#191), an optional\n // range end \"462-65\" / \"462-*65\" (#201), an optional trailing footnote\n // suffix \" n.14\" / \" nn.14-15\" (#202), an optional `p.` / `pp.` prefix for\n // CSM form (`18 Cal.4th at p. 717`; see #236), and `¶` / `¶¶` / `para.` /\n // `paras.` paragraph markers (#204).\n // Optional leading party-name group (#278) captures Bluebook back-references\n // (`Smith, 500 F.2d at 125`). Group order:\n // 1: party name (optional, undefined for bare form)\n // 2: volume\n // 3: reporter\n // 4: pincite\n // Party-name capture mirrors SHORT_FORM_CASE_PATTERN: `v.` / `&` / `,`\n // continuations (#301). `In re` prefix intentionally omitted (see\n // partySupraRegex above for rationale). Pincite-prefix alternation also\n // accepts spelled-out `page` / `pages` (#344).\n const shortFormRegex =\n /(?:([A-Z][a-zA-Z''\\-]+\\.?(?:(?:\\s+v\\.?\\s+|\\s+&\\s+|,\\s+|\\s+)[A-Z][a-zA-Z''\\-]+\\.?)*),\\s+)?(\\d+(?:-\\d+)?)\\s+([A-Z][A-Za-z.''\\s]+?(?:\\d[a-z]{1,2})?)\\s*,?\\s+at\\s+(?:pp?\\.\\s*|pages?\\s+)?(\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?|¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?)/d\n const match = shortFormRegex.exec(text)\n\n if (!match) {\n throw new Error(`Failed to parse short-form case citation: ${text}`)\n }\n\n const rawPartyName = match[1]\n const rawVolume = match[2]\n const volume = /^\\d+$/.test(rawVolume) ? Number.parseInt(rawVolume, 10) : rawVolume\n const reporter = match[3].trim() // Remove trailing spaces\n const pinciteInfo: PinciteInfo | undefined = parsePincite(match[4]) ?? undefined\n const pincite = pinciteInfo?.page\n\n // Strip leading citation signals from the captured party name (#216 helper).\n // The optional party-name group itself doesn't include signal prefixes —\n // the outer SHORT_FORM_CASE_PATTERN's `\\b` anchor lands at the signal word\n // (e.g., `See` is matched as the first capitalized token, then `Smith` as\n // the second). `stripSupraPartyPrefix` peels off any leading signal /\n // sentence-initial connector, mirroring the supra handling.\n let partyName: string | undefined\n let partyNameNormalized: string | undefined\n if (rawPartyName) {\n partyName = stripSupraPartyPrefix(rawPartyName)\n partyNameNormalized = partyName.toLowerCase().replace(/\\s+/g, \" \").trim()\n }\n\n // Component span for pincite (#210)\n let spans: ShortFormCaseComponentSpans | undefined\n if (match.indices?.[4]) {\n spans = {\n pincite: spanFromGroupIndex(span.cleanStart, match.indices[4], transformationMap),\n }\n }\n\n // Translate positions from clean → original\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Confidence: base 0.4, boosted for recognized reporters\n let confidence = 0.4\n if (COMMON_REPORTERS.has(reporter)) {\n confidence += 0.3\n }\n\n // Trailing parenthetical (#303): `Smith, 500 F.2d at 125 (citations omitted)`.\n const parenthetical = extractTrailingParenthetical(cleanedText, span.cleanEnd)\n\n return {\n type: \"shortFormCase\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n volume,\n reporter,\n pincite,\n pinciteInfo,\n partyName,\n partyNameNormalized,\n ...(parenthetical ? { parenthetical } : {}),\n spans,\n }\n}\n","/**\n * Shared body-parsing utilities for statute extractors.\n *\n * Extracts section number, subsection chain, and et seq. indicator\n * from the \"body\" portion of a tokenized statute citation.\n *\n * @module extract/statutes/parseBody\n */\n\n/** Separate subsection chain from section number.\n * Accepts both `(...)` and `[...]` — MSA uses bracket subscripts\n * (`23.710[252]`) interchangeably with parens (`23.710(252)`). #370 */\nconst SUBSECTION_RE = /^([^([]+?)\\s*((?:\\([^)]*\\)|\\[[^\\]]*\\])*)$/\n\n/** Et seq. at end of string */\nconst ET_SEQ_RE = /\\s*et\\s+seq\\.?\\s*$/i\n\nexport interface ParsedBody {\n section: string\n subsection?: string\n hasEtSeq: boolean\n}\n\n/**\n * Parse a raw body string into section, subsection, and et seq.\n *\n * @example\n * parseBody(\"1983(a)(1) et seq.\") → { section: \"1983\", subsection: \"(a)(1)\", hasEtSeq: true }\n * parseBody(\"122.26(b)(14)\") → { section: \"122.26\", subsection: \"(b)(14)\", hasEtSeq: false }\n * parseBody(\"1983\") → { section: \"1983\", hasEtSeq: false }\n */\nexport function parseBody(rawBody: string): ParsedBody {\n // Strip et seq. — single replace + compare (avoids double regex execution)\n const stripped = rawBody.replace(ET_SEQ_RE, \"\")\n const hasEtSeq = stripped !== rawBody\n\n // Split section from subsections: \"1983(a)(1)\" → section=\"1983\", subsection=\"(a)(1)\"\n const trimmed = stripped.trim()\n const subMatch = SUBSECTION_RE.exec(trimmed)\n const subGroups = subMatch?.[2]\n\n if (subMatch !== null && subGroups) {\n return {\n section: subMatch[1].trim(),\n subsection: subGroups,\n hasEtSeq,\n }\n }\n\n return { section: trimmed, hasEtSeq }\n}\n","/**\n * Abbreviated-Code Statute Extraction (Family 3)\n *\n * Parses tokenized citations from states that use compact abbreviations\n * (e.g., \"Fla. Stat.\", \"R.C.\", \"MCL\"). Looks up the abbreviation in the\n * knownCodes registry to determine jurisdiction.\n *\n * Jurisdictions: FL, OH, MI, UT, CO, WA, NC, GA, PA, IN, NJ, DE\n *\n * @module extract/statutes/extractAbbreviated\n */\n\nimport { findAbbreviatedCode } from \"@/data/knownCodes\"\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\n// Section body: period only allowed when followed by alphanumeric so a\n// trailing sentence period is never captured (#283).\n// Section connector mirrors the tokenizer pattern: `§`, `§§`, the\n// spelled-out word `section(s)` / `Section(s)` (#348), or the abbreviation\n// `sec.` / `Sec.` (Tennessee corpora — #398). Without these, the lazy\n// abbreviation capture would absorb the connector word and break\n// `findAbbreviatedCode` lookups.\n// Optional comma between code and connector (`Idaho Code, § N`) #360.\n// Trailing subscript groups accept either parens or brackets — MSA #370.\n// Internal comma allowed when followed by digit — Kansas `23-9,101` #367.\nconst ABBREVIATED_RE =\n /^(?:(\\d+)\\s+)?(.+?)\\s*,?\\s*(?:§§?|[Ss]ections?|[Ss]ec\\.?)?\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9])|,(?=\\d))*(?:\\([^)]*\\)|\\[[^\\]]*\\])*(?:\\s*et\\s+seq\\.?)?)$/d\n\nexport function extractAbbreviated(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = ABBREVIATED_RE.exec(text)\n\n let title: number | undefined\n let abbrevText: string\n let rawBody: string\n\n if (match) {\n title = match[1] ? Number.parseInt(match[1], 10) : undefined\n abbrevText = match[2].trim()\n rawBody = match[3]\n } else {\n abbrevText = text\n rawBody = \"\"\n }\n\n const codeEntry = findAbbreviatedCode(abbrevText)\n const jurisdiction = codeEntry?.jurisdiction\n // Normalize OCR/spacing variants (`AR.S.`, `ARS`, `A. R.S.`) to the canonical\n // short abbreviation when the input doesn't already match a recognized\n // pattern verbatim — the stripped-form fallback in `findAbbreviatedCode`\n // returns the canonical entry, so `entry.abbreviation` is the right\n // normalized `code`. Exact matches keep their original-form `code`. #348\n const code =\n codeEntry && !codeEntry.patterns.some((p) => p.toLowerCase() === abbrevText.toLowerCase())\n ? codeEntry.abbreviation\n : abbrevText\n\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match?.indices) {\n spans = {}\n if (match.indices[1])\n spans.title = spanFromGroupIndex(span.cleanStart, match.indices[1], transformationMap)\n if (match.indices[2])\n spans.code = spanFromGroupIndex(span.cleanStart, match.indices[2], transformationMap)\n if (match.indices[3] && section) {\n const bodyStart = match.indices[3][0]\n // Use section without trailing sentence punctuation for span boundary.\n // Note: section comes from parseBody() which strips et seq. and splits\n // subsections — the leading text still matches the raw match position.\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n const hasSection = text.includes(\"§\")\n let confidence: number\n if (codeEntry && hasSection) {\n confidence = 0.95\n } else if (codeEntry) {\n confidence = 0.85\n } else if (hasSection) {\n confidence = 0.6\n } else {\n confidence = 0.4\n }\n if (title !== undefined) confidence += 0.05\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n title,\n code,\n section,\n subsection,\n pincite: subsection,\n jurisdiction,\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Pre-1975 Alabama Code Extraction\n *\n * Parses citations to the Code of Alabama 1940 — the dominant pre-1975\n * Alabama statutory citation form. Modern Alabama opinions still cite this\n * version when referencing the historical text of a statute:\n *\n * Code 1940, T. 15, § 389\n * Title 26, Section 214, Code of Alabama 1940, as Recompiled 1958\n * Tit. 52, § 361\n *\n * Three tokenizer patternIds route here:\n * - `ala-code-prefix` → Code-first form (year hardcoded to 1940)\n * - `ala-title-trailer` → Title-first with mandatory Code trailer\n * - `ala-tit-bare` → abbreviated `Tit.` form (optional Code trailer)\n *\n * @module extract/statutes/extractAlaCode1940\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\n// Anchored re-match regexes for each Alabama patternId — mirror the\n// tokenizer patterns in `src/patterns/statutePatterns.ts` so the extractor\n// re-parses the same span the tokenizer captured. `d` flag enables\n// `match.indices` for component spans.\n\nconst ALA_CODE_PREFIX_RE =\n /^Code(?:\\s+of\\s+Alabama)?,?\\s+1940,?\\s+T(?:itle|it)?\\.\\s+(\\d+),?\\s+§\\s+(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*)$/d\n\nconst ALA_TITLE_TRAILER_RE =\n /^Title\\s+(\\d+),?\\s+(?:§|Sec(?:tion)?s?\\.?)\\s+(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*),?\\s+Code(?:\\s+of\\s+Alabama)?,?\\s+(\\d{4})(?:,?\\s+(?:as\\s+)?[Rr]ecompiled\\s+(\\d{4}))?$/d\n\nconst ALA_TIT_BARE_RE =\n /^Tit\\.\\s+(\\d+),?\\s+§\\s+(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*)(?:,?\\s+Code(?:\\s+of\\s+Alabama)?,?\\s+(\\d{4})(?:,?\\s+(?:as\\s+)?[Rr]ecompiled\\s+(\\d{4}))?)?$/d\n\nexport function extractAlaCode1940(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n\n let titleRaw: string\n let sectionRaw: string\n let year: number | undefined\n let recompiledYear: number | undefined\n let titleGroupIdx: number\n let sectionGroupIdx: number\n let match: RegExpExecArray\n\n switch (token.patternId) {\n case \"ala-code-prefix\": {\n match = ALA_CODE_PREFIX_RE.exec(text)!\n titleRaw = match[1]\n sectionRaw = match[2]\n year = 1940 // prefix asserts the 1940 edition\n titleGroupIdx = 1\n sectionGroupIdx = 2\n break\n }\n case \"ala-title-trailer\": {\n match = ALA_TITLE_TRAILER_RE.exec(text)!\n titleRaw = match[1]\n sectionRaw = match[2]\n year = Number.parseInt(match[3], 10)\n if (match[4]) recompiledYear = Number.parseInt(match[4], 10)\n titleGroupIdx = 1\n sectionGroupIdx = 2\n break\n }\n default: {\n // ala-tit-bare\n match = ALA_TIT_BARE_RE.exec(text)!\n titleRaw = match[1]\n sectionRaw = match[2]\n if (match[3]) year = Number.parseInt(match[3], 10)\n if (match[4]) recompiledYear = Number.parseInt(match[4], 10)\n titleGroupIdx = 1\n sectionGroupIdx = 2\n break\n }\n }\n\n const title = Number.parseInt(titleRaw, 10)\n const { section, subsection, hasEtSeq } = parseBody(sectionRaw)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n const titleIdx = match.indices[titleGroupIdx]\n if (titleIdx) spans.title = spanFromGroupIndex(span.cleanStart, titleIdx, transformationMap)\n const bodyIdx = match.indices[sectionGroupIdx]\n if (bodyIdx && section) {\n const bodyStart = bodyIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n // Confidence: 0.95 baseline (closed-shape Alabama Code matches are\n // unambiguous when the Code prefix / trailer or `Tit.` abbreviation is\n // present, which all three patternIds enforce). +0.05 with a subsection.\n let confidence = 0.95\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n title,\n code: \"Code of Alabama 1940\",\n section,\n subsection,\n pincite: subsection,\n year,\n recompiledYear,\n jurisdiction: \"AL\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * California bare statute code abbreviations (#296).\n *\n * In single-jurisdiction California practice, counsel and courts establish\n * the California jurisdiction at the top of a document and then drop to\n * bare-code abbreviations for the rest — `Pen. Code § 148`,\n * `Code Civ. Proc., § 1021.5`, `Veh. Code § 23550.5`. The fully-qualified\n * `Cal. Penal Code § 148` form is handled by the existing `named-code`\n * tokenizer pattern; this file supports the bare-code variant via a\n * closed alternation so non-citation prose like \"Insurance Law applies\"\n * cannot accidentally match.\n *\n * Each entry's canonical form is the string returned in the\n * `StatuteCitation.code` field. The `regexAlternative` is what the\n * tokenizer matches in source text — periods are optional/spaces flexible\n * to handle typographic variation.\n */\n\nexport interface CaBareCodeEntry {\n /** Canonical code name returned in `StatuteCitation.code` */\n canonical: string\n /** Regex fragment for tokenizer alternation (periods/whitespace flexible) */\n regexFragment: string\n}\n\n/**\n * Order matters: list longest-first so PEG-style ordered choice picks the\n * most specific match before any shorter prefix. Example: `Code Civ. Proc.`\n * must come before any rule that could match just `Code` or `Civ. Code`.\n */\nexport const caBareCodeEntries: CaBareCodeEntry[] = [\n // Multi-word \"Code <Subject> Proc.\" forms come first — longest prefixes\n { canonical: \"Code Civ. Proc.\", regexFragment: \"Code\\\\s+Civ\\\\.?\\\\s+Proc\\\\.?\" },\n { canonical: \"Code Crim. Proc.\", regexFragment: \"Code\\\\s+Crim\\\\.?\\\\s+Proc\\\\.?\" },\n\n // Two-word \"<Subject> & <Subject> Code\" / \"<Subject> Code\" forms\n { canonical: \"Bus. & Prof. Code\", regexFragment: \"Bus\\\\.?\\\\s*&\\\\s*Prof\\\\.?\\\\s+Code\" },\n { canonical: \"Welf. & Inst. Code\", regexFragment: \"Welf\\\\.?\\\\s*&\\\\s*Inst\\\\.?\\\\s+Code\" },\n { canonical: \"Health & Safety Code\", regexFragment: \"Health\\\\s*&\\\\s*Safety\\\\s+Code\" },\n { canonical: \"Fish & Game Code\", regexFragment: \"Fish\\\\s*&\\\\s*Game\\\\s+Code\" },\n { canonical: \"Food & Agric. Code\", regexFragment: \"Food\\\\s*&\\\\s*Agric\\\\.?\\\\s+Code\" },\n { canonical: \"Harb. & Nav. Code\", regexFragment: \"Harb\\\\.?\\\\s*&\\\\s*Nav\\\\.?\\\\s+Code\" },\n { canonical: \"Mil. & Vet. Code\", regexFragment: \"Mil\\\\.?\\\\s*&\\\\s*Vet\\\\.?\\\\s+Code\" },\n { canonical: \"Rev. & Tax. Code\", regexFragment: \"Rev\\\\.?\\\\s*&\\\\s*Tax\\\\.?\\\\s+Code\" },\n { canonical: \"Sts. & Hy. Code\", regexFragment: \"Sts\\\\.?\\\\s*&\\\\s*Hy\\\\.?\\\\s+Code\" },\n\n // Two-word abbreviated \"<Subject>. <Type>. Code\"\n { canonical: \"Pub. Util. Code\", regexFragment: \"Pub\\\\.?\\\\s+Util\\\\.?\\\\s+Code\" },\n { canonical: \"Pub. Cont. Code\", regexFragment: \"Pub\\\\.?\\\\s+Cont\\\\.?\\\\s+Code\" },\n { canonical: \"Pub. Resources Code\", regexFragment: \"Pub\\\\.?\\\\s+Resources\\\\s+Code\" },\n { canonical: \"Unemp. Ins. Code\", regexFragment: \"Unemp\\\\.?\\\\s+Ins\\\\.?\\\\s+Code\" },\n\n // Single-subject \"<Subject>. Code\" forms — alphabetical\n { canonical: \"Civ. Code\", regexFragment: \"Civ\\\\.?\\\\s+Code\" },\n { canonical: \"Corp. Code\", regexFragment: \"Corp\\\\.?\\\\s+Code\" },\n { canonical: \"Educ. Code\", regexFragment: \"Educ\\\\.?\\\\s+Code\" },\n { canonical: \"Elec. Code\", regexFragment: \"Elec\\\\.?\\\\s+Code\" },\n { canonical: \"Evid. Code\", regexFragment: \"Evid\\\\.?\\\\s+Code\" },\n { canonical: \"Fam. Code\", regexFragment: \"Fam\\\\.?\\\\s+Code\" },\n { canonical: \"Gov. Code\", regexFragment: \"Gov\\\\.?\\\\s+Code\" },\n { canonical: \"Ins. Code\", regexFragment: \"Ins\\\\.?\\\\s+Code\" },\n { canonical: \"Lab. Code\", regexFragment: \"Lab\\\\.?\\\\s+Code\" },\n { canonical: \"Pen. Code\", regexFragment: \"Pen\\\\.?\\\\s+Code\" },\n { canonical: \"Prob. Code\", regexFragment: \"Prob\\\\.?\\\\s+Code\" },\n { canonical: \"Veh. Code\", regexFragment: \"Veh\\\\.?\\\\s+Code\" },\n { canonical: \"Water Code\", regexFragment: \"Water\\\\s+Code\" },\n]\n\n/**\n * Build the bare-code tokenizer regex from the alternation above.\n *\n * Capture groups:\n * (1) bare code name (matched alternative)\n * (2) section body (digits + optional alphanumeric / subsections / et seq.)\n *\n * Alternation is sorted by regex length descending so longer-prefix codes\n * (`Code Civ. Proc.`, `Welf. & Inst. Code`) match before shorter ones.\n */\nexport function buildCaBareCodeRegex(): RegExp {\n const fragments = [...caBareCodeEntries]\n .sort((a, b) => b.regexFragment.length - a.regexFragment.length)\n .map((e) => e.regexFragment)\n const alternation = fragments.join(\"|\")\n return new RegExp(\n `\\\\b(${alternation})\\\\s*,?\\\\s*§§?\\\\s*(\\\\d+(?:[A-Za-z0-9:/-]|\\\\.(?=[A-Za-z0-9]))*(?:\\\\([^)]*\\\\))*(?:\\\\s*et\\\\s+seq\\\\.?)?)`,\n \"g\",\n )\n}\n\n/**\n * Find the canonical CA bare-code name from a raw token match.\n * Normalizes whitespace/period variation back to the canonical string\n * so the StatuteCitation `code` field is stable across input variations.\n */\nexport function findCaBareCode(rawText: string): string | undefined {\n const normalized = rawText.replace(/\\s+/g, \" \").trim()\n for (const entry of caBareCodeEntries) {\n const fragmentRe = new RegExp(`^${entry.regexFragment}$`, \"i\")\n if (fragmentRe.test(normalized)) return entry.canonical\n }\n return undefined\n}\n","/**\n * California Bare-Code Statute Extraction (#296)\n *\n * Parses tokenized citations to California codes that lack the `Cal.`\n * jurisdiction prefix — `Pen. Code § 148`, `Code Civ. Proc., § 1021.5`,\n * `Bus. & Prof. Code § 17200`. The fully-qualified form (`Cal. Penal\n * Code § 148`) is handled by `extractNamedCode`; this extractor\n * recognizes the bare form via the closed alternation defined in\n * `src/data/caBareCodes.ts`.\n *\n * All matches produce `jurisdiction: \"CA\"`.\n *\n * @module extract/statutes/extractCaBareCode\n */\n\nimport { findCaBareCode } from \"@/data/caBareCodes\"\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\n/** Match shape: <bare code name> [,] § <body>. Indices flag enables span computation. */\nconst CA_BARE_CODE_RE =\n /^(.+?)\\s*,?\\s*§§?\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)$/d\n\nexport function extractCaBareCode(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n // The tokenizer's closed-alternation regex guarantees a match here; the\n // extractor's regex is structurally equivalent to that tokenizer pattern,\n // so `match` is always non-null for tokens routed to this extractor.\n const match = CA_BARE_CODE_RE.exec(text)!\n const rawCodeText = match[1].trim()\n const rawBody = match[2]\n\n // Normalize back to canonical bare-code form (\"Pen. Code\", \"Code Civ. Proc.\").\n // `findCaBareCode` is guaranteed to hit because the tokenizer only emits\n // tokens whose code text matched one of the canonical alternations.\n const code = findCaBareCode(rawCodeText)!\n\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match?.indices) {\n spans = {}\n if (match.indices[1])\n spans.code = spanFromGroupIndex(span.cleanStart, match.indices[1], transformationMap)\n if (match.indices[2] && section) {\n const bodyStart = match.indices[2][0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n // Confidence: bare-code matches come from a closed alternation, so the\n // jurisdiction inference is reliable. Match the existing named-code\n // baseline (0.95 when a known code resolves) and bump for subsection.\n let confidence = 0.95\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code,\n section,\n subsection,\n pincite: subsection,\n jurisdiction: \"CA\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Chapter-Act Statute Extraction (Family 4)\n *\n * Parses Illinois Compiled Statutes (ILCS) citations with the unique\n * chapter/act/section format: \"735 ILCS 5/2-1001\"\n *\n * @module extract/statutes/extractChapterAct\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\n/**\n * Parse chapter-act token: chapter + ILCS + act/section.\n *\n * Section body uses the period-followed-by-alphanumeric guard from #283:\n * a trailing sentence period is not absorbed (`5 ILCS 100/1-1.` → `1-1`,\n * not `1-1.`; #331). The body must mirror the tokenizer regex in\n * `statutePatterns.ts` so that the extractor's anchored re-match consumes\n * the same span the tokenizer captured.\n */\nconst CHAPTER_ACT_RE =\n /^(\\d+)\\s+(?:ILCS|Ill\\.?\\s*Comp\\.?\\s*Stat\\.?)\\s*(?:Ann\\.?\\s+)?(\\d+)\\/(\\d+(?:[A-Za-z0-9:-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)$/d\n\nexport function extractChapterAct(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = CHAPTER_ACT_RE.exec(text)\n\n let title: number | undefined // chapter number\n let code: string // act number\n let rawBody: string\n\n if (match) {\n title = Number.parseInt(match[1], 10) // chapter (e.g., 735)\n code = match[2] // act (e.g., 5)\n rawBody = match[3] // section (e.g., 2-1001)\n } else {\n code = text\n rawBody = \"\"\n }\n\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Use section without trailing sentence punctuation for span boundary.\n // Note: section comes from parseBody() which strips et seq. and splits\n // subsections — the leading text still matches the raw match position.\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n\n let spans: StatuteComponentSpans | undefined\n if (match?.indices) {\n spans = {}\n if (match.indices[1]) spans.title = spanFromGroupIndex(span.cleanStart, match.indices[1], transformationMap)\n if (match.indices[2]) spans.code = spanFromGroupIndex(span.cleanStart, match.indices[2], transformationMap)\n if (match.indices[3] && section) {\n const bodyStart = match.indices[3][0]\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n // Title (chapter) is always present on a successful ILCS match — no bonus needed.\n // Only subsection presence provides a confidence boost.\n let confidence = match ? 0.95 : 0.3\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n title,\n code,\n section,\n subsection,\n pincite: subsection,\n jurisdiction: match ? \"IL\" : undefined,\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Colorado Revised Statutes prose form (pre-1973 and modern)\n *\n * Parses citations in the form `Section 148-21-34, Colorado Revised Statutes\n * 1963`, where the section comes BEFORE the code name. Pre-1973 Colorado\n * used a chapter-article-section numbering scheme (`148-21-34` = chapter\n * 148, article 21, section 34); the structured chapter/article fields are\n * not surfaced separately — the full section body goes on `section`.\n *\n * `code` carries the full code name including the year suffix when present\n * (`Colorado Revised Statutes 1963`), so consumers can distinguish editions\n * without looking at `year` (which is reserved for trailing parenthetical\n * edition years). #352\n *\n * @module extract/statutes/extractColoradoProse\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\nconst COLORADO_PROSE_RE =\n /^[Ss]ection\\s+(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*),?\\s+(Colo(?:rado)?\\.?\\s+Rev(?:ised)?\\.?\\s+Stat(?:utes)?\\.?(?:\\s+Ann(?:otated)?\\.?)?)(?:\\s+(19\\d{2}))?$/d\n\nexport function extractColoradoProse(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = COLORADO_PROSE_RE.exec(text)!\n\n const rawBody = match[1]\n const codeName = match[2].replace(/\\s+/g, \" \").trim()\n const editionYear = match[3]\n // `code` preserves the year suffix as part of the name: `Colorado Revised\n // Statutes 1963`. The bare modern form remains `Colorado Revised Statutes`.\n const code = editionYear ? `${codeName} ${editionYear}` : codeName\n\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n const sectionIdx = match.indices[1]\n if (sectionIdx && section) {\n const bodyStart = sectionIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n if (match.indices[2]) {\n spans.code = spanFromGroupIndex(span.cleanStart, match.indices[2], transformationMap)\n }\n }\n\n // Confidence: 0.9 baseline (the closed prose shape is unambiguous); +0.05\n // when a subsection is captured.\n let confidence = 0.9\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code,\n section,\n subsection,\n pincite: subsection,\n jurisdiction: \"CO\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Federal Statute Extraction (USC + CFR)\n *\n * Parses tokenized federal citations to extract title, code, section,\n * subsections, jurisdiction, and et seq. indicators.\n *\n * @module extract/statutes/extractFederal\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\n/** Regex to parse federal token: title + code + § + body */\nconst FEDERAL_SECTION_RE = /^(\\d+)\\s+(\\S+(?:\\.\\S+)*)\\s*§§?\\s*(.+)$/d\n/** Regex to parse federal token: title + code + Part + body */\nconst FEDERAL_PART_RE = /^(\\d+)\\s+(\\S+(?:\\.\\S+)*)\\s+(?:Part|pt\\.)\\s+(.+)$/d\n\n/**\n * Extract a federal statute citation (USC or CFR) from a tokenized match.\n */\nexport function extractFederal(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n\n // Try § form first, then Part form\n const bodyMatch = FEDERAL_SECTION_RE.exec(text) ?? FEDERAL_PART_RE.exec(text)\n\n let title: number | undefined\n let code: string\n let rawBody: string\n\n if (bodyMatch) {\n title = Number.parseInt(bodyMatch[1], 10)\n code = bodyMatch[2]\n rawBody = bodyMatch[3]\n } else {\n // Fallback for edge cases\n code = token.patternId === \"cfr\" ? \"C.F.R.\" : \"U.S.C.\"\n rawBody = text\n title = undefined\n }\n\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n // Translate positions\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (bodyMatch?.indices) {\n spans = {}\n if (bodyMatch.indices[1]) spans.title = spanFromGroupIndex(span.cleanStart, bodyMatch.indices[1], transformationMap)\n if (bodyMatch.indices[2]) spans.code = spanFromGroupIndex(span.cleanStart, bodyMatch.indices[2], transformationMap)\n if (bodyMatch.indices[3] && section) {\n const bodyStart = bodyMatch.indices[3][0]\n // Use section without trailing sentence punctuation for span boundary.\n // Note: section comes from parseBody() which strips et seq. and splits\n // subsections — the leading text still matches the raw match position.\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n // Confidence: known federal code + § = 0.95 base\n let confidence = 0.95\n if (title !== undefined) confidence += 0.05\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n title,\n code,\n section,\n subsection,\n pincite: subsection,\n jurisdiction: \"US\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Florida statute citations — postfix and spelled-out-prefix forms (#356)\n *\n * Florida courts use a distinctive postfix syntax where the code name\n * appears AFTER the section number. The canonical Bluebook prefix form\n * (`Fla. Stat. § 812.035`) is handled by `extractAbbreviated`; this\n * extractor handles two Florida-specific shapes:\n *\n * - postfix: `section 812.035(7), Florida Statutes` /\n * `§83.15, Florida Statutes` (patternId `florida-postfix`)\n * - spelled-out prefix: `Florida Statute 679.504(3)` /\n * `Florida Statutes §73.071(3)(b)` (patternId\n * `florida-prefix-spelled`)\n *\n * Both shapes emit `code: \"Fla. Stat.\"` (normalized) and\n * `jurisdiction: \"FL\"`.\n *\n * @module extract/statutes/extractFloridaStatute\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\n// `^` anchor is fine here — the extractor receives only the matched span,\n// so the leading position is always the start of the captured token text.\nconst FLORIDA_POSTFIX_RE =\n /^(?:[Ss]ections?|§§?)\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*),?\\s+(?:Florida\\s+Statutes|Fla\\.\\s*Stat\\.)$/d\n\nconst FLORIDA_PREFIX_SPELLED_RE =\n /^Florida\\s+Statutes?\\s*§?\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*)$/d\n\nexport function extractFloridaStatute(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n\n const re =\n token.patternId === \"florida-postfix\" ? FLORIDA_POSTFIX_RE : FLORIDA_PREFIX_SPELLED_RE\n const match = re.exec(text)!\n\n const rawBody = match[1]\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n const bodyIdx = match.indices[1]\n if (bodyIdx && section) {\n const bodyStart = bodyIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n // Confidence: 0.95 for both shapes (closed Florida-specific patterns\n // are unambiguous). +0.05 with a subsection.\n let confidence = 0.95\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: \"Fla. Stat.\",\n section,\n subsection,\n pincite: subsection,\n jurisdiction: \"FL\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Georgia pre-1983 Code — `Code § 27-2501`, `Code Ann. § 26-2101`\n *\n * Georgia replaced its old \"Code\" / \"Code of Georgia Annotated\" with OCGA\n * in 1983. Modern Georgia opinions still cite the pre-1983 code for\n * statutory history. The bare `Code Ann.` / `Code` (no state prefix) is\n * always Georgia — other states use prefixed forms (`Md. Code Ann.`,\n * `Ind. Code Ann.`) that the `named-code` pattern handles. #358\n *\n * @module extract/statutes/extractGaPre1983\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\nconst GA_PRE_1983_RE =\n /^(Code(?:\\s+Ann\\.?)?)\\s+§\\s*(\\d+-\\d+(?:[A-Za-z0-9])?(?:\\([A-Za-z0-9]+\\))*)$/d\n\nexport function extractGaPre1983(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = GA_PRE_1983_RE.exec(text)!\n const codeName = match[1]\n const rawBody = match[2]\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n if (match.indices[1])\n spans.code = spanFromGroupIndex(span.cleanStart, match.indices[1], transformationMap)\n const bodyIdx = match.indices[2]\n if (bodyIdx && section) {\n const bodyStart = bodyIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n let confidence = 0.85\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: codeName,\n section,\n subsection,\n pincite: subsection,\n jurisdiction: \"GA\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Indiana Code year-edition form — `IC 1971, 35-13-4-4`\n *\n * The year between IC and the section is the compilation/edition year of\n * the Indiana Code, not the section. The trailing `, NN-N-N` separator\n * distinguishes this from a bare `IC NN-N-N` modern citation. #363\n *\n * Same family as Colorado `C.R.S. 1963 § N` (#352), Minnesota\n * `Minn. St. 1971, § N` (#371), Kansas `K.S.A. YYYY Supp. NN-NNN` (#367).\n *\n * @module extract/statutes/extractIcYearEdition\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\nconst IC_YEAR_RE =\n /^IC\\s+(\\d{4}),\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*)$/d\n\nexport function extractIcYearEdition(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = IC_YEAR_RE.exec(text)!\n const year = Number.parseInt(match[1], 10)\n const rawBody = match[2]\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n const bodyIdx = match.indices[2]\n if (bodyIdx && section) {\n const bodyStart = bodyIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n let confidence = 0.95\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: \"IC\",\n section,\n subsection,\n pincite: subsection,\n year,\n jurisdiction: \"IN\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Idaho Code postfix form — `Section 23-908(4), Idaho Code`\n *\n * Canonical Idaho court style places the code name AFTER the section, just\n * like Florida's `section N, Florida Statutes` form. The leading word\n * \"section\" / \"§\" is optional in some Idaho variants but typically present.\n * #360\n *\n * @module extract/statutes/extractIdahoPostfix\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\nconst IDAHO_POSTFIX_RE =\n /^(?:[Ss]ections?|§§?)\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*),?\\s+Idaho\\s+Code(?:\\s+Ann\\.?)?$/d\n\nexport function extractIdahoPostfix(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = IDAHO_POSTFIX_RE.exec(text)!\n const rawBody = match[1]\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n const bodyIdx = match.indices[1]\n if (bodyIdx && section) {\n const bodyStart = bodyIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n let confidence = 0.95\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: \"Idaho Code\",\n section,\n subsection,\n pincite: subsection,\n jurisdiction: \"ID\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Illinois Revised Statutes Extraction (pre-1993 format)\n *\n * Parses citations to Illinois Revised Statutes, the dominant pre-1993\n * Illinois statutory citation form:\n *\n * Ill. Rev. Stat. 1985, ch. 40, par. 504(a)\n * Ill. Rev. Stat. 1987, ch. 85, pars. 8-102, 8-103\n * Ill.Rev.Stat. 1985, Ch. 127, par. 780.04.\n *\n * Modern Illinois opinions continue to cite ILRS when referencing the\n * historical version of a statute. Companion to `extractChapterAct` (which\n * handles the modern post-1993 ILCS form). #330\n *\n * @module extract/statutes/extractIllRevStat\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\nconst ILL_REV_STAT_RE =\n /^Ill\\.?\\s*Rev\\.?\\s*Stat\\.?,?\\s+(\\d{4}),?\\s+[Cc]h\\.\\s+(\\d+[A-Z]?),?\\s+pars?\\.\\s+(\\d+(?:[A-Za-z0-9:-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)$/d\n\nexport function extractIllRevStat(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n // The tokenizer's regex guarantees a match here — same shape as the\n // extractor's regex. Use non-null assertion to keep the code path tight.\n const match = ILL_REV_STAT_RE.exec(text)!\n const year = Number.parseInt(match[1], 10)\n const chapterRaw = match[2]\n // Chapter can carry a letter suffix (`110A`). Use only the digit-prefix for\n // the numeric `title` field; the full chapter string is preserved in\n // `matchedText`.\n const title = Number.parseInt(chapterRaw, 10)\n const rawBody = match[3]\n\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n if (match.indices[2])\n spans.title = spanFromGroupIndex(span.cleanStart, match.indices[2], transformationMap)\n if (match.indices[3] && section) {\n const bodyStart = match.indices[3][0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n // Confidence: 0.95 baseline (closed-shape Ill. Rev. Stat. matches are\n // unambiguous); +0.05 when a subsection is captured.\n let confidence = 0.95\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n title,\n code: \"Ill. Rev. Stat.\",\n section,\n subsection,\n pincite: subsection,\n year,\n jurisdiction: \"IL\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Internal Revenue Code (IRC) — federal tax code citations\n *\n * I.R.C. § 1367\n * I.R.C. § 1366(a)(1)\n * IRC § 1341\n *\n * The `I.R.C.` form is the canonical Bluebook abbreviation for Title 26 of\n * the U.S. Code (Internal Revenue Code); bare `IRC` is also common in tax\n * opinions. Without this dedicated pattern, Ohio's `R.C.` regex fragment\n * matched the suffix of `I.R.C.` and silently routed every IRC citation\n * to Ohio jurisdiction. #376\n *\n * @module extract/statutes/extractIrc\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\nconst IRC_RE =\n /^(?:I\\.R\\.C\\.|IRC)\\s*§§?\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)$/d\n\nexport function extractIrc(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = IRC_RE.exec(text)!\n const rawBody = match[1]\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n const bodyIdx = match.indices[1]\n if (bodyIdx && section) {\n const bodyStart = bodyIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n let confidence = 0.95\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: \"I.R.C.\",\n section,\n subsection,\n pincite: subsection,\n jurisdiction: \"US\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Kansas Statutes Annotated year-edition form — `K.S.A. 2009 Supp. 44-501(d)(2)`\n *\n * Kansas opinions cite a specific compilation year to indicate which\n * version of the statute was in effect at the time of the events. The\n * `Supp.` marker is optional — bound-volume citations omit it. The\n * abbreviated-code pattern would otherwise capture the year as section,\n * silently substituting the year for the actual section number.\n *\n * Same family as Minnesota `Minn. St. YYYY, § N` (#371), Colorado\n * `C.R.S. YYYY § N` (#352), Indiana `IC YYYY` (#363 deferred).\n *\n * @module extract/statutes/extractKsaYearEdition\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\nconst KSA_YEAR_RE =\n /^K\\.?\\s*S\\.?\\s*A\\.?\\s+(\\d{4})(?:\\s+(Supp\\.?))?\\s+(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9])|,(?=\\d))*(?:\\([^)]*\\))*)$/d\n\nexport function extractKsaYearEdition(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = KSA_YEAR_RE.exec(text)!\n const year = Number.parseInt(match[1], 10)\n const hasSupp = match[2] !== undefined\n const rawBody = match[3]\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n const bodyIdx = match.indices[3]\n if (bodyIdx && section) {\n const bodyStart = bodyIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n let confidence = 0.95\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: \"K.S.A.\",\n section,\n subsection,\n pincite: subsection,\n year,\n editionLabel: hasSupp ? \"Supp.\" : undefined,\n jurisdiction: \"KS\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Montana Code Annotated postfix form — `§ 77-6-205(2), MCA`\n *\n * Canonical Montana court style places the section first, with the code name\n * `MCA` after a comma — same shape as Florida's `§ N, Florida Statutes` and\n * Idaho's `§ N, Idaho Code`. The trailing edition-year parenthetical (e.g.\n * `MCA (1983)`) is attached by the generic year-paren absorber in\n * `extractCitations.ts`. #372\n *\n * @module extract/statutes/extractMcaPostfix\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\nconst MCA_POSTFIX_RE =\n /^(?:[Ss]ections?|§§?)\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*),?\\s+MCA$/d\n\nexport function extractMcaPostfix(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = MCA_POSTFIX_RE.exec(text)!\n const rawBody = match[1]\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n const bodyIdx = match.indices[1]\n if (bodyIdx && section) {\n const bodyStart = bodyIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n let confidence = 0.95\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: \"MCA\",\n section,\n subsection,\n pincite: subsection,\n jurisdiction: \"MT\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Maryland article-letter codes — post-2002 Maryland Code\n *\n * HG § 19-906 (Health-General)\n * CP § 10-105(e)(4)(ii) (Criminal Procedure)\n * R.P. § 8-211 (Real Property — dotted variant)\n * BR § 1-101 (Business Regulation)\n *\n * Maryland reorganized its code in 2002 into ~30 named articles, each\n * cited by a 2- or 3-letter prefix. This is the dominant Maryland court\n * style for every modern Maryland appellate opinion. #368\n *\n * @module extract/statutes/extractMdArticleLetter\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\nconst MD_ARTICLE_LETTER_RE =\n /^(AB|AG|BO|BR|CJ|CL|CP|CR|CS|EC|ED|EL|EN|ET|FI|FL|GP|HG|HO|HS|HU|IN|LE|LG|LU|NR|PS|PUC|R\\.?P\\.?|RP|SF|SG|TA|TG|TP|TR)\\s*§§?\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)$/d\n\nexport function extractMdArticleLetter(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = MD_ARTICLE_LETTER_RE.exec(text)!\n const codePrefix = match[1]\n const rawBody = match[2]\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n if (match.indices[1])\n spans.code = spanFromGroupIndex(span.cleanStart, match.indices[1], transformationMap)\n const bodyIdx = match.indices[2]\n if (bodyIdx && section) {\n const bodyStart = bodyIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n let confidence = 0.95\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: codePrefix,\n section,\n subsection,\n pincite: subsection,\n jurisdiction: \"MD\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Minnesota Statutes year-edition form — `Minn. St. 1971, § 176.66`\n *\n * The year (1971 / 1974 / 1967 / etc.) is the EDITION of Minnesota Statutes\n * that was in effect when the underlying events occurred, not the section\n * number. The default abbreviated-code pattern would capture the year as\n * the section; this dedicated pattern preserves the year-as-edition\n * semantics. #371\n *\n * Same family as Colorado `C.R.S. 1963` (#352), Indiana `IC 1971` (#363\n * deferred), Kansas `K.S.A. Supp. YYYY` (#367 deferred).\n *\n * @module extract/statutes/extractMinnStYearEdition\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\nconst MINN_ST_YEAR_RE =\n /^Minn\\.?\\s+(?:Stat|St)\\.?\\s+(19\\d{2}),\\s*§\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\)|\\[[^\\]]*\\])*)$/d\n\nexport function extractMinnStYearEdition(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = MINN_ST_YEAR_RE.exec(text)!\n const year = Number.parseInt(match[1], 10)\n const rawBody = match[2]\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n const bodyIdx = match.indices[2]\n if (bodyIdx && section) {\n const bodyStart = bodyIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n let confidence = 0.95\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: \"Minn. Stat.\",\n section,\n subsection,\n pincite: subsection,\n year,\n jurisdiction: \"MN\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Named-Code State Statute Extraction (Family 4)\n *\n * Parses tokenized citations from states that identify their code by name\n * in the citation (e.g., \"N.Y. Penal Law § 120.05\", \"Cal. Civ. Proc. Code § 437c\").\n *\n * Handles two patternIds:\n * - \"named-code\" — NY, CA, TX, MD, VA, AL citations (prefix + code name + §)\n * - \"mass-chapter\" — MA citations (corpus + ch. + chapter, § section)\n *\n * Jurisdictions: NY, CA, TX, MD, VA, AL, MA (7 total)\n *\n * @module extract/statutes/extractNamedCode\n */\n\nimport { findNamedCode } from \"@/data/knownCodes\"\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\n/** Match named-code token: jurisdiction prefix + code name + § + body */\nconst NAMED_CODE_RE =\n /^(N\\.?\\s*Y\\.?|Cal(?:ifornia)?\\.?|Tex(?:as)?\\.?|Md\\.?|Va\\.?|Ala(?:bama)?\\.?)\\s+(.*?)\\s*§§?\\s*(.+)$/sd\n\n/** Match mass-chapter token: corpus abbreviation + ch./c. + chapter + optional (§|sec.) + section.\n * Section connector and section body are optional — `G.L. c. 93A`\n * chapter-only citations are valid (#364). Spacing before `c.` is optional\n * so `G.L.c.` matches (also #364). */\nconst MASS_CHAPTER_RE = /^(.*?)\\s*(?:ch\\.?|c\\.?)\\s*(\\w+)(?:,?\\s*(?:§§?|[Ss]ec\\.?|[Ss]ection)\\s*(.+))?$/d\n\n/** Map normalized jurisdiction prefixes to 2-letter state codes */\nconst PREFIX_MAP: Record<string, string> = {\n \"n.y.\": \"NY\",\n \"n.y\": \"NY\",\n ny: \"NY\",\n \"cal.\": \"CA\",\n cal: \"CA\",\n \"california.\": \"CA\",\n california: \"CA\",\n \"tex.\": \"TX\",\n tex: \"TX\",\n \"texas.\": \"TX\",\n texas: \"TX\",\n \"md.\": \"MD\",\n md: \"MD\",\n \"va.\": \"VA\",\n va: \"VA\",\n \"ala.\": \"AL\",\n ala: \"AL\",\n \"alabama.\": \"AL\",\n alabama: \"AL\",\n}\n\n/** Normalize a jurisdiction prefix string to a 2-letter state code */\nfunction resolveJurisdiction(prefix: string): string | undefined {\n return PREFIX_MAP[prefix.toLowerCase().replace(/\\s+/g, \"\")]\n}\n\n/**\n * Strip common trailing/leading suffixes from code name text to produce a\n * lookup key for the namedCodes registry.\n *\n * Examples:\n * \"Penal Law\" → \"Penal\"\n * \"Penal Code\" → \"Penal\"\n * \"Civ. Proc. Code\" → \"Civ. Proc.\"\n * \"Code Ann., Crim. Law\" → \"Crim. Law\" (MD \"Code Ann.,\" prefix stripped)\n * \"Code, Ins.\" → \"Ins.\" (MD \"Code,\" prefix stripped)\n * \"Code Ann.\" → \"Code\" (VA/AL trailing Ann. stripped)\n * \"Code\" → \"Code\" (VA/AL — matches pattern directly)\n * \"C.P.L.R.\" → \"C.P.L.R.\" (no suffixes — passed through)\n */\nfunction cleanCodeName(raw: string): string {\n return (\n raw\n // MD: \"Code Ann., Crim. Law\" → \"Crim. Law\"\n .replace(/^\\s*Code\\s+Ann\\.\\s*,\\s*/i, \"\")\n // MD: \"Code, Ins.\" → \"Ins.\"\n .replace(/^\\s*Code\\s*,\\s*/i, \"\")\n // Trailing \" Code\" only (e.g., \"Penal Code\" → \"Penal\", \"Civ. Proc. Code\" → \"Civ. Proc.\")\n // Do NOT strip \" Law\" — MD article names contain \"Law\" (e.g., \"Crim. Law\", \"Criminal Law\")\n // and NY \"Penal Law\" → \"Penal Law\" still matches registry via startsWith(\"Penal\")\n .replace(/\\s+Code\\s*$/i, \"\")\n // Trailing \" Ann.\" (e.g., \"Code Ann.\" → \"Code\" after prior rules skip)\n .replace(/\\s+Ann\\.?\\s*$/i, \"\")\n // Trailing comma/space artifacts\n .replace(/,\\s*$/, \"\")\n .trim()\n )\n}\n\n/**\n * Extract a statute citation from a \"named-code\" or \"mass-chapter\" token.\n *\n * Named-code: \"Cal. Penal Code § 187(a)\" → jurisdiction=CA, code=\"Penal\", section=\"187\", subsection=\"(a)\"\n * Mass-chapter: \"Mass. Gen. Laws ch. 93A, § 2\" → jurisdiction=MA, code=\"93A\", section=\"2\"\n */\nexport function extractNamedCode(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n\n let jurisdiction: string | undefined\n let code: string\n let rawBody: string\n let massMatch: RegExpExecArray | null = null\n let namedMatch: RegExpExecArray | null = null\n\n if (token.patternId === \"mass-chapter\") {\n massMatch = MASS_CHAPTER_RE.exec(text)\n if (massMatch) {\n jurisdiction = \"MA\"\n code = massMatch[2] // chapter number (e.g., \"93A\")\n // Section body is optional — chapter-only citations like `G.L. c. 93A`\n // are valid. When absent, leave the section empty. (#364)\n rawBody = massMatch[3] ?? \"\"\n } else {\n code = text\n rawBody = \"\"\n }\n } else {\n // named-code: \"[State prefix] [Code Name] § [body]\"\n namedMatch = NAMED_CODE_RE.exec(text)\n if (namedMatch) {\n jurisdiction = resolveJurisdiction(namedMatch[1])\n const rawCodeName = namedMatch[2]\n const cleaned = cleanCodeName(rawCodeName)\n\n if (jurisdiction) {\n // Look up in registry — use cleaned name as the lookup key\n const entry = findNamedCode(jurisdiction, cleaned)\n // Store the cleaned name (e.g., \"Penal\" not \"Penal Code\"); fall back to raw if no registry hit\n code = entry ? cleaned : rawCodeName.trim()\n } else {\n code = rawCodeName.trim()\n }\n\n rawBody = namedMatch[3]\n } else {\n // Unparseable token — graceful fallback\n code = text\n rawBody = \"\"\n }\n }\n\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Use section without trailing sentence punctuation for span boundary.\n // Note: section comes from parseBody() which strips et seq. and splits\n // subsections — the leading text still matches the raw match position.\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n\n let spans: StatuteComponentSpans | undefined\n if (massMatch?.indices) {\n spans = {}\n if (massMatch.indices[2]) spans.code = spanFromGroupIndex(span.cleanStart, massMatch.indices[2], transformationMap)\n if (massMatch.indices[3] && section) {\n const bodyStart = massMatch.indices[3][0]\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n } else if (namedMatch?.indices) {\n spans = {}\n if (namedMatch.indices[2]) spans.code = spanFromGroupIndex(span.cleanStart, namedMatch.indices[2], transformationMap)\n if (namedMatch.indices[3] && section) {\n const bodyStart = namedMatch.indices[3][0]\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n // Confidence: named-code patterns always require §, so known jurisdiction → 0.95 base\n let confidence = jurisdiction ? 0.95 : 0.5\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code,\n section,\n subsection,\n pincite: subsection,\n jurisdiction,\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * New Mexico bare-section form — `Section 32A-2-7(A)`, `§ 41-2-2`\n *\n * NM opinions cite NMSA 1978 sections in a distinctive bare form without\n * the code abbreviation. The three-hyphen section format\n * (`\\d[A-Z]?-\\d[A-Z]?-\\d[A-Z]?`) is unique among state codes and serves\n * as the disambiguator. #382\n *\n * @module extract/statutes/extractNmBareSection\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\nconst NM_BARE_SECTION_RE =\n /^(?:§\\s*|[Ss]ection\\s+)(\\d+[A-Z]?-\\d+[A-Z]?-\\d+[A-Z]?(?:\\([A-Za-z0-9]+\\))*)$/d\n\nexport function extractNmBareSection(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = NM_BARE_SECTION_RE.exec(text)!\n const rawBody = match[1]\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n const bodyIdx = match.indices[1]\n if (bodyIdx && section) {\n const bodyStart = bodyIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n let confidence = 0.9\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: \"NMSA 1978\",\n section,\n subsection,\n pincite: subsection,\n jurisdiction: \"NM\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * New York bare named-code form — `Penal Law § 130.52`, `Labor Law § 220 [3-a]`\n *\n * NY opinions omit the `N.Y.` prefix when citing their own state's codes.\n * The word `Law` after the code name is the disambiguator — other states\n * use `Code`. Bracket-subdivision groups (`[3]`, `[a]`, `[iv]`) are\n * accepted alongside the canonical `(N)` form. #386\n *\n * @module extract/statutes/extractNyBareLaw\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\nconst NY_BARE_LAW_RE =\n /^(Penal|Labor|Real Property|General Business|General Obligations|General Municipal|Municipal Home Rule|Criminal Procedure|Insurance|Executive|Judiciary|Civil Practice|Civil Rights|Education|Public Health|Banking|Domestic Relations|Environmental Conservation|Election|Social Services|Estates Powers and Trusts|Vehicle and Traffic|Surrogate's Court Procedure|Family Court|Court of Claims|Workers' Compensation|Highway|Tax|Personal Property)\\s+Law\\s+§§?\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\)|\\[[^\\]]*\\])*(?:\\s+(?:\\([^)]*\\)|\\[[^\\]]*\\]))*)$/d\n\nexport function extractNyBareLaw(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = NY_BARE_LAW_RE.exec(text)!\n const codeName = match[1]\n const rawBody = match[2]\n // parseBody splits on first `(` or `[` — strip any whitespace + extra\n // subdivision tail groups so the section captures just the number.\n const cleanBody = rawBody.replace(/\\s+/g, \"\")\n const { section, subsection, hasEtSeq } = parseBody(cleanBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n if (match.indices[1])\n spans.code = spanFromGroupIndex(span.cleanStart, match.indices[1], transformationMap)\n const bodyIdx = match.indices[2]\n if (bodyIdx && section) {\n const bodyStart = bodyIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n let confidence = 0.9\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: `${codeName} Law`,\n section,\n subsection,\n pincite: subsection,\n jurisdiction: \"NY\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Ohio Revised Code Chapter form — `R.C. Chapter 4509`, `R. C. Chapter 1702`\n *\n * Ohio (like NH) allows chapter-only references where the chapter number\n * functions as a complete citation. Spacing between `R.` and `C.` is\n * optional. The chapter identifier goes into the `section` field, matching\n * the convention established by the NH `rsa-chapter` extractor. #388\n *\n * @module extract/statutes/extractOhChapter\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\n\nconst OH_CHAPTER_RE = /^R\\.?\\s*C\\.?\\s+Chapter\\s+(\\d+)$/d\n\nexport function extractOhChapter(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = OH_CHAPTER_RE.exec(text)!\n const section = match[1]\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n if (match.indices[1])\n spans.section = spanFromGroupIndex(span.cleanStart, match.indices[1], transformationMap)\n }\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence: 0.95,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: \"R.C.\",\n section,\n jurisdiction: \"OH\",\n spans,\n }\n}\n","/**\n * Oregon Revised Statutes chapter-only form — `ORS chapter 34`\n *\n * Oregon (like NH and OH) allows chapter-only references where the\n * chapter number functions as a complete citation. The modern\n * `ORS NNN.NNN` section form is already handled by `abbreviated-code`.\n * #387\n *\n * @module extract/statutes/extractOrsChapter\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\n\nconst ORS_CHAPTER_RE = /^ORS\\s+chapter\\s+(\\d+)$/d\n\nexport function extractOrsChapter(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = ORS_CHAPTER_RE.exec(text)!\n const section = match[1]\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n if (match.indices[1])\n spans.section = spanFromGroupIndex(span.cleanStart, match.indices[1], transformationMap)\n }\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence: 0.95,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: \"ORS\",\n section,\n jurisdiction: \"OR\",\n spans,\n }\n}\n","/**\n * Prose-form Statute Extraction\n *\n * Parses natural language references like \"section 1983 of title 42\"\n * into structured StatuteCitation objects.\n *\n * @module extract/statutes/extractProse\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\n\n/** Parse \"section X(subsections) of title Y\" */\nconst PROSE_RE = /[Ss]ection\\s+(\\d+[A-Za-z0-9-]*)((?:\\([^)]*\\))*)\\s+of\\s+title\\s+(\\d+)/d\n\n/**\n * Extract a prose-form statute citation.\n * Currently handles federal \"section X of title Y\" form.\n */\nexport function extractProse(token: Token, transformationMap: TransformationMap): StatuteCitation {\n const { text, span } = token\n\n const match = PROSE_RE.exec(text)\n\n let section: string\n let subsection: string | undefined\n let title: number | undefined\n\n if (match) {\n section = match[1]\n subsection = match[2] || undefined\n title = Number.parseInt(match[3], 10)\n } else {\n section = text\n }\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match?.indices) {\n spans = {}\n if (match.indices[1]) spans.section = spanFromGroupIndex(span.cleanStart, match.indices[1], transformationMap)\n if (match.indices[3]) spans.title = spanFromGroupIndex(span.cleanStart, match.indices[3], transformationMap)\n if (match.indices[2] && subsection) {\n spans.subsection = spanFromGroupIndex(span.cleanStart, match.indices[2], transformationMap)\n }\n }\n\n let confidence = 0.85\n if (title !== undefined) confidence += 0.05\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n title,\n code: \"U.S.C.\",\n section,\n subsection,\n pincite: subsection,\n jurisdiction: \"US\",\n spans,\n }\n}\n","/**\n * Revised Laws of Hawaii (pre-1955) — historical compilation citations\n *\n * Hawaii compiled its statutes as RLH 1935, RLH 1945, and RLH 1955 before\n * adopting the modern Hawaii Revised Statutes (HRS) in 1968. Modern Hawaii\n * opinions still cite RLH when referencing pre-1955 statutory text. #359\n *\n * RLH 1935 § 2545\n * RLH 1945 § 7186\n * RLH 1955 § 7186\n *\n * The `RLH` token is distinctively Hawaii-only, so no jurisdiction\n * disambiguation is needed.\n *\n * @module extract/statutes/extractRlh\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\nconst RLH_RE =\n /^RLH\\s+(\\d{4})\\s+§\\s+(\\d+(?:[A-Za-z0-9:-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)$/d\n\nexport function extractRlh(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = RLH_RE.exec(text)!\n const year = Number.parseInt(match[1], 10)\n const rawBody = match[2]\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n const bodyIdx = match.indices[2]\n if (bodyIdx && section) {\n const bodyStart = bodyIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n // Confidence: 0.95 baseline; +0.05 with subsection.\n let confidence = 0.95\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: \"RLH\",\n section,\n subsection,\n pincite: subsection,\n year,\n jurisdiction: \"HI\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Nebraska Reissue Revised Statutes 1943 (R.R.S. 1943) — historical form\n *\n * section 38-901, R. R. S. 1943\n * § 30-2806, R.R.S. 1943, Reissue 1975\n *\n * Nebraska compiled its statutes in 1943 and re-issues individual volumes\n * on a rolling basis. The trailing `Reissue YYYY` clause gives the volume\n * year — when present it goes into `year` (and `editionLabel` is set to\n * `\"Reissue\"`). When absent, the citation refers to the original 1943\n * compilation. #373\n *\n * @module extract/statutes/extractRrs1943\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\nconst RRS_1943_RE =\n /^(?:[Ss]ections?|§§?)\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*),?\\s+R\\.?\\s*R\\.?\\s*S\\.?\\s+1943(?:,\\s+Reissue\\s+(\\d{4}))?$/d\n\nexport function extractRrs1943(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = RRS_1943_RE.exec(text)!\n const rawBody = match[1]\n const reissueYear = match[2] ? Number.parseInt(match[2], 10) : undefined\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n const bodyIdx = match.indices[1]\n if (bodyIdx && section) {\n const bodyStart = bodyIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n let confidence = 0.95\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: \"R.R.S. 1943\",\n section,\n subsection,\n pincite: subsection,\n year: reissueYear,\n editionLabel: reissueYear !== undefined ? \"Reissue\" : undefined,\n jurisdiction: \"NE\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Rhode Island General Laws 1956 — `G.L. 1956 (1969 Reenactment) §11-23-1`\n *\n * The `1956` literal year is the disambiguator vs. Massachusetts `G.L. c.\n * NNN` (chapter form). The optional `(YYYY Reenactment)` parenthetical\n * indicates which reenactment volume was in effect. #393\n *\n * @module extract/statutes/extractRigl1956\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\nconst RIGL_1956_RE =\n /^G\\.?\\s*L\\.?\\s+1956\\s*(?:\\((\\d{4})\\s+Reenactment\\))?\\s*,?\\s*§§?\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*)$/d\n\nexport function extractRigl1956(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = RIGL_1956_RE.exec(text)!\n const reenactmentYear = match[1] ? Number.parseInt(match[1], 10) : undefined\n const rawBody = match[2]\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n const bodyIdx = match.indices[2]\n if (bodyIdx && section) {\n const bodyStart = bodyIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n let confidence = 0.95\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: \"G.L. 1956\",\n section,\n subsection,\n pincite: subsection,\n year: reenactmentYear,\n editionLabel: reenactmentYear !== undefined ? \"Reenactment\" : undefined,\n jurisdiction: \"RI\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * New Hampshire RSA chapter form — `RSA chapter 169-D`, `RSA ch. 458-C`\n *\n * NH uniquely cites the chapter number alone (no section after the chapter)\n * as a complete citation. The colon-section form `RSA 511:2` is already\n * handled by the `abbreviated-code` family. #378\n *\n * The chapter goes into the `section` field — that's the canonical NH\n * shape: `code: \"RSA\"`, `section: \"169-D\"`. NH opinions treat the chapter\n * number as the citation identifier when no individual subsection is being\n * pin-cited.\n *\n * @module extract/statutes/extractRsaChapter\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\n\nconst RSA_CHAPTER_RE = /^RSA\\s+(?:\\[chapter\\]|chapter|ch\\.?)\\s+(\\d+(?:-[A-Z])?)$/d\n\nexport function extractRsaChapter(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = RSA_CHAPTER_RE.exec(text)!\n const section = match[1]\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n if (match.indices[1])\n spans.section = spanFromGroupIndex(span.cleanStart, match.indices[1], transformationMap)\n }\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence: 0.95,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: \"RSA\",\n section,\n jurisdiction: \"NH\",\n spans,\n }\n}\n","/**\n * Washington RCW chapter postfix form — `chapter 49.60 RCW`\n *\n * Canonical Washington court style places the chapter before RCW\n * (the opposite of the prefix `RCW chapter` form used in other states).\n * The chapter is in `NN.NN` format. The chapter identifier goes into the\n * `section` field, matching the convention from `rsa-chapter` (NH),\n * `oh-chapter`, and `ors-chapter`. #408\n *\n * @module extract/statutes/extractRcwChapterPostfix\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\n\nconst RCW_CHAPTER_POSTFIX_RE = /^[Cc]hapter\\s+(\\d+\\.\\d+)\\s+RCW$/d\n\nexport function extractRcwChapterPostfix(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = RCW_CHAPTER_POSTFIX_RE.exec(text)!\n const section = match[1]\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n if (match.indices[1])\n spans.section = spanFromGroupIndex(span.cleanStart, match.indices[1], transformationMap)\n }\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence: 0.95,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: \"RCW\",\n section,\n jurisdiction: \"WA\",\n spans,\n }\n}\n","/**\n * Tennessee Code Annotated postfix form — `§ 39-904, T.C.A.`\n *\n * Tennessee opinions sometimes place the code abbreviation AFTER the\n * section, separated by a comma. Sibling to Florida, Idaho, and Montana\n * postfix forms. #398\n *\n * @module extract/statutes/extractTcaPostfix\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\nconst TCA_POSTFIX_RE =\n /^(?:[Ss]ections?|[Ss]ec\\.?|§§?)\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*),?\\s+T\\.?C\\.?A\\.?$/d\n\nexport function extractTcaPostfix(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = TCA_POSTFIX_RE.exec(text)!\n const rawBody = match[1]\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n const bodyIdx = match.indices[1]\n if (bodyIdx && section) {\n const bodyStart = bodyIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n let confidence = 0.95\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: \"T.C.A.\",\n section,\n subsection,\n pincite: subsection,\n jurisdiction: \"TN\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Virginia bare-Code form — `Code § 18.2-308.2`, `Virginia Code § 8.01-581.17`\n *\n * Virginia's canonical court style omits the `Va.` prefix. The\n * disambiguator from Georgia pre-1983 (also bare `Code §`) is the PERIOD\n * in the title or section — Virginia sections always include at least one\n * period (`18.2-308.2`, `20-107.3`), while Georgia pre-1983 sections never\n * do (`26-2101`, `27-2501`). #405\n *\n * @module extract/statutes/extractVaBareCode\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\nconst VA_BARE_CODE_RE =\n /^(Virginia\\s+Code|Code)\\s+§\\s*((?:\\d+\\.\\d+-\\d+(?:\\.\\d+)?|\\d+-\\d+\\.\\d+)(?:\\([A-Za-z0-9]+\\))*)$/d\n\nexport function extractVaBareCode(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = VA_BARE_CODE_RE.exec(text)!\n const codeName = match[1].replace(/\\s+/g, \" \")\n const rawBody = match[2]\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n if (match.indices[1])\n spans.code = spanFromGroupIndex(span.cleanStart, match.indices[1], transformationMap)\n const bodyIdx = match.indices[2]\n if (bodyIdx && section) {\n const bodyStart = bodyIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n let confidence = 0.9\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: codeName,\n section,\n subsection,\n pincite: subsection,\n jurisdiction: \"VA\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Wisconsin Statutes postfix form — `§ 76.09, Stats.`\n *\n * § 76.09, Stats.\n * sec. 805.13(3), Stats.\n * § 48.415(l)(a)3, STATS. (uppercase, with trailing sub-subsection 3)\n *\n * Wisconsin court style places the `Stats.` abbreviation AFTER the\n * section, separated by a comma. Both lowercase `Stats.` and uppercase\n * `STATS.` are common. #414\n *\n * @module extract/statutes/extractWiStatsPostfix\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\nconst WI_STATS_POSTFIX_RE =\n /^(?:§§?|[Ss]ections?|[Ss]ec\\.?)\\s*(\\d+\\.\\d+(?:[A-Za-z0-9])?(?:\\s*\\([^)]*\\))*[A-Za-z0-9]*),?\\s+(?:Stats\\.|STATS\\.)$/d\n\nexport function extractWiStatsPostfix(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = WI_STATS_POSTFIX_RE.exec(text)!\n const rawBody = match[1].replace(/\\s+/g, \"\")\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n const bodyIdx = match.indices[1]\n if (bodyIdx && section) {\n const bodyStart = bodyIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n let confidence = 0.95\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: \"Wis. Stat.\",\n section,\n subsection,\n pincite: subsection,\n jurisdiction: \"WI\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * West Virginia historical Code 1931 — `Code 1931, 49-6-3, as amended`\n *\n * Code 1931, 49-6-3, as amended\n * Code, 1931, 49-6-3\n * Code, 14-2-13 (no year, comma-separated)\n *\n * West Virginia compiled its statutes in 1931. Modern WV opinions still\n * cite the 1931 code for statutory history. The 3-part hyphenated section\n * format (`N-N-N`) disambiguates from Georgia pre-1983 (2-part) and\n * Virginia bare-Code (always contains period). #406\n *\n * @module extract/statutes/extractWvCode1931\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport type { StatuteComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\nimport { parseBody } from \"./parseBody\"\n\nconst WV_CODE_1931_RE =\n /^Code,?(?:\\s+(1931))?,\\s+(\\d+-\\d+[A-Z]?-\\d+(?:[A-Za-z0-9])?(?:\\([A-Za-z0-9]+\\))*)$/d\n\nexport function extractWvCode1931(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n const { text, span } = token\n const match = WV_CODE_1931_RE.exec(text)!\n const year = match[1] ? Number.parseInt(match[1], 10) : undefined\n const rawBody = match[2]\n const { section, subsection, hasEtSeq } = parseBody(rawBody)\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let spans: StatuteComponentSpans | undefined\n if (match.indices) {\n spans = {}\n const bodyIdx = match.indices[2]\n if (bodyIdx && section) {\n const bodyStart = bodyIdx[0]\n const sectionSpanLen = section.replace(/[.,;:]\\s*$/, \"\").length\n spans.section = spanFromGroupIndex(\n span.cleanStart,\n [bodyStart, bodyStart + sectionSpanLen],\n transformationMap,\n )\n if (subsection) {\n const subStart = bodyStart + section.length\n spans.subsection = spanFromGroupIndex(\n span.cleanStart,\n [subStart, subStart + subsection.length],\n transformationMap,\n )\n }\n }\n }\n\n let confidence = 0.9\n if (subsection) confidence += 0.05\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: \"W. Va. Code\",\n section,\n subsection,\n pincite: subsection,\n year,\n jurisdiction: \"WV\",\n hasEtSeq: hasEtSeq || undefined,\n spans,\n }\n}\n","/**\n * Statute Citation Extraction — Dispatcher\n *\n * Routes statute tokens to family-specific extractors based on patternId.\n * This is the entry point called by extractCitations.ts (line 234).\n *\n * Family dispatch:\n * - \"usc\", \"cfr\" → extractFederal\n * - \"prose\" → extractProse\n * - \"abbreviated-code\" → extractAbbreviated\n * - \"chapter-act\" → extractChapterAct\n * - unknown → legacy inline parser (safety net for unknown patternIds)\n *\n * @module extract/extractStatute\n */\n\nimport type { Token } from \"@/tokenize\"\nimport type { StatuteCitation } from \"@/types/citation\"\nimport { resolveOriginalSpan, type TransformationMap } from \"@/types/span\"\nimport { extractAbbreviated } from \"./statutes/extractAbbreviated\"\nimport { extractAlaCode1940 } from \"./statutes/extractAlaCode1940\"\nimport { extractCaBareCode } from \"./statutes/extractCaBareCode\"\nimport { extractChapterAct } from \"./statutes/extractChapterAct\"\nimport { extractColoradoProse } from \"./statutes/extractColoradoProse\"\nimport { extractFederal } from \"./statutes/extractFederal\"\nimport { extractFloridaStatute } from \"./statutes/extractFloridaStatute\"\nimport { extractGaPre1983 } from \"./statutes/extractGaPre1983\"\nimport { extractIcYearEdition } from \"./statutes/extractIcYearEdition\"\nimport { extractIdahoPostfix } from \"./statutes/extractIdahoPostfix\"\nimport { extractIllRevStat } from \"./statutes/extractIllRevStat\"\nimport { extractIrc } from \"./statutes/extractIrc\"\nimport { extractKsaYearEdition } from \"./statutes/extractKsaYearEdition\"\nimport { extractMcaPostfix } from \"./statutes/extractMcaPostfix\"\nimport { extractMdArticleLetter } from \"./statutes/extractMdArticleLetter\"\nimport { extractMinnStYearEdition } from \"./statutes/extractMinnStYearEdition\"\nimport { extractNamedCode } from \"./statutes/extractNamedCode\"\nimport { extractNmBareSection } from \"./statutes/extractNmBareSection\"\nimport { extractNyBareLaw } from \"./statutes/extractNyBareLaw\"\nimport { extractOhChapter } from \"./statutes/extractOhChapter\"\nimport { extractOrsChapter } from \"./statutes/extractOrsChapter\"\nimport { extractProse } from \"./statutes/extractProse\"\nimport { extractRlh } from \"./statutes/extractRlh\"\nimport { extractRrs1943 } from \"./statutes/extractRrs1943\"\nimport { extractRigl1956 } from \"./statutes/extractRigl1956\"\nimport { extractRsaChapter } from \"./statutes/extractRsaChapter\"\nimport { extractRcwChapterPostfix } from \"./statutes/extractRcwChapterPostfix\"\nimport { extractTcaPostfix } from \"./statutes/extractTcaPostfix\"\nimport { extractVaBareCode } from \"./statutes/extractVaBareCode\"\nimport { extractWiStatsPostfix } from \"./statutes/extractWiStatsPostfix\"\nimport { extractWvCode1931 } from \"./statutes/extractWvCode1931\"\n\n/**\n * Legacy inline parser for unknown patterns.\n * Safety net for any patternId not explicitly handled by the dispatcher.\n */\nfunction extractLegacy(token: Token, transformationMap: TransformationMap): StatuteCitation {\n const { text, span } = token\n\n const statuteRegex = /^(?:(\\d+)\\s+)?([A-Za-z.\\s]+?)\\s*§+\\s*(\\d+[A-Za-z0-9-]*)/\n const match = statuteRegex.exec(text)\n\n // Graceful fallback for unparseable tokens — return low-confidence citation\n // rather than throwing (spec: \"Unknown codes produce citations with low confidence\")\n if (!match) {\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n return {\n type: \"statute\",\n text,\n span: { cleanStart: span.cleanStart, cleanEnd: span.cleanEnd, originalStart, originalEnd },\n confidence: 0.3,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n code: text,\n section: \"\",\n }\n }\n\n const title = match[1] ? Number.parseInt(match[1], 10) : undefined\n const code = match[2].trim()\n const section = match[3]\n\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n let confidence = 0.5\n const knownCodes = [\n \"U.S.C.\",\n \"C.F.R.\",\n \"Cal. Civ. Code\",\n \"Cal. Penal Code\",\n \"N.Y. Civ. Prac. L. & R.\",\n \"Tex. Civ. Prac. & Rem. Code\",\n ]\n\n if (knownCodes.some((c) => code.includes(c))) {\n confidence += 0.3\n }\n\n confidence = Math.min(confidence, 1.0)\n\n return {\n type: \"statute\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n title,\n code,\n section,\n }\n}\n\n/**\n * Extracts statute citation metadata from a tokenized citation.\n * Dispatches to family-specific extractors based on patternId.\n */\nexport function extractStatute(\n token: Token,\n transformationMap: TransformationMap,\n): StatuteCitation {\n switch (token.patternId) {\n case \"usc\":\n case \"cfr\":\n return extractFederal(token, transformationMap)\n case \"prose\":\n return extractProse(token, transformationMap)\n case \"abbreviated-code\":\n return extractAbbreviated(token, transformationMap)\n case \"ca-bare-code\":\n return extractCaBareCode(token, transformationMap)\n case \"named-code\":\n case \"mass-chapter\":\n return extractNamedCode(token, transformationMap)\n case \"chapter-act\":\n return extractChapterAct(token, transformationMap)\n case \"ill-rev-stat\":\n return extractIllRevStat(token, transformationMap)\n case \"ala-code-prefix\":\n case \"ala-title-trailer\":\n case \"ala-tit-bare\":\n return extractAlaCode1940(token, transformationMap)\n case \"colorado-prose\":\n return extractColoradoProse(token, transformationMap)\n case \"florida-postfix\":\n case \"florida-prefix-spelled\":\n return extractFloridaStatute(token, transformationMap)\n case \"ga-pre-1983\":\n return extractGaPre1983(token, transformationMap)\n case \"ic-year-edition\":\n return extractIcYearEdition(token, transformationMap)\n case \"irc\":\n return extractIrc(token, transformationMap)\n case \"idaho-postfix\":\n return extractIdahoPostfix(token, transformationMap)\n case \"ksa-year-edition\":\n return extractKsaYearEdition(token, transformationMap)\n case \"mca-postfix\":\n return extractMcaPostfix(token, transformationMap)\n case \"md-article-letter\":\n return extractMdArticleLetter(token, transformationMap)\n case \"minn-st-year-edition\":\n return extractMinnStYearEdition(token, transformationMap)\n case \"nm-bare-section\":\n return extractNmBareSection(token, transformationMap)\n case \"ny-bare-named-code\":\n return extractNyBareLaw(token, transformationMap)\n case \"oh-chapter\":\n return extractOhChapter(token, transformationMap)\n case \"ors-chapter\":\n return extractOrsChapter(token, transformationMap)\n case \"rlh\":\n return extractRlh(token, transformationMap)\n case \"rrs-1943\":\n return extractRrs1943(token, transformationMap)\n case \"rcw-chapter-postfix\":\n return extractRcwChapterPostfix(token, transformationMap)\n case \"rigl-1956\":\n return extractRigl1956(token, transformationMap)\n case \"rsa-chapter\":\n return extractRsaChapter(token, transformationMap)\n case \"tca-postfix\":\n return extractTcaPostfix(token, transformationMap)\n case \"va-bare-code\":\n return extractVaBareCode(token, transformationMap)\n case \"wi-stats-postfix\":\n return extractWiStatsPostfix(token, transformationMap)\n case \"wv-code-1931\":\n return extractWvCode1931(token, transformationMap)\n default:\n // unknown patterns use legacy parser\n return extractLegacy(token, transformationMap)\n }\n}\n","/**\n * Statutes at Large Citation Extractor\n *\n * Extracts session law citations from the Statutes at Large (e.g., \"124 Stat. 119\").\n * These are chronological compilations of federal laws, distinct from both\n * codified statutes (U.S.C.) and case reporters.\n *\n * Format: volume Stat. page [(year)]\n *\n * @module extract/extractStatutesAtLarge\n */\n\nimport type { Token } from \"@/tokenize/tokenizer\"\nimport type { StatutesAtLargeCitation } from \"@/types/citation\"\nimport type { StatutesAtLargeComponentSpans } from \"@/types/componentSpans\"\nimport { resolveOriginalSpan, spanFromGroupIndex, type TransformationMap } from \"@/types/span\"\n\nexport function extractStatutesAtLarge(\n token: Token,\n transformationMap: TransformationMap,\n): StatutesAtLargeCitation {\n const { text, span } = token\n\n // Parse volume-Stat.-page\n const statRegex = /^(\\d+(?:-\\d+)?)\\s+Stat\\.\\s+(\\d+)/d\n const match = statRegex.exec(text)\n\n if (!match) {\n throw new Error(`Failed to parse Statutes at Large citation: ${text}`)\n }\n\n const rawVolume = match[1]\n const volume = /^\\d+$/.test(rawVolume) ? Number.parseInt(rawVolume, 10) : rawVolume\n const page = Number.parseInt(match[2], 10)\n\n let spans: StatutesAtLargeComponentSpans | undefined\n if (match.indices) {\n spans = {\n volume: spanFromGroupIndex(span.cleanStart, match.indices[1]!, transformationMap),\n page: spanFromGroupIndex(span.cleanStart, match.indices[2]!, transformationMap),\n }\n }\n\n // Extract optional year in parentheses\n const yearRegex = /\\((?:.*?\\s)?(\\d{4})\\)/\n const yearMatch = yearRegex.exec(text)\n const year = yearMatch ? Number.parseInt(yearMatch[1], 10) : undefined\n\n // Translate positions from clean → original\n const { originalStart, originalEnd } = resolveOriginalSpan(span, transformationMap)\n\n // Confidence: 0.9 (Statutes at Large format is standardized)\n const confidence = 0.9\n\n return {\n type: \"statutesAtLarge\",\n text,\n span: {\n cleanStart: span.cleanStart,\n cleanEnd: span.cleanEnd,\n originalStart,\n originalEnd,\n },\n confidence,\n matchedText: text,\n processTimeMs: 0,\n patternsChecked: 1,\n volume,\n page,\n year,\n spans,\n }\n}\n","/**\n * Case Citation Regex Patterns\n *\n * These patterns are designed for tokenization (broad matching) not extraction.\n * They identify potential case citations in text for the tokenizer (Plan 3).\n * Metadata parsing and validation against reporters-db happens in Phase 2 Plan 5 (extraction layer).\n *\n * Pattern Design Principles (from RESEARCH.md):\n * - Use \\b word boundaries to avoid matching \"F.\" in \"F.B.I.\"\n * - Avoid nested quantifiers: (a+)+ causes ReDoS\n * - Keep patterns simple: tokenization only needs to find candidates\n * - Use global flag /g for matchAll()\n */\n\nimport type { FullCitationType } from \"@/types/citation\"\n\nexport interface Pattern {\n id: string\n regex: RegExp\n description: string\n type: FullCitationType\n}\n\nexport const casePatterns: Pattern[] = [\n {\n id: \"federal-reporter\",\n // Edition suffix accepts any ordinal (\"2d\", \"3d\", or generic \"Nth\") so the\n // pattern survives the eventual rollout of F.5th / F.6th / F.Supp.Nth (#234).\n // F.Supp.* and F.App'x must come before the generic F.* alternative so the\n // longer prefixes win during alternation.\n regex:\n /\\b(\\d+(?:-\\d+)?)\\s+(F\\.\\s?Supp\\.(?:\\s?(?:\\d+(?:st|nd|rd|th)|2d|3d))?|F\\.\\s?App'x|F\\.(?:\\d+(?:st|nd|rd|th)|2d|3d)?)\\s+(\\d+|_{3,}|-{3,})(?=\\s|$|\\(|,|;|\\.)/g,\n description: \"Federal Reporter (F., F.2d, F.3d, F.Nth, F.Supp., F.App'x, etc.)\",\n type: \"case\",\n },\n {\n id: \"supreme-court\",\n // L.Ed. edition suffix accepts any ordinal so a future L.Ed.3d edition does\n // not silently fall through to the state-reporter fallback (#234).\n regex:\n /\\b(\\d+(?:-\\d+)?)\\s+(U\\.\\s?S\\.|S\\.\\s?Ct\\.|L\\.\\s?Ed\\.(?:\\s?(?:\\d+(?:st|nd|rd|th)|2d|3d))?)\\s+(?:\\(\\d+\\s+[A-Z][A-Za-z.]+\\)\\s+)?(\\d+|_{3,}|-{3,})(?=\\s|$|\\(|,|;|\\.)/g,\n description:\n \"U.S. Supreme Court reporters (with optional nominative reporter parenthetical)\",\n type: \"case\",\n },\n {\n id: \"state-reporter\",\n // Character class admits `&` so reporters with ampersands tokenize correctly\n // (e.g., `I&N Dec.` and `I. & N. Dec.` for BIA immigration decisions — #244).\n // Apostrophe `'` is admitted for reporters like `F. App'x` already covered by\n // federal-reporter; including it here is harmless and future-proofs other\n // possessive forms. Trailing lookahead also accepts `[` (NY Slip Op `[U]`\n // markers — #231) and `]` (California Style Manual bracketed parallel\n // cites like `[266 Cal.Rptr. 569]` — #237). Negative lookahead on the\n // reporter body rejects ` at ` so `18 Cal.4th at p. 717` (CSM short-form,\n // #236) doesn't absorb `at p.` into the reporter; the short-form pattern\n // handles it instead.\n //\n // ` R.\\s+\\d` guard (#332): Illinois Supreme Court Rules cite as\n // `177 Ill. 2d R. 234` (volume + reporter + `R. ruleNum`), which the lazy\n // reporter capture used to absorb as `Ill. 2d R.` with `page=234`,\n // emitting a phantom case citation. The lookahead stops the lazy match\n // before it consumes ` R.` when a digit follows — leaving the input\n // untokenized rather than misclassified. A typed rule citation is out\n // of scope; the goal here is to suppress the false positive.\n regex:\n /\\b(\\d+(?:-\\d+)?)\\s+([A-Z](?:(?! L\\.[JQR\\s])(?! R\\.\\s+\\d)(?!\\s+vs?\\.\\s)(?!\\s+at\\s)[A-Za-z.\\d\\s&'])+?)\\s+(\\d+|_{3,}|-{3,})(?=\\s|$|\\(|,|;|\\.|\\[|\\])/g,\n description:\n 'State reporters (broad pattern allowing multi-word reporters with & and \\', excludes journal patterns with \" L.J/Q/Rev\", phantom matches across \" v. \"/\" vs. \", CSM \" at \" short-form boundaries, and Illinois \" R. N\" rule-marker boundaries, validated against reporters-db in Phase 3)',\n type: \"case\",\n },\n]\n","/**\n * Docket-Number Citation Patterns\n *\n * Patterns for case citations identified by docket / slip-opinion number\n * rather than a traditional reporter assignment. Common shapes:\n *\n * - NY Court of Appeals slip ops: `Party v. Party, No. 51 (N.Y. 2023)`\n * - Federal district court pre-reporter: `Smith v. Jones, No. 12-3456 (S.D.N.Y. 2024)`\n *\n * Disambiguation: a bare `No. 51 (N.Y. 2023)` is too generic to extract\n * without strong context. The pattern matches the `No. <docket> (<court> <year>)`\n * core; the extractor enforces a case-name anchor and only emits a citation\n * when a `Party v. Party,` (or `In re Party,`) prefix is found.\n */\n\nimport type { Pattern } from \"./casePatterns\"\n\nexport const docketPatterns: Pattern[] = [\n {\n id: \"docket-paren-court-year\",\n // Match: \"No. <docket-number> (<court...> <year>)\"\n // docket-number: digits, optionally hyphenated (51, 12-3456, 22-cv-1234)\n // parenthetical: anything (lazy) ending with a 4-digit year\n // The `\\bNo\\.` anchor + space-separated paren keep this narrow without a\n // case-name lookbehind — case-name validation lives in the extractor.\n regex: /\\bNo\\.\\s+([\\d]+(?:-[\\w\\d]+)*)\\s+\\(([^)]+\\s(\\d{4}))\\)/g,\n description: 'Docket-number citation: \"Party v. Party, No. <docket> (<court> <year>)\"',\n type: \"docket\",\n },\n]\n","/**\n * Journal Citation Regex Patterns\n *\n * Patterns for law review and journal citations.\n * These are intentionally broad for tokenization - validation against\n * journals-db happens in Phase 3 (extraction layer).\n *\n * Pattern Design:\n * - Matches volume-journal-page format\n * - Broad journal name matching (validated later)\n * - Simple structure to avoid ReDoS\n */\n\nimport type { Pattern } from \"./casePatterns\"\n\nexport const journalPatterns: Pattern[] = [\n {\n id: \"law-review\",\n regex: /\\b(\\d+(?:-\\d+)?)\\s+([A-Z](?:(?!\\s+vs?\\.\\s)(?!\\s+at\\s+\\d)[A-Za-z.\\s])+)\\s+(\\d+)\\b/g,\n description:\n 'Law review citations (e.g., \"120 Harv. L. Rev. 500\"), validated against journals-db in Phase 3. Negative lookaheads exclude \" v. \"/\" vs. \" (so a party-name run isn\\'t mis-captured as a journal) and \" at <digit>\" (so a short-form pincite like \"554 U.S. at 621\" isn\\'t mis-captured as a journal).',\n type: \"journal\",\n },\n]\n","/**\n * Neutral and Online Citation Regex Patterns\n *\n * Patterns for WestLaw, LexisNexis, public laws, and Federal Register citations.\n * These have predictable formats and don't require external validation.\n *\n * Pattern Design:\n * - Matches year-database-number format for online citations\n * - Matches Pub. L. No. format for public laws\n * - Matches volume-Fed. Reg.-page for Federal Register\n * - Simple structure to avoid ReDoS\n */\n\nimport type { Pattern } from \"./casePatterns\"\n\nexport const neutralPatterns: Pattern[] = [\n {\n // Mississippi 4-segment form: year-caseType-number-appellateTrack. Listed\n // before the 3-segment hyphenated pattern so it wins on the longer match\n // (e.g., \"2010-CT-01234-SCT\"). (#233)\n id: \"state-vendor-neutral-hyphenated-ms\",\n regex: /\\b(\\d{4})-([A-Z]+)-(\\d+)-([A-Z]+)\\b/g,\n description:\n 'Mississippi 4-segment vendor-neutral (e.g., \"2010-CT-01234-SCT\", \"2015-CA-00567-COA\")',\n type: \"neutral\",\n },\n {\n // 3-segment hyphenated form used by NM (NMSC, NMCA, NMCERT), Ohio\n // (mixed-case \"Ohio\" token), and NC (NCSC, NCCOA). The court token starts\n // with an uppercase letter and may contain lowercase (so the Ohio token\n // matches). (#233)\n id: \"state-vendor-neutral-hyphenated\",\n regex: /\\b(\\d{4})-([A-Z][A-Za-z]+)-(\\d+)\\b/g,\n description:\n 'Hyphenated vendor-neutral (e.g., \"2010-NMSC-007\", \"2024-Ohio-764\", \"2020-NCSC-118\")',\n type: \"neutral\",\n },\n {\n // Multi-word neutral courts (#230). Alternation order matters — longer,\n // more specific patterns must precede the bare `[A-Z]{2}` fallback so the\n // regex prefers the more specific match:\n // - `IL App (Nst)` — Illinois Rule 23 form with district parenthetical\n // (districts 1st / 2d / 3d / 4th / 5th)\n // - `OK CIV APP|CR|AG` — Oklahoma multi-word courts\n // - `[A-Z]{2}(?:\\s+App\\.?)?` — existing single-word + optional App fallback\n // The trailing `(-U)?` captures Illinois Rule 23 unpublished marker; the\n // extractor consumes it into the `unpublished` flag and strips it from\n // `documentNumber`.\n id: \"state-vendor-neutral\",\n regex:\n /\\b(\\d{4})\\s+(IL\\s+App\\s+\\(\\d+(?:st|nd|rd|th|d)\\)|OK\\s+(?:CIV\\s+APP|CR|AG)|[A-Z]{2}(?:\\s+App\\.?)?)\\s+(\\d+(?:-U)?)\\b/g,\n description:\n 'State vendor-neutral citations (e.g., \"2007 UT 49\", \"2017 WI 17\", \"2013 IL 112116\", \"2011 IL App (1st) 101234\", \"2020 OK CIV APP 67\", \"2020 IL App (2d) 190123-U\")',\n type: \"neutral\",\n },\n {\n id: \"westlaw\",\n regex: /\\b(\\d{4})\\s+WL\\s+(\\d+)\\b/g,\n description: 'WestLaw citations (e.g., \"2021 WL 123456\")',\n type: \"neutral\",\n },\n {\n // Generalized to accept any uppercase-prefixed court abbreviation before\n // LEXIS so state variants (Cal. LEXIS, Tex. App. LEXIS, N.Y. Misc. LEXIS,\n // Ill. App. LEXIS, etc.) tokenize alongside the federal U.S. forms (#228).\n // The non-greedy `[A-Z][A-Za-z.\\s]+?` is bounded by the literal `\\s+LEXIS`\n // that follows it, so it can't run away.\n id: \"lexis\",\n regex: /\\b(\\d{4})\\s+[A-Z][A-Za-z.\\s]+?\\s+LEXIS\\s+(\\d+)\\b/g,\n description:\n 'LexisNexis citations (federal: \"2021 U.S. LEXIS 5000\", \"2021 U.S. App. LEXIS 12345\"; state: \"2020 Cal. LEXIS 1000\", \"2020 Tex. App. LEXIS 5000\")',\n type: \"neutral\",\n },\n {\n id: \"public-law\",\n regex: /\\bPub\\.\\s?L\\.(?:\\s?No\\.)?\\s?(\\d+-\\d+)\\b/g,\n description: 'Public Law citations (e.g., \"Pub. L. No. 117-58\" or \"Pub. L. 116-283\")',\n type: \"publicLaw\",\n },\n {\n id: \"federal-register\",\n regex: /\\b(\\d+(?:-\\d+)?)\\s+Fed\\.\\s?Reg\\.\\s+(\\d+)\\b/g,\n description: 'Federal Register citations (e.g., \"86 Fed. Reg. 12345\")',\n type: \"federalRegister\",\n },\n {\n id: \"statutes-at-large\",\n regex: /\\b(\\d+(?:-\\d+)?)\\s+Stat\\.\\s+(\\d+)\\b/g,\n description: 'Statutes at Large citations (e.g., \"124 Stat. 119\")',\n type: \"statutesAtLarge\",\n },\n {\n id: \"compact-law-review\",\n regex: /\\b(\\d+(?:-\\d+)?)\\s+([A-Z][A-Za-z.]+L\\.(?:Rev|J|Q)\\.)\\s+(\\d+)\\b/g,\n description: 'Compact law review citations without spaces (e.g., \"93 Harv.L.Rev. 752\")',\n type: \"journal\",\n },\n]\n","/**\n * Short-form Citation Regex Patterns\n *\n * Patterns for Id., Ibid., supra, and short-form case citations.\n * These refer to earlier citations in the document.\n *\n * Pattern Design:\n * - Simple structure to avoid ReDoS (no nested quantifiers)\n * - Broad matching for tokenization; validation happens in extraction layer\n * - Word boundaries to prevent false positives (e.g., \"Idaho\" vs \"Id.\")\n */\n\nimport type { Pattern } from \"./casePatterns\"\n\n/** Id. with optional pincite: \"Id.\" or \"Id. at 253\" or \"Id., at 253\" or\n * \"Id. ¶ 12\" (#204).\n *\n * Punctuation tolerance (#305):\n * - Optional space before the period — `Id .` / `Ibid .` (OCR + older\n * typesetting).\n * - Comma instead of period — `Id, at 1483` — only when immediately\n * followed by `at` so bare `Id,` in prose (\"his Id, but...\") is not\n * misread as a citation.\n *\n * Pincite captures an optional \"*\" prefix for star-pagination (NY Slip Op,\n * Westlaw, Lexis; see #191), an optional trailing \" n.14\" / \" nn.14-15\"\n * footnote suffix (see #202), an optional `p.` / `pp.` prefix for\n * California Style Manual form (see #236), and `¶` / `¶¶` / `para.` /\n * `paras.` paragraph markers (#204). When the pincite is a paragraph form,\n * `at` is optional — `Id. ¶ 12` and `Id. at ¶ 12` both match. */\nexport const ID_PATTERN: RegExp =\n /(?:^|(?<=\\s)|(?<=[\"'(\\[—]))\\b[Ii]d(?:\\s*\\.|\\s*,(?=\\s+at\\s))(?:(?:,\\s+|,?\\s+(?:at\\s+(?:pp?\\.\\s*)?|(?=¶|paras?\\.?\\b)))(¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?|\\*?\\d+(?:\\s*[-–]\\s*\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?))?/g\n\n/** Ibid. with optional pincite (less common variant). Paragraph forms (#204)\n * follow the same convention as Id. Optional space before the period (#305). */\nexport const IBID_PATTERN: RegExp =\n /(?:^|(?<=\\s)|(?<=[\"'(\\[—]))\\b[Ii]bid\\s*\\.(?:(?:,\\s+|,?\\s+(?:at\\s+(?:pp?\\.\\s*)?|(?=¶|paras?\\.?\\b)))(¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?|\\*?\\d+(?:\\s*[-–]\\s*\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?))?/g\n\n/**\n * Supra with party name and optional pincite.\n * Pattern: word(s), supra [note N] [, at page]\n * Captures: (1) party name, (2) note number (if any), (3) pincite\n *\n * Party-name capture (#301): continuation accepts `\\s+v\\.?\\s+` (v.),\n * `\\s+&\\s+` (ampersand-joined parties — `Walker & Horwich, supra`),\n * `,\\s+` (corporate-suffix continuation — `Thorn Americas, Inc., supra`),\n * and plain whitespace (multi-word names). Each continuation requires a\n * capital-letter follow-on, so `, supra` (lowercase `s`) still terminates\n * the name. NOTE: `In re` prefix is NOT included here — the resolver's\n * BKTree matches full-cite party names that don't carry the prefix\n * (#216 / #21), so a supra with `In re X` won't match a full cite\n * indexed as `X`. Handling that gap requires resolver-side normalization,\n * which is intentionally out of scope for #301.\n *\n * Pincite accepts optional \"*\" prefix for star-pagination (#191), an optional\n * range end (#236), an optional trailing footnote suffix (#202), an optional\n * `p.` / `pp.` prefix for California Style Manual form (#236), and `¶` /\n * `¶¶` / `para.` / `paras.` paragraph markers (#204). When the pincite is a\n * paragraph form, `at` is optional.\n */\nexport const SUPRA_PATTERN: RegExp =\n /\\b([A-Z][a-zA-Z''\\-]+\\.?(?:(?:\\s+v\\.?\\s+|\\s+&\\s+|,\\s+|\\s+)[A-Z][a-zA-Z''\\-]+\\.?)*)\\s*,?\\s+supra(?:\\s+note\\s+(\\d+))?(?:(?:,\\s+|,?\\s+(?:at\\s+(?:pp?\\.\\s*)?|(?=¶|paras?\\.?\\b)))(¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?|\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?))?/g\n\n/**\n * Standalone supra without party name (common in footnotes).\n * Matches: \"supra note 12\", \"supra at 15\", \"supra § 3\", \"supra Part II\"\n * Requires \"note\", \"at\", \"§\", \"Part\", or \"p.\" after supra to avoid matching\n * the word \"supra\" in prose. Preceded by whitespace, start, or signal words.\n * Captures: (1) note number (if any), (2) pincite (with optional \"*\" prefix,\n * #191, optional range end / `p.`/`pp.` prefix #236, optional trailing\n * footnote suffix #202, and `¶`/`para.` paragraph markers #204).\n */\nexport const STANDALONE_SUPRA_PATTERN: RegExp =\n /(?:^|(?<=\\s)|(?<=[;.]))supra(?:\\s+note\\s+(\\d+)(?:,?\\s+(?:at\\s+(?:pp?\\.\\s*)?|(?=¶|paras?\\.?\\b))(¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?|\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?))?|\\s+(?:at\\s+(?:pp?\\.\\s*)?|(?=¶|paras?\\.?\\b))(¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?|\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?)|\\s+(?:§+|Part|p\\.)\\s*\\S+)/g\n\n/**\n * Bracketed supra forms (#306) — `[supra]`, `[supra, 705]`, `[supra at 78-82]`,\n * and `State v. Jarzbek, [supra, 705]`. Connecticut Supreme/Appellate use\n * brackets around the supra token when it appears inside a string-cite or\n * quotation. The comma-pincite form `[supra, 705]` accepts NO `at` before\n * the page — that's the Connecticut convention.\n *\n * Captures: (1) party name (optional; undefined for bare standalone form),\n * (2) pincite (optional, accepts comma-form `, N` or `at N` shape with\n * optional range `N-M`).\n */\nexport const BRACKETED_SUPRA_PATTERN: RegExp =\n /(?:\\b([A-Z][a-zA-Z''\\-]+\\.?(?:(?:\\s+v\\.?\\s+|\\s+&\\s+|,\\s+|\\s+)[A-Z][a-zA-Z''\\-]+\\.?)*)\\s*,?\\s+)?\\[supra(?:(?:,\\s+|\\s+at\\s+(?:pp?\\.\\s*)?)(\\d+(?:[-–—]\\d+)?))?\\]/g\n\n/**\n * Short-form case: [Party,] volume reporter [,] at page\n * Pattern: optional Party name, then number space abbreviation [, ] at space number.\n * Simplified detection; full parsing in extraction layer.\n * Supports reporters with 1-2 letter ordinal suffixes (e.g., F.4th, Cal.4th).\n * Handles SCOTUS/federal comma-before-at: \"597 U.S., at 721\", \"116 F.4th, at 1193\".\n * Pincite accepts optional \"*\" prefix for star-pagination (#191), an optional\n * range end \"462-65\" / \"462-*65\" (#201), an optional trailing footnote suffix\n * \" n.14\" / \" nn.14-15\" (#202), an optional `p.` / `pp.` prefix for\n * California Style Manual form (`18 Cal.4th at p. 717`; see #236), and `¶` /\n * `¶¶` / `para.` / `paras.` paragraph markers (#204).\n *\n * Optional leading party-name group captures Bluebook back-references like\n * `Smith, 500 F.2d at 125` so the resolver can disambiguate when multiple\n * full citations share the same volume+reporter (#278). Group order:\n * 1: party name (optional)\n * 2: volume\n * 3: reporter\n * 4: pincite\n *\n * Pincite prefix also tolerates the spelled-out `page` / `pages` form\n * (`281 Ala. at page 322`, `38 Ala.App. at pages 186`) common in Alabama\n * appellate writing (#344). Without this, the input slipped past the\n * short-form-case pattern and was misclassified as a journal citation by\n * a later pattern.\n */\nexport const SHORT_FORM_CASE_PATTERN: RegExp =\n /\\b(?:([A-Z][a-zA-Z''\\-]+\\.?(?:(?:\\s+v\\.?\\s+|\\s+&\\s+|,\\s+|\\s+)[A-Z][a-zA-Z''\\-]+\\.?)*),\\s+)?(\\d+(?:-\\d+)?)\\s+([A-Z][A-Za-z.''\\s]+?(?:\\d[a-z]{1,2})?)\\s*,?\\s+at\\s+(?:pp?\\.\\s*|pages?\\s+)?(\\*?\\d+(?:[-–—]\\*?\\d+)?(?:\\s+(?:nn?|note)\\s*\\.?\\s*\\d+(?:[-–—]\\d+)?)?|¶¶?\\s*\\d+(?:[-–—]\\d+)?|paras?\\.?\\s*\\d+(?:[-–—]\\d+)?)\\b/g\n\n/** All short-form patterns for tokenization */\nexport const SHORT_FORM_PATTERNS: readonly RegExp[] = [\n ID_PATTERN,\n IBID_PATTERN,\n BRACKETED_SUPRA_PATTERN,\n SUPRA_PATTERN,\n STANDALONE_SUPRA_PATTERN,\n SHORT_FORM_CASE_PATTERN,\n] as const\n\n/** Pattern objects for consistency with other pattern modules */\nexport const shortFormPatterns: Pattern[] = [\n {\n id: \"id\",\n regex: ID_PATTERN,\n description: 'Id. citations (e.g., \"Id.\" or \"Id. at 253\")',\n type: \"case\", // Will be typed as 'id' in extraction layer\n },\n {\n id: \"ibid\",\n regex: IBID_PATTERN,\n description: 'Ibid. citations (e.g., \"Ibid.\" or \"Ibid. at 125\")',\n type: \"case\", // Will be typed as 'id' in extraction layer\n },\n {\n id: \"supra\",\n regex: BRACKETED_SUPRA_PATTERN,\n description:\n 'Bracketed supra citations (e.g., \"State v. Jarzbek, [supra, 705]\" — Connecticut style; #306)',\n type: \"case\", // Will be typed as 'supra' in extraction layer\n },\n {\n id: \"supra\",\n regex: SUPRA_PATTERN,\n description: 'Supra citations (e.g., \"Smith, supra\" or \"Smith, supra, at 460\")',\n type: \"case\", // Will be typed as 'supra' in extraction layer\n },\n {\n id: \"supra\",\n regex: STANDALONE_SUPRA_PATTERN,\n description: 'Standalone supra (e.g., \"supra note 12\" or \"supra at 15\")',\n type: \"case\", // Will be typed as 'supra' in extraction layer\n },\n {\n id: \"shortFormCase\",\n regex: SHORT_FORM_CASE_PATTERN,\n description: 'Short-form case citations (e.g., \"500 F.2d at 125\")',\n type: \"case\", // Will be typed as 'shortFormCase' in extraction layer\n },\n]\n","/**\n * Statute Citation Regex Patterns\n *\n * Patterns for federal (USC, CFR), state, and prose-form statute citations.\n * Intentionally broad for tokenization — extraction layer validates and\n * routes to jurisdiction-specific extractors.\n *\n * Pattern families (spec Section 2):\n * - Federal: usc, cfr (enhanced with subsections, et seq., §§)\n * - Prose: \"section X of title Y\"\n * - Illinois: chapter-act (ILCS chapter/act/section format)\n */\n\nimport { buildCaBareCodeRegex } from \"@/data/caBareCodes\"\nimport { buildAbbreviatedCodeRegex } from \"@/data/stateStatutes\"\nimport type { Pattern } from \"./casePatterns\"\n\nexport const statutePatterns: Pattern[] = [\n {\n id: \"usc\",\n regex:\n /\\b(\\d+)\\s+(?:U\\.S\\.C\\.?|USC)\\s*§§?\\s*(\\d+[A-Za-z0-9-]*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)/g,\n description:\n 'U.S. Code citations with optional subsections and et seq. (e.g., \"42 U.S.C. § 1983(a)(1) et seq.\")',\n type: \"statute\",\n },\n {\n id: \"cfr\",\n regex:\n /\\b(\\d+)\\s+C\\.?F\\.?R\\.?\\s*(?:(?:Part|pt\\.)\\s+|§§?\\s*)(\\d+(?:\\.\\d+)?[A-Za-z0-9-]*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)/g,\n description:\n 'Code of Federal Regulations with Part or §, subsections, et seq. (e.g., \"12 C.F.R. Part 226\", \"40 C.F.R. § 122.26(b)(14)\")',\n type: \"statute\",\n },\n {\n // Internal Revenue Code (IRC) — federal tax code citations. The `I.R.C.`\n // form is the canonical Bluebook abbreviation; bare `IRC` is also common.\n // Without this pattern, Ohio's `R.C.` regex matches the suffix of\n // `I.R.C.` and silently routes every IRC citation to Ohio jurisdiction\n // (14/14 misclassifications in a 37-opinion NJ sweep). #376\n //\n // Listed BEFORE `abbreviated-code` so the longer `I.R.C.` match wins\n // span dedup over Ohio's `R.C.` match at the same position.\n //\n // Captures: (1) section body.\n id: \"irc\",\n regex:\n /\\b(?:I\\.R\\.C\\.|IRC)\\s*§§?\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)/g,\n description: 'Internal Revenue Code: \"I.R.C. § 1367\", \"IRC § 1341\" — #376',\n type: \"statute\",\n },\n {\n // New Hampshire Revised Statutes Annotated — chapter form. NH uniquely\n // cites the chapter number alone (no section) as a valid citation:\n // `RSA chapter 169-D`, `RSA ch. 458-C`, `RSA [chapter] 173-B`. The\n // bracketed-chapter form (`[chapter]`) is a typographical convention\n // used by some NH opinions to indicate the chapter token was inserted\n // by the reporter rather than original text. The colon-section form\n // (`RSA 511:2`) is already handled by `abbreviated-code`. #378\n //\n // Captures: (1) chapter body — digits + optional hyphen-letter suffix\n // (NH uses forms like `169-D`, `458-C`).\n id: \"rsa-chapter\",\n regex: /\\bRSA\\s+(?:\\[chapter\\]|chapter|ch\\.?)\\s+(\\d+(?:-[A-Z])?)/g,\n description:\n 'New Hampshire RSA chapter-only form: \"RSA chapter 169-D\" / \"RSA ch. 458-C\" — #378',\n type: \"statute\",\n },\n {\n // Ohio Revised Code Chapter form: `R.C. Chapter 4509`, `R. C. Chapter\n // 1702`. The chapter number is treated as a complete citation (Ohio,\n // like NH, allows chapter-only references). Spacing between `R.` and\n // `C.` is optional — both `R.C.` and `R. C.` are accepted. #388\n //\n // Captures: (1) chapter number.\n id: \"oh-chapter\",\n regex: /\\bR\\.?\\s*C\\.?\\s+Chapter\\s+(\\d+)/g,\n description:\n 'Ohio Revised Code chapter-only form: \"R.C. Chapter 4509\" / \"R. C. Chapter 1702\" — #388',\n type: \"statute\",\n },\n {\n // Oregon Revised Statutes chapter-only form: `ORS chapter 34`. The\n // modern `ORS NNN.NNN` section form is already handled by\n // `abbreviated-code`; this pattern captures chapter-only references.\n // #387\n //\n // Captures: (1) chapter number.\n id: \"ors-chapter\",\n regex: /\\bORS\\s+chapter\\s+(\\d+)/g,\n description: 'Oregon Revised Statutes chapter-only form: \"ORS chapter 34\" — #387',\n type: \"statute\",\n },\n {\n id: \"prose\",\n regex: /\\b[Ss]ection\\s+(\\d+[A-Za-z0-9-]*(?:\\([^)]*\\))*)\\s+of\\s+title\\s+(\\d+)\\b/g,\n description:\n 'Prose-form federal citations (e.g., \"section 1983 of title 42\"). Note: MD-style \"section X of the Y Article\" deferred to PR 3.',\n type: \"statute\",\n },\n {\n id: \"named-code\",\n // Matches: [State abbrev]. [Code/Law Name] § [section]\n // Captures: (1) jurisdiction prefix, (2) code name text, (3) section+subsections+et seq\n //\n // Code-name body (#328): each word must be capitalized — real code names\n // are title-case sequences like `Penal Code`, `Civ. Prac. & Rem. Code Ann.`,\n // `Insurance Law`. The previous broad `[A-Za-z.&',\\s]+?` accepted lowercase\n // prose, so when the input had a stray earlier `California` followed by\n // sentence prose then `California Penal Code § 549`, the regex absorbed\n // the entire intervening clause into the code-name span.\n //\n // Section body: period only allowed when followed by alphanumeric, so a\n // trailing sentence period is not absorbed (#283).\n regex:\n /\\b(N\\.?\\s*Y\\.?|Cal(?:ifornia)?\\.?|Tex(?:as)?\\.?|Md\\.?|(?<!W\\.?\\s?)Va\\.?|Ala(?:bama)?\\.?)\\s+([A-Z][A-Za-z.&']*(?:(?:\\s+|,\\s+)(?:&|[A-Z][A-Za-z.&']*))*)\\s*§§?\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)/g,\n description:\n \"Named-code state citations (NY, CA, TX, MD, VA, AL) with jurisdiction prefix + code name + §\",\n type: \"statute\",\n },\n {\n // New York bare named-code form: `Penal Law § 130.52`, `Labor Law §\n // 220 [3-a]`. NY opinions omit the `N.Y.` prefix when citing their own\n // state's codes (other states use `Code` while NY uses `Law` — the\n // word `Law` after the code name is the disambiguator). The enumerated\n // list of NY law names is closed; matching is restricted to known NY\n // codes so the false-positive risk is bounded. Bracket-subdivision\n // groups `[3]`, `[a]`, `[iv]` are accepted alongside the canonical\n // `(N)` form — NY style is to use brackets when paren collisions are\n // a concern. #386\n //\n // Listed AFTER `named-code` so the longer `N.Y. Penal Law § N` form\n // wins span dedup when the `N.Y.` prefix is present.\n //\n // Captures: (1) code name, (2) section body with bracket/paren chain.\n id: \"ny-bare-named-code\",\n regex:\n /\\b(Penal|Labor|Real Property|General Business|General Obligations|General Municipal|Municipal Home Rule|Criminal Procedure|Insurance|Executive|Judiciary|Civil Practice|Civil Rights|Education|Public Health|Banking|Domestic Relations|Environmental Conservation|Election|Social Services|Estates Powers and Trusts|Vehicle and Traffic|Surrogate's Court Procedure|Family Court|Court of Claims|Workers' Compensation|Highway|Tax|Personal Property)\\s+Law\\s+§§?\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\)|\\[[^\\]]*\\])*(?:\\s+(?:\\([^)]*\\)|\\[[^\\]]*\\]))*)/g,\n description:\n 'New York bare named-code form: \"Penal Law § 130.00 [3]\", \"Labor Law § 220 [3-a]\" — #386',\n type: \"statute\",\n },\n {\n id: \"mass-chapter\",\n // Matches: Mass. Gen. Laws ch. X, § Y / M.G.L.A. c. X, § Y / G.L. c. X, § Y / A.L.M. c. X, § Y\n // Spacing between corpus prefix and `c.` is optional (`G.L.c.` is common\n // Massachusetts court style). The section connector accepts both `§` and\n // `sec.` / `Sec.` (Massachusetts opinions use both). The section portion\n // itself is optional — chapter-only citations like `G.L. c. 93A`\n // refer to the entire chapter and are valid by themselves. #364\n //\n // Section body: period only allowed when followed by alphanumeric, so a\n // trailing sentence period is not absorbed (#283).\n regex:\n /\\b(Mass\\.?\\s*Gen\\.?\\s*Laws|General\\s+Laws|M\\.?G\\.?L\\.?A?\\.?|A\\.?L\\.?M\\.?|G\\.?\\s*L\\.?)\\s*(?:ch\\.?|c\\.?)\\s*(\\w+)(?:,?\\s*(?:§§?|[Ss]ec\\.?|[Ss]ection)\\s*(\\w+(?:[\\w/-]|\\.(?=\\w))*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?))?/g,\n description: 'Massachusetts chapter-based citations (e.g., \"Mass. Gen. Laws ch. 93A, § 2\")',\n type: \"statute\",\n },\n {\n id: \"chapter-act\",\n // IL: \"735 ILCS 5/2-1001\" or \"735 Ill. Comp. Stat. 5/2-1001\"\n // Captures: (1) chapter, (2) act, (3) section+subsections+et seq\n //\n // Section body: digits then alphanumeric/colon/slash/hyphen OR\n // period-followed-by-alphanumeric (lookahead). The period guard\n // prevents sentence-ending punctuation from being absorbed into\n // the section field (`5 ILCS 100/1-1.` → section \"1-1\", not \"1-1.\";\n // #283 / #331).\n regex:\n /\\b(\\d+)\\s+(?:ILCS|Ill\\.?\\s*Comp\\.?\\s*Stat\\.?)\\s*(?:Ann\\.?\\s+)?(\\d+)\\/(\\d+(?:[A-Za-z0-9:-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)/g,\n description: 'Illinois Compiled Statutes chapter-act citations (e.g., \"735 ILCS 5/2-1001\")',\n type: \"statute\",\n },\n {\n id: \"ill-rev-stat\",\n // Pre-1993 Illinois Revised Statutes (#330): `Ill. Rev. Stat. YYYY, ch. N,\n // par. N.N(N)`. Modern Illinois opinions still use this form when\n // referencing the historical version of a statute.\n //\n // Tolerance: spaced/no-space (`Ill. Rev. Stat.` / `Ill.Rev.Stat.`),\n // capitalized/lowercase `[Cc]h.`, singular/plural `pars?.`, optional\n // commas after `Stat.` and after the chapter number.\n //\n // Captures: (1) year-of-edition, (2) chapter (incl. letter suffix `110A`),\n // (3) paragraph body (subparagraphs + et seq.).\n regex:\n /\\bIll\\.?\\s*Rev\\.?\\s*Stat\\.?,?\\s+(\\d{4}),?\\s+[Cc]h\\.\\s+(\\d+[A-Z]?),?\\s+pars?\\.\\s+(\\d+(?:[A-Za-z0-9:-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)/g,\n description:\n \"Illinois Revised Statutes (pre-1993): Ill. Rev. Stat. YYYY, ch. N, par. N\",\n type: \"statute\",\n },\n {\n // Revised Laws of Hawaii — pre-1955 Hawaii statutory compilations\n // (`RLH 1935 § 2545`, `RLH 1945 § 7186`, `RLH 1955 § 7186`). Modern\n // Hawaii opinions still cite RLH when discussing pre-1955 statutory\n // history. The `RLH` token is distinctively Hawaii-only. #359\n //\n // Captures: (1) edition year, (2) section body.\n id: \"rlh\",\n regex:\n /\\bRLH\\s+(\\d{4})\\s+§\\s+(\\d+(?:[A-Za-z0-9:-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)/g,\n description: 'Revised Laws of Hawaii (pre-1955): \"RLH 1935 § 2545\" — #359',\n type: \"statute\",\n },\n {\n // Pre-1973 Colorado Revised Statutes (prose form): `Section 148-21-34,\n // Colorado Revised Statutes 1963` / `Section 13-25-126, Colo. Rev. Stat.\n // 1973`. Pre-1973 Colorado used a chapter-article-section numbering\n // scheme that surfaces here as the section body (`148-21-34`). The\n // section comes BEFORE the code name — opposite of the canonical\n // `<code> § <section>` shape — so this needs its own pattern.\n //\n // Listed BEFORE `abbreviated-code` so the prose-form container wins span\n // dedup over the abbreviated-code match (which would otherwise consume\n // the trailing `Colorado Revised Statutes 1963` and treat `1963` as the\n // section, producing a duplicate citation). #352\n //\n // Captures: (1) section body, (2) optional edition year (1963/1973).\n id: \"colorado-prose\",\n regex:\n /\\b[Ss]ection\\s+(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*),?\\s+Colo(?:rado)?\\.?\\s+Rev(?:ised)?\\.?\\s+Stat(?:utes)?\\.?(?:\\s+Ann(?:otated)?\\.?)?(?:\\s+(19\\d{2}))?/g,\n description:\n 'Pre-1973 Colorado prose form: \"Section 148-21-34, Colorado Revised Statutes 1963\" — #352',\n type: \"statute\",\n },\n {\n // Florida postfix form: `section 812.035(7), Florida Statutes` or\n // `§83.15, Florida Statutes`. The code name appears AFTER the section\n // — canonical Florida court style since at least the 1970s. Listed\n // BEFORE `abbreviated-code` so the container-shape wins span dedup\n // (otherwise the trailing `Florida Statutes` could tokenize as a\n // separate abbreviated-code match). #356\n //\n // Captures: (1) section body.\n id: \"florida-postfix\",\n // Use a lookbehind boundary (`(?<![A-Za-z])`) instead of `\\b` so the\n // pattern can start at `§` — `\\b` requires a word/non-word transition\n // and `§` is a non-word char, so `\\b` fails when `§` is the first char.\n // No trailing `\\b` because `Fla. Stat.` ends with `.` (non-word) and\n // `\\b` wouldn't anchor at end-of-string. The closed alternation\n // (`Florida Statutes | Fla. Stat.`) is specific enough on its own.\n regex:\n /(?<![A-Za-z])(?:[Ss]ections?|§§?)\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*),?\\s+(?:Florida\\s+Statutes|Fla\\.\\s*Stat\\.)/g,\n description:\n 'Florida postfix statute form: \"section 812.035(7), Florida Statutes\" / \"§83.15, Florida Statutes\" — #356',\n type: \"statute\",\n },\n {\n // Florida spelled-out-prefix form: `Florida Statute 679.504(3)` or\n // `Florida Statutes §73.071(3)(b)`. Spelled-out (singular or plural)\n // code name with optional `§` connector — distinct from the canonical\n // Bluebook `Fla. Stat. §` prefix handled by `abbreviated-code`.\n //\n // Captures: (1) section body.\n id: \"florida-prefix-spelled\",\n regex:\n /\\bFlorida\\s+Statutes?\\s*§?\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*)/g,\n description:\n 'Florida spelled-out prefix form: \"Florida Statute 679.504(3)\" / \"Florida Statutes §73.071\" — #356',\n type: \"statute\",\n },\n {\n // Idaho postfix form: `Section 23-908(4), Idaho Code` — the code name\n // appears AFTER the section. Sibling to florida-postfix. Listed BEFORE\n // `abbreviated-code` so the container-shape wins span dedup (otherwise\n // the trailing `Idaho Code` could tokenize as a separate abbreviated-code\n // match). #360\n //\n // Captures: (1) section body.\n id: \"idaho-postfix\",\n regex:\n /(?<![A-Za-z])(?:[Ss]ections?|§§?)\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*),?\\s+Idaho\\s+Code(?:\\s+Ann\\.?)?/g,\n description:\n 'Idaho postfix statute form: \"Section 23-908(4), Idaho Code\" — #360',\n type: \"statute\",\n },\n {\n // Montana postfix form: `§ 77-6-205(2), MCA` or `Section 40-4-121(7)(a),\n // MCA` — the dominant Montana citation style (every modern Montana opinion\n // uses this form). Sibling to florida-postfix and idaho-postfix. The\n // trailing edition-year parenthetical (`MCA (1983)`) is left to the\n // generic year-paren absorber in extractCitations.ts to attach. #372\n //\n // Captures: (1) section body.\n id: \"mca-postfix\",\n regex:\n /(?<![A-Za-z])(?:[Ss]ections?|§§?)\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*),?\\s+MCA/g,\n description: 'Montana Code Annotated postfix form: \"§ 77-6-205(2), MCA\" — #372',\n type: \"statute\",\n },\n {\n // Tennessee Code Annotated postfix form: `§ 39-904, T.C.A.` — the code\n // name appears AFTER the section, separated by a comma. Sibling to\n // florida-postfix, idaho-postfix, mca-postfix. Listed BEFORE\n // `abbreviated-code` so the container shape wins span dedup. #398\n //\n // Captures: (1) section body.\n id: \"tca-postfix\",\n regex:\n /(?<![A-Za-z])(?:[Ss]ections?|[Ss]ec\\.?|§§?)\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*),?\\s+T\\.?C\\.?A\\.?/g,\n description:\n 'Tennessee Code Annotated postfix form: \"§ 39-904, T.C.A.\" — #398',\n type: \"statute\",\n },\n {\n // Washington chapter-postfix form: `chapter 49.60 RCW`, `Chapter 41.26\n // RCW`. Canonical Washington court style places the chapter number\n // before RCW (the opposite of the prefix `RCW chapter` form used in\n // other states). The chapter is in `NN.NN` format — distinctively\n // Washington. Listed BEFORE `abbreviated-code` so the container shape\n // wins span dedup over a potential `RCW` standalone match. #408\n //\n // Captures: (1) chapter body in NN.NN form.\n id: \"rcw-chapter-postfix\",\n regex: /\\b[Cc]hapter\\s+(\\d+\\.\\d+)\\s+RCW/g,\n description:\n 'Washington RCW chapter postfix form: \"chapter 49.60 RCW\" — #408',\n type: \"statute\",\n },\n {\n // Wisconsin Statutes postfix form: `§ 76.09, Stats.`, `sec. 805.13(3),\n // Stats.`, `§ 48.415(l)(a)3, STATS.`. Wisconsin court style places the\n // `Stats.` abbreviation AFTER the section, separated by a comma. Both\n // lowercase `Stats.` and uppercase `STATS.` are common. The trailing\n // alphanumeric character (`3` in `48.415(l)(a)3`) is the Wisconsin\n // sub-subsection marker. Listed BEFORE `abbreviated-code` so the\n // container shape wins span dedup. #414\n //\n // Captures: (1) section body.\n id: \"wi-stats-postfix\",\n regex:\n /(?<![A-Za-z])(?:§§?|[Ss]ections?|[Ss]ec\\.?)\\s*(\\d+\\.\\d+(?:[A-Za-z0-9])?(?:\\s*\\([^)]*\\))*[A-Za-z0-9]*),?\\s+(?:Stats\\.|STATS\\.)/g,\n description:\n 'Wisconsin Statutes postfix form: \"§ 76.09, Stats.\" / \"sec. 805.13(3), Stats.\" — #414',\n type: \"statute\",\n },\n {\n // Nebraska Reissue Revised Statutes 1943 (R.R.S. 1943) — historical\n // form: `section 38-901, R. R. S. 1943` or `§ 30-2806, R.R.S. 1943,\n // Reissue 1975`. Nebraska compiled its statutes in 1943 and re-issues\n // volumes on a rolling basis, so the trailing `Reissue YYYY` clause\n // gives the volume year. The `R.R.S.` token admits inter-letter\n // spacing (`R. R. S.`) — common OCR variant. Listed BEFORE\n // `abbreviated-code` so the container shape wins. #373\n //\n // Captures: (1) section body, (2) optional Reissue year.\n id: \"rrs-1943\",\n regex:\n /(?<![A-Za-z])(?:[Ss]ections?|§§?)\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*),?\\s+R\\.?\\s*R\\.?\\s*S\\.?\\s+1943(?:,\\s+Reissue\\s+(\\d{4}))?/g,\n description:\n 'Nebraska Reissue Revised Statutes 1943: \"§ 30-2806, R.R.S. 1943, Reissue 1975\" — #373',\n type: \"statute\",\n },\n {\n // Rhode Island General Laws 1956 — modern RI statutory code. The\n // `G.L. 1956` (or spaced `G. L. 1956`) prefix is distinctive because\n // of the `1956` literal year, which disambiguates from Massachusetts\n // `G.L. c. NNN` (chapter form). RI opinions often include a\n // `(YYYY Reenactment)` parenthetical indicating which reenactment\n // volume was in effect. #393\n //\n // Captures: (1) optional reenactment year, (2) section body.\n id: \"rigl-1956\",\n regex:\n /\\bG\\.?\\s*L\\.?\\s+1956\\s*(?:\\((\\d{4})\\s+Reenactment\\))?\\s*,?\\s*§§?\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*)/g,\n description:\n 'Rhode Island General Laws 1956: \"G.L. 1956 (1969 Reenactment) §11-23-1\" — #393',\n type: \"statute\",\n },\n {\n // Maryland article-letter codes — post-2002 the Maryland Code is\n // organized into named articles, each with a 2- or 3-letter prefix\n // used as the bare citation form (no `Md.` prefix): `HG § 19-906`,\n // `CP § 10-105(e)(4)`, `R.P. § 8-211`. This is the dominant Maryland\n // citation style for any Maryland appellate opinion since 2002. The\n // letter prefixes are a closed enumeration; matching is restricted to\n // that set to keep the false-positive risk bounded. #368\n //\n // The mandatory `§` connector disambiguates the letter prefix from\n // ordinary prose tokens like `IN` or `TR` that happen to appear at\n // the start of a sentence.\n //\n // Captures: (1) code-letter prefix, (2) section body.\n id: \"md-article-letter\",\n regex:\n /\\b(AB|AG|BO|BR|CJ|CL|CP|CR|CS|EC|ED|EL|EN|ET|FI|FL|GP|HG|HO|HS|HU|IN|LE|LG|LU|NR|PS|PUC|R\\.?P\\.?|RP|SF|SG|TA|TG|TP|TR)\\s*§§?\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*(?:\\s*et\\s+seq\\.?)?)/g,\n description:\n 'Maryland article-letter codes: \"HG § 19-906\", \"CP § 10-105\", \"R.P. § 8-211\" — #368',\n type: \"statute\",\n },\n {\n // Minnesota Statutes year-edition form: `Minn. St. 1971, § 176.66`.\n // The year (1971/1974/etc.) is the edition of Minnesota Statutes, not\n // the section number — abbreviated-code would mis-capture the year as\n // section. Listed BEFORE `abbreviated-code` so the year-edition shape\n // wins. The trailing `, § N` is REQUIRED so we don't false-positive on\n // bare years that happen to follow `Minn. St.`. #371\n //\n // Captures: (1) edition year, (2) section body.\n id: \"minn-st-year-edition\",\n regex:\n /\\bMinn\\.?\\s+(?:Stat|St)\\.?\\s+(19\\d{2}),\\s*§\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\)|\\[[^\\]]*\\])*)/g,\n description:\n 'Minnesota Statutes year-edition form: \"Minn. St. 1971, § 176.66\" — #371',\n type: \"statute\",\n },\n {\n // Kansas Statutes Annotated year-edition / Supp. form: `K.S.A. 2009\n // Supp. 44-501(d)(2)`. The year between K.S.A. and the section number\n // is the compilation/supplement year, not the section — abbreviated-code\n // would mis-capture the year. The Supp. token is optional (some Kansas\n // courts write `K.S.A. YYYY NN-NNN` without `Supp.` to mean the bound\n // volume of that year). Listed BEFORE `abbreviated-code` so this shape\n // wins. The internal comma (`23-9,101`) is the Kansas comma-section\n // form, also supported. #367\n //\n // Captures: (1) edition year, (2) optional Supp. marker, (3) section.\n id: \"ksa-year-edition\",\n regex:\n /\\bK\\.?\\s*S\\.?\\s*A\\.?\\s+(\\d{4})(?:\\s+(Supp\\.?))?\\s+(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9])|,(?=\\d))*(?:\\([^)]*\\))*)/g,\n description:\n 'Kansas Statutes Annotated year-edition: \"K.S.A. 2009 Supp. 44-501(d)(2)\" — #367',\n type: \"statute\",\n },\n {\n // Indiana Code year-edition form: `IC 1971, 35-13-4-4` — the year\n // between IC and the section is the compilation/edition year of the\n // Indiana Code, not the section. abbreviated-code would silently\n // capture the year as section. The trailing `, NN-N-N` separator\n // distinguishes this from a bare `IC NN-N-N` modern citation. Listed\n // BEFORE `abbreviated-code` so this shape wins. #363\n //\n // Captures: (1) edition year, (2) section body.\n id: \"ic-year-edition\",\n regex:\n /\\bIC\\s+(\\d{4}),\\s*(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*)/g,\n description: 'Indiana Code year-edition form: \"IC 1971, 35-13-4-4\" — #363',\n type: \"statute\",\n },\n {\n id: \"abbreviated-code\",\n regex: buildAbbreviatedCodeRegex(),\n description: \"Abbreviated state code citations for all US jurisdictions\",\n type: \"statute\",\n },\n {\n // Georgia pre-1983 Code — `Code § 27-2501`, `Code Ann. § 26-2101`,\n // `Code § 110-501`. Georgia replaced its old \"Code\" / \"Code of Georgia\n // Annotated\" with OCGA in 1983. Modern Georgia opinions still cite the\n // pre-1983 code for statutory history. The TWO-part hyphenated section\n // format (`\\d+-\\d+` with negative lookahead `(?![\\d-])` so 3-part\n // OCGA-style sections don't partial-match) is the disambiguator —\n // bare `Code Ann.` is always Georgia (other states use prefixed\n // `Md. Code Ann.`, `Ind. Code Ann.`, etc., which the `named-code`\n // and `abbreviated-code` patterns handle). Listed AFTER\n // `abbreviated-code` so prefixed forms win span dedup. #358\n //\n // Captures: (1) \"Code Ann.\" or \"Code\", (2) section body.\n // West Virginia historical Code 1931 form: `Code 1931, 49-6-3, as\n // amended` / `Code, 1931, 49-6-3` / `Code, 14-2-13` (no year). West\n // Virginia compiled its statutes in 1931 and modern WV opinions still\n // cite the 1931 code for statutory history. The 3-part hyphenated\n // section format (`N-N-N`) plus the `Code 1931` or comma-separated\n // `Code, ` prefix disambiguates from Georgia pre-1983 and Virginia\n // bare-Code. Listed BEFORE `ga-pre-1983` so the longer 3-part WV\n // sections win span dedup. #406\n //\n // Captures: (1) optional 1931 year, (2) section body.\n id: \"wv-code-1931\",\n regex:\n /\\bCode,?(?:\\s+(1931))?,\\s+(\\d+-\\d+[A-Z]?-\\d+(?:[A-Za-z0-9])?(?:\\([A-Za-z0-9]+\\))*)/g,\n description:\n 'West Virginia historical Code 1931: \"Code 1931, 49-6-3, as amended\" / \"Code, 14-2-13\" — #406',\n type: \"statute\",\n },\n {\n id: \"ga-pre-1983\",\n regex:\n /\\b(Code(?:\\s+Ann\\.?)?)\\s+§\\s*(\\d+-\\d+(?![\\d-])(?!\\.\\d)(?:[A-Za-z0-9])?(?:\\([A-Za-z0-9]+\\))*)/g,\n description:\n 'Georgia pre-1983 Code: \"Code Ann. § 26-2101\" / \"Code § 27-2501\" — #358 (#405 tightened)',\n type: \"statute\",\n },\n {\n // Virginia bare-Code form: `Code § 18.2-308.2`, `Code § 46.2-1571`,\n // `Virginia Code § 8.01-581.17`, `Code § 20-107.3(D)`. Virginia's\n // canonical court style omits the `Va.` prefix. The disambiguator from\n // Georgia pre-1983 (also bare `Code §`) is the PERIOD in the title or\n // section — Virginia sections always include at least one period\n // (`18.2-308.2`, `20-107.3`), while Georgia pre-1983 sections never\n // do (`26-2101`, `27-2501`). #405\n //\n // Listed AFTER `abbreviated-code` and `ga-pre-1983` so the more\n // specific patterns win span dedup when their conditions are met.\n //\n // Captures: (1) \"Virginia Code\" or \"Code\", (2) section body.\n id: \"va-bare-code\",\n regex:\n /\\b(Virginia\\s+Code|Code)\\s+§\\s*((?:\\d+\\.\\d+-\\d+(?:\\.\\d+)?|\\d+-\\d+\\.\\d+)(?:\\([A-Za-z0-9]+\\))*)/g,\n description:\n 'Virginia bare Code form: \"Code § 18.2-308.2\" / \"Virginia Code § 8.01-581.17\" — #405',\n type: \"statute\",\n },\n {\n // New Mexico bare-section form: `Section 32A-2-7(A)`, `§ 41-2-2`. NM\n // opinions cite NMSA 1978 sections without a code prefix — the three-\n // hyphen section format (`\\d[A-Z]?-\\d[A-Z]?-\\d[A-Z]?`) is distinctive\n // among state codes and serves as the disambiguator. Listed AFTER\n // `abbreviated-code` so a full `NMSA 1978, § 41-2-2` citation is not\n // double-counted by this bare-section pattern (the abbreviated-code\n // container would otherwise tie with this contained pattern, leaving\n // a duplicate cite at the inner span). #382\n //\n // Captures: (1) section body — three-part hyphenated form with\n // optional uppercase-letter suffixes and optional parenthetical\n // subsection (`(A)`, `(B)`, `(1)`).\n id: \"nm-bare-section\",\n regex:\n /(?<![A-Za-z])(?:§\\s*|[Ss]ection\\s+)(\\d+[A-Z]?-\\d+[A-Z]?-\\d+[A-Z]?(?:\\([A-Za-z0-9]+\\))*)/g,\n description:\n 'New Mexico bare-section form: \"Section 32A-2-7(A)\" / \"§ 41-2-2\" — #382',\n type: \"statute\",\n },\n {\n id: \"ca-bare-code\",\n regex: buildCaBareCodeRegex(),\n description:\n 'California bare-code citations (#296) — `Pen. Code § 148`, `Code Civ. Proc., § 1021.5`, `Bus. & Prof. Code § 17200` (no \"Cal.\" prefix; common in single-jurisdiction California practice).',\n type: \"statute\",\n },\n {\n // Pre-1975 Alabama Code (Code-prefix form): `Code 1940, T. 15, § 389`.\n // The leading `Code [of Alabama,] 1940` is an unambiguous Alabama signal,\n // so the title body uses `T.` / `Tit.` / `Title` interchangeably. The\n // section uses the period-followed-by-alphanumeric guard from #283.\n // Captures: (1) chapter (title), (2) section body. Year is hardcoded to\n // 1940 in the extractor (the prefix asserts 1940). #343\n id: \"ala-code-prefix\",\n regex:\n /\\bCode(?:\\s+of\\s+Alabama)?,?\\s+1940,?\\s+T(?:itle|it)?\\.\\s+(\\d+),?\\s+§\\s+(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*)/g,\n description:\n 'Alabama Code 1940 (Code-prefix form, pre-1975): \"Code 1940, T. 15, § 389\" / \"Code of Alabama 1940, T. NN, § NNN\" — #343',\n type: \"statute\",\n },\n {\n // Pre-1975 Alabama Code (Title-first with mandatory Code trailer):\n // `Title 26, Section 214, Code of Alabama 1940, as Recompiled 1958`.\n // Requires the trailing `Code [of Alabama] YYYY` clause so the spelled-out\n // `Title NN` form doesn't false-positive on bare prose like USC's\n // `Title 18, § 1001`. Captures: (1) title, (2) section, (3) edition year,\n // (4) optional recompilation year.\n id: \"ala-title-trailer\",\n regex:\n /\\bTitle\\s+(\\d+),?\\s+(?:§|Sec(?:tion)?s?\\.?)\\s+(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*),?\\s+Code(?:\\s+of\\s+Alabama)?,?\\s+(\\d{4})(?:,?\\s+(?:as\\s+)?[Rr]ecompiled\\s+(\\d{4}))?/g,\n description:\n 'Alabama Code (Title-first with Code trailer): \"Title 26, Section 214, Code of Alabama 1940, as Recompiled 1958\" — #343',\n type: \"statute\",\n },\n {\n // Pre-1975 Alabama Code (abbreviated bare form): `Tit. 52, § 361`.\n // The `Tit.` abbreviation is itself an Alabama signal — USC and other\n // federal codes spell out `Title` instead. Optional Code trailer captures\n // year and recompilation when present. Captures: (1) title, (2) section,\n // (3) optional edition year, (4) optional recompilation year.\n id: \"ala-tit-bare\",\n regex:\n /\\bTit\\.\\s+(\\d+),?\\s+§\\s+(\\d+(?:[A-Za-z0-9:/-]|\\.(?=[A-Za-z0-9]))*(?:\\([^)]*\\))*)(?:,?\\s+Code(?:\\s+of\\s+Alabama)?,?\\s+(\\d{4})(?:,?\\s+(?:as\\s+)?[Rr]ecompiled\\s+(\\d{4}))?)?/g,\n description:\n 'Alabama Code (abbreviated `Tit.` form): \"Tit. 52, § 361\" — #343',\n type: \"statute\",\n },\n]\n","/**\n * Tokenization Layer for Citation Extraction\n *\n * Applies regex patterns to cleaned text to produce citation candidate tokens.\n * This is the second stage of the parsing pipeline:\n * 1. Clean text (remove HTML, normalize Unicode)\n * 2. Tokenize (apply patterns to find candidates) ← THIS MODULE\n * 3. Extract (parse metadata, validate against reporters-db)\n *\n * Tokenization is intentionally broad - it finds potential citations without\n * validating them. The extraction layer (Plan 5) validates tokens against\n * reporters-db and parses metadata.\n *\n * @module tokenize\n */\n\nimport type { Pattern } from \"@/patterns\"\nimport { casePatterns, journalPatterns, neutralPatterns, statutePatterns } from \"@/patterns\"\nimport { shortFormPatterns } from \"@/patterns/shortForm\"\nimport type { Span } from \"@/types/span\"\n\n/**\n * A token representing a potential citation found in cleaned text.\n *\n * Tokens are produced by applying regex patterns to cleaned text.\n * They include matched text, position in cleaned text, and pattern metadata\n * for use in the extraction layer.\n */\nexport interface Token {\n /** Matched text from input */\n text: string\n\n /** Position in cleaned text (cleanStart/cleanEnd only, no original positions yet) */\n span: Pick<Span, \"cleanStart\" | \"cleanEnd\">\n\n /** Pattern type that matched this token */\n type: Pattern[\"type\"]\n\n /** Pattern ID that matched this token */\n patternId: string\n}\n\n/**\n * Tokenizes cleaned text by applying regex patterns to find citation candidates.\n *\n * For each pattern in the patterns array:\n * 1. Apply pattern.regex.matchAll(cleanedText)\n * 2. Create Token for each match with position, text, and pattern metadata\n * 3. Collect all tokens from all patterns\n * 4. Sort by cleanStart position (ascending)\n *\n * Timeout protection: If a pattern throws (e.g., ReDoS), skip it and continue\n * with remaining patterns. Logs warning to console.\n *\n * Note: This function is synchronous because regex matching is inherently\n * synchronous. This enables both sync (extractCitations) and async\n * (extractCitationsAsync) APIs in Plan 6.\n *\n * @param cleanedText - Text that has been cleaned by cleanText() from Plan 1\n * @param patterns - Regex patterns to apply (defaults to all patterns from Plan 2)\n * @returns Array of tokens sorted by position (cleanStart ascending)\n *\n * @example\n * ```typescript\n * import { tokenize } from '@/tokenize'\n * import { cleanText } from '@/clean'\n *\n * const original = \"See Smith v. Doe, 500 F.2d 123 (9th Cir. 2020)\"\n * const { cleanedText } = cleanText(original)\n * const tokens = tokenize(cleanedText)\n * // tokens[0] = {\n * // text: \"500 F.2d 123\",\n * // span: { cleanStart: 18, cleanEnd: 30 },\n * // type: \"case\",\n * // patternId: \"federal-reporter\"\n * // }\n * ```\n */\nexport function tokenize(\n cleanedText: string,\n patterns: Pattern[] = [\n ...casePatterns,\n ...statutePatterns,\n ...journalPatterns,\n ...neutralPatterns,\n ...shortFormPatterns,\n ],\n): Token[] {\n const tokens: Token[] = []\n\n for (const pattern of patterns) {\n try {\n // Apply pattern to cleaned text\n const matches = cleanedText.matchAll(pattern.regex)\n\n for (const match of matches) {\n // Create token from match\n tokens.push({\n text: match[0],\n span: {\n cleanStart: match.index!,\n cleanEnd: match.index! + match[0].length,\n },\n type: pattern.type,\n patternId: pattern.id,\n })\n }\n } catch (error) {\n // Timeout protection: If pattern throws (ReDoS, etc.), skip it\n console.warn(\n `Pattern ${pattern.id} threw error, skipping:`,\n error instanceof Error ? error.message : String(error),\n )\n }\n }\n\n // Sort tokens by position (cleanStart ascending)\n tokens.sort((a, b) => a.span.cleanStart - b.span.cleanStart)\n\n return tokens\n}\n","/**\n * BK-Tree (Burkhard-Keller Tree)\n *\n * A metric tree that indexes strings by pairwise edit distance, enabling\n * threshold queries that prune dissimilar candidates via triangle inequality.\n * Used internally for supra citation party name matching.\n */\n\ninterface BKTreeNode {\n key: string\n insertionOrder: number\n children: Map<number, BKTreeNode>\n}\n\nexport interface BKQueryResult {\n key: string\n distance: number\n insertionOrder: number\n}\n\n/**\n * A BK-Tree for approximate string matching using a metric distance function.\n *\n * @param distanceFn - A metric distance function (must satisfy triangle inequality).\n * Accepts an optional third parameter `maxDistance` for early termination.\n */\nexport class BKTree {\n private root: BKTreeNode | null = null\n private nextOrder = 0\n private readonly distanceFn: (a: string, b: string, maxDistance?: number) => number\n\n constructor(distanceFn: (a: string, b: string, maxDistance?: number) => number) {\n this.distanceFn = distanceFn\n }\n\n /**\n * Insert a key into the tree. Duplicate keys are ignored (first insertion wins).\n */\n insert(key: string): void {\n const node: BKTreeNode = {\n key,\n insertionOrder: this.nextOrder++,\n children: new Map(),\n }\n\n if (this.root === null) {\n this.root = node\n return\n }\n\n let current = this.root\n while (true) {\n const d = this.distanceFn(key, current.key)\n if (d === 0) return // duplicate key, keep first\n const child = current.children.get(d)\n if (child) {\n current = child\n } else {\n current.children.set(d, node)\n return\n }\n }\n }\n\n /**\n * Find all keys within `maxDistance` of the query key.\n *\n * Uses triangle inequality to prune branches: if d(query, node) = k,\n * only children at distances in [k - maxDistance, k + maxDistance] can\n * possibly contain matches.\n *\n * Results are sorted by distance (ascending), then insertion order (ascending).\n */\n query(queryKey: string, maxDistance: number): BKQueryResult[] {\n if (this.root === null) return []\n\n const results: BKQueryResult[] = []\n const stack: BKTreeNode[] = [this.root]\n\n let node: BKTreeNode | undefined\n while ((node = stack.pop())) {\n // Compute exact distance — early termination is NOT safe here because\n // the BK-Tree needs the true distance for triangle inequality pruning.\n // A truncated distance shifts the child exploration range and causes false negatives.\n const d = this.distanceFn(queryKey, node.key)\n\n if (d <= maxDistance) {\n results.push({ key: node.key, distance: d, insertionOrder: node.insertionOrder })\n }\n\n // Triangle inequality pruning\n const lo = d - maxDistance\n const hi = d + maxDistance\n for (const [childDist, childNode] of node.children) {\n if (childDist >= lo && childDist <= hi) {\n stack.push(childNode)\n }\n }\n }\n\n results.sort((a, b) => a.distance - b.distance || a.insertionOrder - b.insertionOrder)\n return results\n }\n}\n","/**\n * Levenshtein Distance\n *\n * Calculates edit distance between strings for fuzzy party name matching\n * in supra citation resolution.\n *\n * Uses dynamic programming for O(m*n) time complexity.\n */\n\n/**\n * Calculates Levenshtein distance (edit distance) between two strings.\n *\n * Uses a space-optimized rolling two-row DP approach: only the previous and\n * current rows are kept in memory (O(min(m,n)) space instead of O(m*n)).\n *\n * @param a - First string\n * @param b - Second string\n * @param maxDistance - Optional threshold for early termination. When provided,\n * the function returns `maxDistance + 1` as soon as it determines the true\n * distance must exceed the threshold. This avoids unnecessary computation\n * for obviously dissimilar strings.\n * @returns Exact edit distance if ≤ maxDistance, otherwise maxDistance + 1\n */\nexport function levenshteinDistance(a: string, b: string, maxDistance: number = Infinity): number {\n if (a.length === 0) return Math.min(b.length, maxDistance + 1)\n if (b.length === 0) return Math.min(a.length, maxDistance + 1)\n\n // Ensure short is the shorter string so rows are min(m,n) long\n const short = a.length <= b.length ? a : b\n const long = a.length <= b.length ? b : a\n\n const cols = short.length\n let prev = Array.from({ length: cols + 1 }, (_, k) => k) // base-case row\n let curr = new Array<number>(cols + 1)\n\n for (let i = 1; i <= long.length; i++) {\n curr[0] = i\n let rowMin = i // curr[0] is always i\n\n for (let j = 1; j <= cols; j++) {\n if (long[i - 1] === short[j - 1]) {\n curr[j] = prev[j - 1]\n } else {\n curr[j] = 1 + Math.min(prev[j], curr[j - 1], prev[j - 1])\n }\n if (curr[j] < rowMin) rowMin = curr[j]\n }\n\n // Early termination: row minimums are non-decreasing, so if the\n // cheapest cell already exceeds the threshold, no future row can help\n if (rowMin > maxDistance) return maxDistance + 1\n\n const swap = prev\n prev = curr\n curr = swap\n }\n\n return prev[cols]\n}\n\n/**\n * Calculates normalized Levenshtein similarity (0-1 scale).\n *\n * Returns similarity score where:\n * - 1.0 = identical strings\n * - 0.0 = completely different\n *\n * Comparison is case-insensitive.\n *\n * @param a - First string\n * @param b - Second string\n * @returns Similarity score from 0 to 1\n */\nexport function normalizedLevenshteinDistance(a: string, b: string): number {\n // Normalize to lowercase for case-insensitive comparison\n const lowerA = a.toLowerCase()\n const lowerB = b.toLowerCase()\n\n // Calculate raw edit distance\n const distance = levenshteinDistance(lowerA, lowerB)\n\n // Normalize by max length\n const maxLength = Math.max(lowerA.length, lowerB.length)\n if (maxLength === 0) return 1.0 // Both empty strings\n\n // Convert distance to similarity: 1 - (distance / maxLength)\n return 1 - distance / maxLength\n}\n","/**\n * Scope Boundary Detection\n *\n * Detects paragraph/section boundaries in text and validates whether\n * an antecedent citation is within the resolution scope.\n */\n\nimport type { FootnoteMap } from \"../footnotes/types\"\nimport type { Citation } from \"../types/citation\"\nimport type { ScopeStrategy } from \"./types\"\n\n/**\n * Binary search returning the insertion point for `value` in sorted `arr`.\n * Returns the smallest index i such that arr[i] > value (or arr.length if none).\n */\nfunction bisectRight(arr: number[], value: number): number {\n let lo = 0\n let hi = arr.length\n while (lo < hi) {\n const mid = (lo + hi) >>> 1\n if (arr[mid] <= value) lo = mid + 1\n else hi = mid\n }\n return lo\n}\n\n/**\n * Detects paragraph boundaries from text and assigns each citation to a paragraph.\n *\n * @param text - Original document text\n * @param citations - Extracted citations with position spans\n * @param boundaryPattern - Regex pattern to detect boundaries (default: /\\n\\n+/)\n * @returns Map of citation index to paragraph number (0-based)\n */\nexport function detectParagraphBoundaries(\n text: string,\n citations: Citation[],\n boundaryPattern: RegExp = /\\n\\n+/g,\n): Map<number, number> {\n const paragraphMap = new Map<number, number>()\n\n // Find all paragraph boundaries (positions in text)\n const boundaries: number[] = [0] // Start of document is first boundary\n let match: RegExpExecArray | null\n\n while ((match = boundaryPattern.exec(text)) !== null) {\n // Boundary is at end of match (start of next paragraph)\n boundaries.push(match.index + match[0].length)\n }\n\n boundaries.push(text.length) // End of document\n\n // Assign each citation to a paragraph\n for (let i = 0; i < citations.length; i++) {\n const citation = citations[i]\n const citationStart = citation.span.originalStart\n\n paragraphMap.set(i, bisectRight(boundaries, citationStart) - 1)\n }\n\n return paragraphMap\n}\n\n/**\n * Build a scope map from footnote zones.\n * Zone 0 = body text, Zone N = footnote N.\n *\n * The footnoteMap must be in the same coordinate space as the citation spans\n * being looked up. When called from extractCitations, both are in clean-text\n * coordinates (zones mapped through TransformationMap, spans set during extraction).\n */\nexport function buildFootnoteScopes(\n citations: Citation[],\n footnoteMap: FootnoteMap,\n): Map<number, number> {\n const scopeMap = new Map<number, number>()\n\n for (let i = 0; i < citations.length; i++) {\n const pos = citations[i].span.cleanStart\n\n let zoneId = 0\n let lo = 0\n let hi = footnoteMap.length - 1\n\n while (lo <= hi) {\n const mid = (lo + hi) >>> 1\n const zone = footnoteMap[mid]\n\n if (pos < zone.start) {\n hi = mid - 1\n } else if (pos >= zone.end) {\n lo = mid + 1\n } else {\n zoneId = zone.footnoteNumber\n break\n }\n }\n\n scopeMap.set(i, zoneId)\n }\n\n return scopeMap\n}\n\n/**\n * Checks if an antecedent citation is within resolution scope.\n *\n * @param antecedentIndex - Index of the antecedent citation\n * @param currentIndex - Index of current citation being resolved\n * @param paragraphMap - Map of citation index to paragraph/zone number\n * @param strategy - Scope boundary strategy\n * @param allowCrossZone - If true (footnote strategy), allow resolution from footnote to body (zone 0)\n * @returns true if antecedent is within scope, false otherwise\n */\nexport function isWithinBoundary(\n antecedentIndex: number,\n currentIndex: number,\n paragraphMap: Map<number, number>,\n strategy: ScopeStrategy,\n allowCrossZone = false,\n): boolean {\n if (strategy === \"none\") {\n // No boundary restriction - can resolve across entire document\n return true\n }\n\n // Get scope numbers for both citations\n const antecedentScope = paragraphMap.get(antecedentIndex)\n const currentScope = paragraphMap.get(currentIndex)\n\n // If either is undefined, default to allowing resolution\n if (antecedentScope === undefined || currentScope === undefined) {\n return true\n }\n\n if (antecedentScope === currentScope) {\n return true\n }\n\n // Cross-zone: footnote strategy allows supra/shortFormCase to reach body\n if (strategy === \"footnote\" && allowCrossZone && antecedentScope === 0) {\n return true\n }\n\n // For paragraph/section strategies, citations must be in same boundary.\n // (section currently behaves the same as paragraph — future enhancement.)\n return false\n}\n","/**\n * Document-Scoped Citation Resolver\n *\n * Resolves short-form citations (Id./supra/short-form case) to their full antecedent citations\n * by maintaining resolution context and enforcing scope boundaries.\n *\n * Resolution rules:\n * - Id. resolves to the most recently cited authority (within scope)\n * - Supra resolves to full citation with matching party name (within scope)\n * - Short-form case resolves to full case with matching volume/reporter (within scope)\n */\n\nimport type {\n Citation,\n FullCaseCitation,\n IdCitation,\n ShortFormCaseCitation,\n SupraCitation,\n} from \"../types/citation\"\nimport { isFullCitation } from \"../types/guards\"\nimport type { Span } from \"../types/span\"\nimport { BKTree } from \"./bkTree\"\nimport { levenshteinDistance } from \"./levenshtein\"\nimport { buildFootnoteScopes, detectParagraphBoundaries, isWithinBoundary } from \"./scopeBoundary\"\nimport type {\n ResolutionContext,\n ResolutionOptions,\n ResolutionResult,\n ResolvedCitation,\n} from \"./types\"\n\n/**\n * Returns the citation's `fullSpan` if it has one. Only `case` and `docket`\n * citations carry `fullSpan` (set during case-name backward search). Other\n * full citation types (statute, journal, neutral, etc.) don't have a\n * case-name span concept and never participate in parenthetical-child checks.\n */\nfunction getFullSpan(citation: Citation): Span | undefined {\n if (citation.type === \"case\" || citation.type === \"docket\") {\n return citation.fullSpan\n }\n return undefined\n}\n\n/**\n * Document-scoped resolver that processes citations sequentially\n * and resolves short-form citations to their antecedents.\n */\nexport class DocumentResolver {\n private readonly citations: Citation[]\n private readonly text: string\n private readonly options: Required<Omit<ResolutionOptions, \"footnoteMap\">> & {\n footnoteMap: ResolutionOptions[\"footnoteMap\"]\n }\n private readonly context: ResolutionContext\n private readonly partyNameTree: BKTree\n\n /**\n * Creates a new DocumentResolver.\n *\n * @param citations - All citations in document (in order of appearance)\n * @param text - Original document text\n * @param options - Resolution options\n */\n constructor(citations: Citation[], text: string, options: ResolutionOptions = {}) {\n this.citations = citations\n this.text = text\n\n // Apply defaults to options\n this.options = {\n scopeStrategy: options.scopeStrategy ?? \"none\",\n autoDetectParagraphs: options.autoDetectParagraphs ?? true,\n paragraphBoundaryPattern: options.paragraphBoundaryPattern ?? /\\n\\n+/g,\n fuzzyPartyMatching: options.fuzzyPartyMatching ?? true,\n partyMatchThreshold: options.partyMatchThreshold ?? 0.8,\n reportUnresolved: options.reportUnresolved ?? true,\n footnoteMap: options.footnoteMap,\n }\n\n this.partyNameTree = new BKTree(levenshteinDistance)\n\n // Initialize resolution context\n this.context = {\n citationIndex: 0,\n allCitations: citations,\n lastResolvedIndex: undefined,\n fullCitationHistory: new Map(),\n paragraphMap: new Map(),\n }\n\n // Detect paragraph boundaries if enabled\n if (this.options.autoDetectParagraphs) {\n this.context.paragraphMap = detectParagraphBoundaries(\n text,\n citations,\n this.options.paragraphBoundaryPattern,\n )\n }\n\n // Override with footnote scopes when available\n if (this.options.scopeStrategy === \"footnote\" && this.options.footnoteMap) {\n this.context.paragraphMap = buildFootnoteScopes(citations, this.options.footnoteMap)\n }\n }\n\n /**\n * Resolves all citations in the document.\n *\n * @returns Array of citations with resolution metadata\n */\n resolve(): ResolvedCitation[] {\n const resolved: ResolvedCitation[] = []\n\n for (let i = 0; i < this.citations.length; i++) {\n this.context.citationIndex = i\n const citation = this.citations[i]\n\n // Resolve based on citation type\n let resolution: ResolutionResult | undefined\n\n switch (citation.type) {\n case \"id\":\n resolution = this.resolveId(citation)\n break\n case \"supra\":\n resolution = this.resolveSupra(citation)\n break\n case \"shortFormCase\":\n resolution = this.resolveShortFormCase(citation)\n break\n default:\n // Full citation - update context for future resolutions.\n if (isFullCitation(citation)) {\n // Bluebook Rule 4.1: Id. refers to the immediately preceding\n // *cited authority*. A full citation parsed inside another\n // citation's explanatory parenthetical (e.g. \"(citing X)\" or\n // \"(quoting Y)\") is a sub-reference within the parent's\n // citation, not the cited authority of that sentence — so it\n // must not become Id.'s default antecedent. Detect this by\n // checking whether the current cite's span lies within an\n // earlier full cite's fullSpan. We still track it for\n // supra/short-form resolution.\n const isParentheticalChild = resolved.some((prior) => {\n const priorFullSpan = getFullSpan(prior)\n if (!priorFullSpan) return false\n return (\n priorFullSpan.cleanStart <= citation.span.cleanStart &&\n priorFullSpan.cleanEnd >= citation.span.cleanEnd\n )\n })\n if (!isParentheticalChild) {\n this.context.lastResolvedIndex = i\n }\n this.trackFullCitation(citation, i)\n }\n break\n }\n\n // After resolving a short-form citation, update lastResolvedIndex\n // to the full citation it resolved to (transitive resolution).\n // If resolution failed, lastResolvedIndex is NOT updated --\n // a subsequent Id. will also fail (matching Python eyecite behavior).\n if (resolution?.resolvedTo !== undefined) {\n this.context.lastResolvedIndex = resolution.resolvedTo\n }\n\n // Add citation with resolution metadata\n // Type assertion is safe: runtime logic only sets resolution on short-form citations\n resolved.push({\n ...citation,\n resolution,\n } as ResolvedCitation)\n }\n\n return resolved\n }\n\n /**\n * Resolves Id. citation to the most recently cited authority.\n * Uses lastResolvedIndex which tracks the most recent successfully\n * resolved citation (full, short-form, supra, or Id.).\n */\n private resolveId(_citation: IdCitation): ResolutionResult | undefined {\n const currentIndex = this.context.citationIndex\n const antecedentIndex = this.context.lastResolvedIndex\n\n // No preceding citation has been resolved yet\n if (antecedentIndex === undefined) {\n return this.createFailureResult(\"No preceding citation found\")\n }\n\n // Check scope boundary\n if (!this.isWithinScope(antecedentIndex, currentIndex)) {\n return this.createFailureResult(\"Antecedent citation outside scope boundary\")\n }\n\n return {\n resolvedTo: antecedentIndex,\n confidence: 1.0, // Id. resolution is unambiguous when successful\n }\n }\n\n /**\n * Resolves supra citation by matching party name.\n */\n private resolveSupra(citation: SupraCitation): ResolutionResult | undefined {\n if (!citation.partyName) return undefined // Standalone supra — cannot resolve by party name\n const currentIndex = this.context.citationIndex\n const targetPartyName = this.normalizePartyName(citation.partyName)\n\n // Query BK-Tree for candidates within distance threshold, then filter by scope\n const queryLen = targetPartyName.length\n const threshold = this.options.partyMatchThreshold\n // Safe upper bound: guarantees no match with similarity >= threshold is missed\n const maxDistance = queryLen === 0 ? 0 : Math.ceil((queryLen * (1 - threshold)) / threshold)\n const candidates = this.partyNameTree.query(targetPartyName, maxDistance)\n\n // Sort by insertion order to match Map iteration behavior (first-inserted wins on ties)\n candidates.sort((a, b) => a.insertionOrder - b.insertionOrder)\n\n let bestMatch: { index: number; similarity: number } | undefined\n\n for (const candidate of candidates) {\n const citationIndex = this.context.fullCitationHistory.get(candidate.key)\n if (citationIndex === undefined) continue\n\n // Check scope boundary (supra allows cross-zone: footnote -> body)\n if (!this.isWithinScope(citationIndex, currentIndex, true)) continue\n\n // Convert distance to normalized similarity\n const maxLen = Math.max(queryLen, candidate.key.length)\n const similarity = maxLen === 0 ? 1.0 : 1 - candidate.distance / maxLen\n\n // Update best match if this is better (strict > preserves first-wins tie-breaking)\n if (!bestMatch || similarity > bestMatch.similarity) {\n bestMatch = { index: citationIndex, similarity }\n }\n }\n\n // Check if we found a match above threshold\n if (!bestMatch) {\n return this.createFailureResult(\"No full citation found in scope\")\n }\n\n if (bestMatch.similarity < this.options.partyMatchThreshold) {\n return this.createFailureResult(\n `Party name similarity ${bestMatch.similarity.toFixed(2)} below threshold ${this.options.partyMatchThreshold}`,\n )\n }\n\n // Return successful resolution with confidence based on similarity\n const warnings: string[] = []\n if (bestMatch.similarity < 1.0) {\n warnings.push(`Fuzzy match: similarity ${bestMatch.similarity.toFixed(2)}`)\n }\n\n return {\n resolvedTo: bestMatch.index,\n confidence: bestMatch.similarity,\n warnings: warnings.length > 0 ? warnings : undefined,\n }\n }\n\n /**\n * Resolves short-form case citation by matching volume/reporter, with\n * party-name disambiguation when the short-form includes a back-reference\n * name (#278).\n *\n * Algorithm:\n * 1. Collect all backward candidates with matching volume + normalized\n * reporter that are in-scope.\n * 2. If the short-form has `partyNameNormalized`: prefer the candidate\n * whose plaintiff/defendant matches (substring containment in either\n * direction handles abbreviations: `\"Smith\" ⊂ \"Smith\"` or\n * `\"Smith\"` in `\"Smith, Inc.\"`). Tie-break by recency.\n * 3. If no candidate matches the party name (or no party name on the\n * short-form): fall back to recency.\n */\n private resolveShortFormCase(citation: ShortFormCaseCitation): ResolutionResult | undefined {\n const currentIndex = this.context.citationIndex\n const targetReporter = this.normalizeReporter(citation.reporter)\n const targetParty = citation.partyNameNormalized\n\n // Collect all backward candidates (most recent first) that match\n // vol+reporter AND are in-scope.\n const candidates: number[] = []\n for (let i = currentIndex - 1; i >= 0; i--) {\n const candidate = this.citations[i]\n if (candidate.type !== \"case\") continue\n if (candidate.volume !== citation.volume) continue\n if (this.normalizeReporter(candidate.reporter) !== targetReporter) continue\n if (!this.isWithinScope(i, currentIndex, true)) continue\n candidates.push(i)\n }\n\n if (candidates.length === 0) {\n return this.createFailureResult(\"No matching full case citation found\")\n }\n\n // With a party name, prefer the candidate whose plaintiff or defendant\n // normalized name contains (or is contained by) the short-form's party\n // name. Substring containment in either direction tolerates common\n // abbreviation patterns: short-form `Smith` matches full `Smith, Inc.`\n // and vice versa. Recency breaks ties.\n if (targetParty) {\n const namedMatch = candidates.find((idx) => {\n const c = this.citations[idx]\n if (c.type !== \"case\") return false\n const plaintiff = c.plaintiffNormalized\n const defendant = c.defendantNormalized\n const hit = (name: string | undefined) =>\n name !== undefined &&\n (name === targetParty ||\n name.includes(targetParty) ||\n targetParty.includes(name))\n return hit(plaintiff) || hit(defendant)\n })\n if (namedMatch !== undefined) {\n return {\n resolvedTo: namedMatch,\n confidence: 0.98, // Higher than bare vol+reporter — party-name disambiguation tightens.\n }\n }\n }\n\n // No party name (or no name match): pick most recent candidate.\n return {\n resolvedTo: candidates[0],\n confidence: 0.95,\n }\n }\n\n /**\n * Tracks a full citation in the resolution history.\n * Extracts party name for supra resolution.\n * Uses extracted party names (Phase 7) when available, falls back to backward search.\n */\n private trackFullCitation(citation: Citation, index: number): void {\n // Only case citations have party names for supra resolution\n if (citation.type === \"case\") {\n // Phase 7: Use extracted party names when available\n // Defendant name stored first (preferred for Bluebook-style supra matching)\n if (citation.defendantNormalized) {\n this.context.fullCitationHistory.set(citation.defendantNormalized, index)\n this.partyNameTree.insert(citation.defendantNormalized)\n }\n if (citation.plaintiffNormalized) {\n this.context.fullCitationHistory.set(citation.plaintiffNormalized, index)\n this.partyNameTree.insert(citation.plaintiffNormalized)\n }\n\n // Fallback: backward search from text (pre-Phase 7 compatibility)\n if (!citation.plaintiffNormalized && !citation.defendantNormalized) {\n const partyName = this.extractPartyName(citation)\n if (partyName) {\n const normalized = this.normalizePartyName(partyName)\n this.context.fullCitationHistory.set(normalized, index)\n this.partyNameTree.insert(normalized)\n }\n }\n }\n }\n\n /**\n * Extracts party name from full case citation text.\n * Handles \"Party v. Party\" format by looking at text before citation span.\n */\n private extractPartyName(citation: FullCaseCitation): string | undefined {\n // Look at text before citation span to find party names\n // Case citations typically appear as: \"Smith v. Jones, 100 F.2d 10\"\n // But tokenizer only captures \"100 F.2d 10\" - we need to look backwards in text\n\n const citationStart = citation.span.originalStart\n // Look backwards up to 100 characters for party name\n const lookbackStart = Math.max(0, citationStart - 100)\n const beforeText = this.text.substring(lookbackStart, citationStart)\n\n // Match pattern: \"FirstParty v. SecondParty, \" before the citation\n // Capture the first party name (handles single-letter party names like \"A\" or \"B\")\n const vMatch = beforeText.match(\n /([A-Z][a-zA-Z]*(?:\\s+[A-Z][a-zA-Z]*)*)\\s+v\\.?\\s+[A-Z][a-zA-Z]*(?:\\s+[A-Z][a-zA-Z]*)*,\\s*$/,\n )\n if (vMatch) {\n return this.stripSignalWords(vMatch[1].trim())\n }\n\n // Fallback: try to find any capitalized word(s) before comma\n const beforeComma = beforeText.match(/([A-Z][a-zA-Z]*(?:\\s+[A-Z][a-zA-Z]*)*),\\s*$/)\n if (beforeComma) {\n return this.stripSignalWords(beforeComma[1].trim())\n }\n return undefined\n }\n\n /**\n * Strips citation signal words that may precede party names.\n * E.g., \"In Smith\" → \"Smith\", \"See Also Jones\" → \"Jones\"\n * Preserves \"In re\" which is a case name format, not a signal word.\n */\n private stripSignalWords(name: string): string {\n const stripped = name\n .replace(/^(?:In(?!\\s+re\\b)|See(?:\\s+[Aa]lso)?|Compare|But(?:\\s+[Ss]ee)?|Cf\\.?|Also)\\s+/i, \"\")\n .trim()\n // Only return stripped version if something remains\n return stripped.length > 0 ? stripped : name\n }\n\n /**\n * Normalizes party name for matching.\n */\n private normalizePartyName(name: string): string {\n return name\n .toLowerCase()\n .replace(/\\s+/g, \" \") // Normalize whitespace\n .trim()\n }\n\n /**\n * Normalizes reporter abbreviation for matching.\n */\n private normalizeReporter(reporter: string): string {\n return reporter\n .toLowerCase()\n .replace(/\\s+/g, \"\") // Remove spaces (F.2d vs F. 2d)\n .replace(/\\./g, \"\") // Remove periods\n }\n\n /**\n * Checks if antecedent citation is within scope boundary.\n */\n private isWithinScope(\n antecedentIndex: number,\n currentIndex: number,\n allowCrossZone = false,\n ): boolean {\n return isWithinBoundary(\n antecedentIndex,\n currentIndex,\n this.context.paragraphMap,\n this.options.scopeStrategy,\n allowCrossZone,\n )\n }\n\n /**\n * Creates a failure result for unresolved citations.\n */\n private createFailureResult(reason: string): ResolutionResult | undefined {\n if (this.options.reportUnresolved) {\n return {\n resolvedTo: undefined,\n failureReason: reason,\n confidence: 0.0,\n }\n }\n return undefined\n }\n}\n","/**\n * Citation Resolution\n *\n * Resolves short-form citations (Id./supra/short-form case) to their full antecedents.\n *\n * @example\n * ```ts\n * import { resolveCitations } from 'eyecite-ts/resolve'\n * import { extractCitations } from 'eyecite-ts'\n *\n * const text = 'See Smith v. Jones, 500 F.2d 100 (1974). Id. at 105.'\n * const citations = extractCitations(text)\n * const resolved = resolveCitations(citations, text)\n *\n * // resolved[1] is Id. citation with resolution.resolvedTo = 0\n * console.log(resolved[1].resolution?.resolvedTo) // 0 (points to Smith v. Jones)\n * ```\n */\n\nimport type { Citation } from \"../types/citation\"\nimport { DocumentResolver } from \"./DocumentResolver\"\nimport type { ResolutionOptions, ResolvedCitation } from \"./types\"\n\n/**\n * Resolves short-form citations to their full antecedents.\n *\n * Convenience wrapper around DocumentResolver that handles common use cases.\n *\n * @param citations - Extracted citations in order of appearance\n * @param text - Original document text\n * @param options - Resolution options\n * @returns Citations with resolution metadata\n */\nexport function resolveCitations(\n citations: Citation[],\n text: string,\n options?: ResolutionOptions,\n): ResolvedCitation[] {\n const resolver = new DocumentResolver(citations, text, options)\n return resolver.resolve()\n}\n\n// Re-export core types and classes\nexport { DocumentResolver } from \"./DocumentResolver\"\nexport type {\n ResolutionOptions,\n ResolutionResult,\n ResolvedCitation,\n ScopeStrategy,\n} from \"./types\"\n","/**\n * Parallel Citation Detection\n *\n * Detects parallel citation groups (same case in multiple reporters) using\n * comma-separated case citations sharing a closing parenthetical.\n *\n * Detection happens after tokenization and deduplication, before extraction\n * in the main extractCitations pipeline.\n *\n * @module extract/detectParallel\n */\n\nimport type { Token } from \"@/tokenize/tokenizer\"\n\n/**\n * Maximum characters allowed between end of comma and start of next citation.\n * Bluebook standard uses tight spacing: \"500 F.2d 123, 200 F. Supp. 456\"\n */\nconst MAX_PROXIMITY = 5\n\n/**\n * Maximum total gap (chars) between end of one citation and start of next\n * to even consider them as parallel candidates. Beyond this distance, we can\n * skip all other checks (comma, parenthetical, etc.) for performance.\n * Includes comma, spaces, and potential pincite: \", 125, \" = ~10 chars\n */\nconst MAX_GAP_FOR_PARALLEL = 20\n\n/**\n * Detect parallel citation groups from tokenized citations.\n *\n * Returns a map of primary citation index to array of secondary citation indices.\n * Parallel citations are comma-separated case citations sharing a parenthetical.\n *\n * Detection algorithm:\n * 1. Iterate tokens with lookahead (i, i+1, i+2...)\n * 2. Check if token[i] and token[i+1] are both case citations\n * 3. Check if comma separates them (within MAX_PROXIMITY chars)\n * 4. Check if both citations share a closing parenthetical (via cleaned text)\n * 5. If all conditions met, add to parallel group\n * 6. Continue for chain (i+1, i+2, i+3...) until no more matches\n *\n * @param tokens - Tokenized citations (after deduplication)\n * @param cleanedText - Cleaned text to check for commas and parentheticals\n * @returns Map of primary index to array of secondary indices\n *\n * @example\n * ```typescript\n * const tokens = [\n * { text: \"410 U.S. 113\", span: { cleanStart: 0, cleanEnd: 12 }, type: \"case\" },\n * { text: \"93 S. Ct. 705\", span: { cleanStart: 14, cleanEnd: 27 }, type: \"case\" }\n * ]\n * const cleaned = \"410 U.S. 113, 93 S. Ct. 705 (1973)\"\n * const result = detectParallelCitations(tokens, cleaned)\n * // result = Map { 0 => [1] }\n * ```\n */\nexport function detectParallelCitations(tokens: Token[], cleanedText = \"\"): Map<number, number[]> {\n const parallelGroups = new Map<number, number[]>()\n\n // Edge cases: empty array or no text\n if (tokens.length === 0 || cleanedText === \"\") {\n return parallelGroups\n }\n\n // Track which tokens are already in a parallel group (as secondary)\n const usedAsSecondary = new Set<number>()\n\n for (let i = 0; i < tokens.length; i++) {\n const primary = tokens[i]\n\n // Skip if not a case citation\n if (primary.type !== \"case\") {\n continue\n }\n\n // Skip if already used as secondary in another group\n if (usedAsSecondary.has(i)) {\n continue\n }\n\n const secondaryIndices: number[] = []\n\n // Look ahead for potential secondary citations\n // Chain detection: \"A, B, C (year)\" where A is primary, B and C are secondaries\n for (let j = i + 1; j < tokens.length; j++) {\n const secondary = tokens[j]\n\n // Only case citations can be parallel\n if (secondary.type !== \"case\") {\n break // Stop looking once we hit non-case citation\n }\n\n // Check proximity: comma should be right after primary (or previous secondary in chain)\n const prevToken = j === i + 1 ? primary : tokens[j - 1]\n const gapStart = prevToken.span.cleanEnd\n const gapEnd = secondary.span.cleanStart\n\n // Early exit: If gap is too large, no need to check comma/parenthetical\n // This optimization reduces O(n²) to O(n×k) where k is avg tokens within MAX_GAP\n const gapSize = gapEnd - gapStart\n if (gapSize > MAX_GAP_FOR_PARALLEL) {\n break // Too far apart to be parallel, stop looking\n }\n\n // Extract the gap text between citations\n const gapText = cleanedText.substring(gapStart, gapEnd)\n\n // California Style Manual bracket form (#237): the parallel citation\n // is wrapped in brackets — `<primary> (<year>) [<secondary>]`. Check\n // this BEFORE the comma-requirement gate so we don't reject CA parallels.\n const inBracket =\n gapText.includes(\"[\") &&\n cleanedText[secondary.span.cleanEnd] === \"]\"\n if (inBracket) {\n secondaryIndices.push(j)\n usedAsSecondary.add(j)\n // CA brackets always close after a single parallel cite — chain ends here.\n break\n }\n\n // Bluebook requires comma separator for parallel citations\n if (!gapText.includes(\",\")) {\n break // No comma = not parallel, stop looking\n }\n\n // Check proximity: distance from comma to next citation start\n // MAX_PROXIMITY enforces tight spacing: \"A, B\" not \"A, B\"\n const commaIndex = gapText.indexOf(\",\")\n const distanceAfterComma = gapText.length - commaIndex - 1\n\n if (distanceAfterComma > MAX_PROXIMITY) {\n break // Too far apart, stop looking\n }\n\n // Check for shared parenthetical\n // Both citations must share the SAME closing parenthetical\n // Reject: \"A (1970), B (1971)\" - separate parens = different cases\n // Accept: \"A, B (1970)\" - shared paren = parallel citations\n const textBetween = cleanedText.substring(primary.span.cleanEnd, secondary.span.cleanEnd)\n if (textBetween.includes(\")\")) {\n break // Separate parentheticals = not parallel, stop looking\n }\n\n // Check that there IS a parenthetical after the secondary citation\n if (!hasSharedParenthetical(cleanedText, secondary.span.cleanEnd)) {\n break // No shared parenthetical, stop looking\n }\n\n // All conditions met - this is a parallel citation\n secondaryIndices.push(j)\n usedAsSecondary.add(j)\n }\n\n // If we found any secondary citations, record the group\n if (secondaryIndices.length > 0) {\n parallelGroups.set(i, secondaryIndices)\n }\n }\n\n return parallelGroups\n}\n\n/**\n * Check if there's a closing parenthetical after the given position.\n *\n * This is a simple heuristic: look for \"(...)\" pattern within reasonable distance.\n * Full parenthetical parsing happens in extractCase, this just validates presence.\n *\n * @param cleanedText - Cleaned text\n * @param position - Position to start searching from\n * @returns true if closing parenthetical found\n */\nfunction hasSharedParenthetical(cleanedText: string, position: number): boolean {\n // Look ahead up to 200 characters for opening parenthesis\n const searchText = cleanedText.substring(position, position + 200)\n\n // Find opening parenthesis\n const openIndex = searchText.indexOf(\"(\")\n if (openIndex === -1) {\n return false\n }\n\n // Find matching closing parenthesis (simple depth tracking)\n let depth = 0\n for (let i = openIndex; i < searchText.length; i++) {\n if (searchText[i] === \"(\") {\n depth++\n } else if (searchText[i] === \")\") {\n depth--\n if (depth === 0) {\n // Found matching closing parenthesis\n return true\n }\n }\n }\n\n return false\n}\n","/**\n * String Citation Detection\n *\n * Detects semicolon-separated string citation groups (multiple authorities\n * supporting the same proposition). Runs as a post-extract phase after\n * individual citation extraction and subsequent history linking.\n *\n * @module extract/detectStringCites\n */\n\nimport type { Citation, CitationSignal, FullCaseCitation } from \"@/types/citation\"\n\n/**\n * Signal words recognized between string citation members (case-insensitive).\n * Longer patterns first so \"see also\" matches before \"see\".\n *\n * Each entry also carries a pre-built `endRegex` for matching the signal at the\n * end of preceding text (used in leading-signal detection). These are built once\n * at module load to avoid reconstructing RegExp objects inside hot loops.\n */\nconst SIGNAL_PATTERNS: ReadonlyArray<{\n regex: RegExp\n endRegex: RegExp\n signal: CitationSignal\n}> = buildSignalPatterns()\n\nfunction buildSignalPatterns() {\n // Combined `, e.g.` forms (Bluebook Rule 1.3) come BEFORE their bare-signal\n // counterparts so the alternation prefers the more specific match. The\n // trailing `,?` after `e\\.g\\.` allows the optional comma that typically\n // separates the signal from the citation (e.g., \"See, e.g., Smith v. Jones\").\n const raw: ReadonlyArray<{ regex: RegExp; signal: CitationSignal }> = [\n { regex: /^but\\s+cf\\.,\\s+e\\.g\\.,?(?=\\s|$)/i, signal: \"but cf., e.g.\" },\n { regex: /^see\\s+also,\\s+e\\.g\\.,?(?=\\s|$)/i, signal: \"see also, e.g.\" },\n { regex: /^but\\s+see,\\s+e\\.g\\.,?(?=\\s|$)/i, signal: \"but see, e.g.\" },\n { regex: /^cf\\.,\\s+e\\.g\\.,?(?=\\s|$)/i, signal: \"cf., e.g.\" },\n { regex: /^see,\\s+e\\.g\\.,?(?=\\s|$)/i, signal: \"see, e.g.\" },\n { regex: /^see\\s+generally\\b/i, signal: \"see generally\" },\n { regex: /^see\\s+also\\b/i, signal: \"see also\" },\n { regex: /^but\\s+see\\b/i, signal: \"but see\" },\n { regex: /^but\\s+cf\\.?(?=\\s|$)/i, signal: \"but cf\" },\n { regex: /^compare\\b/i, signal: \"compare\" },\n { regex: /^accord\\b/i, signal: \"accord\" },\n { regex: /^contra\\b/i, signal: \"contra\" },\n { regex: /^see\\b/i, signal: \"see\" },\n { regex: /^cf\\.?(?=\\s|$)/i, signal: \"cf\" },\n { regex: /^e\\.g\\.,?(?=\\s|$)/i, signal: \"e.g.\" },\n ]\n return raw.map(({ regex, signal }) => ({\n regex,\n // Matches the signal at the end of a string (for leading-signal lookback).\n // Replaces the leading `^` anchor with a negative lookbehind for word chars.\n endRegex: new RegExp(`${regex.source.replace(/^\\^/, \"(?<![a-z])\")}\\\\s*$`, regex.flags),\n signal,\n }))\n}\n\n/**\n * Get the end position of a citation's full extent in cleaned text.\n * Uses fullSpan if available on any citation type (currently only case\n * citations carry fullSpan, but this is future-proof for other types).\n */\nfunction getCitationEnd(c: Citation): number {\n const fullSpan = \"fullSpan\" in c ? (c as FullCaseCitation).fullSpan : undefined\n return fullSpan ? fullSpan.cleanEnd : c.span.cleanEnd\n}\n\n/**\n * Get the start position of a citation's full extent in cleaned text.\n * Uses fullSpan if available on any citation type.\n */\nfunction getCitationStart(c: Citation): number {\n const fullSpan = \"fullSpan\" in c ? (c as FullCaseCitation).fullSpan : undefined\n return fullSpan ? fullSpan.cleanStart : c.span.cleanStart\n}\n\n/** Set a signal on a citation without triggering type errors on the union. */\nfunction setSignal(c: Citation, sig: CitationSignal): void {\n ;(c as { signal?: CitationSignal }).signal = sig\n}\n\n/**\n * Parse a recognized signal word from text.\n * Returns the normalized signal and the length of the match, or undefined.\n */\nfunction parseSignal(text: string): { signal: CitationSignal; length: number } | undefined {\n const trimmed = text.trimStart()\n for (const { regex, signal } of SIGNAL_PATTERNS) {\n const match = regex.exec(trimmed)\n if (match) {\n return { signal, length: match[0].length }\n }\n }\n return undefined\n}\n\n/**\n * Check if the gap text between two citations is a valid string cite separator.\n *\n * Valid gaps contain only: whitespace, a single semicolon, and optionally a\n * recognized signal word. Returns the parsed signal if present.\n *\n * @returns Object with `valid` flag and optional `signal` if a mid-group signal was found\n */\nfunction analyzeGap(gapText: string): { valid: boolean; signal?: CitationSignal } {\n // Must contain a semicolon\n const semiIndex = gapText.indexOf(\";\")\n if (semiIndex === -1) return { valid: false }\n\n // Text before semicolon must be only whitespace\n const before = gapText.substring(0, semiIndex).trim()\n if (before !== \"\") return { valid: false }\n\n // Text after semicolon: optional whitespace + optional signal word + optional whitespace\n const after = gapText.substring(semiIndex + 1).trim()\n\n // Empty after semicolon (just whitespace) — valid, no signal\n if (after === \"\") return { valid: true }\n\n // Try to parse a signal word\n const signalResult = parseSignal(after)\n if (signalResult) {\n // Everything after the signal must be whitespace\n const remainder = after.substring(signalResult.length).trim()\n if (remainder === \"\") return { valid: true, signal: signalResult.signal }\n }\n\n // Non-signal text after semicolon — not a valid string cite gap\n return { valid: false }\n}\n\n/**\n * Detect string citation groups from extracted citations.\n *\n * Walks adjacent citations in document order, examines the gap text between\n * them, and groups citations separated by semicolons (with optional signal\n * words). Mutates citations in place to set grouping fields.\n *\n * Must run AFTER subsequent history linking (needs `subsequentHistoryOf` to\n * exclude history citations) and AFTER parallel detection.\n *\n * @param citations - Extracted citations sorted by span.cleanStart (document order)\n * @param cleanedText - Cleaned text used for gap analysis\n */\nexport function detectStringCitations(citations: Citation[], cleanedText: string): void {\n if (citations.length < 2) return\n\n // Build groups as arrays of citation indices\n const groups: number[][] = []\n let currentGroup: number[] = []\n\n for (let i = 0; i < citations.length - 1; i++) {\n const current = citations[i]\n const next = citations[i + 1]\n\n // Skip if next citation is a subsequent history entry\n if (next.type === \"case\" && (next as FullCaseCitation).subsequentHistoryOf) {\n // Finalize current group if any\n if (currentGroup.length >= 2) {\n groups.push(currentGroup)\n }\n currentGroup = []\n continue\n }\n\n // Skip if current citation is a subsequent history entry\n if (current.type === \"case\" && (current as FullCaseCitation).subsequentHistoryOf) {\n continue\n }\n\n // Extract gap text between end of current's full extent and start of next's full extent\n const gapStart = getCitationEnd(current)\n const gapEnd = getCitationStart(next)\n\n // Guard against overlapping or adjacent spans with no gap\n if (gapEnd <= gapStart) {\n if (currentGroup.length >= 2) {\n groups.push(currentGroup)\n }\n currentGroup = []\n continue\n }\n\n const gapText = cleanedText.substring(gapStart, gapEnd)\n const analysis = analyzeGap(gapText)\n\n if (analysis.valid) {\n // Start a new group with the current citation, or continue the existing one\n if (currentGroup.length === 0) {\n currentGroup.push(i)\n }\n // Always add the next citation to the group\n currentGroup.push(i + 1)\n // Set mid-group signal on next citation if found and not already set\n if (analysis.signal && !next.signal) {\n setSignal(next, analysis.signal)\n }\n } else {\n // Group breaks — finalize current group if any\n if (currentGroup.length >= 2) {\n groups.push(currentGroup)\n }\n currentGroup = []\n }\n }\n\n // Finalize any remaining group\n if (currentGroup.length >= 2) {\n groups.push(currentGroup)\n }\n\n // Assign group metadata\n for (let g = 0; g < groups.length; g++) {\n const group = groups[g]\n if (group.length < 2) continue\n\n // Group IDs are sequential per extractCitations() call. If citations from\n // multiple documents are merged downstream, IDs may collide — callers\n // should namespace or regenerate IDs in that scenario.\n const groupId = `sc-${g}`\n for (let idx = 0; idx < group.length; idx++) {\n const citIndex = group[idx]\n const cit = citations[citIndex]\n cit.stringCitationGroupId = groupId\n cit.stringCitationIndex = idx\n cit.stringCitationGroupSize = group.length\n }\n }\n\n // Detect leading signal for first member of each group.\n // (The broader detectLeadingSignals pass below also covers these, but\n // running here first ensures group-first citations get their signal before\n // the general pass skips them as \"already set\".)\n for (const group of groups) {\n if (group.length < 2) continue\n const first = citations[group[0]]\n if (first.signal) continue // Already set (e.g., by extractCase)\n\n // Look backward from citation start for a signal word\n const searchStart = Math.max(0, getCitationStart(first) - 60)\n const precedingText = cleanedText.substring(searchStart, getCitationStart(first)).trim()\n\n // Check if preceding text ends with a signal word (uses pre-built endRegex)\n for (const { endRegex, signal } of SIGNAL_PATTERNS) {\n if (endRegex.test(precedingText)) {\n setSignal(first, signal)\n break\n }\n }\n }\n}\n\n/**\n * Detect leading introductory signals for ALL citations that don't already\n * have one. Scans backward from each citation's start position to find\n * Bluebook signal words (See, But see, Cf., Accord, See also, etc.).\n *\n * Should run AFTER detectStringCitations (which sets signals for string cite\n * group members) so we skip citations that already have signals.\n *\n * @param citations - Extracted citations sorted by span.cleanStart\n * @param cleanedText - Cleaned text used for lookback\n */\nexport function detectLeadingSignals(citations: Citation[], cleanedText: string): void {\n for (let i = 0; i < citations.length; i++) {\n const c = citations[i]\n // Skip if already has a signal from STRING CITE detection (those are scoped\n // and reliable). Do NOT skip signals set by case name extraction — those\n // come from greedy backward search and may be wrong (e.g., cite3 picks up\n // \"See\" from cite1's case name because fullSpan extends too far back).\n if (c.signal && c.stringCitationGroupId) continue\n\n // Determine the search window: from the end of the previous citation (or\n // start of text) to this citation's span.cleanStart. This scopes the\n // search to just the gap between citations.\n const prevEnd = i > 0 ? getCitationEnd(citations[i - 1]) : 0\n const citStart = c.span.cleanStart\n if (citStart <= prevEnd) continue\n\n const gapText = cleanedText.substring(prevEnd, citStart)\n\n // Find the LAST signal word in the gap text (closest to the citation).\n // Use the endRegex patterns which match at the END of a string.\n // We trim the gap text to remove trailing whitespace/punctuation after\n // the signal word (e.g., \"See Smith v. Jones, \" → check if \"See\" is\n // near the start, but we need to find it even with case name after it).\n //\n // Strategy: progressively trim from the end and check for endRegex match.\n // More efficient: use the start-anchored regex on each \"sentence\" in the gap.\n // Simplest correct approach: search for signal words as standalone tokens.\n\n // Find ALL signal matches in the gap, then pick the best one:\n // closest to the citation (highest end position), with ties broken\n // by longest match (so \"but see\" beats \"see\" when they overlap).\n const matches: Array<{ signal: CitationSignal; start: number; end: number }> = []\n\n for (const { signal } of SIGNAL_PATTERNS) {\n const escaped = signal.replace(/\\./g, \"\\\\.\").replace(/\\s+/g, \"\\\\s+\")\n const pattern = new RegExp(`(?<![a-zA-Z])(${escaped})(?![a-zA-Z])`, \"gi\")\n let match: RegExpExecArray | null\n while ((match = pattern.exec(gapText)) !== null) {\n matches.push({ signal, start: match.index, end: match.index + match[0].length })\n }\n }\n\n if (matches.length === 0) continue\n\n // Sort by: (1) end position descending (closest to citation), then\n // (2) length descending (prefer longer/more-specific signals).\n matches.sort((a, b) => {\n const endDiff = b.end - a.end\n if (endDiff !== 0) return endDiff\n return (b.end - b.start) - (a.end - a.start)\n })\n\n // The best match is the one closest to the citation. But if a shorter\n // signal (e.g., \"see\") is a substring of a longer signal (\"but see\")\n // that starts just before it, prefer the longer one.\n let best = matches[0]\n for (const m of matches) {\n // If this match fully contains the current best (or starts within\n // a few chars), it's the more specific signal.\n if (m.start <= best.start && m.end >= best.end) {\n best = m\n }\n }\n\n // Reject signals followed by lowercase prose (#304). `Contra plaintiff's\n // argument, Smith v. Jones, ...` matches `Contra` as a signal, but the\n // following `plaintiff's` is prose, not a citation-introducer context.\n // Real Bluebook signals are followed by a case-name (capital-letter-led),\n // a comma+capital, or directly by the citation core. Multi-word signal\n // forms (`see also`, `but see`, `see, e.g.`) are already captured as\n // complete units by SIGNAL_PATTERNS, so the post-signal text should\n // always begin with case-name context.\n const afterSignal = gapText.substring(best.end).replace(/^[\\s,]+/, \"\")\n if (afterSignal.length > 0) {\n const firstChar = afterSignal[0]\n // Lowercase next char → signal is part of sentence prose, not a\n // citation introducer. Reject. (Digits start citation tokens like\n // `id.`/short-form, but those are already consumed before this gap.)\n if (firstChar >= \"a\" && firstChar <= \"z\") continue\n }\n\n setSignal(c, best.signal)\n }\n}\n","/**\n * False Positive Citation Filtering\n *\n * Flags or removes citations that are likely false positives:\n * - Non-US reporter abbreviations (international, UK, European, historical English)\n * - Citations with years predating US legal reporting (before 1750)\n *\n * Runs as a post-extraction phase (Step 4.9) after string citation grouping.\n * Uses a lightweight static blocklist, enhanced with reporters-db validation\n * when loaded (for single-digit-volume paragraph/footnote marker detection).\n *\n * @module extract/filterFalsePositives\n */\n\nimport type {\n Citation,\n FederalRegisterCitation,\n FullCaseCitation,\n JournalCitation,\n ShortFormCaseCitation,\n StatutesAtLargeCitation,\n Warning,\n} from \"@/types/citation\"\nimport { getReportersSync } from \"@/data/reportersCache\"\n\n/** Year threshold: US legal reporting starts ~1790 (Dallas Reports). 1750 gives headroom. */\nconst MIN_PLAUSIBLE_YEAR = 1750\n\n/** Confidence floor for flagged citations in penalize mode. */\nconst FLAGGED_CONFIDENCE = 0.1\n\n/**\n * Static blocklist of known non-US reporter abbreviations (lowercase, trimmed).\n *\n * International tribunals/treaties:\n * I.C.J., U.N.T.S., I.L.M., I.L.R., P.C.I.J.\n * UK reporters:\n * A.C., W.L.R., All E.R., Q.B., K.B., Ch., Co. Rep.\n * European:\n * E.C.R., E.H.R.R., C.M.L.R.\n * Historical English:\n * Edw. (standalone — \"Edw. Ch.\" is a valid US reporter)\n */\nconst BLOCKED_REPORTERS: ReadonlySet<string> = new Set([\n // International\n \"i.c.j.\",\n \"u.n.t.s.\",\n \"i.l.m.\",\n \"i.l.r.\",\n \"p.c.i.j.\",\n // UK\n \"a.c.\",\n \"w.l.r.\",\n \"all e.r.\",\n \"q.b.\",\n \"k.b.\",\n \"ch.\",\n \"co. rep.\",\n // European\n \"e.c.r.\",\n \"e.h.r.r.\",\n \"c.m.l.r.\",\n // Historical English\n \"edw.\",\n])\n\n/**\n * Get the reporter string to check against the blocklist.\n * Returns undefined for citation types that don't have a reporter,\n * or for short-form types (id, supra, shortFormCase) which inherit\n * their reporter from an antecedent — filtering the antecedent is sufficient.\n */\nfunction getReporter(citation: Citation): string | undefined {\n if (citation.type === \"case\") return (citation as FullCaseCitation).reporter\n if (citation.type === \"shortFormCase\") return (citation as ShortFormCaseCitation).reporter\n if (citation.type === \"journal\") return (citation as JournalCitation).abbreviation\n return undefined\n}\n\n/**\n * Get the year from a citation, if present.\n * Returns undefined for citation types without a year field.\n */\nfunction getYear(citation: Citation): number | undefined {\n switch (citation.type) {\n case \"case\":\n return (citation as FullCaseCitation).year\n case \"journal\":\n return (citation as JournalCitation).year\n case \"federalRegister\":\n return (citation as FederalRegisterCitation).year\n case \"statutesAtLarge\":\n return (citation as StatutesAtLargeCitation).year\n default:\n return undefined\n }\n}\n\n/**\n * Words that should never appear as standalone tokens in a reporter string.\n * These appear in English prose (e.g., \"the District 2 Court dismissed\") and get\n * falsely matched by the broad state-reporter regex.\n * Note: English-only — extend if the library is used on non-English legal text.\n */\nconst REPORTER_BLOCKLIST_WORDS: ReadonlySet<string> = new Set([\n \"court\",\n \"rule\",\n \"section\",\n \"chapter\",\n \"article\",\n \"part\",\n \"title\",\n \"paragraph\",\n \"clause\",\n \"amendment\",\n \"dismissed\",\n \"granted\",\n \"denied\",\n \"filed\",\n \"argued\",\n])\n\n/**\n * Month names matched as `reporter` on date-shaped sequences like `8 April 1988`\n * (day-first European-style dates) where the state-reporter tokenizer's broad\n * `<volume> <Word> <page>` pattern captures the day, month name, and year as a\n * phantom case citation. #302\n */\nconst MONTH_NAMES: ReadonlySet<string> = new Set([\n \"january\",\n \"february\",\n \"march\",\n \"april\",\n \"may\",\n \"june\",\n \"july\",\n \"august\",\n \"september\",\n \"october\",\n \"november\",\n \"december\",\n])\n\n/** Earliest plausible year for a citation's reporting date. Anything below this\n * is almost certainly a false positive (most likely a date misparse). */\nconst MIN_PLAUSIBLE_REPORT_YEAR = 1700\n/** Latest plausible year — current year plus a small buffer for not-yet-reported\n * cases / advance sheets. */\nconst MAX_PLAUSIBLE_REPORT_YEAR = new Date().getFullYear() + 5\n/** Maximum day-of-month for the date-shape filter. */\nconst MAX_DAY_OF_MONTH = 31\n\n/**\n * Date misparse: `<day> <Month> <year>` matched as case citation (#302).\n *\n * The state-reporter regex captures `8 April 1988` as\n * `volume=8, reporter=\"April\", page=1988`. Real reporters never use month names,\n * so any cite whose reporter is a month name AND whose volume/page shape look\n * like day+year is a false positive — rejected unconditionally regardless of\n * the caller's `filterFalsePositives` opt-in.\n */\nfunction isMonthNameDateMisparse(citation: Citation): boolean {\n if (citation.type !== \"case\" && citation.type !== \"shortFormCase\") return false\n const c = citation as FullCaseCitation | ShortFormCaseCitation\n if (!c.reporter) return false\n if (!MONTH_NAMES.has(c.reporter.toLowerCase().trim())) return false\n const vol = typeof c.volume === \"number\" ? c.volume : Number.parseInt(String(c.volume), 10)\n if (Number.isNaN(vol) || vol < 1 || vol > MAX_DAY_OF_MONTH) return false\n const page = typeof c.page === \"number\" ? c.page : Number.parseInt(String(c.page), 10)\n if (Number.isNaN(page)) return false\n return page >= MIN_PLAUSIBLE_REPORT_YEAR && page <= MAX_PLAUSIBLE_REPORT_YEAR\n}\n\n/** Maximum length for a reporter string without periods.\n * Real period-less reporters (e.g., \"Cal\", \"Wis\", \"Mass\") are short.\n * Prose false positives (\"Court dismissed the complaint...\") are long.\n * Threshold of 12 accommodates the longest known period-less reporters\n * (e.g., \"Mass App Ct\" at 11 chars). Raise if new reporters exceed this. */\nconst MAX_PERIODLESS_REPORTER_LENGTH = 12\n\n/**\n * Check if a reporter string looks implausible (prose text matched as reporter).\n * Real reporters contain periods (F.2d, N.W.2d, So. 2d) or are very short (Cal, Wis).\n */\nfunction isImplausibleReporter(reporter: string): boolean {\n const words = reporter.toLowerCase().split(/\\s+/)\n if (words.some((w) => REPORTER_BLOCKLIST_WORDS.has(w))) return true\n if (!reporter.includes(\".\") && reporter.length > MAX_PERIODLESS_REPORTER_LENGTH) return true\n return false\n}\n\n/**\n * Words that appear in prose false positives for single-digit-volume citations\n * but never in legitimate reporter abbreviations (verified against reporters-db).\n * Used as fallback when reporters-db is not loaded.\n */\nconst SINGLE_DIGIT_PROSE_WORDS: ReadonlySet<string> = new Set([\n // Prepositions / conjunctions / articles\n \"the\", \"a\", \"an\", \"in\", \"on\", \"at\", \"but\", \"and\", \"for\", \"by\", \"to\",\n \"with\", \"from\", \"as\", \"if\", \"so\", \"nor\", \"yet\", \"not\", \"no\", \"then\",\n \"when\", \"where\", \"who\", \"what\", \"how\", \"that\", \"this\", \"these\", \"those\",\n // Pronouns\n \"he\", \"she\", \"it\", \"they\", \"we\", \"his\", \"her\", \"its\", \"their\", \"our\",\n // Common verbs\n \"was\", \"were\", \"is\", \"are\", \"has\", \"had\", \"been\", \"being\",\n \"did\", \"does\", \"do\", \"may\", \"shall\", \"will\", \"would\", \"could\", \"should\",\n \"held\", \"said\", \"found\", \"made\", \"took\", \"gave\", \"see\", \"also\",\n // Month names (after HTML stripping, \"¶2 In July 2016\" → \"2 In July 2016\")\n \"january\", \"february\", \"march\", \"april\", \"june\",\n \"july\", \"august\", \"september\", \"october\", \"november\", \"december\",\n])\n\n/** Maximum plausible volume number for US reporters.\n * The most prolific reporters (F. Supp. 3d, F.3d) have volumes in the\n * low-to-mid hundreds. 2000 gives generous headroom while still catching\n * zip codes (5-digit numbers like 20006) and other non-citation numbers. */\nconst MAX_PLAUSIBLE_VOLUME = 2000\n\n/** Docket number pattern: 1-2 digit prefix + hyphen + 4+ digit suffix.\n * E.g., \"24-30706\", \"23-12345\". Real hyphenated citation volumes look\n * like \"1984-1\" (4-digit year + short index). */\nconst DOCKET_VOLUME_REGEX = /^\\d{1,2}-\\d{4,}$/\n\n/**\n * Check if a case citation has an implausibly large volume number.\n * US reporter volumes rarely exceed ~1000. 5-digit volumes are typically\n * zip codes (e.g., \"DC 20006 Counsel for Appellants 20004\").\n */\nfunction isImplausibleVolume(citation: Citation): boolean {\n if (citation.type !== \"case\" && citation.type !== \"shortFormCase\") return false\n const caseCit = citation as FullCaseCitation | ShortFormCaseCitation\n // Only check purely numeric volumes; hyphenated volumes (strings) are\n // handled by isDocketNumberVolume\n if (typeof caseCit.volume !== \"number\") return false\n return caseCit.volume > MAX_PLAUSIBLE_VOLUME\n}\n\n/**\n * Check if a hyphenated volume matches a docket-number pattern.\n * Docket numbers have a short prefix and long suffix (e.g., \"24-30706\").\n * Real hyphenated citation volumes have the opposite shape (e.g., \"1984-1\").\n */\nfunction isDocketNumberVolume(citation: Citation): boolean {\n if (citation.type !== \"case\" && citation.type !== \"shortFormCase\") return false\n const caseCit = citation as FullCaseCitation | ShortFormCaseCitation\n const vol = String(caseCit.volume)\n return DOCKET_VOLUME_REGEX.test(vol)\n}\n\n/**\n * Check if a case citation with small volume (1–20) is likely a\n * paragraph/footnote marker misidentified as a citation.\n *\n * After HTML stripping, paragraph markers like \"¶2\" become bare \"2\", which the\n * broad state-reporter regex matches as volume + prose-as-reporter + next-number-as-page.\n *\n * For reporters WITHOUT periods: validates against reporters-db when loaded\n * (precise, zero false negatives), falls back to prose-word blocklist.\n *\n * For reporters WITH periods: also validates against reporters-db, since\n * non-reporter abbreviations like \"R. Civ. P.\" and \"Fed. R. Civ. P.\" contain\n * periods but are not real reporters.\n */\nfunction isSuspiciousSmallVolume(citation: Citation): boolean {\n if (citation.type !== \"case\" && citation.type !== \"shortFormCase\") return false\n const caseCit = citation as FullCaseCitation | ShortFormCaseCitation\n const vol =\n typeof caseCit.volume === \"number\"\n ? caseCit.volume\n : Number.parseInt(String(caseCit.volume), 10)\n if (Number.isNaN(vol) || vol < 1 || vol > 20) return false\n\n const reporter = caseCit.reporter\n if (!reporter) return false\n\n // Primary: check reporters-db if loaded (works for all reporters)\n const db = getReportersSync()\n if (db) {\n const matches = db.byAbbreviation.get(reporter.toLowerCase()) ?? []\n return matches.length === 0\n }\n\n // Fallback when reporters-db not loaded:\n // Period-containing reporters are more likely real (F.2d, Cal., Ohio St.)\n // but we can't validate without the db, so let them through\n if (reporter.includes(\".\")) return false\n\n // For period-less reporters, use expanded prose-word heuristic\n const words = reporter.toLowerCase().split(/\\s+/)\n return words.some((w) => SINGLE_DIGIT_PROSE_WORDS.has(w))\n}\n\n/**\n * Check if a citation is a likely false positive (short-circuit, no allocations).\n */\nfunction isFalsePositive(citation: Citation): boolean {\n const reporter = getReporter(citation)\n if (reporter && BLOCKED_REPORTERS.has(reporter.toLowerCase().trim())) return true\n if (reporter && (citation.type === \"case\" || citation.type === \"shortFormCase\") && isImplausibleReporter(reporter)) return true\n if (isImplausibleVolume(citation)) return true\n if (isDocketNumberVolume(citation)) return true\n if (isSuspiciousSmallVolume(citation)) return true\n\n const year = getYear(citation)\n if (year !== undefined && year < MIN_PLAUSIBLE_YEAR) return true\n\n return false\n}\n\n/**\n * Collect all false positive reasons for a citation.\n * Returns an empty array if the citation is clean.\n * Only called in penalize mode where we need the reason strings for warnings.\n */\nfunction collectFalsePositiveReasons(citation: Citation): string[] {\n const reasons: string[] = []\n\n const reporter = getReporter(citation)\n if (reporter) {\n const normalized = reporter.toLowerCase().trim()\n if (BLOCKED_REPORTERS.has(normalized)) {\n reasons.push(`Reporter \"${reporter}\" is a known non-US source`)\n }\n if ((citation.type === \"case\" || citation.type === \"shortFormCase\") && isImplausibleReporter(reporter)) {\n reasons.push(`Reporter \"${reporter}\" contains prose words or is implausibly long`)\n }\n }\n\n if (isImplausibleVolume(citation)) {\n const caseCit = citation as FullCaseCitation | ShortFormCaseCitation\n reasons.push(\n `Volume ${caseCit.volume} exceeds maximum plausible volume (${MAX_PLAUSIBLE_VOLUME}) — likely a zip code or other number`,\n )\n }\n\n if (isDocketNumberVolume(citation)) {\n const caseCit = citation as FullCaseCitation | ShortFormCaseCitation\n reasons.push(\n `Hyphenated volume \"${caseCit.volume}\" matches docket number pattern — likely a case number, not a citation volume`,\n )\n }\n\n if (isSuspiciousSmallVolume(citation)) {\n const caseCit = citation as FullCaseCitation | ShortFormCaseCitation\n reasons.push(\n `Small volume (${caseCit.volume}) with unrecognized reporter \"${caseCit.reporter}\" — likely a paragraph or footnote marker`,\n )\n }\n\n const year = getYear(citation)\n if (year !== undefined && year < MIN_PLAUSIBLE_YEAR) {\n reasons.push(`Year ${year} predates US legal reporting (threshold: ${MIN_PLAUSIBLE_YEAR})`)\n }\n\n return reasons\n}\n\n/**\n * Apply false positive filters to extracted citations.\n *\n * @param citations - Extracted citations (may be mutated in penalize mode)\n * @param remove - If true, remove flagged citations. If false, penalize confidence + add warning.\n * @returns Filtered array (same reference if remove=false, new array if remove=true and items removed)\n */\nexport function applyFalsePositiveFilters(citations: Citation[], remove: boolean): Citation[] {\n // Hard-reject pass: unconditionally drop unambiguous garbage like\n // `<day> <Month> <year>` date misparses (#302). These are never legitimate\n // citations under any policy, so they should not survive even when the\n // caller asked for soft-flag mode.\n const hardFiltered = citations.filter((c) => !isMonthNameDateMisparse(c))\n\n if (remove) {\n return hardFiltered.filter((c) => !isFalsePositive(c))\n }\n\n for (const citation of hardFiltered) {\n // Skip if already penalized (idempotency guard)\n if (citation.confidence === FLAGGED_CONFIDENCE && citation.warnings?.length) continue\n\n const reasons = collectFalsePositiveReasons(citation)\n if (reasons.length > 0) {\n citation.confidence = FLAGGED_CONFIDENCE\n const warnings: Warning[] = reasons.map((message) => ({\n level: \"warning\" as const,\n message,\n position: { start: citation.span.originalStart, end: citation.span.originalEnd },\n }))\n citation.warnings = [...(citation.warnings || []), ...warnings]\n }\n }\n\n return hardFiltered\n}\n","/**\n * Main Citation Extraction Pipeline\n *\n * Orchestrates the complete citation extraction flow:\n * 1. Clean text (remove HTML, normalize Unicode)\n * 2. Tokenize (apply patterns to find candidates)\n * 3. Extract (parse metadata from tokens)\n *\n * This is the primary public API for citation extraction.\n *\n * @module extract/extractCitations\n */\n\nimport { cleanText } from \"@/clean\"\nimport { UnionFind } from \"@/extract/unionFind\"\nimport { detectFootnotes } from \"@/footnotes/detectFootnotes\"\nimport { mapFootnoteZones } from \"@/footnotes/mapZones\"\nimport { tagCitationsWithFootnotes } from \"@/footnotes/tagging\"\nimport type { FootnoteMap } from \"@/footnotes/types\"\nimport {\n extractCase,\n extractConstitutional,\n extractDocket,\n extractFederalRegister,\n extractJournal,\n extractNeutral,\n extractPublicLaw,\n extractStatute,\n extractStatutesAtLarge,\n} from \"@/extract\"\nimport type { Pattern } from \"@/patterns\"\nimport {\n casePatterns,\n constitutionalPatterns,\n docketPatterns,\n journalPatterns,\n neutralPatterns,\n shortFormPatterns,\n statutePatterns,\n} from \"@/patterns\"\nimport { tokenize } from \"@/tokenize\"\nimport type { Citation, HistorySignal } from \"@/types/citation\"\nimport { resolveCitations } from \"../resolve\"\nimport type { ResolutionOptions, ResolvedCitation } from \"../resolve/types\"\nimport { detectParallelCitations } from \"./detectParallel\"\nimport { detectStringCitations, detectLeadingSignals } from \"./detectStringCites\"\nimport { extractId, extractShortFormCase, extractSupra } from \"./extractShortForms\"\nimport { applyFalsePositiveFilters } from \"./filterFalsePositives\"\n\n/**\n * Regex to parse \"volume reporter page\" from a citation token's text.\n * Used to build groupId and parallelCitations metadata for parallel citation groups.\n */\nconst CITATION_PARTS_RE = /^(\\S+)\\s+(.+)\\s+(\\d+)$/\n\n/**\n * Options for customizing citation extraction behavior.\n */\nexport interface ExtractOptions {\n /**\n * Custom text cleaners (overrides defaults).\n *\n * If provided, these cleaners replace the default pipeline:\n * [stripHtmlTags, normalizeWhitespace, normalizeUnicode, fixSmartQuotes]\n *\n * @example\n * ```typescript\n * // Use only HTML stripping, skip Unicode normalization\n * const citations = extractCitations(text, {\n * cleaners: [stripHtmlTags]\n * })\n * ```\n */\n cleaners?: Array<(text: string) => string>\n\n /**\n * Custom regex patterns (overrides defaults).\n *\n * If provided, these patterns replace the default pattern set:\n * [casePatterns, statutePatterns, journalPatterns, neutralPatterns, shortFormPatterns]\n *\n * @example\n * ```typescript\n * // Extract only case citations\n * const citations = extractCitations(text, {\n * patterns: casePatterns\n * })\n * ```\n */\n patterns?: Pattern[]\n\n /**\n * Resolve short-form citations to their full antecedents (default: false).\n *\n * If true, returns ResolvedCitation[] with resolution metadata for short-form citations\n * (Id., supra, short-form case). Full citations are unchanged.\n *\n * @example\n * ```typescript\n * const text = \"Smith v. Jones, 500 F.2d 100 (1974). Id. at 105.\"\n * const citations = extractCitations(text, { resolve: true })\n * // citations[1].resolution.resolvedTo === 0 (points to Smith v. Jones)\n * ```\n */\n resolve?: boolean\n\n /**\n * Options for citation resolution (only used if resolve: true).\n *\n * @example\n * ```typescript\n * const citations = extractCitations(text, {\n * resolve: true,\n * resolutionOptions: {\n * scopeStrategy: 'paragraph',\n * fuzzyPartyMatching: true\n * }\n * })\n * ```\n */\n resolutionOptions?: ResolutionOptions\n\n /**\n * Remove citations flagged as likely false positives (default: false).\n *\n * When false (default), flagged citations get reduced confidence (0.1) and a warning.\n * When true, flagged citations are removed from results entirely.\n *\n * False positive detection uses:\n * - A static blocklist of known non-US reporter abbreviations (international, UK, European)\n * - A year plausibility heuristic (years before 1750 predate US legal reporting)\n *\n * @example\n * ```typescript\n * // Remove false positives from results\n * const citations = extractCitations(text, { filterFalsePositives: true })\n * ```\n */\n filterFalsePositives?: boolean\n\n /** Detect footnote zones and annotate citations with inFootnote/footnoteNumber (default: false) */\n detectFootnotes?: boolean\n}\n\n/**\n * Extracts legal citations from text using the full parsing pipeline.\n *\n * Pipeline flow:\n * 1. **Clean:** Remove HTML tags, normalize Unicode, fix smart quotes\n * 2. **Tokenize:** Apply regex patterns to find citation candidates\n * 3. **Extract:** Parse metadata (volume, reporter, page, etc.)\n * 4. **Translate:** Map positions from cleaned text back to original text\n *\n * This function is synchronous because all stages (cleaning, tokenization,\n * extraction) are synchronous. For async operations (e.g., future reporters-db\n * lookups), use extractCitationsAsync().\n *\n * Position tracking:\n * - TransformationMap is built during cleaning\n * - Tokens contain positions in cleaned text (cleanStart/cleanEnd)\n * - Extraction translates cleaned positions → original positions\n * - Final citations have originalStart/originalEnd pointing to input text\n *\n * Warnings from cleaning layer are attached to all extracted citations.\n *\n * @param text - Raw text to extract citations from (may contain HTML, Unicode)\n * @param options - Optional customization (cleaners, patterns)\n * @returns Array of citations with parsed metadata and accurate positions\n *\n * @example\n * ```typescript\n * const text = \"See Smith v. Doe, 500 F.2d 123 (9th Cir. 2020)\"\n * const citations = extractCitations(text)\n * // citations[0] = {\n * // type: \"case\",\n * // volume: 500,\n * // reporter: \"F.2d\",\n * // page: 123,\n * // court: \"9th Cir.\",\n * // year: 2020,\n * // span: { originalStart: 18, originalEnd: 30, ... }\n * // }\n * ```\n *\n * @example\n * ```typescript\n * // Extract from HTML\n * const html = \"<p>In <b>Smith</b>, 500 F.2d 123, the court held...</p>\"\n * const citations = extractCitations(html)\n * // HTML is stripped, positions point to original HTML\n * ```\n *\n * @example\n * ```typescript\n * // Extract multiple citation types\n * const text = \"See 42 U.S.C. § 1983; Smith, 500 F.2d 123; 123 Harv. L. Rev. 456\"\n * const citations = extractCitations(text)\n * // citations[0].type === \"statute\"\n * // citations[1].type === \"case\"\n * // citations[2].type === \"journal\"\n * ```\n */\nexport function extractCitations(\n text: string,\n options: ExtractOptions & { resolve: true },\n): ResolvedCitation[]\nexport function extractCitations(text: string, options?: ExtractOptions): Citation[]\nexport function extractCitations(\n text: string,\n options?: ExtractOptions,\n): Citation[] | ResolvedCitation[] {\n const startTime = performance.now()\n\n // Step 1: Clean text\n const { cleaned, transformationMap, warnings } = cleanText(text, options?.cleaners)\n\n // Step 1.5: Detect footnote zones (opt-in)\n let cleanFootnoteMap: FootnoteMap | undefined\n if (options?.detectFootnotes) {\n const rawZones = detectFootnotes(text)\n if (rawZones.length > 0) {\n cleanFootnoteMap = mapFootnoteZones(rawZones, transformationMap)\n }\n }\n\n // Step 2: Tokenize (synchronous)\n // Note: Pattern order matters for deduplication - more specific patterns first\n const allPatterns = options?.patterns || [\n ...neutralPatterns, // Most specific (year-based format)\n ...docketPatterns, // Docket-number citations (anchored by \"No. \")\n ...shortFormPatterns, // Short-form (requires \" at \" keyword)\n ...casePatterns, // Case citations (reporter-specific)\n ...constitutionalPatterns, // Constitutional citations (more specific than statutes)\n ...statutePatterns, // Statutes (code-specific)\n ...journalPatterns, // Least specific (broad pattern)\n ]\n const tokens = tokenize(cleaned, allPatterns)\n\n // Step 3: Deduplicate overlapping tokens via priority-aware subsumption.\n //\n // Multiple patterns may match the same or overlapping text:\n // (a) Exact-span duplicates — e.g., `100 F.3d 456` matches both\n // `federal-reporter` and `state-reporter`. Keep the higher-priority\n // one (earlier in the pattern list = more specific).\n // (b) Subsumed matches — a token whose span is a strict subset of\n // another token's span, and the containing token comes from a\n // more-or-equally-specific pattern. Canonical failure: `state-reporter`\n // or `law-review` treating `F.3d at` / `U.S. at` as a reporter/journal\n // name inside a `shortFormCase` cite (see #207, #209). Drop the\n // subsumed token only when the container is at least as specific —\n // otherwise we'd swallow legitimate shorter matches (e.g., a\n // `state-constitution` token that sits inside a broader `named-code`\n // match for \"Cal. Const. art. I, § 7.\").\n //\n // Priority = first occurrence index in `allPatterns`. Duplicate patternIds\n // (e.g., \"supra\") share the earliest index, which is fine — they form a\n // single priority bucket.\n const priorityByPatternId = new Map<string, number>()\n for (let i = 0; i < allPatterns.length; i++) {\n const id = allPatterns[i].id\n if (!priorityByPatternId.has(id)) priorityByPatternId.set(id, i)\n }\n const priorityOf = (t: (typeof tokens)[number]): number =>\n priorityByPatternId.get(t.patternId) ?? Number.POSITIVE_INFINITY\n\n // Sort by (cleanStart asc, cleanEnd desc, priority asc) so containers come\n // before their contained tokens at each start position, and within any\n // (start, end) bucket the higher-priority token comes first.\n const sortedTokens = [...tokens].sort(\n (a, b) =>\n a.span.cleanStart - b.span.cleanStart ||\n b.span.cleanEnd - a.span.cleanEnd ||\n priorityOf(a) - priorityOf(b),\n )\n const deduplicatedTokens: typeof tokens = []\n for (const token of sortedTokens) {\n let subsumed = false\n for (const kept of deduplicatedTokens) {\n const contains =\n kept.span.cleanStart <= token.span.cleanStart && kept.span.cleanEnd >= token.span.cleanEnd\n if (!contains) continue\n if (priorityOf(kept) > priorityOf(token)) continue // kept is less specific, don't let it swallow\n // `kept` is at least as specific AND contains this token. Drop `token`\n // if this is a strict containment or an equal-span lower-priority duplicate.\n if (\n kept.span.cleanStart < token.span.cleanStart ||\n kept.span.cleanEnd > token.span.cleanEnd ||\n priorityOf(kept) < priorityOf(token)\n ) {\n subsumed = true\n break\n }\n }\n if (!subsumed) deduplicatedTokens.push(token)\n }\n\n // Step 3.5: Detect parallel citation groups\n // Map of primary token index -> array of secondary token indices\n const parallelGroups = detectParallelCitations(deduplicatedTokens, cleaned)\n\n // Build reverse-lookup: secondary index -> primary index (O(1) instead of O(N×M))\n const secondaryToGroup = new Map<number, number>()\n for (const [primary, secondaries] of parallelGroups.entries()) {\n for (const s of secondaries) secondaryToGroup.set(s, primary)\n }\n\n // Span list for all case-shape tokens. Passed to extractCase so the per-cite\n // pincite/year/caseName logic can see what's adjacent and avoid scanning\n // INTO neighbor citations (parallel-cite chains share a trailing year paren\n // and the case-name backward walk for a parallel cite must stop at the\n // prior cite's end).\n const caseTokenSpans = deduplicatedTokens\n .filter((t) => t.type === \"case\")\n .map((t) => ({ cleanStart: t.span.cleanStart, cleanEnd: t.span.cleanEnd }))\n\n // Step 4: Extract citations from deduplicated tokens\n const citations: Citation[] = []\n for (let i = 0; i < deduplicatedTokens.length; i++) {\n const token = deduplicatedTokens[i]\n let citation: Citation\n\n switch (token.type) {\n case \"case\":\n // Check pattern ID to distinguish short-form from full citations\n if (token.patternId === \"id\" || token.patternId === \"ibid\") {\n citation = extractId(token, transformationMap, cleaned)\n } else if (token.patternId === \"supra\") {\n citation = extractSupra(token, transformationMap, cleaned)\n } else if (token.patternId === \"shortFormCase\") {\n citation = extractShortFormCase(token, transformationMap, cleaned)\n } else {\n citation = extractCase(\n token,\n transformationMap,\n cleaned,\n text,\n caseTokenSpans,\n )\n }\n break\n case \"docket\": {\n // Docket extractor returns undefined when no case-name anchor is\n // found — the bare \"No. <N> (<court> <year>)\" shape is too generic\n // to surface without context.\n const result = extractDocket(token, transformationMap, cleaned, text)\n if (!result) continue\n citation = result\n break\n }\n case \"statute\":\n citation = extractStatute(token, transformationMap)\n break\n case \"journal\":\n citation = extractJournal(token, transformationMap, cleaned)\n break\n case \"neutral\":\n citation = extractNeutral(token, transformationMap, cleaned)\n break\n case \"publicLaw\":\n citation = extractPublicLaw(token, transformationMap)\n break\n case \"federalRegister\":\n citation = extractFederalRegister(token, transformationMap)\n break\n case \"statutesAtLarge\":\n citation = extractStatutesAtLarge(token, transformationMap)\n break\n case \"constitutional\":\n citation = extractConstitutional(token, transformationMap)\n break\n default:\n // Unknown type - skip\n continue\n }\n\n // Attach cleaning warnings to citation if any\n if (warnings.length > 0) {\n citation.warnings = [...(citation.warnings || []), ...warnings]\n }\n\n // Update processing time\n citation.processTimeMs = performance.now() - startTime\n\n // Populate parallel citation metadata (Phase 8)\n if (citation.type === \"case\") {\n const isPrimary = parallelGroups.has(i)\n const isSecondary = secondaryToGroup.has(i)\n\n if (isPrimary || isSecondary) {\n const primaryIndex = isSecondary ? (secondaryToGroup.get(i) ?? i) : i\n const primaryToken = deduplicatedTokens[primaryIndex]\n const match = CITATION_PARTS_RE.exec(primaryToken.text)\n if (match) {\n const [, volume, reporter, page] = match\n citation.groupId = `${volume}-${reporter.replace(/\\s+/g, \".\")}-${page}`\n\n if (isPrimary) {\n const secondaryIndices = parallelGroups.get(i) ?? []\n citation.parallelCitations = secondaryIndices.map((secIdx) => {\n const secToken = deduplicatedTokens[secIdx]\n const secMatch = CITATION_PARTS_RE.exec(secToken.text)\n if (secMatch) {\n const [, secVol, secRep, secPage] = secMatch\n return {\n volume: /^\\d+$/.test(secVol) ? Number.parseInt(secVol, 10) : secVol,\n reporter: secRep,\n page: Number.parseInt(secPage, 10),\n }\n }\n return { volume: 0, reporter: \"\", page: 0 }\n })\n }\n }\n }\n }\n\n citations.push(citation)\n }\n\n // Step 4.5: Link subsequent history citations using Union-Find.\n // Three-phase approach: match signals → union chains → aggregate entries.\n // Invariant: citations are in text order (guaranteed by token-order processing above).\n linkSubsequentHistory(citations)\n\n // Step 4.55: Inherit case name from chain root for subsequent-history children (#224).\n // Per Bluebook 10.7, all citations in a history chain reference one case.\n // Without this, extractCaseName scans back from the child, captures the\n // parent cite + connector (\"Smith v. Doe, 100 F.3d 200 (...), aff'd\"),\n // and produces a nonsense caseName.\n inheritSubsequentHistoryCaseName(citations)\n\n // Step 4.6: Propagate caseName from the primary onto each parallel-cite\n // secondary (#282). Detection in step 4 sets the shared `groupId` and\n // populates `parallelCitations` on the primary; this pass fills in the\n // shared caption fields on secondaries that have no caseName of their own.\n // Runs AFTER 4.55 so a primary that inherited from a history chain root\n // still propagates that inherited caption to its parallels.\n inheritParallelCaseName(citations)\n\n // Step 4.65: Attach year-of-edition / publisher from a trailing parenthetical\n // to statute citations (#285). E.g. `HRS § 91-14(a) (1985)` → year=1985;\n // `28 U.S.C. § 1331 (West 2018)` → publisher=\"West\", year=2018.\n attachStatuteYearParen(citations, cleaned)\n\n // Step 4.75: Detect string citation groups (semicolon-separated)\n detectStringCitations(citations, cleaned)\n\n // Step 4.8: Detect leading introductory signals for all citations.\n // Runs after string cite detection (which sets mid-group signals) so we\n // only scan backward for citations that still lack a signal.\n detectLeadingSignals(citations, cleaned)\n\n // Step 4.9: Apply false positive filters (blocklist + year heuristic)\n const filtered = applyFalsePositiveFilters(citations, options?.filterFalsePositives ?? false)\n\n // Step 4.95: Tag citations with footnote metadata\n if (cleanFootnoteMap) {\n tagCitationsWithFootnotes(filtered, cleanFootnoteMap)\n }\n\n // Step 5: Resolve short-form citations if requested\n if (options?.resolve) {\n const resolutionOpts = cleanFootnoteMap\n ? { ...options.resolutionOptions, footnoteMap: cleanFootnoteMap }\n : options.resolutionOptions\n return resolveCitations(filtered, text, resolutionOpts)\n }\n\n return filtered\n}\n\n/**\n * Asynchronous version of extractCitations().\n *\n * Currently wraps the synchronous extractCitations() function. This API\n * exists for future extensibility when async operations are added:\n * - Async reporters-db lookups (Phase 3)\n * - Async resolution/annotation services\n * - Web Workers for parallel processing\n *\n * For now, this function immediately resolves with the same results as\n * the synchronous version.\n *\n * @param text - Raw text to extract citations from\n * @param options - Optional customization (cleaners, patterns, resolve)\n * @returns Promise resolving to array of citations (or ResolvedCitation[] if resolve: true)\n *\n * @example\n * ```typescript\n * const citations = await extractCitationsAsync(text, { resolve: true })\n * // Returns ResolvedCitation[] with resolution metadata\n * ```\n */\nexport async function extractCitationsAsync(\n text: string,\n options: ExtractOptions & { resolve: true },\n): Promise<ResolvedCitation[]>\nexport async function extractCitationsAsync(\n text: string,\n options?: ExtractOptions,\n): Promise<Citation[]>\nexport async function extractCitationsAsync(\n text: string,\n options?: ExtractOptions,\n): Promise<Citation[] | ResolvedCitation[]> {\n // Async wrapper for future extensibility (e.g., async reporters-db lookup)\n // For MVP, wraps synchronous extractCitations\n return extractCitations(text, options)\n}\n\n/**\n * Link subsequent history citations using a three-phase Union-Find approach.\n * Replaces the old mutation-during-iteration pattern with cleanly separated phases.\n */\nfunction linkSubsequentHistory(citations: Citation[]): void {\n // Phase 1: Signal matching — collect (parent, child) pairs without mutating citations.\n // Also record each child's signal text for back-pointer assignment in Phase 3.\n const pairs: Array<{ parentIdx: number; childIdx: number; signal: HistorySignal }> = []\n\n for (let i = 0; i < citations.length; i++) {\n const parent = citations[i]\n if (parent.type !== \"case\" || !parent.subsequentHistoryEntries) continue\n\n const entries = parent.subsequentHistoryEntries\n let entryIdx = 0\n\n for (let j = i + 1; j < citations.length && entryIdx < entries.length; j++) {\n const child = citations[j]\n if (child.type !== \"case\") continue\n\n const signalEnd = entries[entryIdx].signalSpan.cleanEnd\n if (child.span.cleanStart >= signalEnd) {\n pairs.push({ parentIdx: i, childIdx: j, signal: entries[entryIdx].signal })\n entryIdx++\n }\n }\n }\n\n if (pairs.length === 0) return\n\n // Phase 2: Union — build connected components from parent-child pairs.\n const uf = new UnionFind(citations.length)\n for (const pair of pairs) {\n uf.union(pair.parentIdx, pair.childIdx)\n }\n\n // Build lookup: childIdx → signal (for back-pointer assignment)\n const childSignals = new Map<number, HistorySignal>()\n for (const pair of pairs) {\n childSignals.set(pair.childIdx, pair.signal)\n }\n\n // Phase 3: Aggregation — set back-pointers and collect entries onto chain roots.\n for (const [root, members] of uf.components()) {\n if (members.length === 1) continue\n\n const rootCitation = citations[root]\n if (rootCitation.type !== \"case\") continue\n\n const allEntries = [...(rootCitation.subsequentHistoryEntries ?? [])]\n\n for (const memberIdx of members) {\n if (memberIdx === root) continue\n\n const member = citations[memberIdx]\n if (member.type !== \"case\") continue\n\n // Set back-pointer to chain root.\n // Signal is guaranteed to exist: every non-root member was recorded as a\n // child in Phase 1, which always stores the signal in childSignals.\n const signal = childSignals.get(memberIdx)\n if (!signal) continue\n member.subsequentHistoryOf = { index: root, signal }\n\n // Aggregate entries from non-root members onto the root\n if (member.subsequentHistoryEntries) {\n for (const entry of member.subsequentHistoryEntries) {\n allEntries.push({ ...entry, order: allEntries.length })\n }\n member.subsequentHistoryEntries = undefined\n }\n }\n\n rootCitation.subsequentHistoryEntries = allEntries\n }\n}\n\n/**\n * Inherit case name fields from the chain root for subsequent-history children.\n *\n * In a chain like `<full cite A>, modified on other grounds, <full cite B>`,\n * citation B has no preceding case-name string in the document — it implicitly\n * shares A's case name (Bluebook Rule 10.7). The default case-name scanner\n * walks left from B and captures all of A's text plus the history connector.\n *\n * After `linkSubsequentHistory` has set `subsequentHistoryOf` back-pointers,\n * this pass overwrites the child's case-name fields with the chain root's,\n * clears component spans (the child has no anchor), and trims `fullSpan`\n * back to the child's own citation core.\n *\n * Closes #224.\n */\nfunction inheritSubsequentHistoryCaseName(citations: Citation[]): void {\n for (const child of citations) {\n if (child.type !== \"case\") continue\n if (!child.subsequentHistoryOf) continue\n const parent = citations[child.subsequentHistoryOf.index]\n if (!parent || parent.type !== \"case\") continue\n if (!parent.caseName) continue\n\n child.caseName = parent.caseName\n child.plaintiff = parent.plaintiff\n child.defendant = parent.defendant\n child.plaintiffNormalized = parent.plaintiffNormalized\n child.defendantNormalized = parent.defendantNormalized\n child.proceduralPrefix = parent.proceduralPrefix\n\n if (child.spans) {\n child.spans.caseName = undefined\n child.spans.plaintiff = undefined\n child.spans.defendant = undefined\n }\n\n // Trim fullSpan to the child's own citation core. extractCaseName had\n // anchored fullSpan at the parent's case name; that's not the child's\n // text. Keep the original cleanEnd/originalEnd (parenthetical end).\n if (child.fullSpan) {\n child.fullSpan = {\n cleanStart: child.span.cleanStart,\n cleanEnd: child.fullSpan.cleanEnd,\n originalStart: child.span.originalStart,\n originalEnd: child.fullSpan.originalEnd,\n }\n }\n }\n}\n\n/**\n * Propagate caseName fields from the primary onto each parallel-cite secondary.\n *\n * A parallel-citation group (`Roe v. Wade, 410 U.S. 113, 93 S. Ct. 705, 35 L. Ed. 2d 147\n * (1973)`) shares one caption across multiple reporter citations. Step 4 in the main\n * pipeline assigns the shared `groupId` to every cite in the group and populates\n * `parallelCitations` on the primary, but only the primary's per-cite case-name\n * extraction captures `Roe v. Wade` — secondaries land with `caseName === undefined`.\n *\n * The primary in a group is the cite that has a non-undefined `caseName` (it appears\n * first in document order and so is the only one the backward case-name scan can anchor\n * on without breaking the parallel-cite disambiguation from #281). This pass joins on\n * `groupId`, takes the first cite per group that has a `caseName`, and copies the\n * shared caption fields onto every other cite in the same group.\n *\n * Closes #282.\n */\n/**\n * Statute year-of-edition parenthetical regex (#285 + #349).\n *\n * Matches a parenthetical that follows a statute citation core. Anchored at\n * the start of the lookahead text (we substring from `span.cleanEnd`) and\n * allows whitespace, an optional comma + whitespace, and an optional\n * `, at <pincite>` intervening text. The parenthetical body is one of:\n *\n * - `(YYYY)` — bare year\n * - `(Publisher YYYY)` — publisher-first (`(West 2018)`,\n * `(Lexis Nexis 2019)`)\n * - `(Label. YYYY)` — edition-label-first (`(Repl. 1996)`,\n * `(Supp. 1985)`, `(Cum. Supp. 1985)`)\n * - `(YYYY Label.)` — year-first edition label (`(1969 Supp.)`,\n * `(1985 Cum. Supp.)`)\n *\n * Subsection parens like `(a)` and `(1)` don't match — the body must contain\n * a 4-digit year. The pre/post token (group 1 / group 3) admits a trailing\n * `.` and an optional second capitalized word so `Cum. Supp.` and\n * `Lexis Nexis` both flow through the same regex.\n */\nconst STATUTE_YEAR_PAREN_REGEX =\n /^\\s*(?:,\\s*(?:at\\s+)?\\d+(?:-\\d+)?\\s*)?\\(\\s*([A-Z][A-Za-z]+\\.?(?:\\s+[A-Z][A-Za-z]+\\.?)?)?\\s*(\\d{4})\\s*([A-Z][A-Za-z]+\\.?(?:\\s+[A-Z][A-Za-z]+\\.?)?)?\\s*\\)/\n\n/**\n * Edition-label set — captured tokens that should populate `editionLabel`\n * rather than `publisher`. `Repl.` = replacement volume, `Supp.` = supplement,\n * `Cum. Supp.` = cumulative supplement. `Reissue` = reissued volume\n * (Nebraska's rolling-reissue convention) #349 #373\n */\nconst EDITION_LABEL_REGEX = /^(?:Repl|Supp|Cum\\.?\\s*Supp|Reissue)\\.?$/i\n\n/**\n * Attach `year` (and optional `publisher` / `editionLabel`) to statute\n * citations whose citation core is immediately followed by a year-of-edition\n * parenthetical.\n *\n * `HRS § 91-14(a) (1985)`, `42 U.S.C. § 1983 (1976)`,\n * `28 U.S.C. § 1331 (West 2018)`, and `Ark. Code Ann. § 11-9-514(a)(1)\n * (Repl. 1996)` are all common code-edition forms. The tokenizer captures\n * only the citation core; this post-pass scans forward from `span.cleanEnd`\n * for the year-paren and routes the non-year token to `publisher` or\n * `editionLabel` depending on whether it's a replacement/supplement marker.\n *\n * Closes #285. Extended for `Repl.` / `Supp.` / `Cum. Supp.` in #349.\n */\nfunction attachStatuteYearParen(citations: Citation[], cleaned: string): void {\n for (const cite of citations) {\n if (cite.type !== \"statute\") continue\n if (cite.year !== undefined) continue\n const after = cleaned.slice(cite.span.cleanEnd)\n const match = STATUTE_YEAR_PAREN_REGEX.exec(after)\n if (!match) continue\n const [, prefixToken, yearStr, suffixToken] = match\n cite.year = Number.parseInt(yearStr, 10)\n // Route the non-year token: edition label vs publisher. Either slot may\n // carry it (publisher conventionally precedes the year; edition labels\n // appear on either side).\n const token = prefixToken ?? suffixToken\n if (!token) continue\n if (EDITION_LABEL_REGEX.test(token)) {\n // Normalize spacing inside `Cum. Supp.` to a single space.\n cite.editionLabel = token.replace(/\\s+/g, \" \").trim()\n } else {\n cite.publisher = token\n }\n }\n}\n\nfunction inheritParallelCaseName(citations: Citation[]): void {\n const primaryByGroup = new Map<string, number>()\n for (let i = 0; i < citations.length; i++) {\n const c = citations[i]\n if (c.type !== \"case\") continue\n if (!c.groupId || !c.caseName) continue\n if (!primaryByGroup.has(c.groupId)) primaryByGroup.set(c.groupId, i)\n }\n\n for (const secondary of citations) {\n if (secondary.type !== \"case\") continue\n if (!secondary.groupId) continue\n if (secondary.caseName) continue\n const primaryIdx = primaryByGroup.get(secondary.groupId)\n if (primaryIdx === undefined) continue\n const primary = citations[primaryIdx]\n if (!primary || primary.type !== \"case\") continue\n\n secondary.caseName = primary.caseName\n secondary.plaintiff = primary.plaintiff\n secondary.defendant = primary.defendant\n secondary.plaintiffNormalized = primary.plaintiffNormalized\n secondary.defendantNormalized = primary.defendantNormalized\n secondary.proceduralPrefix = primary.proceduralPrefix\n }\n}\n"],"mappings":"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M,kBACN,OACA,KAAM,CACJ,WAAY,EAAK,WACjB,SAAU,EAAK,SACf,gBACA,cACD,CACD,WAXiB,GAYjB,YAAa,EACb,cAAe,EACf,gBAAiB,EACjB,SACA,OACA,OACA,QACD,CChDH,MAAa,GAA0B,CACrC,CACE,GAAI,mBAKJ,MACE,4JACF,YAAa,mEACb,KAAM,OACP,CACD,CACE,GAAI,gBAGJ,MACE,mKACF,YACE,iFACF,KAAM,OACP,CACD,CACE,GAAI,iBAmBJ,MACE,oJACF,YACE,2RACF,KAAM,OACP,CACF,CCtDY,GAA4B,CACvC,CACE,GAAI,0BAMJ,MAAO,wDACP,YAAa,0EACb,KAAM,SACP,CACF,CCdY,GAA6B,CACxC,CACE,GAAI,aACJ,MAAO,oFACP,YACE,uSACF,KAAM,UACP,CACF,CCRY,GAA6B,CACxC,CAIE,GAAI,qCACJ,MAAO,uCACP,YACE,wFACF,KAAM,UACP,CACD,CAKE,GAAI,kCACJ,MAAO,sCACP,YACE,sFACF,KAAM,UACP,CACD,CAWE,GAAI,uBACJ,MACE,sHACF,YACE,qKACF,KAAM,UACP,CACD,CACE,GAAI,UACJ,MAAO,4BACP,YAAa,6CACb,KAAM,UACP,CACD,CAME,GAAI,QACJ,MAAO,oDACP,YACE,mJACF,KAAM,UACP,CACD,CACE,GAAI,aACJ,MAAO,2CACP,YAAa,yEACb,KAAM,YACP,CACD,CACE,GAAI,mBACJ,MAAO,8CACP,YAAa,0DACb,KAAM,kBACP,CACD,CACE,GAAI,oBACJ,MAAO,uCACP,YAAa,sDACb,KAAM,kBACP,CACD,CACE,GAAI,qBACJ,MAAO,kEACP,YAAa,2EACb,KAAM,UACP,CACF,CCgCY,GAA+B,CAC1C,CACE,GAAI,KACJ,MArGF,wPAsGE,YAAa,8CACb,KAAM,OACP,CACD,CACE,GAAI,OACJ,MAtGF,sOAuGE,YAAa,oDACb,KAAM,OACP,CACD,CACE,GAAI,QACJ,MAzDF,iKA0DE,YACE,+FACF,KAAM,OACP,CACD,CACE,GAAI,QACJ,MA1FF,2SA2FE,YAAa,mEACb,KAAM,OACP,CACD,CACE,GAAI,QACJ,MApFF,4ZAqFE,YAAa,4DACb,KAAM,OACP,CACD,CACE,GAAI,gBACJ,MA/CF,sTAgDE,YAAa,sDACb,KAAM,OACP,CACF,CCtJY,GAA6B,CACxC,CACE,GAAI,MACJ,MACE,4FACF,YACE,qGACF,KAAM,UACP,CACD,CACE,GAAI,MACJ,MACE,qHACF,YACE,6HACF,KAAM,UACP,CACD,CAWE,GAAI,MACJ,MACE,2GACF,YAAa,8DACb,KAAM,UACP,CACD,CAWE,GAAI,cACJ,MAAO,4DACP,YACE,oFACF,KAAM,UACP,CACD,CAOE,GAAI,aACJ,MAAO,mCACP,YACE,yFACF,KAAM,UACP,CACD,CAOE,GAAI,cACJ,MAAO,2BACP,YAAa,qEACb,KAAM,UACP,CACD,CACE,GAAI,QACJ,MAAO,0EACP,YACE,iIACF,KAAM,UACP,CACD,CACE,GAAI,aAaJ,MACE,8OACF,YACE,+FACF,KAAM,UACP,CACD,CAeE,GAAI,qBACJ,MACE,6iBACF,YACE,0FACF,KAAM,UACP,CACD,CACE,GAAI,eAUJ,MACE,qNACF,YAAa,+EACb,KAAM,UACP,CACD,CACE,GAAI,cASJ,MACE,mJACF,YAAa,+EACb,KAAM,UACP,CACD,CACE,GAAI,eAWJ,MACE,8JACF,YACE,4EACF,KAAM,UACP,CACD,CAOE,GAAI,MACJ,MACE,oGACF,YAAa,8DACb,KAAM,UACP,CACD,CAcE,GAAI,iBACJ,MACE,+KACF,YACE,2FACF,KAAM,UACP,CACD,CASE,GAAI,kBAOJ,MACE,0IACF,YACE,2GACF,KAAM,UACP,CACD,CAOE,GAAI,yBACJ,MACE,yFACF,YACE,oGACF,KAAM,UACP,CACD,CAQE,GAAI,gBACJ,MACE,+HACF,YACE,qEACF,KAAM,UACP,CACD,CAQE,GAAI,cACJ,MACE,wGACF,YAAa,mEACb,KAAM,UACP,CACD,CAOE,GAAI,cACJ,MACE,2HACF,YACE,mEACF,KAAM,UACP,CACD,CASE,GAAI,sBACJ,MAAO,mCACP,YACE,kEACF,KAAM,UACP,CACD,CAUE,GAAI,mBACJ,MACE,iIACF,YACE,uFACF,KAA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"}