Parsing Legal Citations in Code: Bluebook Forms, State Variants, and the Ones You Must Refuse

A legal citation is a human-typed string, and the same provision gets written a dozen ways, so a citation parser is a normalization problem before it is a lookup problem. The hard part is not 42 U.S.C. 1983. It is the hyphen that came out of a PDF and is not a hyphen, the trailing period that belongs to one state and not another, and the two-letter acronym two states both claim. Below: the shape classes that actually appear in traffic, five traps we hit and fixed, and the argument that a citator must be allowed to say no.

TL;DR

  • Citation parsing is string archaeology. Federal codes, state acronym codes, state subject codes, court rules, and colloquial names are five different grammars sharing one input box.
  • Invisible characters are the top silent failure. Six Unicode dash variants render identically to a plain hyphen, so a pasted citation looks perfect and resolves to nothing.
  • A corpus that cannot read its own published citation is the worst class of bug. Every Georgia section carried O.C.G.A. while the resolver only knew Ga. Code Ann., so the API handed out a string it then rejected.
  • Fix trailing punctuation with a retry, never a normalization. Connecticut publishes citations where the trailing period is genuinely part of the citation.
  • MCA and IC are each claimed by two states, so we answer resolved: false on purpose. For a citator, refusing is a feature and confidently wrong is the only unacceptable outcome.
  • The measured effect of these fixes: misses fell from 84 to 44, and fully working citation templates went from 40 of 62 to 50 of 62.

Three cards reading six Unicode dash variants, five silent failure traps, and refusing rather than guessing on ambiguous citations.

4-question check
Question 1 of 4

Why is trailing sentence punctuation handled as a retry instead of a normalization?

This post belongs to our legal data infrastructure series.

For related coverage, see Legal Citation Resolution: Turning a Cite Into the Right Section, Every Time for what a resolver does with a parsed string, and Hybrid Search Over 12 Million Legal Passages: Why Semantic Alone Fails on Law for where a resolved citation enters the ranking.

The problem, stated concretely

Ask ten lawyers to cite the same provision and you get several strings. All of them are correct in their own context. Your parser gets one input box.

Style manuals do not save you here. A style manual tells an author how to write a citation. It does not describe what your users will type. Users type what their state prints, what their firm's template prints, and what they half-remember.

Here is what actually arrives, grouped by grammar rather than by publisher. The forms our own resolver accepts are inventoried in the citation formats reference.

Shape classExamplesWhat the parser has to pull out
Federal statute42 U.S.C. 1983, 42 U.S.C. § 1983Title number, code token, section
Federal regulation29 CFR 1910.1200, 29 C.F.R. § 1910.1200Title number, CFR token, part and section joined by a dot
State code by compilation acronymO.C.G.A. 51-1-6, RCW 4.16.080, SDCL 15-2-14, NDCC 32-12.1-03, ORC 2305.10, NMSA 37-1-8, RSMo 516.120, HRS 657-7, CGS 52-584, CRS 13-80-102, KSA 60-513The acronym is the state's own name for its code, so the jurisdiction is implied and never written
State code by named subject codeTex. Prop. Code 24.005, Cal. Civ. Code 1542State abbreviation, subject code name, section. The subject code is load-bearing
Court ruleFed. R. Civ. P. 12, Del. Ct. Ch. R. 126A rule set, which is a different corpus from the statute book
Bare form with no jurisdictionCode 8.01-243 (Virginia), General Statutes 42a-1-101 (Connecticut)Nothing identifies the state. The state is context the string does not carry
Colloquial name"Section 230", "the CARES Act"Not a citation at all. A name for a provision

The acronym row is where most engineering time goes. Each acronym is a jurisdiction claim compressed into a few characters, and the compression is lossy. RCW is unambiguous. IC is not.

The colloquial row is where product expectations go wrong. "Section 230" is a nickname for a federal statute section, and "the CARES Act" is a public law. Neither is a citation your resolver should pretend to parse. Send those down the search path.

Five traps, and why each one happens

Every one of these was live in our resolver and is now fixed. They are ordered by how quietly they fail.

1. Invisible dash characters

Our normalizer handled two dash characters: U+2013 and U+2014. It did not handle U+2010, U+2011, U+2012, U+2015, U+2212, or U+FF0D.

Those six look like a plain hyphen in almost every font a lawyer sees. A citation copied out of a PDF or a Word brief carries one and the reader cannot tell. Every hyphen-numbered jurisdiction broke on this, which is most state codes: 51-1-6, 8.01-243, 42a-1-101, 15-2-14.

