Skip to content

OCR for Khmer Script With Stacked Subscript Consonants

9 min read · updated August 11, 2026

Khmer OCR output frequently renders as a flat row of consonants where the page shows a stack. The characters are individually correct. What is missing is a code point that has no visual form at all, and no recogniser that predicts one label per visible unit can ever emit it.

COENG: a character with no shape

Khmer occupies U+1780U+17FF and has thirty-three consonants, each of which has a subscript form — the coeng, drawn smaller and below the main letter to write a consonant cluster.

Unicode does not encode those subscript forms as characters. There is one control character, U+17D2 KHMER SIGN COENG, and the subscript is written as that character followed by the ordinary consonant. The renderer sees the sequence and draws the second consonant in its subscript shape beneath the first. So the string for a stacked pair is three code points — base, U+17D2, subscript consonant — of which exactly two have ink and one has none.

A recogniser trained to emit one label per visual unit therefore cannot produce correct Khmer. Shown a stack, its best available output is the two consonants side by side, which is a real Khmer string that means something different and renders differently. The failure is not low-confidence output; it is confident output from a label set that does not contain the answer, the same structural problem as the closed Hangul label set in mixed Hangul and Hanja text.

The fix is to make the recogniser predict sequences rather than symbols. A CTC or attention decoder over a line image can emit U+17D2 as a label with no corresponding ink, because it is learning a mapping from image to string rather than from segment to character. Any pipeline that segments first and classifies second will keep losing it.

Stacks break line segmentation

The second failure is geometric. A subscript hangs below the baseline; stacks of two are common, and Pali and Sanskrit loanwords produce three-deep stacks. Above the line sit the vowel signs and diacritics. Khmer typesetting is often tightly leaded, so the descending stack of one line occupies the same vertical band as the ascending marks of the line beneath it.

Horizontal projection profiling — the standard line-finding technique — cuts where the ink density falls to a minimum. On Khmer body text at normal leading, there is frequently no such minimum between lines: the ink is continuous from one line’s subscripts into the next line’s superscripts. Profile-based segmentation then either merges two lines into one or cuts through the middle of a stack, decapitating the subscript and assigning it to the wrong line.

What works instead is seam-carving or A* path-finding line separation, which finds a minimum-cost path through the page that is allowed to curve around ink rather than being a straight horizontal cut, or a learned line detector that predicts baselines directly. Both are standard in historical-document toolkits and both are worth reaching for before touching the recogniser.

The marks that change the word

Khmer carries a dense set of small marks above the consonant, and several of them are semantically load-bearing rather than decorative:

  • Register shifters — muusikatoan U+17C9 and triisap U+17CA. Khmer consonants belong to one of two registers, which determines how a following vowel sign is pronounced; these two marks switch a consonant to the other register. Losing one does not misspell a word, it changes which word it is.
  • Nikahit U+17C6 and reahmuk U+17C7 — final nasal and aspirate, small circles and dots above and after the letter.
  • Toandakhiat U+17CD — marks a letter as silent. Dropping it inserts a syllable that is not pronounced.
  • Bantoc U+17CB and robat U+17CC — vowel shortening and a historical r, both a few pixels at body-text sizes.

Like Hebrew niqqud, these are small isolated connected components and are removed by despeckling before recognition. And like niqqud, their absence does not lower any confidence score; the output is well-formed Khmer that says something else. Turn noise removal off and scan higher, for the reasons set out in OCR for Hebrew with and without niqqud.

Two dependent vowels also render on both sides of their consonant — U+17C4 and U+17C5 — so Khmer has the split-sign reassembly problem as well, on top of everything above.

Why NFC does not help

A Khmer orthographic cluster can contain a base consonant, one or two COENG sequences, a vowel sign and one or more diacritics. Several orderings of those elements render identically, so two transcriptions of the same page can differ as strings while being visually and semantically the same text.

The usual answer to that is Unicode normalisation, and it does not apply. Canonical reordering in NFC only reorders combining marks that carry a non-zero canonical combining class; most Khmer combining marks carry class 0, so the normaliser leaves the sequence exactly as it found it. NFC on Khmer is close to the identity function for this purpose.

The practical consequence is that you need your own canonical ordering pass — a documented rule such as base, then COENG sequences in stack order, then vowel, then diacritics — applied to both your output and your ground truth before any comparison, index write or embedding call. Without it, character error rate on a good model can read several points worse than it is, and an exact-match search will miss documents it contains.

No word spaces, and what that leaves you

Khmer does not put spaces between words. Spaces mark phrase and clause boundaries, roughly where English would use a comma. So even a perfect transcription is an unsegmented run of characters, and word segmentation is a separate model applied afterwards — the same situation as Thai, treated in OCR for Thai without word spaces.

Digital Khmer text often carries zero-width spaces, U+200B, at word boundaries to help line breaking. OCR never produces them, because they are not on the page. If your downstream index or line-breaking logic assumes they are present, Khmer arriving from OCR will behave differently from Khmer arriving from a web form, and the difference is invisible in any inspection that renders the text rather than dumping its code points.

  1. Scan at 600 dpi greyscale; disable despeckling.
  2. Segment lines with a seam-carving or learned baseline detector, never a horizontal projection profile.
  3. Recognise whole lines with a sequence model whose label set includes U+17D2 and every diacritic in U+17C6U+17DD.
  4. Apply your documented cluster ordering to the output.
  5. Segment words with a Khmer segmenter and insert U+200B if downstream consumers expect it — as a separate, reversible step.
  6. Evaluate on grapheme clusters after ordering, and report the rate at which COENG sequences were produced at all as its own number.