This episode walks through scraping, enriching, and cross-checking ten thousand Warsaw Airbnb listings against pretty much every registry Poland has, just to find out who actually owns this market, and the results turned out to be boring and completely unsurprising.
Go directly to the scrollytelling

If you haven't seen part one yet, it's still up


If you follow this blog, you already know I like data, and I like it even more when it tells a story, presented in a format people can actually follow, not just a spreadsheet dump. This episode picks up the lead from part one and takes a look at short-term rental apartments in Warsaw.
Think of it as a guided tour through the sources, the annoying obstacles, the data clean up, and the visualization decisions behind a Airbnb in Warsaw.
But before any of that, thank you to everyone who came to my workshop on critical infrastructure. Great students, great questions, and I appreciated the chance to be there.

I know you would preferer food tasting though.
Introduction
Part one focused on politicians and their ownership stakes in companies, with a particular eye on a real estate. It's a topic that matters well beyond Poland. Access to housing is a hot subject across most of the West right now. That piece was about figuring out which politician holds the biggest stake in real estate, and how you could go about pulling and cross-referencing that kind of data yourself.
This time we're staying in real estate, but narrowing in to short-term rentals specifically, the segment the Polish government is currently trying to fight with
https://legislacja.rcl.gov.pl/projekt/12405554/katalog/13177393
so far without much success. The draft law (known in the legislative process as UC135) would introduce no-rental zones in residential buildings, require sign-off from fellow residents before a unit can be listed short term, and generally try to shrink the grey area the market currently operates in. It's been bouncing between committees and public consultations for months.
Instead of starting from people and their companies like in part one, this time we're flipping the approach and start from the listings themselves. Who has the most apartments up on Airbnb, follow the corporate trail behind them, and look for connections between companies that share an address, a phone number, or an email, then present all of it as clean, understandable scrollytelling in D3.js.

Methodology
Sometimes I get a thought stuck in my head and the only way to get rid of it is to go find the actual answer. What share of Warsaw's short-term rental listings belongs to companies, and what share to private individuals, and who actually runs this market? Chasing that question meant I first needed to get my hands on every single listing.
That's harder than it sounds. Airbnb doesn't expose a clean "give me everything" endpoint, and the map view caps out at 15 pages of 18 results each (270 listings, tops) no matter how many hundreds or thousands actually exist in the area you're looking at. My workaround was to grab Warsaw's official bounding box coordinates, chop it into 6 smaller tiles, and paginate through each tile separately (zooming in further wherever a tile still hit the cap on its own). That's how I ended up with 10,047 short-term rental listings in Warsaw. For context, a DELab UW
https://delab.uw.edu.pl/wp-content/uploads/2024/09/prezentacja_airbnb_delab_v3_compressed.pdf
on the same city put the number at 9,631. So the market's grown by roughly 400 listings in a year and a half, which is its own small story.
Worth calling out that none of this research would even be possible without a fairly recent EU rule that forces platforms like Airbnb to collect and display a business registration number for any host operating as a company. Without that, there'd be nothing to enrich in the first place: just a name and a phone number. That data only shows up on the individual listing page, not in the search results, so I still needed a decent chunk of horsepower (and proxies) to pull it at scale.

Getting the initial dataset together wasn't too painful once the tile-splitting trick was sorted. Here's roughly what one raw record looked like straight out of the scrape (trimmed for readability, the real ones carry a full photo gallery and more host metadata)
{
"listing_id": "1698446444052177806",
"url": "https://www.airbnb.com/rooms/1698446444052177806",
"title": "Apartment in Mokotów",
"subtitle": "Comfortable flat next to SGH and Pole Mokotowskie",
"rating": "5.0 (5)",
"price": "zł 1,070",
"latitude": 52.2086,
"longitude": 20.9994,
"photos": ["https://a0.muscache.com/im/pictures/hosting/.../original/....jpeg", "..."],
"host": {
"name": "Aleksander",
"is_superhost": false,
"is_verified": true,
"rating_average": 4.62,
"rating_count": 3828,
"years_hosting": { "years": 8, "months": 11 },
"is_airbnb_managed": false
},
"is_business": true,
"business_details": {
"business_name": "XXXXXXXXX Spółka Z Ograniczoną Odpowiedzialnością",
"email": "XXX@gmail.com",
"phone": "+48 XXXXXXX",
"address": "XXXXXXX",
"business_registration_number": "PLXXXXXXXX"
},
"cohosts": [
{ "name": "Maciej" },
{ "name": "Błażej" },
{ "name": "Jakub" }
]
}At that point I could throw all 10,047 points on a single map and see the whole city at once, instead of tile-hopping and zooming the way Airbnb's own interface forces you to.

