Mall Store Directory — Multi-Country, Unified Schema avatar

Mall Store Directory — Multi-Country, Unified Schema

Pricing

from $3.00 / 1,000 store record extracteds

Go to Apify Store
Mall Store Directory — Multi-Country, Unified Schema

Mall Store Directory — Multi-Country, Unified Schema

Extract store directories from major mall operators across the US, UK and Europe (Simon, CBL, Landsec, Westfield/URW, NEPI Rockcastle, AFI, Iulius and more). One unified schema with mall, country, operator, floor, category, contact data and anchor-tenant flags.

Pricing

from $3.00 / 1,000 store record extracteds

Rating

0.0

(0)

Developer

Oaida Adrian

Oaida Adrian

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

19 hours ago

Last modified

Share

Mall Store Directory — Multi-Country, One Dataset

Extract public mall tenant directories from the configured Romanian, US, UK and Hungarian mall registry into a consistent 20-field dataset. Sources include NEPI Rockcastle, AFI Europe, Iulius Group, independent Romanian malls, Simon Property Group, CBL, Landsec and Westfield.

A default run is a small demo, not a full-country crawl: {} selects only mega-mall-bucuresti. Registry membership is not a guarantee that a source is reachable or that its directory is complete. Inspect the run's COVERAGE key-value record alongside the dataset.

Input

Quick demo (also the effective empty-input default)

{
"malls": ["mega-mall-bucuresti"],
"extractDetails": false,
"mallTimeoutSeconds": 75
}

Explicit full registry

{"countries": ["all"], "mallTimeoutSeconds": 75}

{"malls":["all"]} also selects the whole registry. Full runs are sequential and may take much longer than a demo; configure an adequate overall Actor run timeout.

Country/operator subset

{"countries": ["gb", "us"], "operators": ["landsec", "simon"], "extractDetails": true, "mallTimeoutSeconds": 120}
FieldTypeDefaultMeaning
countriesarray[]Country codes ro, us, gb, hu; ["all"] selects all countries
operatorsarray[]nepi, afi, iulius, sunplaza, baneasa, parklake, veranda, colosseum, simon, cbl, landsec, westfield (alias urw)
mallsarray[]Specific mall IDs, or ["all"] for the registry
includeAnchorbooleantrueInclude anchor tenants; false filters them out before saving/billing
extractDetailsbooleanfalseVisit optional detail pages; slower and consumes the same per-mall budget
mallTimeoutSecondsinteger75Source time budget per mall, from 1 to 300 seconds
outputstringjsonNative dataset JSON; csv/xlsx are Console export choices, not separate files generated by the Actor

The three selection lists intersect. An empty list imposes no restriction when another selector is explicit. Only when all three lists are empty is Mega Mall selected. Schema defaults/prefills are deliberately empty, so they cannot override explicit countries with a default mall. Unknown mall IDs fail validation; duplicate IDs are crawled once. countries:["ro"] means the Romanian registry, not the demo. Selecting malls:["all"] with a country filter means all registered malls in that country.

Execution, deadlines and partial results

  • Each completed mall's records are normalized, anchor-filtered, deduplicated by (mall_id, normalized store_name), and pushed before the next mall starts. Previously saved malls survive a later source failure or run timeout.
  • After a successful dataset push, one awaited store-extracted charge is requested with count equal to that batch's saved unique records. Filtered rows, duplicates and failed pushes are not charged by this code. Dataset writes and billing are not one atomic transaction; a platform interruption between them cannot be made exactly-once by this Actor.
  • The per-mall budget includes robots fetching, network calls, retries, backoff, pacing, pagination and optional details. HTTP timeouts and lock waits are clamped to the remaining thread-local deadline; expiry prevents further requests. Permanent HTTP 400 responses are not retried. Transient connection errors/statuses retain retry handling; impersonated requests retain WAF fingerprint rotation.
  • A daemon-thread watchdog additionally bounds waiting for a DNS/native-library call that ignores its timeout. This avoids the default executor's indefinite shutdown wait. Python cannot forcibly kill an in-flight thread: a timed-out call may finish later, but cannot publish/bill late records or begin further budget-aware requests, and cannot prevent process exit.
  • A timed-out mall's not-yet-returned rows are not saved. There is no within-mall streaming or resume checkpoint. Dataset/KV/billing operations are outside the source budget, and the platform's overall run timeout still applies.
  • Source exceptions, empty results and timeouts do not discard other malls. If the entire run saves zero records, it writes the coverage manifest and fails. Dataset storage errors are fatal and reported when KV storage remains available.

Coverage manifest

The default key-value store's COVERAGE record is written before sources start, before each mall, after each mall, and at normal completion. It contains:

  • requestedMallCount, attemptedMallCount, mallsWithRecords, recordsPushed, mallTimeoutSeconds, executionFinished;
  • per-mall mall_id, status, recordsExtracted (before filtering/deduplication), recordsPushed, httpFailures, error, and elapsed source time;
  • coverageStatus: partial when there are known gaps, otherwise unverified;
  • fullCoverage: false: returning rows alone never certifies a complete tenant directory.

Per-mall statuses are pending, running, returned, partial, empty, failed, timeout, no_adapter, or dataset_error. returned means only that an adapter returned rows without a detected HTTP gap. Failed source requests, invalid JSON, exhausted WAF attempts and robots exclusions are recorded; a recovered fallback can still leave a conservative partial warning. CBL is explicitly marked partial because it exposes featured tenants only. Parser omissions or sources that silently truncate can remain undetected; inspect source totals independently when completeness matters.

