Is this spot good?
activity: "kitesurfing"location: {lat:36.013, lng:-5.604}window: {from:"…", to:"…"}
The inverse-query side of the engine. Given an activity, a region, and a window, return the top-K ranked sub-spots — calibrated per-spot, safety-gated, optionally personalised. The endpoint that powers 'best kitesurf near me this weekend' inside booking apps and consumer products. Sub-spot coverage is in active expansion: see the open catalogue for current regions, and note that the predicate /v1/score endpoint works on any coordinate worldwide regardless.
One physics engine, two question shapes. The predicate path validates a chosen spot. The inverse path enumerates a region, scores every candidate, drops the unsafe ones and returns the top-K — same calibrated curves, same hard gates, same confidence model. Two buyers, one bill.
activity: "kitesurfing"location: {lat:36.013, lng:-5.604}window: {from:"…", to:"…"}
activity: "kitesurfing"regionCenter: {lat:36.01, lng:-5.6}radiusKm: 50topK: 5
A real-world inverse query for a kitesurf weekend on the Cádiz coast. The engine resolves five catalog sub-spots within 25km of the region centre, scores each across the six-hour window, drops the unsafe ones, and returns this ranked list. Every row is the same score you'd get calling /v1/score one location at a time.
POST /v1/recommend-spot
{
activity: "kitesurfing",
regionCenter: { lat: 36.013, lng: -5.604 },
radiusKm: 25,
topK: 5,
window: {
from: "2026-07-15T10:00Z",
to: "2026-07-15T16:00Z"
}
}The endpoint introduces zero new physics and zero new machine learning. It's pure orchestration of layers you already trust. The novelty is the spatial enumeration step — and the discipline of dropping unsafe spots BEFORE ranking, not after.
Catalog sub-spots looked up in microseconds against an in-memory R-tree built at boot.
Same physics + calibration + confidence pipeline as /v1/score, fanned out at concurrency 6 to spare upstream providers.
Lightning ≥0.85 or hazardous AQI → dropped before ranking. allGated=true when EVERY candidate fails.
Sorted DESC by score; within ±0.5 the closer sub-spot wins. Top-K slice returned.
One audit row + recommendation.completed event (HMAC-signed, top-3 payload).
Replace N client-side score calls with one ranked recommend.
Powers region-and-activity search result lists.
Itinerary tooling. One call instead of an analyst with five tabs.
Radius and topK are tiered. Personalisation (cold-start blend on a per-user pseudonym) unlocks at Pro; counterfactual hints (binding-constraint surfacing) at Scale. Daily quota is separate from /v1/score — each recommend call fans out N internal scores, so the cap is independent and tighter. Starter includes 1,500 recommend calls/month, Pro 5,000; overage at €7.50/1k (Starter) and €4.00/1k (Pro).
The physics-driven score always carries the majority signal. Even a user with 30+ logged outcomes can't override the engine's objective ranking — the personal model nudges within a ±20 point window, never beyond. Discovery results stay anchored to fitness, not bias.
The caller hashes the user identity client-side; we store the hash + model weights + nothing else. GDPR Article 17 deletion via DELETE /v1/decision/user-data/:pseudonym scrubs every trace from the personalization + audit + recommendation stores in one call.
Every successful call writes one row to recommendation_runs (region centre as PostGIS POINT, full top-K, latency, allGated flag). That's the primitive behind the upcoming search-heatmap analytics and the forecast-verification join.
Tenants subscribed to recommendation.completed receive an HMAC-signed webhook after each call. Payload trims to the top 3 — the full top-K stays in the audit DB for query-time access.
{
"event": "recommendation.completed",
"tenantId": "tn_...",
"data": {
"activity": "kitesurfing",
"regionCenter": { "lat": 36.01, "lng": -5.6 },
"radiusKm": 50, "topK": 5,
"results": [
{ "rank": 1, "spotSlug": "kitesurfing-spot-tarifa-balneario",
"effectiveScore": 87, "verdict": "favorable" },
{ "rank": 2, "spotSlug": "kitesurfing-spot-tarifa-valdevaqueros",
"effectiveScore": 84, "verdict": "favorable" },
{ "rank": 3, "spotSlug": "kitesurfing-spot-tarifa-punta-paloma",
"effectiveScore": 79, "verdict": "favorable" }
],
"allGated": false, "latencyMs": 412
}
}