validate([ 'query' => 'required|string|max:200', 'bias_lat' => 'nullable|numeric', 'bias_lng' => 'nullable|numeric', ]); $key = config('services.google.maps_key'); if (empty($key)) { return response()->json(['error' => 'Geocoding nicht konfiguriert'], 503); } $query = $request->string('query')->toString(); $biasLat = $request->filled('bias_lat') ? (float) $request->input('bias_lat') : null; $biasLng = $request->filled('bias_lng') ? (float) $request->input('bias_lng') : null; // 1. Versuch: Places Text Search - findet POIs anhand des Namens $hit = $this->tryPlacesText($query, $biasLat, $biasLng, $key); if ($hit) { return response()->json($hit); } // 2. Fallback: Forward Geocoding - falls's eine Adresse ist $hit = $this->tryGeocode($query, $biasLat, $biasLng, $key); if ($hit) { return response()->json($hit); } return response()->json(['error' => 'not_found'], 404); } protected function tryPlacesText(string $query, ?float $lat, ?float $lng, string $key): ?array { try { $body = [ 'textQuery' => $query, 'languageCode' => 'de', 'regionCode' => 'AT', 'maxResultCount' => 1, ]; if ($lat !== null && $lng !== null) { $body['locationBias'] = [ 'circle' => [ 'center' => ['latitude' => $lat, 'longitude' => $lng], 'radius' => 20_000.0, ], ]; } $response = Http::timeout(8) ->withHeaders([ 'Content-Type' => 'application/json', 'X-Goog-Api-Key' => $key, 'X-Goog-FieldMask' => 'places.id,places.displayName,places.formattedAddress,places.location', ]) ->post('https://places.googleapis.com/v1/places:searchText', $body); if (! $response->successful()) { Log::warning('Places-Text HTTP-Fehler', ['status' => $response->status(), 'body' => mb_substr($response->body(), 0, 200)]); return null; } $place = $response->json('places.0'); if (! $place) { return null; } return [ 'name' => $place['displayName']['text'] ?? null, 'address' => $place['formattedAddress'] ?? '', 'lat' => $place['location']['latitude'] ?? null, 'lng' => $place['location']['longitude'] ?? null, 'place_id' => $place['id'] ?? null, 'source' => 'places_text', ]; } catch (\Throwable $e) { Log::warning('Places-Text exception', ['e' => $e->getMessage()]); return null; } } protected function tryGeocode(string $query, ?float $lat, ?float $lng, string $key): ?array { $params = [ 'address' => $query, 'language' => 'de', 'region' => 'at', 'key' => $key, ]; if ($lat !== null && $lng !== null) { $params['bounds'] = sprintf('%f,%f|%f,%f', $lat - 0.1, $lng - 0.15, $lat + 0.1, $lng + 0.15); } try { $response = Http::timeout(8) ->get('https://maps.googleapis.com/maps/api/geocode/json', $params); if (! $response->successful()) { return null; } $data = $response->json(); if (($data['status'] ?? null) !== 'OK' || empty($data['results'])) { return null; } $r = $data['results'][0]; return [ 'name' => $r['address_components'][0]['long_name'] ?? null, 'address' => $r['formatted_address'] ?? '', 'lat' => $r['geometry']['location']['lat'] ?? null, 'lng' => $r['geometry']['location']['lng'] ?? null, 'place_id' => $r['place_id'] ?? null, 'source' => 'geocode', ]; } catch (\Throwable $e) { Log::warning('Geocode exception', ['e' => $e->getMessage()]); return null; } } }