Recipe: bulk POI scoring for a property catalog
Score every listing in your database for proximity to schools, transit, shops, etc. Run nightly, store the result, filter on it in your own search.
Scenario
You have 50 000 active listings. You want users to filter on “within 5 min walking of a school”. Calling Yatmo at search time is too slow and unnecessary; you want a precomputed score per listing, refreshed nightly.
Approach
- Iterate over your listings. For each, call
/summaryfor its coordinates. - Walk the response and pull out the travel time to the categories you care about.
- Store the score on the listing record. Reindex your search.
- Throttle. Sleep 50–100 ms between calls to be a polite client and avoid 429s.
Worker example
# Pseudo-shell: for a single listing.
# Real pipelines iterate; see the JS/PHP/C# examples for the loop.
curl -H 'LicenseKey: YOUR_KEY' \
"https://be.yatmo.com/summary?latitude=$LAT&longitude=$LNG&language=EN" \
| jq '.AvailableCategoriesAroundPosition'
// Node.js worker
import fs from 'node:fs/promises';
async function scoreListing(listing) {
const url = `https://be.yatmo.com/summary?latitude=${listing.lat}&longitude=${listing.lng}&language=EN`;
const res = await fetch(url, { headers: { LicenseKey: process.env.YATMO_KEY } });
if (!res.ok) {
console.error('Yatmo error', res.status, await res.text());
return null;
}
const summary = await res.json();
// Pick a metric: walking time to the nearest school.
const schoolCat = summary.AvailableCategoriesAroundPosition
.flatMap(c => c.SubCategories)
.find(sc => sc.Label === 'School'); // localized name
const walking = schoolCat?.Data?.[0]?.TravelDataElements
?.find(t => t.TravelMode === 2); // 2 = Walking
return walking?.TravelTime ?? null; // seconds
}
const listings = JSON.parse(await fs.readFile('listings.json', 'utf8'));
for (const l of listings) {
l.walkToSchoolSec = await scoreListing(l);
await new Promise(r => setTimeout(r, 80)); // throttle 80ms
}
await fs.writeFile('scored.json', JSON.stringify(listings, null, 2));
// PHP worker (CLI)
$key = getenv('YATMO_KEY');
$listings = json_decode(file_get_contents('listings.json'), true);
foreach ($listings as &$l) {
$url = "https://be.yatmo.com/summary?latitude={$l['lat']}&longitude={$l['lng']}&language=EN";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["LicenseKey: $key"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$summary = json_decode(curl_exec($ch), true);
curl_close($ch);
// Walk the structure to find your metric; store it on the listing.
$l['walkToSchoolSec'] = extractWalkToSchool($summary);
usleep(80 * 1000);
}
file_put_contents('scored.json', json_encode($listings));
// .NET worker
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("LicenseKey", Environment.GetEnvironmentVariable("YATMO_KEY"));
var listings = JsonSerializer.Deserialize<List<Listing>>(
await File.ReadAllTextAsync("listings.json"))!;
foreach (var l in listings) {
var url = $"https://be.yatmo.com/summary?latitude={l.Lat}&longitude={l.Lng}&language=EN";
var summary = await http.GetFromJsonAsync<JsonElement>(url);
l.WalkToSchoolSec = ExtractWalkToSchool(summary);
await Task.Delay(80); // throttle
}
await File.WriteAllTextAsync("scored.json", JsonSerializer.Serialize(listings));
Production checklist
- Run incrementally. Don’t rescore every listing every night — only the new ones plus a rolling sample of stale records.
- Handle 429 calmly. If you start seeing 429s, slow down rather than hammering — the back-off will let your job complete cleanly instead of failing.
- Persist partial progress. If the worker crashes after 30 000 of 50 000 listings, you want to resume, not restart.
- Cache server-side — the Yatmo Summary endpoint is cached for 7 days per (lat, lng, language). If you rescore the same property a week later, you’re likely getting the same data anyway.