Extracting Analyte Results From a Multi-Page Lab Panel
11 min read · updated August 11, 2026
A comprehensive panel runs to four or five pages. Page one has a patient banner, a specimen block, a collection time and a column header. Pages two onward have a thin footer and a wall of numbers, and if you process pages independently you will produce a hundred results that belong to nobody.
Why page two is the hard page
Page-parallel extraction is the obvious design. It is fast, each page fits comfortably in context, and failures are isolated. It is also wrong for this document, because a lab report is not a set of pages — it is one report that happens to be paginated, and almost everything that identifies a result lives on the first page only.
Three specific things go missing after page one. The patient and specimen identity, printed once in the banner. The column definitions, because continuation pages frequently omit the header row entirely and the fourth column is only a reference range because page one said so. And the collection datetime, which is the timestamp every result on every page inherits — and which is not the report datetime printed in the footer, a distinction that matters because the two can differ by days on a send-out.
There is a fourth problem that only appears in scanned input: page order is not guaranteed. A duplex scanner that misfeeds, a stapled report photographed out of sequence, or a merge that interleaved two reports all produce a file whose page order is not the report’s order. The Page 2 of 5 footer is the only in-document evidence, and it is worth extracting for that reason alone.
The state a page inherits
Model the extraction as a small state machine. There is document-level state, established once, and page-level content that inherits it:
DocumentContext (established on the first page that carries it) patient_key internal pseudonymous id, never the name specimen_id accession or specimen number, as printed specimen_type serum, plasma, whole blood, urine collected_at datetime with timezone performing_lab name and, where printed, CLIA identifier page_count from "Page N of M" columns ordered list, from the page-one header row PageResult (produced per page) page_number header_present did this page repeat the column header? banner_present did this page repeat the patient banner? rows[] test, value, unit, range, flag section_headings[] "CHEMISTRY", "HEMATOLOGY", ...
Two fields there earn their place. header_present lets you decide per page whether to use that page’s own column geometry or to inherit page one’s, rather than guessing. banner_present is your detector for a second report having been concatenated into the same file: a banner appearing on page three, with a different specimen number, means the document contains two reports and the naive carry-forward is about to attach one patient’s results to another. That is the single worst outcome available in this task and it is detectable in one boolean.
patient_key through the pipeline rather than the name and identifiers. Under HIPAA’s minimum necessary standard, the join key does not need to be the identifier, and keeping the identifier out of the per-result records means the large, widely-read results table is not itself a store of direct identifiers.The method, step by step
- Split the file into pages and keep the index. Do not concatenate the text first. Page boundaries are the only evidence you have for the carry-forward, and once the pages are joined into one string the footer of page two is indistinguishable from a mid-table row.
- Extract the footer pagination from every page before anything else. A short, cheap pass looking only for a
Page N of Mpattern. Sort pages by N, assert that the set of N is exactly 1..M with no gaps and no duplicates, and stop the document if it is not. This is a five-line check that catches misordered scans, missing pages and accidental duplicates before any expensive work happens. - Extract the document context from the first page that has a banner. Usually page one. Emit it as its own object with its own schema, separate from the results, so that a failure here is a document-level failure rather than a hundred bad rows.
- Record the column geometry from the header row. Capture the horizontal position of each column boundary, not just the column names. On a continuation page with no header, the boundaries from page one are what let you assign a token in the fourth zone to the reference range rather than to the unit. The failure this prevents is column misalignment across pages.
- Extract each page’s rows independently, with the context injected. Pass the column definitions and the specimen type into the per-page prompt or parser. Do not pass the patient identifiers: the page does not need them to parse, and every copy you make of them is a copy you have to account for.
- Detect banner repetition and specimen change. If a page carries a banner whose specimen identifier differs from the document context, close the current document and open a new one at that page. Report it as a split rather than absorbing it — a file holding two reports is the case multi-entity document schema design exists for.
- Join, then validate as a whole. Attach the document context to every row, then run the panel-level checks in the next section. A row that passed its own validation can still be wrong in the context of the assembled panel, and that is the check the page-parallel design cannot do at all.
The one prompt-level instruction that matters
On continuation pages, instruct the extractor to return rows in the order they appear and to include a row for anything it can see in the test-name column even when the remaining cells are empty. An empty row is information — it is usually a section heading, a comment line or a wrapped test name — and dropping it is what causes the off-by-one alignment failure described in extracting reference ranges. Getting a null-heavy row you then discard is cheap; recovering a row that was never emitted is impossible.
Reconciling the assembled panel
Once the pages are joined, four checks are available that no individual page could perform:
- Duplicate analyte detection. The same LOINC concept appearing twice within one specimen is either a repeat measurement, which should carry a different result time, or evidence that a page was processed twice. Compare the values: identical values on identical pages is a duplicate page, and different values with no time difference is a genuine anomaly for review.
- Panel completeness. A named panel has a known membership — a basic metabolic panel has a fixed set of analytes — so a panel heading with a missing constituent points at a dropped row rather than at a test that was not performed. Treat it as a prompt to look, not as an error.
- Calculated fields. Several results are arithmetic functions of others on the same page, and that arithmetic is a free end-to-end check on your extraction. An anion gap is sodium minus chloride minus bicarbonate; a globulin is total protein minus albumin. Recompute and compare to the printed value. If the printed derived value disagrees with the arithmetic on your extracted inputs, one of the three numbers was read wrong, and you know which page to look at.
- Result count against any printed total. Some reports print a count of tests performed. Where they do, it is the cheapest completeness check in existence.
The calculated-field check is the strongest of the four because it is the only one whose failure implicates a specific value. Everything else tells you that something is wrong somewhere.
What this costs to run
The arithmetic is worth doing before you commit to a design, and it is arithmetic you can do from your own assumptions rather than from anyone else’s quoted figure. Take a labelled set: 8,000 reports averaging 4 pages, so 32,000 pages. Assume, as a working figure you will replace with your provider’s published rate, a per-page cost of P for the vision pass. The pagination pre-pass in step 2 is a second read of the same page, so a naive implementation doubles the page count to 64,000.
It does not have to. The pagination pattern is in the footer, which is a small crop of the page and, on a text-layer PDF, is available without any model call at all. Splitting the pipeline so that step 2 runs on extracted text or on a footer crop, and only the results table goes to the vision model, keeps the page count at 32,000 and turns the document-integrity check into a rounding error. The general shape of per-image cost is in image token pricing, and detail levels — which change the token count per page substantially — in image detail levels.