Esta página aún no está traducida. Estás leyendo la versión en inglés.

SDSS endpoints

Run site suitability, Huff visit forecasts with cannibalization, catchment rings and multi-site comparisons through the API.

The spatial decision support endpoints run MapDove’s models on demand. They are POST endpoints with JSON bodies, require authentication and are relative to https://api.mapdove.com/api/v1.

Each successful call counts one analysis toward your monthly plan allowance (Free 25, Pro 500, Business and Enterprise unlimited), in addition to the API call. When the allowance is used up, calls return 402 plan_limit. Validation errors (400) do not consume an analysis.

Method Path Purpose
POST /sdss/suitability Weighted multi-criteria score for every hex, plus top sites
POST /sdss/huff Visit forecast and cannibalization for a candidate
POST /sdss/catchment Demographics in up to four rings
POST /sdss/compare-sites Score, forecast and catchment for up to five sites

Model details: Methodology.

Weights

Suitability and compare-sites take a Weights object. Every key is optional, each value is a number from 0 to 10, missing keys count as 0, and at least one weight must be positive. Unknown keys return 400.

{
  "population": 5, "income": 3, "daytime": 8, "traffic": 6, "growth": 2,
  "competition": 6, "complementary": 4, "rent": 3, "gap": 7
}

competition and rent are inverted criteria — a positive weight rewards less competition and lower rent. See Site suitability.

Suitability

POST /sdss/suitability

Field Type Description
city string required city slug
category string required category slug
weights Weights required
top_n integer 1–50, default 10
min_spacing_km number 0–50, default 1.5
curl -X POST https://api.mapdove.com/api/v1/sdss/suitability \
  -H "Authorization: Bearer $MAPDOVE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "city": "austin",
    "category": "coffee",
    "weights": { "daytime": 8, "population": 5, "traffic": 6, "competition": 6, "gap": 7 },
    "top_n": 5,
    "min_spacing_km": 1.5
  }'
const res = await fetch('https://api.mapdove.com/api/v1/sdss/suitability', {
  method: 'POST',
  headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    city: 'austin',
    category: 'coffee',
    weights: { daytime: 8, population: 5, traffic: 6, competition: 6, gap: 7 },
    top_n: 5,
  }),
});
const { top_sites, stats } = await res.json();
body = {
    "city": "austin",
    "category": "coffee",
    "weights": {"daytime": 8, "population": 5, "traffic": 6, "competition": 6, "gap": 7},
    "top_n": 5,
}
r = requests.post(f"{BASE}/sdss/suitability", headers=H, json=body, timeout=120)
r.raise_for_status()
for s in r.json()["top_sites"]:
    print(s["rank"], s["neighborhood"], s["score"])

Response (trimmed):

{
  "geojson": {
    "type": "FeatureCollection",
    "features": [
      {
        "type": "Feature",
        "geometry": { "type": "Polygon", "coordinates": ["…"] },
        "properties": {
          "hex_id": 77, "score": 72.4, "neighborhood": "Mueller",
          "population": 0.612, "income": 0.544, "daytime": 0.701, "traffic": 0.655, "growth": 0.833,
          "competition": 0.719, "complementary": 0.402, "rent": 0.488, "gap": 0.391
        }
      }
    ]
  },
  "top_sites": [
    {
      "rank": 1, "hex_id": 77, "score": 72.4, "lng": -97.7046, "lat": 30.2981, "neighborhood": "Mueller",
      "criteria": {
        "population": 0.612, "income": 0.544, "daytime": 0.701, "traffic": 0.655, "growth": 0.833,
        "competition": 0.719, "complementary": 0.402, "rent": 0.488, "gap": 0.391, "competitors_nearby": 1.84
      }
    },
    { "rank": 2, "hex_id": 431, "score": 70.9, "lng": -97.7232, "lat": 30.3998, "neighborhood": "The Domain", "criteria": { "…": "…" } }
  ],
  "stats": { "min": 11.8, "max": 72.4, "mean": 41.3 }
}

Criteria values are normalized 0–1 (already inverted for competition and rent). competitors_nearby is the raw distance-weighted competitor count.

Huff

POST /sdss/huff

Field Type Description
city string required
category string required
lng, lat number required; must be inside the city’s study area
sqft number 100–2,000,000; default = category typical size
brand_strength number 0.6–1.5; default 1.0
curl -X POST https://api.mapdove.com/api/v1/sdss/huff \
  -H "Authorization: Bearer $MAPDOVE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "city": "austin", "category": "coffee", "lng": -97.7046, "lat": 30.2981, "sqft": 2200, "brand_strength": 1.1 }'
const huff = await fetch('https://api.mapdove.com/api/v1/sdss/huff', {
  method: 'POST',
  headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ city: 'austin', category: 'coffee', lng: -97.7046, lat: 30.2981, sqft: 2200, brand_strength: 1.1 }),
}).then((r) => r.json());
console.log(huff.predicted_daily_visits, huff.total_cannibalized_daily);
huff = requests.post(f"{BASE}/sdss/huff", headers=H, timeout=120, json={
    "city": "austin", "category": "coffee", "lng": -97.7046, "lat": 30.2981,
    "sqft": 2200, "brand_strength": 1.1,
}).json()

Response (trimmed):

