FAILURE MAP
← Case archive

FA-80161 / Typography line breaking / Open access

Kerning and ligature line measure: kerning after ligature · case 01

Punctuation after a ligature is kerned against a hidden component glyph.

Verified by executionVariant 1 · 8 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

After emitting a ligature the previous-glyph state is set to its last component.

THE FAILURE

After emitting a ligature the previous-glyph state is set to its last component.

Unsuccessful approach: Using the first component is the same mistake with a different pair.

Case contract

Input [words, width]. Glyph widths: i,l,j,.,, 4; t,f 6; m 15; w 14; M 16; W 18; other lowercase 10; other uppercase 13; space 5. Within a word, fi/fl/ff form ligatures (11/11/12) with no kerning into or out of them; case-sensitive kern pairs apply between adjacent glyphs; "|" is a non-joiner (invisible, blocks ligatures and kerning). No kerning across spaces. Greedy fill. Return [[text, width]].

Why this case matters

Line breaking decides where paragraphs wrap on screen and in print; a wrong decision point shifts every following line.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(x):
    words, width = x
    W = {'i': 4, 'l': 4, 'j': 4, '.': 4, ',': 4, 't': 6, 'f': 6, 'm': 15, 'w': 14, 'M': 16, 'W': 18}
    LIG = {'fi': 11, 'fl': 11, 'ff': 12}
    KERN = {'AV': -3, 'VA': -3, 'To': -2, 'Ta': -2, 'Yo': -2, 'LT': -3, 'Wa': -2, 'av': -1, 'va': -1, 'y.': -2, 'r.': -2, 'l.': -1, 'f.': -1, 'Va': -2}
    SPACE = 5
    def glyph(c):
        return W.get(c, 13 if c.isupper() else 10)
    def measure(w):
        total, prev, i = 0, None, 0
        while i < len(w):
            if w[i] == '|':
                prev = None
                i += 1
                continue
            pair = w[i:i + 2]
            if pair in LIG:
                total += LIG[pair]
                prev = w[i + 1]
                i += 2
                continue
            g = w[i]
            total += glyph(g)
            if prev is not None:
                total += KERN.get(prev + g, 0)
            prev = g
            i += 1
        return total
    def line_width(ws):
        return sum(measure(t) for t in ws) + SPACE * (len(ws) - 1)
    lines, cur = [], []
    for w in words:
        cand = cur + [w]
        if not cur or line_width(cand) <= width:
            cur = cand
        else:
            lines.append(cur)
            cur = [w]
    if cur:
        lines.append(cur)
    return [[' '.join(t.replace('|', '') for t in ln), line_width(ln)] for ln in lines]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('ligature then period', [['fl.', 'fi,'], 200], [['fl. fi,', 35]]), ('regression: kerning after ligature', [['fi', 'fl.', 'AVAIL', 'fly.', 'Tofu'], 100], [['fi fl. AVAIL', 95], ['fly. Tofu', 65]]), ('regression: kerning after ligature', [['staff.', 'LA|VALLE', 'fl.', 'shelf|life', 'our'], 95], [['staff.', 42], ['LAVALLE', 88], ['fl. shelflife', 84], ['our', 30]]), ('regression: kerning after ligature', [['VIA', 'flow', 'dry.', 'y.', 'AVAIL', 'fl.', 'av'], 42], [['VIA', 39], ['flow', 35], ['dry.', 32], ['y.', 12], ['AVAIL', 59], ['fl. av', 39]]), ('case-sensitive pair', [['Av', 'av'], 200], [['Av av', 47]]), ('non-joiner blocks ligature', [['shelf|life'], 200], [['shelflife', 64]]), ('control layout', [['Av', 'office', 'Ta', 'LA|VALLE', 'AVAIL', 'Wavy', 'Vat'], 99], [['Av office', 74], ['Ta', 21], ['LAVALLE', 88], ['AVAIL', 59], ['Wavy Vat', 77]]), ('control layout', [['fly.', 'office', 'Vat', 'AT', 'Av', 'our'], 57], [['fly.', 23], ['office', 46], ['Vat', 27], ['AT Av', 54], ['our', 30]])], [('regression: kerning after ligature', [['fl.', 'staff.', 'flow', 'Tofu'], 109], [['fl. staff. flow', 102], ['Tofu', 37]]), ('regression: kerning after ligature', [['staff.', 'wolf|like', 'Ta', 'Tofu', 'flow'], 94], [['staff.', 42], ['wolflike Ta', 88], ['Tofu flow', 77]]), ('regression: kerning after ligature', [['LA', 'staff.', 'Av'], 120], [['LA staff. Av', 101]]), ('regression: kerning after ligature', [['fi', 'staff.', 'fi', 'fi'], 118], [['fi staff. fi fi', 90]]), ('case-sensitive pair', [['Av', 'av'], 200], [['Av av', 47]]), ('non-joiner blocks ligature', [['shelf|life'], 200], [['shelflife', 64]]), ('control layout', [['VIA', 'AVAIL', 'office', 'Av'], 112], [['VIA AVAIL', 103], ['office Av', 74]]), ('control layout', [['fi', 'Ta', 'Ta', 'Tofu', 'y.'], 103], [['fi Ta Ta', 63], ['Tofu y.', 54]])], [('regression: kerning after ligature', [['fly.', 'staff.', 'y.', 'raffle', 'av', 'raffle'], 99], [['fly. staff. y.', 87], ['raffle av', 70], ['raffle', 46]]), ('regression: kerning after ligature', [['staff.', 'AVAIL', 'LA', 'LA|VALLE'], 42], [['staff.', 42], ['AVAIL', 59], ['LA', 26], ['LAVALLE', 88]]), ('regression: kerning after ligature', [['fl.', 'Wavy', 'our', 'VIA', 'Tofu', 'staff.', 'staff.'], 58], [['fl.', 15], ['Wavy', 45], ['our', 30], ['VIA', 39], ['Tofu', 37], ['staff.', 42], ['staff.', 42]]), ('regression: kerning after ligature', [['shelf|life', 'raffle', 'av', 'our', 'staff.', 'y.'], 43], [['shelflife', 64], ['raffle', 46], ['av', 19], ['our', 30], ['staff.', 42], ['y.', 12]]), ('ligature then period', [['fl.', 'fi,'], 200], [['fl. fi,', 35]]), ('case-sensitive pair', [['Av', 'av'], 200], [['Av av', 47]]), ('control layout', [['Ta', 'raffle', 'Yo'], 80], [['Ta raffle', 72], ['Yo', 21]]), ('control layout', [['fly.', 'shelf|life', 'AT', 'flow', 'av'], 91], [['fly.', 23], ['shelflife', 64], ['AT flow av', 90]])], [('regression: kerning after ligature', [['our', 'av', 'staff.', 'Av'], 56], [['our av', 54], ['staff.', 42], ['Av', 23]]), ('regression: kerning after ligature', [['av', 'fl.', 'Tofu', 'flow'], 112], [['av fl. Tofu', 81], ['flow', 35]]), ('regression: kerning after ligature', [['fl.', 'wolf|like', 'Wavy', 'Vat'], 50], [['fl.', 15], ['wolflike', 62], ['Wavy', 45], ['Vat', 27]]), ('regression: kerning after ligature', [['flow', 'av', 'av', 'wolf|like', 'fl.', 'Av'], 88], [['flow av av', 83], ['wolflike fl.', 82], ['Av', 23]]), ('non-joiner blocks ligature', [['shelf|life'], 200], [['shelflife', 64]]), ('ligature then period', [['fl.', 'fi,'], 200], [['fl. fi,', 35]]), ('control layout', [['AT', 'flow', 'raffle', 'wolf|like', 'VIA', 'dry.'], 79], [['AT flow', 66], ['raffle', 46], ['wolflike', 62], ['VIA dry.', 76]]), ('control layout', [['flow', 'Yo', 'y.', 'y.'], 102], [['flow Yo y. y.', 95]])], [('regression: kerning after ligature', [['dry.', 'LA', 'shelf|life', 'staff.', 'fi', 'staff.', 'av'], 116], [['dry. LA', 63], ['shelflife staff.', 111], ['fi staff. av', 82]]), ('regression: kerning after ligature', [['fly.', 'office', 'raffle', 'staff.', 'fly.', 'VIA'], 68], [['fly.', 23], ['office', 46], ['raffle', 46], ['staff.', 42], ['fly. VIA', 67]]), ('regression: kerning after ligature', [['fl.', 'wolf|like', 'Wavy', 'Vat'], 50], [['fl.', 15], ['wolflike', 62], ['Wavy', 45], ['Vat', 27]]), ('regression: kerning after ligature', [['Av', 'staff.', 'office'], 55], [['Av', 23], ['staff.', 42], ['office', 46]]), ('non-joiner blocks ligature', [['shelf|life'], 200], [['shelflife', 64]]), ('case-sensitive pair', [['Av', 'av'], 200], [['Av av', 47]]), ('control layout', [['office', 'wolf|like'], 70], [['office', 46], ['wolflike', 62]]), ('control layout', [['our', 'av', 'our', 'Wavy'], 94], [['our av our', 89], ['Wavy', 45]])]]
