NEVERENDERSEditorial channel

Dispatches / Engineering

Engineering · 2026-07-25 · 7 min

Reading 239 addresses off a site that does not publish them

One operator ships clean structured data. The other ships a JavaScript app with none. The second parser worked, and quietly corrupted 187 of 239 records in a way that looked fine until a search returned one result instead of five.

Building a directory of plasma donation centres means getting several hundred real addresses out of several hundred pages that were not designed to give them to you. Two operators, two completely different problems.

The easy one

CSL Plasma publishes a schema.org PostalAddress block as JSON-LD on each centre page. Street, locality, region, postal code, phone, all as discrete labelled fields. This is the case the entire structured data standard exists for, and it took very little work: fetch, locate the JSON-LD, read the fields. 291 centres, effectively no parsing risk, because nothing was inferred. Every value came out of a field whose meaning was declared.

The hard one

BioLife runs a Next.js application. There is no address JSON-LD to read. What there is, once the page is rendered, is a stable textual shape:

Plasma Donation Center <street> <City>, <State> <ZIP> Get Directions <phone>

That is parseable, so we parsed it. A regular expression with a non greedy street group, then city, then a two letter state, then five digits. It ran across all 239 centre pages without a single error and produced 239 complete records.

It was also wrong 187 times.

How a working parser produces broken data

The bug is in the interaction between two innocuous choices. The street group was non greedy, meaning it matches as little as it can. The city group allowed spaces and full stops, because real city names contain both, as in St. Louis and Port St. Lucie.

Given 2005 W Greenway Rd. Phoenix, Arizona 85023, the non greedy street group takes the minimum it can get away with, and the permissive city group happily absorbs the rest. The result is a street of 2005 W and a city of Greenway Rd. Phoenix.

Every field is populated. Nothing throws. The record looks complete. And the city is garbage, which means the generated URL is garbage, the page title is garbage, and that centre is filed under a city that does not exist. We also produced Auto Mall Dr Tucson, Hampton Ave. Mesa and Way St. Cloud, among others.

This is the failure mode worth internalising. A parser that crashes tells you it failed. A parser that returns plausible nonsense does not, and a completeness check will pass it, because every field is full.

What actually caught it

Not a test. A search for centres in Phoenix returned one result, and Phoenix is a city where that number was obviously implausible. The symptom surfaced two layers away from the cause, in a feature nobody thought was related to scraping.

The repair, without re-scraping

The useful realisation was that no information had been lost. The boundary between street and city had been misplaced, but street + " " + city still contained the complete address. The problem was knowing where to cut.

The answer came from data already in hand. Every record has a ZIP, and the directory was already joining ZIPs against the GeoNames postal dataset to get coordinates. GeoNames also carries the authoritative place name for each ZIP. So: take the full blob, walk backwards from the end looking for the GeoNames city name, and cut there.

Two refinements were needed. The comparison has to normalise Saint against St., Fort against Ft. and Mount against Mt., or ten records fail to match on an abbreviation. And where GeoNames could not settle it, a fallback peels known street suffixes and bare compass directions off the front of the city field.

The repair only fires when GeoNames actually disagrees with the parsed city, so correct records are never touched. 187 records repaired, and the assertion that guards it now requires three numbers to be zero: cities beginning with a street suffix, streets containing no digit, and centres missing coordinates.

Two things worth keeping

First, prefer declared data over inferred data, and accept a smaller dataset to get it. The 291 records read out of JSON-LD needed no repair, because nothing about them was guessed. The 239 inferred records needed 187 repairs.

Second, when you must infer, find a second independent source to check the inference against. GeoNames was originally added for coordinates. It ended up being the thing that made the address parse trustworthy, because it knew something the page text alone could not settle. A parser checking its own output against itself will always agree with itself.

Record counts are this group's own figures from the build of 25 July 2026. Coordinates and place names come from the GeoNames postal dataset, used under CC BY 4.0.

Also published