filled('postcode')) { $location = $this->postcodeService->resolve($request->string('postcode')->toString()); if ($location === null) { throw ValidationException::withMessages(['postcode' => 'Postcode not found.']); } $lat = $location->lat; $lng = $location->lng; } else { $lat = (float) $request->input('lat'); $lng = (float) $request->input('lng'); } $fuelType = $request->fuelType(); $radius = $request->radius(); $sort = $request->sort(); $all = Station::query() ->selectRaw( 'stations.*, spc.price_pence, spc.fuel_type, spc.price_effective_at, (6371 * acos(GREATEST(-1.0, LEAST(1.0, cos(radians(?)) * cos(radians(lat)) * cos(radians(lng) - radians(?)) + sin(radians(?)) * sin(radians(lat)) )))) AS distance_km', [$lat, $lng, $lat], ) ->join('station_prices_current as spc', function (JoinClause $join) use ($fuelType): void { $join->on('stations.node_id', '=', 'spc.station_id') ->where('spc.fuel_type', '=', $fuelType->value); }) ->where('stations.temporary_closure', false) ->where('stations.permanent_closure', false) ->get(); $stations = $all ->filter(fn ($s) => (float) $s->distance_km <= $radius) ->sortBy($sort === 'price' ? 'price_pence' : 'distance_km') ->values(); $prices = $stations->pluck('price_pence'); Search::create([ 'lat_bucket' => round($lat, 2), 'lng_bucket' => round($lng, 2), 'fuel_type' => $fuelType->value, 'results_count' => $stations->count(), 'lowest_pence' => $prices->min(), 'highest_pence' => $prices->max(), 'avg_pence' => $prices->isNotEmpty() ? round($prices->avg(), 2) : null, 'searched_at' => now(), 'ip_hash' => hash('sha256', $request->ip() ?? ''), ]); return response()->json([ 'data' => StationResource::collection($stations), 'meta' => [ 'count' => $stations->count(), 'fuel_type' => $fuelType->value, 'radius_km' => $radius, 'lowest_pence' => $prices->min(), 'highest_pence' => $prices->max(), 'cheapest_price_pence' => $prices->min(), 'avg_pence' => $prices->isNotEmpty() ? round($prices->avg(), 2) : null, ], ]); } }