The deterministic matcher was supposed to be the safe part. It mostly is. But safe does not mean finished, and the matcher had failure modes of its own. Most of the work in this project was not building it. It was tuning it.
The dial
The matcher has one main dial, but the dial only makes sense once you have watched the thing work. It is worth seeing once, because the whole safety argument rests on how dumb and how legible this machine is.
Say you type zoloff. The matcher does not know what that is, and it does not try to guess. It measures. For every real drug name in the database, it counts the smallest number of single-character edits (a changed letter, a dropped one) that would turn zoloff into that name.
| Real drug | Edits from zoloff | The move |
|---|---|---|
| Zoloft | 1 | change the last f to a t |
| Zocor | 3 | three separate changes |
| Zofran | 4 | four separate changes |
| Ambien | 6 | replace nearly the whole word |
Then it sorts by that number and keeps the closest few. Zoloft sits one edit away. Ambien, the drug that handed someone the wrong prescription back in chapter one, sits six edits away, which is just a precise way of saying it is not a misspelling of zoloff at all. It never comes up. The user sees Zoloft, confirms it, and moves on.
That is the whole trick. Count the edits, then show the nearest real names. There is no model in the loop, and nothing to guess. The same input gives the same list every time, and I can point at the exact number that put each option where it is.
Which brings us back to the dial. The dial is how far the matcher will reach before it gives up and says no match.
Turn the dial too tight and it quits on real typos. Someone fat-fingers metformin into metfrmin, and the matcher, being strict, shrugs and says it found nothing. That is a bad experience, but a safe one. The user just retypes.
Turn the dial too loose and it does something worse. It reaches so far that it snaps your typo onto a drug you never meant.
When cocaine became Codeine
I found the worst version of this by accident.
I typed cocaine, which is not a word this app should ever turn into a prescription, and the matcher handed back Codeine Sulfate. Two edits away. Codeine is a real opioid. It was sitting close enough in edit distance that a loose dial called it a match.
That is the exact failure the whole architecture was built to prevent, and here it was again, this time coming from the boring deterministic half instead of the model. A wrong drug is a wrong drug no matter which component produced it.
So I tightened the floor. Short words now have to match almost exactly before the matcher will offer anything, because on a short word two edits can land you on a completely different molecule. Longer words get more slack, because on a twelve-letter drug name two typos still clearly point at the same thing. The dial is not one number. It scales with how much word you gave it.
The English word problem
Then there is the problem I did not see coming, which is English.
The matcher does not know that some strings are drugs and some are just words. Type anvil and it notices you are one edit from Advil, and helpfully offers you ibuprofen. Type layer and it gives you Bayer.
You cannot fix this by blocking words that sit close to drugs, because that is also the definition of a misspelled drug. Zanax is one edit from Xanax, and it is a real thing people type. If I block everything one edit away from a drug name, I break the misspellings I most need to catch. The signal and the noise are the same distance apart.
The honest answer turned out to be a boring one. I keep a small hand-written list of English words that happen to collide with drug names, checked before the matcher runs. It has anvil on it, and color, and a few others, and I add to it when I find a new one. It does not generalize, and it does not need to. There are not many everyday words one keystroke away from a medication. Boring, but right.
The stack that gets to ninety-nine percent
The matcher handles misspellings, which is most of the traffic. But most is not ninety-nine percent. The last stretch came from a row of small deterministic filters standing in front of and behind the matcher, each one added the day a specific bad input got through. None of them is clever, and that is the point. Every one is a rule I can read and test by hand.
You have already met the matcher and the stoplist. Here is where they sit in the line, and what stands with them, in the order an input actually travels.
Normalize the text. Before anything else, the input gets cleaned. Accents are flattened, so
DépakoteandDepakotebecome the same word. Invisible junk like zero-width spaces and look-alike foreign letters gets stripped, so a name cannot smuggle in a character that renders one way and matches another.Catch the emergency first. Some words never reach the matcher. Anything about an overdose or self-harm short-circuits straight to a crisis line and a phone number, and the lookup is suppressed. I set this to over-trigger on purpose. A false alarm shows a hotline nobody needed. A miss is unforgivable.
Resolve known aliases. A hand-built table maps the names people actually type to the real drug. Brand short-names like
tylenoland international spellings likeparacetamolresolve here, exactly, before any guessing happens.Expand drug classes. If someone types a category instead of a drug, a curated dictionary answers.
statinreturns the real statins in the database, andblood thinnerreturns the real anticoagulants. The members are hand-listed and checked against the database, so the app cannot invent one.Block the English words. The stoplist from a moment ago.
anvilandcolorand their kind get stopped before the matcher can turn them into medication.Fuzzy match. Only now does the edit-distance dial run, on whatever is left standing.
Force a choice when it is close. If the best match has a different real drug about as close as it, the app refuses to pick for you.
clonipinlands the same distance from Klonopin and from Clonidine, so it shows both and makes you choose.Confirm before showing. You tap the drug you meant before a single line of FDA label appears. The last gate is a person.
Under all eight sits the rule that makes the number mean anything. The matcher can only ever return a drug that already exists in the database. It has no way to produce anything else. So the target was never “is this answer correct in some abstract sense.” It was “is the right drug somewhere in the four options on the screen,” and across the whole stress set the answer is better than ninety-nine percent. The single miss is a drug I have not loaded yet, which is a hole in the shelf, not a wrong answer.
The temptation
Every one of these was a moment where I wanted to reach back for the model.
The matcher is dumb. It counts letters. It has no idea that tilenol is somebody trying to spell Tylenol. A language model knows that instantly. So the tempting move, every single time the matcher stumbled, was to put the model back in as a helper. Let the matcher try, and when it fails, ask the model what drug the person probably meant.
So I tested exactly that. I took tilenol, a misspelling of Tylenol, whose active ingredient is acetaminophen, and I asked three different models what the user meant.
The 2B model said it did not know. A miss, but an honest one, and a safe one.
Qwen at 3B said acetaminophen. Correct.
Llama at 3B, the biggest of the three, said Atenolol. It said it with total confidence. Atenolol is a beta blocker for blood pressure. It is not Tylenol. It is not close to Tylenol in any way that matters to a person standing in a kitchen with a headache. It is a different drug for a different problem, and the largest model was the one that produced it, without hesitation.
The whole chapter in one test
The bigger model was not safer. It was more confidently wrong.
And here is the part that kills the “let the model help” idea for good. Atenolol is a real drug. If I had built the obvious guardrail from chapter two, the one that checks whether the model’s answer exists in the database, Atenolol would have sailed right through. It is in there. The guardrail would have handed a blood-pressure drug to someone who typed a painkiller, and logged it as a success.
So the model does not get to help. Not as a backstop, not as a last resort, and not at the larger size where you would assume it simply knows more.
The matcher’s failures are boring. It gives up too early or it reaches too far, and both are fixed by turning a dial or adding a word to a list. I can see every one of them coming, and I can trace every one after the fact.
The model’s failures are a coin flip with a confident voice. Same input, three models, and one of them hands you a heart medication for a headache and swears it is right. Making the model bigger did not fix that. It only made the wrong answer sound more sure.
Boring keeps the job.
Next chapter, I spend a weekend trying to break the whole thing on purpose.
