Extracting Structured Fields From a Vehicle Title Document
10 min read · updated August 11, 2026
A vehicle title is a short document that decides whether a car can be sold, insured or financed. Three of its fields — the brand, the odometer disclosure and the lien — are the ones that decide it, and all three break the same way: they are extracted as strings, and a string cannot express the difference between “no” and “the document does not say”.
The VIN is the join key, not the title number
Begin with the field that connects this document to every other one. A title carries a title number, and it is tempting to key on it because it is short, printed clearly, and unique on the face of the document. It is the wrong key. A title number is issued by one state and is replaced whenever the title is: a duplicate title, a transfer to a new owner, or a move across a state line all produce a new number for the same vehicle. Two titles for the same car will not share it.
The VIN will. It is the only identifier that appears on the title, the registration card, the insurance declaration, the inspection report, the lien filing, the auction sale receipt and the manufacturer’s recall record — which makes it the join key for the whole file, and makes an error in it expensive in proportion to how many documents you hold. Validate it with the published check-digit arithmetic before it enters the database, and then compare it character by character against the VIN on every other document for the same vehicle. Two documents that disagree on one character of a VIN are far more likely to be one extraction error than two vehicles.
Brands are a state vocabulary, not an enum
A title brand is a permanent notation that the vehicle has been damaged, recovered, rebuilt or otherwise compromised. The common ones are salvage, rebuilt or reconstructed, flood or water damage, hail, junk or non-repairable, lemon or manufacturer buyback, and odometer discrepancy. That list will tempt you into an enum. Resist it, for the reason set out under schema design for unseen variants: titles are issued by states, and each state defines its own brand vocabulary and its own threshold for applying one. The same physical car can carry a word in one state that has no equivalent in the next, and two states can use the same word for materially different conditions.
The US Department of Justice operates the National Motor Vehicle Title Information System precisely because of this, and NMVTIS publishes a mapping between the brands states report and a standard set. Use it as your normalisation source rather than writing your own mapping, and check its current version — the reporting standard is revised.
The extraction consequence is that a brand field must be two fields. Keep the verbatim string the document actually bears, along with the issuing state, and keep a normalised code derived from it. The verbatim string is what you can defend; the normalised code is what you can query. Collapsing them loses the ability to answer “what did the document actually say” six months later, and on a branded title that is exactly the question somebody asks.
The subtler failure is a clean title. Most titles carry no brand at all, and the brand area is simply blank or absent from the form layout. An extractor that returns null for brand on a clean title and null when the brand area was obscured by a fold has told you nothing. Return an explicit brands: [] when the brand area was read and empty, and a distinct brand_area_unreadable flag when it was not read.
Odometer disclosure has three states
Federal odometer disclosure requirements sit in 49 CFR Part 580, and the form is designed around a mileage figure plus two checkboxes. The resulting field has three legitimate values, not one number:
- Actual mileage. Neither box is ticked; the figure is the vehicle’s real mileage.
- Not actual mileage. The disclosure carries an odometer-discrepancy warning. The figure on the form is still a reading; it is not the vehicle’s mileage.
- Exceeds mechanical limits. The odometer rolled over. A five-digit odometer reading 034,210 means 134,210 or 234,210, and the document does not say which.
A schema with a single integer odometer field cannot represent the last two, and the arithmetic consequence is severe: a vehicle whose odometer exceeded mechanical limits will look like a low-mileage car to every downstream valuation, pricing or fraud check you run. Model this as a mileage value plus a required status enum, and make the status field non-nullable so that an extraction which failed to read the checkboxes cannot quietly default to actual — the general treatment is under missing required field handling.
Older vehicles are exempt from the disclosure entirely, and the exemption period is the part of this page most likely to be out of date by the time you read it. NHTSA amended the exemption in 2021, extending it for newer model years; read the current text of 49 CFR 580.17 rather than trusting a remembered figure.
Unreleased and absent are different answers
A title records any security interest against the vehicle: the lienholder’s name and address, the date the lien was recorded, and a separate release section with a signature and date. A vehicle with an unreleased lien cannot be sold clean, so this is the field that stops a transaction.
It needs three values, for the same reason as the brand field. There is no_lien_recorded, where the lien section was read and is empty. There is lien_recorded_released, where a lienholder is named and the release section carries a date and a signature. And there is lien_recorded_not_released, where a lienholder is named and the release section is blank. A boolean has_lien collapses the first and third into different answers to the same question and gives you no way to distinguish the second from either.
The specific failure worth guarding against is the release section being read as a release because it exists on the form. Every title has the section printed; almost none of them start with it filled in. The test is whether it contains a date and a signature mark, not whether the heading was found. Signature presence is a genuinely hard visual question — a scrawl over a printed line at low resolution is often ambiguous — so it belongs in the small set of fields you route to human review by default rather than by confidence score. Where the release date is present but the signature is doubtful, record both observations rather than resolving them.
What the record has to hold
Putting the three together, with the verbatim-plus-normalised pattern applied consistently:
{
"vin": "1M8GDM9AXKP042788",
"vin_checksum": "pass",
"issuing_state": "TX",
"title_number": "T7719284005",
"issue_date": "2026-02-19",
"brands": [
{ "verbatim": "REBUILT SALVAGE", "normalised": "rebuilt", "state": "TX" }
],
"brand_area_read": true,
"odometer": { "value": 118442, "unit": "mi", "status": "actual" },
"lien": {
"status": "lien_recorded_not_released",
"holder_name": "Example Credit Union",
"recorded_date": "2024-06-03",
"release_date": null,
"release_signature_observed": false
}
}Every field that could be absent has a way to say so that is not the same as saying no. That is the whole design, and it is worth applying before you tune a prompt, because no amount of prompting fixes a schema that cannot express the answer. For how far to take this on a document type you will only ever see a few hundred of, see schema design at low volume, and for keeping the behaviour stable as you edit prompts, see extraction prompt regression tests.