← All notes
Document & OCR Automation7 min

When OCR turns zeros into O's: designing for input that is already wrong

OCR input arrives pre-damaged — lens, compression, recognizer — so the design question is repair versus refuse, not prevention. Zero-versus-O substitutions only become safe when a manufacturer catalog confirms the variant; column swaps that make quantities absurd go to a human. Confidence without a confirming source is how bad orders ship.

The document is not the data

It helps to be precise about what a PDF actually is in this workflow. It is a rendering of a record that lived somewhere else, in a vendor's system, in a structured form. By the time it reaches us it has been flattened into marks on a page, possibly printed and rescanned, and then re-derived into text by a recognizer that is guessing.

So there are two distinct kinds of error to design for. There is the parser misreading a document that is intact — a column ordering it did not expect, a code sitting in the wrong field. And there is the document itself carrying characters that were never in the original record. The first is recoverable by reasoning about layout. The second is only recoverable by reasoning about the character set, and sometimes not at all.

Conflating them produces bad fixes. Layout logic applied to a character-level corruption will confidently do the wrong thing, because the layout is fine — it is the glyph that lied.

Zero and the letter O

The specific corruption we hit most is zero read as capital O, and occasionally the reverse. In a part code this is fatal in a quiet way: the string is still the right length, still the right shape, still looks like a valid code to a human skimming a screen. It just does not exist.

You cannot resolve this locally. Looking at the string alone, there is no way to know whether the O is a real letter in a real code or a scanner artifact — both are common in manufacturer catalogs. The only thing that resolves it is the catalog itself. So the repair is not 'replace O with 0.' The repair is 'generate the variant, look it up, and adopt it only if the manufacturer actually sells that part.'

This is why the character-level fix and the confirmation rule are the same mechanism. A substitution pass without a lookup is a string mangler. With a lookup, it becomes a constrained search over a known-valid space, which is a completely different reliability profile.

Failure shapes we've had to handle
ShapeWhat it looks likeWhy it's dangerous
Code leads description`9745299 CARTRIDGE ASSY`The part column is empty or holds a stock number
Code buried mid-text`...for use with LG974 series`Position-based extraction misses it entirely
OCR digit swap`O745299` for `0745299`Looks like a valid code and matches nothing
Legitimately numeric`9745299` is the real partNaive filters discard the correct answer
Swapped columnsQty 249.99, price 2Passes every type check and is wrong

Each shape ships as its own recovery pass, with a test built from the purchase order that exposed it.

When the part number is legitimately all numbers

One of the cheapest-looking heuristics in this domain is to treat an all-numeric code as a vendor stock number rather than a manufacturer part. It is cheap because it is usually true, and it is dangerous because plenty of real parts are all digits. A rule that discards them discards real orders.

This is where the gating check earns its keep. The question a pass asks is not 'does this look like a stock number' in isolation, but whether the line's existing code is unusable — and a numeric string that resolves in the manufacturer database is not unusable, it is correct. Lookup comes before classification, always. Classify first and you will throw away data that would have matched.

The general lesson: in document pipelines, any heuristic that decides what a field means by looking at its shape should be checked against the authoritative data before it is allowed to act. Shape is a hint. Membership is a fact.

Columns that swap

A different class of damage entirely: some vendor templates order quantity and unit price the opposite way from everyone else. Nothing is corrupted here. Every character is correct. The document is simply laid out differently, and the parser mapped positions to fields the way it usually does.

The symptom is line items that are individually well-formed and collectively ridiculous — an order for eight hundred of something at a unit price of three. A human spots it instantly, which is a hint about where the check belongs. Some errors are best caught by validating the relationship between fields rather than any single field, and relationship checks are cheap to write and easy to explain.

The right response to this one is not silent repair. Swapping the values back is a guess about intent, and it is the kind of guess that would occasionally be wrong on a legitimate bulk order of an inexpensive fitting. It goes to review with the anomaly named, and a person decides.

Repair or refuse

Every failure mode gets sorted into one of two buckets. Repairable means there is a mechanical transformation and an authoritative source to confirm it against — the OCR substitution qualifies, because the catalog can confirm the result. Not repairable means any fix would be an inference about what someone meant, and those go to a human with the specific problem stated.

The line between the buckets is the existence of a confirming source, not the confidence of the heuristic. We have declined to auto-fix things we were fairly sure about, because 'fairly sure' with no way to verify is exactly the profile of an error that survives all the way to an outgoing purchase order.

Designing for garbage input mostly means being honest about which garbage you can actually clean. The pipeline that admits it cannot resolve something, and says so on screen, is more useful than the one that resolves everything and is occasionally, invisibly, wrong.

Questions

How should a PO pipeline handle O vs 0 OCR errors in part codes?
Do not blindly replace characters. Generate candidates, look them up in the manufacturer catalog, and adopt a variant only if that part is actually sold. Substitution without membership checks is string mangling.
Why is shape-based classification of part numbers dangerous?
Heuristics like "all digits means vendor stock number" are usually true and still discard real numeric manufacturer parts. Look up before you classify; membership in the authoritative list beats local shape guesses.
When should the system refuse to auto-repair?
When any fix is an inference about intent without a confirming source — e.g. silently swapping quantity and unit price. Flag the relationship problem for a person with the document in front of them.

Sources

  1. PO processing tool case study
  2. Tesseract OCR documentation
  3. Apple Vision — text recognition

Have something to build?

Tell us what you're working on and we'll tell you honestly whether we're the right fit.

Work with us