The user sees a correct citation, the API returns nothing, and no error message can help, because the string on screen is right.

Fix: normalize the whole Unicode dash family to the ASCII hyphen before you tokenize anything, then add a test that feeds each codepoint through and asserts an identical result.

2. The doubled section symbol

The federal parser accepted §§. The state parser did not.

So 42 U.S.C. §§ 1983 resolved and O.C.G.A. §§ 51-1-6 did not, and the difference was invisible to the caller because both inputs are the same shape. Two parsers that disagree about the same token is a worse bug than one parser that is wrong, because the behavior is inconsistent rather than merely broken.

The doubled symbol is a plural marker in ordinary legal writing. It appears whenever an author cited a range and then trimmed it, which happens constantly in briefs and in model output.

Fix: one symbol-handling routine, shared. A preprocessing step that exists in two places will eventually exist in two versions.

3. Trailing sentence punctuation

42 U.S.C. § 1983. failed everywhere. A citation at the end of a sentence carries the period, and users paste sentences.

The obvious fix is to strip trailing punctuation during normalization. That fix is wrong. Connecticut publishes regulation citations where the trailing period is genuinely part of the citation, for example Conn. Agencies Regs. § 38a-147 Exhibit 1. Strip it and you break a form that was correct.

So the fix is a retry, not a normalization. Try the string exactly as given. If that misses, try again with trailing punctuation trimmed. The ordering matters more than it looks: a retry can only turn a miss into a hit, and can never turn a hit into a different hit. A normalization can do both.

That asymmetry is the reusable idea. When a cleanup step might destroy a legitimate input, do not clean. Attempt, then attempt again with the cleanup applied, and take the first success.

4. Cross-corpus collisions

Del. Ct. Ch. R. 126 is a Delaware Court of Chancery rule. Our resolver returned 5 Del. C. § 126, a Delaware statute. SCR 31.08 is a court rule; the resolver returned a Wisconsin statute about dams and bridges.

Both happened because the number matched and nobody checked that the code token belonged to the corpus being searched. A statute resolver saw a Delaware jurisdiction and a section number 126 and did what it was built to do.

This is the most dangerous class in the list. It returns a plausible wrong answer with a real citation and a real source URL attached. A miss is visible; a wrong hit is not, and here it answers a rule of procedure with substantive law.

Fix: refuse another corpus's code. If the token identifies a court rule set, the statute resolver returns nothing rather than falling back on the number. Corpus identity beats numeric match, always.

5. The widened regex that started matching a fragment

This one lives in ingestion rather than resolve, and it is the subtlest. A pattern that pulls a document number out of a publisher's page was widened to cover one more state's numbering scheme. The wider alternation then matched a fragment sitting later inside a longer identifier on a different state's pages, and that fragment became part of a permanent stable id.

A forward-scanning search does not stop at the shortest match, and alternation order cannot save you. The engine scans left to right and returns the first position where any branch matches. Put the longer branch first and you still have not constrained where the scan starts. The pattern was never anchored, so it was free to succeed on a tail fragment.

Fix: anchor the pattern, or match the whole field and validate the capture against the shape you expected. Then log what each candidate document became, so a malformed id shows up in an audit record rather than in a customer's bookmark six months later.

The case study: when a citation format round-trips badly

Every Georgia statute section in our corpus carries O.C.G.A. in its citation field. That is correct. O.C.G.A. is what the Official Code of Georgia Annotated is called, and it is the form a Georgia practitioner types.

Our citation resolver did not know that string. It knew Ga. Code Ann.

So the API returned a section to a customer, the customer copied the citation field out of that response, sent it back to the resolve endpoint, and got nothing. The system published an identifier it could not consume. Zero percent of Georgia's own published citation form resolved.

Two things about how it escaped are more useful than the bug itself.

It escaped an audit because of timing. We had run a citation-form audit, and Georgia's corpus went live after it. A corpus that ships after an audit inherits none of the audit's guarantees. An audit is a photograph, and photographs go stale the moment anything ships.

So the fix could not be a sweep. A one-time repair would have closed Georgia and left the next jurisdiction to fail the same way. What we added instead is a per-jurisdiction round-trip test: take a real citation string out of the served corpus, feed it to the resolver, and assert it comes back to the same section. Adding a jurisdiction adds a test row. Shipping a corpus that cannot read itself now fails the build.

