DEV Community

Cover image for Building a Romania Mall Store Directory: Every Store in 29 Malls, One Schema
Oaida Adrian
Oaida Adrian

Posted on

Building a Romania Mall Store Directory: Every Store in 29 Malls, One Schema

Romania's shopping malls publish their store directories, but they publish them in eight different ways: one operator runs a headless CMS, another a WordPress store list, another a Laravel JSON blob. Nobody publishes the whole country. So I built an actor that crawls every major operator and emits every store as one consistent record.

The gap

If you want the store mix of every mall in Romania — for retail analytics, site selection, or leasing research — you'd normally stitch together NEPI's site, AFI's site, Iulius's site, and so on, and still end up with per-operator formats that don't join. Worse, nobody flags the anchor tenants: the hypermarkets, cinemas and department stores that actually drive foot traffic. That's the gap: a country-wide directory with one schema and anchor flags built in.

The sources

Each operator publishes its own public store data, and each needed a different reverse-engineering pass:

  • NEPI Rockcastle (19 centres, ~2,434 tenants) — their own Strapi CMS JSON API (cms.<site>/api/tenants), including Mega Mall's 215 stores and Promenada's 182
  • AFI Europe (Cotroceni, Ploiești) — the evx_retailers sitemap
  • Iulius Group (Cluj, Iași, Suceava, Iulius Town) — a shopping index mixing category and store links, disambiguated by nesting
  • Sun Plaza, Veranda, Colosseum — WordPress store sitemaps
  • Băneasa — a Laravel/Inertia data-page JSON payload across 11 pages

The robots.txt lesson deserves a callout: fetching it with Python's default RobotFileParser gets you 403'd by Sun Plaza and Veranda's WAFs — empty rules, everything disallowed. Fetching it with requests and a browser User-Agent works, and the actor then honours the real rules.

The smoke test

Input {"country":"ro","malls":["all"],"includeAnchor":true}:

  • HTTP 201, 3,868 well-formed records in ~86 seconds
  • 0 empty store names, 0 malformed key sets
  • 382 anchor tenants flagged, 2,495 records with floor/unit, 1,499 with phone
  • 29 of 30 registered malls covered
  • All 10 identity fields (mall, store, brand, category, url, …) 100% filled

Run it again and the first_seen / last_seen / is_closed fields turn the directory into a vacancy and churn time-series — which stores opened, which closed, which anchors moved.

The honest bits

  • ParkLake is WAF-gapped from the cloud. Sonae Sierra serves an HTTP 202 JavaScript-challenge page to datacenter IPs for its API, sitemap and archive alike; even curl_cffi impersonation hits the wall. From a residential or proxy IP the REST API returns ~223 stores with full detail. The adapter tries hard, then logs the challenge honestly instead of faking data — attach a residential proxy if you need ParkLake from the cloud.
  • is_closed only becomes meaningful from the second run — the first run is your baseline.
  • Fields an operator doesn't publish come back as empty strings, so exports stay clean but sometimes thin.

Try it

👉 Romania Mall Store Directory on Apify Store

More from me

While you're here, these might be worth a read:

Top comments (0)