How to compare Unicode characters that look alike?
ML System Design practice on Codemia
Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.
Unicode characters that look alike but have different code points are called homoglyphs. To compare them reliably, you need Unicode normalization (NFC, NFD, NFKC, NFKD) and, when that is not enough, confusable detection via the Unicode Consortium's security mechanisms. Simple equality checks will fail because two visually identical strings can have entirely different byte representations.
Why Visual Equality Does Not Mean Byte Equality
Consider the Latin letter "A" (U+0041) and the Cyrillic letter "A" (U+0410). They render identically in most fonts, but they are different code points. A standard == comparison will return false because the underlying bytes differ.
This is not a corner case. There are thousands of homoglyph pairs across Latin, Cyrillic, Greek, Armenian, and other scripts. The problem shows up in domain name spoofing, username impersonation, password bypass, and search deduplication.
These two characters are indistinguishable on screen but completely different to the interpreter.
Unicode Normalization Forms
Normalization handles a different class of look-alikes: characters that have multiple valid encodings. The accented character "e" (U+00E9) can also be represented as "e" (U+0065) followed by a combining acute accent (U+0301). Normalization collapses these equivalent representations into a single canonical form.
Unicode defines four normalization forms:
| Form | Name | What It Does |
| NFC | Canonical Composition | Decomposes then recomposes to the shortest canonical form |
| NFD | Canonical Decomposition | Decomposes characters into base + combining marks |
| NFKC | Compatibility Composition | Like NFC but also replaces compatibility characters (e.g., "fi" ligature becomes "fi") |
| NFKD | Compatibility Decomposition | Like NFD but also decomposes compatibility characters |
NFC is the most common choice for storage and comparison. NFKC is more aggressive and useful when you want "fi" (the ligature) to match "fi" (two separate letters) or when comparing user input against a canonical database.
Detecting Homoglyphs Across Scripts
Normalization solves the problem of equivalent encodings within the same script. It does not solve cross-script homoglyphs like Latin "o" vs. Cyrillic "o". For that, you need confusable detection.
The Unicode Consortium publishes a confusables mapping (part of Unicode Technical Standard #39) that maps visually similar characters to a common skeleton. Python's confusables library and ICU's SpoofChecker implement this.
In Java, ICU4J provides SpoofChecker:
Real-World Security Example: Domain Spoofing
A domain like apple.com can be spoofed by registering аррӏе.com using Cyrillic characters. Browsers have largely mitigated this with IDN homograph attack protection (displaying the punycode form when mixed scripts are detected), but application-layer code still needs to handle it.
Handling Homoglyphs in JavaScript and Go
The same principles apply in other languages. In JavaScript, normalization is built into the String prototype:
In Go, the golang.org/x/text/unicode/norm package provides the same normalization forms:
For confusable detection across scripts, ICU libraries exist for most languages.
Comparison of Approaches
| Approach | Solves | Does Not Solve | Best For |
== equality | Exact byte match | Any visual equivalence | Known-clean data |
| NFC/NFD normalization | Same-script encoding variants | Cross-script homoglyphs | Text storage and indexing |
| NFKC/NFKD normalization | Encoding variants + compatibility chars | Cross-script homoglyphs | Search and user input matching |
| Confusable detection | Cross-script homoglyphs | Semantic similarity | Security, anti-spoofing |
Common Pitfalls
- Assuming normalization handles all look-alike problems. NFC and NFKC only collapse equivalent encodings within the Unicode standard. They do not map Latin "A" to Cyrillic "A".
- Using raw string comparison for usernames, emails, or URLs without any normalization. This allows both encoding-variant duplicates and homoglyph spoofing.
- Normalizing once at comparison time but storing the original unnormalized form. Normalize on input, store the normalized form, and compare normalized values.
- Forgetting that confusable detection can produce false positives. The skeleton mapping is intentionally broad, so use it as a flag rather than an automatic reject.
- Applying NFKC when you need to preserve formatting distinctions. NFKC collapses ligatures, superscripts, and other compatibility characters, which may not be appropriate for display-oriented text.
Summary
- Characters that look identical on screen can have completely different Unicode code points. Simple equality checks will miss these matches.
- Use NFC or NFKC normalization to collapse equivalent encodings before comparison. NFC is the standard choice; NFKC adds compatibility decomposition.
- For cross-script homoglyphs (Latin vs. Cyrillic, Greek, etc.), use confusable/skeleton detection from the Unicode Consortium's confusables data.
- Always normalize on input and store the normalized form. Comparing unnormalized strings is a recurring source of bugs and security vulnerabilities.
- Domain spoofing, username impersonation, and phishing attacks all exploit homoglyph confusion, making this a security-critical topic, not just a text-processing curiosity.
Related reading
- How to compute the similarity between two text documents?
- How to count string occurrence in string?
- How to deal with large2GB embedding lookup table in tensorflow?
- How to determine if two sentences talk about similar topics?
- How to determine the encoding of text
- How to determine the language of a piece of text?
- How to embed a text file in a .NET assembly?
- How to embed image or picture in jupyter notebook, either from a local machine or from a web resource?
.png&w=3840&q=75)
Tackling System Design Interview Problems
A short course that equips you with the skills to approach system design interviews methodically.
Start the free courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
ML System Design practice on Codemia
Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.