A 300-page policy file does not fail the way a 2-page invoice fails. It fails in one section, at one seam, on one retry that nobody watched — and the pipeline still reports a processed document, because the document is the only unit it knows how to report on.
Once a file is too long to process in one pass, the unit of work stops being the document and becomes the part. Everything downstream has to follow: the context budget is spent per part, failures happen per part, and the final answer is assembled from parts that succeeded at different times, under different conditions, with different evidence attached.
This is narrower than completion rate, which is about defining the metric and its denominator. This article is about the mechanics underneath it: how to split, how to retry the piece that failed, and how to put the evidence back together without losing track of where each value came from.
A long document is not one extraction
The first failure is conceptual. Teams keep a per-document interface long after the processing has become per-part, so the system has no vocabulary for “18 of 22 parts succeeded”. It reports a document that worked or a document that did not, and both answers are wrong.
Long files also tend to be multi-class. A loan package is an application, two bank statements, an appraisal and a set of disclosures bound into one PDF. Treating that as a single document forces one schema onto five different things, and the extraction quality collapses in the sections that do not match the assumed class.
The practical test: ask your pipeline which page a given field came from, and whether the part containing it was retried. If neither question has an answer, the document is still the unit, and every number derived from it is an average across parts you cannot see.
Split on meaning, not on page count
Fixed-size chunking is easy to implement and wrong for documents. Cutting every 10 pages puts boundaries in the middle of tables, separates a total from the rows that produce it, and splits a signature from the clause it belongs to. The split has to respect the structure the document already has.
Deep split means finding the boundaries the document declares, then falling back to heuristics only where it declares none:
- Class transitions — a page-level classifier marks where one sub-document ends and the next begins, which is the boundary that matters most in bundles.
- Structural anchors — bookmarks, numbered headings, form identifiers, footer labels and repeated headers that name a section.
- Table continuity — a table that continues across pages is one unit, and a boundary inside it must be forbidden rather than repaired later.
- Blank and separator pages — scanned bundles often keep the physical dividers the operator inserted, and they are usually correct.
- Size ceiling — only after the above, cap a part so it fits the budget, and record that this boundary was arbitrary.
That last flag matters more than it looks. A boundary the document justified and a boundary you imposed carry different risk, and only the second one needs overlap, cross-checking and a reconciliation step later. Marking them the same way means paying the reconciliation cost everywhere or nowhere.
Give every part an identity
A part that is just an index into an array cannot be retried safely, cached, or traced back to evidence. Each one needs a record that survives independently of the run that produced it:
{
"document_id": "doc_7f2a91",
"part_id": "doc_7f2a91:p0042-0057",
"page_range": [42, 57],
"content_hash": "sha256:9c1f...",
"class": "bank_statement",
"schema_version": "statement.v4",
"boundary": { "kind": "class_transition", "imposed": false },
"overlap_pages": [41],
"attempt": 2,
"status": "extracted"
}
The content hash is what makes a retry idempotent: the same bytes under the same schema version should not be re-extracted at all, and a cache hit is indistinguishable from a successful attempt. The page range is what makes evidence resolvable, because a coordinate is only meaningful next to the page it was measured on.
The context budget is spent per part
Context is a budget, and long documents make the allocation explicit. Every part competes for the same window with the instructions, the schema, the overlap from the neighbouring part and whatever carry-forward state the previous part produced.
Three allocation decisions are worth making deliberately rather than by default:
- Schema slice — send the fields that apply to this part's class, not the union of every field in the bundle, which is often the largest avoidable cost.
- Overlap window — one page of overlap at an imposed boundary is usually enough to recover a split row or a truncated clause; more is rarely free.
- Carry-forward state — a compact summary of what earlier parts established, such as the account holder or the contract parties, rather than the earlier parts themselves.
When the budget is exceeded, the failure should be a split, not a truncation. Silent truncation is the worst outcome available: the model answers confidently about a document it only partly saw, and nothing in the output records that pages were dropped.
Retry the part, not the document
Reprocessing 300 pages because page 214 timed out is the expensive habit that per-part identity exists to break. With parts addressable, retry becomes a targeted operation, and the interesting work is classifying why a part failed, because the three causes need different responses.
| Failure | Signal | Response |
|---|---|---|
| Transient | Timeout, rate limit, 5xx from the model or storage | Backoff and retry the same part unchanged; cap attempts |
| Budget | Part did not fit, or output was truncated mid-structure | Re-split into smaller parts and reprocess only those |
| Content | Unreadable scan, wrong class, schema mismatch | Stop retrying; mark the part unresolved and route it to your review queue |
Retrying a content failure is the mistake that hides in the metrics. The attempts consume budget, the outcome never changes, and the part eventually lands in whatever bucket the last attempt produced — frequently an empty result that reads like an honest “field not present”. A part that failed for content reasons should be marked unresolved and reported as such, which is a different statement from a field being absent.
Two guarantees keep this honest. Retries must be idempotent at the part level, so a duplicated attempt cannot produce a second set of values for the same pages. And the attempt count belongs in the output, because a value accepted on attempt 4 has a different history from one accepted on attempt 1.
Reassembly is where the evidence goes missing
Merging is treated as plumbing and is usually where provenance dies. Part-local results get flattened into one object, coordinates that were measured against page 3 of a part become meaningless against the document, and the resulting record cannot answer the only question an auditor asks: where did this number come from?
Reassembly has to preserve three things:
- Absolute references — translate every page index and region from part-local to document-absolute at the merge boundary, once, and never again downstream.
- Attribution per value — each field keeps the part, page and region it came from, so that the region is a contract rather than a debugging artifact.
- Conflicts, not silent winners — when two parts disagree, record both values with their sources instead of letting the last writer decide.
Overlap makes conflicts routine rather than exceptional. If one page appears in two parts, an invoice number may be extracted twice, and identical answers are a free confirmation signal. Different answers are the finding: the seam disturbed something, and that is precisely the case worth surfacing rather than resolving by array order.
Repeated furniture needs the opposite treatment. Headers, footers, page numbers and continuation labels recur in every part and will be extracted in every part. Deduplicate on the value and its role, not on the value alone, or a statement's account number becomes 22 findings.
Reconcile what crosses the seam
Some facts only exist across parts, so no single part can be checked for them. These are the cases worth an explicit reconciliation pass after the merge:
- Continued tables — a table split across a boundary needs its rows concatenated in order, with the repeated header row dropped exactly once.
- Running totals — a closing balance on the last part should be checked against the opening balance plus the movements that every part contributed.
- Carried entities — a party, account or case identifier established early and referenced later should resolve to one entity, not several near-duplicates.
- Ordering and gaps — page ranges must tile the document with no missing pages and no unintended double coverage outside declared overlap.
The gap check is the cheapest and catches the most embarrassing bug in this whole area: a document reported complete while a part silently never ran. Sum the page ranges, subtract declared overlap, compare to the page count. It is arithmetic, it costs nothing, and it fails loudly when a part is lost.
Report completion per part and per document
Splitting changes what completion means, so the metric has to change with it. A document with 22 parts where 3 are unresolved is not a failed document and not a successful one, and reporting either erases the information that decides what happens next.
Two numbers, not one. Part completion says how much of the document was processed to an acceptable standard. Document completion says whether the parts that matter for this document type all succeeded — because a missing disclosures section and a missing signature page are not equivalent losses, even though each is one part.
That second number is where this rejoins the denominator argument: once parts exist, the honest denominator is stated in parts, and the document-level rate is a policy on top of it. Keeping both makes the failure legible — which part, which page range, which failure class, how many attempts — and legible failure is the difference between a pipeline you can fix and one you can only rerun.
None of this makes long documents easy. It makes them addressable: split where the document says to split, spend the budget per part, retry the part that failed for reasons a retry can fix, and reassemble so that every value still points at the page it came from. The seams stop being where the pipeline quietly loses things and become the places it checks itself.
