Howell & Gibbs · Open source

HPD lookup


Context

Designer & Builder · 2026

Studio: Howell & Gibbs

Services: Content design · API design · Front-end development · Open-source maintenance · Civic tech

Ships as: @howellandgibbs/hpd-lookup on npm · Source on GitHub · MIT licensed

Demo: hpd-lookup.howellandgibbs.com · v1.1.0 published

Companion study: Tenant Triage NYC, where this code started

Typefaces: Inter and Domine on the demo site

The hpd-lookup widget showing violations for a Brooklyn address, each one with a plain-English status, a hazard class, a date, and the repair the landlord was ordered to make

  • Extracted the housing-violation parser from Tenant Triage NYC and published it as a standalone open-source package, so tenant organizations, legal aid providers, and journalists can present NYC violation records in plain English without writing their own translation layer
  • Audited the parser against thousands of live records during the extraction, correcting three long-standing faults and a status map that reported roughly 48,700 open violations as resolved
  • Shipped an embeddable <hpd-lookup> web component with no framework dependencies, themed entirely through CSS custom properties, so a host site can offer the lookup in its own brand

My contributions

Content design

Decided what a tenant needs to see from a violation record, and wrote plain-English labels for all 23 status codes HPD uses. This translation layer is the piece I could not find available as a reusable package.

Data correctness

Audited the extracted parser against thousands of live records: ran the old and new versions side by side, counted every difference, and read every case where the output got shorter. That process surfaced all three faults carried over from the original.

API & package design

Designed the public surface: three lookup functions, one error type with a code for every failure mode, and zero runtime dependencies. Ships as ESM and CJS with TypeScript types, for Node, Deno, Bun, and the browser.

Widget & theming

Built the embeddable custom element and its theming tokens, then tested the token set by styling the element to two real brand guides, one of which does not allow bold body text. The tokens exist so that rules like that can be honored from outside the widget.


Summary of work

New York City publishes every HPD housing violation as open data. It is public in the legal sense, but difficult for many users to read and understand: filled with all-caps prose with the legal citation fused to the front of the sentence, and a status code sitting beside it that reads NOV CERTIFIED LATE (what does that even mean?!).

A typical status lookup returns this:

§ 27-2005 ADM CODE REPAIR THE BROKEN OR DEFECTIVE PLASTERED SURFACES AND PAINT
IN A UNIFORM COLOR AT EAST AND WEST WALLS IN THE 2ND ROOM FROM NORTH LOCATED AT
APT 4B, 1ST STORY

A tenant trying to work out whether they can add prior violations to their report has to decode all of that first.

Several good tools will show you the violations on a building: JustFix's Who Owns What, Openigloo, and HPD's own search all handle the lookup. What I could not find was a reusable, open-source translation layer for the records themselves.

I had already built one version of this parser for Tenant Triage NYC. This project extracted it into a standalone package, corrected it against the live data, gave it a documented public API and an embeddable widget, and published it under an MIT license so the next tenant tool can start from a working parser instead of from the raw records.


Process

Scoping the package around the parser

The first decision was what the package should be. A general-purpose client for NYC's housing data would have been the conventional choice, but the city's endpoints are already free, unauthenticated, and well documented; wrapping them would add a dependency without adding much value.

The genuinely difficult work is turning a raw record into a readable sentence. So the package is organized around that. parseViolation() takes a raw record and returns a description, a location, a plain-English status, an open or closed state, a severity, and a rent-impairing flag. The lookup functions are conveniences layered on top. There is no hosted service in the middle: the package calls the city's endpoints directly from wherever it runs, so there is no server for me to maintain and no uptime for anyone else to depend on.

// what HPD returns
{
  "novdescription": "§ 27-2005 ADM CODE REPAIR THE BROKEN OR DEFECTIVE …",
  "currentstatus": "NOV SENT OUT",
  "violationstatus": "Open",
  "class": "B"
}

// what the parser returns
{
  "description": "Repair the broken or defective plastered surfaces and paint …",
  "location": "Located at Apt 4B, 1st story",
  "status": "Notice sent to landlord",
  "state": "open",
  "severity": "Hazardous",
  "rentImpairing": false
}

Stripping the citation without losing the instruction

Every violation description opens with a legal citation, and there is no consistent format for them. These are all real examples from the live data:

§ 27-2005 ADM CODE …
HMC ADM CODE: § 27-2017.4 …
D26-10.01, 10.05 ADM CODE …
§ 27-2045(B)(1)(A) HMC, § 12-01, § 12-03 RCNY …
28 RCNY § 25-171; & 67 (7)(B) MDL; NYC FIRE CODE § 703.1.3; …

A pattern written for every known citation format would never be complete, so the parser relies on a property of the records instead: violations are instructions to a landlord, and instructions almost always contain an imperative verb. The parser walks the description word by word and cuts at the first HPD action verb it finds, drawing on a list of repair, abate, exterminate, provide, and about sixty others. When no verb appears, it falls back to the first substantive word after the citation material, which covers records that are prose from the start, like OWNER FAILED TO FILE A VALID REGISTRATION STATEMENT.

If neither rule fires, the description comes back whole. That was a deliberate content-design call.

Measuring accuracy

I audited the parser by running the old and new implementations over 6,996 real records, counting every output that differed, and reading every case where the result got shorter (shorter is where content goes missing).

That audit found three faults, all of which had been shipping in the original version:

  • Verbs joined to the citation by a colon were being dropped, so HMC:FILE ANNUAL BEDBUG REPORT lost its leading "File". Roughly 7.5% of records were affected by this.
  • Two-letter HPD shorthand was read as prose, so ADM CODE AW PROVIDE ADEQUATE LIGHTING came out beginning "Aw provide adequate lighting".
  • When a record opened with several citations chained together, the original could cut partway through the chain, leaving the later citations in the description text.

None of the three would have been caught by a test suite written from the documentation. They only became visible when the parser was evaluated against the data as it actually is.

Status from the data, not the wording

Status translation turned out to be the area that needed more work than anticipated. HPD emits 23 distinct status values, and the original map only covered 12 of them. Two of its keys matched nothing in live data at all.

The important part of a violation record is not the wording of its status, it is whether it's still open: status text as it's delivered turns into a generally unreliable guide to that. INVALID CERTIFICATION and FALSE CERTIFICATION both read as resolved, but they mean the landlord claimed the work was done and HPD disagreed, so the violation is still open. Together they cover about 59,000 records. A parser that keys on the word "invalid" gets the first of them exactly backwards, which accounts for about 48,700 records. It lands on the right answer for the second use case only because the word it matches on happens not to appear there. The solution was to map the codes explicitly instead of pattern-matching their text.

Two more things the live data showed that the documentation does not:

  • VIOLATION WILL BE REINSPECTED is genuinely ambiguous. Across the dataset it splits roughly 57/43 between closed and open, and nothing in the text distinguishes the two. The package reads open versus closed from HPD's own violationstatus field instead.
  • HPD's own spacing is inconsistent in a way that matters. FIRST NO ACCESS TO RE- INSPECT VIOLATION has a space after the hyphen; SECOND NO ACCESS TO RE-INSPECT VIOLATION does not. The original code matched only the unspaced form and silently missed 325,000 records. Status keys are now normalized before lookup.

When the package encounters a status code it does not recognize, translateStatus marks the result known: false instead of guessing, so a caller can qualify the wording rather than present a guess as fact.

Restoring sentence case

Changing all-caps descriptions to sentence case required affordances for acronyms and unit designators. The sentence-caser restores about forty agency acronyms and legal codes (HPD, HMC, MDL, DOB) to uppercase, and puts unit designators back together, so APT 4b becomes Apt 4B.

Addresses needed their own formatting rule. GeoSearch returns the street portion of an address in all caps and the rest in normal case: 1742 EAST 172 STREET, Bronx, NY, USA. The sentence-caser built for violation text can't fix this, because it's designed for prose, where a word following a number (like 40 watts) should stay lowercase; running an address through it turns the whole street name lowercase.

So the package includes a separate formatAddress() function just for addresses. It converts the all-caps street portion to title case (1742 East 172 Street), keeps NY and USA uppercase, lowercases small joining words inside a street name (1 Avenue of the Americas) while still capitalizing a word that directly follows the house number (1 The Bowery), and leaves house numbers, ranges, fractions, and ordinals unchanged. I checked the results against 1,200 real street names from the violation data: none came back still in all caps, and none had a word incorrectly lowercased.

McDonald Avenue has its own affordance. Mac names deliberately are not capitalized, because MACON, MACY, MACE, and MACDOUGAL all appear in the HPD data and there's nothing to distinguish which should be capitalized and which shouldn't. That limitation is written into the documentation.

Try it

This is the published package, running here, themed with the same tokens any host would use.

Loading the widget…

Live data from NYC Open Data and Planning Labs GeoSearch. Plain-English labels are a translation, not legal advice.

One component, any brand

An npm package serves developers, and many of the organizations this work is for do not have developers on staff. So the second half of the project is an embeddable custom element: one script tag, one <hpd-lookup> tag, no framework, no build step.

Everything visual is themed through CSS custom properties set on the host. Nothing reaches inside the shadow root, so a host cannot break the widget by restyling it and I cannot break a host by changing the styles. I styled the same element using two real brand guides to confirm it worked as intended.

The same hpd-lookup element rendered twice side by side, once in the Howell & Gibbs brand and once in the Tenant Triage NYC brand, with the CSS custom properties that produce each look shown underneath

A release process other tools can rely on

The package follows semantic versioning and keeps a changelog that records the reasoning behind each release rather than a list of commits. The test suite includes over 100 tests, none of which touch the network; upstream responses are mocked, and the parser fixtures are real records captured from the live dataset, covering every status code.

The releases publish from CI through npm trusted publishing, so no long-lived token exists.

Version 1.1.0 added a required field to the Building type. That change was included in the changelog under its own entry so it's easier for folks to see the change was made.


Outcomes & impact

@howellandgibbs/hpd-lookup is published on npm at v1.1.0, MIT licensed, with a demo site showing a real building in each borough and a raw-versus-parsed comparison.

Shipped to date: the parser and status translation, three lookup functions over Planning Labs GeoSearch and the HPD violations dataset on Socrata, a single error type covering every failure mode, the <hpd-lookup> web component with its theming tokens, and a release pipeline that publishes from CI.


Learnings

Verification was most of the work

Pulling working code out of a working product looks like a packaging task, but most of the real work here was verification. The original parser's failures were invisible in normal use: a description missing its first verb still reads as a sentence, and a violation wrongly labeled resolved still renders correctly on the page. Auditing against thousands of live records is what made the faults visible; a test suite written from the documentation would have shipped all three of them again.

The missing layer in public records data

Rewriting NOV SENT OUT as "Notice sent to landlord" is a writing task, but each of those labels is also a claim about someone's housing situation. That is why unrecognized codes are flagged instead of guessed at, why the open state comes from a dedicated field rather than from the words, and why the documentation says plainly that the labels are a translation and not legal advice. Working with public-records data means being accountable for what each sentence asserts, not only for how it reads.

If the solution doesn't exist, build it

One of the first things that really stuck with me when I started working at Google was the idea that if you see a gap that you can fill, you should feel empowered (and even responsible!) to fill it. I applied this same thinking to this work. The parser was the part folks were either building quietly or missing completely, so I filled the gap I saw.