Your agent turns a table into a stream of loose numbers
The grid is a property of the page, not of the text. Extract the text naively and the rows stop lining up with the columns.
There is a table on page 12 and you asked for the figure in the third column. A figure comes back. It is a real figure from that table, in the wrong row, and nothing in the answer says so.
What arrived was not a table. It was the values from the table, run together into a sequence: the headers, then the cells in whatever order they came out, with the one thing that carried the meaning gone. A cell means what it means because of the row and the column it sits in. Strip the grid and every number is just a number.
This is the failure mode that gets past review, because there is nothing to review. The answer is fluent, the number is real, and the arithmetic your agent does with it afterwards is arithmetic on the wrong cell.
A PDF does not contain rows or columns. It contains glyphs, each with a position on the page, and a text extractor returns them in the order they were written into the file. That order is whatever the program that generated the PDF happened to emit, and it is not the order a person reads. A two-column page can come back with the two columns interleaved line by line. A table can come back column by column, so the first row of your output is the first column of the page.
Turning those positions back into a grid means clustering coordinates: values that share an x range are a column, values that share a y range are a row. Every library that does this does it by heuristic, and the heuristics break on the things real tables are full of. A merged header cell. A value that wraps onto two lines. A column separated by whitespace instead of a ruled line. A footnote sitting between two rows. When the clustering guesses wrong it does not fail, it produces a different table, well formed and wrong.
On a scanned table there are no coordinates to cluster in the first place. There is one image of a page, and the grid has to be recovered from pixels: that is character recognition and layout analysis at once, which is a model, not a parsing library.
exact skips the local text extraction, which is the step that flattens the grid, and sends the pages to an engine. pages keeps the work to the range that has the table. Read engine and used_fallback in the response: they tell you which of the two engines answered, and that is what decides whether the grid survived.
upload(filename="report.pdf", content_type="application/pdf")
-> key, upload_url
curl -T report.pdf "<upload_url>"
read(key="<key>", mode="exact", pages="12-14")
-> handle, engine, used_fallback, cost_eur, structure
result(handle="<handle>", pages="12")
-> that page, in reading order
result(handle="<handle>", find="Total")
-> excerpts, each with the page it falls onexact costs 0.016 EUR per page and is the mode to ask for on a dense table. balanced costs 0.007 EUR per page and tries the local text layer first, which is free and which is exactly the path that loses the grid. On a table the free attempt is not a saving, it is the failure you came here to avoid.
pages narrows the work to the range that matters, so a table in a two hundred page report is a read of three pages and not of two hundred. Pages are counted from the file itself, never estimated from its size, and result is free however many times you page back through what was read.
The same file read twice is charged once: it is recognised by its content, not by its name. A read that fails is not charged at all.
- Table structure is not part of what the mode guarantees. Every mode has a contract that any engine in it must satisfy, and the grid is not in it: what is guaranteed is the text, and the page each line came from. The first engine reconstructs tables and the backup returns the page flat, at the same price.
engineandused_fallbackin the response say which one you got, on every call, and that is deliberate: a fallback you cannot see is a weaker result you paid full price for. - You get rows of text, not a spreadsheet. There is no cell you can address by coordinates, and no CSV.
structurereports what was found, andstructure_sourcesays whether that structure was read from the document or inferred from the text. Inferred structure is a good guess about where a section starts, nothing stronger.- A table split across a page break comes back as two tables, one per page. Rejoining them is your side of the work.
- A spreadsheet is a file whose table is already a table: read it as one, without paying anyone. This page is for the case where all you have is the PDF.
Without markup
This page in markdown: /problems/keep-a-table-a-table.md. All 20 of them in one file: /llms-full.txt.