feat: city selection now uses haversine radius filtering (10km)

Enrich kerala_pincodes.json with lat/lng for all 463 entries via
pgeocode offline DB (453 exact matches + 10 district centroids).

Update SearchScreen _LocationItem to carry lat/lng fields, load them
from JSON on init, and pass them through every selection path
(_selectWithPincode, _selectAndClose, search result onTap).

Result: selecting Chavakkad (or any Kerala city) now pops
{label, pincode, lat:10.59322, lng:76.0297} → home_screen saves coords
to prefs → getEventsByLocation sends lat/lng to Django → haversine
filtering returns events within 10km radius, expanding to 25/50/100km
if fewer than 6 events found.
This commit is contained in:
2026-04-04 19:10:07 +05:30
parent 8481b14a7a
commit c6c313854d
2 changed files with 3261 additions and 475 deletions

View File

@@ -9,13 +9,15 @@ import '../core/utils/error_utils.dart';
import 'package:geolocator/geolocator.dart';
import 'package:geocoding/geocoding.dart';
/// Data model for a location suggestion (city + optional pincode).
/// Data model for a location suggestion (city + optional pincode + optional coords).
class _LocationItem {
final String city;
final String? district;
final String? pincode;
final double? lat;
final double? lng;
const _LocationItem({required this.city, this.district, this.pincode});
const _LocationItem({required this.city, this.district, this.pincode, this.lat, this.lng});
String get displayTitle => district != null && district!.isNotEmpty ? '$city, $district' : city;
String get displaySubtitle => pincode ?? '';
@@ -71,6 +73,8 @@ class _SearchScreenState extends State<SearchScreen> {
city: e['city'] as String,
district: e['district'] as String?,
pincode: e['pincode'] as String?,
lat: (e['lat'] as num?)?.toDouble(),
lng: (e['lng'] as num?)?.toDouble(),
)).toList();
if (mounted) {
setState(() {
@@ -106,24 +110,28 @@ class _SearchScreenState extends State<SearchScreen> {
});
}
/// Pop with a structured result so home_screen can update both the display
/// label AND the pincode / GPS coordinates used for API filtering.
void _selectWithPincode(String label, {String? pincode}) {
Navigator.of(context).pop(<String, dynamic>{
/// Pop with a structured result so home_screen can update the display label,
/// pincode, and GPS coordinates used for haversine filtering.
void _selectWithPincode(String label, {String? pincode, double? lat, double? lng}) {
final result = <String, dynamic>{
'label': label,
'pincode': pincode ?? 'all',
});
};
if (lat != null && lng != null) {
result['lat'] = lat;
result['lng'] = lng;
}
Navigator.of(context).pop(result);
}
void _selectAndClose(String location) {
// Legacy path for plain strings — wrap in structured map.
// Tries to look up a pincode from the database for the given city name.
// Looks up pincode + coordinates from the database for the given city name.
final match = _locationDb.cast<_LocationItem?>().firstWhere(
(loc) => loc!.city.toLowerCase() == location.toLowerCase() ||
loc.displayTitle.toLowerCase() == location.toLowerCase(),
orElse: () => null,
);
_selectWithPincode(location, pincode: match?.pincode);
_selectWithPincode(location, pincode: match?.pincode, lat: match?.lat, lng: match?.lng);
}
Future<void> _useCurrentLocation() async {
@@ -344,7 +352,7 @@ class _SearchScreenState extends State<SearchScreen> {
subtitle: loc.pincode != null && loc.pincode!.isNotEmpty
? Text(loc.pincode!, style: TextStyle(color: Colors.grey[500], fontSize: 13))
: null,
onTap: () => _selectWithPincode(loc.displayTitle, pincode: loc.pincode),
onTap: () => _selectWithPincode(loc.displayTitle, pincode: loc.pincode, lat: loc.lat, lng: loc.lng),
);
},
),