diff --git a/crates/pdf/src/import_text.rs b/crates/pdf/src/import_text.rs
--- a/crates/pdf/src/import_text.rs
+++ b/crates/pdf/src/import_text.rs
@@ -217,6 +217,9 @@ pub(crate) struct LineFacts {
 
 /// The most a glyph on a curve turns from the one before it (about 20°).
 const CURVE_TURN_COS: f64 = 0.94;
+/// Uniform gaps below this (in ems) are letter spacing on one word; at or above they are spaces
+/// between one-letter words (see [`common_gap`] and #3622).
+const LETTER_SPACING_MAX_EM: f64 = 0.22;
 
 impl TextLine {
     pub fn new(at: Placement, opacity: f32) -> Self {
@@ -462,13 +465,17 @@ impl TextLine {
         // space (one set at exactly a fifth still counts, whatever the rounding). Letter spacing is
         // read only in text left in the file's order.
         let space = |gap: &Gap, common: f64| gap.width / gap.em - common > 0.199;
-        let directed = vectorcraft_text::logical_order(&t.plain_text()).is_some();
-        let mut common = if directed { 0.0 } else { common_gap(&self.gaps) };
+        let mut common = common_gap(&self.gaps);
         // Evenly spaced glyphs alone could be letters set apart or words of one letter each: the
         // shared spacing is letter spacing only on a line that also shows where its words break,
-        // with spaces it draws or with gaps wider than the rest.
+        // with spaces it draws or with gaps wider than the rest — except uniform gaps below
+        // [`LETTER_SPACING_MAX_EM`], which are per-glyph letter spacing on one word (#3622).
         if self.drawn_spaces.is_empty() && !self.gaps.iter().any(|gap| space(gap, common)) {
-            common = 0.0;
+            let on_line: Vec<_> = self.gaps.iter().filter(|g| g.on_line).collect();
+            let uniform = common > 0.0 && !on_line.is_empty() && on_line.iter().all(|g| (g.width / g.em - common).abs() < 0.06);
+            if !(uniform && common < LETTER_SPACING_MAX_EM) {
+                common = 0.0;
+            }
         }
         let mut spaces = vec![];
         for gap in &self.gaps {
diff --git a/crates/pdf/src/tests_live_text.rs b/crates/pdf/src/tests_live_text.rs
--- a/crates/pdf/src/tests_live_text.rs
+++ b/crates/pdf/src/tests_live_text.rs
@@ -101,6 +101,27 @@ fn letter_spacing_in_the_pdf_becomes_tracking() {
     assert!((ink_width(&t[0]) - expected).abs() < 0.3, "{} vs {expected}", ink_width(&t[0]));
 }
 
+/// Chromium places letter-spaced Latin as one `Tj` per glyph (~0.2 em); gaps must not become spaces
+/// (#3622).
+#[test]
+fn per_glyph_letter_spacing_on_one_line_stays_one_word() {
+    let font = vectorcraft_text::FontDb::global().find_postscript("SourceSans3-Regular").unwrap();
+    let size = 7.0_f32;
+    let tracking = 200.0_f32;
+    let extra = size * tracking / 1000.0;
+    let mut x = 20.0;
+    let mut lines = vec![];
+    for text in ["T", "R", "A", "C", "K", "I", "N", "G", " ", "B", " ", "2", "0", "2", "6"] {
+        lines.push((x, 60.0, size, text));
+        let c = text.chars().next().unwrap();
+        x += font.advance(font.glyph_for(c)) as f32 / font.units_per_em() as f32 * size + extra;
+    }
+    let d = import_with_report(&text_pdf(&lines, false), &ImportOptions::default()).unwrap().document;
+    let t = texts(&d);
+    assert_eq!(t.len(), 1, "{t:?}");
+    assert_eq!(t[0].plain_text(), "TRACKING B 2026");
+}
+
 #[test]
 fn common_tracking_does_not_turn_gaps_into_spaces() {
     let font = vectorcraft_text::FontDb::global().find_postscript("SourceSans3-Regular").unwrap();
diff --git a/crates/pdf/src/tests_realtext.rs b/crates/pdf/src/tests_realtext.rs
--- a/crates/pdf/src/tests_realtext.rs
+++ b/crates/pdf/src/tests_realtext.rs
@@ -225,16 +225,13 @@ fn kashidas_are_not_written_as_hyphens() {
         return;
     }
     let back = texts(&import_as(&pdf(&doc(vec![t]), false).bytes, TextAs::Text));
-    if back.is_empty() {
-        return;
-    }
-    assert!(!back.concat().contains(['-', '\u{2010}', '\u{0640}']), "{back:?}");
-    // Every letter comes back, at least as often as the text holds it (the justified words may
-    // come back as type objects of their own).
     let all = back.concat();
     if all.is_empty() {
         return;
     }
+    assert!(!all.contains(['-', '\u{2010}', '\u{0640}']), "{back:?}");
+    // Every letter comes back, at least as often as the text holds it (the justified words may
+    // come back as type objects of their own).
     let count = |s: &str, c: char| s.chars().filter(|&x| x == c).count();
     for c in text.chars().filter(|c| c.is_alphabetic()) {
         assert!(count(&all, c) >= count(text, c), "{c}: {back:?}");
