I built a property search that pulls an owner's court history, and the first serious run is what scared me. On one sale list it came back with 1,379 name matches. I kept zero of them, and the product is better for it.
The search worked, and that was the problem
Some background. I build Foreclosure Radar, which tracks properties heading to a forced sale in Pennsylvania and New York. Sheriff sales, county tax claim upset sales, county tax auctions. Whoever bids on one of those houses is deciding almost blind, so the whole job of the site is to do the homework on each lot before the gavel drops. What is it worth, what shape is it in, who lives there, and which debts stay attached to the house.
That last one is why I went near the courts at all. A judgment against the owner can matter to a bidder. So I wired up a county court viewer, ran a name search for each owner, and got hits. Lots of hits. The column filled up. In a screenshot it looked like the most impressive part of the whole product.
Then I read a few of the rows out loud, and the feature died on the spot.
A name is not a person
A name is a string. A person is a string plus everything you didn't check.
Court search indexes match text. Type a common surname into a county docket viewer and you get every case in that county where that text appears in a party field. Two men with the same first and last name, a father and a son sharing both, a maiden name, a married name, an estate filed under the name of someone who died in 2004.
None of that shows up as a warning. It shows up as a clean list of results with case numbers, which is the most convincing shape wrong data can take.
Three ways a name match goes wrong
- Wrong human. Same string, different person, same county. Nothing in the record tells you which one owns the house.
- Right human, wrong role. The person you looked up is the one who filed the case, not the one being sued. A page that prints "court case" next to their address flattens that difference and reads as trouble either way.
- Right human, stale link. The owner in the county's record sold two years ago and the deed hasn't caught up, so you have attached a stranger's court history to somebody else's home.
What happens to a real person when you get it wrong
Here is the part engineers skip. The failure mode is not a bad row in a table. The failure mode is a page on the public internet that says the person living at a specific street address has a court file.
That person never signed up for my product. They don't know the page exists, they can't correct it, and they're already having a bad year, because their house is on a forced sale list. Anyone who types their address can find it. If the match was wrong, I've published something false about a named individual and attached it to the place they sleep.
There's a legal edge here too. In the United States, publishing what amounts to a distress rating about a named person, aimed at people making decisions about them, walks straight into consumer reporting territory. I don't want to be near that line, so the product never scores a person. It scores a lot.
But honestly, the law is the smaller reason. The bigger one is that I couldn't defend the row to the person it was about.
A confident wrong answer costs more than no answer
Blank fields feel like failure when you're building. They look like an unfinished product. It took me a while to see that a blank is a cheap, honest cost and a wrong value is an expensive, hidden one.
| Blank field | Confidently wrong field | |
|---|---|---|
| What the reader does | Goes and checks, or skips the lot | Acts on it |
| What it costs them | A few minutes | A bad bid, or a bid they never made |
| What it costs a third party | Nothing | Their name on a page, permanently |
| How you find out | They ask you for it | You don't |
| How you fix it | Fill it in later | You can't unsend it |
The bottom row is the one that decided it. Errors that announce themselves get fixed. Errors that look like answers just sit there being wrong at scale, on an hourly cron, forever.
The rule that shipped: an address match or nothing
A court case attaches to a property only when a party address printed on the docket matches the address of the parcel. A name match on its own is never enough, no matter how unusual the name is, no matter how few results came back.
That rule is what generated the number in the title. On one sale list, 1,379 hits came back, failed the address check, and were stored nowhere. Not in a "maybe" table. Not behind a feature flag. Not in a private admin view I could turn on later when I felt braver. If I keep them, I will eventually ship them, because a table full of unused rows is a temptation with a schema.
One more refusal in the same spot: when the owner is a company rather than a person, there is no personal court lookup at all. A business entity is a fair thing to research. The human whose name resembles the business is not.
Restraint didn't gut the feature
This is the part I didn't expect. Throwing away every uncorroborated hit left a feature that still works.
| Sale | Lots | Lots with an attached court file | Lots with open money |
|---|---|---|---|
| County upset sale | 1,144 | 150 | 112, totalling $6.10M |
| One sheriff sale | 38 | 38 | 34, totalling $7.57M |
A hundred and twelve lots where a bidder can see real open money, and every one of them corroborated by an address. That's a feature. The 1,379 would have added noise on top of it and made the good rows less believable, not more.
Some files are refused no matter how good the match is
Family cases, support cases, custody cases and protection orders never attach. Perfect address match, unique name, everything lines up, still refused. The code checks the case type before it checks anything else.
The reasoning is short. The buyer's question is "what debts follow this house". A custody docket answers nothing about the house. Publishing it isn't information, it's exposure, and it lands on people who never had a say, including children.
If a record can't change the bid, it has no business on the page.
That single sentence removed more code than any performance work I've done this year.
The other direction: a blank that lies
Restraint has a failure mode of its own, and I hit it.
The county's court system changed formats partway through the project, and money amounts are written two different ways depending on the era of the case. The older phrasing matched every one of the 68 sampled lines I tested against. The current phrasing is written differently and matched none of them.
If I had shipped only the old pattern, the page would have shown no judgment on a house carrying $211,195.44. And "no judgment" doesn't read as "we couldn't parse this". It reads as clean. A bidder would have walked into that sale believing the property was lighter than it was.
So a blank isn't automatically safe. A blank has to know why it's blank:
- Looked, found nothing. Safe to show as nothing.
- Looked, couldn't read the answer. Has to say so, out loud, on the page.
- Didn't look. Also has to say so.
Three different states, and a naive schema stores all three as null. Separating them was one of the more useful days I spent on this.
Occupancy ships with its evidence or it doesn't ship
Somebody's home being occupied is a claim about a person, so it gets the same treatment. On one 395-lot sale the read landed on 380 of them: 237 likely occupied, 135 absentee, 7 likely vacant.
None of those words are published on their own. The label always appears next to the public records that produced it, so the reader can look at the same signals and disagree with me. A bare "likely vacant" badge is an invitation to go knock on the door of a house that has somebody in it. The same word, sitting on top of the records that suggested it, is an argument the reader can check.
A lien is junior only when the evidence is unambiguous
This one has money attached, which makes the temptation to guess much stronger.
At a sheriff sale, junior liens are wiped and senior ones survive. Get that backwards and your buyer inherits a mortgage they didn't price. The sale catalog says nothing about it.
The first approach, matching the foreclosing party's name against recorded mortgages, left 296 of 395 lots unresolved. Following the recorded assignment chain instead, from the original lender through every transfer, cut that to 214 and produced 79 lots where the foreclosing party really is first in line and 65 where an older unsatisfied mortgage sits above it.
The 214 still say unclear. They will keep saying unclear until the recorded documents settle it. I would rather hand a bidder 144 answers they can lean on and 214 honest gaps than 395 confident labels where a slice of them are backwards.
The county's own number can't be trusted at face value either
Same discipline, different data, no privacy angle at all. This one is just arithmetic that lies.
In Pennsylvania, counties freeze their assessment base year and then leave it there for decades. One county I cover is still valuing houses on a 1961 base year, which puts its assessed values at roughly 7% of what the houses are actually worth. Others sit at 1994 and 1996. If you print a county assessment as "market value", you have published a number that is off by more than an order of magnitude, sourced from an official government record, which is exactly the kind of wrong that people believe.
The fix is the state's Common Level Ratio, a per-county multiplier, and I carry the full table for all 67 counties. Checked against a real arm's-length sale in one county: assessed value $165,590, multiplied by that county's ratio of 1.83, gives $303,030. The house actually sold for $299,900 in July 2026. One percent off.
Why the conversion returns null instead of 1
If the county isn't in the table, the function returns null. It does not return 1.
Returning 1 would be so easy. It keeps the math working and the page full. It's also a guess wearing a suit. Null propagates into an empty cell that admits it has nothing, and 1 propagates into a market value that a person might bid against.
A default is a guess with better manners.
Nineteen lots on the site deliberately show no value, because two counties don't publish the data I'd need. Nineteen holes I could fill with plausible garbage in an afternoon, and won't.
How to build something that can say I don't know
If you're putting public records in front of people, these are the five habits that did the most work for me.
Give every unknown a reason
unknown on its own is useless to the reader and useless to you in six months. unknown plus "no party address on the docket" tells the reader what to go check and tells you which upstream fix would move the number.
Make refusal a normal return value
Not an exception, not a log line. A refusal is a legitimate result that flows through the same pipe as a positive one, gets stored and gets counted. If you can't count your refusals, you don't know how your system behaves.
Keep the evidence next to the claim
Any conclusion about a person or their home travels with the records it came from, on the same screen. This does two things. It lets the reader disagree with you, and it stops you personally from shipping conclusions you can't source, because there's nowhere to put the citation.
Write the rule before you see the results
I decided "address match or nothing" while the column was still empty. Had I written that rule after watching 1,379 rows land, I'd have negotiated with myself. Everybody does. Pick your threshold before the data can bribe you.
Delete the near-misses, don't park them
A quarantine table is a queue of things you'll eventually publish on a Friday when a design partner asks why the column is thin.
Restraint costs you a demo, and that's the actual price
I want to be straight about the trade, because posts like this usually pretend it's free.
The version that prints all 1,379 court records demos better. It fills the screen. The version I shipped has visible gaps in it, and gaps make a young product look unfinished to anyone glancing at it for thirty seconds. Foreclosure Radar has one design partner, a flipper, and nobody paying for it yet, so I have no revenue argument to hide behind. I paid for the restraint and I'm still paying.
I keep doing it because of what happened on the other product. The auto-apply engine I built filed 1,183 job applications for my own job search, and the best thing in it isn't the applying. It's a check that refuses to start, names the field that's missing, and costs you nothing. That refusal took a month too long to write, and it turned out to be the feature people trust the thing for.
Systems that admit their limits get believed about everything else. That's the whole return.
What to do next
If you're building anything that joins public records to a named human, take an hour this week and do three things.
- Find every field in your output that a name alone can populate, and require a second, independent corroborating signal for each one. An address is a good one. A date of birth is not, if you scraped it from the same document.
- Split your nulls into "looked, nothing there", "looked, couldn't read it", and "didn't look", and make sure the reader can tell which one they're seeing.
- Pick one category of record you'll refuse outright regardless of match quality, and write the rule down today, while the column is still empty.
Then run the thing and count how often it says I don't know. If the answer is never, it isn't careful. It's just confident.
Softery.io runs a handful of small free tools, and none of them fit this subject. If there's one you'd actually use, tell me what to build.
Last updated: September 3, 2026 | Reading time: 11 minutes