Unprofane

The Scunthorpe problem

Filters that find bad words inside other words block Scunthorpe, the surname Dickinson and the cockatoo. Here is how often, and the fix.

Enter to check

Live, on the real engine. Type an innocent word, or a disguised one.

What the Scunthorpe problem is

The Scunthorpe problem is a filter blocking innocent text because a profane string sits inside it. It is named for the English town whose name holds one.

The same mistake turns up in surnames (Dickinson, Hancock), animals (cockatoo, peacock), medicine (breast cancer, anal fissure) and ordinary words (assassin, classic). A user named Dickinson who cannot sign up is the same failure as a review of a garden hoe held for moderation.

Why filters do it

A filter has to decide where a word ends. Match a bad word anywhere inside the text and you catch it glued to other words or hidden in a username, which has no spaces. You also block every innocent word that contains it.

Match only whole words and Scunthorpe goes through, but so does a slur run into the next word. Disguises make it worse: a filter that reads sh1t as a word reads look-alike letters everywhere, and an innocent word read through them can turn profane.

Here is a plain word list, matched as typed, beside the engine on four texts. The list blocks the town and misses the disguises.

  1. Moving to Scunthorpe next month

    The list finds a slur inside a town.

    Blocks it

    allow

  2. f.u.c.k off, nobody wants you here

    Four dots and the list sees no word.

    Misses it

    block

  3. ƒυ¢к off

    A hooked f, a Greek u, a cent sign and a Cyrillic k.

    Misses it

    review

  4. I know where you live

    A threat in ordinary words.

    Misses it

    block

Out of 4

1 innocent blocked3 missed

1 innocent let through3 caught

Which innocent words get blocked

The Scunthorpe Test's public split holds 525 innocent items in seven kinds, each a word or phrase that contains a profane string. Every system below read the same items, so every flag in the table is a false positive.

SystemPlacesSurnamesOrdinary wordsAnimalsMedical termsBrands and titlesToolsAll innocent
obscenity 0.4.623.1%20.0%13.1%53.2%42.3%26.3%0.0%20.2%
bad-words 4.1.51.3%12.6%3.9%51.1%34.6%31.6%14.3%12.5%
2toad-profanity 3.3.01.3%10.5%3.9%51.1%34.6%21.1%7.1%11.5%
leo-profanity 1.9.01.3%4.2%2.6%46.8%34.6%10.5%0.0%8.7%
OpenAI moderation1.3%1.1%3.1%10.6%0.0%10.5%0.0%3.2%
Perspective API0.0%0.0%0.9%23.4%3.8%5.3%0.0%3.0%
Unprofane, rules alone0.0%0.0%0.0%0.0%0.0%0.0%0.0%0.0%
Items of each kind7810523647261914525

The Scunthorpe Test, public split, scunthorpe-test.jsonl; result files 2026-09-24-14-29-32-dev.json, 2026-09-28-02-10-51-dev.json, 2026-09-28-08-29-25-dev.json, 2026-10-18-21-05-29-dev.json, 2026-10-20-14-48-25-dev.json in bench/results/. The rules' run of 2026-10-20 read a later version of the split, which adds 24 place names written with accents (0.0% of them flagged); the table shows the kinds every run read. How it was measured.

The kinds are not equally hard: obscenity 0.4.6, the worst of these, blocks animals most often, 53.2% of them. Examples of each kind, from the engine's innocent list:

Read the zero carefully

Those innocent items come from the engine's own innocent list. A zero for Unprofane there means it lets through the words it was built to let through, not that it never blocks an innocent word.

For that, look at text nobody wrote the list from. The head-to-head's development set is built from third-party datasets and labeled blind by AI judges. The share of each kind of clean text each system flagged:

SystemAn innocent word that holds a profane stringA place nameA surnameThe title of a film, book or songA violent idiom ("killing it")
Unprofane, balanced7.9%0.8%0.5%0.9%1.4%
Unprofane, rules alone3.1%1.1%0.6%0.4%5.3%
Perspective API3.1%1.3%0.3%1.1%8.9%
OpenAI moderation0.0%0.0%0.0%0.0%13.7%
bad-words0.9%0.4%0.5%0.4%0.1%
obscenity7.0%5.4%4.5%2.7%2.2%
Clean texts of each kind2291,8471,2688821,610

Gold DEV, 19,584 texts, rubric version 2, read 2026-09-28, engine 2026-09-29. Perspective and OpenAI are read at the score where they flag as much clean text overall as Unprofane balanced does. DEV is where the engine was tuned, so its numbers here are, if anything, kind to it.

Here Unprofane balanced flags 7.9% of innocent words, and the rules alone 3.1%. That is more than the word lists, which flag at most 7.0%.

But a list that flags little also catches little: none catches more than 9.9% of the toxic text in the same set, where Unprofane balanced catches 91.8%.

A false-positive rate means nothing without the catch rate beside it, and both depend on whose innocent words you test. Test yours.

How Unprofane lets them through

The rules find every candidate the way a substring filter would, through every disguise, then rule out the innocent ones:

What the rules cannot rule out, a word innocent in one sentence and an insult in the next, goes to the model in balanced mode, and the answer says which one decided.

The evasion tags name every disguise the engine read through, so a false positive can be traced to the reading that caused it.

Stop it in your own product

  1. Test on your own innocent text. Your users' towns, your product names, your community's words. A test key is free, and the batch endpoint takes many texts in one request. Read every block on text you know is clean.
  2. Allow what is yours. Put place names, surnames and product names on the project's allow list, or send them with the request. A listed term is folded like the text, so Scunthorpe also allows Scunth0rpe.
  3. Say what the text is. A username, a comment and a line of game chat are read differently: set the context.
  4. Hold, do not block, when unsure. A policy can send a category to review rather than block, so a borderline word waits for a person instead of turning a user away.
  5. Tell us when it is wrong. A correction is read by a person. A missing innocent word goes into the innocent list in a new dated engine version, and a version you pinned does not move.

Try the playground

Related