for label, args, expected in fixtures[N - 1]:
    check(label, solve(args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
ligature then period[['fl. fi,', 34]][['fl. fi,', 35]]Failed
regression: kerning after ligature[['fi fl. AVAIL', 94], ['fly. Tofu', 65]][['fi fl. AVAIL', 95], ['fly. Tofu', 65]]Failed
regression: kerning after ligature[['staff.', 41], ['LAVALLE', 88], ['fl. shelflife', 83], ['our', 30]][['staff.', 42], ['LAVALLE', 88], ['fl. shelflife', 84], ['our', 30]]Failed
regression: kerning after ligature[['VIA', 39], ['flow', 35], ['dry.', 32], ['y.', 12], ['AVAIL', 59], ['fl. av', 38]][['VIA', 39], ['flow', 35], ['dry.', 32], ['y.', 12], ['AVAIL', 59], ['fl. av', 39]]Failed
case-sensitive pair[['Av av', 47]][['Av av', 47]]Passed
non-joiner blocks ligature[['shelflife', 64]][['shelflife', 64]]Passed
control layout[['Av office', 74], ['Ta', 21], ['LAVALLE', 88], ['AVAIL', 59], ['Wavy Vat', 77]][['Av office', 74], ['Ta', 21], ['LAVALLE', 88], ['AVAIL', 59], ['Wavy Vat', 77]]Passed
control layout[['fly.', 23], ['office', 46], ['Vat', 27], ['AT Av', 54], ['our', 30]][['fly.', 23], ['office', 46], ['Vat', 27], ['AT Av', 54], ['our', 30]]Passed

SHA-256 / 3904c7dec3787f392e19b3dd33607c5dcf34c8fa4601124f62c1f59706a302e3

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(x):
    words, width = x
    W = {'i': 4, 'l': 4, 'j': 4, '.': 4, ',': 4, 't': 6, 'f': 6, 'm': 15, 'w': 14, 'M': 16, 'W': 18}
    LIG = {'fi': 11, 'fl': 11, 'ff': 12}
    KERN = {'AV': -3, 'VA': -3, 'To': -2, 'Ta': -2, 'Yo': -2, 'LT': -3, 'Wa': -2, 'av': -1, 'va': -1, 'y.': -2, 'r.': -2, 'l.': -1, 'f.': -1, 'Va': -2}
    SPACE = 5
    def glyph(c):
        return W.get(c, 13 if c.isupper() else 10)
    def measure(w):
        total, prev, i = 0, None, 0
        while i < len(w):
            if w[i] == '|':
                prev = None
                i += 1
                continue
            pair = w[i:i + 2]
            if pair in LIG:
                total += LIG[pair]
                prev = w[i]
                i += 2
                continue
            g = w[i]
            total += glyph(g)
            if prev is not None:
                total += KERN.get(prev + g, 0)
            prev = g
            i += 1
        return total
    def line_width(ws):
        return sum(measure(t) for t in ws) + SPACE * (len(ws) - 1)
    lines, cur = [], []
    for w in words:
        cand = cur + [w]
        if not cur or line_width(cand) <= width:
            cur = cand
        else:
            lines.append(cur)
            cur = [w]
    if cur:
        lines.append(cur)
    return [[' '.join(t.replace('|', '') for t in ln), line_width(ln)] for ln in lines]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('ligature then period', [['fl.', 'fi,'], 200], [['fl. fi,', 35]]), ('regression: kerning after ligature', [['fi', 'fl.', 'AVAIL', 'fly.', 'Tofu'], 100], [['fi fl. AVAIL', 95], ['fly. Tofu', 65]]), ('regression: kerning after ligature', [['staff.', 'LA|VALLE', 'fl.', 'shelf|life', 'our'], 95], [['staff.', 42], ['LAVALLE', 88], ['fl. shelflife', 84], ['our', 30]]), ('regression: kerning after ligature', [['VIA', 'flow', 'dry.', 'y.', 'AVAIL', 'fl.', 'av'], 42], [['VIA', 39], ['flow', 35], ['dry.', 32], ['y.', 12], ['AVAIL', 59], ['fl. av', 39]]), ('case-sensitive pair', [['Av', 'av'], 200], [['Av av', 47]]), ('non-joiner blocks ligature', [['shelf|life'], 200], [['shelflife', 64]]), ('control layout', [['Av', 'office', 'Ta', 'LA|VALLE', 'AVAIL', 'Wavy', 'Vat'], 99], [['Av office', 74], ['Ta', 21], ['LAVALLE', 88], ['AVAIL', 59], ['Wavy Vat', 77]]), ('control layout', [['fly.', 'office', 'Vat', 'AT', 'Av', 'our'], 57], [['fly.', 23], ['office', 46], ['Vat', 27], ['AT Av', 54], ['our', 30]])], [('regression: kerning after ligature', [['fl.', 'staff.', 'flow', 'Tofu'], 109], [['fl. staff. flow', 102], ['Tofu', 37]]), ('regression: kerning after ligature', [['staff.', 'wolf|like', 'Ta', 'Tofu', 'flow'], 94], [['staff.', 42], ['wolflike Ta', 88], ['Tofu flow', 77]]), ('regression: kerning after ligature', [['LA', 'staff.', 'Av'], 120], [['LA staff. Av', 101]]), ('regression: kerning after ligature', [['fi', 'staff.', 'fi', 'fi'], 118], [['fi staff. fi fi', 90]]), ('case-sensitive pair', [['Av', 'av'], 200], [['Av av', 47]]), ('non-joiner blocks ligature', [['shelf|life'], 200], [['shelflife', 64]]), ('control layout', [['VIA', 'AVAIL', 'office', 'Av'], 112], [['VIA AVAIL', 103], ['office Av', 74]]), ('control layout', [['fi', 'Ta', 'Ta', 'Tofu', 'y.'], 103], [['fi Ta Ta', 63], ['Tofu y.', 54]])], [('regression: kerning after ligature', [['fly.', 'staff.', 'y.', 'raffle', 'av', 'raffle'], 99], [['fly. staff. y.', 87], ['raffle av', 70], ['raffle', 46]]), ('regression: kerning after ligature', [['staff.', 'AVAIL', 'LA', 'LA|VALLE'], 42], [['staff.', 42], ['AVAIL', 59], ['LA', 26], ['LAVALLE', 88]]), ('regression: kerning after ligature', [['fl.', 'Wavy', 'our', 'VIA', 'Tofu', 'staff.', 'staff.'], 58], [['fl.', 15], ['Wavy', 45], ['our', 30], ['VIA', 39], ['Tofu', 37], ['staff.', 42], ['staff.', 42]]), ('regression: kerning after ligature', [['shelf|life', 'raffle', 'av', 'our', 'staff.', 'y.'], 43], [['shelflife', 64], ['raffle', 46], ['av', 19], ['our', 30], ['staff.', 42], ['y.', 12]]), ('ligature then period', [['fl.', 'fi,'], 200], [['fl. fi,', 35]]), ('case-sensitive pair', [['Av', 'av'], 200], [['Av av', 47]]), ('control layout', [['Ta', 'raffle', 'Yo'], 80], [['Ta raffle', 72], ['Yo', 21]]), ('control layout', [['fly.', 'shelf|life', 'AT', 'flow', 'av'], 91], [['fly.', 23], ['shelflife', 64], ['AT flow av', 90]])], [('regression: kerning after ligature', [['our', 'av', 'staff.', 'Av'], 56], [['our av', 54], ['staff.', 42], ['Av', 23]]), ('regression: kerning after ligature', [['av', 'fl.', 'Tofu', 'flow'], 112], [['av fl. Tofu', 81], ['flow', 35]]), ('regression: kerning after ligature', [['fl.', 'wolf|like', 'Wavy', 'Vat'], 50], [['fl.', 15], ['wolflike', 62], ['Wavy', 45], ['Vat', 27]]), ('regression: kerning after ligature', [['flow', 'av', 'av', 'wolf|like', 'fl.', 'Av'], 88], [['flow av av', 83], ['wolflike fl.', 82], ['Av', 23]]), ('non-joiner blocks ligature', [['shelf|life'], 200], [['shelflife', 64]]), ('ligature then period', [['fl.', 'fi,'], 200], [['fl. fi,', 35]]), ('control layout', [['AT', 'flow', 'raffle', 'wolf|like', 'VIA', 'dry.'], 79], [['AT flow', 66], ['raffle', 46], ['wolflike', 62], ['VIA dry.', 76]]), ('control layout', [['flow', 'Yo', 'y.', 'y.'], 102], [['flow Yo y. y.', 95]])], [('regression: kerning after ligature', [['dry.', 'LA', 'shelf|life', 'staff.', 'fi', 'staff.', 'av'], 116], [['dry. LA', 63], ['shelflife staff.', 111], ['fi staff. av', 82]]), ('regression: kerning after ligature', [['fly.', 'office', 'raffle', 'staff.', 'fly.', 'VIA'], 68], [['fly.', 23], ['office', 46], ['raffle', 46], ['staff.', 42], ['fly. VIA', 67]]), ('regression: kerning after ligature', [['fl.', 'wolf|like', 'Wavy', 'Vat'], 50], [['fl.', 15], ['wolflike', 62], ['Wavy', 45], ['Vat', 27]]), ('regression: kerning after ligature', [['Av', 'staff.', 'office'], 55], [['Av', 23], ['staff.', 42], ['office', 46]]), ('non-joiner blocks ligature', [['shelf|life'], 200], [['shelflife', 64]]), ('case-sensitive pair', [['Av', 'av'], 200], [['Av av', 47]]), ('control layout', [['office', 'wolf|like'], 70], [['office', 46], ['wolflike', 62]]), ('control layout', [['our', 'av', 'our', 'Wavy'], 94], [['our av our', 89], ['Wavy', 45]])]]
for label, args, expected in fixtures[N - 1]:
    check(label, solve(args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
ligature then period[['fl. fi,', 34]][['fl. fi,', 35]]Failed
regression: kerning after ligature[['fi fl. AVAIL', 94], ['fly. Tofu', 65]][['fi fl. AVAIL', 95], ['fly. Tofu', 65]]Failed
regression: kerning after ligature[['staff.', 41], ['LAVALLE', 88], ['fl. shelflife', 83], ['our', 30]][['staff.', 42], ['LAVALLE', 88], ['fl. shelflife', 84], ['our', 30]]Failed
regression: kerning after ligature[['VIA', 39], ['flow', 35], ['dry.', 32], ['y.', 12], ['AVAIL', 59], ['fl. av', 38]][['VIA', 39], ['flow', 35], ['dry.', 32], ['y.', 12], ['AVAIL', 59], ['fl. av', 39]]Failed
case-sensitive pair[['Av av', 47]][['Av av', 47]]Passed
non-joiner blocks ligature[['shelflife', 64]][['shelflife', 64]]Passed
control layout[['Av office', 74], ['Ta', 21], ['LAVALLE', 88], ['AVAIL', 59], ['Wavy Vat', 77]][['Av office', 74], ['Ta', 21], ['LAVALLE', 88], ['AVAIL', 59], ['Wavy Vat', 77]]Passed
control layout[['fly.', 23], ['office', 46], ['Vat', 27], ['AT Av', 54], ['our', 30]][['fly.', 23], ['office', 46], ['Vat', 27], ['AT Av', 54], ['our', 30]]Passed

SHA-256 / 028818cb77ea71e53e928a325c8b80428690498dc18d3fbec2d51e3129115f94

HELD IN THE MEMBER ARCHIVE

The verified repair and its recorded checks are member-only.

This mechanism has 8 recorded checks per implementation. The open-access tier publishes the failure and the unsuccessful fix; the repaired source that passes every check, and the observations that prove it, are available to members.

Every case sharing this mechanism uses the same contract and the same repair, so this one record is held back for all of them.

Member access is invitation-based. Sign in with your invited account to inspect the repair.

Sign in to the archive ↗

Verification & scope

A deterministic toy typesetting model with integer widths and a stipulated rule set; it does not claim conformance to any engine. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.

Observations recorded using Python 3.12.14 at 2026-09-29T14:49:51.285899+00:00.

Case digest / 363b3d40427fda64ceac8b8c56c7c783bc85426941e0f65feaaebc9bfc6433cd