PDFs Are Where Good Data Goes to Die
A number is born in a clean spreadsheet. Then someone pastes it into a report, wraps it in three paragraphs of prose, exports a PDF, and buries it forever. No markup. No boundary. Nothing telling a machine where the story stops and the numbers start.
I built a version of this for FDA regulatory submissions, and the failure that actually bites is never "can't read the PDF." It is a lazy parser swallowing the paragraph between two tables, shifting its header by one row, and quietly corrupting every number after it. Nobody notices until an audit. That is the expensive kind of silent.
tablextract finds the tables, ignores the prose, and answers your question with a citation back to the exact row and column. You get the number and the proof of where it lives.
It Reads Columns, Not Just Lines
The trick is looking at shape, not position.
• Block detection: it splits each line on pipes or runs of spaces, then groups the runs that share a column count into a real table. A lonely sentence never gets to pretend it is a data row.
• Confidence scoring: it keeps only the rows that match the block's dominant column count, so the prose that sneaks in gets thrown out on sight.
• Cited answers: ask a plain-English question and it returns the value plus the row label, column, and source it came from. Not a naked number you have to go verify yourself.
• Real PDFs: an optional pdfplumber path takes raw bytes over the API and returns a clean 400 on garbage input instead of exploding with a 500.
The Ugly Case Is the Whole Point
So the benchmark ships the ugly case on purpose: prose jammed directly between two tables, the exact shape that wrecked pipelines I worked on. A naive line-by-line reader merges that paragraph into the table, moves the header, and poisons every cell lookup that follows.
The base64 PDF endpoint taught me a smaller lesson the hard way. It first took the encoded file as a query parameter, which silently truncates a real document because URLs have length limits. Moving it into the request body killed a bug that only ever shows up in production, never in the tidy little local test with a two-page file.
The Number
On the bundled document (adverse-event and dosing tables wrapped in prose), the naive extractor scores 0% cell accuracy. Zero. tablextract scores 100%, 8 of 8, every value traceable to its source.
17 tests green across the CI matrix, plus a live Docker smoke test that boots the service and calls the real extract endpoint end to end.