Most document extraction begins by inferring structure. A Closing Disclosure is the case where inferring structure is strictly worse than reading it off the specification, because the form’s layout is prescribed by regulation and every section carries a letter you can address it by.
The form is prescribed, so use that
The Closing Disclosure was introduced by the Consumer Financial Protection Bureau under the integrated mortgage disclosure rules, and the form itself is published as a model form in the appendices to Regulation Z. The CFPB maintains the TILA-RESPA integrated disclosure resources, including the annotated forms and the completed samples, and those are the authority for any field question this page raises.
What that buys an extraction pipeline is an addressing scheme. Closing cost details are grouped under letters — origination charges, services the borrower did not shop for, services the borrower did shop for, then a total; then taxes and government fees, prepaids, initial escrow, other, and their total; and then the overall total closing costs. Those letters print on the page. They are stable across lenders, across settlement software, and across every copy of the form.
So the extraction prompt should not ask “find the appraisal fee”. It should ask for the line items under each lettered section, in order, with the section letter attached. The difference is not stylistic:
- A fee found by name has no context. The same fee name can appear in two sections on one form, and which section it sits in is what determines its regulatory treatment.
- A fee found under a section letter can be checked, because the section has a printed subtotal that its items must sum to.
- An empty section is meaningful and detectable. “No items under this letter” is a fact; “I did not find anything” is not, and only the section-addressed version can tell them apart.
This is the general argument for anchoring to a known form rather than inferring, and it applies to any document with a prescribed layout. The contrast case is a document with no fixed structure at all, where designing a schema before you have seen every variant is the harder problem. Here the variants do not exist.
Five money columns, one line
The closing cost details page is where naive extraction actually dies, and the reason is not the section letters. It is that each line has more than one amount. A single line item can be paid by the borrower at closing, by the borrower before closing, by the seller at closing, by the seller before closing, or by somebody else entirely, and the form provides a column for each:
Borrower-Paid Seller-Paid Paid by
At Closing Before At Closing Before Others
Appraisal Fee 650.00
Credit Report Fee 42.50
Flood Determination Fee 20.00
Title - Lender's Title Ins. 985.00
Title - Owner's Title Ins. 1,240.00
A schema with one amount field per line item cannot represent that page. What happens in practice is that the extraction returns the first number it finds on each line, and the resulting record says the borrower paid the $1,240 owner’s title policy that the seller actually paid, and the $650 appraisal that a lender credit covered. No section total then foots, and because the totals are also being extracted rather than derived, the mismatch is invisible unless somebody checks.
The correct shape is a line item with a column-keyed amount map:
{
"section": "B",
"description": "Appraisal Fee",
"amounts": {
"borrower_at_closing": null,
"borrower_before_closing": null,
"seller_at_closing": null,
"seller_before_closing": null,
"paid_by_others": "650.00"
},
"paid_by_others_marker": "L"
}
The marker field matters: amounts in the paid-by-others column are annotated to indicate who paid, with a letter denoting a lender-paid item, and that annotation is part of the datum rather than typographical noise. Extract it as a separate field, and never let it contaminate the numeric parse — a value read as “L 650.00” that gets stripped to 650.00 loses the only thing distinguishing a lender credit from a third-party payment.
Sections that must foot
Once amounts are column-keyed, the form checks itself. The loan-costs total is the sum of the three loan-cost sections, the other-costs total is the sum of the four other-cost sections, and total closing costs is the sum of those two totals, with lender credits shown as their own line. On a synthetic disclosure, taking the borrower-at-closing column:
A. Origination Charges 2,100.00
B. Services Borrower Did Not Shop For 1,050.00
C. Services Borrower Did Shop For 1,830.00
------------------------------------------------------
D. TOTAL LOAN COSTS (A + B + C) 4,980.00
E. Taxes and Other Government Fees 1,250.00
F. Prepaids 2,120.55
G. Initial Escrow Payment at Closing 1,435.98
H. Other 950.00
------------------------------------------------------
I. TOTAL OTHER COSTS (E + F + G + H) 5,756.53
J. TOTAL CLOSING COSTS (D + I) 10,736.53
Lender Credits (500.00)
Total, net of credits 10,236.53
Check it: 2,100.00 + 1,050.00 + 1,830.00 = 4,980.00. Then 1,250.00 + 2,120.55 = 3,370.55; + 1,435.98 = 4,806.53; + 950.00 = 5,756.53. And 4,980.00 + 5,756.53 = 10,736.53. Every one of those is a printed subtotal on the form, so each is an independent check on the line items you extracted beneath it — and a section whose items are short by exactly one item’s worth tells you which section to re-read rather than sending the whole document back.
Derive the totals rather than only extracting them, and store both. The extracted total is what the document asserts; the derived total is what your line items say. Recording the pair, and flagging disagreement, is the difference between an extraction you can trust and one you hope about. Note also the parenthesised credit: on this form negative amounts are conventionally shown in parentheses, and a numeric parser that strips punctuation turns −500.00 into +500.00 and breaks the net by a thousand dollars.
Section lettering, page ordering and the column headings described here are those of the form as published at the time of writing. The form is amended from time to time, and older disclosures in an archive will reflect the version current when they were issued. Anchor a production extractor to a form version detected from the document, and check the CFPB’s published forms rather than this page before relying on a letter.
Cash to close
The summaries-of-transactions page carries the borrower’s and the seller’s sides in parallel, each as a gross-amount block and a credits block, with the difference being cash to or from that party:
Due from Borrower at Closing 315,600.00
Paid Already by or on Behalf of Borrower 246,150.00
------------------------------------------------------
Cash to Close (315,600.00 - 246,150.00) 69,450.00
The sign is the thing to get right and the thing most easily lost. The form indicates direction with a marked box — from the borrower or to the borrower — rather than with a minus sign, so a schema storing only the magnitude loses the direction entirely, and the direction is the whole meaning on a transaction where the seller is bringing money to the table. Capture direction as its own enum field and keep the amount unsigned, rather than trying to encode both in one number.
What still breaks
- The document is often a scan of a print. Settlement agents print, sign and scan, so the copy you receive can be a skewed image with a signature across the last page. The section letters survive that far better than any positional model does, which is another argument for addressing by letter.
- Continuation pages. A transaction with many line items pushes a section onto an addendum, which repeats the section letter. Group by letter across the whole document rather than per page, or a section’s items get split into two partial groups and neither foots.
- Blank versus zero. An empty cell in a money column means the item was not paid from that column; a printed 0.00 means somebody asserted zero. Preserve null and 0.00 as different values. Coercing null to zero makes the totals foot in a way that hides a genuine misread.
- The disclosure that is not final. Revised disclosures are issued during a transaction, and two versions of the same file differ in ways that matter. Extract the issue date and any revision marking, and treat document identity as version-scoped — the same reasoning as keeping an audit trail of what produced each field, applied to the source rather than the extractor.
- It is full of personal financial data. Names, addresses, loan numbers and sometimes partial account numbers. Where the document leaves your infrastructure to reach a model, the contractual position and the redaction you apply first are part of the design, not an afterthought; see redaction before extraction.
Top comments (0)