That test immediately found four more cross-state classes of the same shape.

Normalize before you match

The dash and space variants are what break naive matching, so strip them first:

import re, unicodedata

DASHES = "\u2010\u2011\u2012\u2013\u2014\u2212\uFE58\uFF0D"

def normalize(cite: str) -> str:
    s = unicodedata.normalize("NFKC", cite)
    s = s.translate({ord(d): "-" for d in DASHES})
    s = s.replace("\u00a7\u00a7", "\u00a7").replace("\u00a0", " ")
    return re.sub(r"\s+", " ", s).strip()

normalize("42 U.S.C. \u00a7\u20091983") # -> '42 U.S.C. \u00a7 1983'
normalize("O.C.G.A. \u00a7 51\u20111\u20116") # -> 'O.C.G.A. \u00a7 51-1-6'

Run it on both sides of every comparison. A parser that normalizes the query but not the stored form fails on exactly the citations a user pasted from a PDF.

Knowing when to refuse

Now the design argument, which is the part most citation parsers get backwards.

Some acronyms are claimed by two states.

FormCould meanCould also meanWhat we return
MCAMontana Code AnnotatedMississippi Code Annotatedresolved: false
ICIndiana CodeIdaho Coderesolved: false
GSambiguous across statesambiguous across statesresolved: false
GLambiguous across statesambiguous across statesresolved: false
bare RSambiguous across statesambiguous across statesresolved: false
Code 8.01-243Virginia, from contextnothing in the string says soresolved: false
General Statutes 42a-1-101Connecticut, from contextnothing in the string says soresolved: false

These are not parser gaps. They are correct refusals, and they are deliberate.

Consider what a guess costs. MCA 27-2-204 under a Montana reading and the same string under a Mississippi reading are two different provisions of two different states' law. A resolver that picks one returns a real section, with a real citation, a real source URL, and a real body of text. Everything about the response looks right except which state's law it is.

No downstream check catches that. Not a citation verifier, because the citation resolves. Not a human skimming a source link, because the link works and the text reads like law. The error survives into a filing.

Compare the cost of a miss. resolved: false sends the user back to add a jurisdiction, which takes four seconds.

For a citator, the ability to refuse is the feature. A parser that resolves 100 percent of inputs is not more capable than one that resolves 90 percent and refuses 10. It is the same parser with the safety removed.

This is the same failure mode that makes legal AI dangerous, at a different layer. A model that will not say "I do not know" invents a case. A resolver that will not say resolved: false invents a jurisdiction. Confidently wrong is the only truly unacceptable outcome here, because it is the one nobody catches.

The rule we hold ourselves to: when a citation names no jurisdiction and more than one jurisdiction claims the form, refuse. Do not rank by corpus size. Do not prefer the state the caller queried last. Do not return the most likely one with a lower confidence score, because a confidence score in a JSON response is a number nobody reads.

What the fixes measured, and where rank-1 exactness comes from

We track a set of practitioner in-state citation templates, the forms lawyers in each jurisdiction actually type. Before and after the work above:

MetricBeforeAfter
Misses8444
Fully working citation templates40 of 6250 of 62

Nearly every one of the 44 remaining misses is a correct refusal of an ambiguous form. That number will not go to zero, and it should not.

Parsing matters this much because of the search path. A resolved citation is spliced into search results at relevanceScore: 1.0 before ranking runs. That splice is the citation fast path, and it is where rank-1 exactness comes from. The rest of that pipeline is walked step by step in the hybrid search post.

Loading diagram...

When a citation does not parse, there is no splice. The string then goes down the normal retrieval path, and a symbolic citation string scored as prose by a cross-encoder ranks close to randomly. A cross-encoder is trained on meaning. O.C.G.A. 51-1-6 carries almost no meaning as text. It is an identifier wearing a costume.

The generalization for anyone building retrieval over structured content: exact-identifier lookup and semantic search are different problems, and a system that only does the second feels unreliable precisely on the queries users are most confident about. Getting a typed citation wrong costs more trust than ten mediocre semantic results.

Hybrid retrieval helps but does not close the gap. Sparse BM25 catches identifiers where a dense vector cannot, which is why both run. The identifier still competes on lexical overlap against every other section that shares a number.

A checklist for your own citation parser

