fix(spacing): Refine single-char threshold to distinguish fragments from per-glyph text

The symmetric single-char check (prev==1 OR curr==1) was too aggressive
for PDFs using per-glyph positioning (each letter as a separate item),
causing word spaces to disappear ("EffectiveDate" instead of
"Effective Date").

Now distinguishes three cases:
- Asymmetric (one single-char, other multi-char): generous 0.25 threshold
  for fragment rejoining ("b"+"illion", "C"+"ultural")
- Both single-char numeric: generous 0.25 threshold for number continuity
  ("1"+"0"+"0" within per-glyph numbers)
- Both single-char alphabetic: normal 0.15 threshold to preserve word
  spaces in per-glyph Latin text

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Abimael Martell
2026-02-13 16:45:37 -08:00
co-authored by Claude Opus 4.6
parent 2c825b034d
commit b2510ffe3e
+18 -4
View File
@@ -754,13 +754,27 @@ fn should_join_items(prev_item: &TextItem, curr_item: &TextItem) -> bool {
}
}
// Single-character fragments from per-glyph positioning are almost never
// standalone words. Use a more generous threshold to rejoin split words
// like "b" + "illion", "C" + "ultural", "togeth" + "er".
if prev_chars == 1 || curr_chars == 1 {
// Single-character fragment joined to a multi-character item: use a
// more generous threshold to rejoin split words like "b" + "illion"
// or "C" + "ultural".
if (prev_chars == 1) != (curr_chars == 1) {
return gap < font_size * 0.25;
}
// Both single-char: per-glyph positioning. For numeric characters
// (digits within "100,000"), use generous threshold. For alphabetic
// per-glyph text ("E"+"f"+"f"...), use normal threshold to preserve
// word spaces between letters like "e" (end of word) + "D" (start).
if prev_chars == 1 && curr_chars == 1 {
if let (Some(p), Some(c)) = (prev_last, curr_first) {
let p_numeric = p.is_ascii_digit() || matches!(p, ',' | '.' | '%' | '+' | '-');
let c_numeric = c.is_ascii_digit() || matches!(c, ',' | '.' | '%');
if p_numeric && c_numeric {
return gap < font_size * 0.25;
}
}
}
// With accurate widths, a gap < 15% of font size means glyphs are
// adjacent (same word). Anything larger is a deliberate space.
return gap < font_size * 0.15;