Reverse-geocode a general area label for logged searches

Each search now stores an `area_label` (district/town) reverse-geocoded from its
coarsened ~1km lat/lng bucket via postcodes.io, surfaced in the Filament Searches
admin as a sortable/searchable column plus an area filter. Geocoding is cached 30
days per bucket, queries a 2km radius so low-density buckets still match the
default 100m miss, and fails gracefully to null. Adds `searches:backfill-areas`
(scheduled hourly) to label existing rows and retry stragglers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ovidiu U
2026-06-12 09:05:03 +01:00
parent 040b2f627e
commit ea22387c9d
10 changed files with 331 additions and 1 deletions

View File

@@ -11,6 +11,7 @@ use App\Services\Forecasting\LocalSnapshotService;
use App\Services\Forecasting\WeeklyForecastService;
use App\Services\HaversineQuery;
use App\Services\PlanFeatures;
use App\Services\PostcodeService;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
@@ -20,6 +21,7 @@ final class StationSearchService
public function __construct(
private readonly WeeklyForecastService $weeklyForecast,
private readonly LocalSnapshotService $localSnapshot,
private readonly PostcodeService $postcodeService,
) {}
public function search(SearchCriteria $criteria, ?User $user, ?string $ipHash): SearchResult
@@ -118,6 +120,7 @@ final class StationSearchService
Search::create([
'lat_bucket' => round($criteria->lat, 2),
'lng_bucket' => round($criteria->lng, 2),
'area_label' => $this->postcodeService->reverseResolve($criteria->lat, $criteria->lng),
'fuel_type' => $criteria->fuelType->value,
'results_count' => $resultsCount,
'lowest_pence' => $prices->min(),