Every Romanian company is registered in the ONRC (the national trade register), and the state publishes the whole thing as open data on data.gov.ro. That's 4.2 million companies, 19.3 million CAEN activity records, 3.68 million legal representatives and 4.64 million status entries — a genuinely useful corpus for lead generation, market analysis and "who actually owns this company" lookups.
The catch: it ships as a pile of enormous CSVs, and the moment you try to query it, Romanian orthography punches you in the face. I built a small free API around it, and this is how.
The source
No scraping, no vendor. The official snapshot is a set of CKAN files, the biggest of which is OD_FIRME.csv at ~690 MB and OD_CAEN_AUTORIZAT.csv at ~425 MB. Combined, raw, that's well over a gigabyte of flat files. Loaded into SQLite with a full-text index it becomes a 3.6 GB database that answers a search in milliseconds.
The build
The load is boring and that's the point: stream the CSVs into SQLite, then create an FTS5 virtual table so lookup_business("dedeman") is a real full-text query rather than a LIKE '%dedeman%' scan. Two tables matter most:
-
firme— companies, keyed by CUI (the tax identifier) and registration code -
reprezentanti_legali— the legal representatives, so you can search by person to find every company a director is attached to
On top of that sits a thin FastAPI wrapper exposing two read-only endpoints — no auth, no keys:
# by company name or CUI
curl "https://onrc-api.adrianhomelab.com/lookup_business?query=DEDEMAN&max_results=3"
# by director / legal-representative name
curl "https://onrc-api.adrianhomelab.com/lookup_director?name=popescu"
The traps (this is the real content)
Three things will bite you, in order of pain:
1. Diacritics. Romanian uses ă â î ș ț, and the registry stores them faithfully. Popescu and Popéscu are different strings to a naive matcher — but a human searching for "Popescu" expects both. The FTS index has to be diacritic-insensitive: normalize ș→s, ț→t, ă→a on both the indexed text and the query. Miss this and your "obvious" search returns nothing for half the surnames in the country.
2. CUI vs registration code. Every firm has a CUI (digits, sometimes zero-padded) and a registration code like J1992002621040 (court + year + serial). They are not interchangeable, and newcomers constantly pass one where the API expects the other. Supporting both on a single query parameter means testing numeric input against the CUI column first, then falling back to the registration code.
3. CSV scale and encoding. The raw dumps are ;-delimited and large enough that csv.DictReader in a tight loop will keep you up all night. Stream with csv.reader, batch your executemany inserts, and watch your transaction size — a single 690 MB file has to be committed incrementally or SQLite's WAL balloons.
What came out of it
A live, free endpoint:
GET /lookup_business?query=<name|CUI>&max_results=NGET /lookup_director?name=<name>&max_results=N-
GET /docs— interactive OpenAPI docs GET /health
The same data is also available as an MCP server (for AI agents) at https://hermes.adrianhomelab.com/mcp, and the code is open. If you're doing anything with Romanian business data — or you just want a worked example of turning a big government CSV dump into a queryable service — the endpoint is live and free to hit. Source code and loaders are on GitHub: https://github.com/darksider4all/leadgen-mcp.
Top comments (0)