Skip to content
back to portfolio

Culmination Digital LLC2026

SoberMark, a national addiction-treatment directory built from public data

Public directory of 10,900+ addiction-treatment providers across all 50 states, assembled from 20 federal and state data sources by a scheduled Python ingest pipeline. Records from different sources are matched and merged into one provider each, geocoded, and validated before they reach a PostGIS search with full-text ranking and a typo-tolerant trigram fallback. English and Spanish.

Founder, sole engineerNext.jsTypeScriptPostgreSQLPostGISPythonStripeVercel

Live: SoberMark, the live directory

One of two products I build and run solo under my LLC, Culmination Digital. Source is private.

Read the build story: Building SoberMark

SoberMark is a public directory of addiction-treatment providers, searchable by location, level of care, insurance, and a dozen other filters. 10,900+ providers across all 50 states, assembled from public federal and state datasets and kept current by a scheduled Python pipeline. It ships in English and Spanish.

Why it exists

A person looking for treatment usually starts at a search engine and lands on a directory. The useful facts (where a provider is, what level of care it offers, what it accepts) already exist in public federal data, but they are scattered across sources that disagree with each other. SoberMark is the work of pulling those sources into one clean, searchable record per provider.

How it's built

Next.js 16 App Router on Vercel, TypeScript strict, in a pnpm monorepo with a Python ingest app beside the web app. Postgres with PostGIS does the heavy lifting.

Ingest. 20 source connectors cover SAMHSA, NPPES, federal exclusion and debarment lists, CMS, and state licensing boards. Addresses are geocoded through the Census batch API. A monthly scheduled job refreshes the core federal sources against a throwaway database first, then production, then refreshes the search views and runs data validation: the run fails if the provider count or state coverage drops below a safe floor, and it also checks geocoding and how recently each source was ingested.

Combining sources. The same facility appears in several datasets with different names, phone numbers, and address formats. Records are matched in a fixed order: exact NPI first, then phone and state only when the city and name or street and ZIP also agree, then a normalized name, city, and state. That middle rule exists because an early version merged providers across cities on a shared phone number. The higher-confidence source wins each field, and every record keeps the list of sources it came from.

Search. Radius search runs on PostGIS geography. Text search uses Postgres full-text ranking and falls back to trigram similarity when someone misspells a name or a city. Results come from a materialized view so the request path stays fast, with filters for level of care, insurance, telehealth, language, medication-assisted treatment, and more.

Data management. 239 forward-only migrations, row-level security on the tables, and pre-merge scripts that validate pending migrations and check environment parity. Claude is used in a few back-office paths, such as summarizing listings and assisting with ownership-claim reviews; the claim assistant is built so it can never approve or reject a claim on its own.

The trade-offs

Building from public data means the directory is broad before it is deep. A listing can be right about address and services and still know nothing about today's waitlist. Curating by hand does not scale to ten thousand providers for one engineer, so I chose breadth with honest labeling, plus a path for providers to correct or enrich their own record.

What I learned

How much of a data product is deciding which source to believe. The search is the visible part; the matching rules, the validation floors, and the migrations underneath are where the real work went. The full story is in the build post.