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>
55 lines
1.6 KiB
PHP
55 lines
1.6 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\Search;
|
|
use App\Services\PostcodeService;
|
|
use Illuminate\Console\Command;
|
|
|
|
class BackfillSearchAreas extends Command
|
|
{
|
|
protected $signature = 'searches:backfill-areas {--limit=0 : Max distinct areas to resolve this run (0 = no limit)}';
|
|
|
|
protected $description = 'Reverse-geocode searches that have no area_label yet';
|
|
|
|
public function handle(PostcodeService $postcodes): int
|
|
{
|
|
$limit = (int) $this->option('limit');
|
|
|
|
$buckets = Search::query()
|
|
->whereNull('area_label')
|
|
->select('lat_bucket', 'lng_bucket')
|
|
->distinct()
|
|
->when($limit > 0, fn ($query) => $query->limit($limit))
|
|
->get();
|
|
|
|
if ($buckets->isEmpty()) {
|
|
$this->info('No searches need an area label.');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$resolved = 0;
|
|
$rowsUpdated = 0;
|
|
|
|
foreach ($buckets as $bucket) {
|
|
$label = $postcodes->reverseResolve((float) $bucket->lat_bucket, (float) $bucket->lng_bucket);
|
|
|
|
if ($label === null) {
|
|
continue;
|
|
}
|
|
|
|
$resolved++;
|
|
$rowsUpdated += Search::query()
|
|
->whereNull('area_label')
|
|
->where('lat_bucket', $bucket->lat_bucket)
|
|
->where('lng_bucket', $bucket->lng_bucket)
|
|
->update(['area_label' => $label]);
|
|
}
|
|
|
|
$this->info("Resolved {$resolved} of {$buckets->count()} areas, updated {$rowsUpdated} search rows.");
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
}
|