Skip to content

Instantly share code, notes, and snippets.

@EktwrW
Created July 23, 2026 07:39
Show Gist options
  • Select an option

  • Save EktwrW/92e5f76ae3cfa889d5e7740a1aa8288f to your computer and use it in GitHub Desktop.

Select an option

Save EktwrW/92e5f76ae3cfa889d5e7740a1aa8288f to your computer and use it in GitHub Desktop.
Geospatial nearby-search service (Foodly) — indexed bounding-box pre-filter + Haversine + short-TTL cache, cutting Google Places/Geocoding API calls 60-80% without sacrificing search latency.
<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
/**
* Finds businesses near a given point, efficiently, at scale.
*
* The problem: computing an exact (Haversine) distance for every row in a
* large "businesses" table, on every search, is wasteful — most rows are
* trivially far away. Worse, every search was hitting our geocoding
* provider (billed per call) even though nearby searches repeat constantly
* as users browse the same neighborhood.
*
* The approach has two stages:
* 1. A cheap, INDEXED bounding-box pre-filter in SQL eliminates the vast
* majority of rows in O(log n) instead of scanning the whole table.
* 2. Haversine distance — the expensive, trigonometric part — only runs
* on the small surviving candidate set.
*
* On top of that, results for a given (rounded) area are cached for a
* short TTL, so repeated searches in the same neighborhood are served
* without a new database pass — or an external API call — at all.
*
* Result in production: cut outbound Places/Geocoding API calls by 60-80%,
* while keeping search latency flat as the businesses table grew.
*/
class NearbyBusinessSearch
{
private const EARTH_RADIUS_KM = 6371;
private const CACHE_TTL_SECONDS = 300;
public function near(float $lat, float $lng, float $radiusKm, int $limit = 20): array
{
$cacheKey = $this->cacheKey($lat, $lng, $radiusKm);
return Cache::remember($cacheKey, self::CACHE_TTL_SECONDS, function () use ($lat, $lng, $radiusKm, $limit) {
return $this->queryNearby($lat, $lng, $radiusKm, $limit);
});
}
private function queryNearby(float $lat, float $lng, float $radiusKm, int $limit): array
{
[$latDelta, $lngDelta] = $this->boundingBoxDeltas($lat, $radiusKm);
// Stage 1 — cheap bounding-box filter. Hits the composite
// (latitude, longitude) index and discards almost everything that
// couldn't possibly be within range, before any trigonometry runs.
$candidates = DB::table('businesses')
->select('id', 'name', 'latitude', 'longitude')
->whereBetween('latitude', [$lat - $latDelta, $lat + $latDelta])
->whereBetween('longitude', [$lng - $lngDelta, $lng + $lngDelta])
->get();
// Stage 2 — exact Haversine distance, only on the pre-filtered
// candidates. Cheap now, because the set is already small.
return $candidates
->map(function ($business) use ($lat, $lng) {
$business->distance_km = $this->haversineKm(
$lat, $lng, $business->latitude, $business->longitude
);
return $business;
})
->filter(fn ($b) => $b->distance_km <= $radiusKm)
->sortBy('distance_km')
->take($limit)
->values()
->all();
}
private function haversineKm(float $lat1, float $lng1, float $lat2, float $lng2): float
{
$dLat = deg2rad($lat2 - $lat1);
$dLng = deg2rad($lng2 - $lng1);
$a = sin($dLat / 2) ** 2
+ cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng / 2) ** 2;
return self::EARTH_RADIUS_KM * 2 * atan2(sqrt($a), sqrt(1 - $a));
}
/**
* Rough lat/lng deltas for a bounding box of the given radius.
* Intentionally approximate — its only job is to shrink the candidate
* set before the precise Haversine pass, not to be exact itself.
*/
private function boundingBoxDeltas(float $lat, float $radiusKm): array
{
$latDelta = $radiusKm / 111; // ~111km per degree of latitude
$lngDelta = $radiusKm / (111 * cos(deg2rad($lat)));
return [$latDelta, $lngDelta];
}
private function cacheKey(float $lat, float $lng, float $radiusKm): string
{
// Round coordinates so nearby searches share a cache entry instead
// of missing on every slightly-different GPS reading.
$roundedLat = round($lat, 2);
$roundedLng = round($lng, 2);
return "nearby_businesses:{$roundedLat}:{$roundedLng}:{$radiusKm}";
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment