Text Normalization › Ligature Expansion

Michael BrenndoerferUpdated March 19, 20254 min read
Language AI HandbookAll chapters
Part 1 of 67Text as Data
Chapter 2Text Normalization › Ligature Expansion
Reading section 7 of 15 · Ligature Expansion · 4 minSection 7 of 15 · 4 min

Book contents

Section 7 of 15 · 4 min read

Text Normalization › Ligature ExpansionOne section · Sections in this chapter7 / 15 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Ligatures are single characters that represent multiple letters joined together. They are common in typeset text, particularly in PDFs and scanned documents, and can cause matching problems when the surrounding text uses standard individual letters.

Think of ligatures as a typographic optimization that Unicode chose to represent explicitly. A printer's ligature like "fi" is a single glyph that prevents the dot of the "i" from colliding with the crossbar of the "f" in certain typefaces. In hand-set type, these were physical pieces of metal. In digital typography, they became single Unicode code points in the "Alphabetic Presentation Forms" block (U+FB00-U+FB4F). The problem arises when a PDF extractor encounters these ligature code points and passes them through unchanged. The word "find" now contains a single character where a text processing system expects two.

In[28]:
Code
# Common ligatures
ligatures = [
    ("fi", "fi", "Latin small ligature fi"),
    ("fl", "fl", "Latin small ligature fl"),
    ("ff", "ff", "Latin small ligature ff"),
    ("ffi", "ffi", "Latin small ligature ffi"),
    ("ffl", "ffl", "Latin small ligature ffl"),
    ("Ꜳ", "AA", "Latin capital letter AA"),
    ("œ", "oe", "Latin small letter oe"),
    ("æ", "ae", "Latin small letter ae"),
]

# NFKC expands most ligatures
expanded = [
    (lig, exp, unicodedata.normalize("NFKC", lig)) for lig, exp, _ in ligatures
]
Out[29]:
Console
Ligature Expansion with NFKC:
---------------------------------------------
Ligature     Expected     NFKC Result 
---------------------------------------------
'fi'          'fi'         'fi' ✓
'fl'          'fl'         'fl' ✓
'ff'          'ff'         'ff' ✓
'ffi'          'ffi'         'ffi' ✓
'ffl'          'ffl'         'ffl' ✓
'Ꜳ'          'AA'         'Ꜳ' ≠
'œ'          'oe'         'œ' ≠
'æ'          'ae'         'æ' ≠

NFKC handles most Latin ligatures correctly. However, some characters like "æ" and "œ" are considered distinct letters in some languages (Danish, French) rather than ligatures, so NFKC preserves them. This reflects a key distinction in Unicode: purely typographic ligatures (the f-ligatures) are expanded because they carry no semantic information beyond their component letters. Linguistic digraphs (æ, œ) are preserved because they function as independent characters in their respective orthographies.

The following table shows common ligatures, their Unicode code points, and their expanded forms. Note the large code point gap between ligatures in the Alphabetic Presentation Forms block (U+FB00-U+FB4F) and their ASCII expansions (U+0000-007F). This gap explains why naive string comparison fails without normalization:

Common ligatures and their NFKC expansions. Typographic ligatures expand; linguistic letters are preserved.
LigatureCode PointExpansionExpansion Code PointsUnicode Block
fiU+FB01fiU+0066 U+0069Alphabetic Presentation Forms
flU+FB02flU+0066 U+006CAlphabetic Presentation Forms
ffU+FB00ffU+0066 U+0066Alphabetic Presentation Forms
ffiU+FB03ffiU+0066 U+0066 U+0069Alphabetic Presentation Forms
fflU+FB04fflU+0066 U+0066 U+006CAlphabetic Presentation Forms
ſtU+FB05stU+0073 U+0074Alphabetic Presentation Forms
œU+0153œ (preserved).Latin Extended-A
æU+00E6æ (preserved).Latin-1 Supplement
ŒU+0152Œ (preserved).Latin Extended-A
ÆU+00C6Æ (preserved).Latin-1 Supplement

The Latin f-ligatures (fi, fl, ff, ffi, ffl, ſt) are expanded by NFKC because they are typographic variants with no independent linguistic identity. However, "æ" and "œ" are preserved because they function as distinct letters in Danish and Norwegian, as well as French. The code point data reveals why ligatures cause string matching problems: the "fi" ligature (U+FB01) and its expansion "fi" (U+0066, U+0069) are separated by over 64,000 code points in Unicode space. Without normalization, a search for "find" will never match "find" even though they are semantically identical.