Just from that, you can already cluster by company vs. private host, and you have to start caring about the difference between a private individual, a sole proprietorship (JDG), and a proper limited-liability company (Sp. z o.o.). Airbnb treats them differently, and so does Polish company law.
I could've stopped right there. Dedupe on business_name or business_registration_number, count them up, and I'd already have an answer to the question that started this whole thing off, but it's kind of boring.
The real story is in who's actually behind these registration numbers. So the next step was enriching everything against Poland's National Court Register (KRS, Krajowy Rejestr Sądowy). Even at this early stage, some connections started popping out on their own: different company names sharing the exact same phone number or email address. More on that later.
There are two ways to pull KRS data, the official government API, or a third-party wrapper. I went with the latter (rejestr.io). It doesn't care whether you passit a KRS number, a NIP, or a REGON and it just tries to match whatever you give it, which meant I had to do a proper clean up and normalization pass first, since Airbnb's business_registration_number field is a free-text mess, sometimes a 10-digit KRS number, sometimes a NIP with or without the "PL" prefix, sometimes a 9-digit REGON, occasionally something else entirely.
Only actual companies (Sp. z o.o. for limited liability, spółka komandytowa for limited partnership, spółka akcyjna for joint-stock) have a management board you can look up. Sole proprietorships (JDG) mostly just return an activity type and a registration date, and the owner's name is already sitting right there in the company name, so I excluded those from this enrichment pass and only ran full lookups on the actual LTDs.
{
"business_details": {
"business_name": "XXX Spółka Z Ograniczoną Odpowiedzialnością",
"registry": {
"source": "rejestr.io",
"matched_by": "direct_number",
"match_confidence": "exact",
"data": {
"adres": { "miejscowosc": "Warszawa", "ulica": "XXXX", "kod": "00-029" },
"glowna_osoba": { "id": "XXX", "imiona_i_nazwisko": "Aleksander XXXXX" },
"numery": { "krs": "XXXX", "nip": "XXXX", "regon": "XXXX" },
"stan": {
"czy_dofinansowana_przez_ue": false,
"czy_jest_na_gpw": false,
"czy_otrzymala_pomoc_publiczna": false,
"czy_wykreslona": false,
"pkd_przewazajace_dzial": "Zakwaterowanie"
}
}
}
}
}Inside business_details we now have a whole new registry object dumped straight from the third-party source. glowna_osoba is the "main person" on file (usually the president), plus the full company number set, and a bunch of useful boolean flags: whether the company is EU-funded, publicly listed, received state aid, or has been struck off the register (czy_wykreslona).
Still not the end of it, though. The endpoint I was using only returns that one main person with no shareholders, no wider board, nobody else. So I went through the whole dataset a second time against a different endpoint and collected a people array from each company:
"people": [
{
"tozsamosc": { "imiona_i_nazwisko": "Bartosz XXXX" },
"krs_powiazania_kwerendowane": [
{ "typ": "KRS_BOARD", "opis": "CZŁONEK ZARZĄDU", "data_start": "2024-06-04" },
{ "typ": "KRS_SHAREHOLDER", "data_start": "2024-06-04" },
{ "typ": "BENEFICIARY" }
]
}
]
Now the dataset knows who's actually a board member, who's a shareholder, and who's a beneficial owner, and since when. It also includes connected entities to the person, so we have a full board, and other companies they are associated with.
That's already a big, messy pile of potential leads, and I still hadn't touched addresses. If two companies share a registered address and it's not an obvious virtual-office building, that's worth a closer look before you write it off as a coincidence.