{
  "attractiveness": 1.26,
  "calibration_k": 0.013812,
  "predicted_daily_visits": 397,
  "predicted_annual_visits": 145002,
  "market_share": 0.0338,
  "category_avg_daily_visits": 345,
  "total_cannibalized_daily": 212,
  "impacted": [
    {
      "place_id": 88, "name": "Bean Republic · Mueller", "lng": -97.6998, "lat": 30.2950,
      "distance_km": 0.6, "current_daily_visits": 412, "delta_daily_visits": -58, "delta_pct": -14.08
    }
  ],
  "trade_area": {
    "type": "FeatureCollection",
    "features": [
      { "type": "Feature", "geometry": { "type": "Polygon", "coordinates": ["…"] }, "properties": { "hex_id": 77, "share": 0.07311 } }
    ]
  }
}

impacted lists up to ten stores with the largest losses. trade_area covers the first 80% of the candidate’s modeled visitors. See Huff forecast and Cannibalization.

Catchment

POST /sdss/catchment

Field Type Description
lng, lat number required
rings number[] required, 1–4 positive values; each radius ≤ 50 km after conversion
unit string km, mi or min

min converts minutes to a radius with the average driving speed of the city containing the point — a radius approximation, not a road-network isochrone (Catchments).

curl -X POST https://api.mapdove.com/api/v1/sdss/catchment \
  -H "Authorization: Bearer $MAPDOVE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "lng": -97.7046, "lat": 30.2981, "rings": [1, 3], "unit": "mi" }'
rings = requests.post(f"{BASE}/sdss/catchment", headers=H, timeout=60, json={
    "lng": -97.7046, "lat": 30.2981, "rings": [5, 10, 15], "unit": "min",
}).json()["rings"]
for r in rings:
    print(r["label"], r["radius_km"], r["population"])

Response (trimmed to one ring):

{
  "rings": [
    {
      "label": "1 mi", "radius_km": 1.609344, "area_km2": 8.14,
      "population": 18412, "households": 8120, "daytime_pop": 21904,
      "median_income": 104300, "median_age": 34.8, "pct_college": 49.6, "pct_family": 33.1,
      "traffic_index": 51.2, "rent_index": 58.7, "growth_rate": 0.0471,
      "segments": [
        { "slug": "urban-professionals", "name": "Urban Professionals", "color": "#14b8a6", "population": 9021, "share": 0.49 },
        { "slug": "suburban-families", "name": "Suburban Families", "color": "#22c55e", "population": 4234, "share": 0.23 }
      ],
      "places_by_category": [
        { "slug": "coffee", "name": "Coffee & Cafés", "color": "#92400e", "count": 4 },
        { "slug": "qsr", "name": "Quick Service", "color": "#dc2626", "count": 3 }
      ],
      "geometry": { "type": "Polygon", "coordinates": ["…"] }
    }
  ]
}

Compare sites

POST /sdss/compare-sites

Field Type Description
city string required
category string required
sites { label, lng, lat }[] required, 1–5 sites
weights Weights suitability weights

The whole comparison counts as one analysis. Each site’s Huff forecast uses the category’s typical size and brand strength 1.0, and omits trade_area. catchment_1 is a 1-mile ring in US cities and a 1.6 km ring in Dubai.

curl -X POST https://api.mapdove.com/api/v1/sdss/compare-sites \
  -H "Authorization: Bearer $MAPDOVE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "city": "austin",
    "category": "coffee",
    "sites": [
      { "label": "Mueller", "lng": -97.7046, "lat": 30.2981 },
      { "label": "The Domain", "lng": -97.7232, "lat": 30.3998 }
    ],
    "weights": { "daytime": 8, "population": 5, "traffic": 6, "competition": 6, "gap": 7 }
  }'
const cmp = await fetch('https://api.mapdove.com/api/v1/sdss/compare-sites', {
  method: 'POST',
  headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ city: 'austin', category: 'coffee', sites, weights }),
}).then((r) => r.json());
console.table(cmp.sites.map((s) => ({ site: s.label, score: s.score, visits: s.huff.predicted_daily_visits })));

Response (trimmed to one site):

{
  "sites": [
    {
      "label": "Mueller", "lng": -97.7046, "lat": 30.2981,
      "score": 72.4,
      "criteria": { "population": 0.612, "daytime": 0.701, "competition": 0.719, "gap": 0.391, "…": "…" },
      "huff": {
        "attractiveness": 1.0, "calibration_k": 0.013812, "predicted_daily_visits": 352,
        "predicted_annual_visits": 128480, "market_share": 0.0302, "category_avg_daily_visits": 345,
        "total_cannibalized_daily": 188, "impacted": [ { "place_id": 88, "delta_daily_visits": -51, "…": "…" } ]
      },
      "catchment_1": { "label": "1 mi", "radius_km": 1.609344, "population": 18412, "…": "…" }
    }
  ]
}

score and criteria come from the hex containing (or nearest to) the site; they are null if the point is not near any hex of the city grid.

Errors specific to SDSS

Status code Typical cause
400 bad_request Unknown criterion; weight outside 0–10; all weights zero; point outside the study area; sqft or brand_strength out of range; more than 4 rings or 5 sites; ring over 50 km; bad unit
402 plan_limit Monthly analyses used up
404 not_found Unknown city or category slug