146 lines
5.3 KiB
PHP
146 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace App\Jobs;
|
|
|
|
use App\Models\EventMeta;
|
|
use App\Models\Location;
|
|
use App\Services\Location\Geocoder;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
|
use Illuminate\Queue\InteractsWithQueue;
|
|
use Illuminate\Queue\SerializesModels;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class GeocodeEventLocations implements ShouldQueue
|
|
{
|
|
use Dispatchable, Queueable, InteractsWithQueue, SerializesModels;
|
|
|
|
// process up to n records this run; null = no cap
|
|
public function __construct(
|
|
public ?int $limit = null
|
|
) {}
|
|
|
|
// convenience helpers
|
|
public static function push(?int $limit = null): void {
|
|
dispatch(new static($limit));
|
|
}
|
|
public static function runNow(?int $limit = null): void {
|
|
dispatch_sync(new static($limit));
|
|
}
|
|
|
|
// handle geocoding
|
|
public function handle(Geocoder $geocoder): void
|
|
{
|
|
// working counters
|
|
$processed = 0;
|
|
$created = 0;
|
|
$updated = 0;
|
|
$skipped = 0;
|
|
$failed = 0;
|
|
|
|
Log::info('GeocodeEventLocations: start', ['limit' => $this->limit]);
|
|
|
|
// events that have a non-empty location string but no linked location row yet
|
|
$todo = EventMeta::query()
|
|
->whereNull('location_id')
|
|
->whereNotNull('location')
|
|
->where('location', '<>', '')
|
|
->orderBy('event_id'); // important for chunkById
|
|
|
|
$stop = false;
|
|
|
|
// log total to process (before limit)
|
|
$total = (clone $todo)->count();
|
|
Log::info('[geo] starting GeocodeEventLocations', ['total' => $total, 'limit' => $this->limit]);
|
|
|
|
// chunk through event_meta rows
|
|
$todo->chunkById(200, function ($chunk) use ($geocoder, &$processed, &$created, &$updated, &$skipped, &$failed, &$stop) {
|
|
foreach ($chunk as $meta) {
|
|
if ($stop) {
|
|
return false; // stop further chunking
|
|
}
|
|
|
|
// respect limit if provided
|
|
if ($this->limit !== null && $processed >= $this->limit) {
|
|
$stop = true;
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
// geocode the free-form location string
|
|
$norm = $geocoder->forward($meta->location);
|
|
|
|
// skip obvious non-address labels or unresolved queries
|
|
if (!$norm || (!$norm['lat'] && !$norm['street'])) {
|
|
$skipped++;
|
|
$processed++;
|
|
continue;
|
|
}
|
|
|
|
// normalized match key to reduce duplicates
|
|
$lookup = [
|
|
'display_name' => $norm['display_name'],
|
|
'street' => $norm['street'],
|
|
'city' => $norm['city'],
|
|
'state' => $norm['state'],
|
|
'postal' => $norm['postal'],
|
|
'country' => $norm['country'],
|
|
];
|
|
|
|
// try to match an existing location (by normalized fields)
|
|
$existing = Location::where($lookup)->first();
|
|
|
|
// fall back to raw string match against any pre-seeded label rows
|
|
if (!$existing && $meta->location) {
|
|
$existing = Location::where('display_name', $meta->location)
|
|
->orWhere('raw_address', $meta->location)
|
|
->first();
|
|
}
|
|
|
|
// reuse existing location or create a new one with coords
|
|
$loc = $existing ?? Location::firstOrCreate(
|
|
$lookup,
|
|
[
|
|
'raw_address' => $norm['raw_address'],
|
|
'lat' => $norm['lat'],
|
|
'lon' => $norm['lon'],
|
|
]
|
|
);
|
|
|
|
// if we matched an existing row missing coords, backfill once
|
|
if ($existing && (is_null($existing->lat) || is_null($existing->lon))) {
|
|
$existing->lat = $norm['lat'];
|
|
$existing->lon = $norm['lon'];
|
|
$existing->raw_address ??= $norm['raw_address'];
|
|
$existing->save();
|
|
$updated++;
|
|
}
|
|
|
|
if ($loc->wasRecentlyCreated) {
|
|
$created++;
|
|
}
|
|
|
|
// link event_meta → locations
|
|
$meta->location_id = $loc->id;
|
|
$meta->save();
|
|
|
|
$processed++;
|
|
|
|
} catch (\Throwable $e) {
|
|
$failed++;
|
|
$processed++;
|
|
Log::warning('GeocodeEventLocations: failed', [
|
|
'event_id' => $meta->event_id,
|
|
'location' => $meta->location,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
}, 'event_id');
|
|
|
|
Log::info('GeocodeEventLocations: done', compact('processed', 'created', 'updated', 'skipped', 'failed'));
|
|
}
|
|
}
|