Sources
Polish corporate transparency is honestly pretty good, all things considered. Once I had the company/people graph built, I wanted more than "these people exist and know each other." I wanted either confirmation or a documented absence in a few specific registries, because either one is a useful signal. I wasn't trying to catch anyone doing anything wrong, to be clear. The goal was mapping the corporate structure behind the biggest operators, and specifically poking around for lobbying ties and conflicts of interest.
Every person and company that made it into the final dataset got checked against:
- https://wl-api.mf.gov.pl/ (Wykaz podatników VAT / "White List") - confirms whether a company was ever removed from the VAT register for fraud-adjacent reasons (fictitious invoicing), versus ordinary administrative housekeeping like a suspended registration or a missed filing.
- https://crbr.podatki.gov.pl/ - the central register of beneficial owners, the actual humans who ultimately control a company, not just whoever's listed as president.
- https://www.saos.org.pl/ (System Analizy Orzeczeń Sądowych) - public court-ruling search API, to check whether a company or person shows up in any published Polish court judgment.
- https://jawnepartie.pl/ - political donation registries, individual donations to political parties above the legal disclosure threshold (10,000 PLN a year in Poland; corporate donations to parties are outright illegal here, so this angle only ever applies to individuals).
- https://przetargi.egospodarka.pl/ - public tender records, i.e. whether a company ever won government contract money.
- EMIS, plus each company's own filed annual financial statements at KRS, for revenue and profit figures.
- https://mapadotacji.gov.pl/ - EU funding transparency data, whether a company received EU grants, and for how much.
On top of that there are the EU's consolidated financial sanctions list (for anyone with a Russian or Belarusian sounding name or connection) and the ICIJ Offshore Leaks database (Panama Papers, Pandora Papers, and the rest), matched on full name rather than surname alone.
All of these sources are free, and every one of them mattered for this investigation, including the negative results. No hits in political donations or public tenders is itself a meaningful finding and it means there's no documented lobbying money or government contract angle to chase for this particular set of people.
Worth being honest about the friction, though, some of these sources have no API at all, some sit behind bot-detection you have to respect rather than fight, and a couple only really work through a government-ID login a script can't use.
Beyond the automated registry checks, the majority of the companies and people that actually mattered got a manual pass too - press interviews, conference appearances, any public trace of a meeting or connection with a politician.
Quick translation of what each source actually proves, the VAT whitelist tells you a company hasn't been struck off for issuing fake invoices. CRBR tells you who really owns a company, beneath the president's name. Court records tell you whether a company has ever been party to a published ruling. Political donations tell you whether a person has funded a party above the disclosure line. Public tenders tell you whether a company has ever won, and how much, government contract money.
If you're doing this for other country, expect a different toolkit because every government publishes a different data in various format, and with different permissions to access it.