executionFinished describes orchestration, not source completeness. A killed run can leave running/pending entries and an older checkpoint; dataset storage is authoritative if termination occurs between a push and the next KV update. Missing tenants must not be interpreted as closures from an incomplete run.

Output

Each item contains:

mall_id, mall_name, mall_city, mall_address, mall_country, mall_operator,
store_name, store_brand, category, floor, unit_number,
website, phone, email, opening_hours, store_url,
anchor_tenant, first_seen, last_seen, is_closed

Unpublished values are generally empty strings. anchor_tenant and is_closed are booleans. Detail availability differs by source; index APIs may supply available detail even with extractDetails:false.

first_seen/last_seen are adapter-generated run timestamps, not durable cross-run history. The Actor does not maintain snapshot diffs or automatically flag disappeared stores as closed. is_closed is an adapter-specific source mapping/default and may also reflect an opening-soon flag; do not use it alone as evidence of permanent closure. Build independently verified historical analysis downstream.

Limitations and pricing

  • ParkLake and other sources may serve WAF challenges or block datacenter traffic. This Actor exposes no proxy input or automatic residential-proxy configuration; no egress choice guarantees success.
  • CBL supplies featured-store lists rather than full directories. Other operators may also omit tenants or fields.
  • A full-registry request attempts the configured malls, not every mall or every store in a country. Registry and source structures change over time.
  • A small budget, especially with detail extraction, can time out a mall. Increase mallTimeoutSeconds within the allowed range or split a large request into explicit mall subsets.
  • The code requests the store-extracted event per saved record. Pay per event: $0.003 per store record (store-extracted, primary) plus a one-time $0.01 actor start. Standard Apify platform compute and per-dataset-item charges also apply. No fixed full-run cost or record count is guaranteed.
  • The per-run cost ceiling, and how to get past it. Apify enforces a maximum cost per run that you set when starting the run (Console → Run optionsMaximum cost per run, or maxTotalChargeUsd via the API). When the limit is reached the platform stops billing and stops writing dataset items, then aborts the run — so a large crawl can end early with only the malls processed so far (the COVERAGE record tells you exactly which ones completed). Recommended per-run ceiling: $4.99. A normal country/operator subset run costs well under $1 in store-extracted events, so $4.99 leaves ample headroom for a large subset while stopping a runaway all crawl from charging ~$21 in a single run. The whole multi-country registry (~6,900 stores) is roughly $21 of store-extracted events, which is above Apify's default per-run ceiling; raise the limit above $4.99 only deliberately, for a one-shot full crawl, or split the request by country/operator so each run stays under it. Re-running a mall already saved is not free, so prefer the split.

Run it from your code or on a schedule

from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("darknezz/mall-store-directory").call(
run_input={"countries": ["gb", "us"], "operators": ["landsec", "simon"],
"extractDetails": True, "mallTimeoutSeconds": 120},
max_total_charge_usd=4.99, # recommended per-run ceiling (see above)
)
for store in client.dataset(run["defaultDatasetId"]).iterate_items():
print(store["mall_name"], store["store_name"], store["category"])
coverage = client.key_value_store(run["defaultKeyValueStoreId"]).get_record("COVERAGE")
print(coverage["value"]["mallsWithRecords"], "of",
coverage["value"]["attemptedMallCount"], "malls returned rows")

The same call over the REST API:

curl -X POST "https://api.apify.com/v2/acts/darknezz~mall-store-directory/runs?token=$APIFY_TOKEN&maxTotalChargeUsd=5" \
-H 'Content-Type: application/json' \
-d '{"countries":["gb","us"],"extractDetails":true}'

Scheduling. For tenant-change monitoring, run the same mall subset on a weekly schedule (Console → Schedules) and diff consecutive datasets on (mall_id, store_name) downstream — this Actor keeps no cross-run snapshot history by design, so the diff is yours to own. Splitting by country/operator keeps each scheduled run inside the per-run cost ceiling and makes each delta attributable to a known set of malls.

FAQ

Why did the run stop before the whole registry finished? Almost always the per-run maximum cost (see the pricing section) or the overall run timeout. The COVERAGE record lists every mall with its status, so the gap is visible rather than silent.

Why do I get fewer stores than the mall actually has? Sources differ in what they publish: some expose only featured tenants (CBL is marked partial deliberately), some silently truncate, and WAF challenges can cut a mall short. coverageStatus reads unverified unless the Actor knows there is a gap — treat the dataset as a verified sample, not a census.

Are is_closed / first_seen / last_seen a change feed? No — they are adapter-level fields describing a single run. Build the historical layer downstream by snapshotting each run and diffing.

Can I route this through a proxy? The Actor has no proxy input; it is datacenter-direct by design. Operators that serve WAF challenges (ParkLake and friends) come back as failed malls in COVERAGE rather than silently contributing zero rows.

How do I get the full multi-country registry in one dataset? {"countries":["all"]}. It is a sequential crawl costing roughly $21 in store-extracted events (~6,900 stores), so raise the per-run cost ceiling for the one-shot run (recommended ceiling: $4.99) or split it by country.

Local tests

$uv run --no-project --with 'apify~=3.4.0' --with 'beautifulsoup4~=4.12' --with 'requests~=2.31' --with 'curl_cffi>=0.16.0' --with 'lxml~=5.0' python -m unittest discover -s tests -v

Execution tests use an in-memory Actor and controlled HTTP responses; the subprocess regression verifies that a permanently blocked worker cannot prevent exit. They perform no Apify API writes and do not establish live source coverage.