Files

72 lines
3.5 KiB
Go

// Package legs answers one question, in one place: does a parcel go straight to
// its receiver, or does it go via a base first?
//
// It exists because two packages need that answer and neither can import the
// other. controllers decides it at pickup-complete (which state the consignment
// lands in, and which base the rider is sent to); internal/routing needs it when
// it sequences a rider's stops, because a hub-routed parcel's next stop is the
// BASE, not the address on the booking. controllers already imports
// internal/routing, so routing cannot import back — and a second copy of the
// rule in routing is exactly the kind of duplication this codebase has been
// bitten by before (the rival status table in dispatchShared, the batch windows
// that drifted between two pages).
//
// So the rule lives here and both call it. Neither owns it.
package legs
import "math"
// HaversineKM is the straight-line distance between two points, in kilometres.
// One definition for the whole codebase — controllers.haversineKM delegates to
// it rather than keeping a second copy.
func HaversineKM(lat1, lon1, lat2, lon2 float64) float64 {
const earthRadiusKM = 6371.0
toRad := func(deg float64) float64 { return deg * math.Pi / 180 }
dLat := toRad(lat2 - lat1)
dLon := toRad(lon2 - lon1)
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
math.Cos(toRad(lat1))*math.Cos(toRad(lat2))*math.Sin(dLon/2)*math.Sin(dLon/2)
return earthRadiusKM * 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
}
// MaxHyperlocalKM bounds the straight-line pickup→delivery distance under which
// a booking with a missing or unusable pincode is still treated as hyperlocal.
// It is ONLY consulted when the pincode rule cannot decide: console-created
// kitchen→customer bookings frequently carry accurate coordinates and no
// delivery pincode, and must not be routed through a base because of it.
const MaxHyperlocalKM = 30.0
// SamePostalArea reports whether two pincodes fall in the same 3-digit postal
// area. Pincodes shorter than 3 characters are treated as unknown rather than
// matching, so bad data falls back to the safe hub route instead of quietly
// claiming two parcels belong together.
func SamePostalArea(pickupPincode, deliveryPincode string) bool {
if len(pickupPincode) < 3 || len(deliveryPincode) < 3 {
return false
}
return pickupPincode[:3] == deliveryPincode[:3]
}
// IsHyperlocal decides whether a parcel can skip the base and be carried
// straight to the receiver by the collecting rider.
//
// The pincode-prefix rule decides outright when both pincodes are present — a
// matching prefix is hyperlocal, a differing one is genuinely inter-area, and
// distance does not get a vote. Pollachi is 40km from Coimbatore, closer than
// plenty of runs treated as local, and it is still a different postal area.
//
// Only when a pincode is missing or too short does the straight-line distance
// decide instead. With neither pincodes nor coordinates the answer is false,
// which routes via a base — the safe direction to be wrong in, because a parcel
// that reaches a base can still be forwarded, while one handed to a rider who
// cannot reach the receiver is stuck.
func IsHyperlocal(pickupPincode, deliveryPincode string, pLat, pLng, dLat, dLng float64) bool {
if len(pickupPincode) >= 3 && len(deliveryPincode) >= 3 {
return SamePostalArea(pickupPincode, deliveryPincode)
}
if pLat != 0 && pLng != 0 && dLat != 0 && dLng != 0 {
return HaversineKM(pLat, pLng, dLat, dLng) <= MaxHyperlocalKM
}
return false
}