At some point I deliberately stopped enriching further, because I could tell I was heading into more nulls than real matches. Instead I started a separate file, research_findings.json, and logged every confirmed match from these registries plus everything found manually, rather than trying to add it all into the main dataset.
By this stage the dataset had been enriched twice over with listings, host info, the KRS record for each company, everyone connected to that company, and all of that cross-checked against the sources above. You'll see what actually came out of it, who's in there, and what it means, in the scrolly telling piece itself.
One thread genuinely led outside the dataset entirely. Following the ownership structure of an entity connected to (but not itself part of) our 10,047 listings, all the way back to its parent fund in Denmark. So the research didn't stay neatly inside the original scrape and a few leads pulled it well past the edges of the dataset.
Looking for patterns
Before any real comparison work can start, the dataset needs cleaning up. The worst offender is business_registration_number as scraped straight from Airbnb. It can be a KRS number, a NIP, or a REGON, and nothing tells you which. So step one was recognizing which format each number actually was, then cross-referencing it against phone number, name, email, and business name to merge everything that clearly pointed at the same real-world entity into one node with shared contact points.
This is the point where the data starts actually talking back to you. You start enumerating unique names, numbers, and people, checking who else they're connected to, and keep finding the same shapes repeating. This kind of structured, well formatted dataset is a good fit for handing to an LLM. It understands the shape and the Polish legal entity vocabulary well enough to spot patterns fast. That said, there's still real fun in writing the correlation code yourself.
Whenever a match turned up in any of the source categories above, I will go one level deeper into that specific company and everyone connected to it. That part I wouldn't hand off to an AI. It's enjoyable manual work, and it forces you to think differently. You have to keep every name you've already seen in your head and watch for it resurfacing somewhere new.
My main focus throughout was lobbying ties and foreign capital. Even the tiniest operator, sometimes just one or two Airbnb listings, often turns out to run its own dedicated booking site once you look past the platform, the way one of the companies featured in the storytelling piece does.
Digging this deep into all 207 companies (and deeper still for the sole proprietorship cases, since JDGs don't have a board to look up) takes real time. What's in the final piece is a sample of what's possible, meant to show you the technique rather than exhaustively cover every single operator. If you want the same treatment for a different city, or all of Poland, I can build that dataset and run more checks on request.

Connections
This is where it all starts clicking and the fun part begins. Once the patterns are visible, you start folding different listings into a single host, merging officers of different-looking companies into one shared node, and the map of "who actually owns this market" stops being a hunch and starts being something you can point at.
A few concrete shapes that kept turning up:
- Shared-address clusters that turn out to be a genuine shared office building rather than an ownership link. You have to check before assuming.
- Company names on accounts flagged "private". Handful of hosts marked private simply put a company name as their own display name anyway. One of them had 168 listings under a brand with its own website and still never got flagged as a business.
- Shared phone/email fingerprints across formally unrelated entities. The same phone number and mailbox showing up next to a consumer brand, an Sp. z o.o., two family foundations, and a sole proprietorship, all of which look unrelated on paper until you notice they all use the same contact line.
- Multi-hat officers who reveal the real structure sitting above a company. Pulling not just a company's own board, but every other board seat each of those officers holds, is what actually surfaces the institutional layer.
- GPS co-location for accounts with no address field. Private hosts never get a published street address on Airbnb, only listing coordinates. Rounding those to building-level precision and clustering by host gives you the same "who's actually operating together" signal.
- PKD mismatches, by far the most common pattern in the whole dataset is that small Polish sole proprietors and companies whose actual registered business has nothing to do with real estate at all.
Scrolly telling
For data-driven investigations, I think the best way to present the results is one that lets the reader go on the same journey you did, rather than handing them a wall of numbers. It's a win-win. I get to walk through the research narratively instead of dumping a spreadsheet, and the reader gets to follow the logic step by step and actually see the underlying data as they go, instead of just taking my word for it.
The actual build is a quite hybrid, I used Canvas for the 10,047-point map, layered under D3.js-driven SVG for everything that needs to be sharp and interactive like network graphs, the ranking treemap, the timeline. A scroll-scrubbing loop ties scroll position directly to each scene's animation progress, so panning, zooming, and fading a graph in all happen exactly in step with how far down the page you've scrolled. I had idea and each slide in my mind, and Claude help was very appreciated.

Summary
So here's the final summary - 10,047 listings, six-plus million lines of JSON, 207 companies and 439 people, run through nine different registries, sanctions lists, and leak databases. The result gave no confirmed fraud, no sanctions matches, nothing dramatic.
What you get instead is a pretty detailed, well-sourced map of who actually owns Warsaw's short term rental market, how top heavy it is, and a couple of real, documented threads tying into the lobbying push around the incoming UC135 law. Most of this market runs on ordinary paperwork, it means family businesses, one company per building project, professional stand-in directors, and one guy listing a spare apartment on the side of his hair salon. Stack enough of that on top of itself and you end up with a market where twenty companies control almost half of everything on offer, through paperwork.
You can find the scrollytelling here