Eight items. Run them against whatever you have built.

  1. Feed it every Unicode dash. U+2010, U+2011, U+2012, U+2013, U+2014, U+2015, U+2212, U+FF0D. All eight must produce the same result as an ASCII hyphen. Add the non-breaking space while you are there.
  2. Round-trip your own output, per jurisdiction. Take a citation string out of your own served data, resolve it, and assert you land on the same record. Make this a test that grows when you add a jurisdiction, not a script someone runs.
  3. Use one preprocessing routine, shared by every code path. If federal and state parsing normalize separately, they will diverge, and the divergence will be invisible from outside.
  4. Handle trailing punctuation as a retry, not a strip. Try as given, then trimmed. Never let a cleanup step destroy a form a publisher prints legitimately.
  5. Refuse another corpus's code token. A court rule must not be answered from a statute table because the number matched. Corpus identity outranks numeric match.
  6. Anchor every extraction pattern, or validate the capture. A forward-scanning search finds fragments inside longer strings, and alternation order does not prevent it.
  7. Enumerate the ambiguous forms deliberately and return a refusal for each. Write them down as a list with the reason. resolved: false with an explanation beats a guess with a confidence score.
  8. Test with strings from real documents, not strings you typed. Copy from a PDF, from Word, from a court's website, from a model's output. Your own keyboard produces only clean input, which is the input that already works.

Items 1, 4 and 8 find something in most parsers within an hour.

FAQ

Ambiguity across jurisdictions, followed by invisible characters. Acronyms like MCA and IC are claimed by more than one state, and Virginia and Connecticut print bare forms that name no jurisdiction at all. The character problem is quieter: six Unicode dash variants render identically to a hyphen, so a pasted citation looks correct and does not resolve.

Do I need the Bluebook to parse citations?

No. A style manual tells an author how to write a citation; it does not describe the strings your users actually type. Build against the forms government publishers print and the forms practitioners in each state use, then test with real pasted input. The Bluebook is copyrighted, and reproducing its tables in your code or docs is a separate problem you do not need to take on.

Why would an API refuse to resolve a citation it could probably guess?

Because a wrong guess returns a real section from the wrong state, with a working source URL and text that reads like law. Nothing downstream catches that: not a citation verifier, because the citation resolves, and not a human skimming the link. A refusal costs the user four seconds. A wrong jurisdiction can reach a filing.

How do you handle a citation with a trailing period?

Try the string exactly as given first, then retry with trailing punctuation trimmed, and take the first success. Stripping punctuation up front looks simpler but breaks Connecticut, which publishes regulation citations where the final period is part of the citation. A retry can only turn a miss into a hit; a normalization can turn a hit into a different hit.

Why does searching a citation string sometimes return poor results?

Because a cross-encoder scores meaning, and a citation carries almost none as prose. In our pipeline a parsed citation is spliced into results at relevanceScore: 1.0 before ranking, which is what makes the correct section rank first. When parsing fails there is no splice, and the identifier competes on lexical overlap with every other section sharing a number.

Resolution answers "which single section is this exact identifier," and it either succeeds or refuses. Search answers "which sections are about this," and it returns a ranked list. Keeping them as separate endpoints prevents a resolver that guesses and a search that pretends to be exact.

Can I build the citation format list myself from public sources?

Partly. Each state publishes its own code and its own preferred form, so the forms are observable, but assembling and maintaining them across 53 jurisdictions is ongoing work rather than a one-time table. Our ingestion pipeline, including the parsers, is open source at open-us-law if you want to read how the shapes are handled.

What happens if a citation resolves but the section moved?

The response carries renumberedTo and transferredTo fields, which are how you follow a citation that moved rather than treating the old identifier as dead. Sections also carry supersedes and supersededBy, and the citation enrichment recipe shows how to follow them. A resolver that only answers "found or not found" leaves you unable to trace a provision across a renumbering.

Try it against a citation you know is hard

Take the ugliest citation in your inbox, the one copied out of a PDF with a dash you cannot identify, and send it to the resolve endpoint in the playground. Then send it to a competitor. The interesting result is not which one resolves it. It is which one tells you honestly that it cannot.

The most complete US primary law API.
Every US statute, regulation, constitution, and executive order through one REST and MCP API. 4M+ sections, section-level citations, and links to the official source. Plus a free open dataset.
21 min read

New legal AI guides, weekly.

Priyansh Khodiyar

Priyansh Khodiyar

Co-Founder & CTO

Priyansh leads engineering and AI at Vaquill, from the matter workbench to drafting, document comparison, document matrix, and citation-verified research.