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' => 'no_key'], 503); } $query = $request->string('query')->toString(); $lat = $request->filled('bias_lat') ? (float) $request->input('bias_lat') : null; $lng = $request->filled('bias_lng') ? (float) $request->input('bias_lng') : null; try { $body = [ 'textQuery' => $query, 'languageCode' => 'de', 'regionCode' => 'AT', 'maxResultCount' => 1, ]; if ($lat !== null && $lng !== null) { $body['locationBias'] = [ 'circle' => [ 'center' => ['latitude' => $lat, 'longitude' => $lng], 'radius' => 30_000.0, ], ]; } $response = Http::timeout(10) ->withHeaders([ 'Content-Type' => 'application/json', 'X-Goog-Api-Key' => $key, 'X-Goog-FieldMask' => 'places.id,places.displayName,places.formattedAddress,' .'places.location,places.rating,places.userRatingCount,places.websiteUri,' .'places.regularOpeningHours,places.types,places.editorialSummary,' .'places.photos,places.priceLevel,' .'places.nationalPhoneNumber,places.internationalPhoneNumber', ]) ->post('https://places.googleapis.com/v1/places:searchText', $body); if (! $response->successful()) { Log::warning('Place-Info HTTP-Fehler', ['status' => $response->status()]); return response()->json(['error' => 'api_error'], 502); } $place = $response->json('places.0'); if (! $place) { return response()->json(['error' => 'not_found'], 404); } $photoUrl = null; if (! empty($place['photos'][0]['name'])) { // Photo URL: /v1/{photo_resource}/media?maxWidthPx=...&key=... $photoUrl = 'https://places.googleapis.com/v1/'.$place['photos'][0]['name'].'/media?maxWidthPx=800&key='.urlencode($key); } $photoUrls = []; foreach (array_slice($place['photos'] ?? [], 0, 5) as $photo) { if (!empty($photo['name'])) { $photoUrls[] = 'https://places.googleapis.com/v1/'.$photo['name'].'/media?maxWidthPx=800&key='.urlencode($key); } } return response()->json([ 'name' => $place['displayName']['text'] ?? null, 'address' => $place['formattedAddress'] ?? '', 'lat' => $place['location']['latitude'] ?? null, 'lng' => $place['location']['longitude'] ?? null, 'rating' => $place['rating'] ?? null, 'rating_count' => $place['userRatingCount'] ?? null, 'website' => $place['websiteUri'] ?? null, 'summary' => $place['editorialSummary']['text'] ?? null, 'opening_hours' => $place['regularOpeningHours']['weekdayDescriptions'] ?? null, 'types' => $place['types'] ?? [], 'photo_url' => $photoUrl, 'price_level' => $place['priceLevel'] ?? null, 'place_id' => $place['id'] ?? null, 'phone' => $place['nationalPhoneNumber'] ?? $place['internationalPhoneNumber'] ?? null, 'photo_urls' => $photoUrls, ]); } catch (\Throwable $e) { Log::warning('Place-Info exception', ['e' => $e->getMessage()]); return response()->json(['error' => 'exception'], 500); } } }