diff --git a/backend/gestion/services/geo_services.go b/backend/gestion/services/geo_services.go index fc7e1092..7353d4a9 100644 --- a/backend/gestion/services/geo_services.go +++ b/backend/gestion/services/geo_services.go @@ -72,14 +72,19 @@ func (gs *GeoService) GeocodeAddress(address string) (*GeoLocation, error) { return location, nil } - // 2. Tentative directe via Nominatim + // 2. TomTom (primaire — plus fiable que Nominatim pour les adresses FR) + if location, err := GeocodeWithTomTom(address); err == nil { + gs.saveToCache(address, location) + return location, nil + } + + // 3. Fallback Nominatim if location, err := gs.fetchFromNominatim(address); err == nil { gs.saveToCache(address, location) return location, nil } - // 3. ── NOUVEAU : correction automatique de l'adresse ────────────────── - // Déclenché uniquement si le géocodage direct a échoué. + // 4. ── Correction automatique de l'adresse ──────────────────────────── log.Printf("🔍 [GEO] Géocodage direct échoué pour '%s', tentative de correction...", address) suggestion, err := gs.correctionService.ResolveAddress(address) diff --git a/backend/gestion/services/tomtom.go b/backend/gestion/services/tomtom.go index 01c7021a..7a3b8ef7 100644 --- a/backend/gestion/services/tomtom.go +++ b/backend/gestion/services/tomtom.go @@ -15,6 +15,72 @@ import ( "time" ) +// GeocodeWithTomTom géocode une adresse via l'API TomTom Search. +func GeocodeWithTomTom(address string) (*GeoLocation, error) { + client := &http.Client{Timeout: 10 * time.Second} + + buildReq := func(key string) (*http.Request, error) { + u := &url.URL{ + Scheme: "https", + Host: "api.tomtom.com", + Path: fmt.Sprintf("/search/2/geocode/%s.json", url.PathEscape(address)), + } + q := url.Values{} + q.Set("key", key) + q.Set("countrySet", "FR") + q.Set("limit", "1") + u.RawQuery = q.Encode() + return http.NewRequest(http.MethodGet, u.String(), nil) + } + + resp, err := tomTomKeys.Do(client, buildReq) + if err != nil { + return nil, fmt.Errorf("TomTom geocode: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("TomTom geocode %d: %s", resp.StatusCode, string(body)) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("TomTom geocode lecture: %w", err) + } + + var parsed struct { + Results []struct { + Position struct { + Lat float64 `json:"lat"` + Lon float64 `json:"lon"` + } `json:"position"` + Address struct { + FreeformAddress string `json:"freeformAddress"` + } `json:"address"` + MatchConfidence struct { + Score float64 `json:"score"` + } `json:"matchConfidence"` + } `json:"results"` + } + if err := json.Unmarshal(body, &parsed); err != nil { + return nil, fmt.Errorf("TomTom geocode parsing: %w", err) + } + if len(parsed.Results) == 0 { + return nil, fmt.Errorf("TomTom geocode: aucun résultat pour '%s'", address) + } + + r := parsed.Results[0] + log.Printf("📍 [GEO] TomTom geocode '%s' → %s (%.6f, %.6f) conf=%.2f", + address, r.Address.FreeformAddress, r.Position.Lat, r.Position.Lon, r.MatchConfidence.Score) + + return &GeoLocation{ + Latitude: r.Position.Lat, + Longitude: r.Position.Lon, + DisplayName: r.Address.FreeformAddress, + }, nil +} + func GetETAWithTraffic(from, to Coordinates) (etaMinutes int, distanceKm float64, err error) { client := &http.Client{Timeout: 10 * time.Second}