/// ───────────────────────────────────────────────────────────────────────── /// WHERE A STOP IS, IN ONE WORD /// /// A rider scanning twenty stops is deciding between *areas* — is Joe on the /// way to Priya, or the opposite side of the city. He is not reading twenty /// street strings, and he certainly is not reading twenty street strings cut /// off mid-word: `SEQTEST Gandhipur…` answers nothing that a blank line would /// not have answered, and costs a line to do it. /// /// So the collapsed queue shows the locality and the full address lives where /// the rare question is asked — the detail sheet, and the map at the door. /// /// ── Why this is one function and not two ── /// /// Home and Deliveries both needed it and both had their own. The rule is /// subtle enough to get wrong in exactly the same way twice: the first version /// skipped a component only when it was *entirely* digits, so /// `12 SNS Colony, Peelamedu, Coimbatore 641004` returned `12 SNS Colony` — a /// street with a door number on it, presented as an area. Two copies of that /// bug is two places to find it. /// ───────────────────────────────────────────────────────────────────────── library; /// The locality component of a stop's address, or `''` when it has none. /// /// Prefers the drop, because on a delivery leg the area that matters is where /// the bag is going. Falls back to the pickup for a stop that carries no drop, /// and returns nothing rather than inventing a placeholder. String areaOf(Map stop, {bool preferDrop = true}) { final keys = preferDrop ? const [ 'dropaddress', 'DropAddress', 'deliveryaddress', 'pickupaddress', 'PickupAddress', ] : const [ 'pickupaddress', 'PickupAddress', 'dropaddress', 'DropAddress', 'deliveryaddress', ]; for (final key in keys) { final raw = (stop[key] ?? '').toString().trim(); if (raw.isEmpty) continue; final parts = raw .split(',') .map((p) => p.trim()) .where((p) => p.isNotEmpty) .toList(); // ── An area is a NAME, not a description ── // // Three versions of this rule, each corrected by a real address: // // 1. "Skip a part only when it is entirely digits" printed `12 SNS // Colony` — a door number presented as an area. // 2. "Skip a part containing any digit" fixed that, but then // `12 Peelamedu, Coimbatore` lost `12 Peelamedu` wholesale and answered // with the *city* — and every stop on a Coimbatore route is in // Coimbatore, so the queue said nothing at all. Seen on a device. // 3. Now the digits are stripped from *inside* the part instead of taking // the part with them: `12 Peelamedu` yields the name `Peelamedu`. // // What separates a locality from a landmark is length in words — names // are short (Gandhipuram, Peelamedu), landmarks and streets are phrases // (`Coimbatore Gandhipuram Mofussil Bus Stand`, verified live). And the // parts are scanned in order because an Indian address runs specific → // general: the earliest name is the most local one, which is exactly what // beats `Coimbatore` when both are one-word names. // // The word left when a number is removed is sometimes administrative // noise, not a place — `Ward 54` is not the ward's name — so those // residues are discarded by a small stop list. // `Ward 54` is not a place called Ward, and `4 Cross` — Indian street // numbering — is not a place called Cross. Both from live addresses. const noise = { 'ward', 'zone', 'no', 'door', 'flat', 'floor', 'plot', 'block', // 'cross', 'main', 'street', 'road', }; String? nameOf(String part) { final words = [ for (final w in part.split(RegExp(r'\s+'))) if (!RegExp(r'[0-9]').hasMatch(w)) w, ]; if (words.isEmpty) return null; // `RS Puram RS Puram` — the geocoder stammer [compactAddress] already // collapses — reaches here too, seen live as `Gandhipuram Gandhipuram` // on the queue. A name whose two halves are the same name is one name. if (words.length.isEven && words.length >= 2) { final half = words.length ~/ 2; final a = words.sublist(0, half).join(' '); final b = words.sublist(half).join(' '); if (a.toLowerCase() == b.toLowerCase()) { words.removeRange(half, words.length); } } final name = words.join(' '); if (words.length == 1 && (name.length < 3 || noise.contains(name.toLowerCase()))) { return null; } return name; } // ── The city rung is named, because position cannot find it ── // // "Drop the last part" was tried and is structurally undecidable: an // address that ends at the city (`2 Saibaba Colony, Coimbatore`) and one // that ends at the locality (`12, Cross Cut Road, Gandhipuram`) look // identical from the end. Both are live addresses, and each broke the // heuristic the other needed. // // So the rung is knowledge, like the noise words above: the region this // tenant operates in, plus the state/country tails geocoders append. A // city name distinguishes nothing on a route that is entirely inside it, // so it ranks last — but it still stands when it is all the address has, // because an honest rung beats a blank. bool isCity(String n) => const { 'coimbatore', 'coimbatore north', 'coimbatore south', // 'tamil nadu', 'india', 'chennai', 'tiruppur', 'erode', 'salem', 'madurai', 'pollachi', 'mettupalayam', }.contains(n.toLowerCase()); // A two-word name ending in a thoroughfare word is a street, not a // locality — `Mettupalayam Road` must lose to the `RS Puram` behind it, // exactly as the longer landmark phrases already do. bool isStreet(String n) => const { 'road', 'street', 'salai', 'lane', 'veedhi', 'highway', // }.contains(n.split(' ').last.toLowerCase()); final names = [ for (final p in parts) if (nameOf(p) case final n?) n, ]; final local = [ for (final n in names) if (!isCity(n)) n, ]; // In rank order: the first one-word name — a locality is a name and // names are short; then the first short non-street name (RS Puram, // Saibaba Colony); then any phrase left; and only then the city. for (final n in local) { if (!n.contains(' ')) return n; } for (final n in local) { if (n.split(' ').length <= 2 && !isStreet(n)) return n; } if (local.isNotEmpty) return local.first; if (names.isNotEmpty) return names.first; } return ''; } /// A full address, minus the boilerplate a geocoder appends. /// /// The record page prints the address in full — it is the one screen whose /// job is the specifics — and on live data that full address arrived as /// `RS Puram RS Puram, Ward 24, West Zone, Perur, Coimbatore South, /// Coimbatore, Tamil Nadu, 641002, India`, wrapped to two lines and cut with /// an ellipsis. The trimmed tail was the useful half; the kept head began by /// stammering. /// /// This strips only what carries no information at a door in this region: /// the state and country rungs, a part that is nothing but a pincode, an /// immediate word-for-word repeat inside a part, and a part that repeats the /// one before it. Everything that names a place stays, in order. String compactAddress(String raw) { final parts = raw .split(',') .map((p) => p.trim()) .where((p) => p.isNotEmpty) .toList(); const tail = {'tamil nadu', 'india'}; String dedupWords(String part) { final w = part.split(RegExp(r'\s+')); if (w.length.isEven && w.length >= 2) { final half = w.length ~/ 2; final a = w.sublist(0, half).join(' '); final b = w.sublist(half).join(' '); if (a.toLowerCase() == b.toLowerCase()) return a; } return part; } final kept = []; for (final p in parts) { final cleaned = dedupWords(p); final lower = cleaned.toLowerCase(); if (tail.contains(lower)) continue; if (RegExp(r'^[0-9]{6}$').hasMatch(cleaned)) continue; if (kept.isNotEmpty && kept.last.toLowerCase() == lower) continue; kept.add(cleaned); } return kept.join(', '); }