OCR failures on a WooCommerce site don’t look like failures. The API call returns 200, the code stores Ifjointreturn as an invoice number, and the garbage lands in order meta with the same standing as clean text. The instinct at that point is to choose an OCR engine that benchmarks better and swap it in. That instinct is what I want to argue with.
That string is real. Ida Silfverskiöld ran 93 documents through 14 OCR engines over a month and published the results on Towards Data Science. Docling produced exactly that word from a tax form, along with a 40-character blob of merged letters on other pages. Her headline finding was that no single engine wins, and that treating OCR as a routing problem beats hunting for one. For a WordPress store that ingests invoices, routing turns out to be the easy half. The half people skip is noticing that the cheap route returned garbage.
Why a better engine doesn’t fix this
Swapping mostly moves the failure. On her scatter chart most of the fourteen engines clustered in the middle, and how they broke told you more than their averages did. Docling merged words and misaligned columns. PaddleOCR fell into repetition loops and emitted Chinese template text, the string 书名:___ repeated hundreds of times, on English documents. DeepSeek-OCR returned empty output on some pages. Tesseract was the fastest engine in the whole test by a wide margin, read clean print well, and produced garbage on photographed and handwritten pages. Every one of those calls succeeded from the caller’s point of view, which is why a store can run for months on a broken pipeline.
The other reflex is to pay. Textract Structured costs around $65 per 1,000 pages in her numbers, and Amazon’s pricing page backs the arithmetic: $0.05 per page for forms and $0.015 for tables below one million pages a month. She priced sending everything through it at $6,500 per 100,000 documents, and made the sharper point that structured output added almost no text accuracy over plain Textract in her test. If you pay that markup, use the key-value pairs and table cells it returns. Paying it just to read text is a donation.
The new wave of small specialist models repeats the pattern at a smaller price. They scored well inside their comfort zone and fell over on document types they hadn’t seen, and an English test set sat outside that zone for the mostly Chinese-origin models more often than not. Sharp boundaries are acceptable when you can see them and expensive when you can’t, and the validation layer further down is how you see them.
Before you choose an OCR engine, classify the documents
Her routing table, compressed: clean high-volume print goes to Tesseract, free and fast. Mixed production documents go to a general vision model; Gemini Flash was the best all-rounder in her test. Financial tables go to a cheaper structured option such as Mistral OCR when you actually need table structure. High-stakes documents escalate to the largest model you can justify, and you accept the cost and latency on that fraction alone.
Copy it with two caveats. First, 93 documents is a small sample, and she says so herself. Benchmarks shortlist engines for you; they can’t pick the winner on your documents. Run fifty to a hundred of your own uploads through two or three finalists first. That’s an afternoon, and it beats any leaderboard.
Second, a WooCommerce store barely needs clever classification, because document class arrives as free metadata. Invoices emailed from three known supplier addresses are one class. Delivery photos uploaded from a phone are another. Bank statements from the accountant are a third. Upload source plus wp_check_filetype() routes on your actual mix, which is something no benchmark can do. It’s the same principle behind the extraction pipeline I wrote up for 4,700 PDFs: the shape of the input stream decides the architecture more than any engine’s score does.
One more thing in Tesseract’s favor: it’s the only engine in her lineup that runs comfortably on the store’s own server, and the project is plain open source. If the documents carry customer or supplier data you’ve promised not to send to third parties, local-first routing settles a privacy question that per-page APIs leave open.
The validation layer that catches silent failures
Routing picks the engine. Validation decides whether to trust what came back, and it’s the piece almost nobody builds:
// Fired by your OCR job after the first, cheap pass.
function bbioon_grade_ocr_output( string $text ): string {
$text = trim( $text );
if ( '' === $text ) {
return 'empty';
}
// Chinese template text leaking from models trained on Chinese documents.
if ( preg_match( '/[\x{4E00}-\x{9FFF}]/u', $text ) ) {
return 'wrong_language';
}
$lines = preg_split( '/\r\n|\r|\n/', $text );
// The same line over and over means the engine is stuck in a loop.
if ( count( $lines ) > 10 ) {
$unique = count( array_unique( array_map( 'trim', $lines ) ) );
if ( $unique / count( $lines ) < 0.4 ) {
return 'repetition';
}
}
// Merged-column soup has almost no usable letters in it.
$alnum = preg_match_all( '/[A-Za-z0-9]/', $text );
if ( $alnum / max( 1, strlen( $text ) ) < 0.5 ) {
return 'garbage';
}
return 'ok';
}
add_action( 'bbioon_ocr_finished', function ( int $attachment_id, string $text ): void {
$grade = bbioon_grade_ocr_output( $text );
update_post_meta( $attachment_id, 'bbioon_ocr_grade', $grade );
if ( 'ok' !== $grade ) {
as_enqueue_async_action( 'bbioon_ocr_escalate', array( $attachment_id ), 'bbioon-ocr' );
}
}, 10, 2 );
It’s all dumb checks, which is what you want. Empty output is a failure no matter what the HTTP status said. Chinese characters in an English-only store mean the model fell back to its training distribution. A unique-line ratio under 0.4 on a long document means it’s looping. Low alphanumeric density is what catches merged-column soup like that 40-character blob from her test. (I haven’t tuned that threshold on non-Latin documents, so treat 0.5 as a starting point rather than a constant.)
WooCommerce ships Action Scheduler, so the escalation runs off the request cycle in the batch loop I described before. The escalated job can call a vision model on the attachment, or just flag it for a human. Both destinations are valid, and the choice depends on volume and on how expensive a wrong invoice number is for that store.
These heuristics flag suspicion. They don’t certify correctness. A confident wrong invoice number passes all four checks. If invoice numbers follow a supplier’s pattern, two letters then six digits for example, add a field-level check for it. The layer above catches engine failure; field validation catches plausible lies.
Where the money goes
Log the engine, the per-page cost, and the grade on every attachment you process. Without that log you can’t see the thing the source keeps circling: how much clean print is paying structured prices. If four out of five pages are uniform supplier PDFs, the router’s whole job is keeping those off per-page billing, and the log is how you prove it worked.
Her cost table put full Textract Structured at $6,500 per 100,000 pages against Tesseract at effectively zero marginal cost. The figure that justifies automating anything is manual handling, which she puts at $15 to $25 per invoice. With the easy pages free and per-page money spent only where it earns something, the arithmetic stops being frightening at volume.
If you’re wiring document intake into WooCommerce and want the routing, validation, and escalation built and instrumented rather than bolted on, that’s the work I do. Send a sample of what actually lands in your uploads folder and I’ll tell you where it would break.
On a client site this week I’d run the last hundred uploaded documents through the grading function, count the failures by grade, and only then argue about engines. A few failures means fix the validation. Half the batch failing means the engine choice was always a routing decision, and now there are numbers to make it with.