disarm

Confusables

Check confusable characters

Paste text to fold homoglyphs toward Latin under both of disarm's digit policies at once. They are not two spellings of one answer — where they disagree, one reading is a number and the other is a word, and which you want depends on what you are about to do with it.

The tool

Three lines, each making a different point. The first spells a brand with Cyrillic letters, which fold the same way under either policy because no digits are involved. The second hides two Devanagari zeros in another brand, where the policies disagree and one reading is the brand being imitated. The third is the trio from disarm’s own documentation — ١ — which TR39 folds to three different letters, o, O and l, where the numeric policy gives 0, 0 and 1.

Paste text to fold it.

Nothing is uploaded. The engine runs inside this page.

Numeric policy

A non-Latin digit becomes the ASCII digit. What prose means.


        

TR39 policy

A non-Latin digit becomes a Latin letter. What a skeleton needs.


        

Loading the engine…

A worked example

The tool above needs JavaScript. This is the same folding written out, so it is legible without running anything.

InputNumericTR39Agree?
g००gleg00glegoogleno
раураlpaypalpaypalyes
०೦١001oOlno
paypalpaypalpaypalyes — nothing to fold

The first row is the argument for having two policies. Under the numeric policy g००gle becomes g00gle, which reads as a string with two zeros in it. Under TR39 it becomes google, which collides with the brand being imitated. If you are storing the text, the first is right. If you are asking whether someone is impersonating a domain, only the second answers the question.

The second row involves no digits, so both policies agree: five Cyrillic letters fold to their Latin lookalikes either way.

The same thing in your own code

Each block is a file CI compiles and runs, so none can quietly stop working, and all seven print the same line. disarm on GitHub →

# Fold confusables under both digit policies. They answer different questions.
#   pip install disarm
from disarm import normalize_confusables

# A brand spelled with two Devanagari zeros standing in for the letter o.
SPOOF = "g००gle"

numeric = normalize_confusables(SPOOF)                        # the default
tr39 = normalize_confusables(SPOOF, digit_policy="tr39")

# Numeric keeps a digit a digit, which is what stored text means. TR39 folds it
# to a letter, which is what makes a spoof collide with the brand it imitates.
assert numeric == "g00gle", numeric
assert tr39 == "google", tr39
assert numeric != tr39, "the policies must disagree here"

print(f'ok: numeric gives "{numeric}", tr39 gives "{tr39}"')

Choosing a policy

You areUseBecause
Cleaning text you will store or displaynumeric A digit keeps its value. Folding to o would turn a quantity into a word.
Comparing usernames or identifiersTR39 A skeleton only has to collide. Whether it reads sensibly is not the question being asked.
Checking a domain against a brandTR39 The spoof must land on the same skeleton as the thing it imitates.
Building a search indexnumeric Query and document should agree, and a user typing a digit means a digit.
Comparing against a published TR39 benchmarkTR39 It is the upstream mapping; anything else will differ from the reference by design.

Folding is one control, not the whole answer. It catches cross-script substitution, where a Latin word borrows a Cyrillic letter. It does not by itself separate a label written entirely in Cyrillic that skeletons to a Latin brand from a legitimate Russian word — that needs a mixed-script or whole-script check, which disarm reports separately.