Apple Maps Location Data for Retail Site Selection 2026
Score candidate retail sites with Apple Maps data: competitor density, category mix, rating quality and co-tenancy inside any radius, scored in Python.

Thirdwatch's Apple Maps Scraper turns any candidate address into a supply-side profile of its trade area. Set latitude, longitude and a radius, list the categories that matter, and get every place inside the circle with coordinates, a machine category path, rating with its provider, rating count, price tier where published and opening hours. Built for retail expansion teams, franchise scouts and founders picking between shortlisted sites.
Why use map data for retail site selection
Site selection fails on the supply side more often than the demand side. A location can sit on a strong footfall corridor and still underperform because four established competitors already own the catchment, or because the co-tenancy is wrong — a specialty coffee unit next to nothing that generates morning trips. The stakes justify the analysis: US retail and food services sales run above $700 billion a month according to the Census Bureau's Monthly Retail Trade report, and a single mid-size lease commits a brand for five to ten years.
The job-to-be-done is a shortlist decision. A coffee brand has six available units in one city and needs to rank them. A gym operator wants to know whether a suburb is under-supplied relative to its population. A franchise scout wants to defend a territory recommendation with something more rigorous than a walk-around. In every case the analyst needs the same three measurements per site, computed identically: how many direct competitors sit inside the catchment, how strong those incumbents are, and what else is nearby that pulls the trips your format depends on. Apple's place records carry all three in one pass because coordinates, mapsCategoryId, ratingOutOf5 and ratingCount arrive together.
How does this compare to the alternatives?
Site-selection data is a market with a very steep price curve at the top.
| Approach | Cost model | Reliability | Setup time | Maintenance |
|---|---|---|---|---|
| Enterprise location-intelligence platform | Annual contract, typically five to six figures | Rich but opaque methodology | Weeks of procurement and onboarding | Vendor-managed, vendor-priced |
| Manual survey of each candidate site | Analyst travel and hours | Snapshot only, not repeatable | Days per site | Redone for every new shortlist |
| Thirdwatch Apple Maps Scraper | Pay per place returned | Same query applied identically to every site | Ten minutes | Thirdwatch tracks Apple-side changes |
Enterprise platforms earn their price when you need mobility panels and demographic overlays. For the supply-side half of the question — who is already there and how good are they — the Apple Maps Scraper actor page gets you a defensible answer the same afternoon, and the method is auditable because you wrote it.
How to score retail sites with Apple Maps data in 5 steps
Step 1: How do I define the candidate sites and the category set?
Authenticate with an Apify token from Settings then Integrations after signing up at apify.com.
export APIFY_TOKEN="apify_api_xxxxxxxxxxxxxxxx"Then write down the shortlist and the two category lists a site score needs: direct competitors, and the anchors that generate the trips your format converts.
import os, requests, pandas as pd, numpy as np
ACTOR = "thirdwatch~apple-maps-scraper"
TOKEN = os.environ["APIFY_TOKEN"]
SITES = [
{"id": "unit-a", "latitude": 30.2672, "longitude": -97.7431},
{"id": "unit-b", "latitude": 30.2500, "longitude": -97.7500},
{"id": "unit-c", "latitude": 30.2849, "longitude": -97.7341},
]
COMPETITORS = ["coffee shops", "cafes", "espresso bars"]
ANCHORS = ["gyms", "coworking spaces", "grocery stores", "hotels", "bookshops"]
RADIUS_KM = 0.8Step 2: How do I pull the trade area around each candidate?
One run per site, with latitude, longitude and radiusKm set. Every query in the array is searched inside that circle, so each site gets an identical treatment.
def trade_area(site, queries, radius_km):
r = requests.post(
f"https://api.apify.com/v2/acts/{ACTOR}/run-sync-get-dataset-items",
params={"token": TOKEN},
json={
"queries": queries,
"latitude": site["latitude"],
"longitude": site["longitude"],
"radiusKm": radius_km,
"maxResults": 200,
"language": "en-US",
"countryCode": "US",
"includePhotos": False,
"includeReviews": False,
},
timeout=1800,
)
df = pd.DataFrame(r.json())
df["site_id"] = site["id"]
return df
comp = pd.concat([trade_area(s, COMPETITORS, RADIUS_KM) for s in SITES])
anch = pd.concat([trade_area(s, ANCHORS, RADIUS_KM) for s in SITES])
comp = comp.drop_duplicates(subset=["site_id", "placeId"])
anch = anch.drop_duplicates(subset=["site_id", "placeId"])Keep radiusKm honest. For a walk-in urban format, 0.5 to 1 km is the real catchment; a 5 km circle in a dense city will drown the signal in businesses nobody would cross town for.
Step 3: How do I measure competitive pressure rather than just count pins?
Distance-weight each incumbent and scale it by how strong that incumbent is. Ten weakly reviewed cafes are not the same threat as three beloved ones.
def metres(lat1, lng1, lat2, lng2):
r = 6_371_000
p1, p2 = np.radians(lat1), np.radians(lat2)
dp, dl = p2 - p1, np.radians(lng2 - lng1)
a = np.sin(dp / 2) ** 2 + np.cos(p1) * np.cos(p2) * np.sin(dl / 2) ** 2
return 2 * r * np.arcsin(np.sqrt(a))
site_by_id = {s["id"]: s for s in SITES}
comp["dist_m"] = [
metres(site_by_id[r.site_id]["latitude"], site_by_id[r.site_id]["longitude"],
r.latitude, r.longitude)
for r in comp.itertuples()
]
# strength: normalised rating x log-damped rating volume
comp["strength"] = (
comp.ratingOutOf5.fillna(3.0) / 5
* np.log1p(comp.ratingCount.fillna(0)) / np.log1p(500)
).clip(0, 1)
# pressure decays with distance across the catchment
comp["pressure"] = comp.strength * np.exp(-comp.dist_m / (RADIUS_KM * 1000 / 2))
pressure = comp.groupby("site_id").pressure.sum().rename("competitive_pressure")Always use ratingOutOf5 here. The raw rating field follows whatever scale ratingProvider publishes, so a Yelp-sourced 4 and an Apple-sourced 80 are the same quality expressed differently.
Step 4: How do I score co-tenancy and category mix?
Anchors are the demand-side proxy you can get from supply-side data: the businesses that pull people into the block at the hours your format sells.
anch["dist_m"] = [
metres(site_by_id[r.site_id]["latitude"], site_by_id[r.site_id]["longitude"],
r.latitude, r.longitude)
for r in anch.itertuples()
]
# roll categories up by Apple's dotted machine path
anch["vertical"] = anch.mapsCategoryId.fillna("").str.split(".").str[0]
anchor_pull = (
anch[anch.dist_m <= RADIUS_KM * 1000]
.assign(w=lambda d: np.log1p(d.ratingCount.fillna(0)) / np.log1p(500))
.groupby("site_id").w.sum().rename("anchor_pull")
)
mix_breadth = (
anch.groupby("site_id").vertical.nunique().rename("category_breadth")
)mapsCategoryId is what makes this cheap. Prefix matching on dining.cafe or shopping rolls a trade area up by vertical without maintaining a translation table of display labels.
Step 5: How do I produce the ranked shortlist?
Combine the three signals into one table your property committee can argue with.
score = (
pd.concat([pressure, anchor_pull, mix_breadth], axis=1)
.fillna(0)
.assign(
competitor_count=comp.groupby("site_id").placeId.nunique(),
site_score=lambda d: (
d.anchor_pull / d.anchor_pull.max()
+ 0.5 * d.category_breadth / d.category_breadth.max()
- d.competitive_pressure / d.competitive_pressure.max()
).round(3),
)
.sort_values("site_score", ascending=False)
)
score.to_csv("site_shortlist.csv")
print(score)Re-run the whole thing quarterly through the Apify scheduler and the same script becomes a market-saturation tracker: rising competitive pressure in a trade area is an early signal for a lease renewal decision.
Sample output
One competitor record from a trade-area pull, trimmed to the fields the scoring uses.
[
{
"placeId": "I15FF30DE01EC121F",
"name": "Daydreamer Coffee",
"category": "Coffee Shop",
"categories": ["Dining", "Coffee Shop", "Cafe"],
"mapsCategoryId": "dining.cafe.coffee_shop",
"placeType": "BUSINESS",
"address": "80 Rainey St, Austin, TX 78701, United States",
"neighborhood": "Downtown",
"city": "Austin",
"stateCode": "TX",
"postalCode": "78701",
"latitude": 30.2592149,
"longitude": -97.7389039,
"timezone": "America/Chicago",
"rating": 4,
"maxRating": 5,
"ratingOutOf5": 4.0,
"ratingCount": 27,
"ratingProvider": "Yelp",
"priceLevel": 2,
"priceSymbol": "$$",
"hours": { "Monday": [{ "open": "07:00", "close": "22:00" }] },
"amenities": [
{ "name": "Accepts Apple Pay", "id": "crossbusiness.payments.applepay", "available": true }
],
"appleMapsUrl": "https://maps.apple.com/place?place-id=I15FF30DE01EC121F"
}
]latitude and longitude drive the distance weighting. ratingOutOf5 and ratingCount together give incumbent strength — a high score on nine ratings should not outweigh a slightly lower one on nine hundred. priceLevel and priceSymbol appear for a subset of restaurants and are useful for checking that a candidate site's incumbents sit in your price band. neighborhood is a free grouping key for reporting, and hours lets you test whether competitors actually trade during your peak daypart.
Common pitfalls
Four mistakes turn a site score into noise. Radius inflation is the most common: a 5 km circle around an urban walk-in unit counts businesses no customer would ever choose over yours, and every site then looks equally saturated. Counting pins instead of weighting strength treats a dormant cafe and a queue-out-the-door one as identical threats. Mixing rating scales does the same damage in the other direction, which is what ratingOutOf5 exists to prevent. Assuming price tier is always present will break a scoring function — priceLevel is sparse outside restaurants, so treat it as an optional bonus signal rather than a required input.
Thirdwatch's Actor applies the same radius search identically to every candidate site, de-duplicates on placeId across the whole run, and finishes a genuinely empty catchment search as a successful run with a status message — which in a site-selection context is itself a finding worth recording, not an error.
Related use cases
Frequently asked questions
What data do I need to compare two retail sites?
At minimum, the count and quality of direct competitors inside the trade area, the surrounding category mix, and which traffic-driving anchors sit nearby. One radius search per candidate site returns all three, since every place carries coordinates, a category path and a rating.
How do I define a trade area with this Actor?
Set latitude and longitude to the candidate address and radiusKm to your catchment, anywhere from 0.5 to 100 kilometres. Every query in the run is then searched inside that circle, so the same category list can be applied to each site identically.
Can I compare competitor quality, not just count?
Yes. Each place returns ratingOutOf5 and ratingCount, so you can weight a trade area by how strong the incumbents actually are. Ten weakly rated competitors with few ratings is a very different site from three with hundreds each.
Does this replace foot traffic data?
No, it complements it. Mobility panels tell you how many people pass a point; map data tells you what is already there and how good it is. Site-selection teams normally use supply-side data to shortlist sites and buy mobility data only for the finalists.
How granular is the category taxonomy?
Apple exposes mapsCategoryId as a dotted machine path such as dining.cafe.coffee_shop, alongside the display category and a categories array. Prefix matching on that path lets you roll a trade area up by vertical without maintaining a list of display strings.
Related
100 free credits, no credit card.
About 30 real searches. Add the MCP to Claude or Cursor in two minutes.