Text Normalization › Ligature Expansion
Language AI HandbookAll chapters 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.
# 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
]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:
| Ligature | Code Point | Expansion | Expansion Code Points | Unicode Block |
|---|---|---|---|---|
| fi | U+FB01 | fi | U+0066 U+0069 | Alphabetic Presentation Forms |
| fl | U+FB02 | fl | U+0066 U+006C | Alphabetic Presentation Forms |
| ff | U+FB00 | ff | U+0066 U+0066 | Alphabetic Presentation Forms |
| ffi | U+FB03 | ffi | U+0066 U+0066 U+0069 | Alphabetic Presentation Forms |
| ffl | U+FB04 | ffl | U+0066 U+0066 U+006C | Alphabetic Presentation Forms |
| ſt | U+FB05 | st | U+0073 U+0074 | Alphabetic 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.