delivery_app/lib/services/geocoding.dart

118 lines
3.9 KiB
Dart

import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;
import 'package:http/http.dart' as http;
import 'package:latlong2/latlong.dart';
import '../hoogerheide_streets.dart';
class GeocodingService {
static final GeocodingService _instance = GeocodingService._internal();
factory GeocodingService() => _instance;
GeocodingService._internal();
static const _nominatimBaseUrl = 'https://nominatim.openstreetmap.org';
static const _userAgent = 'DeliveryApp/1.0 (delivery-app-openclaw@users.noreply.github.com)';
Map<String, dynamic>? _addressCache;
String _normalize(String name) =>
name.replaceAll(RegExp(r'[–—]'), ' ').replaceAll(RegExp(r'\s+'), ' ').trim().toLowerCase();
Future<Map<String, dynamic>> _loadAddressCache() async {
if (_addressCache != null) return _addressCache!;
try {
final jsonStr = await rootBundle.loadString('assets/hoogerheide_addresses.json');
_addressCache = jsonDecode(jsonStr);
return _addressCache!;
} catch (_) {
return {'addresses': []};
}
}
/// Geocode a street + house number to LatLng.
/// Priority: local JSON cache → Nominatim → hoogerheide_streets.dart → default
Future<LatLng> geocode(String street, String houseNum) async {
final ns = _normalize(street);
final hn = houseNum.trim();
// 1. Local address cache (fast, no rate limits)
try {
final cache = await _loadAddressCache();
final addresses = cache['addresses'] as List? ?? [];
for (final addr in addresses) {
final addrStreet = (addr['street'] as String).toLowerCase();
final addrNum = addr['housenumber'] as String;
if (addrStreet == ns && addrNum == hn) {
return LatLng(addr['lat'] as double, addr['lon'] as double);
}
}
} catch (_) {}
// 2. Nominatim API
try {
final query = Uri.encodeComponent('$street $houseNum, Hoogerheide, Netherlands');
final url = '$_nominatimBaseUrl/search?format=jsonv2&q=$query&limit=1';
final resp = await http.get(
Uri.parse(url),
headers: {'User-Agent': _userAgent},
).timeout(const Duration(seconds: 5));
if (resp.statusCode == 200) {
final data = jsonDecode(resp.body);
if (data is List && data.isNotEmpty) {
return LatLng(
double.parse(data[0]['lat']),
double.parse(data[0]['lon']),
);
}
}
} catch (_) {}
// 3. hoogerheide_streets.dart lookup (exact then partial match)
final streetResult = _lookupInStreets(street);
if (streetResult != null) {
// Offset by house number to spread markers along the street
final hnInt = int.tryParse(houseNum.replaceAll(RegExp(r'[A-Za-z]'), '')) ?? 1;
return LatLng(
streetResult.latitude + (hnInt - 1) * 0.00015,
streetResult.longitude + ((hnInt / 25).floor() % 2 == 0 ? 0 : 0.0002),
);
}
// 4. Default to Hoogerheide center
return const LatLng(51.4243390, 4.3238380);
}
LatLng? _lookupInStreets(String street) {
final ns = _normalize(street);
// Exact match
for (final entry in HoogerheideStreets.streetCoords.entries) {
if (_normalize(entry.key) == ns) {
return LatLng(entry.value[0], entry.value[1]);
}
}
// Partial match
for (final entry in HoogerheideStreets.streetCoords.entries) {
final ek = _normalize(entry.key);
if (ns.contains(ek) || ek.contains(ns)) {
return LatLng(entry.value[0], entry.value[1]);
}
}
return null;
}
/// Batch-geocode a list of stops, updating coords only when they changed.
Future<List<LatLng?>> geocodeStops(List<(String street, String houseNum)> addresses) async {
final results = <LatLng?>[];
for (final (street, houseNum) in addresses) {
try {
results.add(await geocode(street, houseNum));
} catch (_) {
results.add(null);
}
}
return results;
}
}