// Waka generated runtime role: public window.WAKA_RUNTIME_ROLE = "public"; window.WAKA_ALLOWED_RUNTIME_TABS = ["passenger","rider"]; // Generated by scripts/generate-cameroon-open-data-areas.js. // Sources: OpenStreetMap Overpass cache (ODbL) and GeoNames Cameroon dump (Creative Commons Attribution 4.0). // Do not edit this file by hand; regenerate after updating .osm-cache. const cameroonOpenDataAreaSeeds = { "Abong-Mbang": [ { "name": "Abong Doum", "latitude": 3.98333, "longitude": 13.15, "x": 50, "y": 52.7, "source": "geonames", "type": "PPL" }, { "name": "Abonis", "latitude": 4.1, "longitude": 13.05, "x": 26, "y": 15.3, "source": "geonames", "type": "PPL" }, { "name": "Adouma", "latitude": 3.98333, "longitude": 13.11667, "x": 42, "y": 52.7, "source": "geonames", "type": "PPL" }, { "name": "Andjou", "latitude": 4.1, "longitude": 13.03333, "x": 22, "y": 15.3, "source": "geonames", "type": "PPL" }, { "name": "Andjouk", "latitude": 4.08333, "longitude": 13.01667, "x": 18, "y": 20.7, "source": "geonames", "type": "PPL" }, { "name": "Angossas", "latitude": 4.08333, "longitude": 12.98333, "x": 10, "y": 20.7, "source": "geonames", "type": "PPL" }, { "name": "Ankoung", "latitude": 4.01667, "longitude": 13.03333, "x": 22, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Ankouomb", "latitude": 4.08333, "longitude": 13.08333, "x": 34, "y": 20.7, "source": "geonames", "type": "PPL" }, { "name": "Ayene", "latitude": 3.96667, "longitude": 13.2, "x": 62, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Azie", "latitude": 3.96667, "longitude": 13.15, "x": 50, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Bagbeze", "latitude": 4.1, "longitude": 13.01667, "x": 18, "y": 15.3, "source": "geonames", "type": "PPL" }, { "name": "Bago", "latitude": 4.05, "longitude": 13.2, "x": 62, "y": 31.3, "source": "geonames", "type": "PPL" }, { "name": "Bagoale", "latitude": 4.06667, "longitude": 13.11667, "x": 42, "y": 26, "source": "geonames", "type": "PPL" }, { "name": "Bagofit", "latitude": 4, "longitude": 13.11667, "x": 42, "y": 47.3, "source": "geonames", "type": "PPL" }, { "name": "Bagoloul", "latitude": 4.06667, "longitude": 13.11667, "x": 42, "y": 26, "source": "geonames", "type": "PPL" }, { "name": "Bamakou", "latitude": 4.01667, "longitude": 13.06667, "x": 30, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Bindanang", "latitude": 4.01667, "longitude": 13.25, "x": 74, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Bonando", "latitude": 4.11667, "longitude": 13.26667, "x": 78, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Djakoundi", "latitude": 4.06667, "longitude": 13.1, "x": 38, "y": 26, "source": "geonames", "type": "PPL" }, { "name": "Djamonomine", "latitude": 4.11667, "longitude": 13.25, "x": 74, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Djenasoumeu", "latitude": 3.95, "longitude": 13.11667, "x": 42, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Djende", "latitude": 4.06667, "longitude": 13.21667, "x": 66, "y": 26, "source": "geonames", "type": "PPL" }, { "name": "Djibe", "latitude": 4, "longitude": 12.98333, "x": 10, "y": 47.3, "source": "geonames", "type": "PPL" }, { "name": "Djodjok", "latitude": 4.01667, "longitude": 13.01667, "x": 18, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Kek", "latitude": 4.1, "longitude": 13.06667, "x": 30, "y": 15.3, "source": "geonames", "type": "PPL" }, { "name": "Madouma", "latitude": 3.96667, "longitude": 13.2, "x": 62, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Mampang", "latitude": 4, "longitude": 13.18333, "x": 58, "y": 47.3, "source": "geonames", "type": "PPL" }, { "name": "Mazzabe", "latitude": 4.01667, "longitude": 13.05, "x": 26, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Motcheboum", "latitude": 4.1, "longitude": 13.23333, "x": 70, "y": 15.3, "source": "geonames", "type": "PPL" }, { "name": "Mpemezok", "latitude": 3.91667, "longitude": 13.26667, "x": 78, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Mpoundou", "latitude": 4.1, "longitude": 13.05, "x": 26, "y": 15.3, "source": "geonames", "type": "PPL" }, { "name": "Myan I", "latitude": 3.95, "longitude": 13.25, "x": 74, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Myan II", "latitude": 3.96667, "longitude": 13.11667, "x": 42, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Nkolmvolan", "latitude": 3.96667, "longitude": 13.13333, "x": 46, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Nkouak", "latitude": 3.86667, "longitude": 13.31667, "x": 90, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ntimbe", "latitude": 3.88333, "longitude": 13.28333, "x": 82, "y": 84.7, "source": "geonames", "type": "PPL" }, { "name": "Ntoung", "latitude": 4, "longitude": 12.98333, "x": 10, "y": 47.3, "source": "geonames", "type": "PPL" }, { "name": "Oboul", "latitude": 3.96667, "longitude": 13.23333, "x": 70, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Sokamalam", "latitude": 3.96667, "longitude": 13.25, "x": 74, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Wama", "latitude": 4.05, "longitude": 13.15, "x": 50, "y": 31.3, "source": "geonames", "type": "PPL" }, { "name": "Zende", "latitude": 4.08333, "longitude": 13, "x": 14, "y": 20.7, "source": "geonames", "type": "PPL" } ], "Akom II": [ { "name": "Abiete", "latitude": 2.91667, "longitude": 10.5, "x": 36.7, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Adjap", "latitude": 2.9, "longitude": 10.58333, "x": 53.3, "y": 38.6, "source": "geonames", "type": "PPL" }, { "name": "Aloum", "latitude": 2.8, "longitude": 10.73333, "x": 83.3, "y": 72.9, "source": "geonames", "type": "PPL" }, { "name": "Assok", "latitude": 2.76667, "longitude": 10.46667, "x": 30, "y": 84.3, "source": "geonames", "type": "PPL" }, { "name": "Awomo", "latitude": 2.75, "longitude": 10.75, "x": 86.7, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Bilobe I", "latitude": 2.8, "longitude": 10.41667, "x": 20, "y": 72.9, "source": "geonames", "type": "PPL" }, { "name": "Bilobe II", "latitude": 2.78333, "longitude": 10.43333, "x": 23.3, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Ebemwok", "latitude": 2.8, "longitude": 10.66667, "x": 70, "y": 72.9, "source": "geonames", "type": "PPL" }, { "name": "Efoulan", "latitude": 2.78333, "longitude": 10.53333, "x": 43.3, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Ekowong", "latitude": 2.8, "longitude": 10.56667, "x": 50, "y": 72.9, "source": "geonames", "type": "PPL" }, { "name": "Evouma", "latitude": 2.76667, "longitude": 10.71667, "x": 80, "y": 84.3, "source": "geonames", "type": "PPL" }, { "name": "Fenda", "latitude": 2.8, "longitude": 10.4, "x": 16.7, "y": 72.9, "source": "geonames", "type": "PPL" }, { "name": "Kienke", "latitude": 2.85, "longitude": 10.58333, "x": 53.3, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Madjabibomg", "latitude": 2.93333, "longitude": 10.5, "x": 36.7, "y": 27.1, "source": "geonames", "type": "PPL" }, { "name": "Mbanga", "latitude": 2.78333, "longitude": 10.51667, "x": 40, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Messambe", "latitude": 2.96667, "longitude": 10.68333, "x": 73.3, "y": 15.7, "source": "geonames", "type": "PPL" }, { "name": "Minkane", "latitude": 2.98333, "longitude": 10.63333, "x": 63.3, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Mvie", "latitude": 2.88333, "longitude": 10.58333, "x": 53.3, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Nelefoup", "latitude": 2.78333, "longitude": 10.76667, "x": 90, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Niabitande", "latitude": 2.76667, "longitude": 10.48333, "x": 33.3, "y": 84.3, "source": "geonames", "type": "PPL" }, { "name": "Njabilobe", "latitude": 2.78333, "longitude": 10.36667, "x": 10, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Nkomakak", "latitude": 2.8, "longitude": 10.55, "x": 46.7, "y": 72.9, "source": "geonames", "type": "PPL" }, { "name": "Nlomoto", "latitude": 2.76667, "longitude": 10.5, "x": 36.7, "y": 84.3, "source": "geonames", "type": "PPL" }, { "name": "Nyangong", "latitude": 2.98333, "longitude": 10.61667, "x": 60, "y": 10, "source": "geonames", "type": "PPL" } ], "Akono": [ { "name": "Abang II", "latitude": 3.58333, "longitude": 11.28333, "x": 50, "y": 23.3, "source": "geonames", "type": "PPL" }, { "name": "Abang Mindi", "latitude": 3.45, "longitude": 11.38333, "x": 76.7, "y": 58.9, "source": "geonames", "type": "PPL" }, { "name": "Akak", "latitude": 3.38333, "longitude": 11.33333, "x": 63.3, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Akok Bikoe", "latitude": 3.46667, "longitude": 11.4, "x": 81.1, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "Andok", "latitude": 3.36667, "longitude": 11.36667, "x": 72.2, "y": 81.1, "source": "geonames", "type": "PPL" }, { "name": "Asok", "latitude": 3.4, "longitude": 11.25, "x": 41.1, "y": 72.2, "source": "geonames", "type": "PPL" }, { "name": "Bakoukoue", "latitude": 3.53333, "longitude": 11.15, "x": 14.4, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Bikoe I", "latitude": 3.63333, "longitude": 11.38333, "x": 76.7, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Bikolok", "latitude": 3.63333, "longitude": 11.38333, "x": 76.7, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Bikoukound", "latitude": 3.5, "longitude": 11.21667, "x": 32.2, "y": 45.6, "source": "geonames", "type": "PPL" }, { "name": "Bilik", "latitude": 3.51667, "longitude": 11.28333, "x": 50, "y": 41.1, "source": "geonames", "type": "PPL" }, { "name": "Bimvia", "latitude": 3.48333, "longitude": 11.36667, "x": 72.2, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Bitoutouk", "latitude": 3.5, "longitude": 11.13333, "x": 10, "y": 45.6, "source": "geonames", "type": "PPL" }, { "name": "Boumnkok", "latitude": 3.48333, "longitude": 11.13333, "x": 10, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Eboloum", "latitude": 3.6, "longitude": 11.26667, "x": 45.6, "y": 18.9, "source": "geonames", "type": "PPL" }, { "name": "Ekoadjom", "latitude": 3.6, "longitude": 11.2, "x": 27.8, "y": 18.9, "source": "geonames", "type": "PPL" }, { "name": "Ekoudindi", "latitude": 3.41667, "longitude": 11.28333, "x": 50, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Ekoundoum", "latitude": 3.53333, "longitude": 11.31667, "x": 58.9, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Evindisi", "latitude": 3.43333, "longitude": 11.4, "x": 81.1, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Fegmimbang", "latitude": 3.5, "longitude": 11.31667, "x": 58.9, "y": 45.6, "source": "geonames", "type": "PPL" }, { "name": "Leblibong", "latitude": 3.51667, "longitude": 11.18333, "x": 23.3, "y": 41.1, "source": "geonames", "type": "PPL" }, { "name": "Lepse", "latitude": 3.43333, "longitude": 11.23333, "x": 36.7, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Loum", "latitude": 3.41667, "longitude": 11.31667, "x": 58.9, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Malande", "latitude": 3.56667, "longitude": 11.21667, "x": 32.2, "y": 27.8, "source": "geonames", "type": "PPL" }, { "name": "Mbalelon", "latitude": 3.55, "longitude": 11.36667, "x": 72.2, "y": 32.2, "source": "geonames", "type": "PPL" }, { "name": "Mbemndjok", "latitude": 3.48333, "longitude": 11.15, "x": 14.4, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Mbing", "latitude": 3.51667, "longitude": 11.21667, "x": 32.2, "y": 41.1, "source": "geonames", "type": "PPL" }, { "name": "Melen", "latitude": 3.51667, "longitude": 11.38333, "x": 76.7, "y": 41.1, "source": "geonames", "type": "PPL" }, { "name": "Mevamebot", "latitude": 3.38333, "longitude": 11.26667, "x": 45.6, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Meyila", "latitude": 3.6, "longitude": 11.36667, "x": 72.2, "y": 18.9, "source": "geonames", "type": "PPL" }, { "name": "Mezali", "latitude": 3.43333, "longitude": 11.33333, "x": 63.3, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Mfida", "latitude": 3.46667, "longitude": 11.3, "x": 54.4, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "Minka", "latitude": 3.56667, "longitude": 11.13333, "x": 10, "y": 27.8, "source": "geonames", "type": "PPL" }, { "name": "Mom", "latitude": 3.6, "longitude": 11.18333, "x": 23.3, "y": 18.9, "source": "geonames", "type": "PPL" }, { "name": "Mvounkeng", "latitude": 3.48333, "longitude": 11.36667, "x": 72.2, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Ndika", "latitude": 3.36667, "longitude": 11.31667, "x": 58.9, "y": 81.1, "source": "geonames", "type": "PPL" }, { "name": "Ngombas", "latitude": 3.58333, "longitude": 11.16667, "x": 18.9, "y": 23.3, "source": "geonames", "type": "PPL" }, { "name": "Ngon", "latitude": 3.56667, "longitude": 11.26667, "x": 45.6, "y": 27.8, "source": "geonames", "type": "PPL" }, { "name": "Ngoumou", "latitude": 3.58333, "longitude": 11.3, "x": 54.4, "y": 23.3, "source": "geonames", "type": "PPL" }, { "name": "Ngoungoum", "latitude": 3.6, "longitude": 11.21667, "x": 32.2, "y": 18.9, "source": "geonames", "type": "PPL" }, { "name": "Nkoakom", "latitude": 3.58333, "longitude": 11.35, "x": 67.8, "y": 23.3, "source": "geonames", "type": "PPL" }, { "name": "Nkoelon", "latitude": 3.53333, "longitude": 11.31667, "x": 58.9, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Nkolakono I", "latitude": 3.55, "longitude": 11.33333, "x": 63.3, "y": 32.2, "source": "geonames", "type": "PPL" }, { "name": "Nkolakono II", "latitude": 3.51667, "longitude": 11.31667, "x": 58.9, "y": 41.1, "source": "geonames", "type": "PPL" }, { "name": "Nkolebae", "latitude": 3.33333, "longitude": 11.43333, "x": 90, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Nkoleman", "latitude": 3.55, "longitude": 11.25, "x": 41.1, "y": 32.2, "source": "geonames", "type": "PPL" }, { "name": "Nkolemomodo", "latitude": 3.6, "longitude": 11.31667, "x": 58.9, "y": 18.9, "source": "geonames", "type": "PPL" }, { "name": "Nkolmedzap", "latitude": 3.61667, "longitude": 11.26667, "x": 45.6, "y": 14.4, "source": "geonames", "type": "PPL" }, { "name": "Nkolmelen", "latitude": 3.6, "longitude": 11.23333, "x": 36.7, "y": 18.9, "source": "geonames", "type": "PPL" }, { "name": "Nkolmesi", "latitude": 3.6, "longitude": 11.36667, "x": 72.2, "y": 18.9, "source": "geonames", "type": "PPL" }, { "name": "Nkolngok", "latitude": 3.5, "longitude": 11.36667, "x": 72.2, "y": 45.6, "source": "geonames", "type": "PPL" }, { "name": "Nkolngok III", "latitude": 3.58333, "longitude": 11.35, "x": 67.8, "y": 23.3, "source": "geonames", "type": "PPL" }, { "name": "Nkolnlong I", "latitude": 3.48333, "longitude": 11.25, "x": 41.1, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Nkolnlong II", "latitude": 3.46667, "longitude": 11.23333, "x": 36.7, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "Nkolnlong III", "latitude": 3.46667, "longitude": 11.2, "x": 27.8, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "Nkolokede", "latitude": 3.56667, "longitude": 11.25, "x": 41.1, "y": 27.8, "source": "geonames", "type": "PPL" }, { "name": "Nkong", "latitude": 3.5, "longitude": 11.38333, "x": 76.7, "y": 45.6, "source": "geonames", "type": "PPL" }, { "name": "Nkongbibega", "latitude": 3.55, "longitude": 11.3, "x": 54.4, "y": 32.2, "source": "geonames", "type": "PPL" }, { "name": "Nkongdougou", "latitude": 3.61667, "longitude": 11.38333, "x": 76.7, "y": 14.4, "source": "geonames", "type": "PPL" }, { "name": "Nkongmeyos", "latitude": 3.61667, "longitude": 11.31667, "x": 58.9, "y": 14.4, "source": "geonames", "type": "PPL" }, { "name": "Nkongnen I", "latitude": 3.48333, "longitude": 11.41667, "x": 85.6, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Nkongnen II", "latitude": 3.46667, "longitude": 11.4, "x": 81.1, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "Nkongnsam", "latitude": 3.48333, "longitude": 11.25, "x": 41.1, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Nkongzok I", "latitude": 3.56667, "longitude": 11.3, "x": 54.4, "y": 27.8, "source": "geonames", "type": "PPL" }, { "name": "Ofoumou Nselek I", "latitude": 3.55, "longitude": 11.38333, "x": 76.7, "y": 32.2, "source": "geonames", "type": "PPL" }, { "name": "Olama", "latitude": 3.43333, "longitude": 11.28333, "x": 50, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Onana Mbesa", "latitude": 3.4, "longitude": 11.31667, "x": 58.9, "y": 72.2, "source": "geonames", "type": "PPL" }, { "name": "Onangona", "latitude": 3.5, "longitude": 11.41667, "x": 85.6, "y": 45.6, "source": "geonames", "type": "PPL" }, { "name": "Osoe Bekada", "latitude": 3.45, "longitude": 11.2, "x": 27.8, "y": 58.9, "source": "geonames", "type": "PPL" }, { "name": "Osoe Bikobo", "latitude": 3.45, "longitude": 11.18333, "x": 23.3, "y": 58.9, "source": "geonames", "type": "PPL" }, { "name": "Osoebemva", "latitude": 3.4, "longitude": 11.25, "x": 41.1, "y": 72.2, "source": "geonames", "type": "PPL" }, { "name": "Otele", "latitude": 3.58333, "longitude": 11.25, "x": 41.1, "y": 23.3, "source": "geonames", "type": "PPL" }, { "name": "Ovangoul", "latitude": 3.55, "longitude": 11.26667, "x": 45.6, "y": 32.2, "source": "geonames", "type": "PPL" }, { "name": "Sim", "latitude": 3.45, "longitude": 11.25, "x": 41.1, "y": 58.9, "source": "geonames", "type": "PPL" }, { "name": "Songmimbias", "latitude": 3.45, "longitude": 11.41667, "x": 85.6, "y": 58.9, "source": "geonames", "type": "PPL" }, { "name": "Yene Yene", "latitude": 3.6, "longitude": 11.26667, "x": 45.6, "y": 18.9, "source": "geonames", "type": "PPL" }, { "name": "Zoalouma", "latitude": 3.45, "longitude": 11.35, "x": 67.8, "y": 58.9, "source": "geonames", "type": "PPL" } ], "Akonolinga": [ { "name": "Abem", "latitude": 3.7, "longitude": 12.3, "x": 57.3, "y": 62.3, "source": "geonames", "type": "PPL" }, { "name": "Aboue", "latitude": 3.86667, "longitude": 12.41667, "x": 82.7, "y": 31.5, "source": "geonames", "type": "PPL" }, { "name": "Afem", "latitude": 3.7, "longitude": 12.25, "x": 46.4, "y": 62.3, "source": "geonames", "type": "PPL" }, { "name": "Akolo", "latitude": 3.83333, "longitude": 12.18333, "x": 31.8, "y": 37.7, "source": "geonames", "type": "PPL" }, { "name": "Akoua", "latitude": 3.75, "longitude": 12.41667, "x": 82.7, "y": 53.1, "source": "geonames", "type": "PPL" }, { "name": "Andom", "latitude": 3.71667, "longitude": 12.15, "x": 24.5, "y": 59.2, "source": "geonames", "type": "PPL" }, { "name": "Asseng", "latitude": 3.78333, "longitude": 12.4, "x": 79.1, "y": 46.9, "source": "geonames", "type": "PPL" }, { "name": "Awae", "latitude": 3.85, "longitude": 12.11667, "x": 17.3, "y": 34.6, "source": "geonames", "type": "PPL" }, { "name": "Biyombo", "latitude": 3.85, "longitude": 12.08333, "x": 10, "y": 34.6, "source": "geonames", "type": "PPL" }, { "name": "Bong", "latitude": 3.56667, "longitude": 12.2, "x": 35.5, "y": 86.9, "source": "geonames", "type": "PPL" }, { "name": "Eboa", "latitude": 3.75, "longitude": 12.25, "x": 46.4, "y": 53.1, "source": "geonames", "type": "PPL" }, { "name": "Ebolova", "latitude": 3.61667, "longitude": 12.13333, "x": 20.9, "y": 77.7, "source": "geonames", "type": "PPL" }, { "name": "Efoulan", "latitude": 3.73333, "longitude": 12.25, "x": 46.4, "y": 56.2, "source": "geonames", "type": "PPL" }, { "name": "Ekok", "latitude": 3.88333, "longitude": 12.18333, "x": 31.8, "y": 28.5, "source": "geonames", "type": "PPL" }, { "name": "Ekoko", "latitude": 3.81667, "longitude": 12.11667, "x": 17.3, "y": 40.8, "source": "geonames", "type": "PPL" }, { "name": "Ekoua", "latitude": 3.86667, "longitude": 12.26667, "x": 50, "y": 31.5, "source": "geonames", "type": "PPL" }, { "name": "Ekougou", "latitude": 3.73333, "longitude": 12.33333, "x": 64.5, "y": 56.2, "source": "geonames", "type": "PPL" }, { "name": "Ekoumdoum", "latitude": 3.63333, "longitude": 12.15, "x": 24.5, "y": 74.6, "source": "geonames", "type": "PPL" }, { "name": "Emvong", "latitude": 3.95, "longitude": 12.28333, "x": 53.6, "y": 16.2, "source": "geonames", "type": "PPL" }, { "name": "Esi", "latitude": 3.8, "longitude": 12.3, "x": 57.3, "y": 43.8, "source": "geonames", "type": "PPL" }, { "name": "Esong", "latitude": 3.81667, "longitude": 12.16667, "x": 28.2, "y": 40.8, "source": "geonames", "type": "PPL" }, { "name": "Evom", "latitude": 3.98333, "longitude": 12.21667, "x": 39.1, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Eyangap", "latitude": 3.76667, "longitude": 12.35, "x": 68.2, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Eye", "latitude": 3.85, "longitude": 12.31667, "x": 60.9, "y": 34.6, "source": "geonames", "type": "PPL" }, { "name": "Kan", "latitude": 3.55, "longitude": 12.25, "x": 46.4, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Kondan", "latitude": 3.6, "longitude": 12.23333, "x": 42.7, "y": 80.8, "source": "geonames", "type": "PPL" }, { "name": "Koum", "latitude": 3.66667, "longitude": 12.23333, "x": 42.7, "y": 68.5, "source": "geonames", "type": "PPL" }, { "name": "Koundesong", "latitude": 3.96667, "longitude": 12.18333, "x": 31.8, "y": 13.1, "source": "geonames", "type": "PPL" }, { "name": "Koundou", "latitude": 3.9, "longitude": 12.11667, "x": 17.3, "y": 25.4, "source": "geonames", "type": "PPL" }, { "name": "Makak", "latitude": 3.6, "longitude": 12.38333, "x": 75.5, "y": 80.8, "source": "geonames", "type": "PPL" }, { "name": "Mbaka", "latitude": 3.91667, "longitude": 12.23333, "x": 42.7, "y": 22.3, "source": "geonames", "type": "PPL" }, { "name": "Mbaldjap", "latitude": 3.61667, "longitude": 12.21667, "x": 39.1, "y": 77.7, "source": "geonames", "type": "PPL" }, { "name": "Mbang", "latitude": 3.78333, "longitude": 12.36667, "x": 71.8, "y": 46.9, "source": "geonames", "type": "PPL" }, { "name": "Mbeuga", "latitude": 3.81667, "longitude": 12.45, "x": 90, "y": 40.8, "source": "geonames", "type": "PPL" }, { "name": "Mbiele", "latitude": 3.75, "longitude": 12.35, "x": 68.2, "y": 53.1, "source": "geonames", "type": "PPL" }, { "name": "Mbili", "latitude": 3.71667, "longitude": 12.15, "x": 24.5, "y": 59.2, "source": "geonames", "type": "PPL" }, { "name": "Medoumou", "latitude": 3.7, "longitude": 12.13333, "x": 20.9, "y": 62.3, "source": "geonames", "type": "PPL" }, { "name": "Medzek", "latitude": 3.6, "longitude": 12.21667, "x": 39.1, "y": 80.8, "source": "geonames", "type": "PPL" }, { "name": "Melan", "latitude": 3.76667, "longitude": 12.28333, "x": 53.6, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Mengana", "latitude": 3.71667, "longitude": 12.33333, "x": 64.5, "y": 59.2, "source": "geonames", "type": "PPL" }, { "name": "Mengou", "latitude": 3.9, "longitude": 12.13333, "x": 20.9, "y": 25.4, "source": "geonames", "type": "PPL" }, { "name": "Meukong", "latitude": 3.86667, "longitude": 12.23333, "x": 42.7, "y": 31.5, "source": "geonames", "type": "PPL" }, { "name": "Miende", "latitude": 3.86667, "longitude": 12.31667, "x": 60.9, "y": 31.5, "source": "geonames", "type": "PPL" }, { "name": "Mingeumeu", "latitude": 3.58333, "longitude": 12.25, "x": 46.4, "y": 83.8, "source": "geonames", "type": "PPL" }, { "name": "Minlop", "latitude": 3.65, "longitude": 12.21667, "x": 39.1, "y": 71.5, "source": "geonames", "type": "PPL" }, { "name": "Mvan", "latitude": 3.61667, "longitude": 12.3, "x": 57.3, "y": 77.7, "source": "geonames", "type": "PPL" }, { "name": "Ndibi", "latitude": 3.76667, "longitude": 12.21667, "x": 39.1, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Ngala", "latitude": 3.71667, "longitude": 12.41667, "x": 82.7, "y": 59.2, "source": "geonames", "type": "PPL" }, { "name": "Ngole", "latitude": 3.7, "longitude": 12.4, "x": 79.1, "y": 62.3, "source": "geonames", "type": "PPL" }, { "name": "Ngoubou", "latitude": 3.71667, "longitude": 12.21667, "x": 39.1, "y": 59.2, "source": "geonames", "type": "PPL" }, { "name": "Ngoulen", "latitude": 3.56667, "longitude": 12.3, "x": 57.3, "y": 86.9, "source": "geonames", "type": "PPL" }, { "name": "Ngoulmekong", "latitude": 3.8, "longitude": 12.3, "x": 57.3, "y": 43.8, "source": "geonames", "type": "PPL" }, { "name": "Nkoldja", "latitude": 3.7, "longitude": 12.11667, "x": 17.3, "y": 62.3, "source": "geonames", "type": "PPL" }, { "name": "Nlobole", "latitude": 3.8, "longitude": 12.38333, "x": 75.5, "y": 43.8, "source": "geonames", "type": "PPL" }, { "name": "Ting", "latitude": 3.61667, "longitude": 12.4, "x": 79.1, "y": 77.7, "source": "geonames", "type": "PPL" }, { "name": "Yagona", "latitude": 3.96667, "longitude": 12.28333, "x": 53.6, "y": 13.1, "source": "geonames", "type": "PPL" }, { "name": "Yemeyeme", "latitude": 3.63333, "longitude": 12.21667, "x": 39.1, "y": 74.6, "source": "geonames", "type": "PPL" }, { "name": "Zalom", "latitude": 3.6, "longitude": 12.1, "x": 13.6, "y": 80.8, "source": "geonames", "type": "PPL" } ], "Ambam": [ { "name": "Abang", "latitude": 2.35, "longitude": 11.25, "x": 42, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Abang-Minko", "latitude": 2.33333, "longitude": 11.43333, "x": 77.2, "y": 81.1, "source": "geonames", "type": "PPL" }, { "name": "Adjap", "latitude": 2.5, "longitude": 11.16667, "x": 26, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Akak-Metom", "latitude": 2.5, "longitude": 11.1, "x": 13.2, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Akam Bitam", "latitude": 2.33333, "longitude": 11.5, "x": 90, "y": 81.1, "source": "geonames", "type": "PPL" }, { "name": "Akam-Messi", "latitude": 2.6, "longitude": 11.31667, "x": 54.8, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Akom-Bikak", "latitude": 2.41667, "longitude": 11.3, "x": 51.6, "y": 58.9, "source": "geonames", "type": "PPL" }, { "name": "Akonekie", "latitude": 2.46667, "longitude": 11.16667, "x": 26, "y": 45.6, "source": "geonames", "type": "PPL" }, { "name": "Akoulouzok", "latitude": 2.38333, "longitude": 11.28333, "x": 48.4, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Andom", "latitude": 2.5, "longitude": 11.15, "x": 22.8, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Assandjik", "latitude": 2.38333, "longitude": 11.35, "x": 61.2, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Bac", "latitude": 2.3, "longitude": 11.3, "x": 51.6, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Biba", "latitude": 2.5, "longitude": 11.13333, "x": 19.6, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Billi", "latitude": 2.43333, "longitude": 11.21667, "x": 35.6, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "Bilossi", "latitude": 2.33333, "longitude": 11.46667, "x": 83.6, "y": 81.1, "source": "geonames", "type": "PPL" }, { "name": "Binda-Meyos", "latitude": 2.35, "longitude": 11.16667, "x": 26, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Bitam", "latitude": 2.33333, "longitude": 11.46667, "x": 83.6, "y": 81.1, "source": "geonames", "type": "PPL" }, { "name": "Douane", "latitude": 2.38333, "longitude": 11.26667, "x": 45.2, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Ebozi", "latitude": 2.45, "longitude": 11.45, "x": 80.4, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Ekoumedoum", "latitude": 2.58333, "longitude": 11.28333, "x": 48.4, "y": 14.4, "source": "geonames", "type": "PPL" }, { "name": "Enoloung", "latitude": 2.35, "longitude": 11.38333, "x": 67.6, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Enselan", "latitude": 2.46667, "longitude": 11.33333, "x": 58, "y": 45.6, "source": "geonames", "type": "PPL" }, { "name": "Man", "latitude": 2.46667, "longitude": 11.4, "x": 70.8, "y": 45.6, "source": "geonames", "type": "PPL" }, { "name": "Mborossi", "latitude": 2.56667, "longitude": 11.3, "x": 51.6, "y": 18.9, "source": "geonames", "type": "PPL" }, { "name": "Medoumou", "latitude": 2.43333, "longitude": 11.31667, "x": 54.8, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "Mekaman", "latitude": 2.38333, "longitude": 11.3, "x": 51.6, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Mekoa", "latitude": 2.53333, "longitude": 11.3, "x": 51.6, "y": 27.8, "source": "geonames", "type": "PPL" }, { "name": "Mekomo", "latitude": 2.36667, "longitude": 11.36667, "x": 64.4, "y": 72.2, "source": "geonames", "type": "PPL" }, { "name": "Mekoue", "latitude": 2.4, "longitude": 11.26667, "x": 45.2, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Memvin", "latitude": 2.33333, "longitude": 11.08333, "x": 10, "y": 81.1, "source": "geonames", "type": "PPL" }, { "name": "Mendjimi", "latitude": 2.48333, "longitude": 11.31667, "x": 54.8, "y": 41.1, "source": "geonames", "type": "PPL" }, { "name": "Mendjimi II", "latitude": 2.5, "longitude": 11.31667, "x": 54.8, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Meyo", "latitude": 2.43333, "longitude": 11.23333, "x": 38.8, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "Meyo-Elie", "latitude": 2.41667, "longitude": 11.25, "x": 42, "y": 58.9, "source": "geonames", "type": "PPL" }, { "name": "Meyo-Nyaka", "latitude": 2.3, "longitude": 11.3, "x": 51.6, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Mfoulokok", "latitude": 2.35, "longitude": 11.18333, "x": 29.2, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Minkok", "latitude": 2.43333, "longitude": 11.48333, "x": 86.8, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "Minsele", "latitude": 2.35, "longitude": 11.15, "x": 22.8, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Minyin", "latitude": 2.38333, "longitude": 11.31667, "x": 54.8, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Mvoutessi", "latitude": 2.4, "longitude": 11.3, "x": 51.6, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Ngom", "latitude": 2.43333, "longitude": 11.18333, "x": 29.2, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "Nkoadoum", "latitude": 2.6, "longitude": 11.31667, "x": 54.8, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Nkohyo", "latitude": 2.45, "longitude": 11.46667, "x": 83.6, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Nkol-Efoulan", "latitude": 2.43333, "longitude": 11.3, "x": 51.6, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "Nkoumekeke", "latitude": 2.36667, "longitude": 11.26667, "x": 45.2, "y": 72.2, "source": "geonames", "type": "PPL" }, { "name": "Nlono", "latitude": 2.33333, "longitude": 11.4, "x": 70.8, "y": 81.1, "source": "geonames", "type": "PPL" }, { "name": "Nsakoua", "latitude": 2.58333, "longitude": 11.3, "x": 51.6, "y": 14.4, "source": "geonames", "type": "PPL" }, { "name": "Nsassoum", "latitude": 2.5, "longitude": 11.11667, "x": 16.4, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Nselang", "latitude": 2.46667, "longitude": 11.38333, "x": 67.6, "y": 45.6, "source": "geonames", "type": "PPL" }, { "name": "Nyang", "latitude": 2.51667, "longitude": 11.31667, "x": 54.8, "y": 32.2, "source": "geonames", "type": "PPL" }, { "name": "Oveng", "latitude": 2.43333, "longitude": 11.21667, "x": 35.6, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "Tho", "latitude": 2.31667, "longitude": 11.28333, "x": 48.4, "y": 85.6, "source": "geonames", "type": "PPL" }, { "name": "Toh", "latitude": 2.45, "longitude": 11.31667, "x": 54.8, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Yam", "latitude": 2.35, "longitude": 11.26667, "x": 45.2, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Zaminkan", "latitude": 2.35, "longitude": 11.25, "x": 42, "y": 76.7, "source": "geonames", "type": "PPL" } ], "Ayos": [ { "name": "Akak", "latitude": 4.15, "longitude": 11.26667, "x": 20, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Akok", "latitude": 3.95, "longitude": 11.25, "x": 10, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Bikogo", "latitude": 4.15, "longitude": 11.31667, "x": 50, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Bisogo", "latitude": 4.1, "longitude": 11.28333, "x": 30, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Ebabsi", "latitude": 4.1, "longitude": 11.3, "x": 40, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Ebanga", "latitude": 4.11667, "longitude": 11.33333, "x": 60, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Ebougsi", "latitude": 4.11667, "longitude": 11.3, "x": 40, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Ekol", "latitude": 4.1, "longitude": 11.28333, "x": 30, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Elig", "latitude": 4.15, "longitude": 11.36667, "x": 80, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Elotkos", "latitude": 4.13333, "longitude": 11.36667, "x": 80, "y": 35, "source": "geonames", "type": "PPL" }, { "name": "Eviane", "latitude": 4.05, "longitude": 11.3, "x": 40, "y": 60, "source": "geonames", "type": "PPL" }, { "name": "Kokodo", "latitude": 4.2, "longitude": 11.3, "x": 40, "y": 15, "source": "geonames", "type": "PPL" }, { "name": "Konabing", "latitude": 4.1, "longitude": 11.33333, "x": 60, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Lekie", "latitude": 4.2, "longitude": 11.36667, "x": 80, "y": 15, "source": "geonames", "type": "PPL" }, { "name": "Mbelekie", "latitude": 4.1, "longitude": 11.36667, "x": 80, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Mebomo", "latitude": 4.16667, "longitude": 11.31667, "x": 50, "y": 25, "source": "geonames", "type": "PPL" }, { "name": "Mfomo", "latitude": 4.16667, "longitude": 11.38333, "x": 90, "y": 25, "source": "geonames", "type": "PPL" }, { "name": "Minso I", "latitude": 4.08333, "longitude": 11.26667, "x": 20, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Minso II", "latitude": 4.03333, "longitude": 11.28333, "x": 30, "y": 65, "source": "geonames", "type": "PPL" }, { "name": "Mva", "latitude": 4.06667, "longitude": 11.31667, "x": 50, "y": 55, "source": "geonames", "type": "PPL" }, { "name": "Ndanging", "latitude": 4.1, "longitude": 11.33333, "x": 60, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Niga", "latitude": 4.2, "longitude": 11.33333, "x": 60, "y": 15, "source": "geonames", "type": "PPL" }, { "name": "Nkinge", "latitude": 4.15, "longitude": 11.3, "x": 40, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Nkolmba", "latitude": 4.16667, "longitude": 11.3, "x": 40, "y": 25, "source": "geonames", "type": "PPL" }, { "name": "Nkolmelen", "latitude": 3.96667, "longitude": 11.26667, "x": 20, "y": 85, "source": "geonames", "type": "PPL" }, { "name": "Nkolobang I", "latitude": 4.15, "longitude": 11.38333, "x": 90, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Nkolobang II", "latitude": 4.18333, "longitude": 11.36667, "x": 80, "y": 20, "source": "geonames", "type": "PPL" }, { "name": "Nkolve", "latitude": 4.21667, "longitude": 11.36667, "x": 80, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Nyemeyong", "latitude": 4.05, "longitude": 11.25, "x": 10, "y": 60, "source": "geonames", "type": "PPL" }, { "name": "Okok", "latitude": 4.13333, "longitude": 11.25, "x": 10, "y": 35, "source": "geonames", "type": "PPL" }, { "name": "Pongsolo", "latitude": 4.11667, "longitude": 11.28333, "x": 30, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Song Onana", "latitude": 4.06667, "longitude": 11.31667, "x": 50, "y": 55, "source": "geonames", "type": "PPL" } ], "Bafang": [ { "name": "Baboate", "latitude": 5.2044, "longitude": 10.18107, "x": 39.5, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Baboutcha-Nitcheu", "latitude": 5.1606, "longitude": 10.16514, "x": 33.8, "y": 46.5, "source": "geonames", "type": "PPL" }, { "name": "Babwantou", "latitude": 5.21412, "longitude": 10.29275, "x": 79.6, "y": 34.5, "source": "geonames", "type": "PPL" }, { "name": "Badoumka", "latitude": 5.19727, "longitude": 10.15585, "x": 30.4, "y": 38.3, "source": "geonames", "type": "PPL" }, { "name": "Bagouaka", "latitude": 5.00568, "longitude": 10.13938, "x": 24.5, "y": 81.3, "source": "geonames", "type": "PPL" }, { "name": "Baka'sa", "latitude": 5.12548, "longitude": 10.23781, "x": 59.8, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "Bakondji", "latitude": 5.10812, "longitude": 10.13209, "x": 21.9, "y": 58.3, "source": "geonames", "type": "PPL" }, { "name": "Bakouini", "latitude": 5.00675, "longitude": 10.16253, "x": 32.8, "y": 81, "source": "geonames", "type": "PPL" }, { "name": "Balaloum", "latitude": 5.06861, "longitude": 10.1343, "x": 22.7, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Balam", "latitude": 5.00728, "longitude": 10.28443, "x": 76.6, "y": 80.9, "source": "geonames", "type": "PPL" }, { "name": "Balouk-Makala", "latitude": 5.00709, "longitude": 10.14446, "x": 26.4, "y": 80.9, "source": "geonames", "type": "PPL" }, { "name": "Bamedjo", "latitude": 5.29766, "longitude": 10.19548, "x": 44.7, "y": 15.8, "source": "geonames", "type": "PPL" }, { "name": "Bameleu", "latitude": 5.06873, "longitude": 10.10254, "x": 11.3, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Bamfelouk", "latitude": 5.2041, "longitude": 10.1807, "x": 39.4, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Bana", "latitude": 5.14655, "longitude": 10.27545, "x": 73.3, "y": 49.7, "source": "geonames", "type": "PPL" }, { "name": "Bandoumdja", "latitude": 5.23426, "longitude": 10.26423, "x": 69.3, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Bandoumkassa", "latitude": 5.12408, "longitude": 10.27238, "x": 72.2, "y": 54.7, "source": "geonames", "type": "PPL" }, { "name": "Banfeko", "latitude": 5.15525, "longitude": 10.224, "x": 54.9, "y": 47.7, "source": "geonames", "type": "PPL" }, { "name": "Bangombe", "latitude": 5.03603, "longitude": 10.23393, "x": 58.5, "y": 74.4, "source": "geonames", "type": "PPL" }, { "name": "Banka", "latitude": 5.16298, "longitude": 10.20844, "x": 49.3, "y": 46, "source": "geonames", "type": "PPL" }, { "name": "Bankambe", "latitude": 5.0407, "longitude": 10.21107, "x": 50.3, "y": 73.4, "source": "geonames", "type": "PPL" }, { "name": "Bankwop", "latitude": 5.12384, "longitude": 10.19648, "x": 45, "y": 54.7, "source": "geonames", "type": "PPL" }, { "name": "Bapou", "latitude": 5.16155, "longitude": 10.30452, "x": 83.8, "y": 46.3, "source": "geonames", "type": "PPL" }, { "name": "Bassa", "latitude": 5.13142, "longitude": 10.2141, "x": 51.3, "y": 53, "source": "geonames", "type": "PPL" }, { "name": "Basseu", "latitude": 5.21882, "longitude": 10.21956, "x": 53.3, "y": 33.4, "source": "geonames", "type": "PPL" }, { "name": "Batcha", "latitude": 5.20141, "longitude": 10.23251, "x": 57.9, "y": 37.4, "source": "geonames", "type": "PPL" }, { "name": "Bone", "latitude": 5.13224, "longitude": 10.17532, "x": 37.4, "y": 52.9, "source": "geonames", "type": "PPL" }, { "name": "Boutcha", "latitude": 5.17536, "longitude": 10.14709, "x": 27.3, "y": 43.2, "source": "geonames", "type": "PPL" }, { "name": "Boutcheu", "latitude": 5.11391, "longitude": 10.16736, "x": 34.6, "y": 57, "source": "geonames", "type": "PPL" }, { "name": "Chichie", "latitude": 5.27725, "longitude": 10.18976, "x": 42.6, "y": 20.3, "source": "geonames", "type": "PPL" }, { "name": "Company", "latitude": 5.26192, "longitude": 10.22689, "x": 55.9, "y": 23.8, "source": "geonames", "type": "PPL" }, { "name": "Denla'", "latitude": 5.31516, "longitude": 10.18276, "x": 40.1, "y": 11.8, "source": "geonames", "type": "PPL" }, { "name": "Djongo", "latitude": 5.00379, "longitude": 10.32188, "x": 90, "y": 81.7, "source": "geonames", "type": "PPL" }, { "name": "Famndou", "latitude": 5.19777, "longitude": 10.22226, "x": 54.3, "y": 38.2, "source": "geonames", "type": "PPL" }, { "name": "Famyalou", "latitude": 5.17972, "longitude": 10.27587, "x": 73.5, "y": 42.2, "source": "geonames", "type": "PPL" }, { "name": "Fokwanke", "latitude": 5.15478, "longitude": 10.1075, "x": 13.1, "y": 47.8, "source": "geonames", "type": "PPL" }, { "name": "Folentcha", "latitude": 5.21142, "longitude": 10.16897, "x": 35.2, "y": 35.1, "source": "geonames", "type": "PPL" }, { "name": "Fombele", "latitude": 5.13262, "longitude": 10.11166, "x": 14.6, "y": 52.8, "source": "geonames", "type": "PPL" }, { "name": "Fomessa II", "latitude": 5.1048, "longitude": 10.10806, "x": 13.3, "y": 59, "source": "geonames", "type": "PPL" }, { "name": "Fomke", "latitude": 5.14947, "longitude": 10.1338, "x": 22.5, "y": 49, "source": "geonames", "type": "PPL" }, { "name": "Fondanti", "latitude": 5.25428, "longitude": 10.15214, "x": 29.1, "y": 25.5, "source": "geonames", "type": "PPL" }, { "name": "Fondjanti", "latitude": 5.06315, "longitude": 10.20579, "x": 48.4, "y": 68.4, "source": "geonames", "type": "PPL" }, { "name": "Fondjomekwet", "latitude": 5.27987, "longitude": 10.1641, "x": 33.4, "y": 19.8, "source": "geonames", "type": "PPL" }, { "name": "Fongoli", "latitude": 5.18999, "longitude": 10.14632, "x": 27, "y": 39.9, "source": "geonames", "type": "PPL" }, { "name": "Fonkouankem", "latitude": 5.16554, "longitude": 10.09884, "x": 10, "y": 45.4, "source": "geonames", "type": "PPL" }, { "name": "Fopwanga", "latitude": 5.03368, "longitude": 10.20157, "x": 46.8, "y": 75, "source": "geonames", "type": "PPL" }, { "name": "Fostinga", "latitude": 5.06583, "longitude": 10.15899, "x": 31.6, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Fotomena", "latitude": 5.21667, "longitude": 10.1, "x": 10.4, "y": 33.9, "source": "geonames", "type": "PPL" }, { "name": "Fotsi", "latitude": 5.11846, "longitude": 10.10793, "x": 13.3, "y": 56, "source": "geonames", "type": "PPL" }, { "name": "Foyave", "latitude": 5.11248, "longitude": 10.10021, "x": 10.5, "y": 57.3, "source": "geonames", "type": "PPL" }, { "name": "Koba", "latitude": 5.04234, "longitude": 10.16504, "x": 33.7, "y": 73, "source": "geonames", "type": "PPL" }, { "name": "Komako", "latitude": 5.04241, "longitude": 10.13887, "x": 24.4, "y": 73, "source": "geonames", "type": "PPL" }, { "name": "Kotcha", "latitude": 5.29287, "longitude": 10.16734, "x": 34.6, "y": 16.8, "source": "geonames", "type": "PPL" }, { "name": "Koyen", "latitude": 5.24112, "longitude": 10.20406, "x": 47.7, "y": 28.4, "source": "geonames", "type": "PPL" }, { "name": "Lafi", "latitude": 5.30353, "longitude": 10.20605, "x": 48.5, "y": 14.4, "source": "geonames", "type": "PPL" }, { "name": "Lakotchala", "latitude": 5.23929, "longitude": 10.22037, "x": 53.6, "y": 28.9, "source": "geonames", "type": "PPL" }, { "name": "Lembo", "latitude": 5.17715, "longitude": 10.15465, "x": 30, "y": 42.8, "source": "geonames", "type": "PPL" }, { "name": "Mamfabom", "latitude": 5.19207, "longitude": 10.1021, "x": 11.2, "y": 39.4, "source": "geonames", "type": "PPL" }, { "name": "Mangombi", "latitude": 5.00696, "longitude": 10.24202, "x": 61.4, "y": 81, "source": "geonames", "type": "PPL" }, { "name": "Mayo", "latitude": 5.09024, "longitude": 10.26938, "x": 71.2, "y": 62.3, "source": "geonames", "type": "PPL" }, { "name": "Mboebo", "latitude": 5.23341, "longitude": 10.10627, "x": 12.7, "y": 30.2, "source": "geonames", "type": "PPL" }, { "name": "Mboma", "latitude": 5.00463, "longitude": 10.16464, "x": 33.6, "y": 81.5, "source": "geonames", "type": "PPL" }, { "name": "Mendjo", "latitude": 5.32335, "longitude": 10.18975, "x": 42.6, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Menghe", "latitude": 5.30903, "longitude": 10.13867, "x": 24.3, "y": 13.2, "source": "geonames", "type": "PPL" }, { "name": "Mwanke", "latitude": 5.18645, "longitude": 10.11034, "x": 14.1, "y": 40.7, "source": "geonames", "type": "PPL" }, { "name": "Ndemtchang", "latitude": 5.27447, "longitude": 10.16178, "x": 32.6, "y": 21, "source": "geonames", "type": "PPL" }, { "name": "Ndemveng", "latitude": 5.2706, "longitude": 10.19099, "x": 43.1, "y": 21.8, "source": "geonames", "type": "PPL" }, { "name": "Ndokolo", "latitude": 5.04803, "longitude": 10.22939, "x": 56.8, "y": 71.8, "source": "geonames", "type": "PPL" }, { "name": "Ndonko", "latitude": 5.19468, "longitude": 10.21772, "x": 52.6, "y": 38.9, "source": "geonames", "type": "PPL" }, { "name": "Ndoto", "latitude": 5, "longitude": 10.28333, "x": 76.2, "y": 82.5, "source": "geonames", "type": "PPL" }, { "name": "Ndoto III", "latitude": 5.00464, "longitude": 10.30914, "x": 85.4, "y": 81.5, "source": "geonames", "type": "PPL" }, { "name": "Ndotoron", "latitude": 5.01806, "longitude": 10.24742, "x": 63.3, "y": 78.5, "source": "geonames", "type": "PPL" }, { "name": "Ndoumve", "latitude": 5.19318, "longitude": 10.24607, "x": 62.8, "y": 39.2, "source": "geonames", "type": "PPL" }, { "name": "Ngaleu", "latitude": 5.10484, "longitude": 10.17305, "x": 36.6, "y": 59, "source": "geonames", "type": "PPL" }, { "name": "Nitcheu", "latitude": 5.16469, "longitude": 10.14556, "x": 26.8, "y": 45.6, "source": "geonames", "type": "PPL" }, { "name": "Nka", "latitude": 5.17635, "longitude": 10.21837, "x": 52.9, "y": 43, "source": "geonames", "type": "PPL" }, { "name": "Nkwop", "latitude": 5.29602, "longitude": 10.22082, "x": 53.8, "y": 16.1, "source": "geonames", "type": "PPL" }, { "name": "Nwa Fochie", "latitude": 5.10573, "longitude": 10.11462, "x": 15.7, "y": 58.8, "source": "geonames", "type": "PPL" }, { "name": "Papouate", "latitude": 5.02114, "longitude": 10.11176, "x": 14.6, "y": 77.8, "source": "geonames", "type": "PPL" }, { "name": "Petit Diboum", "latitude": 5.07797, "longitude": 10.18128, "x": 39.6, "y": 65, "source": "geonames", "type": "PPL" }, { "name": "Sela", "latitude": 5.13219, "longitude": 10.13891, "x": 24.4, "y": 52.9, "source": "geonames", "type": "PPL" }, { "name": "Sohok", "latitude": 4.96667, "longitude": 10.25, "x": 64.2, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Song", "latitude": 5.27673, "longitude": 10.217, "x": 52.4, "y": 20.5, "source": "geonames", "type": "PPL" }, { "name": "Tcha", "latitude": 5.19788, "longitude": 10.25346, "x": 65.5, "y": 38.1, "source": "geonames", "type": "PPL" }, { "name": "Tchitcho'", "latitude": 5.26599, "longitude": 10.15709, "x": 30.9, "y": 22.9, "source": "geonames", "type": "PPL" }, { "name": "Tchougou", "latitude": 5.22222, "longitude": 10.1425, "x": 25.7, "y": 32.7, "source": "geonames", "type": "PPL" }, { "name": "Toula'", "latitude": 5.27758, "longitude": 10.15214, "x": 29.1, "y": 20.3, "source": "geonames", "type": "PPL" }, { "name": "Tso", "latitude": 5.22412, "longitude": 10.23692, "x": 59.5, "y": 32.3, "source": "geonames", "type": "PPL" }, { "name": "Yotieu", "latitude": 5.19418, "longitude": 10.22322, "x": 54.6, "y": 39, "source": "geonames", "type": "PPL" }, { "name": "Youte", "latitude": 5.18958, "longitude": 10.21521, "x": 51.7, "y": 40, "source": "geonames", "type": "PPL" } ], "Bafia": [ { "name": "Asala I", "latitude": 4.61667, "longitude": 11.18333, "x": 46, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Asala II", "latitude": 4.61667, "longitude": 11.23333, "x": 58, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Badisa", "latitude": 4.63333, "longitude": 11.36667, "x": 90, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Bakoa", "latitude": 4.56667, "longitude": 11.16667, "x": 42, "y": 82.4, "source": "geonames", "type": "PPL" }, { "name": "Baliama", "latitude": 4.6, "longitude": 11.31667, "x": 78, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Bamoko", "latitude": 4.86667, "longitude": 11.08333, "x": 22, "y": 13.8, "source": "geonames", "type": "PPL" }, { "name": "Baningoang", "latitude": 4.63333, "longitude": 11.28333, "x": 70, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Bayomen", "latitude": 4.86667, "longitude": 11.1, "x": 26, "y": 13.8, "source": "geonames", "type": "PPL" }, { "name": "Begni", "latitude": 4.76667, "longitude": 11.1, "x": 26, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Bep", "latitude": 4.65, "longitude": 11.16667, "x": 42, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Biamese", "latitude": 4.68333, "longitude": 11.11667, "x": 30, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Biamo", "latitude": 4.76667, "longitude": 11.21667, "x": 54, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Bigna", "latitude": 4.71667, "longitude": 11.23333, "x": 58, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Bilomo", "latitude": 4.6, "longitude": 11.36667, "x": 90, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Bitang", "latitude": 4.65, "longitude": 11.2, "x": 50, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Bogondo", "latitude": 4.6, "longitude": 11.3, "x": 74, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Bokaga", "latitude": 4.58333, "longitude": 11.15, "x": 38, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Bokito", "latitude": 4.56667, "longitude": 11.11667, "x": 30, "y": 82.4, "source": "geonames", "type": "PPL" }, { "name": "Boura I", "latitude": 4.68333, "longitude": 11.33333, "x": 82, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Boura II", "latitude": 4.65, "longitude": 11.35, "x": 86, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Bouraka", "latitude": 4.68333, "longitude": 11.3, "x": 74, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Boyaba", "latitude": 4.63333, "longitude": 11.26667, "x": 66, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Boyabisoumbi", "latitude": 4.61667, "longitude": 11.31667, "x": 78, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Contsing II", "latitude": 4.61667, "longitude": 11.28333, "x": 70, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Dang", "latitude": 4.75, "longitude": 11.23333, "x": 58, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Dink", "latitude": 4.81667, "longitude": 11.15, "x": 38, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Dodiare", "latitude": 4.8, "longitude": 11.16667, "x": 42, "y": 29, "source": "geonames", "type": "PPL" }, { "name": "Doninking", "latitude": 4.73333, "longitude": 11.28333, "x": 70, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Egona", "latitude": 4.71667, "longitude": 11.3, "x": 74, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Enangana", "latitude": 4.58333, "longitude": 11.36667, "x": 90, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Esende", "latitude": 4.63333, "longitude": 11.25, "x": 62, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Gah", "latitude": 4.81667, "longitude": 11.15, "x": 38, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Gbaram", "latitude": 4.73333, "longitude": 11.15, "x": 38, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Geboda", "latitude": 4.58333, "longitude": 11.21667, "x": 54, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Gefige", "latitude": 4.55, "longitude": 11.23333, "x": 58, "y": 86.2, "source": "geonames", "type": "PPL" }, { "name": "Gentsing I", "latitude": 4.65, "longitude": 11.35, "x": 86, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Goufan I", "latitude": 4.66667, "longitude": 11.25, "x": 62, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Goufan II", "latitude": 4.7, "longitude": 11.25, "x": 62, "y": 51.9, "source": "geonames", "type": "PPL" }, { "name": "Goufe", "latitude": 4.83333, "longitude": 11.21667, "x": 54, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Gouife", "latitude": 4.63333, "longitude": 11.15, "x": 38, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Kalong", "latitude": 4.85, "longitude": 11.11667, "x": 30, "y": 17.6, "source": "geonames", "type": "PPL" }, { "name": "Ken", "latitude": 4.83333, "longitude": 11.15, "x": 38, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Kiiki", "latitude": 4.66667, "longitude": 11.18333, "x": 46, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Kombe", "latitude": 4.66667, "longitude": 11.3, "x": 74, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Kon", "latitude": 4.83333, "longitude": 11.06667, "x": 18, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Kon Kidoum", "latitude": 4.71667, "longitude": 11.06667, "x": 18, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Lapouang", "latitude": 4.78333, "longitude": 11.18333, "x": 46, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Mouken", "latitude": 4.75, "longitude": 11.15, "x": 38, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Mouko", "latitude": 4.68333, "longitude": 11.2, "x": 50, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Mouzi", "latitude": 4.88333, "longitude": 11.2, "x": 50, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Nging", "latitude": 4.85, "longitude": 11.21667, "x": 54, "y": 17.6, "source": "geonames", "type": "PPL" }, { "name": "Niamanga II", "latitude": 4.58333, "longitude": 11.31667, "x": 78, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Niambala", "latitude": 4.56667, "longitude": 11.31667, "x": 78, "y": 82.4, "source": "geonames", "type": "PPL" }, { "name": "Niatchotta", "latitude": 4.76667, "longitude": 11.28333, "x": 70, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Nyambay", "latitude": 4.73333, "longitude": 11.03333, "x": 10, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Nyamongo", "latitude": 4.8, "longitude": 11.3, "x": 74, "y": 29, "source": "geonames", "type": "PPL" }, { "name": "Nyamsong", "latitude": 4.75, "longitude": 11.26667, "x": 66, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Ombesa", "latitude": 4.6, "longitude": 11.25, "x": 62, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Oming", "latitude": 4.7, "longitude": 11.08333, "x": 22, "y": 51.9, "source": "geonames", "type": "PPL" }, { "name": "Ponek", "latitude": 4.81667, "longitude": 11.03333, "x": 10, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Ribang", "latitude": 4.7, "longitude": 11.15, "x": 38, "y": 51.9, "source": "geonames", "type": "PPL" }, { "name": "Rionong", "latitude": 4.71667, "longitude": 11.2, "x": 50, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Roum", "latitude": 4.73333, "longitude": 11.13333, "x": 34, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Tchekane", "latitude": 4.7, "longitude": 11.3, "x": 74, "y": 51.9, "source": "geonames", "type": "PPL" }, { "name": "Tchekos", "latitude": 4.6, "longitude": 11.08333, "x": 22, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Yakan", "latitude": 4.71667, "longitude": 11.16667, "x": 42, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Yambasa", "latitude": 4.53333, "longitude": 11.25, "x": 62, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Yambeta", "latitude": 4.85, "longitude": 11.03333, "x": 10, "y": 17.6, "source": "geonames", "type": "PPL" }, { "name": "Zok", "latitude": 4.86667, "longitude": 11.25, "x": 62, "y": 13.8, "source": "geonames", "type": "PPL" } ], "Bafoussam": [ { "name": "Badienton", "latitude": 5.5519135, "longitude": 10.3681438, "x": 29.1, "y": 15.5, "source": "osm", "type": "locality" }, { "name": "Bakeup", "latitude": 5.5532215, "longitude": 10.4137895, "x": 50.7, "y": 14.8, "source": "osm", "type": "locality" }, { "name": "Balaafi", "latitude": 5.5224778, "longitude": 10.4023915, "x": 45.3, "y": 32.4, "source": "osm", "type": "locality" }, { "name": "Balaangwen", "latitude": 5.55388, "longitude": 10.37721, "x": 33.4, "y": 14.4, "source": "geonames", "type": "PPL" }, { "name": "Balaatchuet", "latitude": 5.56136, "longitude": 10.3843, "x": 36.7, "y": 10.1, "source": "geonames", "type": "PPL" }, { "name": "Baleng", "latitude": 5.5173095, "longitude": 10.410089, "x": 48.9, "y": 35.3, "source": "osm", "type": "town" }, { "name": "Bamesseng", "latitude": 5.4245504, "longitude": 10.3629738, "x": 26.6, "y": 88.4, "source": "osm", "type": "village" }, { "name": "Bametou", "latitude": 5.5384378, "longitude": 10.3731327, "x": 31.4, "y": 23.2, "source": "osm", "type": "locality" }, { "name": "Bamougoum", "latitude": 5.50809, "longitude": 10.35625, "x": 23.4, "y": 40.6, "source": "geonames", "type": "PPL" }, { "name": "Banefo", "latitude": 5.4867, "longitude": 10.49396, "x": 88.7, "y": 52.8, "source": "geonames", "type": "PPL" }, { "name": "Banengo", "latitude": 5.4643101, "longitude": 10.4167828, "x": 52.1, "y": 65.6, "source": "osm", "type": "suburb" }, { "name": "Bapi", "latitude": 5.5584393, "longitude": 10.373726, "x": 31.7, "y": 11.8, "source": "osm", "type": "village" }, { "name": "Bassinte", "latitude": 5.518933, "longitude": 10.4523249, "x": 69, "y": 34.4, "source": "osm", "type": "village" }, { "name": "Batcheu", "latitude": 5.51932, "longitude": 10.38247, "x": 35.9, "y": 34.2, "source": "geonames", "type": "PPL" }, { "name": "Batoukop", "latitude": 5.4730836, "longitude": 10.4766602, "x": 80.5, "y": 60.6, "source": "osm", "type": "village" }, { "name": "Baye", "latitude": 5.5321, "longitude": 10.48781, "x": 85.8, "y": 26.9, "source": "geonames", "type": "PPL" }, { "name": "Chechet", "latitude": 5.46719, "longitude": 10.34492, "x": 18.1, "y": 64, "source": "geonames", "type": "PPL" }, { "name": "Choke", "latitude": 5.4445866, "longitude": 10.4518639, "x": 68.7, "y": 76.9, "source": "osm", "type": "locality" }, { "name": "Demsiem", "latitude": 5.4635366, "longitude": 10.4685759, "x": 76.7, "y": 66.1, "source": "osm", "type": "village" }, { "name": "Dendeu", "latitude": 5.48457, "longitude": 10.36366, "x": 26.9, "y": 54, "source": "geonames", "type": "PPL" }, { "name": "Doumlong", "latitude": 5.50037, "longitude": 10.35484, "x": 22.8, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Famkouo", "latitude": 5.50653, "longitude": 10.33346, "x": 12.6, "y": 41.5, "source": "geonames", "type": "PPL" }, { "name": "Famla", "latitude": 5.477734, "longitude": 10.4194794, "x": 53.4, "y": 57.9, "source": "osm", "type": "neighbourhood" }, { "name": "Famleng", "latitude": 5.4889371, "longitude": 10.4701708, "x": 77.4, "y": 51.5, "source": "osm", "type": "village" }, { "name": "Famnja", "latitude": 5.4997, "longitude": 10.47952, "x": 81.8, "y": 45.4, "source": "geonames", "type": "PPL" }, { "name": "Fampie", "latitude": 5.53856, "longitude": 10.42522, "x": 56.1, "y": 23.2, "source": "geonames", "type": "PPL" }, { "name": "Famtchuet", "latitude": 5.50604, "longitude": 10.48875, "x": 86.2, "y": 41.8, "source": "geonames", "type": "PPL" }, { "name": "Fantja", "latitude": 5.4991677, "longitude": 10.4787106, "x": 81.5, "y": 45.7, "source": "osm", "type": "village" }, { "name": "Fombe", "latitude": 5.4672, "longitude": 10.3279, "x": 10, "y": 64, "source": "geonames", "type": "PPL" }, { "name": "Fou'sap", "latitude": 5.472346, "longitude": 10.4593937, "x": 72.3, "y": 61, "source": "osm", "type": "village" }, { "name": "Ghe", "latitude": 5.48191, "longitude": 10.32864, "x": 10.4, "y": 55.6, "source": "geonames", "type": "PPL" }, { "name": "Gouache", "latitude": 5.4880384, "longitude": 10.3937889, "x": 41.2, "y": 52, "source": "osm", "type": "neighbourhood" }, { "name": "Kamkop I", "latitude": 5.5096191, "longitude": 10.3793874, "x": 34.4, "y": 39.7, "source": "osm", "type": "locality" }, { "name": "Kena", "latitude": 5.51379, "longitude": 10.36684, "x": 28.5, "y": 37.3, "source": "geonames", "type": "PPL" }, { "name": "Keuleu", "latitude": 5.5301027, "longitude": 10.3853607, "x": 37.2, "y": 28, "source": "osm", "type": "locality" }, { "name": "King-Place", "latitude": 5.51693, "longitude": 10.41616, "x": 51.8, "y": 35.5, "source": "geonames", "type": "PPL" }, { "name": "Kong Njong", "latitude": 5.45395, "longitude": 10.33399, "x": 12.9, "y": 71.5, "source": "geonames", "type": "PPL" }, { "name": "Kong So", "latitude": 5.49151, "longitude": 10.34546, "x": 18.3, "y": 50.1, "source": "geonames", "type": "PPL" }, { "name": "Kong-Meke", "latitude": 5.4363363, "longitude": 10.4712545, "x": 77.9, "y": 81.6, "source": "osm", "type": "locality" }, { "name": "Kong-Nkeng", "latitude": 5.4469486, "longitude": 10.4756123, "x": 80, "y": 75.5, "source": "osm", "type": "locality" }, { "name": "Konti", "latitude": 5.55515, "longitude": 10.3910091, "x": 39.9, "y": 13.7, "source": "osm", "type": "village" }, { "name": "Koptchou", "latitude": 5.48809, "longitude": 10.4422, "x": 64.2, "y": 52, "source": "geonames", "type": "PPL" }, { "name": "Koung", "latitude": 5.4269738, "longitude": 10.3906156, "x": 39.7, "y": 87, "source": "osm", "type": "locality" }, { "name": "Kouogouo", "latitude": 5.4844707, "longitude": 10.4037848, "x": 46, "y": 54.1, "source": "osm", "type": "neighbourhood" }, { "name": "Kwongouo", "latitude": 5.47086, "longitude": 10.40394, "x": 46, "y": 61.9, "source": "geonames", "type": "PPL" }, { "name": "La'fie", "latitude": 5.461208, "longitude": 10.3690123, "x": 29.5, "y": 67.4, "source": "osm", "type": "locality" }, { "name": "La'rsit", "latitude": 5.4362933, "longitude": 10.3740667, "x": 31.9, "y": 81.6, "source": "osm", "type": "locality" }, { "name": "La'tsit", "latitude": 5.498089, "longitude": 10.3656986, "x": 27.9, "y": 46.3, "source": "osm", "type": "village" }, { "name": "Laangwen", "latitude": 5.51787, "longitude": 10.43784, "x": 62.1, "y": 35, "source": "geonames", "type": "PPL" }, { "name": "Langoueng", "latitude": 5.5093125, "longitude": 10.4404567, "x": 63.3, "y": 39.9, "source": "osm", "type": "village" }, { "name": "Latsit I", "latitude": 5.50659, "longitude": 10.34819, "x": 19.6, "y": 41.4, "source": "geonames", "type": "PPL" }, { "name": "Latsit-Djeutsa", "latitude": 5.51309, "longitude": 10.3418, "x": 16.6, "y": 37.7, "source": "geonames", "type": "PPL" }, { "name": "Latsit-Loumgou", "latitude": 5.52166, "longitude": 10.33919, "x": 15.4, "y": 32.8, "source": "geonames", "type": "PPL" }, { "name": "Lengwo", "latitude": 5.47507, "longitude": 10.45599, "x": 70.7, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Mbi", "latitude": 5.47694, "longitude": 10.34059, "x": 16, "y": 58.4, "source": "geonames", "type": "PPL" }, { "name": "Mbing", "latitude": 5.421666, "longitude": 10.4113849, "x": 49.6, "y": 90, "source": "osm", "type": "village" }, { "name": "Mbouo", "latitude": 5.4344718, "longitude": 10.4228591, "x": 55, "y": 82.7, "source": "osm", "type": "locality" }, { "name": "Mbwo", "latitude": 5.4331895, "longitude": 10.4479588, "x": 66.9, "y": 83.4, "source": "osm", "type": "village" }, { "name": "Mefam", "latitude": 5.53146, "longitude": 10.40426, "x": 46.2, "y": 27.2, "source": "geonames", "type": "PPL" }, { "name": "Mefe", "latitude": 5.4472572, "longitude": 10.3731729, "x": 31.5, "y": 75.4, "source": "osm", "type": "village" }, { "name": "Megom", "latitude": 5.442822, "longitude": 10.4143597, "x": 51, "y": 77.9, "source": "osm", "type": "village" }, { "name": "Menzi", "latitude": 5.47112, "longitude": 10.43449, "x": 60.5, "y": 61.7, "source": "geonames", "type": "PPL" }, { "name": "Ndem'long", "latitude": 5.44833, "longitude": 10.34362, "x": 17.4, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Ndembou Menjo", "latitude": 5.4452665, "longitude": 10.4622131, "x": 73.6, "y": 76.5, "source": "osm", "type": "village" }, { "name": "Ndenbou Menjo", "latitude": 5.4412, "longitude": 10.45806, "x": 71.7, "y": 78.8, "source": "geonames", "type": "PPL" }, { "name": "Ndenbou-Melam", "latitude": 5.4571127, "longitude": 10.4648089, "x": 74.9, "y": 69.7, "source": "osm", "type": "village" }, { "name": "Ndennda", "latitude": 5.45222, "longitude": 10.44339, "x": 64.7, "y": 72.5, "source": "geonames", "type": "PPL" }, { "name": "Ndenso", "latitude": 5.4448799, "longitude": 10.43483, "x": 60.7, "y": 76.7, "source": "osm", "type": "locality" }, { "name": "Ndiangdam", "latitude": 5.456754, "longitude": 10.4339932, "x": 60.3, "y": 69.9, "source": "osm", "type": "neighbourhood" }, { "name": "Ndionkou", "latitude": 5.5205, "longitude": 10.41476, "x": 51.2, "y": 33.5, "source": "geonames", "type": "PPL" }, { "name": "Ndjinga", "latitude": 5.473842, "longitude": 10.4799807, "x": 82.1, "y": 60.2, "source": "osm", "type": "village" }, { "name": "Ndoumdi", "latitude": 5.48517, "longitude": 10.37502, "x": 32.3, "y": 53.7, "source": "geonames", "type": "PPL" }, { "name": "Nefo", "latitude": 5.48928, "longitude": 10.49672, "x": 90, "y": 51.3, "source": "geonames", "type": "PPL" }, { "name": "Nefoloum", "latitude": 5.53028, "longitude": 10.44304, "x": 64.6, "y": 27.9, "source": "geonames", "type": "PPL" }, { "name": "Negou", "latitude": 5.46098, "longitude": 10.41452, "x": 51, "y": 67.5, "source": "geonames", "type": "PPL" }, { "name": "Ngonle", "latitude": 5.56157, "longitude": 10.42127, "x": 54.2, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Ngouo", "latitude": 5.49156, "longitude": 10.33874, "x": 15.1, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Ngwache", "latitude": 5.48329, "longitude": 10.39343, "x": 41.1, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Ngwang", "latitude": 5.4404259, "longitude": 10.3522323, "x": 21.5, "y": 79.3, "source": "osm", "type": "locality" }, { "name": "Njassa", "latitude": 5.4936293, "longitude": 10.4558466, "x": 70.6, "y": 48.8, "source": "osm", "type": "village" }, { "name": "Njimnefo", "latitude": 5.49071, "longitude": 10.38058, "x": 35, "y": 50.5, "source": "geonames", "type": "PPL" }, { "name": "Njinga", "latitude": 5.48327, "longitude": 10.47418, "x": 79.3, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Njinga I", "latitude": 5.4869961, "longitude": 10.4762589, "x": 80.3, "y": 52.6, "source": "osm", "type": "village" }, { "name": "Njingwang", "latitude": 5.43543, "longitude": 10.33872, "x": 15.1, "y": 82.1, "source": "geonames", "type": "PPL" }, { "name": "Njisse", "latitude": 5.47922, "longitude": 10.40514, "x": 46.6, "y": 57.1, "source": "geonames", "type": "PPL" }, { "name": "Njunang La'tsit", "latitude": 5.4346147, "longitude": 10.3918244, "x": 40.3, "y": 82.6, "source": "osm", "type": "locality" }, { "name": "Njunang Mete", "latitude": 5.4444948, "longitude": 10.3810541, "x": 35.2, "y": 76.9, "source": "osm", "type": "locality" }, { "name": "Nkiet", "latitude": 5.44408, "longitude": 10.34474, "x": 18, "y": 77.2, "source": "geonames", "type": "PPL" }, { "name": "Nkouke", "latitude": 5.44745, "longitude": 10.37275, "x": 31.3, "y": 75.3, "source": "geonames", "type": "PPL" }, { "name": "Nkouoke", "latitude": 5.4498309, "longitude": 10.374502, "x": 32.1, "y": 73.9, "source": "osm", "type": "locality" }, { "name": "Nkwabang", "latitude": 5.4711055, "longitude": 10.3615096, "x": 25.9, "y": 61.7, "source": "osm", "type": "village" }, { "name": "Nkwabang I", "latitude": 5.4777731, "longitude": 10.360924, "x": 25.6, "y": 57.9, "source": "osm", "type": "locality" }, { "name": "Oukaha", "latitude": 5.4730089, "longitude": 10.44208, "x": 64.1, "y": 60.6, "source": "osm", "type": "neighbourhood" }, { "name": "Peng", "latitude": 5.4295837, "longitude": 10.3519246, "x": 21.4, "y": 85.5, "source": "osm", "type": "locality" }, { "name": "Tchada", "latitude": 5.5591427, "longitude": 10.4195554, "x": 53.4, "y": 11.4, "source": "osm", "type": "village" }, { "name": "Tchada II", "latitude": 5.5516619, "longitude": 10.4341269, "x": 60.3, "y": 15.7, "source": "osm", "type": "village" }, { "name": "Tchanda I", "latitude": 5.55072, "longitude": 10.43645, "x": 61.4, "y": 16.2, "source": "geonames", "type": "PPL" }, { "name": "Tchibe", "latitude": 5.4733152, "longitude": 10.3521722, "x": 21.5, "y": 60.5, "source": "osm", "type": "locality" }, { "name": "Tchitchap", "latitude": 5.4882, "longitude": 10.42584, "x": 56.4, "y": 52, "source": "geonames", "type": "PPL" }, { "name": "Tcho", "latitude": 5.4728, "longitude": 10.49528, "x": 89.3, "y": 60.8, "source": "geonames", "type": "PPL" }, { "name": "Tchouo", "latitude": 5.4627268, "longitude": 10.3840234, "x": 36.6, "y": 66.5, "source": "osm", "type": "village" }, { "name": "Tchoutchou", "latitude": 5.43074, "longitude": 10.33988, "x": 15.7, "y": 84.8, "source": "geonames", "type": "PPL" }, { "name": "Tchouwong", "latitude": 5.4558792, "longitude": 10.3961282, "x": 42.3, "y": 70.4, "source": "osm", "type": "village" }, { "name": "Tesse", "latitude": 5.4352994, "longitude": 10.4593016, "x": 72.3, "y": 82.2, "source": "osm", "type": "locality" }, { "name": "To' mlem", "latitude": 5.4233634, "longitude": 10.4361118, "x": 61.3, "y": 89, "source": "osm", "type": "locality" }, { "name": "Tobang", "latitude": 5.4530744, "longitude": 10.3874436, "x": 38.2, "y": 72, "source": "osm", "type": "village" }, { "name": "Toket", "latitude": 5.4676418, "longitude": 10.3964678, "x": 42.5, "y": 63.7, "source": "osm", "type": "neighbourhood" }, { "name": "Toloum", "latitude": 5.48391, "longitude": 10.34541, "x": 18.3, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "Topou", "latitude": 5.4370736, "longitude": 10.3651762, "x": 27.7, "y": 81.2, "source": "osm", "type": "locality" }, { "name": "Tossessong", "latitude": 5.49595, "longitude": 10.37739, "x": 33.5, "y": 47.5, "source": "geonames", "type": "PPL" }, { "name": "Toukop", "latitude": 5.4679, "longitude": 10.47001, "x": 77.3, "y": 63.6, "source": "geonames", "type": "PPL" }, { "name": "Toungang", "latitude": 5.49209, "longitude": 10.4183, "x": 52.8, "y": 49.7, "source": "geonames", "type": "PPL" }, { "name": "Toungang II", "latitude": 5.4931631, "longitude": 10.4466267, "x": 66.3, "y": 49.1, "source": "osm", "type": "village" }, { "name": "Tsewong", "latitude": 5.4467761, "longitude": 10.4002314, "x": 44.3, "y": 75.6, "source": "osm", "type": "village" }, { "name": "Tyo", "latitude": 5.49404, "longitude": 10.40042, "x": 44.4, "y": 48.6, "source": "geonames", "type": "PPL" }, { "name": "Tyo-Laangwen", "latitude": 5.50951, "longitude": 10.43944, "x": 62.9, "y": 39.8, "source": "geonames", "type": "PPL" }, { "name": "Tyo-Village", "latitude": 5.50101, "longitude": 10.41184, "x": 49.8, "y": 44.6, "source": "geonames", "type": "PPL" }, { "name": "Wounong III", "latitude": 5.5483685, "longitude": 10.3596589, "x": 25, "y": 17.5, "source": "osm", "type": "locality" }, { "name": "Wouong I", "latitude": 5.5313, "longitude": 10.34432, "x": 17.8, "y": 27.3, "source": "geonames", "type": "PPL" }, { "name": "Wouong II", "latitude": 5.53104, "longitude": 10.36223, "x": 26.3, "y": 27.5, "source": "geonames", "type": "PPL" } ], "Bafut": [ { "name": "Afom Baba", "latitude": 6.13333, "longitude": 10.03333, "x": 10, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Baka", "latitude": 6.13333, "longitude": 10.13333, "x": 90, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Mankwi", "latitude": 6.0851, "longitude": 10.0816, "x": 48.6, "y": 89.1, "source": "geonames", "type": "PPL" }, { "name": "Mbinkas", "latitude": 6.23333, "longitude": 10.11667, "x": 76.7, "y": 10, "source": "geonames", "type": "PPL" } ], "Baham": [ { "name": "Baho", "latitude": 5.30163, "longitude": 10.38761, "x": 64.6, "y": 51.1, "source": "geonames", "type": "PPL" }, { "name": "Balambo", "latitude": 5.17587, "longitude": 10.35123, "x": 53.7, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Baloumngou", "latitude": 5.21749, "longitude": 10.42523, "x": 76, "y": 77.1, "source": "geonames", "type": "PPL" }, { "name": "Bamendjou", "latitude": 5.38988, "longitude": 10.33014, "x": 47.3, "y": 23.7, "source": "geonames", "type": "PPL" }, { "name": "Bangou", "latitude": 5.24998, "longitude": 10.39563, "x": 67.1, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Batie", "latitude": 5.3148, "longitude": 10.32462, "x": 45.7, "y": 47, "source": "geonames", "type": "PPL" }, { "name": "Baving", "latitude": 5.35299, "longitude": 10.23647, "x": 19.2, "y": 35.2, "source": "geonames", "type": "PPL" }, { "name": "Bayangam", "latitude": 5.29341, "longitude": 10.45406, "x": 84.6, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Beng", "latitude": 5.29439, "longitude": 10.44189, "x": 81, "y": 53.3, "source": "geonames", "type": "PPL" }, { "name": "Bete", "latitude": 5.20535, "longitude": 10.34506, "x": 51.8, "y": 80.9, "source": "geonames", "type": "PPL" }, { "name": "Bong", "latitude": 5.32357, "longitude": 10.20604, "x": 10, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Chefou", "latitude": 5.32508, "longitude": 10.41378, "x": 72.5, "y": 43.8, "source": "geonames", "type": "PPL" }, { "name": "Chenye", "latitude": 5.28375, "longitude": 10.37429, "x": 60.6, "y": 56.6, "source": "geonames", "type": "PPL" }, { "name": "Chepang", "latitude": 5.28719, "longitude": 10.26176, "x": 26.8, "y": 55.5, "source": "geonames", "type": "PPL" }, { "name": "Chienke", "latitude": 5.25714, "longitude": 10.40049, "x": 68.5, "y": 64.8, "source": "geonames", "type": "PPL" }, { "name": "Demgo", "latitude": 5.34159, "longitude": 10.37539, "x": 61, "y": 38.7, "source": "geonames", "type": "PPL" }, { "name": "Demtse", "latitude": 5.26915, "longitude": 10.45384, "x": 84.6, "y": 61.1, "source": "geonames", "type": "PPL" }, { "name": "Dendeu", "latitude": 5.42272, "longitude": 10.31556, "x": 43, "y": 13.6, "source": "geonames", "type": "PPL" }, { "name": "Djegwe Bem", "latitude": 5.2868, "longitude": 10.44156, "x": 80.9, "y": 55.6, "source": "geonames", "type": "PPL" }, { "name": "Djembi", "latitude": 5.31001, "longitude": 10.30196, "x": 38.9, "y": 48.5, "source": "geonames", "type": "PPL" }, { "name": "Djemghoue", "latitude": 5.30346, "longitude": 10.38035, "x": 62.5, "y": 50.5, "source": "geonames", "type": "PPL" }, { "name": "Djetcha'", "latitude": 5.29484, "longitude": 10.42309, "x": 75.3, "y": 53.2, "source": "geonames", "type": "PPL" }, { "name": "Djeunkong", "latitude": 5.19624, "longitude": 10.35808, "x": 55.8, "y": 83.7, "source": "geonames", "type": "PPL" }, { "name": "Djeve", "latitude": 5.29064, "longitude": 10.40925, "x": 71.2, "y": 54.5, "source": "geonames", "type": "PPL" }, { "name": "Djewang", "latitude": 5.28608, "longitude": 10.3421, "x": 50.9, "y": 55.9, "source": "geonames", "type": "PPL" }, { "name": "Djiomghouo", "latitude": 5.3529, "longitude": 10.39023, "x": 65.4, "y": 35.2, "source": "geonames", "type": "PPL" }, { "name": "Famghang", "latitude": 5.29835, "longitude": 10.2352, "x": 18.8, "y": 52.1, "source": "geonames", "type": "PPL" }, { "name": "Famgoum", "latitude": 5.31219, "longitude": 10.32301, "x": 45.2, "y": 47.8, "source": "geonames", "type": "PPL" }, { "name": "Famla'", "latitude": 5.25453, "longitude": 10.46982, "x": 89.4, "y": 65.6, "source": "geonames", "type": "PPL" }, { "name": "Fotouni", "latitude": 5.32635, "longitude": 10.22624, "x": 16.1, "y": 43.4, "source": "geonames", "type": "PPL" }, { "name": "Fouchip", "latitude": 5.26067, "longitude": 10.46312, "x": 87.4, "y": 63.7, "source": "geonames", "type": "PPL" }, { "name": "Ghi", "latitude": 5.39783, "longitude": 10.33981, "x": 50.3, "y": 21.3, "source": "geonames", "type": "PPL" }, { "name": "Hiala", "latitude": 5.36031, "longitude": 10.36047, "x": 56.5, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Hom", "latitude": 5.33378, "longitude": 10.40085, "x": 68.6, "y": 41.1, "source": "geonames", "type": "PPL" }, { "name": "Ka'afo", "latitude": 5.30905, "longitude": 10.41085, "x": 71.6, "y": 48.8, "source": "geonames", "type": "PPL" }, { "name": "Ka'goungwe", "latitude": 5.28822, "longitude": 10.42456, "x": 75.8, "y": 55.2, "source": "geonames", "type": "PPL" }, { "name": "Kahangwe", "latitude": 5.23802, "longitude": 10.40152, "x": 68.8, "y": 70.8, "source": "geonames", "type": "PPL" }, { "name": "Kamghe", "latitude": 5.25625, "longitude": 10.45502, "x": 84.9, "y": 65.1, "source": "geonames", "type": "PPL" }, { "name": "Kem", "latitude": 5.25727, "longitude": 10.33596, "x": 49.1, "y": 64.8, "source": "geonames", "type": "PPL" }, { "name": "King-Place", "latitude": 5.27337, "longitude": 10.33476, "x": 48.7, "y": 59.8, "source": "geonames", "type": "PPL" }, { "name": "Kouotche", "latitude": 5.34107, "longitude": 10.23203, "x": 17.8, "y": 38.8, "source": "geonames", "type": "PPL" }, { "name": "Kwo'lie", "latitude": 5.33429, "longitude": 10.22734, "x": 16.4, "y": 40.9, "source": "geonames", "type": "PPL" }, { "name": "Kwo'nkeng", "latitude": 5.35738, "longitude": 10.23977, "x": 20.2, "y": 33.8, "source": "geonames", "type": "PPL" }, { "name": "Kwo'pou", "latitude": 5.30274, "longitude": 10.44088, "x": 80.7, "y": 50.7, "source": "geonames", "type": "PPL" }, { "name": "La'angou", "latitude": 5.28244, "longitude": 10.35089, "x": 53.6, "y": 57, "source": "geonames", "type": "PPL" }, { "name": "La'angwi", "latitude": 5.25369, "longitude": 10.39319, "x": 66.3, "y": 65.9, "source": "geonames", "type": "PPL" }, { "name": "La'tsit", "latitude": 5.42807, "longitude": 10.28642, "x": 34.2, "y": 11.9, "source": "geonames", "type": "PPL" }, { "name": "Langwe", "latitude": 5.24016, "longitude": 10.37417, "x": 60.6, "y": 70.1, "source": "geonames", "type": "PPL" }, { "name": "Lou", "latitude": 5.27444, "longitude": 10.36062, "x": 56.5, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Mba", "latitude": 5.28684, "longitude": 10.45201, "x": 84, "y": 55.6, "source": "geonames", "type": "PPL" }, { "name": "Mbe", "latitude": 5.27665, "longitude": 10.45222, "x": 84.1, "y": 58.8, "source": "geonames", "type": "PPL" }, { "name": "Mbem", "latitude": 5.31514, "longitude": 10.22951, "x": 17.1, "y": 46.9, "source": "geonames", "type": "PPL" }, { "name": "Mbou'oukhe", "latitude": 5.33747, "longitude": 10.38861, "x": 64.9, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Mboum", "latitude": 5.35505, "longitude": 10.32029, "x": 44.4, "y": 34.5, "source": "geonames", "type": "PPL" }, { "name": "Meja", "latitude": 5.38517, "longitude": 10.28123, "x": 32.6, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Mekwo'ngu", "latitude": 5.34352, "longitude": 10.23797, "x": 19.6, "y": 38.1, "source": "geonames", "type": "PPL" }, { "name": "Metsam", "latitude": 5.39568, "longitude": 10.27312, "x": 30.2, "y": 21.9, "source": "geonames", "type": "PPL" }, { "name": "Motchefondom", "latitude": 5.28176, "longitude": 10.25601, "x": 25, "y": 57.2, "source": "geonames", "type": "PPL" }, { "name": "Mou'nye", "latitude": 5.26591, "longitude": 10.39544, "x": 67, "y": 62.1, "source": "geonames", "type": "PPL" }, { "name": "Mu'njouo", "latitude": 5.39482, "longitude": 10.32237, "x": 45, "y": 22.2, "source": "geonames", "type": "PPL" }, { "name": "Ndang", "latitude": 5.37972, "longitude": 10.33276, "x": 48.1, "y": 26.9, "source": "geonames", "type": "PPL" }, { "name": "Ndemanguim", "latitude": 5.33499, "longitude": 10.23299, "x": 18.1, "y": 40.7, "source": "geonames", "type": "PPL" }, { "name": "Ndempa", "latitude": 5.2819, "longitude": 10.33061, "x": 47.5, "y": 57.2, "source": "geonames", "type": "PPL" }, { "name": "Ndenkeng", "latitude": 5.25026, "longitude": 10.34017, "x": 50.4, "y": 67, "source": "geonames", "type": "PPL" }, { "name": "Ndenkop", "latitude": 5.25108, "longitude": 10.35303, "x": 54.2, "y": 66.7, "source": "geonames", "type": "PPL" }, { "name": "Ndennkem", "latitude": 5.24335, "longitude": 10.3217, "x": 44.8, "y": 69.1, "source": "geonames", "type": "PPL" }, { "name": "Ndentcha", "latitude": 5.3728, "longitude": 10.35898, "x": 56, "y": 29, "source": "geonames", "type": "PPL" }, { "name": "Ndiknip", "latitude": 5.18753, "longitude": 10.38179, "x": 62.9, "y": 86.4, "source": "geonames", "type": "PPL" }, { "name": "Nephie", "latitude": 5.38239, "longitude": 10.27352, "x": 30.3, "y": 26, "source": "geonames", "type": "PPL" }, { "name": "Ngakam", "latitude": 5.33805, "longitude": 10.23659, "x": 19.2, "y": 39.8, "source": "geonames", "type": "PPL" }, { "name": "Ngam", "latitude": 5.34148, "longitude": 10.25903, "x": 25.9, "y": 38.7, "source": "geonames", "type": "PPL" }, { "name": "Ngou", "latitude": 5.19685, "longitude": 10.38595, "x": 64.1, "y": 83.5, "source": "geonames", "type": "PPL" }, { "name": "Nguenka", "latitude": 5.34932, "longitude": 10.24005, "x": 20.2, "y": 36.3, "source": "geonames", "type": "PPL" }, { "name": "Ngwengwa", "latitude": 5.29052, "longitude": 10.38287, "x": 63.2, "y": 54.5, "source": "geonames", "type": "PPL" }, { "name": "Ngwesse", "latitude": 5.27692, "longitude": 10.40533, "x": 70, "y": 58.7, "source": "geonames", "type": "PPL" }, { "name": "Njeghang", "latitude": 5.40523, "longitude": 10.29884, "x": 37.9, "y": 19, "source": "geonames", "type": "PPL" }, { "name": "Njekop", "latitude": 5.43422, "longitude": 10.2944, "x": 36.6, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Njekou", "latitude": 5.29614, "longitude": 10.33016, "x": 47.4, "y": 52.8, "source": "geonames", "type": "PPL" }, { "name": "Njeola", "latitude": 5.42569, "longitude": 10.3104, "x": 41.4, "y": 12.6, "source": "geonames", "type": "PPL" }, { "name": "Njuba", "latitude": 5.40893, "longitude": 10.32968, "x": 47.2, "y": 17.8, "source": "geonames", "type": "PPL" }, { "name": "Nka'", "latitude": 5.31027, "longitude": 10.34564, "x": 52, "y": 48.4, "source": "geonames", "type": "PPL" }, { "name": "Nka'ako", "latitude": 5.26387, "longitude": 10.40984, "x": 71.3, "y": 62.8, "source": "geonames", "type": "PPL" }, { "name": "Nka'ndepa'", "latitude": 5.26882, "longitude": 10.40592, "x": 70.1, "y": 61.2, "source": "geonames", "type": "PPL" }, { "name": "Nkang", "latitude": 5.38073, "longitude": 10.31319, "x": 42.2, "y": 26.6, "source": "geonames", "type": "PPL" }, { "name": "Nkwoke", "latitude": 5.40762, "longitude": 10.31417, "x": 42.5, "y": 18.2, "source": "geonames", "type": "PPL" }, { "name": "Nom", "latitude": 5.31621, "longitude": 10.36283, "x": 57.2, "y": 46.5, "source": "geonames", "type": "PPL" }, { "name": "Noo", "latitude": 5.3197, "longitude": 10.23046, "x": 17.3, "y": 45.5, "source": "geonames", "type": "PPL" }, { "name": "Ntsit", "latitude": 5.3575, "longitude": 10.26213, "x": 26.9, "y": 33.8, "source": "geonames", "type": "PPL" }, { "name": "Nving", "latitude": 5.36684, "longitude": 10.24563, "x": 21.9, "y": 30.9, "source": "geonames", "type": "PPL" }, { "name": "Nzindom", "latitude": 5.26598, "longitude": 10.38695, "x": 64.4, "y": 62.1, "source": "geonames", "type": "PPL" }, { "name": "Pango", "latitude": 5.18333, "longitude": 10.36667, "x": 58.3, "y": 87.7, "source": "geonames", "type": "PPL" }, { "name": "Pou'mze", "latitude": 5.33503, "longitude": 10.34339, "x": 51.3, "y": 40.7, "source": "geonames", "type": "PPL" }, { "name": "Sou'e", "latitude": 5.32461, "longitude": 10.3918, "x": 65.9, "y": 43.9, "source": "geonames", "type": "PPL" }, { "name": "T'honta", "latitude": 5.36206, "longitude": 10.33006, "x": 47.3, "y": 32.3, "source": "geonames", "type": "PPL" }, { "name": "Tcha'ave", "latitude": 5.35599, "longitude": 10.34572, "x": 52, "y": 34.2, "source": "geonames", "type": "PPL" }, { "name": "Tchang", "latitude": 5.40272, "longitude": 10.31347, "x": 42.3, "y": 19.8, "source": "geonames", "type": "PPL" }, { "name": "Tchie", "latitude": 5.3425, "longitude": 10.36214, "x": 57, "y": 38.4, "source": "geonames", "type": "PPL" }, { "name": "Tchisso", "latitude": 5.31933, "longitude": 10.22593, "x": 16, "y": 45.6, "source": "geonames", "type": "PPL" }, { "name": "Tchouno", "latitude": 5.33382, "longitude": 10.25996, "x": 26.2, "y": 41.1, "source": "geonames", "type": "PPL" }, { "name": "Tchum", "latitude": 5.38229, "longitude": 10.25803, "x": 25.6, "y": 26.1, "source": "geonames", "type": "PPL" }, { "name": "Tchunkang", "latitude": 5.38328, "longitude": 10.29563, "x": 37, "y": 25.8, "source": "geonames", "type": "PPL" }, { "name": "Tefam", "latitude": 5.263, "longitude": 10.47001, "x": 89.4, "y": 63, "source": "geonames", "type": "PPL" }, { "name": "Tefang", "latitude": 5.34215, "longitude": 10.26274, "x": 27.1, "y": 38.5, "source": "geonames", "type": "PPL" }, { "name": "Thomi", "latitude": 5.39316, "longitude": 10.2945, "x": 36.6, "y": 22.7, "source": "geonames", "type": "PPL" }, { "name": "Togheu", "latitude": 5.36619, "longitude": 10.34879, "x": 53, "y": 31.1, "source": "geonames", "type": "PPL" }, { "name": "Toke", "latitude": 5.39833, "longitude": 10.32451, "x": 45.7, "y": 21.1, "source": "geonames", "type": "PPL" }, { "name": "Tomkheu", "latitude": 5.28323, "longitude": 10.41412, "x": 72.6, "y": 56.8, "source": "geonames", "type": "PPL" }, { "name": "Tougo Meje", "latitude": 5.2724, "longitude": 10.41577, "x": 73.1, "y": 60.1, "source": "geonames", "type": "PPL" }, { "name": "Tougo Mpou", "latitude": 5.26379, "longitude": 10.42944, "x": 77.2, "y": 62.8, "source": "geonames", "type": "PPL" }, { "name": "Toula", "latitude": 5.2775, "longitude": 10.47189, "x": 90, "y": 58.5, "source": "geonames", "type": "PPL" }, { "name": "Toumghem", "latitude": 5.30715, "longitude": 10.43849, "x": 79.9, "y": 49.3, "source": "geonames", "type": "PPL" }, { "name": "Tounang", "latitude": 5.26831, "longitude": 10.46947, "x": 89.3, "y": 61.4, "source": "geonames", "type": "PPL" }, { "name": "Tsela'", "latitude": 5.26129, "longitude": 10.36803, "x": 58.7, "y": 63.5, "source": "geonames", "type": "PPL" }, { "name": "Tsemoya", "latitude": 5.24439, "longitude": 10.33985, "x": 50.3, "y": 68.8, "source": "geonames", "type": "PPL" }, { "name": "Tsep", "latitude": 5.27861, "longitude": 10.46535, "x": 88, "y": 58.2, "source": "geonames", "type": "PPL" }, { "name": "Tsongwi", "latitude": 5.2524, "longitude": 10.45308, "x": 84.3, "y": 66.3, "source": "geonames", "type": "PPL" }, { "name": "Tsoungwi", "latitude": 5.2593, "longitude": 10.44031, "x": 80.5, "y": 64.2, "source": "geonames", "type": "PPL" }, { "name": "Wang", "latitude": 5.3659, "longitude": 10.37241, "x": 60.1, "y": 31.2, "source": "geonames", "type": "PPL" }, { "name": "Yenom", "latitude": 5.27088, "longitude": 10.44291, "x": 81.3, "y": 60.6, "source": "geonames", "type": "PPL" }, { "name": "Zemya", "latitude": 5.39608, "longitude": 10.23861, "x": 19.8, "y": 21.8, "source": "geonames", "type": "PPL" } ], "Bali": [ { "name": "Anyene", "latitude": 5.79284, "longitude": 10.02094, "x": 61.8, "y": 59.8, "source": "geonames", "type": "PPL" }, { "name": "Ashong", "latitude": 5.77724, "longitude": 9.9659, "x": 33.6, "y": 65.3, "source": "geonames", "type": "PPL" }, { "name": "Baba", "latitude": 5.89375, "longitude": 10.06444, "x": 84.1, "y": 24.4, "source": "geonames", "type": "PPL" }, { "name": "Bako", "latitude": 5.87229, "longitude": 10.03491, "x": 69, "y": 31.9, "source": "geonames", "type": "PPL" }, { "name": "Baranja", "latitude": 5.8022, "longitude": 9.9546, "x": 27.8, "y": 56.6, "source": "geonames", "type": "PPL" }, { "name": "Bosa", "latitude": 5.93229, "longitude": 9.98034, "x": 41, "y": 10.8, "source": "geonames", "type": "PPL" }, { "name": "Djenka", "latitude": 5.89819, "longitude": 10.02995, "x": 66.4, "y": 22.8, "source": "geonames", "type": "PPL" }, { "name": "Fomanji", "latitude": 5.70709, "longitude": 10.00447, "x": 53.3, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Fonekuta", "latitude": 5.92444, "longitude": 10.03251, "x": 67.7, "y": 13.6, "source": "geonames", "type": "PPL" }, { "name": "Gengap", "latitude": 5.90603, "longitude": 10.03695, "x": 70, "y": 20, "source": "geonames", "type": "PPL" }, { "name": "Gwofon", "latitude": 5.9112, "longitude": 9.92, "x": 10, "y": 18.2, "source": "geonames", "type": "PPL" }, { "name": "Jemajema", "latitude": 5.8713, "longitude": 9.9833, "x": 42.5, "y": 32.3, "source": "geonames", "type": "PPL" }, { "name": "Jommivora", "latitude": 5.81018, "longitude": 10.06231, "x": 83, "y": 53.8, "source": "geonames", "type": "PPL" }, { "name": "Kai", "latitude": 5.9346, "longitude": 9.9313, "x": 15.8, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Koping", "latitude": 5.86028, "longitude": 10.04871, "x": 76, "y": 36.1, "source": "geonames", "type": "PPL" }, { "name": "Kurabi", "latitude": 5.9, "longitude": 9.9278, "x": 14, "y": 22.2, "source": "geonames", "type": "PPL" }, { "name": "Kwengnen", "latitude": 5.81105, "longitude": 10.05054, "x": 77, "y": 53.4, "source": "geonames", "type": "PPL" }, { "name": "Kwiding", "latitude": 5.78001, "longitude": 10.01674, "x": 59.6, "y": 64.4, "source": "geonames", "type": "PPL" }, { "name": "Mambim", "latitude": 5.79972, "longitude": 10.05855, "x": 81.1, "y": 57.4, "source": "geonames", "type": "PPL" }, { "name": "Mbu", "latitude": 5.85049, "longitude": 10.07419, "x": 89.1, "y": 39.6, "source": "geonames", "type": "PPL" }, { "name": "Mbutong", "latitude": 5.8472, "longitude": 9.9858, "x": 43.8, "y": 40.7, "source": "geonames", "type": "PPL" }, { "name": "Menja", "latitude": 5.85711, "longitude": 10.07592, "x": 90, "y": 37.2, "source": "geonames", "type": "PPL" }, { "name": "Misho", "latitude": 5.90379, "longitude": 10.00552, "x": 53.9, "y": 20.8, "source": "geonames", "type": "PPL" }, { "name": "Monu", "latitude": 5.85515, "longitude": 10.05858, "x": 81.1, "y": 37.9, "source": "geonames", "type": "PPL" }, { "name": "Mukwo", "latitude": 5.8725, "longitude": 9.97, "x": 35.7, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Nenjo", "latitude": 5.8444, "longitude": 10.0722, "x": 88.1, "y": 41.7, "source": "geonames", "type": "PPL" }, { "name": "Nguenmawa", "latitude": 5.8904, "longitude": 9.9574, "x": 29.2, "y": 25.5, "source": "geonames", "type": "PPL" }, { "name": "Nyali", "latitude": 5.78317, "longitude": 10.04658, "x": 74.9, "y": 63.2, "source": "geonames", "type": "PPL" }, { "name": "Pinyin", "latitude": 5.71667, "longitude": 9.98333, "x": 42.5, "y": 86.6, "source": "geonames", "type": "PPL" }, { "name": "Sanyere", "latitude": 5.78699, "longitude": 10.01456, "x": 58.5, "y": 61.9, "source": "geonames", "type": "PPL" }, { "name": "Tonogo", "latitude": 5.76145, "longitude": 10.00993, "x": 56.1, "y": 70.9, "source": "geonames", "type": "PPL" }, { "name": "Wom Guzang", "latitude": 5.8202, "longitude": 9.958, "x": 29.5, "y": 50.2, "source": "geonames", "type": "PPL" }, { "name": "Wontoko", "latitude": 5.79352, "longitude": 10.02741, "x": 65.1, "y": 59.6, "source": "geonames", "type": "PPL" }, { "name": "Wossing", "latitude": 5.92702, "longitude": 10.06021, "x": 81.9, "y": 12.7, "source": "geonames", "type": "PPL" } ], "Bamenda": [ { "name": "Akum", "latitude": 5.887687, "longitude": 10.156404, "x": 51.4, "y": 90, "source": "osm", "type": "village" }, { "name": "Alakuma Junction", "latitude": 5.9710628, "longitude": 10.1343097, "x": 40, "y": 46.2, "source": "osm", "type": "neighbourhood" }, { "name": "Amour Mezam Company Ltd", "latitude": 5.9677802, "longitude": 10.1715808, "x": 59.2, "y": 47.9, "source": "osm", "type": "bus_station" }, { "name": "Azeri", "latitude": 5.94855, "longitude": 10.13787, "x": 41.8, "y": 58, "source": "geonames", "type": "PPLX" }, { "name": "Bafreng", "latitude": 5.9736063, "longitude": 10.1831113, "x": 65.1, "y": 44.8, "source": "osm", "type": "village" }, { "name": "Balewa", "latitude": 6.0165864, "longitude": 10.1974985, "x": 72.5, "y": 22.2, "source": "osm", "type": "village" }, { "name": "Bambui", "latitude": 6.014621, "longitude": 10.2315906, "x": 90, "y": 23.3, "source": "osm", "type": "village" }, { "name": "Bamendankwe", "latitude": 5.9326578, "longitude": 10.1970425, "x": 72.2, "y": 66.4, "source": "osm", "type": "suburb" }, { "name": "Bande", "latitude": 5.97589, "longitude": 10.1563, "x": 51.3, "y": 43.6, "source": "geonames", "type": "PPL" }, { "name": "Bangya", "latitude": 5.92455, "longitude": 10.08259, "x": 13.4, "y": 70.6, "source": "geonames", "type": "PPL" }, { "name": "Bayele", "latitude": 5.97307, "longitude": 10.17272, "x": 59.7, "y": 45.1, "source": "geonames", "type": "PPLX" }, { "name": "Chomba", "latitude": 5.911435, "longitude": 10.095975, "x": 20.3, "y": 77.5, "source": "osm", "type": "village" }, { "name": "Foya", "latitude": 6.0398433, "longitude": 10.1976535, "x": 72.6, "y": 10, "source": "osm", "type": "village" }, { "name": "Grassfield 2", "latitude": 5.95719, "longitude": 10.15943, "x": 52.9, "y": 53.5, "source": "geonames", "type": "PPL" }, { "name": "Lown", "latitude": 5.95452, "longitude": 10.12453, "x": 35, "y": 54.9, "source": "geonames", "type": "PPL" }, { "name": "Mankon", "latitude": 6.0056298, "longitude": 10.1038897, "x": 24.4, "y": 28, "source": "osm", "type": "village" }, { "name": "Mbengwi Car Park", "latitude": 5.9556215, "longitude": 10.130609, "x": 38.1, "y": 54.3, "source": "osm", "type": "taxi" }, { "name": "Mboutou", "latitude": 5.9031932, "longitude": 10.1311734, "x": 38.4, "y": 81.8, "source": "osm", "type": "village" }, { "name": "Menda", "latitude": 5.98848, "longitude": 10.18712, "x": 67.1, "y": 37, "source": "geonames", "type": "PPL" }, { "name": "Menda Nkwe", "latitude": 5.93581, "longitude": 10.19795, "x": 72.7, "y": 64.7, "source": "geonames", "type": "PPL" }, { "name": "Mengop", "latitude": 5.96396, "longitude": 10.16315, "x": 54.8, "y": 49.9, "source": "geonames", "type": "PPLX" }, { "name": "Metta", "latitude": 5.94296, "longitude": 10.15687, "x": 51.6, "y": 60.9, "source": "geonames", "type": "PPLX" }, { "name": "Mile 2 Junction", "latitude": 5.9657723, "longitude": 10.1695625, "x": 58.1, "y": 48.9, "source": "osm", "type": "locality" }, { "name": "Mile 4 junction", "latitude": 5.990921, "longitude": 10.18537, "x": 66.3, "y": 35.7, "source": "osm", "type": "locality" }, { "name": "Mulang", "latitude": 5.9784147, "longitude": 10.1523677, "x": 49.3, "y": 42.3, "source": "osm", "type": "suburb" }, { "name": "Musang", "latitude": 5.9684428, "longitude": 10.1467511, "x": 46.4, "y": 47.5, "source": "osm", "type": "suburb" }, { "name": "Naaka", "latitude": 5.92684, "longitude": 10.09544, "x": 20, "y": 69.4, "source": "geonames", "type": "PPL" }, { "name": "Nabakam", "latitude": 5.96985, "longitude": 10.0759, "x": 10, "y": 46.8, "source": "geonames", "type": "PPL" }, { "name": "Nangah Junction", "latitude": 5.9434638, "longitude": 10.1379008, "x": 41.9, "y": 60.7, "source": "osm", "type": "locality" }, { "name": "Nchoubu Junction", "latitude": 5.9735196, "longitude": 10.1279791, "x": 36.8, "y": 44.9, "source": "osm", "type": "neighbourhood" }, { "name": "Ndabu", "latitude": 5.98615, "longitude": 10.11932, "x": 32.3, "y": 38.2, "source": "geonames", "type": "PPL" }, { "name": "Nekonetakwi", "latitude": 5.90597, "longitude": 10.11086, "x": 28, "y": 80.4, "source": "geonames", "type": "PPL" }, { "name": "Nitop", "latitude": 5.9580977, "longitude": 10.1376906, "x": 41.8, "y": 53, "source": "osm", "type": "suburb" }, { "name": "Nkwen", "latitude": 5.9884973, "longitude": 10.184126, "x": 65.6, "y": 37, "source": "osm", "type": "suburb" }, { "name": "Nkwen Park", "latitude": 5.992703, "longitude": 10.1848591, "x": 66, "y": 34.8, "source": "osm", "type": "bus_station" }, { "name": "Nlabakam", "latitude": 5.96327, "longitude": 10.09532, "x": 20, "y": 50.3, "source": "geonames", "type": "PPL" }, { "name": "Nsongwa", "latitude": 5.9292011, "longitude": 10.1245098, "x": 35, "y": 68.2, "source": "osm", "type": "village" }, { "name": "Ntambeng", "latitude": 5.99449, "longitude": 10.12995, "x": 37.8, "y": 33.8, "source": "geonames", "type": "PPL" }, { "name": "Ntamulung", "latitude": 5.959613, "longitude": 10.1582471, "x": 52.3, "y": 52.2, "source": "osm", "type": "suburb" }, { "name": "Ntarinkon", "latitude": 5.967521, "longitude": 10.1431543, "x": 44.6, "y": 48, "source": "osm", "type": "suburb" }, { "name": "Ntasen", "latitude": 5.9932541, "longitude": 10.1618982, "x": 54.2, "y": 34.5, "source": "osm", "type": "village" }, { "name": "Psalms 23", "latitude": 5.9819398, "longitude": 10.1799683, "x": 63.5, "y": 40.4, "source": "osm", "type": "bus_station" }, { "name": "T-junction", "latitude": 5.950264, "longitude": 10.1524458, "x": 49.3, "y": 57.1, "source": "osm", "type": "locality" }, { "name": "Tanamba", "latitude": 5.91521, "longitude": 10.09825, "x": 21.5, "y": 75.5, "source": "geonames", "type": "PPL" }, { "name": "Tanti", "latitude": 5.91332, "longitude": 10.13288, "x": 39.3, "y": 76.5, "source": "geonames", "type": "PPL" }, { "name": "Titam", "latitude": 5.93656, "longitude": 10.09185, "x": 18.2, "y": 64.3, "source": "geonames", "type": "PPL" }, { "name": "Tunenkweni", "latitude": 5.93023, "longitude": 10.13697, "x": 41.4, "y": 67.6, "source": "geonames", "type": "PPL" }, { "name": "Up Station", "latitude": 5.9499366, "longitude": 10.1655641, "x": 56.1, "y": 57.3, "source": "osm", "type": "neighbourhood" }, { "name": "VATICAN EXPRESS", "latitude": 5.9614336, "longitude": 10.1522772, "x": 49.2, "y": 51.2, "source": "osm", "type": "bus_station" }, { "name": "Veterinary junction", "latitude": 5.9582398, "longitude": 10.160079, "x": 53.3, "y": 52.9, "source": "osm", "type": "locality" } ], "Bambili": [ { "name": "Babank Tungo", "latitude": 5.97619, "longitude": 10.298, "x": 67, "y": 29.1, "source": "geonames", "type": "PPL" }, { "name": "Babanki Tungo", "latitude": 5.98166, "longitude": 10.31584, "x": 76.7, "y": 26.5, "source": "geonames", "type": "PPL" }, { "name": "Bamukumbit", "latitude": 5.84737, "longitude": 10.33896, "x": 89.4, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Banja", "latitude": 5.95754, "longitude": 10.21386, "x": 21, "y": 37.9, "source": "geonames", "type": "PPL" }, { "name": "Benjon", "latitude": 5.87656, "longitude": 10.24654, "x": 38.9, "y": 76.2, "source": "geonames", "type": "PPL" }, { "name": "Bukejam", "latitude": 5.96611, "longitude": 10.33572, "x": 87.6, "y": 33.9, "source": "geonames", "type": "PPL" }, { "name": "Kemkom", "latitude": 5.9648, "longitude": 10.27552, "x": 54.7, "y": 34.5, "source": "geonames", "type": "PPL" }, { "name": "Lumndi", "latitude": 5.8498, "longitude": 10.31776, "x": 77.8, "y": 88.9, "source": "geonames", "type": "PPL" }, { "name": "Mante", "latitude": 5.97456, "longitude": 10.20501, "x": 16.2, "y": 29.9, "source": "geonames", "type": "PPL" }, { "name": "Mbanka", "latitude": 5.85274, "longitude": 10.33525, "x": 87.4, "y": 87.5, "source": "geonames", "type": "PPL" }, { "name": "Mbu", "latitude": 5.9755, "longitude": 10.32512, "x": 81.8, "y": 29.5, "source": "geonames", "type": "PPL" }, { "name": "Nkwen", "latitude": 5.99517, "longitude": 10.19376, "x": 10, "y": 20.2, "source": "geonames", "type": "PPL" }, { "name": "Ntekezon", "latitude": 5.96285, "longitude": 10.32879, "x": 83.8, "y": 35.4, "source": "geonames", "type": "PPL" }, { "name": "Sabga", "latitude": 6.01667, "longitude": 10.31667, "x": 77.2, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Toko", "latitude": 5.84921, "longitude": 10.3255, "x": 82, "y": 89.1, "source": "geonames", "type": "PPL" }, { "name": "Twolo", "latitude": 5.97749, "longitude": 10.31904, "x": 78.5, "y": 28.5, "source": "geonames", "type": "PPL" }, { "name": "Wegwa", "latitude": 5.91587, "longitude": 10.34009, "x": 90, "y": 57.6, "source": "geonames", "type": "PPL" } ], "Bambui": [ { "name": "Babanki", "latitude": 6.11667, "longitude": 10.25, "x": 35.3, "y": 24, "source": "geonames", "type": "PPL" }, { "name": "Ngotin", "latitude": 6.13333, "longitude": 10.23333, "x": 10, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Tubah", "latitude": 6.038, "longitude": 10.286, "x": 90, "y": 90, "source": "geonames", "type": "PPL" } ], "Bamusso": [ { "name": "Akwa Abatin", "latitude": 4.5774, "longitude": 8.8008, "x": 31.7, "y": 24.3, "source": "geonames", "type": "PPL" }, { "name": "Bekumu", "latitude": 4.3788, "longitude": 8.8921, "x": 49.2, "y": 69.8, "source": "geonames", "type": "PPL" }, { "name": "Betanga", "latitude": 4.33333, "longitude": 8.91667, "x": 54, "y": 80.2, "source": "geonames", "type": "PPL" }, { "name": "Betika", "latitude": 4.3078, "longitude": 8.9227, "x": 55.1, "y": 86.1, "source": "geonames", "type": "PPL" }, { "name": "Boa", "latitude": 4.4279, "longitude": 8.9741, "x": 65, "y": 58.6, "source": "geonames", "type": "PPL" }, { "name": "Bonjare", "latitude": 4.4544, "longitude": 8.9868, "x": 67.4, "y": 52.5, "source": "geonames", "type": "PPL" }, { "name": "Dikome", "latitude": 4.4873, "longitude": 9.0058, "x": 71.1, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Edigidim", "latitude": 4.5195, "longitude": 8.8838, "x": 47.7, "y": 37.6, "source": "geonames", "type": "PPL" }, { "name": "Efolofo", "latitude": 4.3629, "longitude": 9.0909, "x": 87.4, "y": 73.5, "source": "geonames", "type": "PPL" }, { "name": "Endu", "latitude": 4.6283, "longitude": 8.8511, "x": 41.4, "y": 12.7, "source": "geonames", "type": "PPL" }, { "name": "Iloani", "latitude": 4.5304, "longitude": 8.9348, "x": 57.4, "y": 35.1, "source": "geonames", "type": "PPL" }, { "name": "Ine Asoma", "latitude": 4.5125, "longitude": 8.9026, "x": 51.3, "y": 39.2, "source": "geonames", "type": "PPL" }, { "name": "Ine Atayo", "latitude": 4.5143, "longitude": 8.6911, "x": 10.7, "y": 38.8, "source": "geonames", "type": "PPL" }, { "name": "Ine Ekpai", "latitude": 4.5613, "longitude": 8.8859, "x": 48.1, "y": 28, "source": "geonames", "type": "PPL" }, { "name": "Ine Idiong", "latitude": 4.63989, "longitude": 8.81513, "x": 34.5, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Ine Ndiop", "latitude": 4.5756, "longitude": 8.7137, "x": 15, "y": 24.7, "source": "geonames", "type": "PPL" }, { "name": "Ine Okon Ideho", "latitude": 4.5311, "longitude": 8.7753, "x": 26.8, "y": 34.9, "source": "geonames", "type": "PPL" }, { "name": "Ine Okpo", "latitude": 4.5483, "longitude": 8.7974, "x": 31.1, "y": 31, "source": "geonames", "type": "PPL" }, { "name": "Ino Akpak", "latitude": 4.5216, "longitude": 8.6875, "x": 10, "y": 37.1, "source": "geonames", "type": "PPL" }, { "name": "Ino Umo", "latitude": 4.56667, "longitude": 8.71667, "x": 15.6, "y": 26.8, "source": "geonames", "type": "PPL" }, { "name": "Inodo", "latitude": 4.4285, "longitude": 8.8867, "x": 48.2, "y": 58.4, "source": "geonames", "type": "PPL" }, { "name": "Inua Abasi", "latitude": 4.53, "longitude": 8.7674, "x": 25.3, "y": 35.2, "source": "geonames", "type": "PPL" }, { "name": "Kesse I", "latitude": 4.5214, "longitude": 8.8844, "x": 47.8, "y": 37.2, "source": "geonames", "type": "PPL" }, { "name": "Kombo a Mbonjo", "latitude": 4.5126, "longitude": 8.7055, "x": 13.5, "y": 39.2, "source": "geonames", "type": "PPL" }, { "name": "Kombo Abosukudu", "latitude": 4.5929, "longitude": 8.8423, "x": 39.7, "y": 20.8, "source": "geonames", "type": "PPL" }, { "name": "Kombo Adibo I", "latitude": 4.563, "longitude": 8.7255, "x": 17.3, "y": 27.6, "source": "geonames", "type": "PPL" }, { "name": "Kombo Adibo II", "latitude": 4.536, "longitude": 8.7627, "x": 24.4, "y": 33.8, "source": "geonames", "type": "PPL" }, { "name": "Kombo Asua", "latitude": 4.5476, "longitude": 8.8002, "x": 31.6, "y": 31.1, "source": "geonames", "type": "PPL" }, { "name": "Kombo Mokoko", "latitude": 4.4857, "longitude": 8.904, "x": 51.5, "y": 45.3, "source": "geonames", "type": "PPL" }, { "name": "Kosse", "latitude": 4.3457, "longitude": 9.0406, "x": 77.7, "y": 77.4, "source": "geonames", "type": "PPL" }, { "name": "Koto I", "latitude": 4.3547, "longitude": 9.0803, "x": 85.3, "y": 75.4, "source": "geonames", "type": "PPL" }, { "name": "Koto II", "latitude": 4.3216, "longitude": 9.0654, "x": 82.5, "y": 82.9, "source": "geonames", "type": "PPL" }, { "name": "Landy-Stage", "latitude": 4.4477, "longitude": 8.9302, "x": 56.5, "y": 54, "source": "geonames", "type": "PPL" }, { "name": "Liongo", "latitude": 4.409, "longitude": 8.9546, "x": 61.2, "y": 62.9, "source": "geonames", "type": "PPL" }, { "name": "Lissombe", "latitude": 4.3802, "longitude": 9.046, "x": 78.8, "y": 69.5, "source": "geonames", "type": "PPL" }, { "name": "Liwenyi", "latitude": 4.3721, "longitude": 9.0131, "x": 72.5, "y": 71.4, "source": "geonames", "type": "PPL" }, { "name": "Madale", "latitude": 4.29078, "longitude": 8.91774, "x": 54.2, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Mandonde", "latitude": 4.5578, "longitude": 8.7879, "x": 29.3, "y": 28.8, "source": "geonames", "type": "PPL" }, { "name": "Matutu", "latitude": 4.5579, "longitude": 8.9374, "x": 57.9, "y": 28.8, "source": "geonames", "type": "PPL" }, { "name": "Mbongo", "latitude": 4.462, "longitude": 8.984, "x": 66.9, "y": 50.8, "source": "geonames", "type": "PPL" }, { "name": "Mbonjo", "latitude": 4.5225, "longitude": 8.7026, "x": 12.9, "y": 36.9, "source": "geonames", "type": "PPL" }, { "name": "Mokara Tanda", "latitude": 4.5098, "longitude": 8.9027, "x": 51.3, "y": 39.8, "source": "geonames", "type": "PPL" }, { "name": "Mossongo", "latitude": 4.3155, "longitude": 8.9249, "x": 55.5, "y": 84.3, "source": "geonames", "type": "PPL" }, { "name": "Mweli", "latitude": 4.3873, "longitude": 9.1046, "x": 90, "y": 67.9, "source": "geonames", "type": "PPL" }, { "name": "Njangassa I", "latitude": 4.3302, "longitude": 8.9247, "x": 55.5, "y": 81, "source": "geonames", "type": "PPL" }, { "name": "Nya Ekang", "latitude": 4.6222, "longitude": 8.7595, "x": 23.8, "y": 14.1, "source": "geonames", "type": "PPL" } ], "Bandjoun": [ { "name": "Baham II", "latitude": 5.39799, "longitude": 10.52839, "x": 74.2, "y": 25.9, "source": "geonames", "type": "PPL" }, { "name": "Bawang", "latitude": 5.37302, "longitude": 10.37508, "x": 29, "y": 38.3, "source": "geonames", "type": "PPL" }, { "name": "Dem Woh", "latitude": 5.34946, "longitude": 10.43035, "x": 45.3, "y": 50.1, "source": "geonames", "type": "PPL" }, { "name": "Dja'", "latitude": 5.34767, "longitude": 10.41884, "x": 41.9, "y": 50.9, "source": "geonames", "type": "PPL" }, { "name": "Djebem Megue", "latitude": 5.32734, "longitude": 10.4548, "x": 52.5, "y": 61.1, "source": "geonames", "type": "PPL" }, { "name": "Djeleng", "latitude": 5.40365, "longitude": 10.42355, "x": 43.3, "y": 23, "source": "geonames", "type": "PPL" }, { "name": "Djetseguem", "latitude": 5.37315, "longitude": 10.42344, "x": 43.2, "y": 38.2, "source": "geonames", "type": "PPL" }, { "name": "Djione", "latitude": 5.36906, "longitude": 10.44765, "x": 50.4, "y": 40.3, "source": "geonames", "type": "PPL" }, { "name": "Djiongwo", "latitude": 5.29221, "longitude": 10.49498, "x": 64.3, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Djo", "latitude": 5.34963, "longitude": 10.40936, "x": 39.1, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Djone", "latitude": 5.37523, "longitude": 10.55057, "x": 80.7, "y": 37.2, "source": "geonames", "type": "PPL" }, { "name": "Domlo", "latitude": 5.38831, "longitude": 10.44243, "x": 48.8, "y": 30.7, "source": "geonames", "type": "PPL" }, { "name": "Famla'", "latitude": 5.31901, "longitude": 10.46174, "x": 54.5, "y": 65.2, "source": "geonames", "type": "PPL" }, { "name": "Famleng", "latitude": 5.39248, "longitude": 10.41577, "x": 41, "y": 28.6, "source": "geonames", "type": "PPL" }, { "name": "Famnwe", "latitude": 5.35489, "longitude": 10.44455, "x": 49.5, "y": 47.3, "source": "geonames", "type": "PPL" }, { "name": "Famtoum", "latitude": 5.30615, "longitude": 10.48481, "x": 61.3, "y": 71.6, "source": "geonames", "type": "PPL" }, { "name": "Fondji", "latitude": 5.32532, "longitude": 10.57869, "x": 89, "y": 62.1, "source": "geonames", "type": "PPL" }, { "name": "Fonegom", "latitude": 5.35505, "longitude": 10.4672, "x": 56.1, "y": 47.3, "source": "geonames", "type": "PPL" }, { "name": "Hiala'", "latitude": 5.34623, "longitude": 10.40494, "x": 37.8, "y": 51.7, "source": "geonames", "type": "PPL" }, { "name": "Hogo", "latitude": 5.31298, "longitude": 10.47416, "x": 58.2, "y": 68.2, "source": "geonames", "type": "PPL" }, { "name": "Houpouo", "latitude": 5.41039, "longitude": 10.38412, "x": 31.6, "y": 19.7, "source": "geonames", "type": "PPL" }, { "name": "Hwa", "latitude": 5.37531, "longitude": 10.43035, "x": 45.3, "y": 37.2, "source": "geonames", "type": "PPL" }, { "name": "Ka Tsela", "latitude": 5.38244, "longitude": 10.39139, "x": 33.8, "y": 33.6, "source": "geonames", "type": "PPL" }, { "name": "Ka'fam", "latitude": 5.322, "longitude": 10.44595, "x": 49.9, "y": 63.7, "source": "geonames", "type": "PPL" }, { "name": "Ka'sap", "latitude": 5.28693, "longitude": 10.47874, "x": 59.6, "y": 81.2, "source": "geonames", "type": "PPL" }, { "name": "Ka'yo", "latitude": 5.3943, "longitude": 10.44811, "x": 50.5, "y": 27.7, "source": "geonames", "type": "PPL" }, { "name": "Kam Djoung", "latitude": 5.37166, "longitude": 10.39226, "x": 34, "y": 39, "source": "geonames", "type": "PPL" }, { "name": "Kamdeng", "latitude": 5.32996, "longitude": 10.44553, "x": 49.8, "y": 59.8, "source": "geonames", "type": "PPL" }, { "name": "Kamngo", "latitude": 5.41054, "longitude": 10.44742, "x": 50.3, "y": 19.6, "source": "geonames", "type": "PPL" }, { "name": "Kamngwike", "latitude": 5.4132, "longitude": 10.43166, "x": 45.7, "y": 18.3, "source": "geonames", "type": "PPL" }, { "name": "Kang", "latitude": 5.36395, "longitude": 10.3838, "x": 31.5, "y": 42.8, "source": "geonames", "type": "PPL" }, { "name": "King-Place", "latitude": 5.35283, "longitude": 10.4054, "x": 37.9, "y": 48.4, "source": "geonames", "type": "PPL" }, { "name": "Kouoneke", "latitude": 5.41598, "longitude": 10.46683, "x": 56, "y": 16.9, "source": "geonames", "type": "PPL" }, { "name": "Kwongoum", "latitude": 5.40495, "longitude": 10.34249, "x": 19.4, "y": 22.4, "source": "geonames", "type": "PPL" }, { "name": "Lemgo", "latitude": 5.39062, "longitude": 10.46019, "x": 54.1, "y": 29.5, "source": "geonames", "type": "PPL" }, { "name": "Lemla", "latitude": 5.36659, "longitude": 10.42722, "x": 44.3, "y": 41.5, "source": "geonames", "type": "PPL" }, { "name": "Manga", "latitude": 5.33209, "longitude": 10.58194, "x": 90, "y": 58.7, "source": "geonames", "type": "PPL" }, { "name": "Mbem", "latitude": 5.33404, "longitude": 10.42859, "x": 44.8, "y": 57.7, "source": "geonames", "type": "PPL" }, { "name": "Mbing", "latitude": 5.41923, "longitude": 10.41116, "x": 39.6, "y": 15.3, "source": "geonames", "type": "PPL" }, { "name": "Mbou", "latitude": 5.38678, "longitude": 10.36352, "x": 25.6, "y": 31.4, "source": "geonames", "type": "PPL" }, { "name": "Meji", "latitude": 5.40598, "longitude": 10.37033, "x": 27.6, "y": 21.9, "source": "geonames", "type": "PPL" }, { "name": "Meka", "latitude": 5.42173, "longitude": 10.33621, "x": 17.5, "y": 14, "source": "geonames", "type": "PPL" }, { "name": "Melang Mefou", "latitude": 5.37832, "longitude": 10.38505, "x": 31.9, "y": 35.7, "source": "geonames", "type": "PPL" }, { "name": "Mendjo", "latitude": 5.32581, "longitude": 10.4248, "x": 43.6, "y": 61.8, "source": "geonames", "type": "PPL" }, { "name": "Messeng", "latitude": 5.42744, "longitude": 10.37495, "x": 28.9, "y": 11.2, "source": "geonames", "type": "PPL" }, { "name": "Moudjo", "latitude": 5.32008, "longitude": 10.48411, "x": 61.1, "y": 64.7, "source": "geonames", "type": "PPL" }, { "name": "Moutcha", "latitude": 5.29777, "longitude": 10.49838, "x": 65.3, "y": 75.8, "source": "geonames", "type": "PPL" }, { "name": "Mouwe", "latitude": 5.35966, "longitude": 10.39566, "x": 35, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Munka", "latitude": 5.42578, "longitude": 10.34397, "x": 19.8, "y": 12, "source": "geonames", "type": "PPL" }, { "name": "Mvou", "latitude": 5.31476, "longitude": 10.45178, "x": 51.6, "y": 67.4, "source": "geonames", "type": "PPL" }, { "name": "Ndem Mbeng", "latitude": 5.3412, "longitude": 10.41154, "x": 39.7, "y": 54.2, "source": "geonames", "type": "PPL" }, { "name": "Ndembom", "latitude": 5.34992, "longitude": 10.46878, "x": 56.6, "y": 49.8, "source": "geonames", "type": "PPL" }, { "name": "Ndenbou", "latitude": 5.38271, "longitude": 10.37873, "x": 30, "y": 33.5, "source": "geonames", "type": "PPL" }, { "name": "Nding", "latitude": 5.33782, "longitude": 10.46315, "x": 55, "y": 55.9, "source": "geonames", "type": "PPL" }, { "name": "Nepe", "latitude": 5.42908, "longitude": 10.33041, "x": 15.8, "y": 10.4, "source": "geonames", "type": "PPL" }, { "name": "Ngang Fondji", "latitude": 5.28045, "longitude": 10.51109, "x": 69.1, "y": 84.5, "source": "geonames", "type": "PPL" }, { "name": "Ngentoum", "latitude": 5.31667, "longitude": 10.51667, "x": 70.7, "y": 66.4, "source": "geonames", "type": "PPL" }, { "name": "Ngwingang", "latitude": 5.26933, "longitude": 10.48712, "x": 62, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Njeleng", "latitude": 5.41606, "longitude": 10.42529, "x": 43.8, "y": 16.8, "source": "geonames", "type": "PPL" }, { "name": "Njesse", "latitude": 5.3844, "longitude": 10.41295, "x": 40.1, "y": 32.6, "source": "geonames", "type": "PPL" }, { "name": "Njila'", "latitude": 5.41955, "longitude": 10.32692, "x": 14.8, "y": 15.1, "source": "geonames", "type": "PPL" }, { "name": "Njone", "latitude": 5.39717, "longitude": 10.39924, "x": 36.1, "y": 26.3, "source": "geonames", "type": "PPL" }, { "name": "Njout", "latitude": 5.4167, "longitude": 10.35238, "x": 22.3, "y": 16.5, "source": "geonames", "type": "PPL" }, { "name": "Nka'sap", "latitude": 5.30192, "longitude": 10.4697, "x": 56.9, "y": 73.8, "source": "geonames", "type": "PPL" }, { "name": "Nke", "latitude": 5.30716, "longitude": 10.44652, "x": 50, "y": 71.1, "source": "geonames", "type": "PPL" }, { "name": "Ntienki", "latitude": 5.37469, "longitude": 10.38551, "x": 32, "y": 37.5, "source": "geonames", "type": "PPL" }, { "name": "Peng", "latitude": 5.42574, "longitude": 10.35103, "x": 21.9, "y": 12, "source": "geonames", "type": "PPL" }, { "name": "Pete", "latitude": 5.38267, "longitude": 10.42136, "x": 42.6, "y": 33.5, "source": "geonames", "type": "PPL" }, { "name": "Soukpen", "latitude": 5.39481, "longitude": 10.52483, "x": 73.1, "y": 27.4, "source": "geonames", "type": "PPL" }, { "name": "Soung Djesse", "latitude": 5.37051, "longitude": 10.41283, "x": 40.1, "y": 39.6, "source": "geonames", "type": "PPL" }, { "name": "Tchife", "latitude": 5.4298, "longitude": 10.3108, "x": 10, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Tchimneke", "latitude": 5.42624, "longitude": 10.32075, "x": 12.9, "y": 11.8, "source": "geonames", "type": "PPL" }, { "name": "Tem", "latitude": 5.33183, "longitude": 10.4822, "x": 60.6, "y": 58.8, "source": "geonames", "type": "PPL" }, { "name": "Tenjouonoun", "latitude": 5.3799, "longitude": 10.54399, "x": 78.8, "y": 34.9, "source": "geonames", "type": "PPL" }, { "name": "To Tsela", "latitude": 5.39392, "longitude": 10.38549, "x": 32, "y": 27.9, "source": "geonames", "type": "PPL" }, { "name": "To'chom", "latitude": 5.3025, "longitude": 10.47893, "x": 59.6, "y": 73.5, "source": "geonames", "type": "PPL" }, { "name": "To'mlem", "latitude": 5.42446, "longitude": 10.43283, "x": 46, "y": 12.7, "source": "geonames", "type": "PPL" }, { "name": "Toba", "latitude": 5.41478, "longitude": 10.45158, "x": 51.5, "y": 17.5, "source": "geonames", "type": "PPL" }, { "name": "Tode", "latitude": 5.40025, "longitude": 10.35317, "x": 22.5, "y": 24.7, "source": "geonames", "type": "PPL" }, { "name": "Tonkwo", "latitude": 5.36267, "longitude": 10.4428, "x": 48.9, "y": 43.5, "source": "geonames", "type": "PPL" }, { "name": "Tounisi", "latitude": 5.37341, "longitude": 10.50547, "x": 67.4, "y": 38.1, "source": "geonames", "type": "PPL" }, { "name": "Tse Ndembom", "latitude": 5.33497, "longitude": 10.48276, "x": 60.7, "y": 57.3, "source": "geonames", "type": "PPL" }, { "name": "Tsego", "latitude": 5.40733, "longitude": 10.45973, "x": 53.9, "y": 21.2, "source": "geonames", "type": "PPL" }, { "name": "Tseguem", "latitude": 5.3794, "longitude": 10.43643, "x": 47.1, "y": 35.1, "source": "geonames", "type": "PPL" }, { "name": "Tsela'", "latitude": 5.38964, "longitude": 10.38838, "x": 32.9, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Tseleng", "latitude": 5.3587, "longitude": 10.41675, "x": 41.3, "y": 45.4, "source": "geonames", "type": "PPL" }, { "name": "Tsesso", "latitude": 5.41352, "longitude": 10.39244, "x": 34.1, "y": 18.1, "source": "geonames", "type": "PPL" }, { "name": "Vele", "latitude": 5.34298, "longitude": 10.45945, "x": 53.9, "y": 53.3, "source": "geonames", "type": "PPL" }, { "name": "Yogam", "latitude": 5.29568, "longitude": 10.45889, "x": 53.7, "y": 76.9, "source": "geonames", "type": "PPL" }, { "name": "Yom", "latitude": 5.3978, "longitude": 10.42279, "x": 43, "y": 26, "source": "geonames", "type": "PPL" } ], "Bangangte": [ { "name": "Babei", "latitude": 5.13187, "longitude": 10.42505, "x": 15.9, "y": 54.2, "source": "geonames", "type": "PPL" }, { "name": "Babou'", "latitude": 5.09206, "longitude": 10.58352, "x": 72.2, "y": 67.9, "source": "geonames", "type": "PPL" }, { "name": "Babou' I", "latitude": 5.09597, "longitude": 10.59042, "x": 74.7, "y": 66.6, "source": "geonames", "type": "PPL" }, { "name": "Babou' II", "latitude": 5.09241, "longitude": 10.57645, "x": 69.7, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Bahouok", "latitude": 5.11667, "longitude": 10.51667, "x": 48.5, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Bakong", "latitude": 5.09286, "longitude": 10.4895, "x": 38.8, "y": 67.7, "source": "geonames", "type": "PPL" }, { "name": "Bakotchou", "latitude": 5.04425, "longitude": 10.49753, "x": 41.6, "y": 84.5, "source": "geonames", "type": "PPL" }, { "name": "Balengou", "latitude": 5.12209, "longitude": 10.4625, "x": 29.2, "y": 57.6, "source": "geonames", "type": "PPL" }, { "name": "Bamena", "latitude": 5.15976, "longitude": 10.43708, "x": 20.1, "y": 44.6, "source": "geonames", "type": "PPL" }, { "name": "Banah", "latitude": 5.03705, "longitude": 10.48952, "x": 38.8, "y": 86.9, "source": "geonames", "type": "PPL" }, { "name": "Bandouan II", "latitude": 5.09615, "longitude": 10.41058, "x": 10.7, "y": 66.5, "source": "geonames", "type": "PPL" }, { "name": "Bandounno-Fongoue", "latitude": 5.17249, "longitude": 10.49716, "x": 41.5, "y": 40.2, "source": "geonames", "type": "PPL" }, { "name": "Bandrefam", "latitude": 5.24073, "longitude": 10.49039, "x": 39.1, "y": 16.6, "source": "geonames", "type": "PPL" }, { "name": "Banenkeng", "latitude": 5.12774, "longitude": 10.57798, "x": 70.3, "y": 55.6, "source": "geonames", "type": "PPL" }, { "name": "Bangang Fokam", "latitude": 5.25079, "longitude": 10.51554, "x": 48.1, "y": 13.2, "source": "geonames", "type": "PPL" }, { "name": "Bassamba", "latitude": 5.05436, "longitude": 10.57121, "x": 67.9, "y": 81, "source": "geonames", "type": "PPL" }, { "name": "Bato", "latitude": 5.1568, "longitude": 10.47556, "x": 33.8, "y": 45.6, "source": "geonames", "type": "PPL" }, { "name": "Bawok", "latitude": 5.10986, "longitude": 10.49125, "x": 39.4, "y": 61.8, "source": "geonames", "type": "PPL" }, { "name": "Bazou", "latitude": 5.06001, "longitude": 10.46751, "x": 31, "y": 79, "source": "geonames", "type": "PPL" }, { "name": "Boba", "latitude": 5.11097, "longitude": 10.56184, "x": 64.5, "y": 61.4, "source": "geonames", "type": "PPL" }, { "name": "Boba II", "latitude": 5.11137, "longitude": 10.5586, "x": 63.4, "y": 61.3, "source": "geonames", "type": "PPL" }, { "name": "Bole", "latitude": 5.10538, "longitude": 10.51334, "x": 47.3, "y": 63.4, "source": "geonames", "type": "PPL" }, { "name": "Bongo", "latitude": 5.10349, "longitude": 10.53952, "x": 56.6, "y": 64, "source": "geonames", "type": "PPL" }, { "name": "Bonke", "latitude": 5.09626, "longitude": 10.52739, "x": 52.3, "y": 66.5, "source": "geonames", "type": "PPL" }, { "name": "Bonzoue", "latitude": 5.11068, "longitude": 10.44038, "x": 21.3, "y": 61.5, "source": "geonames", "type": "PPL" }, { "name": "Bou'leng", "latitude": 5.0473, "longitude": 10.57093, "x": 67.8, "y": 83.4, "source": "geonames", "type": "PPL" }, { "name": "Chouplang", "latitude": 5.17186, "longitude": 10.40958, "x": 10.4, "y": 40.4, "source": "geonames", "type": "PPL" }, { "name": "Dengoue", "latitude": 5.12095, "longitude": 10.42997, "x": 17.6, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Dengoue II", "latitude": 5.12608, "longitude": 10.4476, "x": 23.9, "y": 56.2, "source": "geonames", "type": "PPL" }, { "name": "Dipfad", "latitude": 5.08447, "longitude": 10.46981, "x": 31.8, "y": 70.6, "source": "geonames", "type": "PPL" }, { "name": "Fambe", "latitude": 5.14232, "longitude": 10.57739, "x": 70.1, "y": 50.6, "source": "geonames", "type": "PPL" }, { "name": "Fametcha'", "latitude": 5.19797, "longitude": 10.50187, "x": 43.2, "y": 31.4, "source": "geonames", "type": "PPL" }, { "name": "Famgo Neta'", "latitude": 5.12698, "longitude": 10.51993, "x": 49.6, "y": 55.9, "source": "geonames", "type": "PPL" }, { "name": "Famlem", "latitude": 5.18892, "longitude": 10.41254, "x": 11.4, "y": 34.5, "source": "geonames", "type": "PPL" }, { "name": "Famlouh", "latitude": 5.19299, "longitude": 10.43377, "x": 19, "y": 33.1, "source": "geonames", "type": "PPL" }, { "name": "Famtchwet", "latitude": 5.09905, "longitude": 10.5146, "x": 47.7, "y": 65.5, "source": "geonames", "type": "PPL" }, { "name": "Farmetcha", "latitude": 5.18095, "longitude": 10.54199, "x": 57.5, "y": 37.3, "source": "geonames", "type": "PPL" }, { "name": "Fembat", "latitude": 5.16204, "longitude": 10.50715, "x": 45.1, "y": 43.8, "source": "geonames", "type": "PPL" }, { "name": "Fendja", "latitude": 5.15726, "longitude": 10.53182, "x": 53.8, "y": 45.4, "source": "geonames", "type": "PPL" }, { "name": "Fozou", "latitude": 5.12267, "longitude": 10.45146, "x": 25.3, "y": 57.4, "source": "geonames", "type": "PPL" }, { "name": "Ka'fen", "latitude": 5.09685, "longitude": 10.61431, "x": 83.2, "y": 66.3, "source": "geonames", "type": "PPL" }, { "name": "Ka'tsie", "latitude": 5.09069, "longitude": 10.4416, "x": 21.8, "y": 68.4, "source": "geonames", "type": "PPL" }, { "name": "Kafokseu", "latitude": 5.12578, "longitude": 10.46736, "x": 30.9, "y": 56.3, "source": "geonames", "type": "PPL" }, { "name": "Kamdam", "latitude": 5.11892, "longitude": 10.56754, "x": 66.6, "y": 58.7, "source": "geonames", "type": "PPL" }, { "name": "Kamna", "latitude": 5.23824, "longitude": 10.456, "x": 26.9, "y": 17.5, "source": "geonames", "type": "PPL" }, { "name": "Kasang", "latitude": 5.12232, "longitude": 10.40856, "x": 10, "y": 57.5, "source": "geonames", "type": "PPL" }, { "name": "Kicha", "latitude": 5.08153, "longitude": 10.45469, "x": 26.4, "y": 71.6, "source": "geonames", "type": "PPL" }, { "name": "Kopdna", "latitude": 5.09071, "longitude": 10.51425, "x": 47.6, "y": 68.4, "source": "geonames", "type": "PPL" }, { "name": "Kopking", "latitude": 5.09351, "longitude": 10.50243, "x": 43.4, "y": 67.4, "source": "geonames", "type": "PPL" }, { "name": "Kouang", "latitude": 5.06667, "longitude": 10.5, "x": 42.5, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Koudjou", "latitude": 5.10954, "longitude": 10.42728, "x": 16.7, "y": 61.9, "source": "geonames", "type": "PPL" }, { "name": "La'ving", "latitude": 5.10799, "longitude": 10.50561, "x": 44.5, "y": 62.5, "source": "geonames", "type": "PPL" }, { "name": "Lekwa", "latitude": 5.25996, "longitude": 10.48936, "x": 38.7, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Louh", "latitude": 5.16117, "longitude": 10.42552, "x": 16, "y": 44.1, "source": "geonames", "type": "PPL" }, { "name": "Madoum", "latitude": 5.11333, "longitude": 10.53883, "x": 56.3, "y": 60.6, "source": "geonames", "type": "PPL" }, { "name": "Madoum II", "latitude": 5.11551, "longitude": 10.53795, "x": 56, "y": 59.9, "source": "geonames", "type": "PPL" }, { "name": "Mambit", "latitude": 5.10262, "longitude": 10.63174, "x": 89.4, "y": 64.3, "source": "geonames", "type": "PPL" }, { "name": "Manfi", "latitude": 5.10631, "longitude": 10.52897, "x": 52.8, "y": 63, "source": "geonames", "type": "PPL" }, { "name": "Mango'", "latitude": 5.10425, "longitude": 10.60705, "x": 80.6, "y": 63.7, "source": "geonames", "type": "PPL" }, { "name": "Mangou", "latitude": 5.1874, "longitude": 10.57294, "x": 68.5, "y": 35, "source": "geonames", "type": "PPL" }, { "name": "Manko", "latitude": 5.1619, "longitude": 10.56482, "x": 65.6, "y": 43.8, "source": "geonames", "type": "PPL" }, { "name": "Matam", "latitude": 5.11457, "longitude": 10.61887, "x": 84.8, "y": 60.2, "source": "geonames", "type": "PPL" }, { "name": "Mbou", "latitude": 5.10967, "longitude": 10.45673, "x": 27.1, "y": 61.9, "source": "geonames", "type": "PPL" }, { "name": "Mbou'di", "latitude": 5.04203, "longitude": 10.56975, "x": 67.3, "y": 85.2, "source": "geonames", "type": "PPL" }, { "name": "Mbou'ndang", "latitude": 5.02817, "longitude": 10.48761, "x": 38.1, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Mene", "latitude": 5.08761, "longitude": 10.53866, "x": 56.3, "y": 69.5, "source": "geonames", "type": "PPL" }, { "name": "Metcha", "latitude": 5.18962, "longitude": 10.53585, "x": 55.3, "y": 34.3, "source": "geonames", "type": "PPL" }, { "name": "Mvetchwet", "latitude": 5.12748, "longitude": 10.48781, "x": 38.2, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Nampe", "latitude": 5.15849, "longitude": 10.48952, "x": 38.8, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Ndandut", "latitude": 5.19655, "longitude": 10.54666, "x": 59.1, "y": 31.9, "source": "geonames", "type": "PPL" }, { "name": "Ndepnyo", "latitude": 5.20664, "longitude": 10.53429, "x": 54.7, "y": 28.4, "source": "geonames", "type": "PPL" }, { "name": "Ndeptoup", "latitude": 5.20093, "longitude": 10.45262, "x": 25.7, "y": 30.4, "source": "geonames", "type": "PPL" }, { "name": "Ndia'nse", "latitude": 5.1753, "longitude": 10.50434, "x": 44.1, "y": 39.2, "source": "geonames", "type": "PPL" }, { "name": "Ndja", "latitude": 5.08882, "longitude": 10.41517, "x": 12.4, "y": 69.1, "source": "geonames", "type": "PPL" }, { "name": "Ndjuijong", "latitude": 5.03333, "longitude": 10.53333, "x": 54.4, "y": 88.2, "source": "geonames", "type": "PPL" }, { "name": "Ndou'ndou", "latitude": 5.15494, "longitude": 10.56779, "x": 66.6, "y": 46.2, "source": "geonames", "type": "PPL" }, { "name": "Ndoue", "latitude": 5.18482, "longitude": 10.4237, "x": 15.4, "y": 35.9, "source": "geonames", "type": "PPL" }, { "name": "Ndoumba", "latitude": 5.04652, "longitude": 10.56066, "x": 64.1, "y": 83.7, "source": "geonames", "type": "PPL" }, { "name": "Ndounko", "latitude": 5.21032, "longitude": 10.45571, "x": 26.8, "y": 27.1, "source": "geonames", "type": "PPL" }, { "name": "Ndresseu", "latitude": 5.18295, "longitude": 10.49396, "x": 40.4, "y": 36.6, "source": "geonames", "type": "PPL" }, { "name": "Nekan", "latitude": 5.12583, "longitude": 10.57943, "x": 70.8, "y": 56.3, "source": "geonames", "type": "PPL" }, { "name": "Nekan II", "latitude": 5.12631, "longitude": 10.58618, "x": 73.2, "y": 56.1, "source": "geonames", "type": "PPL" }, { "name": "Nekwan", "latitude": 5.11155, "longitude": 10.55253, "x": 61.2, "y": 61.2, "source": "geonames", "type": "PPL" }, { "name": "Nekwan II", "latitude": 5.10568, "longitude": 10.54842, "x": 59.8, "y": 63.2, "source": "geonames", "type": "PPL" }, { "name": "Nenga", "latitude": 5.13018, "longitude": 10.54842, "x": 59.8, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Nessa", "latitude": 5.14678, "longitude": 10.55998, "x": 63.9, "y": 49.1, "source": "geonames", "type": "PPL" }, { "name": "Netam", "latitude": 5.13059, "longitude": 10.58611, "x": 73.2, "y": 54.7, "source": "geonames", "type": "PPL" }, { "name": "Ngangte", "latitude": 5.1431, "longitude": 10.54236, "x": 57.6, "y": 50.3, "source": "geonames", "type": "PPL" }, { "name": "Ngondjou", "latitude": 5.09517, "longitude": 10.45643, "x": 27, "y": 66.9, "source": "geonames", "type": "PPL" }, { "name": "Ngoulap", "latitude": 5.10715, "longitude": 10.53876, "x": 56.3, "y": 62.7, "source": "geonames", "type": "PPL" }, { "name": "Ngwa", "latitude": 5.20309, "longitude": 10.4891, "x": 38.6, "y": 29.6, "source": "geonames", "type": "PPL" }, { "name": "Njeuta II", "latitude": 5.06947, "longitude": 10.46154, "x": 28.8, "y": 75.7, "source": "geonames", "type": "PPL" }, { "name": "Njinko", "latitude": 5.12158, "longitude": 10.60206, "x": 78.8, "y": 57.8, "source": "geonames", "type": "PPL" }, { "name": "Nkeke", "latitude": 5.1957, "longitude": 10.427, "x": 16.6, "y": 32.2, "source": "geonames", "type": "PPL" }, { "name": "Nlouo", "latitude": 5.21212, "longitude": 10.49842, "x": 42, "y": 26.5, "source": "geonames", "type": "PPL" }, { "name": "Noumga", "latitude": 5.15724, "longitude": 10.55848, "x": 63.3, "y": 45.5, "source": "geonames", "type": "PPL" }, { "name": "Ntankou", "latitude": 5.06011, "longitude": 10.57133, "x": 67.9, "y": 79, "source": "geonames", "type": "PPL" }, { "name": "Nyambe", "latitude": 5.09018, "longitude": 10.63115, "x": 89.2, "y": 68.6, "source": "geonames", "type": "PPL" }, { "name": "Nyambo", "latitude": 5.05173, "longitude": 10.56568, "x": 65.9, "y": 81.9, "source": "geonames", "type": "PPL" }, { "name": "Nyamga'", "latitude": 5.08836, "longitude": 10.55153, "x": 60.9, "y": 69.2, "source": "geonames", "type": "PPL" }, { "name": "Nyu", "latitude": 5.11151, "longitude": 10.58423, "x": 72.5, "y": 61.2, "source": "geonames", "type": "PPL" }, { "name": "Nyu II", "latitude": 5.11877, "longitude": 10.58198, "x": 71.7, "y": 58.7, "source": "geonames", "type": "PPL" }, { "name": "Nzou", "latitude": 5.07069, "longitude": 10.45143, "x": 25.2, "y": 75.3, "source": "geonames", "type": "PPL" }, { "name": "Sa'nya", "latitude": 5.088, "longitude": 10.49029, "x": 39.1, "y": 69.4, "source": "geonames", "type": "PPL" }, { "name": "Sanda", "latitude": 5.14569, "longitude": 10.58277, "x": 72, "y": 49.4, "source": "geonames", "type": "PPL" }, { "name": "Sanya", "latitude": 5.0915, "longitude": 10.52627, "x": 51.9, "y": 68.1, "source": "geonames", "type": "PPL" }, { "name": "Tchila", "latitude": 5.24707, "longitude": 10.52245, "x": 50.5, "y": 14.4, "source": "geonames", "type": "PPL" }, { "name": "Tchitchi", "latitude": 5.19943, "longitude": 10.42187, "x": 14.7, "y": 30.9, "source": "geonames", "type": "PPL" }, { "name": "Tchoundim", "latitude": 5.11855, "longitude": 10.61222, "x": 82.4, "y": 58.8, "source": "geonames", "type": "PPL" }, { "name": "Tchouplan", "latitude": 5.15595, "longitude": 10.45564, "x": 26.7, "y": 45.9, "source": "geonames", "type": "PPL" }, { "name": "Tela", "latitude": 5.13796, "longitude": 10.55715, "x": 62.9, "y": 52.1, "source": "geonames", "type": "PPL" }, { "name": "Toksi", "latitude": 5.15889, "longitude": 10.55059, "x": 60.5, "y": 44.9, "source": "geonames", "type": "PPL" }, { "name": "Tombou", "latitude": 5.10049, "longitude": 10.4365, "x": 19.9, "y": 65, "source": "geonames", "type": "PPL" }, { "name": "Toudatchuet", "latitude": 5.18601, "longitude": 10.5265, "x": 52, "y": 35.5, "source": "geonames", "type": "PPL" }, { "name": "Toukop", "latitude": 5.1595, "longitude": 10.54002, "x": 56.8, "y": 44.7, "source": "geonames", "type": "PPL" }, { "name": "Toungwi", "latitude": 5.2022, "longitude": 10.51159, "x": 46.6, "y": 29.9, "source": "geonames", "type": "PPL" }, { "name": "Tounse", "latitude": 5.13788, "longitude": 10.63346, "x": 90, "y": 52.1, "source": "geonames", "type": "PPL" }, { "name": "Tsela", "latitude": 5.21943, "longitude": 10.44237, "x": 22, "y": 24, "source": "geonames", "type": "PPL" }, { "name": "Wok", "latitude": 5.11524, "longitude": 10.50562, "x": 44.5, "y": 59.9, "source": "geonames", "type": "PPL" }, { "name": "Yavou", "latitude": 5.17922, "longitude": 10.52208, "x": 50.4, "y": 37.9, "source": "geonames", "type": "PPL" } ], "Bangourain": [ { "name": "Bambalang", "latitude": 5.8868, "longitude": 10.53314, "x": 23.2, "y": 36.6, "source": "geonames", "type": "PPL" }, { "name": "Bancoupen", "latitude": 5.86667, "longitude": 10.66667, "x": 62.3, "y": 42.2, "source": "geonames", "type": "PPL" }, { "name": "Bangambi", "latitude": 5.84294, "longitude": 10.73393, "x": 82, "y": 48.9, "source": "geonames", "type": "PPL" }, { "name": "Bangola", "latitude": 5.94651, "longitude": 10.62192, "x": 49.2, "y": 19.9, "source": "geonames", "type": "PPL" }, { "name": "Bangouren", "latitude": 5.90177, "longitude": 10.65637, "x": 59.3, "y": 32.4, "source": "geonames", "type": "PPL" }, { "name": "Banza", "latitude": 5.77588, "longitude": 10.51454, "x": 17.8, "y": 67.7, "source": "geonames", "type": "PPL" }, { "name": "Bendon", "latitude": 5.75149, "longitude": 10.53468, "x": 23.7, "y": 74.5, "source": "geonames", "type": "PPL" }, { "name": "Bendong", "latitude": 5.74806, "longitude": 10.51798, "x": 18.8, "y": 75.5, "source": "geonames", "type": "PPL" }, { "name": "Choshimbere", "latitude": 5.92376, "longitude": 10.62321, "x": 49.6, "y": 26.2, "source": "geonames", "type": "PPL" }, { "name": "Fembenjo", "latitude": 5.77888, "longitude": 10.61119, "x": 46.1, "y": 66.8, "source": "geonames", "type": "PPL" }, { "name": "Fouoya", "latitude": 5.73575, "longitude": 10.48806, "x": 10, "y": 78.9, "source": "geonames", "type": "PPL" }, { "name": "Garap", "latitude": 5.87273, "longitude": 10.67521, "x": 64.8, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Gomalem", "latitude": 5.77781, "longitude": 10.56732, "x": 33.2, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Ketebe", "latitude": 5.72863, "longitude": 10.60419, "x": 44, "y": 80.9, "source": "geonames", "type": "PPL" }, { "name": "Kpembwa", "latitude": 5.78489, "longitude": 10.68996, "x": 69.1, "y": 65.1, "source": "geonames", "type": "PPL" }, { "name": "Kumbit", "latitude": 5.85711, "longitude": 10.52887, "x": 22, "y": 44.9, "source": "geonames", "type": "PPL" }, { "name": "Maki", "latitude": 5.84641, "longitude": 10.7158, "x": 76.7, "y": 47.9, "source": "geonames", "type": "PPL" }, { "name": "Makulung", "latitude": 5.95453, "longitude": 10.62578, "x": 50.3, "y": 17.6, "source": "geonames", "type": "PPL" }, { "name": "Mambay", "latitude": 5.78106, "longitude": 10.66555, "x": 62, "y": 66.2, "source": "geonames", "type": "PPL" }, { "name": "Manseng", "latitude": 5.80608, "longitude": 10.67778, "x": 65.6, "y": 59.2, "source": "geonames", "type": "PPL" }, { "name": "Mapektieu", "latitude": 5.84581, "longitude": 10.69303, "x": 70, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Mayit", "latitude": 5.78634, "longitude": 10.65159, "x": 57.9, "y": 64.7, "source": "geonames", "type": "PPL" }, { "name": "Mban Kong", "latitude": 5.89703, "longitude": 10.54121, "x": 25.6, "y": 33.7, "source": "geonames", "type": "PPL" }, { "name": "Mbapeshi", "latitude": 5.87956, "longitude": 10.53181, "x": 22.8, "y": 38.6, "source": "geonames", "type": "PPL" }, { "name": "Mbashie", "latitude": 5.85209, "longitude": 10.51672, "x": 18.4, "y": 46.3, "source": "geonames", "type": "PPL" }, { "name": "Mbato", "latitude": 5.88969, "longitude": 10.53799, "x": 24.6, "y": 35.8, "source": "geonames", "type": "PPL" }, { "name": "Mbefou", "latitude": 5.80969, "longitude": 10.55467, "x": 29.5, "y": 58.2, "source": "geonames", "type": "PPL" }, { "name": "Mbissa", "latitude": 5.78878, "longitude": 10.59384, "x": 41, "y": 64.1, "source": "geonames", "type": "PPL" }, { "name": "Mbissaka", "latitude": 5.84497, "longitude": 10.57376, "x": 35.1, "y": 48.3, "source": "geonames", "type": "PPL" }, { "name": "Mbumto", "latitude": 5.93883, "longitude": 10.6311, "x": 51.9, "y": 22, "source": "geonames", "type": "PPL" }, { "name": "Mengba", "latitude": 5.78472, "longitude": 10.61356, "x": 46.8, "y": 65.2, "source": "geonames", "type": "PPL" }, { "name": "Meta", "latitude": 5.85677, "longitude": 10.7369, "x": 82.9, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Mfenguem", "latitude": 5.7108, "longitude": 10.61855, "x": 48.2, "y": 85.9, "source": "geonames", "type": "PPL" }, { "name": "Mfonka", "latitude": 5.77914, "longitude": 10.59532, "x": 41.4, "y": 66.8, "source": "geonames", "type": "PPL" }, { "name": "Munwa", "latitude": 5.96778, "longitude": 10.58312, "x": 37.8, "y": 13.9, "source": "geonames", "type": "PPL" }, { "name": "Ndouwoum", "latitude": 5.77046, "longitude": 10.60511, "x": 44.3, "y": 69.2, "source": "geonames", "type": "PPL" }, { "name": "Ngbawarem", "latitude": 5.75648, "longitude": 10.705, "x": 73.6, "y": 73.1, "source": "geonames", "type": "PPL" }, { "name": "Ngon Njitapon", "latitude": 5.77051, "longitude": 10.59895, "x": 42.5, "y": 69.2, "source": "geonames", "type": "PPL" }, { "name": "Nguediem", "latitude": 5.90747, "longitude": 10.66669, "x": 62.3, "y": 30.8, "source": "geonames", "type": "PPL" }, { "name": "Ngwenfo", "latitude": 5.83889, "longitude": 10.66428, "x": 61.6, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Ngwenfoghe", "latitude": 5.87498, "longitude": 10.66838, "x": 62.8, "y": 39.9, "source": "geonames", "type": "PPL" }, { "name": "Ngwenfon", "latitude": 5.92507, "longitude": 10.65779, "x": 59.7, "y": 25.9, "source": "geonames", "type": "PPL" }, { "name": "Njibam", "latitude": 5.88638, "longitude": 10.68945, "x": 69, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Njikan", "latitude": 5.8306, "longitude": 10.74224, "x": 84.5, "y": 52.3, "source": "geonames", "type": "PPL" }, { "name": "Njikepou", "latitude": 5.73045, "longitude": 10.62718, "x": 50.8, "y": 80.4, "source": "geonames", "type": "PPL" }, { "name": "Njikwop", "latitude": 5.76449, "longitude": 10.65037, "x": 57.5, "y": 70.9, "source": "geonames", "type": "PPL" }, { "name": "Njimbet", "latitude": 5.82374, "longitude": 10.72455, "x": 79.3, "y": 54.3, "source": "geonames", "type": "PPL" }, { "name": "Njimgbet", "latitude": 5.70044, "longitude": 10.60141, "x": 43.2, "y": 88.8, "source": "geonames", "type": "PPL" }, { "name": "Njinga", "latitude": 5.75024, "longitude": 10.60166, "x": 43.3, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Njingban", "latitude": 5.82636, "longitude": 10.73226, "x": 81.5, "y": 53.5, "source": "geonames", "type": "PPL" }, { "name": "Njingwen", "latitude": 5.7431, "longitude": 10.73123, "x": 81.2, "y": 76.9, "source": "geonames", "type": "PPL" }, { "name": "Njinkwen", "latitude": 5.83588, "longitude": 10.73302, "x": 81.8, "y": 50.9, "source": "geonames", "type": "PPL" }, { "name": "Njinsen", "latitude": 5.73102, "longitude": 10.59364, "x": 40.9, "y": 80.2, "source": "geonames", "type": "PPL" }, { "name": "Njitam", "latitude": 5.86339, "longitude": 10.7576, "x": 89, "y": 43.1, "source": "geonames", "type": "PPL" }, { "name": "Njitet", "latitude": 5.89821, "longitude": 10.6886, "x": 68.7, "y": 33.4, "source": "geonames", "type": "PPL" }, { "name": "Njiyanguem", "latitude": 5.73804, "longitude": 10.63333, "x": 52.6, "y": 78.3, "source": "geonames", "type": "PPL" }, { "name": "Nkaten", "latitude": 5.86456, "longitude": 10.65573, "x": 59.1, "y": 42.8, "source": "geonames", "type": "PPL" }, { "name": "Nkessie", "latitude": 5.82477, "longitude": 10.51208, "x": 17, "y": 54, "source": "geonames", "type": "PPL" }, { "name": "Nkotokwop", "latitude": 5.75411, "longitude": 10.69003, "x": 69.2, "y": 73.8, "source": "geonames", "type": "PPL" }, { "name": "Nkoumangba", "latitude": 5.78615, "longitude": 10.62955, "x": 51.4, "y": 64.8, "source": "geonames", "type": "PPL" }, { "name": "Nkoumbam", "latitude": 5.8596, "longitude": 10.73252, "x": 81.6, "y": 44.2, "source": "geonames", "type": "PPL" }, { "name": "Nkounamekwop", "latitude": 5.75666, "longitude": 10.64759, "x": 56.7, "y": 73.1, "source": "geonames", "type": "PPL" }, { "name": "Nkouncha'", "latitude": 5.69617, "longitude": 10.62598, "x": 50.4, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Nkounden", "latitude": 5.7031, "longitude": 10.66639, "x": 62.2, "y": 88.1, "source": "geonames", "type": "PPL" }, { "name": "Nkouonnja", "latitude": 5.73693, "longitude": 10.62321, "x": 49.6, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Nkoupamenke", "latitude": 5.7338, "longitude": 10.74426, "x": 85.1, "y": 79.5, "source": "geonames", "type": "PPL" }, { "name": "Nkourom", "latitude": 5.76394, "longitude": 10.69561, "x": 70.8, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Nkourom-Centre", "latitude": 5.7747, "longitude": 10.64959, "x": 57.3, "y": 68, "source": "geonames", "type": "PPL" }, { "name": "Nkoutou Ngwen", "latitude": 5.88653, "longitude": 10.76115, "x": 90, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Nkoutoungwen", "latitude": 5.73359, "longitude": 10.74003, "x": 83.8, "y": 79.5, "source": "geonames", "type": "PPL" }, { "name": "Nkoutoupi", "latitude": 5.98168, "longitude": 10.68632, "x": 68.1, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Nkoutourom", "latitude": 5.74466, "longitude": 10.69629, "x": 71, "y": 76.4, "source": "geonames", "type": "PPL" }, { "name": "Nkwat", "latitude": 5.7889, "longitude": 10.71315, "x": 75.9, "y": 64, "source": "geonames", "type": "PPL" }, { "name": "Nkwen", "latitude": 5.71125, "longitude": 10.61028, "x": 45.8, "y": 85.8, "source": "geonames", "type": "PPL" }, { "name": "Nkwenja", "latitude": 5.76644, "longitude": 10.60566, "x": 44.5, "y": 70.3, "source": "geonames", "type": "PPL" }, { "name": "Nkwetenjou", "latitude": 5.76473, "longitude": 10.5975, "x": 42.1, "y": 70.8, "source": "geonames", "type": "PPL" }, { "name": "Nkwichong", "latitude": 5.77166, "longitude": 10.73429, "x": 82.1, "y": 68.8, "source": "geonames", "type": "PPL" }, { "name": "Nomkoup", "latitude": 5.74333, "longitude": 10.6198, "x": 48.6, "y": 76.8, "source": "geonames", "type": "PPL" }, { "name": "Pagha", "latitude": 5.85846, "longitude": 10.68633, "x": 68.1, "y": 44.5, "source": "geonames", "type": "PPL" }, { "name": "Sanyere", "latitude": 5.89658, "longitude": 10.75629, "x": 88.6, "y": 33.8, "source": "geonames", "type": "PPL" } ], "Bankim": [ { "name": "Bandam (new)", "latitude": 6.17093, "longitude": 11.56294, "x": 76.3, "y": 39.8, "source": "geonames", "type": "PPL" }, { "name": "Bandam, ancien", "latitude": 6.15, "longitude": 11.45, "x": 34.6, "y": 45.4, "source": "geonames", "type": "PPL" }, { "name": "Beng-Beng", "latitude": 5.98333, "longitude": 11.51667, "x": 59.2, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ble", "latitude": 6.21667, "longitude": 11.43333, "x": 28.5, "y": 27.6, "source": "geonames", "type": "PPL" }, { "name": "Kol", "latitude": 6.25, "longitude": 11.6, "x": 90, "y": 18.7, "source": "geonames", "type": "PPL" }, { "name": "Kongui", "latitude": 6.15, "longitude": 11.56667, "x": 77.7, "y": 45.4, "source": "geonames", "type": "PPL" }, { "name": "Matseri", "latitude": 6.01667, "longitude": 11.5, "x": 53.1, "y": 81.1, "source": "geonames", "type": "PPL" }, { "name": "Mvesson", "latitude": 6.1, "longitude": 11.41667, "x": 22.3, "y": 58.8, "source": "geonames", "type": "PPL" }, { "name": "Ndoum Djandi", "latitude": 6.25, "longitude": 11.46667, "x": 40.8, "y": 18.7, "source": "geonames", "type": "PPL" }, { "name": "Ngati", "latitude": 6.06667, "longitude": 11.38333, "x": 10, "y": 67.7, "source": "geonames", "type": "PPL" }, { "name": "Nyamboya", "latitude": 6.28254, "longitude": 11.57667, "x": 81.4, "y": 10, "source": "geonames", "type": "PPL" } ], "Banyo": [ { "name": "Amadjoura", "latitude": 6.6, "longitude": 11.71667, "x": 30.8, "y": 86.2, "source": "geonames", "type": "PPL" }, { "name": "Bodeo", "latitude": 6.85, "longitude": 11.7, "x": 27.3, "y": 29, "source": "geonames", "type": "PPL" }, { "name": "Bounjoukoura", "latitude": 6.85, "longitude": 11.61667, "x": 10, "y": 29, "source": "geonames", "type": "PPL" }, { "name": "Dembesse", "latitude": 6.85, "longitude": 11.65, "x": 16.9, "y": 29, "source": "geonames", "type": "PPL" }, { "name": "Djiberou", "latitude": 6.78048, "longitude": 12.00182, "x": 90, "y": 44.9, "source": "geonames", "type": "PPL" }, { "name": "Gamti", "latitude": 6.93333, "longitude": 11.71667, "x": 30.8, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Gandoua", "latitude": 6.93333, "longitude": 11.71667, "x": 30.8, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Labare", "latitude": 6.71667, "longitude": 11.91667, "x": 72.3, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Laman", "latitude": 6.91667, "longitude": 11.88333, "x": 65.4, "y": 13.8, "source": "geonames", "type": "PPL" }, { "name": "Ma Banyo", "latitude": 6.86667, "longitude": 11.81667, "x": 51.5, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Malam Adamou", "latitude": 6.6, "longitude": 11.85, "x": 58.4, "y": 86.2, "source": "geonames", "type": "PPL" }, { "name": "Mansourou", "latitude": 6.6, "longitude": 11.83333, "x": 55, "y": 86.2, "source": "geonames", "type": "PPL" }, { "name": "Mayo Banyo", "latitude": 6.81667, "longitude": 11.95, "x": 79.2, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Mayo Bodeo", "latitude": 6.78333, "longitude": 11.7, "x": 27.3, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Mayo Boutare", "latitude": 6.58333, "longitude": 11.8, "x": 48.1, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ndipele", "latitude": 6.88333, "longitude": 11.98333, "x": 86.1, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Ngoum Petel", "latitude": 6.66667, "longitude": 11.66667, "x": 20.4, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Nlamkoumi", "latitude": 6.80192, "longitude": 12.00198, "x": 90, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Sakoude", "latitude": 6.91667, "longitude": 11.7, "x": 27.3, "y": 13.8, "source": "geonames", "type": "PPL" }, { "name": "Toukouroua", "latitude": 6.63333, "longitude": 11.68333, "x": 23.8, "y": 78.6, "source": "geonames", "type": "PPL" } ], "Batibo": [ { "name": "Abebung", "latitude": 5.938, "longitude": 9.8412, "x": 55.5, "y": 27.4, "source": "geonames", "type": "PPL" }, { "name": "Abeshia", "latitude": 6.0034, "longitude": 9.767, "x": 37.6, "y": 12.6, "source": "geonames", "type": "PPL" }, { "name": "Abeshom", "latitude": 5.7598, "longitude": 9.8853, "x": 66.1, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Achama", "latitude": 5.9523, "longitude": 9.7407, "x": 31.3, "y": 24.2, "source": "geonames", "type": "PPL" }, { "name": "Achang", "latitude": 5.9953, "longitude": 9.7905, "x": 43.3, "y": 14.4, "source": "geonames", "type": "PPL" }, { "name": "Afu", "latitude": 5.8212, "longitude": 9.72, "x": 26.3, "y": 53.9, "source": "geonames", "type": "PPL" }, { "name": "Agwofon", "latitude": 5.73333, "longitude": 9.95, "x": 81.6, "y": 73.8, "source": "geonames", "type": "PPL" }, { "name": "Aje", "latitude": 5.8325, "longitude": 9.9312, "x": 77.1, "y": 51.4, "source": "geonames", "type": "PPL" }, { "name": "Ajei", "latitude": 5.9415, "longitude": 9.8668, "x": 61.6, "y": 26.6, "source": "geonames", "type": "PPL" }, { "name": "Akulabai", "latitude": 5.8102, "longitude": 9.846, "x": 56.6, "y": 56.4, "source": "geonames", "type": "PPL" }, { "name": "Akuruku", "latitude": 5.7833, "longitude": 9.7873, "x": 42.5, "y": 62.5, "source": "geonames", "type": "PPL" }, { "name": "Akuta", "latitude": 6.0058, "longitude": 9.8201, "x": 50.4, "y": 12, "source": "geonames", "type": "PPL" }, { "name": "Alonkong", "latitude": 5.6801, "longitude": 9.9849, "x": 90, "y": 85.9, "source": "geonames", "type": "PPL" }, { "name": "Ambo", "latitude": 5.7963, "longitude": 9.8871, "x": 66.5, "y": 59.6, "source": "geonames", "type": "PPL" }, { "name": "Andek", "latitude": 5.97641, "longitude": 9.83114, "x": 53, "y": 18.7, "source": "geonames", "type": "PPL" }, { "name": "Angai", "latitude": 5.9957, "longitude": 9.8298, "x": 52.7, "y": 14.3, "source": "geonames", "type": "PPL" }, { "name": "Angong", "latitude": 5.8972, "longitude": 9.9012, "x": 69.9, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Anguie", "latitude": 5.80063, "longitude": 9.80881, "x": 47.7, "y": 58.6, "source": "geonames", "type": "PPL" }, { "name": "Angwi", "latitude": 5.91, "longitude": 9.7834, "x": 41.6, "y": 33.8, "source": "geonames", "type": "PPL" }, { "name": "Atanga", "latitude": 5.8108, "longitude": 9.8793, "x": 64.6, "y": 56.3, "source": "geonames", "type": "PPL" }, { "name": "Ati", "latitude": 5.8097, "longitude": 9.7062, "x": 23, "y": 56.5, "source": "geonames", "type": "PPL" }, { "name": "Awun", "latitude": 5.8238, "longitude": 9.9082, "x": 71.6, "y": 53.3, "source": "geonames", "type": "PPL" }, { "name": "Azem", "latitude": 5.90559, "longitude": 9.79695, "x": 44.8, "y": 34.8, "source": "geonames", "type": "PPL" }, { "name": "Bajem", "latitude": 5.8933, "longitude": 9.775, "x": 39.5, "y": 37.6, "source": "geonames", "type": "PPL" }, { "name": "Baliben", "latitude": 5.85, "longitude": 9.78333, "x": 41.5, "y": 47.4, "source": "geonames", "type": "PPL" }, { "name": "Bamben", "latitude": 5.8888, "longitude": 9.7927, "x": 43.8, "y": 38.6, "source": "geonames", "type": "PPL" }, { "name": "Bambi", "latitude": 5.8366, "longitude": 9.6791, "x": 16.5, "y": 50.4, "source": "geonames", "type": "PPL" }, { "name": "Banatu", "latitude": 5.9649, "longitude": 9.847, "x": 56.9, "y": 21.3, "source": "geonames", "type": "PPL" }, { "name": "Banteng", "latitude": 5.7109, "longitude": 9.9817, "x": 89.2, "y": 78.9, "source": "geonames", "type": "PPL" }, { "name": "Banti", "latitude": 5.7044, "longitude": 9.9239, "x": 75.3, "y": 80.4, "source": "geonames", "type": "PPL" }, { "name": "Barambichang", "latitude": 5.9641, "longitude": 9.6972, "x": 20.8, "y": 21.5, "source": "geonames", "type": "PPL" }, { "name": "Bator", "latitude": 5.7861, "longitude": 9.6969, "x": 20.8, "y": 61.9, "source": "geonames", "type": "PPL" }, { "name": "Bechati", "latitude": 5.66517, "longitude": 9.90798, "x": 71.5, "y": 89.3, "source": "geonames", "type": "PPL" }, { "name": "Befang", "latitude": 5.8839, "longitude": 9.7614, "x": 36.3, "y": 39.7, "source": "geonames", "type": "PPL" }, { "name": "Bendi", "latitude": 5.9014, "longitude": 9.7537, "x": 34.4, "y": 35.7, "source": "geonames", "type": "PPL" }, { "name": "Besi", "latitude": 5.81558, "longitude": 9.92417, "x": 75.4, "y": 55.2, "source": "geonames", "type": "PPL" }, { "name": "Bosi", "latitude": 5.9692, "longitude": 9.7778, "x": 40.2, "y": 20.3, "source": "geonames", "type": "PPL" }, { "name": "Bulam", "latitude": 5.9073, "longitude": 9.7307, "x": 28.9, "y": 34.4, "source": "geonames", "type": "PPL" }, { "name": "Chinda", "latitude": 5.8315, "longitude": 9.6521, "x": 10, "y": 51.6, "source": "geonames", "type": "PPL" }, { "name": "Denku", "latitude": 5.8778, "longitude": 9.7279, "x": 28.2, "y": 41.1, "source": "geonames", "type": "PPL" }, { "name": "Diche I", "latitude": 5.8499, "longitude": 9.7555, "x": 34.9, "y": 47.4, "source": "geonames", "type": "PPL" }, { "name": "Diche II", "latitude": 5.8284, "longitude": 9.7769, "x": 40, "y": 52.3, "source": "geonames", "type": "PPL" }, { "name": "Ebang", "latitude": 5.9926, "longitude": 9.8183, "x": 50, "y": 15, "source": "geonames", "type": "PPL" }, { "name": "Edem", "latitude": 5.9758, "longitude": 9.8011, "x": 45.8, "y": 18.9, "source": "geonames", "type": "PPL" }, { "name": "Efa", "latitude": 5.7773, "longitude": 9.8544, "x": 58.6, "y": 63.9, "source": "geonames", "type": "PPL" }, { "name": "Egbeachu", "latitude": 5.9337, "longitude": 9.6963, "x": 20.6, "y": 28.4, "source": "geonames", "type": "PPL" }, { "name": "Enyo", "latitude": 5.7823, "longitude": 9.9046, "x": 70.7, "y": 62.7, "source": "geonames", "type": "PPL" }, { "name": "Esaw", "latitude": 5.9517, "longitude": 9.7705, "x": 38.5, "y": 24.3, "source": "geonames", "type": "PPL" }, { "name": "Etet", "latitude": 6.01482, "longitude": 9.79259, "x": 43.8, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Ewa", "latitude": 5.7555, "longitude": 9.9161, "x": 73.5, "y": 68.8, "source": "geonames", "type": "PPL" }, { "name": "Ewangabi", "latitude": 5.88333, "longitude": 9.68333, "x": 17.5, "y": 39.8, "source": "geonames", "type": "PPL" }, { "name": "Folepi", "latitude": 5.6815, "longitude": 9.9196, "x": 74.3, "y": 85.6, "source": "geonames", "type": "PPL" }, { "name": "Fonkem", "latitude": 5.6621, "longitude": 9.9155, "x": 73.3, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Fumbe", "latitude": 5.6994, "longitude": 9.8757, "x": 63.8, "y": 81.5, "source": "geonames", "type": "PPL" }, { "name": "Gana", "latitude": 5.9215, "longitude": 9.7475, "x": 32.9, "y": 31.2, "source": "geonames", "type": "PPL" }, { "name": "Gurete Bambat", "latitude": 5.7083, "longitude": 9.8781, "x": 64.3, "y": 79.5, "source": "geonames", "type": "PPL" }, { "name": "Guzang", "latitude": 5.8388, "longitude": 9.9324, "x": 77.4, "y": 49.9, "source": "geonames", "type": "PPL" }, { "name": "Igumba", "latitude": 5.6912, "longitude": 9.9708, "x": 86.6, "y": 83.4, "source": "geonames", "type": "PPL" }, { "name": "Jimako", "latitude": 5.8151, "longitude": 9.8992, "x": 69.4, "y": 55.3, "source": "geonames", "type": "PPL" }, { "name": "Kekpoti", "latitude": 5.7438, "longitude": 9.7208, "x": 26.5, "y": 71.5, "source": "geonames", "type": "PPL" }, { "name": "Kendem", "latitude": 5.7277, "longitude": 9.6892, "x": 18.9, "y": 75.1, "source": "geonames", "type": "PPL" }, { "name": "Kenyang", "latitude": 5.6655, "longitude": 9.9441, "x": 80.2, "y": 89.2, "source": "geonames", "type": "PPL" }, { "name": "Koano", "latitude": 5.7721, "longitude": 9.7976, "x": 45, "y": 65.1, "source": "geonames", "type": "PPL" }, { "name": "Kong", "latitude": 5.9816, "longitude": 9.7588, "x": 35.6, "y": 17.5, "source": "geonames", "type": "PPL" }, { "name": "Korube", "latitude": 5.8462, "longitude": 9.8368, "x": 54.4, "y": 48.2, "source": "geonames", "type": "PPL" }, { "name": "Kugwe", "latitude": 5.7628, "longitude": 9.82, "x": 50.4, "y": 67.2, "source": "geonames", "type": "PPL" }, { "name": "Kunok", "latitude": 5.8558, "longitude": 9.8644, "x": 61, "y": 46.1, "source": "geonames", "type": "PPL" }, { "name": "Mbengok", "latitude": 5.8489, "longitude": 9.8525, "x": 58.2, "y": 47.6, "source": "geonames", "type": "PPL" }, { "name": "Mbome", "latitude": 5.76, "longitude": 9.7794, "x": 40.6, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Mbunjei", "latitude": 5.8531, "longitude": 9.9008, "x": 69.8, "y": 46.7, "source": "geonames", "type": "PPL" }, { "name": "Menka", "latitude": 5.99129, "longitude": 9.71441, "x": 25, "y": 15.3, "source": "geonames", "type": "PPL" }, { "name": "Mezang", "latitude": 5.91667, "longitude": 9.9, "x": 69.6, "y": 32.3, "source": "geonames", "type": "PPL" }, { "name": "Mobang", "latitude": 5.8126, "longitude": 9.7779, "x": 40.2, "y": 55.9, "source": "geonames", "type": "PPL" }, { "name": "Mosie", "latitude": 5.7397, "longitude": 9.74, "x": 31.1, "y": 72.4, "source": "geonames", "type": "PPL" }, { "name": "Nen", "latitude": 5.827, "longitude": 9.8332, "x": 53.5, "y": 52.6, "source": "geonames", "type": "PPL" }, { "name": "Numba", "latitude": 5.8389, "longitude": 9.7143, "x": 25, "y": 49.9, "source": "geonames", "type": "PPL" }, { "name": "Numben", "latitude": 5.8687, "longitude": 9.825, "x": 51.6, "y": 43.1, "source": "geonames", "type": "PPL" }, { "name": "Nyen", "latitude": 5.7445, "longitude": 9.9503, "x": 81.7, "y": 71.3, "source": "geonames", "type": "PPL" }, { "name": "Nyeneba", "latitude": 5.7163, "longitude": 9.8534, "x": 58.4, "y": 77.7, "source": "geonames", "type": "PPL" }, { "name": "Nyenjei", "latitude": 5.8634, "longitude": 9.9147, "x": 73.1, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Oche", "latitude": 5.8906, "longitude": 9.715, "x": 25.1, "y": 38.2, "source": "geonames", "type": "PPL" }, { "name": "Ofit", "latitude": 5.8937, "longitude": 9.8588, "x": 59.7, "y": 37.5, "source": "geonames", "type": "PPL" }, { "name": "Old Bechati", "latitude": 5.6766, "longitude": 9.9693, "x": 86.3, "y": 86.7, "source": "geonames", "type": "PPL" }, { "name": "Olorunti", "latitude": 5.9177, "longitude": 9.6867, "x": 18.3, "y": 32, "source": "geonames", "type": "PPL" }, { "name": "Onamafong", "latitude": 5.7208, "longitude": 9.8483, "x": 57.2, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Oshum", "latitude": 5.8781, "longitude": 9.9118, "x": 72.4, "y": 41, "source": "geonames", "type": "PPL" }, { "name": "Tezie", "latitude": 5.94217, "longitude": 9.81488, "x": 49.1, "y": 26.5, "source": "geonames", "type": "PPL" }, { "name": "Tiben", "latitude": 5.86196, "longitude": 9.8064, "x": 47.1, "y": 44.7, "source": "geonames", "type": "PPL" }, { "name": "Trib", "latitude": 5.9776, "longitude": 9.8394, "x": 55, "y": 18.4, "source": "geonames", "type": "PPL" }, { "name": "Widekum", "latitude": 5.8719, "longitude": 9.7707, "x": 38.5, "y": 42.4, "source": "geonames", "type": "PPL" } ], "Batouri": [ { "name": "Abeganga", "latitude": 4.41667, "longitude": 14.51667, "x": 80.4, "y": 50.9, "source": "geonames", "type": "PPL" }, { "name": "Akakele", "latitude": 4.45, "longitude": 14.16667, "x": 13.2, "y": 44.4, "source": "geonames", "type": "PPL" }, { "name": "Amedjoume", "latitude": 4.36667, "longitude": 14.56667, "x": 90, "y": 60.7, "source": "geonames", "type": "PPL" }, { "name": "Amoure", "latitude": 4.23333, "longitude": 14.41667, "x": 61.2, "y": 86.7, "source": "geonames", "type": "PPL" }, { "name": "Anbanga", "latitude": 4.43333, "longitude": 14.5, "x": 77.2, "y": 47.7, "source": "geonames", "type": "PPL" }, { "name": "Anoye", "latitude": 4.4, "longitude": 14.28333, "x": 35.6, "y": 54.2, "source": "geonames", "type": "PPL" }, { "name": "Bakombele", "latitude": 4.31667, "longitude": 14.41667, "x": 61.2, "y": 70.5, "source": "geonames", "type": "PPL" }, { "name": "Bakonbo", "latitude": 4.4, "longitude": 14.25, "x": 29.2, "y": 54.2, "source": "geonames", "type": "PPL" }, { "name": "Bandongwe", "latitude": 4.4, "longitude": 14.21667, "x": 22.8, "y": 54.2, "source": "geonames", "type": "PPL" }, { "name": "Bindisola", "latitude": 4.31667, "longitude": 14.41667, "x": 61.2, "y": 70.5, "source": "geonames", "type": "PPL" }, { "name": "Borongoue II", "latitude": 4.35, "longitude": 14.56667, "x": 90, "y": 63.9, "source": "geonames", "type": "PPL" }, { "name": "Boso", "latitude": 4.56667, "longitude": 14.51667, "x": 80.4, "y": 21.6, "source": "geonames", "type": "PPL" }, { "name": "Daligene", "latitude": 4.43333, "longitude": 14.16667, "x": 13.2, "y": 47.7, "source": "geonames", "type": "PPL" }, { "name": "Dem", "latitude": 4.46622, "longitude": 14.44359, "x": 66.4, "y": 41.2, "source": "geonames", "type": "PPL" }, { "name": "Dimako I", "latitude": 4.38333, "longitude": 14.38333, "x": 54.8, "y": 57.4, "source": "geonames", "type": "PPL" }, { "name": "Dimako II", "latitude": 4.36667, "longitude": 14.16667, "x": 13.2, "y": 60.7, "source": "geonames", "type": "PPL" }, { "name": "Djida", "latitude": 4.45, "longitude": 14.46667, "x": 70.8, "y": 44.4, "source": "geonames", "type": "PPL" }, { "name": "Dogbwo", "latitude": 4.41667, "longitude": 14.33333, "x": 45.2, "y": 50.9, "source": "geonames", "type": "PPL" }, { "name": "Garoua Sanbe", "latitude": 4.62596, "longitude": 14.30231, "x": 39.2, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Kambele", "latitude": 4.46667, "longitude": 14.4, "x": 58, "y": 41.1, "source": "geonames", "type": "PPL" }, { "name": "Kasangba", "latitude": 4.46667, "longitude": 14.45, "x": 67.6, "y": 41.1, "source": "geonames", "type": "PPL" }, { "name": "Konbo", "latitude": 4.35, "longitude": 14.38333, "x": 54.8, "y": 63.9, "source": "geonames", "type": "PPL" }, { "name": "Lobi", "latitude": 4.4, "longitude": 14.38333, "x": 54.8, "y": 54.2, "source": "geonames", "type": "PPL" }, { "name": "Mama", "latitude": 4.51667, "longitude": 14.35, "x": 48.4, "y": 31.4, "source": "geonames", "type": "PPL" }, { "name": "Mbangou", "latitude": 4.45, "longitude": 14.15, "x": 10, "y": 44.4, "source": "geonames", "type": "PPL" }, { "name": "Mboua", "latitude": 4.41667, "longitude": 14.26667, "x": 32.4, "y": 50.9, "source": "geonames", "type": "PPL" }, { "name": "Moso", "latitude": 4.41667, "longitude": 14.25, "x": 29.2, "y": 50.9, "source": "geonames", "type": "PPL" }, { "name": "Moundia", "latitude": 4.3, "longitude": 14.4, "x": 58, "y": 73.7, "source": "geonames", "type": "PPL" }, { "name": "Ndeina", "latitude": 4.21667, "longitude": 14.36667, "x": 51.6, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ndouminbe", "latitude": 4.51667, "longitude": 14.48333, "x": 74, "y": 31.4, "source": "geonames", "type": "PPL" }, { "name": "Ngemo", "latitude": 4.4, "longitude": 14.55, "x": 86.8, "y": 54.2, "source": "geonames", "type": "PPL" }, { "name": "Ngese", "latitude": 4.58333, "longitude": 14.51667, "x": 80.4, "y": 18.3, "source": "geonames", "type": "PPL" }, { "name": "Ngoura", "latitude": 4.4, "longitude": 14.56667, "x": 90, "y": 54.2, "source": "geonames", "type": "PPL" }, { "name": "Nkolmbomo", "latitude": 4.41667, "longitude": 14.3, "x": 38.8, "y": 50.9, "source": "geonames", "type": "PPL" }, { "name": "Nzinbe", "latitude": 4.46667, "longitude": 14.36667, "x": 51.6, "y": 41.1, "source": "geonames", "type": "PPL" }, { "name": "Nzinbele", "latitude": 4.38333, "longitude": 14.18333, "x": 16.4, "y": 57.4, "source": "geonames", "type": "PPL" }, { "name": "Nzingoue", "latitude": 4.21667, "longitude": 14.41667, "x": 61.2, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Patere", "latitude": 4.4, "longitude": 14.38333, "x": 54.8, "y": 54.2, "source": "geonames", "type": "PPL" }, { "name": "Sandae", "latitude": 4.41667, "longitude": 14.31667, "x": 42, "y": 50.9, "source": "geonames", "type": "PPL" }, { "name": "Touki II", "latitude": 4.23333, "longitude": 14.38333, "x": 54.8, "y": 86.7, "source": "geonames", "type": "PPL" } ], "Batcham": [ { "name": "Bafemto", "latitude": 5.55639, "longitude": 10.28583, "x": 84.3, "y": 34.4, "source": "geonames", "type": "PPL" }, { "name": "Bafouletsi", "latitude": 5.50204, "longitude": 10.25042, "x": 63.6, "y": 71.3, "source": "geonames", "type": "PPL" }, { "name": "Bagong", "latitude": 5.55698, "longitude": 10.26941, "x": 74.7, "y": 34, "source": "geonames", "type": "PPL" }, { "name": "Bakadjou", "latitude": 5.54509, "longitude": 10.29508, "x": 89.8, "y": 42.1, "source": "geonames", "type": "PPL" }, { "name": "Bakeup", "latitude": 5.5544, "longitude": 10.2955, "x": 90, "y": 35.8, "source": "geonames", "type": "PPL" }, { "name": "Balaanka", "latitude": 5.52376, "longitude": 10.26423, "x": 71.7, "y": 56.6, "source": "geonames", "type": "PPL" }, { "name": "Balaatchi", "latitude": 5.54202, "longitude": 10.25357, "x": 65.5, "y": 44.2, "source": "geonames", "type": "PPL" }, { "name": "Balefa", "latitude": 5.54718, "longitude": 10.26602, "x": 72.7, "y": 40.7, "source": "geonames", "type": "PPL" }, { "name": "Balegang", "latitude": 5.53612, "longitude": 10.26676, "x": 73.2, "y": 48.2, "source": "geonames", "type": "PPL" }, { "name": "Balepa", "latitude": 5.51239, "longitude": 10.25938, "x": 68.9, "y": 64.3, "source": "geonames", "type": "PPL" }, { "name": "Baletsi", "latitude": 5.50948, "longitude": 10.26507, "x": 72.2, "y": 66.2, "source": "geonames", "type": "PPL" }, { "name": "Bambi I", "latitude": 5.57426, "longitude": 10.25307, "x": 65.2, "y": 22.3, "source": "geonames", "type": "PPL" }, { "name": "Bamelang", "latitude": 5.55523, "longitude": 10.26429, "x": 71.7, "y": 35.2, "source": "geonames", "type": "PPL" }, { "name": "Bameli", "latitude": 5.51732, "longitude": 10.27416, "x": 77.5, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Bamendjeu", "latitude": 5.55642, "longitude": 10.28078, "x": 81.4, "y": 34.4, "source": "geonames", "type": "PPL" }, { "name": "Bamendou", "latitude": 5.54631, "longitude": 10.2591, "x": 68.7, "y": 41.3, "source": "geonames", "type": "PPL" }, { "name": "Bamenka I", "latitude": 5.56514, "longitude": 10.25302, "x": 65.1, "y": 28.5, "source": "geonames", "type": "PPL" }, { "name": "Bamenka II", "latitude": 5.51543, "longitude": 10.26355, "x": 71.3, "y": 62.2, "source": "geonames", "type": "PPL" }, { "name": "Bametiegou", "latitude": 5.56245, "longitude": 10.2831, "x": 82.7, "y": 30.3, "source": "geonames", "type": "PPL" }, { "name": "Bamongo", "latitude": 5.5683, "longitude": 10.27435, "x": 77.6, "y": 26.4, "source": "geonames", "type": "PPL" }, { "name": "Bandjinsi", "latitude": 5.54468, "longitude": 10.28365, "x": 83.1, "y": 42.4, "source": "geonames", "type": "PPL" }, { "name": "Bankui", "latitude": 5.51528, "longitude": 10.25328, "x": 65.3, "y": 62.3, "source": "geonames", "type": "PPL" }, { "name": "Basso", "latitude": 5.53524, "longitude": 10.27832, "x": 79.9, "y": 48.8, "source": "geonames", "type": "PPL" }, { "name": "Batchuetio", "latitude": 5.56525, "longitude": 10.26491, "x": 72.1, "y": 28.4, "source": "geonames", "type": "PPL" }, { "name": "Batoussop", "latitude": 5.55245, "longitude": 10.27914, "x": 80.4, "y": 37.1, "source": "geonames", "type": "PPL" }, { "name": "Batoutcham", "latitude": 5.53044, "longitude": 10.25248, "x": 64.8, "y": 52, "source": "geonames", "type": "PPL" }, { "name": "Bekou", "latitude": 5.54757, "longitude": 10.18416, "x": 24.8, "y": 40.4, "source": "geonames", "type": "PPL" }, { "name": "Boughang", "latitude": 5.53851, "longitude": 10.18172, "x": 23.4, "y": 46.6, "source": "geonames", "type": "PPL" }, { "name": "Datchio", "latitude": 5.53799, "longitude": 10.16786, "x": 15.3, "y": 46.9, "source": "geonames", "type": "PPL" }, { "name": "Dengang", "latitude": 5.55851, "longitude": 10.21269, "x": 41.5, "y": 33, "source": "geonames", "type": "PPL" }, { "name": "Femelhi", "latitude": 5.55205, "longitude": 10.16791, "x": 15.3, "y": 37.4, "source": "geonames", "type": "PPL" }, { "name": "Fiala", "latitude": 5.54443, "longitude": 10.23049, "x": 51.9, "y": 42.6, "source": "geonames", "type": "PPL" }, { "name": "Ghang", "latitude": 5.5439, "longitude": 10.1755, "x": 19.7, "y": 42.9, "source": "geonames", "type": "PPL" }, { "name": "Ghante", "latitude": 5.53811, "longitude": 10.19623, "x": 31.9, "y": 46.8, "source": "geonames", "type": "PPL" }, { "name": "King Place", "latitude": 5.57995, "longitude": 10.21272, "x": 41.5, "y": 18.5, "source": "geonames", "type": "PPL" }, { "name": "Konti", "latitude": 5.54624, "longitude": 10.25016, "x": 63.5, "y": 41.3, "source": "geonames", "type": "PPL" }, { "name": "Kontia", "latitude": 5.56432, "longitude": 10.1653, "x": 13.8, "y": 29.1, "source": "geonames", "type": "PPL" }, { "name": "Koontse", "latitude": 5.56897, "longitude": 10.16429, "x": 13.2, "y": 25.9, "source": "geonames", "type": "PPL" }, { "name": "La'atchuet", "latitude": 5.541, "longitude": 10.24186, "x": 58.6, "y": 44.9, "source": "geonames", "type": "PPL" }, { "name": "Lafotio", "latitude": 5.58771, "longitude": 10.19272, "x": 29.8, "y": 13.2, "source": "geonames", "type": "PPL" }, { "name": "Lekwet", "latitude": 5.49112, "longitude": 10.1999, "x": 34, "y": 78.7, "source": "geonames", "type": "PPL" }, { "name": "Lena", "latitude": 5.52893, "longitude": 10.21334, "x": 41.9, "y": 53.1, "source": "geonames", "type": "PPL" }, { "name": "Lepang", "latitude": 5.49947, "longitude": 10.18409, "x": 24.8, "y": 73, "source": "geonames", "type": "PPL" }, { "name": "Lepipi I", "latitude": 5.50943, "longitude": 10.22903, "x": 51.1, "y": 66.3, "source": "geonames", "type": "PPL" }, { "name": "Lepipi II", "latitude": 5.51182, "longitude": 10.22222, "x": 47.1, "y": 64.7, "source": "geonames", "type": "PPL" }, { "name": "Lepipi III", "latitude": 5.50643, "longitude": 10.21223, "x": 41.3, "y": 68.3, "source": "geonames", "type": "PPL" }, { "name": "Lessing", "latitude": 5.50262, "longitude": 10.23359, "x": 53.8, "y": 70.9, "source": "geonames", "type": "PPL" }, { "name": "Lessintchuet", "latitude": 5.52139, "longitude": 10.19623, "x": 31.9, "y": 58.2, "source": "geonames", "type": "PPL" }, { "name": "Letia", "latitude": 5.56091, "longitude": 10.16105, "x": 11.3, "y": 31.4, "source": "geonames", "type": "PPL" }, { "name": "Letsi", "latitude": 5.51272, "longitude": 10.17103, "x": 17.1, "y": 64, "source": "geonames", "type": "PPL" }, { "name": "Lewa", "latitude": 5.54879, "longitude": 10.17492, "x": 19.4, "y": 39.6, "source": "geonames", "type": "PPL" }, { "name": "Mbi", "latitude": 5.53293, "longitude": 10.19837, "x": 33.1, "y": 50.3, "source": "geonames", "type": "PPL" }, { "name": "Mbie I", "latitude": 5.57543, "longitude": 10.23996, "x": 57.5, "y": 21.5, "source": "geonames", "type": "PPL" }, { "name": "Mbie II", "latitude": 5.56791, "longitude": 10.24311, "x": 59.3, "y": 26.6, "source": "geonames", "type": "PPL" }, { "name": "Mbikeng", "latitude": 5.47848, "longitude": 10.2022, "x": 35.4, "y": 87.3, "source": "geonames", "type": "PPL" }, { "name": "Mbing", "latitude": 5.49629, "longitude": 10.2347, "x": 54.4, "y": 75.2, "source": "geonames", "type": "PPL" }, { "name": "Mbwe I", "latitude": 5.55798, "longitude": 10.2352, "x": 54.7, "y": 33.4, "source": "geonames", "type": "PPL" }, { "name": "Mbwe II", "latitude": 5.57255, "longitude": 10.21838, "x": 44.9, "y": 23.5, "source": "geonames", "type": "PPL" }, { "name": "Mefou", "latitude": 5.55261, "longitude": 10.19332, "x": 30.2, "y": 37, "source": "geonames", "type": "PPL" }, { "name": "Meghui", "latitude": 5.57605, "longitude": 10.17075, "x": 17, "y": 21.1, "source": "geonames", "type": "PPL" }, { "name": "Meka", "latitude": 5.50976, "longitude": 10.21543, "x": 43.1, "y": 66.1, "source": "geonames", "type": "PPL" }, { "name": "Mekie", "latitude": 5.51802, "longitude": 10.22893, "x": 51, "y": 60.5, "source": "geonames", "type": "PPL" }, { "name": "Mekoo", "latitude": 5.48555, "longitude": 10.22473, "x": 48.6, "y": 82.5, "source": "geonames", "type": "PPL" }, { "name": "Mela", "latitude": 5.54291, "longitude": 10.16782, "x": 15.3, "y": 43.6, "source": "geonames", "type": "PPL" }, { "name": "Mendou", "latitude": 5.49513, "longitude": 10.26826, "x": 74.1, "y": 76, "source": "geonames", "type": "PPL" }, { "name": "Menguea I", "latitude": 5.56992, "longitude": 10.18567, "x": 25.7, "y": 25.3, "source": "geonames", "type": "PPL" }, { "name": "Menguea II", "latitude": 5.57657, "longitude": 10.18633, "x": 26.1, "y": 20.8, "source": "geonames", "type": "PPL" }, { "name": "Menka", "latitude": 5.56186, "longitude": 10.24762, "x": 62, "y": 30.7, "source": "geonames", "type": "PPL" }, { "name": "Menkwa'a", "latitude": 5.56637, "longitude": 10.18066, "x": 22.8, "y": 27.7, "source": "geonames", "type": "PPL" }, { "name": "Messa", "latitude": 5.5601, "longitude": 10.17144, "x": 17.4, "y": 31.9, "source": "geonames", "type": "PPL" }, { "name": "Messang", "latitude": 5.55286, "longitude": 10.21141, "x": 40.8, "y": 36.8, "source": "geonames", "type": "PPL" }, { "name": "Meta I", "latitude": 5.49667, "longitude": 10.22824, "x": 50.6, "y": 74.9, "source": "geonames", "type": "PPL" }, { "name": "Meta II", "latitude": 5.5027, "longitude": 10.22348, "x": 47.8, "y": 70.8, "source": "geonames", "type": "PPL" }, { "name": "Mewak", "latitude": 5.51412, "longitude": 10.2085, "x": 39.1, "y": 63.1, "source": "geonames", "type": "PPL" }, { "name": "Montane", "latitude": 5.58333, "longitude": 10.2, "x": 34.1, "y": 16.2, "source": "geonames", "type": "PPL" }, { "name": "Mougong", "latitude": 5.57924, "longitude": 10.21857, "x": 45, "y": 19, "source": "geonames", "type": "PPL" }, { "name": "Ndjimbwap", "latitude": 5.51726, "longitude": 10.23658, "x": 55.5, "y": 61, "source": "geonames", "type": "PPL" }, { "name": "Ndjuinla II", "latitude": 5.59245, "longitude": 10.17507, "x": 19.5, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Ndjuintim", "latitude": 5.58066, "longitude": 10.18351, "x": 24.4, "y": 18, "source": "geonames", "type": "PPL" }, { "name": "Ngang", "latitude": 5.56747, "longitude": 10.17435, "x": 19.1, "y": 26.9, "source": "geonames", "type": "PPL" }, { "name": "Ngouang I", "latitude": 5.52788, "longitude": 10.23741, "x": 56, "y": 53.8, "source": "geonames", "type": "PPL" }, { "name": "Ngouo", "latitude": 5.53279, "longitude": 10.18897, "x": 27.6, "y": 50.4, "source": "geonames", "type": "PPL" }, { "name": "Ngueala", "latitude": 5.55416, "longitude": 10.18171, "x": 23.4, "y": 36, "source": "geonames", "type": "PPL" }, { "name": "Nguechou", "latitude": 5.49557, "longitude": 10.2624, "x": 70.6, "y": 75.7, "source": "geonames", "type": "PPL" }, { "name": "Nguie", "latitude": 5.54469, "longitude": 10.21998, "x": 45.8, "y": 42.4, "source": "geonames", "type": "PPL" }, { "name": "Ngwa", "latitude": 5.52628, "longitude": 10.19509, "x": 31.2, "y": 54.9, "source": "geonames", "type": "PPL" }, { "name": "Nkoango", "latitude": 5.48312, "longitude": 10.22372, "x": 48, "y": 84.1, "source": "geonames", "type": "PPL" }, { "name": "Nkongto", "latitude": 5.52457, "longitude": 10.15885, "x": 10, "y": 56, "source": "geonames", "type": "PPL" }, { "name": "Nteng", "latitude": 5.50336, "longitude": 10.20778, "x": 38.6, "y": 70.4, "source": "geonames", "type": "PPL" }, { "name": "Ntim", "latitude": 5.54427, "longitude": 10.20298, "x": 35.8, "y": 42.7, "source": "geonames", "type": "PPL" }, { "name": "Ntio", "latitude": 5.54948, "longitude": 10.24086, "x": 58, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Ntsie", "latitude": 5.52314, "longitude": 10.17877, "x": 21.7, "y": 57, "source": "geonames", "type": "PPL" }, { "name": "Nzihi", "latitude": 5.49184, "longitude": 10.18791, "x": 27, "y": 78.2, "source": "geonames", "type": "PPL" }, { "name": "Nzindong", "latitude": 5.58079, "longitude": 10.19719, "x": 32.4, "y": 17.9, "source": "geonames", "type": "PPL" }, { "name": "Pepa", "latitude": 5.56378, "longitude": 10.21403, "x": 42.3, "y": 29.4, "source": "geonames", "type": "PPL" }, { "name": "Ponga", "latitude": 5.57094, "longitude": 10.2602, "x": 69.3, "y": 24.6, "source": "geonames", "type": "PPL" }, { "name": "Taki", "latitude": 5.55857, "longitude": 10.20547, "x": 37.3, "y": 33, "source": "geonames", "type": "PPL" }, { "name": "Tchowang", "latitude": 5.48722, "longitude": 10.24928, "x": 62.9, "y": 81.3, "source": "geonames", "type": "PPL" }, { "name": "Tchuefi", "latitude": 5.48704, "longitude": 10.20476, "x": 36.9, "y": 81.5, "source": "geonames", "type": "PPL" }, { "name": "To'olong", "latitude": 5.58995, "longitude": 10.22447, "x": 48.4, "y": 11.7, "source": "geonames", "type": "PPL" }, { "name": "Tomogo I", "latitude": 5.58831, "longitude": 10.17599, "x": 20, "y": 12.8, "source": "geonames", "type": "PPL" }, { "name": "Toula", "latitude": 5.56631, "longitude": 10.19967, "x": 33.9, "y": 27.7, "source": "geonames", "type": "PPL" }, { "name": "Touleteng", "latitude": 5.50938, "longitude": 10.20883, "x": 39.3, "y": 66.3, "source": "geonames", "type": "PPL" }, { "name": "Toumbi", "latitude": 5.52861, "longitude": 10.20202, "x": 35.3, "y": 53.3, "source": "geonames", "type": "PPL" }, { "name": "Toumegou", "latitude": 5.56509, "longitude": 10.23095, "x": 52.2, "y": 28.5, "source": "geonames", "type": "PPL" }, { "name": "Toutsa", "latitude": 5.53739, "longitude": 10.24121, "x": 58.2, "y": 47.3, "source": "geonames", "type": "PPL" }, { "name": "Tsa", "latitude": 5.54327, "longitude": 10.23818, "x": 56.4, "y": 43.3, "source": "geonames", "type": "PPL" }, { "name": "Tsela", "latitude": 5.52297, "longitude": 10.20494, "x": 37, "y": 57.1, "source": "geonames", "type": "PPL" }, { "name": "Tsimbi", "latitude": 5.52035, "longitude": 10.20998, "x": 39.9, "y": 58.9, "source": "geonames", "type": "PPL" }, { "name": "Tsimegang", "latitude": 5.57574, "longitude": 10.23114, "x": 52.3, "y": 21.3, "source": "geonames", "type": "PPL" }, { "name": "Tsinla", "latitude": 5.55783, "longitude": 10.18952, "x": 28, "y": 33.5, "source": "geonames", "type": "PPL" }, { "name": "Wouang I", "latitude": 5.52185, "longitude": 10.24768, "x": 62, "y": 57.9, "source": "geonames", "type": "PPL" }, { "name": "Wouang II", "latitude": 5.52535, "longitude": 10.25155, "x": 64.3, "y": 55.5, "source": "geonames", "type": "PPL" }, { "name": "Wounwoua", "latitude": 5.50107, "longitude": 10.24243, "x": 58.9, "y": 71.9, "source": "geonames", "type": "PPL" }, { "name": "Zemekong", "latitude": 5.47443, "longitude": 10.22769, "x": 50.3, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Zemeto", "latitude": 5.51627, "longitude": 10.1636, "x": 12.8, "y": 61.6, "source": "geonames", "type": "PPL" }, { "name": "Zimegong I", "latitude": 5.53543, "longitude": 10.23765, "x": 56.1, "y": 48.7, "source": "geonames", "type": "PPL" }, { "name": "Zimegong II", "latitude": 5.53033, "longitude": 10.24097, "x": 58.1, "y": 52.1, "source": "geonames", "type": "PPL" }, { "name": "Zindza", "latitude": 5.48208, "longitude": 10.22993, "x": 51.6, "y": 84.8, "source": "geonames", "type": "PPL" }, { "name": "Zintia", "latitude": 5.58669, "longitude": 10.21723, "x": 44.2, "y": 13.9, "source": "geonames", "type": "PPL" } ], "Bekoko": [ { "name": "Adjap", "latitude": 2.18333, "longitude": 11.13333, "x": 36.7, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Akom", "latitude": 2.18333, "longitude": 11.2, "x": 58, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ayee", "latitude": 2.18333, "longitude": 11.3, "x": 90, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Bebein", "latitude": 2.18333, "longitude": 11.18333, "x": 52.7, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Biyi", "latitude": 2.21667, "longitude": 11.05, "x": 10, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Dama", "latitude": 2.21667, "longitude": 11.21667, "x": 63.3, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Damingo", "latitude": 2.2, "longitude": 11.23333, "x": 68.7, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Douanes", "latitude": 2.25, "longitude": 11.2, "x": 58, "y": 26, "source": "geonames", "type": "PPL" }, { "name": "Ebengon", "latitude": 2.23333, "longitude": 11.2, "x": 58, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Eyiantoum", "latitude": 2.23333, "longitude": 11.13333, "x": 36.7, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Fenete", "latitude": 2.21667, "longitude": 11.25, "x": 74, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Koumou", "latitude": 2.2, "longitude": 11.11667, "x": 31.3, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Mbae", "latitude": 2.2, "longitude": 11.06667, "x": 15.3, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Mbedoumou", "latitude": 2.18333, "longitude": 11.08333, "x": 20.7, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Medji-Ossi", "latitude": 2.2, "longitude": 11.2, "x": 58, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Mekoa", "latitude": 2.2, "longitude": 11.13333, "x": 36.7, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Mekoassi", "latitude": 2.21667, "longitude": 11.11667, "x": 31.3, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Mekomengona", "latitude": 2.25, "longitude": 11.18333, "x": 52.7, "y": 26, "source": "geonames", "type": "PPL" }, { "name": "Mekomo", "latitude": 2.2, "longitude": 11.23333, "x": 68.7, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Mekomo-Ondossi", "latitude": 2.2, "longitude": 11.26667, "x": 79.3, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Mendjikom", "latitude": 2.18333, "longitude": 11.11667, "x": 31.3, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Mendong", "latitude": 2.25, "longitude": 11.26667, "x": 79.3, "y": 26, "source": "geonames", "type": "PPL" }, { "name": "Mendoum", "latitude": 2.21667, "longitude": 11.23333, "x": 68.7, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Mimbaminga", "latitude": 2.23333, "longitude": 11.2, "x": 58, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Mindjo", "latitude": 2.2, "longitude": 11.11667, "x": 31.3, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Minkomo", "latitude": 2.18333, "longitude": 11.26667, "x": 79.3, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ncumadyap", "latitude": 2.18333, "longitude": 11.18333, "x": 52.7, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Nkane", "latitude": 2.25, "longitude": 11.23333, "x": 68.7, "y": 26, "source": "geonames", "type": "PPL" }, { "name": "Nkoun Adjap", "latitude": 2.18333, "longitude": 11.18333, "x": 52.7, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Nsana", "latitude": 2.26667, "longitude": 11.26667, "x": 79.3, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Nsangbwang", "latitude": 2.25, "longitude": 11.3, "x": 90, "y": 26, "source": "geonames", "type": "PPL" }, { "name": "Nsezeng", "latitude": 2.21667, "longitude": 11.18333, "x": 52.7, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Obang", "latitude": 2.21667, "longitude": 11.08333, "x": 20.7, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Olamze", "latitude": 2.21667, "longitude": 11.08333, "x": 20.7, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Olang", "latitude": 2.21667, "longitude": 11.16667, "x": 47.3, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Seng", "latitude": 2.18333, "longitude": 11.28333, "x": 84.7, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Yos", "latitude": 2.25, "longitude": 11.06667, "x": 15.3, "y": 26, "source": "geonames", "type": "PPL" } ], "Belabo": [ { "name": "Adia", "latitude": 4.85, "longitude": 13.4, "x": 68.5, "y": 71.8, "source": "geonames", "type": "PPL" }, { "name": "Bombi", "latitude": 4.93333, "longitude": 13.48333, "x": 83.8, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Darang", "latitude": 5.13333, "longitude": 13.23333, "x": 37.7, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Demba I", "latitude": 4.81667, "longitude": 13.4, "x": 68.5, "y": 79.1, "source": "geonames", "type": "PPL" }, { "name": "Doumba II", "latitude": 4.88333, "longitude": 13.25, "x": 40.8, "y": 64.5, "source": "geonames", "type": "PPL" }, { "name": "Ebaka", "latitude": 4.93333, "longitude": 13.31667, "x": 53.1, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Ekak", "latitude": 4.85, "longitude": 13.21667, "x": 34.6, "y": 71.8, "source": "geonames", "type": "PPL" }, { "name": "Esselegue", "latitude": 4.8, "longitude": 13.26667, "x": 43.8, "y": 82.7, "source": "geonames", "type": "PPL" }, { "name": "Kano", "latitude": 4.96667, "longitude": 13.5, "x": 86.9, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Mbaki", "latitude": 5.1, "longitude": 13.45, "x": 77.7, "y": 17.3, "source": "geonames", "type": "PPL" }, { "name": "Mbambo", "latitude": 5.06667, "longitude": 13.38333, "x": 65.4, "y": 24.5, "source": "geonames", "type": "PPL" }, { "name": "Mbargue", "latitude": 4.9, "longitude": 13.08333, "x": 10, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Mbiambia", "latitude": 5.01667, "longitude": 13.33333, "x": 56.2, "y": 35.5, "source": "geonames", "type": "PPL" }, { "name": "Mbinang", "latitude": 4.88333, "longitude": 13.16667, "x": 25.4, "y": 64.5, "source": "geonames", "type": "PPL" }, { "name": "Mbombia", "latitude": 4.98333, "longitude": 13.33333, "x": 56.2, "y": 42.7, "source": "geonames", "type": "PPL" }, { "name": "Mbondji", "latitude": 5.03333, "longitude": 13.28333, "x": 46.9, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Mvounde", "latitude": 4.91667, "longitude": 13.28333, "x": 46.9, "y": 57.3, "source": "geonames", "type": "PPL" }, { "name": "Ndemba I", "latitude": 4.83333, "longitude": 13.38333, "x": 65.4, "y": 75.5, "source": "geonames", "type": "PPL" }, { "name": "Ndjangane", "latitude": 4.86667, "longitude": 13.38333, "x": 65.4, "y": 68.2, "source": "geonames", "type": "PPL" }, { "name": "Ndumba", "latitude": 4.86667, "longitude": 13.36667, "x": 62.3, "y": 68.2, "source": "geonames", "type": "PPL" }, { "name": "Ngombe I", "latitude": 4.78333, "longitude": 13.15, "x": 22.3, "y": 86.4, "source": "geonames", "type": "PPL" }, { "name": "Ngombe II", "latitude": 4.76667, "longitude": 13.16667, "x": 25.4, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Nlang", "latitude": 4.76667, "longitude": 13.15, "x": 22.3, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Sanzane", "latitude": 4.9, "longitude": 13.33333, "x": 56.2, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Viali", "latitude": 4.91667, "longitude": 13.51667, "x": 90, "y": 57.3, "source": "geonames", "type": "PPL" }, { "name": "Yambeng", "latitude": 4.93333, "longitude": 13.5, "x": 86.9, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Yanda", "latitude": 4.88333, "longitude": 13.36667, "x": 62.3, "y": 64.5, "source": "geonames", "type": "PPL" }, { "name": "Yebi", "latitude": 4.96667, "longitude": 13.31667, "x": 53.1, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Yoa", "latitude": 4.83333, "longitude": 13.23333, "x": 37.7, "y": 75.5, "source": "geonames", "type": "PPL" } ], "Belo": [ { "name": "Ake", "latitude": 6.3, "longitude": 10.45, "x": 90, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Bat", "latitude": 6.15, "longitude": 10.45, "x": 90, "y": 65.4, "source": "geonames", "type": "PPL" }, { "name": "Fulani", "latitude": 6.08333, "longitude": 10.36667, "x": 50, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Itchimo", "latitude": 6.26667, "longitude": 10.45, "x": 90, "y": 22.3, "source": "geonames", "type": "PPL" }, { "name": "Mbengo", "latitude": 6.16667, "longitude": 10.28333, "x": 10, "y": 59.2, "source": "geonames", "type": "PPL" }, { "name": "Nyajua", "latitude": 6.21667, "longitude": 10.4, "x": 66, "y": 40.8, "source": "geonames", "type": "PPL" }, { "name": "Oku", "latitude": 6.25, "longitude": 10.43333, "x": 82, "y": 28.5, "source": "geonames", "type": "PPL" } ], "Belel": [ { "name": "Aladji Saido", "latitude": 7.18333, "longitude": 14.4, "x": 44.3, "y": 17.6, "source": "geonames", "type": "PPL" }, { "name": "Bakari-Bata", "latitude": 7, "longitude": 14.26667, "x": 13.8, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Bayara", "latitude": 6.96667, "longitude": 14.46667, "x": 59.5, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Beka Modibo", "latitude": 6.9, "longitude": 14.45, "x": 55.7, "y": 82.4, "source": "geonames", "type": "PPL" }, { "name": "Bondo", "latitude": 7.05, "longitude": 14.6, "x": 90, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Dibi Manbere", "latitude": 6.9, "longitude": 14.41667, "x": 48.1, "y": 82.4, "source": "geonames", "type": "PPL" }, { "name": "Djaboule", "latitude": 7.16667, "longitude": 14.38333, "x": 40.5, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Djakva", "latitude": 7.21667, "longitude": 14.51667, "x": 71, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Djaoro Ndigou", "latitude": 7.15, "longitude": 14.38333, "x": 40.5, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Dzerkoka", "latitude": 7.11667, "longitude": 14.33333, "x": 29, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Dzindan", "latitude": 7.16667, "longitude": 14.55, "x": 78.6, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Dzir Toro", "latitude": 6.9, "longitude": 14.28333, "x": 17.6, "y": 82.4, "source": "geonames", "type": "PPL" }, { "name": "Feirde Wamde", "latitude": 7.1, "longitude": 14.46667, "x": 59.5, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Gasol", "latitude": 6.98333, "longitude": 14.53333, "x": 74.8, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Goloumou", "latitude": 7.11667, "longitude": 14.46667, "x": 59.5, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Goumra", "latitude": 6.88333, "longitude": 14.41667, "x": 48.1, "y": 86.2, "source": "geonames", "type": "PPL" }, { "name": "Hore Ngoura", "latitude": 6.95, "longitude": 14.46667, "x": 59.5, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Hosere Lesdi", "latitude": 6.88333, "longitude": 14.48333, "x": 63.3, "y": 86.2, "source": "geonames", "type": "PPL" }, { "name": "Kim", "latitude": 7.18333, "longitude": 14.41667, "x": 48.1, "y": 17.6, "source": "geonames", "type": "PPL" }, { "name": "Koriong", "latitude": 6.96667, "longitude": 14.5, "x": 67.1, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Koudini Mandal", "latitude": 7.13333, "longitude": 14.53333, "x": 74.8, "y": 29, "source": "geonames", "type": "PPL" }, { "name": "Kounvou", "latitude": 7.15, "longitude": 14.55, "x": 78.6, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Mayo Badji", "latitude": 6.86667, "longitude": 14.4, "x": 44.3, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Mayo Dabi", "latitude": 6.95, "longitude": 14.51667, "x": 71, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Mbang", "latitude": 7.16667, "longitude": 14.48333, "x": 63.3, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Mbangalan", "latitude": 7.15, "longitude": 14.4, "x": 44.3, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Mbangbere", "latitude": 6.86667, "longitude": 14.35, "x": 32.9, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Migalat", "latitude": 7.16667, "longitude": 14.38333, "x": 40.5, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Ndip", "latitude": 7.16667, "longitude": 14.48333, "x": 63.3, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Ngao Bakala", "latitude": 7.15, "longitude": 14.56667, "x": 82.4, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Ngaou Bemi", "latitude": 7.18333, "longitude": 14.48333, "x": 63.3, "y": 17.6, "source": "geonames", "type": "PPL" }, { "name": "Sing", "latitude": 7.15, "longitude": 14.53333, "x": 74.8, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Sokande", "latitude": 6.96667, "longitude": 14.25, "x": 10, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Yangai", "latitude": 6.96667, "longitude": 14.26667, "x": 13.8, "y": 67.1, "source": "geonames", "type": "PPL" } ], "Bertoua": [ { "name": "Boni", "latitude": 4.55, "longitude": 13.61667, "x": 39.1, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Gamboula", "latitude": 4.56667, "longitude": 13.83333, "x": 86.4, "y": 83.3, "source": "geonames", "type": "PPL" }, { "name": "Gouekong", "latitude": 4.56667, "longitude": 13.5, "x": 13.6, "y": 83.3, "source": "geonames", "type": "PPL" }, { "name": "Gounte", "latitude": 4.7, "longitude": 13.81667, "x": 82.7, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Koume", "latitude": 4.65, "longitude": 13.65, "x": 46.4, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Koundi", "latitude": 4.73333, "longitude": 13.6, "x": 35.5, "y": 16.7, "source": "geonames", "type": "PPL" }, { "name": "Mandjou", "latitude": 4.6, "longitude": 13.73333, "x": 64.5, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Moundi", "latitude": 4.56667, "longitude": 13.48333, "x": 10, "y": 83.3, "source": "geonames", "type": "PPL" }, { "name": "Ndembo", "latitude": 4.55, "longitude": 13.85, "x": 90, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ndumbi", "latitude": 4.56667, "longitude": 13.55, "x": 24.5, "y": 83.3, "source": "geonames", "type": "PPL" }, { "name": "Yoko Betougou", "latitude": 4.75, "longitude": 13.55, "x": 24.5, "y": 10, "source": "geonames", "type": "PPL" } ], "Betare-Oya": [ { "name": "Aba", "latitude": 5.6, "longitude": 14.05, "x": 50, "y": 48.3, "source": "geonames", "type": "PPL" }, { "name": "Bangbel", "latitude": 5.7, "longitude": 14.13333, "x": 75, "y": 27.4, "source": "geonames", "type": "PPL" }, { "name": "Bongo", "latitude": 5.53333, "longitude": 14.18333, "x": 90, "y": 62.2, "source": "geonames", "type": "PPL" }, { "name": "Deoule", "latitude": 5.6, "longitude": 14.03333, "x": 45, "y": 48.3, "source": "geonames", "type": "PPL" }, { "name": "Kissi", "latitude": 5.75, "longitude": 14.11667, "x": 70, "y": 17, "source": "geonames", "type": "PPL" }, { "name": "Kongolo", "latitude": 5.4, "longitude": 14.03333, "x": 45, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Monay", "latitude": 5.78333, "longitude": 14.08333, "x": 60, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Ndanga Gandima", "latitude": 5.41667, "longitude": 14.03333, "x": 45, "y": 86.5, "source": "geonames", "type": "PPL" }, { "name": "Ndokayo", "latitude": 5.51467, "longitude": 14.11923, "x": 70.8, "y": 66.1, "source": "geonames", "type": "PPL" }, { "name": "Ndougla", "latitude": 5.55, "longitude": 14.1, "x": 65, "y": 58.7, "source": "geonames", "type": "PPL" }, { "name": "Oudou", "latitude": 5.46667, "longitude": 14.08333, "x": 60, "y": 76.1, "source": "geonames", "type": "PPL" }, { "name": "Sarambi", "latitude": 5.56667, "longitude": 13.91667, "x": 10, "y": 55.2, "source": "geonames", "type": "PPL" } ], "Bipindi": [ { "name": "Bandewouri", "latitude": 3.05, "longitude": 10.2, "x": 10, "y": 65, "source": "geonames", "type": "PPL" }, { "name": "Bibindi", "latitude": 3.01667, "longitude": 10.41667, "x": 59.5, "y": 75, "source": "geonames", "type": "PPL" }, { "name": "Bidjouka", "latitude": 3.11667, "longitude": 10.46667, "x": 71, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Ebimimbang", "latitude": 3.05, "longitude": 10.48333, "x": 74.8, "y": 65, "source": "geonames", "type": "PPL" }, { "name": "Goap", "latitude": 3.21667, "longitude": 10.26667, "x": 25.2, "y": 15, "source": "geonames", "type": "PPL" }, { "name": "Grand Zambi", "latitude": 3.05, "longitude": 10.28333, "x": 29, "y": 65, "source": "geonames", "type": "PPL" }, { "name": "Lambi", "latitude": 3.1, "longitude": 10.43333, "x": 63.3, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Madoungou", "latitude": 3.06667, "longitude": 10.33333, "x": 40.5, "y": 60, "source": "geonames", "type": "PPL" }, { "name": "Mbikiliki", "latitude": 3.18333, "longitude": 10.55, "x": 90, "y": 25, "source": "geonames", "type": "PPL" }, { "name": "Memel", "latitude": 3.11667, "longitude": 10.4, "x": 55.7, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Mimfombo", "latitude": 3.06667, "longitude": 10.51667, "x": 82.4, "y": 60, "source": "geonames", "type": "PPL" }, { "name": "Ndoa", "latitude": 3.05, "longitude": 10.23333, "x": 17.6, "y": 65, "source": "geonames", "type": "PPL" }, { "name": "Nkolo", "latitude": 3.23333, "longitude": 10.25, "x": 21.4, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Nlongkeng", "latitude": 2.96667, "longitude": 10.45, "x": 67.1, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Nsola", "latitude": 3.16667, "longitude": 10.38333, "x": 51.9, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Saa", "latitude": 3.05, "longitude": 10.46667, "x": 71, "y": 65, "source": "geonames", "type": "PPL" }, { "name": "Tyango", "latitude": 3.05, "longitude": 10.41667, "x": 59.5, "y": 65, "source": "geonames", "type": "PPL" } ], "Bogo": [ { "name": "Aba Tchabi", "latitude": 10.72675, "longitude": 14.47239, "x": 16.9, "y": 51, "source": "geonames", "type": "PPL" }, { "name": "Adjit", "latitude": 10.71214, "longitude": 14.46566, "x": 15.2, "y": 54.9, "source": "geonames", "type": "PPL" }, { "name": "Agaida", "latitude": 10.71064, "longitude": 14.56461, "x": 39.4, "y": 55.4, "source": "geonames", "type": "PPL" }, { "name": "Ardawala", "latitude": 10.78915, "longitude": 14.69755, "x": 72, "y": 34, "source": "geonames", "type": "PPL" }, { "name": "Atiba", "latitude": 10.70749, "longitude": 14.75539, "x": 86.1, "y": 56.2, "source": "geonames", "type": "PPL" }, { "name": "Babadam", "latitude": 10.78182, "longitude": 14.73783, "x": 81.8, "y": 36, "source": "geonames", "type": "PPL" }, { "name": "Badeo", "latitude": 10.79462, "longitude": 14.46858, "x": 15.9, "y": 32.5, "source": "geonames", "type": "PPL" }, { "name": "Badjioal", "latitude": 10.79102, "longitude": 14.58756, "x": 45.1, "y": 33.4, "source": "geonames", "type": "PPL" }, { "name": "Bagalaf", "latitude": 10.69611, "longitude": 14.53314, "x": 31.7, "y": 59.3, "source": "geonames", "type": "PPL" }, { "name": "Bakourehi", "latitude": 10.85, "longitude": 14.58333, "x": 44, "y": 17.4, "source": "geonames", "type": "PPL" }, { "name": "Balaza Alcali", "latitude": 10.69511, "longitude": 14.47363, "x": 17.2, "y": 59.6, "source": "geonames", "type": "PPL" }, { "name": "Balaza Louane", "latitude": 10.69183, "longitude": 14.45118, "x": 11.7, "y": 60.5, "source": "geonames", "type": "PPL" }, { "name": "BalazaDomayo", "latitude": 10.67806, "longitude": 14.47529, "x": 17.6, "y": 64.2, "source": "geonames", "type": "PPL" }, { "name": "Balda", "latitude": 10.84622, "longitude": 14.65778, "x": 62.2, "y": 18.4, "source": "geonames", "type": "PPL" }, { "name": "Baouli Ardjanire", "latitude": 10.7049, "longitude": 14.69059, "x": 70.3, "y": 56.9, "source": "geonames", "type": "PPL" }, { "name": "Baouli Boreire", "latitude": 10.75115, "longitude": 14.70292, "x": 73.3, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Baouli Hamadou", "latitude": 10.76122, "longitude": 14.70041, "x": 72.7, "y": 41.6, "source": "geonames", "type": "PPL" }, { "name": "Baouli Naibi", "latitude": 10.73441, "longitude": 14.70109, "x": 72.8, "y": 48.9, "source": "geonames", "type": "PPL" }, { "name": "Bariyaere", "latitude": 10.68395, "longitude": 14.75892, "x": 87, "y": 62.6, "source": "geonames", "type": "PPL" }, { "name": "Bawo", "latitude": 10.70069, "longitude": 14.4935, "x": 22, "y": 58.1, "source": "geonames", "type": "PPLX" }, { "name": "Bernassi", "latitude": 10.70076, "longitude": 14.47419, "x": 17.3, "y": 58.1, "source": "geonames", "type": "PPL" }, { "name": "Borey", "latitude": 10.66126, "longitude": 14.63869, "x": 57.6, "y": 68.8, "source": "geonames", "type": "PPL" }, { "name": "Boudou", "latitude": 10.68708, "longitude": 14.66202, "x": 63.3, "y": 61.8, "source": "geonames", "type": "PPL" }, { "name": "Boulare", "latitude": 10.84176, "longitude": 14.68216, "x": 68.2, "y": 19.6, "source": "geonames", "type": "PPL" }, { "name": "Dakelouo", "latitude": 10.79494, "longitude": 14.49835, "x": 23.2, "y": 32.4, "source": "geonames", "type": "PPL" }, { "name": "DangaNyebe", "latitude": 10.76231, "longitude": 14.44871, "x": 11.1, "y": 41.3, "source": "geonames", "type": "PPL" }, { "name": "Danki", "latitude": 10.80765, "longitude": 14.63847, "x": 57.5, "y": 28.9, "source": "geonames", "type": "PPL" }, { "name": "Darak", "latitude": 10.6316, "longitude": 14.6588, "x": 62.5, "y": 76.9, "source": "geonames", "type": "PPL" }, { "name": "Diguir", "latitude": 10.71644, "longitude": 14.50127, "x": 23.9, "y": 53.8, "source": "geonames", "type": "PPL" }, { "name": "Djedjebe", "latitude": 10.84164, "longitude": 14.51093, "x": 26.3, "y": 19.6, "source": "geonames", "type": "PPL" }, { "name": "Djibire", "latitude": 10.69723, "longitude": 14.47591, "x": 17.7, "y": 59, "source": "geonames", "type": "PPL" }, { "name": "Djidere Balol", "latitude": 10.83432, "longitude": 14.7118, "x": 75.5, "y": 21.6, "source": "geonames", "type": "PPL" }, { "name": "Doubbel", "latitude": 10.82361, "longitude": 14.47635, "x": 17.8, "y": 24.6, "source": "geonames", "type": "PPL" }, { "name": "Fadama", "latitude": 10.86174, "longitude": 14.63754, "x": 57.3, "y": 14.2, "source": "geonames", "type": "PPL" }, { "name": "Fort Lamy", "latitude": 10.59214, "longitude": 14.53977, "x": 33.4, "y": 87.7, "source": "geonames", "type": "PPL" }, { "name": "Gadakaral", "latitude": 10.72197, "longitude": 14.64181, "x": 58.3, "y": 52.3, "source": "geonames", "type": "PPL" }, { "name": "Gadakaral Djadjil", "latitude": 10.77153, "longitude": 14.56162, "x": 38.7, "y": 38.8, "source": "geonames", "type": "PPL" }, { "name": "Gadamayo", "latitude": 10.81588, "longitude": 14.48301, "x": 19.5, "y": 26.7, "source": "geonames", "type": "PPL" }, { "name": "Galaga", "latitude": 10.64834, "longitude": 14.56014, "x": 38.3, "y": 72.3, "source": "geonames", "type": "PPL" }, { "name": "Golombere", "latitude": 10.74989, "longitude": 14.76941, "x": 89.6, "y": 44.7, "source": "geonames", "type": "PPL" }, { "name": "Gouzouboulam", "latitude": 10.77255, "longitude": 14.71334, "x": 75.8, "y": 38.5, "source": "geonames", "type": "PPL" }, { "name": "Guiddeo", "latitude": 10.63049, "longitude": 14.58119, "x": 43.5, "y": 77.2, "source": "geonames", "type": "PPL" }, { "name": "Guingley", "latitude": 10.78114, "longitude": 14.68152, "x": 68.1, "y": 36.1, "source": "geonames", "type": "PPL" }, { "name": "Guirle", "latitude": 10.6195, "longitude": 14.64757, "x": 59.7, "y": 80.2, "source": "geonames", "type": "PPL" }, { "name": "Hardowo", "latitude": 10.63467, "longitude": 14.55586, "x": 37.3, "y": 76.1, "source": "geonames", "type": "PPL" }, { "name": "Hodande", "latitude": 10.69107, "longitude": 14.49194, "x": 21.7, "y": 60.7, "source": "geonames", "type": "PPL" }, { "name": "IssouoGuinadji", "latitude": 10.79917, "longitude": 14.49565, "x": 22.6, "y": 31.2, "source": "geonames", "type": "PPL" }, { "name": "Kaodjiga", "latitude": 10.82685, "longitude": 14.58682, "x": 44.9, "y": 23.7, "source": "geonames", "type": "PPL" }, { "name": "Karagari", "latitude": 10.74635, "longitude": 14.48043, "x": 18.8, "y": 45.6, "source": "geonames", "type": "PPL" }, { "name": "Kareo", "latitude": 10.63604, "longitude": 14.62007, "x": 53, "y": 75.7, "source": "geonames", "type": "PPL" }, { "name": "Kawaya", "latitude": 10.61344, "longitude": 14.70124, "x": 72.9, "y": 81.9, "source": "geonames", "type": "PPL" }, { "name": "Kidjeme", "latitude": 10.84992, "longitude": 14.4957, "x": 22.6, "y": 17.4, "source": "geonames", "type": "PPL" }, { "name": "Koro Guiddeo", "latitude": 10.59295, "longitude": 14.69254, "x": 70.7, "y": 87.4, "source": "geonames", "type": "PPL" }, { "name": "Kourdaya", "latitude": 10.87288, "longitude": 14.659, "x": 62.5, "y": 11.1, "source": "geonames", "type": "PPL" }, { "name": "Mabiouo", "latitude": 10.69567, "longitude": 14.46913, "x": 16.1, "y": 59.4, "source": "geonames", "type": "PPL" }, { "name": "Madaka Lawanat", "latitude": 10.75, "longitude": 14.55, "x": 35.9, "y": 44.6, "source": "geonames", "type": "PPL" }, { "name": "Madide", "latitude": 10.82377, "longitude": 14.61478, "x": 51.7, "y": 24.5, "source": "geonames", "type": "PPL" }, { "name": "Mahel", "latitude": 10.72442, "longitude": 14.46265, "x": 14.5, "y": 51.6, "source": "geonames", "type": "PPL" }, { "name": "Malam Mango", "latitude": 10.86667, "longitude": 14.55, "x": 35.9, "y": 12.8, "source": "geonames", "type": "PPL" }, { "name": "Malam Wadjao", "latitude": 10.87643, "longitude": 14.5645, "x": 39.4, "y": 10.2, "source": "geonames", "type": "PPL" }, { "name": "Marvak", "latitude": 10.73, "longitude": 14.76831, "x": 89.3, "y": 50.1, "source": "geonames", "type": "PPL" }, { "name": "Mayel Guinadji", "latitude": 10.78692, "longitude": 14.50773, "x": 25.5, "y": 34.6, "source": "geonames", "type": "PPL" }, { "name": "Messere", "latitude": 10.72112, "longitude": 14.53756, "x": 32.8, "y": 52.5, "source": "geonames", "type": "PPL" }, { "name": "Metche", "latitude": 10.68958, "longitude": 14.506, "x": 25.1, "y": 61.1, "source": "geonames", "type": "PPLX" }, { "name": "Mogozomay", "latitude": 10.76248, "longitude": 14.66367, "x": 63.7, "y": 41.2, "source": "geonames", "type": "PPL" }, { "name": "Mokodos", "latitude": 10.75008, "longitude": 14.67158, "x": 65.6, "y": 44.6, "source": "geonames", "type": "PPL" }, { "name": "Molkore", "latitude": 10.68405, "longitude": 14.48594, "x": 20.2, "y": 62.6, "source": "geonames", "type": "PPL" }, { "name": "Mondire", "latitude": 10.6572, "longitude": 14.46779, "x": 15.7, "y": 69.9, "source": "geonames", "type": "PPL" }, { "name": "Mororo", "latitude": 10.7503, "longitude": 14.61126, "x": 50.9, "y": 44.5, "source": "geonames", "type": "PPL" }, { "name": "Nderere", "latitude": 10.75047, "longitude": 14.63117, "x": 55.7, "y": 44.5, "source": "geonames", "type": "PPL" }, { "name": "Ndjimkilo", "latitude": 10.8193, "longitude": 14.4937, "x": 22.1, "y": 25.7, "source": "geonames", "type": "PPL" }, { "name": "Ngaba", "latitude": 10.82479, "longitude": 14.53844, "x": 33, "y": 24.2, "source": "geonames", "type": "PPL" }, { "name": "Ngaba Tonguel", "latitude": 10.81484, "longitude": 14.5136, "x": 27, "y": 26.9, "source": "geonames", "type": "PPL" }, { "name": "Ouro Alao", "latitude": 10.73708, "longitude": 14.71036, "x": 75.1, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Ouro Atikou", "latitude": 10.86562, "longitude": 14.54487, "x": 34.6, "y": 13.1, "source": "geonames", "type": "PPL" }, { "name": "Ouro Daye", "latitude": 10.61722, "longitude": 14.62845, "x": 55.1, "y": 80.8, "source": "geonames", "type": "PPL" }, { "name": "Ouro Gabdo", "latitude": 10.62534, "longitude": 14.674, "x": 66.2, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Ouro Liman", "latitude": 10.60863, "longitude": 14.66977, "x": 65.2, "y": 83.2, "source": "geonames", "type": "PPL" }, { "name": "Ouro Moussa", "latitude": 10.66714, "longitude": 14.69559, "x": 71.5, "y": 67.2, "source": "geonames", "type": "PPL" }, { "name": "Ouro Ngama", "latitude": 10.79961, "longitude": 14.52487, "x": 29.7, "y": 31.1, "source": "geonames", "type": "PPL" }, { "name": "Ouro Sadi", "latitude": 10.71322, "longitude": 14.58383, "x": 44.1, "y": 54.7, "source": "geonames", "type": "PPL" }, { "name": "Ouro Tehekehi", "latitude": 10.68795, "longitude": 14.71326, "x": 75.8, "y": 61.5, "source": "geonames", "type": "PPL" }, { "name": "Ouro Yaya", "latitude": 10.75963, "longitude": 14.62085, "x": 53.2, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Ouro Zanga", "latitude": 10.83192, "longitude": 14.65835, "x": 62.4, "y": 22.3, "source": "geonames", "type": "PPL" }, { "name": "OuroAbadama", "latitude": 10.71447, "longitude": 14.4805, "x": 18.9, "y": 54.3, "source": "geonames", "type": "PPL" }, { "name": "OuroAbalodo", "latitude": 10.71412, "longitude": 14.45293, "x": 12.1, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "OuroBaldima", "latitude": 10.68183, "longitude": 14.45506, "x": 12.6, "y": 63.2, "source": "geonames", "type": "PPL" }, { "name": "OuroBideidi", "latitude": 10.71342, "longitude": 14.44714, "x": 10.7, "y": 54.6, "source": "geonames", "type": "PPL" }, { "name": "OuroBonyo", "latitude": 10.69693, "longitude": 14.45904, "x": 13.6, "y": 59.1, "source": "geonames", "type": "PPL" }, { "name": "OuroGaldima", "latitude": 10.69247, "longitude": 14.46914, "x": 16.1, "y": 60.3, "source": "geonames", "type": "PPL" }, { "name": "OuroKatchala", "latitude": 10.72174, "longitude": 14.48356, "x": 19.6, "y": 52.3, "source": "geonames", "type": "PPL" }, { "name": "OuroKatchalassali", "latitude": 10.74884, "longitude": 14.48297, "x": 19.5, "y": 44.9, "source": "geonames", "type": "PPL" }, { "name": "OuroMaloabi", "latitude": 10.68831, "longitude": 14.47, "x": 16.3, "y": 61.4, "source": "geonames", "type": "PPL" }, { "name": "OuroSayar", "latitude": 10.67179, "longitude": 14.46838, "x": 15.9, "y": 65.9, "source": "geonames", "type": "PPL" }, { "name": "OuroTchadloudi", "latitude": 10.69611, "longitude": 14.44685, "x": 10.6, "y": 59.3, "source": "geonames", "type": "PPL" }, { "name": "OuroTchode", "latitude": 10.68247, "longitude": 14.46775, "x": 15.7, "y": 63, "source": "geonames", "type": "PPL" }, { "name": "OuroYerema", "latitude": 10.67809, "longitude": 14.48615, "x": 20.2, "y": 64.2, "source": "geonames", "type": "PPL" }, { "name": "Reque", "latitude": 10.81704, "longitude": 14.71901, "x": 77.2, "y": 26.3, "source": "geonames", "type": "PPL" }, { "name": "Sabare", "latitude": 10.73361, "longitude": 14.5671, "x": 40.1, "y": 49.1, "source": "geonames", "type": "PPL" }, { "name": "Salakere", "latitude": 10.80824, "longitude": 14.54874, "x": 35.6, "y": 28.7, "source": "geonames", "type": "PPL" }, { "name": "Sawao", "latitude": 10.73271, "longitude": 14.5209, "x": 28.7, "y": 49.3, "source": "geonames", "type": "PPL" }, { "name": "Sedek", "latitude": 10.6725, "longitude": 14.72481, "x": 78.6, "y": 65.8, "source": "geonames", "type": "PPL" }, { "name": "Seratare", "latitude": 10.69082, "longitude": 14.47839, "x": 18.3, "y": 60.8, "source": "geonames", "type": "PPL" }, { "name": "Takay", "latitude": 10.87701, "longitude": 14.53725, "x": 32.7, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Tankirou", "latitude": 10.62568, "longitude": 14.56208, "x": 38.8, "y": 78.5, "source": "geonames", "type": "PPL" }, { "name": "Taram", "latitude": 10.71403, "longitude": 14.77121, "x": 90, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "Tayle", "latitude": 10.5957, "longitude": 14.6605, "x": 62.9, "y": 86.7, "source": "geonames", "type": "PPL" }, { "name": "Tchabaol", "latitude": 10.8064, "longitude": 14.68239, "x": 68.3, "y": 29.3, "source": "geonames", "type": "PPL" }, { "name": "Tchaloudi", "latitude": 10.7201, "longitude": 14.44603, "x": 10.4, "y": 52.8, "source": "geonames", "type": "PPL" }, { "name": "Tchalouwol", "latitude": 10.8674, "longitude": 14.6685, "x": 64.9, "y": 12.6, "source": "geonames", "type": "PPL" }, { "name": "Tchebiwo", "latitude": 10.60028, "longitude": 14.55647, "x": 37.4, "y": 85.4, "source": "geonames", "type": "PPL" }, { "name": "Tchoufol Kraguire", "latitude": 10.7802, "longitude": 14.60292, "x": 48.8, "y": 36.4, "source": "geonames", "type": "PPL" }, { "name": "Wakabongou", "latitude": 10.69469, "longitude": 14.5637, "x": 39.2, "y": 59.7, "source": "geonames", "type": "PPL" }, { "name": "Wayamayo", "latitude": 10.65407, "longitude": 14.69416, "x": 71.1, "y": 70.8, "source": "geonames", "type": "PPL" }, { "name": "Yagabouba", "latitude": 10.75845, "longitude": 14.4443, "x": 10, "y": 42.3, "source": "geonames", "type": "PPL" }, { "name": "YagaOuroLaoannSainn", "latitude": 10.78343, "longitude": 14.45133, "x": 11.7, "y": 35.5, "source": "geonames", "type": "PPL" }, { "name": "Yolde", "latitude": 10.85625, "longitude": 14.58879, "x": 45.4, "y": 15.7, "source": "geonames", "type": "PPL" }, { "name": "Yolde Tchoukkol", "latitude": 10.83333, "longitude": 14.63333, "x": 56.3, "y": 21.9, "source": "geonames", "type": "PPL" }, { "name": "Yoldeo", "latitude": 10.58357, "longitude": 14.55499, "x": 37.1, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Yolel", "latitude": 10.59925, "longitude": 14.63063, "x": 55.6, "y": 85.7, "source": "geonames", "type": "PPL" } ], "Buea": [ { "name": "Bitingui", "latitude": 4.1742487, "longitude": 9.2807099, "x": 68.1, "y": 53.4, "source": "osm", "type": "village" }, { "name": "Biuku", "latitude": 4.1143, "longitude": 9.2463, "x": 59.1, "y": 76.9, "source": "geonames", "type": "PPL" }, { "name": "Blessing Station", "latitude": 4.1423535, "longitude": 9.3070681, "x": 75.1, "y": 65.9, "source": "osm", "type": "locality" }, { "name": "Bokoko", "latitude": 4.1543436, "longitude": 9.2707471, "x": 65.5, "y": 61.2, "source": "osm", "type": "village" }, { "name": "Bokova", "latitude": 4.1951481, "longitude": 9.2780796, "x": 67.5, "y": 45.2, "source": "osm", "type": "village" }, { "name": "Bokwae", "latitude": 4.1727, "longitude": 9.2688, "x": 65, "y": 54, "source": "geonames", "type": "PPL" }, { "name": "Bokwai", "latitude": 4.1701626, "longitude": 9.2699715, "x": 65.3, "y": 55, "source": "osm", "type": "village" }, { "name": "Bokwango", "latitude": 4.1343413, "longitude": 9.2211932, "x": 52.4, "y": 69, "source": "osm", "type": "village" }, { "name": "Bokwango junction", "latitude": 4.1400455, "longitude": 9.2275064, "x": 54.1, "y": 66.8, "source": "osm", "type": "locality" }, { "name": "Bolifamba", "latitude": 4.1362197, "longitude": 9.3113582, "x": 76.2, "y": 68.3, "source": "osm", "type": "village" }, { "name": "Bomana Bakweri", "latitude": 4.2848, "longitude": 9.0603, "x": 10, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Bonakanda", "latitude": 4.2060835, "longitude": 9.2737666, "x": 66.3, "y": 40.9, "source": "osm", "type": "village" }, { "name": "Boniamavio", "latitude": 4.1507958, "longitude": 9.3235425, "x": 79.4, "y": 62.6, "source": "osm", "type": "village" }, { "name": "Bova", "latitude": 4.1871444, "longitude": 9.2632094, "x": 63.5, "y": 48.3, "source": "osm", "type": "village" }, { "name": "Bowanda", "latitude": 4.1413881, "longitude": 9.3211494, "x": 78.8, "y": 66.3, "source": "osm", "type": "village" }, { "name": "Bulu", "latitude": 4.1419026, "longitude": 9.3009848, "x": 73.5, "y": 66.1, "source": "osm", "type": "village" }, { "name": "Bwassa", "latitude": 4.1158514, "longitude": 9.2116141, "x": 49.9, "y": 76.3, "source": "osm", "type": "village" }, { "name": "Carrefour biaka", "latitude": 4.15868, "longitude": 9.273054, "x": 66.1, "y": 59.5, "source": "osm", "type": "locality" }, { "name": "Chefferie de Lower Muea", "latitude": 4.1700485, "longitude": 9.3020026, "x": 73.8, "y": 55, "source": "osm", "type": "locality" }, { "name": "Ekona Mbenge", "latitude": 4.2302649, "longitude": 9.3348554, "x": 82.4, "y": 31.4, "source": "osm", "type": "village" }, { "name": "Ekona Yard", "latitude": 4.2146139, "longitude": 9.3351587, "x": 82.5, "y": 37.5, "source": "osm", "type": "village" }, { "name": "Ewonda", "latitude": 4.1769192, "longitude": 9.2543719, "x": 61.2, "y": 52.3, "source": "osm", "type": "village" }, { "name": "GARE ROUTIERE MILE 17", "latitude": 4.1508047, "longitude": 9.3005202, "x": 73.4, "y": 62.6, "source": "osm", "type": "bus_station" }, { "name": "Gares Routiere", "latitude": 4.0910931, "longitude": 9.315527, "x": 77.3, "y": 86, "source": "osm", "type": "bus_station" }, { "name": "Great Sopo", "latitude": 4.1517, "longitude": 9.2514, "x": 60.4, "y": 62.2, "source": "geonames", "type": "PPL" }, { "name": "Likoko Mimbia", "latitude": 4.1448, "longitude": 9.2275, "x": 54.1, "y": 64.9, "source": "geonames", "type": "PPL" }, { "name": "Likombe", "latitude": 4.1063788, "longitude": 9.1996219, "x": 46.8, "y": 80, "source": "osm", "type": "village" }, { "name": "Lissoka", "latitude": 4.2139223, "longitude": 9.3029847, "x": 74, "y": 37.8, "source": "osm", "type": "village" }, { "name": "Lower Muea", "latitude": 4.1704922, "longitude": 9.2994642, "x": 73.1, "y": 54.8, "source": "osm", "type": "locality" }, { "name": "Mamu", "latitude": 4.2051954, "longitude": 9.3199298, "x": 78.5, "y": 41.2, "source": "osm", "type": "village" }, { "name": "Meviokulu village", "latitude": 4.0808584, "longitude": 9.2660477, "x": 64.3, "y": 90, "source": "osm", "type": "village" }, { "name": "Mile 16", "latitude": 4.1427084, "longitude": 9.3059618, "x": 74.8, "y": 65.7, "source": "osm", "type": "village" }, { "name": "Mile 17", "latitude": 4.1502427, "longitude": 9.3001245, "x": 73.3, "y": 62.8, "source": "osm", "type": "bus_station" }, { "name": "Moliko", "latitude": 4.1529, "longitude": 9.285, "x": 69.3, "y": 61.7, "source": "geonames", "type": "PPL" }, { "name": "Molyko", "latitude": 4.1591154, "longitude": 9.2805172, "x": 68.1, "y": 59.3, "source": "osm", "type": "village" }, { "name": "Muea", "latitude": 4.1703879, "longitude": 9.3071177, "x": 75.1, "y": 54.9, "source": "osm", "type": "village" }, { "name": "Musaka", "latitude": 4.1709476, "longitude": 9.3138712, "x": 76.9, "y": 54.7, "source": "osm", "type": "locality" }, { "name": "Mutengene", "latitude": 4.09011, "longitude": 9.3169578, "x": 77.7, "y": 86.4, "source": "osm", "type": "town" }, { "name": "Ndu", "latitude": 4.26667, "longitude": 9.23333, "x": 55.6, "y": 17.1, "source": "geonames", "type": "PPL" }, { "name": "NEW STORES", "latitude": 4.083042, "longitude": 9.3559328, "x": 88, "y": 89.1, "source": "osm", "type": "marketplace" }, { "name": "Ombe", "latitude": 4.0814832, "longitude": 9.2904632, "x": 70.7, "y": 89.8, "source": "osm", "type": "town" }, { "name": "PONT", "latitude": 4.1545746, "longitude": 9.2471724, "x": 59.3, "y": 61.1, "source": "osm", "type": "locality" }, { "name": "Powo Camp", "latitude": 4.2368939, "longitude": 9.3635559, "x": 90, "y": 28.8, "source": "osm", "type": "village" }, { "name": "Sasse", "latitude": 4.1140877, "longitude": 9.2274789, "x": 54.1, "y": 77, "source": "osm", "type": "village" }, { "name": "Saxenhof", "latitude": 4.0982, "longitude": 9.2172, "x": 51.4, "y": 83.2, "source": "geonames", "type": "PPL" }, { "name": "Saxoenhof", "latitude": 4.0969936, "longitude": 9.2152855, "x": 50.9, "y": 83.7, "source": "osm", "type": "village" }, { "name": "Small Sopo", "latitude": 4.1241065, "longitude": 9.2528489, "x": 60.8, "y": 73, "source": "osm", "type": "village" }, { "name": "Small-Soppo", "latitude": 4.1469279, "longitude": 9.2415169, "x": 57.8, "y": 64.1, "source": "osm", "type": "suburb" }, { "name": "Station BOCOM Muea", "latitude": 4.1691897, "longitude": 9.3018915, "x": 73.7, "y": 55.4, "source": "osm", "type": "locality" }, { "name": "Tamben", "latitude": 4.113794, "longitude": 9.3438681, "x": 84.8, "y": 77.1, "source": "osm", "type": "village" }, { "name": "Tole", "latitude": 4.1149041, "longitude": 9.2458106, "x": 58.9, "y": 76.6, "source": "osm", "type": "village" }, { "name": "Wona Ma Vio", "latitude": 4.1788716, "longitude": 9.3034018, "x": 74.1, "y": 51.6, "source": "osm", "type": "locality" } ], "Campo": [ { "name": "Afan", "latitude": 2.38333, "longitude": 10, "x": 72.9, "y": 53.3, "source": "geonames", "type": "PPL" }, { "name": "Akak", "latitude": 2.36667, "longitude": 9.98333, "x": 67.1, "y": 56.7, "source": "geonames", "type": "PPL" }, { "name": "Badjele", "latitude": 2.51667, "longitude": 9.88333, "x": 32.9, "y": 26.7, "source": "geonames", "type": "PPL" }, { "name": "Beio", "latitude": 2.6, "longitude": 9.83333, "x": 15.7, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Bendji", "latitude": 2.55, "longitude": 9.83333, "x": 15.7, "y": 20, "source": "geonames", "type": "PPL" }, { "name": "Bibabimwoto", "latitude": 2.35, "longitude": 9.91667, "x": 44.3, "y": 60, "source": "geonames", "type": "PPL" }, { "name": "Bibae", "latitude": 2.36667, "longitude": 9.9, "x": 38.6, "y": 56.7, "source": "geonames", "type": "PPL" }, { "name": "Bitande", "latitude": 2.31667, "longitude": 9.95, "x": 55.7, "y": 66.7, "source": "geonames", "type": "PPL" }, { "name": "Bitande-Assok", "latitude": 2.36667, "longitude": 9.96667, "x": 61.4, "y": 56.7, "source": "geonames", "type": "PPL" }, { "name": "Bitande-Moloko", "latitude": 2.3, "longitude": 9.95, "x": 55.7, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Bouandjo", "latitude": 2.5, "longitude": 9.83333, "x": 15.7, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Campa-Beach", "latitude": 2.35, "longitude": 9.81667, "x": 10, "y": 60, "source": "geonames", "type": "PPL" }, { "name": "Dipikar", "latitude": 2.25, "longitude": 9.91667, "x": 44.3, "y": 80, "source": "geonames", "type": "PPL" }, { "name": "Doum", "latitude": 2.36667, "longitude": 9.95, "x": 55.7, "y": 56.7, "source": "geonames", "type": "PPL" }, { "name": "Ebodie", "latitude": 2.56667, "longitude": 9.83333, "x": 15.7, "y": 16.7, "source": "geonames", "type": "PPL" }, { "name": "Edjom", "latitude": 2.41667, "longitude": 10, "x": 72.9, "y": 46.7, "source": "geonames", "type": "PPL" }, { "name": "Ekout Mintoum", "latitude": 2.4, "longitude": 10.05, "x": 90, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Epiniandji", "latitude": 2.58333, "longitude": 9.83333, "x": 15.7, "y": 13.3, "source": "geonames", "type": "PPL" }, { "name": "Essokie", "latitude": 2.38333, "longitude": 10.01667, "x": 78.6, "y": 53.3, "source": "geonames", "type": "PPL" }, { "name": "Eyon-Bot", "latitude": 2.28333, "longitude": 9.95, "x": 55.7, "y": 73.3, "source": "geonames", "type": "PPL" }, { "name": "Itonde sur Mer", "latitude": 2.41667, "longitude": 9.81667, "x": 10, "y": 46.7, "source": "geonames", "type": "PPL" }, { "name": "Itondefang", "latitude": 2.4, "longitude": 9.83333, "x": 15.7, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Lalemoto", "latitude": 2.58333, "longitude": 9.83333, "x": 15.7, "y": 13.3, "source": "geonames", "type": "PPL" }, { "name": "Mabiogo", "latitude": 2.3, "longitude": 9.88333, "x": 32.9, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Malaba", "latitude": 2.46667, "longitude": 9.83333, "x": 15.7, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Mbondo", "latitude": 2.45, "longitude": 9.81667, "x": 10, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Melogo", "latitude": 2.23333, "longitude": 9.91667, "x": 44.3, "y": 83.3, "source": "geonames", "type": "PPL" }, { "name": "Mintom", "latitude": 2.36667, "longitude": 9.86667, "x": 27.1, "y": 56.7, "source": "geonames", "type": "PPL" }, { "name": "Mipe", "latitude": 2.2, "longitude": 9.95, "x": 55.7, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Moloko", "latitude": 2.21667, "longitude": 9.93333, "x": 50, "y": 86.7, "source": "geonames", "type": "PPL" }, { "name": "Mvasse", "latitude": 2.36667, "longitude": 9.86667, "x": 27.1, "y": 56.7, "source": "geonames", "type": "PPL" }, { "name": "Nangabodoua", "latitude": 2.38333, "longitude": 9.81667, "x": 10, "y": 53.3, "source": "geonames", "type": "PPL" }, { "name": "Nkoadjap", "latitude": 2.36667, "longitude": 9.95, "x": 55.7, "y": 56.7, "source": "geonames", "type": "PPL" }, { "name": "Nkolbikok", "latitude": 2.35, "longitude": 9.93333, "x": 50, "y": 60, "source": "geonames", "type": "PPL" }, { "name": "Nkolenieng", "latitude": 2.43333, "longitude": 10.01667, "x": 78.6, "y": 43.3, "source": "geonames", "type": "PPL" }, { "name": "Nkong-Milong", "latitude": 2.36667, "longitude": 9.9, "x": 38.6, "y": 56.7, "source": "geonames", "type": "PPL" }, { "name": "Nloayong", "latitude": 2.38333, "longitude": 10.03333, "x": 84.3, "y": 53.3, "source": "geonames", "type": "PPL" }, { "name": "Pande", "latitude": 2.51667, "longitude": 9.83333, "x": 15.7, "y": 26.7, "source": "geonames", "type": "PPL" }, { "name": "Tala", "latitude": 2.56667, "longitude": 9.83333, "x": 15.7, "y": 16.7, "source": "geonames", "type": "PPL" }, { "name": "Tondefom", "latitude": 2.36667, "longitude": 9.85, "x": 21.4, "y": 56.7, "source": "geonames", "type": "PPL" }, { "name": "Ukono", "latitude": 2.58333, "longitude": 9.83333, "x": 15.7, "y": 13.3, "source": "geonames", "type": "PPL" }, { "name": "Ypono", "latitude": 2.33333, "longitude": 9.85, "x": 21.4, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Zok-Assembang", "latitude": 2.25, "longitude": 9.95, "x": 55.7, "y": 80, "source": "geonames", "type": "PPL" } ], "Deido": [ { "name": "Bangue", "latitude": 4.1055, "longitude": 9.7552, "x": 35, "y": 43.2, "source": "geonames", "type": "PPL" }, { "name": "Bassa", "latitude": 4.0559, "longitude": 9.7475, "x": 32.3, "y": 60.7, "source": "geonames", "type": "PPL" }, { "name": "Bassa Ndokati", "latitude": 4.08333, "longitude": 9.7, "x": 15.8, "y": 51, "source": "geonames", "type": "PPL" }, { "name": "Bayis", "latitude": 4.1107, "longitude": 9.8142, "x": 55.5, "y": 41.4, "source": "geonames", "type": "PPL" }, { "name": "Bessengue", "latitude": 4.0326, "longitude": 9.8096, "x": 53.9, "y": 68.9, "source": "geonames", "type": "PPL" }, { "name": "Bogbo", "latitude": 4.0766, "longitude": 9.8565, "x": 70.1, "y": 53.4, "source": "geonames", "type": "PPL" }, { "name": "Bomkoul", "latitude": 4.0942, "longitude": 9.8013, "x": 51, "y": 47.2, "source": "geonames", "type": "PPL" }, { "name": "Bonamatoumbe", "latitude": 4.1096, "longitude": 9.6832, "x": 10, "y": 41.8, "source": "geonames", "type": "PPL" }, { "name": "Bonamouang", "latitude": 4.0874, "longitude": 9.7251, "x": 24.5, "y": 49.6, "source": "geonames", "type": "PPL" }, { "name": "Bonangang", "latitude": 4.1042, "longitude": 9.7391, "x": 29.4, "y": 43.7, "source": "geonames", "type": "PPL" }, { "name": "Boro", "latitude": 4.2, "longitude": 9.86667, "x": 73.7, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Carrefour Ndokoti", "latitude": 4.04345, "longitude": 9.74248, "x": 30.6, "y": 65.1, "source": "geonames", "type": "PPLX" }, { "name": "Djapoma", "latitude": 4.0365, "longitude": 9.8196, "x": 57.3, "y": 67.5, "source": "geonames", "type": "PPL" }, { "name": "Djebale", "latitude": 4.1094, "longitude": 9.702, "x": 16.5, "y": 41.9, "source": "geonames", "type": "PPL" }, { "name": "Kambo", "latitude": 4.0299, "longitude": 9.7962, "x": 49.2, "y": 69.8, "source": "geonames", "type": "PPL" }, { "name": "Kondjok", "latitude": 4.0504, "longitude": 9.8631, "x": 72.4, "y": 62.6, "source": "geonames", "type": "PPL" }, { "name": "Koto", "latitude": 4.0957, "longitude": 9.7572, "x": 35.7, "y": 46.7, "source": "geonames", "type": "PPL" }, { "name": "Lendi", "latitude": 4.1265, "longitude": 9.7759, "x": 42.2, "y": 35.8, "source": "geonames", "type": "PPL" }, { "name": "Logbessou", "latitude": 4.0808, "longitude": 9.7949, "x": 48.8, "y": 51.9, "source": "geonames", "type": "PPL" }, { "name": "Logpom", "latitude": 4.084, "longitude": 9.7675, "x": 39.3, "y": 50.8, "source": "geonames", "type": "PPL" }, { "name": "Mahohi", "latitude": 4.1014, "longitude": 9.8438, "x": 65.7, "y": 44.7, "source": "geonames", "type": "PPL" }, { "name": "Makepe Missoke", "latitude": 4.0605, "longitude": 9.73592, "x": 28.3, "y": 59.1, "source": "geonames", "type": "PPLX" }, { "name": "Malangue", "latitude": 4.0684, "longitude": 9.7586, "x": 36.2, "y": 56.3, "source": "geonames", "type": "PPL" }, { "name": "Massoumbou Carrefour", "latitude": 4.1249, "longitude": 9.8301, "x": 61, "y": 36.4, "source": "geonames", "type": "PPL" }, { "name": "Massoumbou-Chantiers", "latitude": 4.0842, "longitude": 9.8737, "x": 76.1, "y": 50.7, "source": "geonames", "type": "PPL" }, { "name": "Mbanga", "latitude": 4.0066, "longitude": 9.8002, "x": 50.6, "y": 78, "source": "geonames", "type": "PPL" }, { "name": "Ndoghem-Poudriere", "latitude": 4.073, "longitude": 9.794, "x": 48.5, "y": 54.7, "source": "geonames", "type": "PPL" }, { "name": "Ndole", "latitude": 4.0588, "longitude": 9.8843, "x": 79.8, "y": 59.7, "source": "geonames", "type": "PPL" }, { "name": "Ngoma", "latitude": 4.1157, "longitude": 9.7884, "x": 46.5, "y": 39.6, "source": "geonames", "type": "PPL" }, { "name": "Niala", "latitude": 4.0378, "longitude": 9.7646, "x": 38.3, "y": 67, "source": "geonames", "type": "PPL" }, { "name": "Nkolmbong", "latitude": 4.1145, "longitude": 9.8375, "x": 63.6, "y": 40.1, "source": "geonames", "type": "PPL" }, { "name": "Piti", "latitude": 4.0293, "longitude": 9.9137, "x": 90, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Yasika", "latitude": 3.9725, "longitude": 9.81556, "x": 55.9, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Yassa", "latitude": 3.98333, "longitude": 9.81667, "x": 56.3, "y": 86.2, "source": "geonames", "type": "PPL" } ], "Dibombari": [ { "name": "Babenga", "latitude": 4.107, "longitude": 9.5744, "x": 25.7, "y": 85.3, "source": "geonames", "type": "PPL" }, { "name": "Bakongkom", "latitude": 4.3138, "longitude": 9.6851, "x": 52.1, "y": 25.3, "source": "geonames", "type": "PPL" }, { "name": "Bamwen", "latitude": 4.3567, "longitude": 9.7458, "x": 66.6, "y": 12.9, "source": "geonames", "type": "PPL" }, { "name": "Bandjiou", "latitude": 4.2941, "longitude": 9.6733, "x": 49.3, "y": 31, "source": "geonames", "type": "PPL" }, { "name": "Bedou", "latitude": 4.3017, "longitude": 9.6782, "x": 50.5, "y": 28.8, "source": "geonames", "type": "PPL" }, { "name": "Bekoko", "latitude": 4.1137, "longitude": 9.5795, "x": 27, "y": 83.3, "source": "geonames", "type": "PPL" }, { "name": "Bekouma", "latitude": 4.2627, "longitude": 9.6042, "x": 32.8, "y": 40.1, "source": "geonames", "type": "PPL" }, { "name": "Bengse", "latitude": 4.3647, "longitude": 9.7427, "x": 65.9, "y": 10.6, "source": "geonames", "type": "PPL" }, { "name": "Besoung Kang", "latitude": 4.36667, "longitude": 9.75, "x": 67.6, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Beyang Mbondjo", "latitude": 4.2388, "longitude": 9.5415, "x": 17.9, "y": 47.1, "source": "geonames", "type": "PPL" }, { "name": "Biendende", "latitude": 4.162, "longitude": 9.6442, "x": 42.4, "y": 69.3, "source": "geonames", "type": "PPL" }, { "name": "Bomono Ba Djerou", "latitude": 4.1558, "longitude": 9.6153, "x": 35.5, "y": 71.1, "source": "geonames", "type": "PPL" }, { "name": "Bomono Ba Mbengue", "latitude": 4.1406, "longitude": 9.5913, "x": 29.8, "y": 75.5, "source": "geonames", "type": "PPL" }, { "name": "Bomono Gare", "latitude": 4.1664, "longitude": 9.6068, "x": 33.5, "y": 68.1, "source": "geonames", "type": "PPL" }, { "name": "Bona Sedi", "latitude": 4.18333, "longitude": 9.68333, "x": 51.7, "y": 63.1, "source": "geonames", "type": "PPL" }, { "name": "Bona-Daja", "latitude": 4.21667, "longitude": 9.65, "x": 43.8, "y": 53.5, "source": "geonames", "type": "PPL" }, { "name": "Bonabweng", "latitude": 4.201, "longitude": 9.6789, "x": 50.7, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Bonakou", "latitude": 4.2693, "longitude": 9.6711, "x": 48.8, "y": 38.2, "source": "geonames", "type": "PPL" }, { "name": "Bonakwassi", "latitude": 4.3114, "longitude": 9.7312, "x": 63.1, "y": 26, "source": "geonames", "type": "PPL" }, { "name": "Bonalea Mandouka", "latitude": 4.342, "longitude": 9.7366, "x": 64.4, "y": 17.2, "source": "geonames", "type": "PPL" }, { "name": "Bonamboule", "latitude": 4.3169, "longitude": 9.7291, "x": 62.6, "y": 24.4, "source": "geonames", "type": "PPL" }, { "name": "Bonambwasse", "latitude": 4.1526, "longitude": 9.6745, "x": 49.6, "y": 72.1, "source": "geonames", "type": "PPL" }, { "name": "Bonamombe", "latitude": 4.2449, "longitude": 9.7817, "x": 75.2, "y": 45.3, "source": "geonames", "type": "PPL" }, { "name": "Bonamone", "latitude": 4.1047, "longitude": 9.5461, "x": 19, "y": 85.9, "source": "geonames", "type": "PPL" }, { "name": "Bonandika", "latitude": 4.3502, "longitude": 9.7427, "x": 65.9, "y": 14.8, "source": "geonames", "type": "PPL" }, { "name": "Bonangando", "latitude": 4.1472, "longitude": 9.669, "x": 48.3, "y": 73.6, "source": "geonames", "type": "PPL" }, { "name": "Bonanka", "latitude": 4.2496, "longitude": 9.6364, "x": 40.5, "y": 43.9, "source": "geonames", "type": "PPL" }, { "name": "Bonasson", "latitude": 4.1013, "longitude": 9.5411, "x": 17.8, "y": 86.9, "source": "geonames", "type": "PPL" }, { "name": "Bondjo", "latitude": 4.2681, "longitude": 9.7973, "x": 78.9, "y": 38.6, "source": "geonames", "type": "PPL" }, { "name": "Bonebanda", "latitude": 4.1602, "longitude": 9.5344, "x": 16.2, "y": 69.9, "source": "geonames", "type": "PPL" }, { "name": "Bonendale", "latitude": 4.1175, "longitude": 9.6542, "x": 44.8, "y": 82.2, "source": "geonames", "type": "PPL" }, { "name": "Bonepea", "latitude": 4.232, "longitude": 9.7779, "x": 74.3, "y": 49, "source": "geonames", "type": "PPL" }, { "name": "Boneyan", "latitude": 4.3168, "longitude": 9.7375, "x": 64.6, "y": 24.5, "source": "geonames", "type": "PPL" }, { "name": "Bongo", "latitude": 4.2233, "longitude": 9.7273, "x": 62.2, "y": 51.6, "source": "geonames", "type": "PPL" }, { "name": "Bonindi", "latitude": 4.2815, "longitude": 9.8257, "x": 85.7, "y": 34.7, "source": "geonames", "type": "PPL" }, { "name": "Bossamba", "latitude": 4.2221, "longitude": 9.754, "x": 68.6, "y": 51.9, "source": "geonames", "type": "PPL" }, { "name": "Bwadibo", "latitude": 4.0907, "longitude": 9.5853, "x": 28.3, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Bwelelo", "latitude": 4.16667, "longitude": 9.66667, "x": 47.7, "y": 68, "source": "geonames", "type": "PPL" }, { "name": "Bwene", "latitude": 4.2927, "longitude": 9.831, "x": 86.9, "y": 31.4, "source": "geonames", "type": "PPL" }, { "name": "Djouki", "latitude": 4.1445, "longitude": 9.6226, "x": 37.2, "y": 74.4, "source": "geonames", "type": "PPL" }, { "name": "Fiko", "latitude": 4.2989, "longitude": 9.7176, "x": 59.9, "y": 29.6, "source": "geonames", "type": "PPL" }, { "name": "Grand Makemba", "latitude": 4.2516, "longitude": 9.6671, "x": 47.8, "y": 43.4, "source": "geonames", "type": "PPL" }, { "name": "Grand Souza", "latitude": 4.2348, "longitude": 9.6408, "x": 41.6, "y": 48.2, "source": "geonames", "type": "PPL" }, { "name": "Kake", "latitude": 4.2867, "longitude": 9.6096, "x": 34.1, "y": 33.2, "source": "geonames", "type": "PPL" }, { "name": "Koki", "latitude": 4.2713, "longitude": 9.72, "x": 60.5, "y": 37.6, "source": "geonames", "type": "PPL" }, { "name": "Kombiang", "latitude": 4.2244, "longitude": 9.6339, "x": 39.9, "y": 51.2, "source": "geonames", "type": "PPL" }, { "name": "Kongwe", "latitude": 4.1185, "longitude": 9.5258, "x": 14.1, "y": 81.9, "source": "geonames", "type": "PPL" }, { "name": "Koto", "latitude": 4.18333, "longitude": 9.63333, "x": 39.8, "y": 63.1, "source": "geonames", "type": "PPL" }, { "name": "Kounang", "latitude": 4.3366, "longitude": 9.788, "x": 76.7, "y": 18.7, "source": "geonames", "type": "PPL" }, { "name": "Kwoussi", "latitude": 4.35, "longitude": 9.76667, "x": 71.6, "y": 14.8, "source": "geonames", "type": "PPL" }, { "name": "Londo", "latitude": 4.26667, "longitude": 9.7, "x": 55.7, "y": 39, "source": "geonames", "type": "PPL" }, { "name": "Mabanga", "latitude": 4.2089, "longitude": 9.6624, "x": 46.7, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Maka", "latitude": 4.13333, "longitude": 9.61667, "x": 35.8, "y": 77.6, "source": "geonames", "type": "PPL" }, { "name": "Maleke", "latitude": 4.3112, "longitude": 9.5978, "x": 31.3, "y": 26.1, "source": "geonames", "type": "PPL" }, { "name": "Mangamba", "latitude": 4.33333, "longitude": 9.75, "x": 67.6, "y": 19.7, "source": "geonames", "type": "PPL" }, { "name": "Mayen", "latitude": 4.2844, "longitude": 9.6423, "x": 41.9, "y": 33.8, "source": "geonames", "type": "PPL" }, { "name": "Mbangue I", "latitude": 4.1646, "longitude": 9.7034, "x": 56.5, "y": 68.6, "source": "geonames", "type": "PPL" }, { "name": "Mbangue II", "latitude": 4.1837, "longitude": 9.7065, "x": 57.2, "y": 63, "source": "geonames", "type": "PPL" }, { "name": "Mbomboo", "latitude": 4.3594, "longitude": 9.7397, "x": 65.2, "y": 12.1, "source": "geonames", "type": "PPL" }, { "name": "Mbondjo I", "latitude": 4.2279, "longitude": 9.5481, "x": 19.5, "y": 50.2, "source": "geonames", "type": "PPL" }, { "name": "Mbondjo II", "latitude": 4.2169, "longitude": 9.5263, "x": 14.3, "y": 53.4, "source": "geonames", "type": "PPL" }, { "name": "Mbouma", "latitude": 4.2424, "longitude": 9.8258, "x": 85.7, "y": 46, "source": "geonames", "type": "PPL" }, { "name": "Miang", "latitude": 4.2952, "longitude": 9.7027, "x": 56.3, "y": 30.7, "source": "geonames", "type": "PPL" }, { "name": "Minkwele", "latitude": 4.1006, "longitude": 9.6086, "x": 33.9, "y": 87.1, "source": "geonames", "type": "PPL" }, { "name": "Missaka", "latitude": 4.1874, "longitude": 9.5168, "x": 12, "y": 62, "source": "geonames", "type": "PPL" }, { "name": "Moulanga", "latitude": 4.15, "longitude": 9.55, "x": 19.9, "y": 72.8, "source": "geonames", "type": "PPL" }, { "name": "Moundja Moussadi", "latitude": 4.2469, "longitude": 9.8038, "x": 80.4, "y": 44.7, "source": "geonames", "type": "PPL" }, { "name": "Moussoko", "latitude": 4.2557, "longitude": 9.722, "x": 60.9, "y": 42.2, "source": "geonames", "type": "PPL" }, { "name": "Moutimbelembe", "latitude": 4.2578, "longitude": 9.8191, "x": 84.1, "y": 41.6, "source": "geonames", "type": "PPL" }, { "name": "Mungo", "latitude": 4.1063, "longitude": 9.5429, "x": 18.2, "y": 85.5, "source": "geonames", "type": "PPL" }, { "name": "Ndobo", "latitude": 4.1013, "longitude": 9.6315, "x": 39.4, "y": 86.9, "source": "geonames", "type": "PPL" }, { "name": "Ndokama", "latitude": 4.33333, "longitude": 9.7, "x": 55.7, "y": 19.7, "source": "geonames", "type": "PPL" }, { "name": "Ndongo", "latitude": 4.2627, "longitude": 9.6867, "x": 52.5, "y": 40.1, "source": "geonames", "type": "PPL" }, { "name": "Ndoulou", "latitude": 4.2966, "longitude": 9.6691, "x": 48.3, "y": 30.3, "source": "geonames", "type": "PPL" }, { "name": "New Bonako", "latitude": 4.1464, "longitude": 9.5084, "x": 10, "y": 73.9, "source": "geonames", "type": "PPL" }, { "name": "Ngombe", "latitude": 4.2187, "longitude": 9.7755, "x": 73.7, "y": 52.9, "source": "geonames", "type": "PPL" }, { "name": "Nkapa", "latitude": 4.2114, "longitude": 9.6133, "x": 35, "y": 55, "source": "geonames", "type": "PPL" }, { "name": "Nkende", "latitude": 4.2013, "longitude": 9.6092, "x": 34, "y": 57.9, "source": "geonames", "type": "PPL" }, { "name": "Nkongbong", "latitude": 4.1677, "longitude": 9.6828, "x": 51.6, "y": 67.7, "source": "geonames", "type": "PPL" }, { "name": "Nono", "latitude": 4.2915, "longitude": 9.8267, "x": 85.9, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Pembe", "latitude": 4.2778, "longitude": 9.6165, "x": 35.8, "y": 35.8, "source": "geonames", "type": "PPL" }, { "name": "Sodiko", "latitude": 4.1176, "longitude": 9.6658, "x": 47.5, "y": 82.2, "source": "geonames", "type": "PPL" }, { "name": "Souza Gare", "latitude": 4.2344, "longitude": 9.6164, "x": 35.8, "y": 48.3, "source": "geonames", "type": "PPL" }, { "name": "Tonde", "latitude": 4.2224, "longitude": 9.8439, "x": 90, "y": 51.8, "source": "geonames", "type": "PPL" }, { "name": "Yabea", "latitude": 4.2051, "longitude": 9.721, "x": 60.7, "y": 56.8, "source": "geonames", "type": "PPL" }, { "name": "Yamikoki", "latitude": 4.1799, "longitude": 9.6851, "x": 52.1, "y": 64.1, "source": "geonames", "type": "PPL" }, { "name": "Yandom", "latitude": 4.1781, "longitude": 9.6742, "x": 49.5, "y": 64.7, "source": "geonames", "type": "PPL" }, { "name": "Yandoungou", "latitude": 4.1737, "longitude": 9.6955, "x": 54.6, "y": 65.9, "source": "geonames", "type": "PPL" }, { "name": "Yassem", "latitude": 4.1879, "longitude": 9.7513, "x": 67.9, "y": 61.8, "source": "geonames", "type": "PPL" }, { "name": "Yassouka", "latitude": 4.1919, "longitude": 9.7272, "x": 62.2, "y": 60.7, "source": "geonames", "type": "PPL" }, { "name": "Yato", "latitude": 4.1532, "longitude": 9.5587, "x": 22, "y": 71.9, "source": "geonames", "type": "PPL" }, { "name": "Yundi", "latitude": 4.1481, "longitude": 9.5347, "x": 16.3, "y": 73.4, "source": "geonames", "type": "PPL" } ], "Dimako": [ { "name": "Baktala", "latitude": 4.43333, "longitude": 13.61667, "x": 65.4, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Dongoro", "latitude": 4.2, "longitude": 13.68333, "x": 90, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Kagama", "latitude": 4.46667, "longitude": 13.61667, "x": 65.4, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Kouen", "latitude": 4.31667, "longitude": 13.5, "x": 22.3, "y": 64.5, "source": "geonames", "type": "PPL" }, { "name": "Longtimbi", "latitude": 4.45, "longitude": 13.61667, "x": 65.4, "y": 35.5, "source": "geonames", "type": "PPL" }, { "name": "Mbet II", "latitude": 4.56667, "longitude": 13.46667, "x": 10, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Ngolambele", "latitude": 4.43333, "longitude": 13.6, "x": 59.2, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Toungrelo", "latitude": 4.36667, "longitude": 13.53333, "x": 34.6, "y": 53.6, "source": "geonames", "type": "PPL" } ], "Dizangue": [ { "name": "Dibongo", "latitude": 3.7, "longitude": 10.03333, "x": 83.1, "y": 49.6, "source": "geonames", "type": "PPL" }, { "name": "Dikola", "latitude": 3.72528, "longitude": 9.91361, "x": 33.8, "y": 42.7, "source": "geonames", "type": "PPL" }, { "name": "Dipendi", "latitude": 3.615, "longitude": 9.94806, "x": 48, "y": 72.5, "source": "geonames", "type": "PPL" }, { "name": "Douala", "latitude": 3.75528, "longitude": 9.94472, "x": 46.6, "y": 34.6, "source": "geonames", "type": "PPL" }, { "name": "Ileka", "latitude": 3.67306, "longitude": 9.99194, "x": 66.1, "y": 56.8, "source": "geonames", "type": "PPL" }, { "name": "Kotto", "latitude": 3.63972, "longitude": 9.9575, "x": 51.9, "y": 65.8, "source": "geonames", "type": "PPL" }, { "name": "Loghoui", "latitude": 3.82944, "longitude": 9.88722, "x": 22.9, "y": 14.6, "source": "geonames", "type": "PPL" }, { "name": "Lombe", "latitude": 3.63389, "longitude": 9.98611, "x": 63.7, "y": 67.4, "source": "geonames", "type": "PPL" }, { "name": "Madouma", "latitude": 3.74028, "longitude": 9.86889, "x": 15.4, "y": 38.7, "source": "geonames", "type": "PPL" }, { "name": "Malimba", "latitude": 3.71667, "longitude": 10.05, "x": 90, "y": 45.1, "source": "geonames", "type": "PPL" }, { "name": "Mbalmayo", "latitude": 3.72361, "longitude": 9.955, "x": 50.9, "y": 43.2, "source": "geonames", "type": "PPL" }, { "name": "Mbanda", "latitude": 3.7975, "longitude": 9.88222, "x": 20.9, "y": 23.3, "source": "geonames", "type": "PPL" }, { "name": "Mbenbou", "latitude": 3.77083, "longitude": 9.87528, "x": 18, "y": 30.5, "source": "geonames", "type": "PPL" }, { "name": "Mboumas", "latitude": 3.61667, "longitude": 10, "x": 69.4, "y": 72, "source": "geonames", "type": "PPL" }, { "name": "Mokolo", "latitude": 3.74028, "longitude": 9.95139, "x": 49.4, "y": 38.7, "source": "geonames", "type": "PPL" }, { "name": "Mpolbe", "latitude": 3.58861, "longitude": 9.99306, "x": 66.5, "y": 79.6, "source": "geonames", "type": "PPL" }, { "name": "Ndog Bam", "latitude": 3.78111, "longitude": 9.85583, "x": 10, "y": 27.7, "source": "geonames", "type": "PPL" }, { "name": "Ndog Odjong", "latitude": 3.78333, "longitude": 9.86667, "x": 14.5, "y": 27.1, "source": "geonames", "type": "PPL" }, { "name": "Ndonga", "latitude": 3.84667, "longitude": 9.865, "x": 13.8, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Ngombi", "latitude": 3.61694, "longitude": 9.99, "x": 65.3, "y": 71.9, "source": "geonames", "type": "PPL" }, { "name": "Nkoungoue", "latitude": 3.79833, "longitude": 9.90306, "x": 29.5, "y": 23, "source": "geonames", "type": "PPL" }, { "name": "Nsepe", "latitude": 3.66667, "longitude": 9.96667, "x": 55.7, "y": 58.5, "source": "geonames", "type": "PPL" }, { "name": "Nyoungou", "latitude": 3.64194, "longitude": 9.94833, "x": 48.1, "y": 65.2, "source": "geonames", "type": "PPL" }, { "name": "Okola", "latitude": 3.63139, "longitude": 9.92833, "x": 39.9, "y": 68.1, "source": "geonames", "type": "PPL" }, { "name": "Pongo Piti", "latitude": 3.66667, "longitude": 10.05, "x": 90, "y": 58.5, "source": "geonames", "type": "PPL" }, { "name": "Sakimbanda", "latitude": 3.83694, "longitude": 9.88861, "x": 23.5, "y": 12.6, "source": "geonames", "type": "PPL" }, { "name": "Tinaso", "latitude": 3.61861, "longitude": 9.95722, "x": 51.8, "y": 71.5, "source": "geonames", "type": "PPL" }, { "name": "Yambong", "latitude": 3.55, "longitude": 10.01667, "x": 76.3, "y": 90, "source": "geonames", "type": "PPL" } ], "Djoum": [ { "name": "Aboelon", "latitude": 2.7, "longitude": 12.63333, "x": 35.5, "y": 26.8, "source": "geonames", "type": "PPL" }, { "name": "Akom", "latitude": 2.66667, "longitude": 12.71667, "x": 53.6, "y": 35.3, "source": "geonames", "type": "PPL" }, { "name": "Akombinien", "latitude": 2.68333, "longitude": 12.65, "x": 39.1, "y": 31.1, "source": "geonames", "type": "PPL" }, { "name": "Akonetye", "latitude": 2.66667, "longitude": 12.86667, "x": 86.4, "y": 35.3, "source": "geonames", "type": "PPL" }, { "name": "Akonotangan", "latitude": 2.61667, "longitude": 12.66667, "x": 42.7, "y": 47.9, "source": "geonames", "type": "PPL" }, { "name": "Anmvam", "latitude": 2.66667, "longitude": 12.81667, "x": 75.5, "y": 35.3, "source": "geonames", "type": "PPL" }, { "name": "Atak", "latitude": 2.66667, "longitude": 12.73333, "x": 57.3, "y": 35.3, "source": "geonames", "type": "PPL" }, { "name": "Avobengono", "latitude": 2.66667, "longitude": 12.88333, "x": 90, "y": 35.3, "source": "geonames", "type": "PPL" }, { "name": "Ayene", "latitude": 2.55, "longitude": 12.66667, "x": 42.7, "y": 64.7, "source": "geonames", "type": "PPL" }, { "name": "Bindoumba", "latitude": 2.48333, "longitude": 12.68333, "x": 46.4, "y": 81.6, "source": "geonames", "type": "PPL" }, { "name": "Djop", "latitude": 2.6, "longitude": 12.66667, "x": 42.7, "y": 52.1, "source": "geonames", "type": "PPL" }, { "name": "Djouze", "latitude": 2.73333, "longitude": 12.63333, "x": 35.5, "y": 18.4, "source": "geonames", "type": "PPL" }, { "name": "Doum I", "latitude": 2.58333, "longitude": 12.66667, "x": 42.7, "y": 56.3, "source": "geonames", "type": "PPL" }, { "name": "Doum II", "latitude": 2.56667, "longitude": 12.66667, "x": 42.7, "y": 60.5, "source": "geonames", "type": "PPL" }, { "name": "Efoulan", "latitude": 2.65, "longitude": 12.76667, "x": 64.5, "y": 39.5, "source": "geonames", "type": "PPL" }, { "name": "Eleng", "latitude": 2.65, "longitude": 12.73333, "x": 57.3, "y": 39.5, "source": "geonames", "type": "PPL" }, { "name": "Endenge", "latitude": 2.68333, "longitude": 12.65, "x": 39.1, "y": 31.1, "source": "geonames", "type": "PPL" }, { "name": "Esong", "latitude": 2.51667, "longitude": 12.66667, "x": 42.7, "y": 73.2, "source": "geonames", "type": "PPL" }, { "name": "Melen", "latitude": 2.76667, "longitude": 12.53333, "x": 13.6, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Meuban", "latitude": 2.45, "longitude": 12.68333, "x": 46.4, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Meyos Obam", "latitude": 2.65, "longitude": 12.75, "x": 60.9, "y": 39.5, "source": "geonames", "type": "PPL" }, { "name": "Mfem", "latitude": 2.53333, "longitude": 12.66667, "x": 42.7, "y": 68.9, "source": "geonames", "type": "PPL" }, { "name": "Miatta", "latitude": 2.73333, "longitude": 12.63333, "x": 35.5, "y": 18.4, "source": "geonames", "type": "PPL" }, { "name": "Mindoung", "latitude": 2.65, "longitude": 12.76667, "x": 64.5, "y": 39.5, "source": "geonames", "type": "PPL" }, { "name": "Minkoo", "latitude": 2.63333, "longitude": 12.66667, "x": 42.7, "y": 43.7, "source": "geonames", "type": "PPL" }, { "name": "Mveng", "latitude": 2.75, "longitude": 12.61667, "x": 31.8, "y": 14.2, "source": "geonames", "type": "PPL" }, { "name": "Nkan", "latitude": 2.66667, "longitude": 12.66667, "x": 42.7, "y": 35.3, "source": "geonames", "type": "PPL" }, { "name": "Nko", "latitude": 2.75, "longitude": 12.58333, "x": 24.5, "y": 14.2, "source": "geonames", "type": "PPL" }, { "name": "Nkolafendek", "latitude": 2.76667, "longitude": 12.51667, "x": 10, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Nkong", "latitude": 2.75, "longitude": 12.6, "x": 28.2, "y": 14.2, "source": "geonames", "type": "PPL" }, { "name": "Nyabibete", "latitude": 2.76667, "longitude": 12.56667, "x": 20.9, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Otong Mbong", "latitude": 2.66667, "longitude": 12.85, "x": 82.7, "y": 35.3, "source": "geonames", "type": "PPL" } ], "Djohong": [ { "name": "Amadou Nabata", "latitude": 6.81667, "longitude": 14.65, "x": 50, "y": 51.7, "source": "geonames", "type": "PPL" }, { "name": "Ardo Amadou", "latitude": 6.76667, "longitude": 14.53333, "x": 18.9, "y": 62.2, "source": "geonames", "type": "PPL" }, { "name": "Bafouk", "latitude": 6.63333, "longitude": 14.61667, "x": 41.1, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Bakari Bata", "latitude": 6.93333, "longitude": 14.6, "x": 36.7, "y": 27.4, "source": "geonames", "type": "PPL" }, { "name": "Batouri", "latitude": 6.9, "longitude": 14.8, "x": 90, "y": 34.3, "source": "geonames", "type": "PPL" }, { "name": "Borgou", "latitude": 6.91667, "longitude": 14.8, "x": 90, "y": 30.9, "source": "geonames", "type": "PPL" }, { "name": "Congo", "latitude": 6.75, "longitude": 14.56667, "x": 27.8, "y": 65.7, "source": "geonames", "type": "PPL" }, { "name": "Doh", "latitude": 7.01667, "longitude": 14.75, "x": 76.7, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Doua", "latitude": 6.75, "longitude": 14.53333, "x": 18.9, "y": 65.7, "source": "geonames", "type": "PPL" }, { "name": "Goubla", "latitude": 6.66667, "longitude": 14.63333, "x": 45.6, "y": 83, "source": "geonames", "type": "PPL" }, { "name": "Gouhigon", "latitude": 6.73333, "longitude": 14.51667, "x": 14.4, "y": 69.1, "source": "geonames", "type": "PPL" }, { "name": "Laende Mami", "latitude": 6.75, "longitude": 14.55, "x": 23.3, "y": 65.7, "source": "geonames", "type": "PPL" }, { "name": "Madibo Marou", "latitude": 6.75, "longitude": 14.53333, "x": 18.9, "y": 65.7, "source": "geonames", "type": "PPL" }, { "name": "Mandib I", "latitude": 6.8, "longitude": 14.63333, "x": 45.6, "y": 55.2, "source": "geonames", "type": "PPL" }, { "name": "Mayo Lebou", "latitude": 6.73333, "longitude": 14.5, "x": 10, "y": 69.1, "source": "geonames", "type": "PPL" }, { "name": "Mbondo", "latitude": 6.83333, "longitude": 14.66667, "x": 54.4, "y": 48.3, "source": "geonames", "type": "PPL" }, { "name": "Nabemo", "latitude": 6.88333, "longitude": 14.76667, "x": 81.1, "y": 37.8, "source": "geonames", "type": "PPL" }, { "name": "Nana", "latitude": 6.86667, "longitude": 14.76667, "x": 81.1, "y": 41.3, "source": "geonames", "type": "PPL" }, { "name": "Ngam", "latitude": 6.73333, "longitude": 14.56667, "x": 27.8, "y": 69.1, "source": "geonames", "type": "PPL" }, { "name": "Niase Soukol", "latitude": 6.88333, "longitude": 14.53333, "x": 18.9, "y": 37.8, "source": "geonames", "type": "PPL" }, { "name": "Yafounou", "latitude": 6.75, "longitude": 14.63333, "x": 45.6, "y": 65.7, "source": "geonames", "type": "PPL" } ], "Douala": [ { "name": "Akwa", "latitude": 4.052544, "longitude": 9.6963304, "x": 41.5, "y": 50.8, "source": "osm", "type": "suburb" }, { "name": "Ancien Aeroport", "latitude": 4.0147686, "longitude": 9.70769, "x": 46.1, "y": 75, "source": "osm", "type": "suburb" }, { "name": "Ancien Dalip", "latitude": 4.0460192, "longitude": 9.6986773, "x": 42.5, "y": 55, "source": "osm", "type": "locality" }, { "name": "Ange Raphael", "latitude": 4.0225624, "longitude": 9.7017512, "x": 43.7, "y": 70, "source": "osm", "type": "neighbourhood" }, { "name": "Babylone", "latitude": 4.0261148, "longitude": 9.7085218, "x": 46.5, "y": 67.8, "source": "osm", "type": "suburb" }, { "name": "Bali", "latitude": 4.0399946, "longitude": 9.6930482, "x": 40.2, "y": 58.9, "source": "osm", "type": "suburb" }, { "name": "Bangue", "latitude": 4.1056394, "longitude": 9.7528416, "x": 64.6, "y": 16.9, "source": "osm", "type": "suburb" }, { "name": "Bassa", "latitude": 4.0458695, "longitude": 9.7289916, "x": 54.8, "y": 55.1, "source": "osm", "type": "suburb" }, { "name": "Bayis", "latitude": 4.1116212, "longitude": 9.8152313, "x": 90, "y": 13.1, "source": "osm", "type": "suburb" }, { "name": "Beedi", "latitude": 4.0611714, "longitude": 9.7662924, "x": 70, "y": 45.3, "source": "osm", "type": "suburb" }, { "name": "Bell", "latitude": 4.05, "longitude": 9.71667, "x": 49.8, "y": 52.5, "source": "geonames", "type": "PPLX" }, { "name": "Bepanda", "latitude": 4.0565417, "longitude": 9.7227206, "x": 52.3, "y": 48.3, "source": "osm", "type": "suburb" }, { "name": "Bepele", "latitude": 4.1058985, "longitude": 9.6223042, "x": 11.3, "y": 16.7, "source": "osm", "type": "suburb" }, { "name": "Besseke", "latitude": 4.0726387, "longitude": 9.6804632, "x": 35, "y": 38, "source": "osm", "type": "suburb" }, { "name": "Bessengue", "latitude": 4.0566106, "longitude": 9.7111669, "x": 47.6, "y": 48.2, "source": "osm", "type": "suburb" }, { "name": "Bois des Singes", "latitude": 4.0080364, "longitude": 9.7079395, "x": 46.3, "y": 79.3, "source": "osm", "type": "suburb" }, { "name": "Bojongo", "latitude": 4.0866081, "longitude": 9.6190381, "x": 10, "y": 29.1, "source": "osm", "type": "suburb" }, { "name": "Bomkoul", "latitude": 4.0949123, "longitude": 9.8026592, "x": 84.9, "y": 23.8, "source": "osm", "type": "suburb" }, { "name": "Bonaberi", "latitude": 4.0822098, "longitude": 9.6649718, "x": 28.7, "y": 31.9, "source": "osm", "type": "suburb" }, { "name": "Bonadiwoto", "latitude": 4.0161627, "longitude": 9.7217454, "x": 51.9, "y": 74.1, "source": "osm", "type": "suburb" }, { "name": "Bonadouma", "latitude": 4.028734, "longitude": 9.6998938, "x": 43, "y": 66.1, "source": "osm", "type": "suburb" }, { "name": "Bonadouma II", "latitude": 4.0190947, "longitude": 9.7011987, "x": 43.5, "y": 72.2, "source": "osm", "type": "suburb" }, { "name": "Bonadoumbe", "latitude": 4.0230872, "longitude": 9.6988735, "x": 42.6, "y": 69.7, "source": "osm", "type": "suburb" }, { "name": "Bonajinje", "latitude": 4.0654728, "longitude": 9.7130541, "x": 48.3, "y": 42.6, "source": "osm", "type": "suburb" }, { "name": "Bonamatoumbe", "latitude": 4.0952147, "longitude": 9.6711081, "x": 31.2, "y": 23.6, "source": "osm", "type": "suburb" }, { "name": "Bonambape", "latitude": 4.0762703, "longitude": 9.6755731, "x": 33.1, "y": 35.7, "source": "osm", "type": "suburb" }, { "name": "Bonaminkano", "latitude": 4.0820341, "longitude": 9.675549, "x": 33, "y": 32, "source": "osm", "type": "suburb" }, { "name": "Bonamouang", "latitude": 4.0829325, "longitude": 9.721624, "x": 51.8, "y": 31.4, "source": "osm", "type": "suburb" }, { "name": "Bonamoudourou", "latitude": 4.06185, "longitude": 9.71758, "x": 50.2, "y": 44.9, "source": "osm", "type": "suburb" }, { "name": "Bonamoussadi", "latitude": 4.094354, "longitude": 9.7393663, "x": 59.1, "y": 24.1, "source": "osm", "type": "suburb" }, { "name": "Bonamoussongo", "latitude": 4.0717211, "longitude": 9.7254947, "x": 53.4, "y": 38.6, "source": "osm", "type": "suburb" }, { "name": "Bonamouti", "latitude": 4.0717537, "longitude": 9.7144253, "x": 48.9, "y": 38.6, "source": "osm", "type": "suburb" }, { "name": "Bonangang", "latitude": 4.1056467, "longitude": 9.744505, "x": 61.2, "y": 16.9, "source": "osm", "type": "suburb" }, { "name": "Bonanjo", "latitude": 4.043024, "longitude": 9.6864962, "x": 37.5, "y": 56.9, "source": "osm", "type": "suburb" }, { "name": "Bonanloka", "latitude": 4.0139573, "longitude": 9.7327861, "x": 56.4, "y": 75.5, "source": "osm", "type": "suburb" }, { "name": "Bonantone", "latitude": 4.0618085, "longitude": 9.7075205, "x": 46.1, "y": 44.9, "source": "osm", "type": "suburb" }, { "name": "Bonapriso", "latitude": 4.0256151, "longitude": 9.6930153, "x": 40.2, "y": 68.1, "source": "osm", "type": "suburb" }, { "name": "Bonateki", "latitude": 4.0703304, "longitude": 9.7175548, "x": 50.2, "y": 39.5, "source": "osm", "type": "suburb" }, { "name": "Bonatene", "latitude": 4.0657787, "longitude": 9.7076223, "x": 46.1, "y": 42.4, "source": "osm", "type": "suburb" }, { "name": "Bonendale 1", "latitude": 4.1122222, "longitude": 9.6375552, "x": 17.6, "y": 12.7, "source": "osm", "type": "suburb" }, { "name": "Bonendale 2", "latitude": 4.1164184, "longitude": 9.6544209, "x": 24.4, "y": 10, "source": "osm", "type": "suburb" }, { "name": "Bonewonda", "latitude": 4.0755225, "longitude": 9.7190991, "x": 50.8, "y": 36.2, "source": "osm", "type": "suburb" }, { "name": "Brazzaville", "latitude": 4.0233735, "longitude": 9.7292726, "x": 54.9, "y": 69.5, "source": "osm", "type": "suburb" }, { "name": "Camp Yabassi", "latitude": 4.0431861, "longitude": 9.7092426, "x": 46.8, "y": 56.8, "source": "osm", "type": "suburb" }, { "name": "Carrefour 3 Baham", "latitude": 4.0698676, "longitude": 9.7267781, "x": 53.9, "y": 39.8, "source": "osm", "type": "locality" }, { "name": "Carrefour Alhadji", "latitude": 4.0687563, "longitude": 9.656389, "x": 25.2, "y": 40.5, "source": "osm", "type": "locality" }, { "name": "Carrefour Alphonse", "latitude": 4.0004064, "longitude": 9.7644208, "x": 69.3, "y": 84.2, "source": "osm", "type": "locality" }, { "name": "Carrefour Amacam", "latitude": 4.0525611, "longitude": 9.6953681, "x": 41.1, "y": 50.8, "source": "osm", "type": "locality" }, { "name": "Carrefour Andem", "latitude": 4.0869015, "longitude": 9.7654453, "x": 69.7, "y": 28.9, "source": "osm", "type": "locality" }, { "name": "Carrefour Basson", "latitude": 4.0807626, "longitude": 9.7716482, "x": 72.2, "y": 32.8, "source": "osm", "type": "locality" }, { "name": "Carrefour Block 7", "latitude": 4.0775048, "longitude": 9.6596847, "x": 26.6, "y": 34.9, "source": "osm", "type": "locality" }, { "name": "Carrefour Bonabassem", "latitude": 4.0710983, "longitude": 9.7187862, "x": 50.7, "y": 39, "source": "osm", "type": "locality" }, { "name": "Carrefour Casino", "latitude": 4.0335372, "longitude": 9.7190464, "x": 50.8, "y": 63, "source": "osm", "type": "locality" }, { "name": "Carrefour Casmando", "latitude": 4.0698297, "longitude": 9.7293801, "x": 55, "y": 39.8, "source": "osm", "type": "locality" }, { "name": "Carrefour CCC", "latitude": 4.0335557, "longitude": 9.7402439, "x": 59.4, "y": 63, "source": "osm", "type": "locality" }, { "name": "Carrefour Chico", "latitude": 4.0704557, "longitude": 9.6532568, "x": 24, "y": 39.4, "source": "osm", "type": "locality" }, { "name": "Carrefour de Garage", "latitude": 4.0718958, "longitude": 9.6747529, "x": 32.7, "y": 38.5, "source": "osm", "type": "locality" }, { "name": "Carrefour de l'Air", "latitude": 4.0198627, "longitude": 9.7011758, "x": 43.5, "y": 71.7, "source": "osm", "type": "locality" }, { "name": "Carrefour Dernier Poteau", "latitude": 4.0379173, "longitude": 9.7204324, "x": 51.3, "y": 60.2, "source": "osm", "type": "locality" }, { "name": "Carrefour des ruelles", "latitude": 4.0652754, "longitude": 9.7209836, "x": 51.6, "y": 42.7, "source": "osm", "type": "locality" }, { "name": "Carrefour Deux Eglises", "latitude": 4.042904, "longitude": 9.7062184, "x": 45.5, "y": 57, "source": "osm", "type": "locality" }, { "name": "Carrefour Eto'o", "latitude": 4.087056, "longitude": 9.7438087, "x": 60.9, "y": 28.8, "source": "osm", "type": "locality" }, { "name": "Carrefour Etoo", "latitude": 4.0870735, "longitude": 9.7440198, "x": 61, "y": 28.8, "source": "osm", "type": "locality" }, { "name": "Carrefour Express", "latitude": 4.0827223, "longitude": 9.7655549, "x": 69.7, "y": 31.5, "source": "osm", "type": "locality" }, { "name": "Carrefour Fraternite", "latitude": 4.0647772, "longitude": 9.7293484, "x": 55, "y": 43, "source": "osm", "type": "locality" }, { "name": "Carrefour Garden logbaba", "latitude": 4.0341274, "longitude": 9.764468, "x": 69.3, "y": 62.6, "source": "osm", "type": "locality" }, { "name": "Carrefour grand towoh", "latitude": 4.1139168, "longitude": 9.631248, "x": 15, "y": 11.6, "source": "osm", "type": "locality" }, { "name": "Carrefour KM", "latitude": 4.0995735, "longitude": 9.7410248, "x": 59.7, "y": 20.8, "source": "osm", "type": "locality" }, { "name": "Carrefour La Manne", "latitude": 4.0488517, "longitude": 9.6903877, "x": 39.1, "y": 53.2, "source": "osm", "type": "locality" }, { "name": "Cite Des Palmiers", "latitude": 4.0542657, "longitude": 9.7640556, "x": 69.1, "y": 49.7, "source": "osm", "type": "suburb" }, { "name": "Cite SIC", "latitude": 4.0539142, "longitude": 9.7307181, "x": 55.5, "y": 50, "source": "osm", "type": "suburb" }, { "name": "Congo", "latitude": 4.0373892, "longitude": 9.7053201, "x": 45.2, "y": 60.5, "source": "osm", "type": "suburb" }, { "name": "Denver", "latitude": 4.0915045, "longitude": 9.7323226, "x": 56.2, "y": 25.9, "source": "osm", "type": "suburb" }, { "name": "Didom II", "latitude": 4.0141502, "longitude": 9.7430749, "x": 60.6, "y": 75.4, "source": "osm", "type": "suburb" }, { "name": "Domaine Universitaire", "latitude": 4.0596227, "longitude": 9.7401261, "x": 59.4, "y": 46.3, "source": "osm", "type": "suburb" }, { "name": "Kambo", "latitude": 4.0236763, "longitude": 9.7941289, "x": 81.4, "y": 69.3, "source": "osm", "type": "suburb" }, { "name": "KM 6", "latitude": 4.0379991, "longitude": 9.7206786, "x": 51.4, "y": 60.2, "source": "osm", "type": "suburb" }, { "name": "Koto", "latitude": 4.1003794, "longitude": 9.7555747, "x": 65.7, "y": 20.3, "source": "osm", "type": "suburb" }, { "name": "Koumassi", "latitude": 4.0357924, "longitude": 9.6932786, "x": 40.3, "y": 61.6, "source": "osm", "type": "suburb" }, { "name": "Lobe", "latitude": 4.0990957, "longitude": 9.6554455, "x": 24.8, "y": 21.1, "source": "osm", "type": "suburb" }, { "name": "Logbaba", "latitude": 4.0328521, "longitude": 9.7603178, "x": 67.6, "y": 63.4, "source": "osm", "type": "suburb" }, { "name": "Logbessou I", "latitude": 4.0844731, "longitude": 9.7842289, "x": 77.4, "y": 30.4, "source": "osm", "type": "suburb" }, { "name": "Logbessou II", "latitude": 4.0810919, "longitude": 9.7970314, "x": 82.6, "y": 32.6, "source": "osm", "type": "suburb" }, { "name": "Logpom", "latitude": 4.0769067, "longitude": 9.7711838, "x": 72, "y": 35.3, "source": "osm", "type": "suburb" }, { "name": "Mabanda", "latitude": 4.0706087, "longitude": 9.6573311, "x": 25.6, "y": 39.3, "source": "osm", "type": "suburb" }, { "name": "Madagascar", "latitude": 4.033651, "longitude": 9.7360237, "x": 57.7, "y": 62.9, "source": "osm", "type": "suburb" }, { "name": "Makepe II Bonamoussadi", "latitude": 4.0826348, "longitude": 9.75451, "x": 65.2, "y": 31.6, "source": "osm", "type": "suburb" }, { "name": "Makepe II Yonyong", "latitude": 4.0671936, "longitude": 9.7308224, "x": 55.6, "y": 41.5, "source": "osm", "type": "suburb" }, { "name": "Malangue", "latitude": 4.0684699, "longitude": 9.7613769, "x": 68, "y": 40.7, "source": "osm", "type": "suburb" }, { "name": "Mbanga Bakoko", "latitude": 4.0070341, "longitude": 9.7992035, "x": 83.5, "y": 80, "source": "osm", "type": "suburb" }, { "name": "Ndobo", "latitude": 4.1018152, "longitude": 9.6360158, "x": 16.9, "y": 19.3, "source": "osm", "type": "suburb" }, { "name": "Ndogbati", "latitude": 4.0495046, "longitude": 9.7236832, "x": 52.7, "y": 52.8, "source": "osm", "type": "suburb" }, { "name": "Ndogbong", "latitude": 4.0644714, "longitude": 9.7521285, "x": 64.3, "y": 43.2, "source": "osm", "type": "suburb" }, { "name": "Ndoghem", "latitude": 4.0675197, "longitude": 9.7904885, "x": 79.9, "y": 41.3, "source": "osm", "type": "suburb" }, { "name": "Ndogmbe", "latitude": 4.0349769, "longitude": 9.7529055, "x": 64.6, "y": 62.1, "source": "osm", "type": "suburb" }, { "name": "Ndogpassi III", "latitude": 4.0100854, "longitude": 9.7566698, "x": 66.1, "y": 78, "source": "osm", "type": "suburb" }, { "name": "Ndogsimbi", "latitude": 4.0404139, "longitude": 9.7311528, "x": 55.7, "y": 58.6, "source": "osm", "type": "suburb" }, { "name": "Ndokoti", "latitude": 4.0434344, "longitude": 9.7436776, "x": 60.8, "y": 56.7, "source": "osm", "type": "suburb" }, { "name": "New Bell", "latitude": 4.0314849, "longitude": 9.7057582, "x": 45.4, "y": 64.3, "source": "osm", "type": "suburb" }, { "name": "New Deido", "latitude": 4.0594866, "longitude": 9.7146748, "x": 49, "y": 46.4, "source": "osm", "type": "suburb" }, { "name": "Ngangue", "latitude": 4.0207674, "longitude": 9.7068203, "x": 45.8, "y": 71.2, "source": "osm", "type": "suburb" }, { "name": "Ngwele", "latitude": 4.0911188, "longitude": 9.6505071, "x": 22.8, "y": 26.2, "source": "osm", "type": "suburb" }, { "name": "Nkolmbong", "latitude": 4.0182989, "longitude": 9.7955265, "x": 82, "y": 72.7, "source": "osm", "type": "suburb" }, { "name": "Nkolminta", "latitude": 4.0372098, "longitude": 9.7260913, "x": 53.7, "y": 60.7, "source": "osm", "type": "suburb" }, { "name": "Nkolmintag", "latitude": 4.03506, "longitude": 9.71518, "x": 49.2, "y": 62, "source": "geonames", "type": "PPLX" }, { "name": "Nkololoun", "latitude": 4.0334598, "longitude": 9.7195305, "x": 51, "y": 63.1, "source": "osm", "type": "suburb" }, { "name": "Nkomba", "latitude": 4.0715713, "longitude": 9.6733797, "x": 32.2, "y": 38.7, "source": "osm", "type": "suburb" }, { "name": "Nouvel Aeroport", "latitude": 4.0110463, "longitude": 9.7211998, "x": 51.7, "y": 77.4, "source": "osm", "type": "suburb" }, { "name": "Nyalla Bassa", "latitude": 4.0334103, "longitude": 9.7744243, "x": 73.4, "y": 63.1, "source": "osm", "type": "suburb" }, { "name": "Nylon", "latitude": 4.0282399, "longitude": 9.7307532, "x": 55.6, "y": 66.4, "source": "osm", "type": "suburb" }, { "name": "Oyak", "latitude": 4.0266304, "longitude": 9.7397861, "x": 59.2, "y": 67.4, "source": "osm", "type": "suburb" }, { "name": "Pindo", "latitude": 4.0596128, "longitude": 9.7839646, "x": 77.3, "y": 46.3, "source": "osm", "type": "suburb" }, { "name": "Sodiko", "latitude": 4.097744, "longitude": 9.6623609, "x": 27.7, "y": 21.9, "source": "osm", "type": "suburb" }, { "name": "Songbikako", "latitude": 4.0456961, "longitude": 9.7681459, "x": 70.8, "y": 55.2, "source": "osm", "type": "suburb" }, { "name": "Vallee Bessengue", "latitude": 4.0526728, "longitude": 9.7052693, "x": 45.2, "y": 50.8, "source": "osm", "type": "suburb" }, { "name": "Village", "latitude": 4.011774, "longitude": 9.7294207, "x": 55, "y": 76.9, "source": "osm", "type": "suburb" }, { "name": "Washington", "latitude": 4.0842012, "longitude": 9.6534023, "x": 24, "y": 30.6, "source": "osm", "type": "suburb" }, { "name": "Yassa", "latitude": 3.9913253, "longitude": 9.8103803, "x": 88, "y": 90, "source": "osm", "type": "suburb" }, { "name": "Youpwe", "latitude": 4.0105092, "longitude": 9.699614, "x": 42.9, "y": 77.7, "source": "osm", "type": "suburb" }, { "name": "Zusa Bassa", "latitude": 4.1050886, "longitude": 9.8091756, "x": 87.5, "y": 17.2, "source": "osm", "type": "suburb" } ], "Doume": [ { "name": "Balengue", "latitude": 4.33333, "longitude": 13.3, "x": 14.2, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Bayong", "latitude": 4.33333, "longitude": 13.35, "x": 26.8, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Bayong I", "latitude": 4.28333, "longitude": 13.38333, "x": 35.3, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Bent", "latitude": 4.18333, "longitude": 13.33333, "x": 22.6, "y": 75.5, "source": "geonames", "type": "PPL" }, { "name": "Boumpial", "latitude": 4.2, "longitude": 13.35, "x": 26.8, "y": 68.2, "source": "geonames", "type": "PPL" }, { "name": "Diaglassi", "latitude": 4.15, "longitude": 13.28333, "x": 10, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Goumbegerong", "latitude": 4.25, "longitude": 13.53333, "x": 73.2, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Grand Pol", "latitude": 4.2, "longitude": 13.6, "x": 90, "y": 68.2, "source": "geonames", "type": "PPL" }, { "name": "Kamalone", "latitude": 4.33333, "longitude": 13.35, "x": 26.8, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Kempong", "latitude": 4.26667, "longitude": 13.51667, "x": 68.9, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Kobila", "latitude": 4.25, "longitude": 13.46667, "x": 56.3, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Loumbou", "latitude": 4.26667, "longitude": 13.5, "x": 64.7, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Mbama", "latitude": 4.25, "longitude": 13.43333, "x": 47.9, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Mendim", "latitude": 4.2, "longitude": 13.38333, "x": 35.3, "y": 68.2, "source": "geonames", "type": "PPL" }, { "name": "Nkolmeyanga", "latitude": 4.21667, "longitude": 13.6, "x": 90, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Nkoum", "latitude": 4.16667, "longitude": 13.3, "x": 14.2, "y": 82.7, "source": "geonames", "type": "PPL" }, { "name": "Oulemendamba", "latitude": 4.26667, "longitude": 13.4, "x": 39.5, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Paki", "latitude": 4.21667, "longitude": 13.41667, "x": 43.7, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Petit Pol", "latitude": 4.23333, "longitude": 13.56667, "x": 81.6, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Sibita", "latitude": 4.23333, "longitude": 13.45, "x": 52.1, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Simeyong", "latitude": 4.21667, "longitude": 13.58333, "x": 85.8, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Tounkoumbe", "latitude": 4.21667, "longitude": 13.58333, "x": 85.8, "y": 60.9, "source": "geonames", "type": "PPL" } ], "Dschang": [ { "name": "Atoutchang", "latitude": 5.42368, "longitude": 10.07589, "x": 54.9, "y": 72.3, "source": "geonames", "type": "PPL" }, { "name": "Azizia", "latitude": 5.41425, "longitude": 10.12448, "x": 83.6, "y": 75, "source": "geonames", "type": "PPL" }, { "name": "Bafou", "latitude": 5.47026, "longitude": 10.1185, "x": 80.1, "y": 58.5, "source": "geonames", "type": "PPL" }, { "name": "Doping", "latitude": 5.43251, "longitude": 10.11196, "x": 76.2, "y": 69.6, "source": "geonames", "type": "PPL" }, { "name": "Doumbwo", "latitude": 5.39051, "longitude": 10.09709, "x": 67.4, "y": 82.1, "source": "geonames", "type": "PPL" }, { "name": "Febing", "latitude": 5.4719, "longitude": 10.00182, "x": 11.1, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Femletagli", "latitude": 5.40772, "longitude": 10.06603, "x": 49.1, "y": 77, "source": "geonames", "type": "PPL" }, { "name": "Fiala'", "latitude": 5.43422, "longitude": 10.04638, "x": 37.4, "y": 69.1, "source": "geonames", "type": "PPL" }, { "name": "Fonakeke", "latitude": 5.40924, "longitude": 10.07631, "x": 55.1, "y": 76.5, "source": "geonames", "type": "PPL" }, { "name": "Foreke Dschang", "latitude": 5.43571, "longitude": 10.05494, "x": 42.5, "y": 68.7, "source": "geonames", "type": "PPL" }, { "name": "Fossong-Tsintchuet", "latitude": 5.50971, "longitude": 10.00906, "x": 15.4, "y": 46.8, "source": "geonames", "type": "PPL" }, { "name": "Fotetsa", "latitude": 5.41667, "longitude": 10, "x": 10, "y": 74.3, "source": "geonames", "type": "PPL" }, { "name": "Foto", "latitude": 5.46459, "longitude": 10.07251, "x": 52.9, "y": 60.2, "source": "geonames", "type": "PPL" }, { "name": "Fotsouli", "latitude": 5.44699, "longitude": 10.00513, "x": 13, "y": 65.4, "source": "geonames", "type": "PPL" }, { "name": "Fou", "latitude": 5.4711, "longitude": 10.11324, "x": 77, "y": 58.2, "source": "geonames", "type": "PPL" }, { "name": "Honto", "latitude": 5.40271, "longitude": 10.10989, "x": 75, "y": 78.4, "source": "geonames", "type": "PPL" }, { "name": "Kekang", "latitude": 5.48094, "longitude": 10.09389, "x": 65.5, "y": 55.3, "source": "geonames", "type": "PPL" }, { "name": "Keleng", "latitude": 5.44534, "longitude": 10.07733, "x": 55.7, "y": 65.9, "source": "geonames", "type": "PPL" }, { "name": "Keleng III", "latitude": 5.45501, "longitude": 10.06863, "x": 50.6, "y": 63, "source": "geonames", "type": "PPL" }, { "name": "Kemtswop", "latitude": 5.44171, "longitude": 10.03591, "x": 31.2, "y": 66.9, "source": "geonames", "type": "PPL" }, { "name": "Kop", "latitude": 5.47005, "longitude": 10.03956, "x": 33.4, "y": 58.6, "source": "geonames", "type": "PPL" }, { "name": "Kuchie", "latitude": 5.38172, "longitude": 10.01668, "x": 19.9, "y": 84.6, "source": "geonames", "type": "PPL" }, { "name": "La'atchuet", "latitude": 5.5179, "longitude": 10.03561, "x": 31.1, "y": 44.4, "source": "geonames", "type": "PPL" }, { "name": "Lap", "latitude": 5.48046, "longitude": 10.07627, "x": 55.1, "y": 55.5, "source": "geonames", "type": "PPL" }, { "name": "Lap Zekeng", "latitude": 5.47369, "longitude": 10.01891, "x": 21.2, "y": 57.5, "source": "geonames", "type": "PPL" }, { "name": "Latioupou", "latitude": 5.42698, "longitude": 10.02699, "x": 26, "y": 71.3, "source": "geonames", "type": "PPL" }, { "name": "Lefang", "latitude": 5.469, "longitude": 10.0451, "x": 36.7, "y": 58.9, "source": "geonames", "type": "PPL" }, { "name": "Lefatsa'", "latitude": 5.46858, "longitude": 10.08074, "x": 57.8, "y": 59, "source": "geonames", "type": "PPL" }, { "name": "Lefe", "latitude": 5.48472, "longitude": 10.07658, "x": 55.3, "y": 54.2, "source": "geonames", "type": "PPL" }, { "name": "Lefe II", "latitude": 5.49618, "longitude": 10.09166, "x": 64.2, "y": 50.8, "source": "geonames", "type": "PPL" }, { "name": "Lekia", "latitude": 5.51083, "longitude": 10.10293, "x": 70.9, "y": 46.5, "source": "geonames", "type": "PPL" }, { "name": "Lekong", "latitude": 5.40037, "longitude": 10.05303, "x": 41.4, "y": 79.1, "source": "geonames", "type": "PPL" }, { "name": "Leng", "latitude": 5.47392, "longitude": 10.10429, "x": 71.7, "y": 57.4, "source": "geonames", "type": "PPL" }, { "name": "Lepe", "latitude": 5.47974, "longitude": 10.06622, "x": 49.2, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Lepia", "latitude": 5.42823, "longitude": 10.04385, "x": 35.9, "y": 70.9, "source": "geonames", "type": "PPL" }, { "name": "Lepo", "latitude": 5.41362, "longitude": 10.04028, "x": 33.8, "y": 75.2, "source": "geonames", "type": "PPL" }, { "name": "Lepouo", "latitude": 5.48877, "longitude": 10.08946, "x": 62.9, "y": 53, "source": "geonames", "type": "PPL" }, { "name": "Lesse", "latitude": 5.48343, "longitude": 10.01033, "x": 16.1, "y": 54.6, "source": "geonames", "type": "PPL" }, { "name": "Letagli", "latitude": 5.40982, "longitude": 10.08998, "x": 63.2, "y": 76.3, "source": "geonames", "type": "PPL" }, { "name": "Letet", "latitude": 5.41068, "longitude": 10.11456, "x": 77.8, "y": 76.1, "source": "geonames", "type": "PPL" }, { "name": "Letie", "latitude": 5.40987, "longitude": 10.01685, "x": 20, "y": 76.3, "source": "geonames", "type": "PPL" }, { "name": "Letsa", "latitude": 5.49224, "longitude": 10.05692, "x": 43.7, "y": 52, "source": "geonames", "type": "PPL" }, { "name": "Letset", "latitude": 5.4305, "longitude": 10.12459, "x": 83.7, "y": 70.2, "source": "geonames", "type": "PPL" }, { "name": "Levet", "latitude": 5.45254, "longitude": 10.13497, "x": 89.8, "y": 63.7, "source": "geonames", "type": "PPL" }, { "name": "Levongli", "latitude": 5.51586, "longitude": 10.05457, "x": 42.3, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Lingan", "latitude": 5.48135, "longitude": 10.03006, "x": 27.8, "y": 55.2, "source": "geonames", "type": "PPL" }, { "name": "Loung", "latitude": 5.52409, "longitude": 10.04117, "x": 34.4, "y": 42.6, "source": "geonames", "type": "PPL" }, { "name": "Maka", "latitude": 5.41597, "longitude": 10.03476, "x": 30.6, "y": 74.5, "source": "geonames", "type": "PPL" }, { "name": "Makong I", "latitude": 5.4549, "longitude": 10.02192, "x": 23, "y": 63, "source": "geonames", "type": "PPL" }, { "name": "Makong II", "latitude": 5.44879, "longitude": 10.0269, "x": 25.9, "y": 64.8, "source": "geonames", "type": "PPL" }, { "name": "Mangha", "latitude": 5.49787, "longitude": 10.01199, "x": 17.1, "y": 50.3, "source": "geonames", "type": "PPL" }, { "name": "Mbeng", "latitude": 5.4869, "longitude": 10.09932, "x": 68.7, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Mefet", "latitude": 5.4985, "longitude": 10.02678, "x": 25.8, "y": 50.1, "source": "geonames", "type": "PPL" }, { "name": "Meka", "latitude": 5.38488, "longitude": 10.08956, "x": 63, "y": 83.7, "source": "geonames", "type": "PPL" }, { "name": "Mela'", "latitude": 5.46169, "longitude": 10.06436, "x": 48.1, "y": 61, "source": "geonames", "type": "PPL" }, { "name": "Melhi", "latitude": 5.51116, "longitude": 10.08062, "x": 57.7, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Menouet", "latitude": 5.44604, "longitude": 10.03932, "x": 33.3, "y": 65.6, "source": "geonames", "type": "PPL" }, { "name": "Menpe", "latitude": 5.46071, "longitude": 10.07443, "x": 54, "y": 61.3, "source": "geonames", "type": "PPL" }, { "name": "Mensi", "latitude": 5.42685, "longitude": 10.10972, "x": 74.9, "y": 71.3, "source": "geonames", "type": "PPL" }, { "name": "Mete", "latitude": 5.48961, "longitude": 10.03671, "x": 31.7, "y": 52.8, "source": "geonames", "type": "PPL" }, { "name": "Metsang", "latitude": 5.42995, "longitude": 10.12815, "x": 85.8, "y": 70.4, "source": "geonames", "type": "PPL" }, { "name": "Metsi", "latitude": 5.51178, "longitude": 10.08803, "x": 62.1, "y": 46.2, "source": "geonames", "type": "PPL" }, { "name": "Mindjou", "latitude": 5.41073, "longitude": 10.05786, "x": 44.2, "y": 76.1, "source": "geonames", "type": "PPL" }, { "name": "Mingou", "latitude": 5.44402, "longitude": 10.04401, "x": 36, "y": 66.2, "source": "geonames", "type": "PPL" }, { "name": "Minka", "latitude": 5.42532, "longitude": 10.05134, "x": 40.4, "y": 71.8, "source": "geonames", "type": "PPL" }, { "name": "Moula", "latitude": 5.49196, "longitude": 10.09992, "x": 69.1, "y": 52.1, "source": "geonames", "type": "PPL" }, { "name": "Mve I", "latitude": 5.41965, "longitude": 10.05702, "x": 43.7, "y": 73.4, "source": "geonames", "type": "PPL" }, { "name": "Mve II", "latitude": 5.41404, "longitude": 10.05231, "x": 40.9, "y": 75.1, "source": "geonames", "type": "PPL" }, { "name": "Mve III", "latitude": 5.39924, "longitude": 10.06814, "x": 50.3, "y": 79.5, "source": "geonames", "type": "PPL" }, { "name": "Ndenkwop", "latitude": 5.48954, "longitude": 10.01102, "x": 16.5, "y": 52.8, "source": "geonames", "type": "PPL" }, { "name": "Ndounga", "latitude": 5.43046, "longitude": 10.0526, "x": 41.1, "y": 70.2, "source": "geonames", "type": "PPL" }, { "name": "Ndoutsang", "latitude": 5.43082, "longitude": 10.10794, "x": 73.8, "y": 70.1, "source": "geonames", "type": "PPL" }, { "name": "Nfa", "latitude": 5.46421, "longitude": 10.01017, "x": 16, "y": 60.3, "source": "geonames", "type": "PPL" }, { "name": "Ngoua", "latitude": 5.4296, "longitude": 10.08564, "x": 60.7, "y": 70.5, "source": "geonames", "type": "PPL" }, { "name": "Ngoua II", "latitude": 5.43332, "longitude": 10.07275, "x": 53, "y": 69.4, "source": "geonames", "type": "PPL" }, { "name": "Nguili", "latitude": 5.4125, "longitude": 10.10766, "x": 73.7, "y": 75.6, "source": "geonames", "type": "PPL" }, { "name": "Nkak", "latitude": 5.494, "longitude": 10.11155, "x": 76, "y": 51.5, "source": "geonames", "type": "PPL" }, { "name": "Nki", "latitude": 5.38657, "longitude": 10.05823, "x": 44.4, "y": 83.2, "source": "geonames", "type": "PPL" }, { "name": "Nli", "latitude": 5.44861, "longitude": 10.01485, "x": 18.8, "y": 64.9, "source": "geonames", "type": "PPL" }, { "name": "Nsui", "latitude": 5.46376, "longitude": 10.08065, "x": 57.7, "y": 60.4, "source": "geonames", "type": "PPL" }, { "name": "Nte", "latitude": 5.47729, "longitude": 10.08556, "x": 60.6, "y": 56.4, "source": "geonames", "type": "PPL" }, { "name": "Ntsa", "latitude": 5.4992, "longitude": 10.10053, "x": 69.5, "y": 49.9, "source": "geonames", "type": "PPL" }, { "name": "Ntse'", "latitude": 5.37183, "longitude": 10.01234, "x": 17.3, "y": 87.6, "source": "geonames", "type": "PPL" }, { "name": "Nza", "latitude": 5.47986, "longitude": 10.05385, "x": 41.9, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Nzenkop", "latitude": 5.48193, "longitude": 10.10815, "x": 74, "y": 55, "source": "geonames", "type": "PPL" }, { "name": "Nzinfeng", "latitude": 5.46794, "longitude": 10.10339, "x": 71.2, "y": 59.2, "source": "geonames", "type": "PPL" }, { "name": "Nzong", "latitude": 5.47196, "longitude": 10.08892, "x": 62.6, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Pagapuh", "latitude": 5.63441, "longitude": 10.05196, "x": 40.7, "y": 10, "source": "geonames", "type": "PPLX" }, { "name": "Sinte", "latitude": 5.46355, "longitude": 10.0464, "x": 37.4, "y": 60.5, "source": "geonames", "type": "PPL" }, { "name": "Suala'", "latitude": 5.44121, "longitude": 10.10347, "x": 71.2, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Tchouagoua", "latitude": 5.41436, "longitude": 10.06386, "x": 47.8, "y": 75, "source": "geonames", "type": "PPL" }, { "name": "Tchouale", "latitude": 5.45431, "longitude": 10.03877, "x": 32.9, "y": 63.2, "source": "geonames", "type": "PPL" }, { "name": "Titia", "latitude": 5.46065, "longitude": 10.05555, "x": 42.9, "y": 61.3, "source": "geonames", "type": "PPL" }, { "name": "To'tchi", "latitude": 5.3636, "longitude": 10.0688, "x": 50.7, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Tougong", "latitude": 5.38238, "longitude": 10.09587, "x": 66.7, "y": 84.5, "source": "geonames", "type": "PPL" }, { "name": "Toula", "latitude": 5.50339, "longitude": 10.04447, "x": 36.3, "y": 48.7, "source": "geonames", "type": "PPL" }, { "name": "Toula' Dezong", "latitude": 5.46368, "longitude": 10.09587, "x": 66.7, "y": 60.4, "source": "geonames", "type": "PPL" }, { "name": "Toula' Foguimgo", "latitude": 5.48943, "longitude": 10.04756, "x": 38.1, "y": 52.8, "source": "geonames", "type": "PPL" }, { "name": "Toulepe", "latitude": 5.47342, "longitude": 10.07278, "x": 53, "y": 57.6, "source": "geonames", "type": "PPL" }, { "name": "Touopou", "latitude": 5.41957, "longitude": 10.03005, "x": 27.8, "y": 73.5, "source": "geonames", "type": "PPL" }, { "name": "Touzong", "latitude": 5.44125, "longitude": 10.11148, "x": 75.9, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Tsinga", "latitude": 5.4416, "longitude": 10.09524, "x": 66.3, "y": 67, "source": "geonames", "type": "PPL" }, { "name": "Tsingbe", "latitude": 5.45403, "longitude": 10.11942, "x": 80.6, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Tsingfou", "latitude": 5.45937, "longitude": 10.11678, "x": 79.1, "y": 61.7, "source": "geonames", "type": "PPL" }, { "name": "Tsingko", "latitude": 5.43647, "longitude": 10.00426, "x": 12.5, "y": 68.5, "source": "geonames", "type": "PPL" }, { "name": "Tsingla'", "latitude": 5.47047, "longitude": 10.00922, "x": 15.5, "y": 58.4, "source": "geonames", "type": "PPL" }, { "name": "Tsinkop", "latitude": 5.45452, "longitude": 10.0579, "x": 44.2, "y": 63.1, "source": "geonames", "type": "PPL" }, { "name": "Tsintse", "latitude": 5.43218, "longitude": 10.03313, "x": 29.6, "y": 69.7, "source": "geonames", "type": "PPL" }, { "name": "Tsinza'", "latitude": 5.49557, "longitude": 10.04207, "x": 34.9, "y": 51, "source": "geonames", "type": "PPL" }, { "name": "Wonla'", "latitude": 5.42745, "longitude": 10.00602, "x": 13.6, "y": 71.1, "source": "geonames", "type": "PPL" }, { "name": "Yang", "latitude": 5.45486, "longitude": 10.09862, "x": 68.3, "y": 63, "source": "geonames", "type": "PPL" }, { "name": "Zem", "latitude": 5.4749, "longitude": 10.0575, "x": 44, "y": 57.1, "source": "geonames", "type": "PPL" }, { "name": "Zemda", "latitude": 5.51301, "longitude": 10.01617, "x": 19.6, "y": 45.9, "source": "geonames", "type": "PPL" }, { "name": "Zemeza", "latitude": 5.44412, "longitude": 10.13525, "x": 90, "y": 66.2, "source": "geonames", "type": "PPL" }, { "name": "Zemgho", "latitude": 5.37687, "longitude": 10.09543, "x": 66.4, "y": 86.1, "source": "geonames", "type": "PPL" }, { "name": "Zemny", "latitude": 5.425, "longitude": 10.12198, "x": 82.2, "y": 71.9, "source": "geonames", "type": "PPL" }, { "name": "Zengbing", "latitude": 5.42002, "longitude": 10.11044, "x": 75.3, "y": 73.3, "source": "geonames", "type": "PPL" }, { "name": "Zideng", "latitude": 5.43982, "longitude": 10.01746, "x": 20.3, "y": 67.5, "source": "geonames", "type": "PPL" }, { "name": "Zingbwot", "latitude": 5.4249, "longitude": 10.00167, "x": 11, "y": 71.9, "source": "geonames", "type": "PPL" }, { "name": "Zuenla", "latitude": 5.50142, "longitude": 10.10674, "x": 73.1, "y": 49.3, "source": "geonames", "type": "PPL" } ], "Ebolowa": [ { "name": "Abang", "latitude": 2.91667, "longitude": 11.11667, "x": 43.8, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "About", "latitude": 2.9, "longitude": 11.21667, "x": 62.3, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Adjandounga", "latitude": 2.85, "longitude": 11.36667, "x": 90, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Adjap", "latitude": 2.96667, "longitude": 11.11667, "x": 43.8, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Adoum", "latitude": 2.88333, "longitude": 11.13333, "x": 46.9, "y": 51.9, "source": "geonames", "type": "PPL" }, { "name": "Afanengong", "latitude": 2.93333, "longitude": 11.05, "x": 31.5, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Akak", "latitude": 2.93333, "longitude": 11.1, "x": 40.8, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Akok", "latitude": 2.71667, "longitude": 11.25, "x": 68.5, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Akom", "latitude": 2.93333, "longitude": 11.05, "x": 31.5, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Alame", "latitude": 2.86667, "longitude": 10.96667, "x": 16.2, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Alen", "latitude": 2.76667, "longitude": 11.1, "x": 40.8, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Aloum", "latitude": 2.96667, "longitude": 10.95, "x": 13.1, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Amvam", "latitude": 2.71667, "longitude": 11.26667, "x": 71.5, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Amwam", "latitude": 2.91667, "longitude": 11.06667, "x": 34.6, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Assosseng", "latitude": 2.81667, "longitude": 11.13333, "x": 46.9, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Ating", "latitude": 3.06667, "longitude": 11.11667, "x": 43.8, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Atooveng", "latitude": 2.96667, "longitude": 11.35, "x": 86.9, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Azem", "latitude": 2.86667, "longitude": 11.15, "x": 50, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Bakoa", "latitude": 2.8, "longitude": 11.2, "x": 59.2, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Bebae", "latitude": 2.75, "longitude": 11.1, "x": 40.8, "y": 82.4, "source": "geonames", "type": "PPL" }, { "name": "Bembe", "latitude": 2.8, "longitude": 11.23333, "x": 65.4, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Bernbe", "latitude": 2.93333, "longitude": 11.25, "x": 68.5, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Biba", "latitude": 2.76667, "longitude": 11.3, "x": 77.7, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Bibouleman", "latitude": 2.86667, "longitude": 11.26667, "x": 71.5, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Bikou", "latitude": 2.81667, "longitude": 10.95, "x": 13.1, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Bilom", "latitude": 2.93333, "longitude": 11.15, "x": 50, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Bipwai", "latitude": 2.81667, "longitude": 11.2, "x": 59.2, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Bityli", "latitude": 2.93333, "longitude": 11.18333, "x": 56.2, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Biyeyem", "latitude": 2.83333, "longitude": 11.13333, "x": 46.9, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Bouss", "latitude": 2.9, "longitude": 11.21667, "x": 62.3, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Djop", "latitude": 2.85, "longitude": 11.13333, "x": 46.9, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Ebae", "latitude": 2.95, "longitude": 11.13333, "x": 46.9, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Ebolakoun", "latitude": 2.83333, "longitude": 11.2, "x": 59.2, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Ebolebola", "latitude": 2.91667, "longitude": 11.35, "x": 86.9, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Ekouk", "latitude": 2.78333, "longitude": 11.28333, "x": 74.6, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Ekoumdoum", "latitude": 2.71667, "longitude": 11.06667, "x": 34.6, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Eminwong", "latitude": 2.76667, "longitude": 11.28333, "x": 74.6, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Engom", "latitude": 2.85, "longitude": 11.05, "x": 31.5, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Engong", "latitude": 2.83333, "longitude": 11.01667, "x": 25.4, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Enongal", "latitude": 2.88333, "longitude": 11.18333, "x": 56.2, "y": 51.9, "source": "geonames", "type": "PPL" }, { "name": "Essinguili", "latitude": 2.9, "longitude": 11.18333, "x": 56.2, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Eves", "latitude": 2.93333, "longitude": 11.08333, "x": 37.7, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Foulasi", "latitude": 3, "longitude": 11.11667, "x": 43.8, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Foulassi", "latitude": 3, "longitude": 11.11667, "x": 43.8, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Keke", "latitude": 2.91667, "longitude": 11.26667, "x": 71.5, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Konda", "latitude": 2.86667, "longitude": 11.1, "x": 40.8, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Kongolou", "latitude": 2.98333, "longitude": 11.11667, "x": 43.8, "y": 29, "source": "geonames", "type": "PPL" }, { "name": "Lloa", "latitude": 2.95, "longitude": 11.11667, "x": 43.8, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Maamezam", "latitude": 2.76667, "longitude": 11.18333, "x": 56.2, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Madjap", "latitude": 2.83333, "longitude": 10.98333, "x": 19.2, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Mamenyin", "latitude": 3.05, "longitude": 11.05, "x": 31.5, "y": 13.8, "source": "geonames", "type": "PPL" }, { "name": "Mbilntangan", "latitude": 2.75, "longitude": 11.21667, "x": 62.3, "y": 82.4, "source": "geonames", "type": "PPL" }, { "name": "Mbondo", "latitude": 2.91667, "longitude": 11.33333, "x": 83.8, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Mbong", "latitude": 2.95, "longitude": 10.98333, "x": 19.2, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Mbout", "latitude": 2.73333, "longitude": 11.08333, "x": 37.7, "y": 86.2, "source": "geonames", "type": "PPL" }, { "name": "Mebem", "latitude": 2.98333, "longitude": 10.95, "x": 13.1, "y": 29, "source": "geonames", "type": "PPL" }, { "name": "Mekaa", "latitude": 2.88333, "longitude": 10.93333, "x": 10, "y": 51.9, "source": "geonames", "type": "PPL" }, { "name": "Mekalate", "latitude": 2.9, "longitude": 11.16667, "x": 53.1, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Melange I", "latitude": 3.03333, "longitude": 11.11667, "x": 43.8, "y": 17.6, "source": "geonames", "type": "PPL" }, { "name": "Melate", "latitude": 2.88333, "longitude": 10.95, "x": 13.1, "y": 51.9, "source": "geonames", "type": "PPL" }, { "name": "Metchypale", "latitude": 2.85, "longitude": 11.18333, "x": 56.2, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Meyo", "latitude": 2.83333, "longitude": 11.01667, "x": 25.4, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Meyos", "latitude": 2.8, "longitude": 11.25, "x": 68.5, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Mintom", "latitude": 2.95, "longitude": 10.96667, "x": 16.2, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Monelam", "latitude": 2.86667, "longitude": 11.13333, "x": 46.9, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Mvam", "latitude": 2.86667, "longitude": 11.15, "x": 50, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Mveng", "latitude": 2.85, "longitude": 11.13333, "x": 46.9, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Mvie", "latitude": 2.93333, "longitude": 11.23333, "x": 65.4, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Mvieng", "latitude": 2.81667, "longitude": 10.95, "x": 13.1, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Mvoula", "latitude": 2.85, "longitude": 11.18333, "x": 56.2, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Mwila", "latitude": 2.95, "longitude": 11.01667, "x": 25.4, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Nboabeng", "latitude": 2.96667, "longitude": 11.35, "x": 86.9, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Ndengue", "latitude": 2.78333, "longitude": 11.11667, "x": 43.8, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Ndjantom", "latitude": 2.95, "longitude": 10.98333, "x": 19.2, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Nemeyong", "latitude": 2.9, "longitude": 11.25, "x": 68.5, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Nezam", "latitude": 2.93333, "longitude": 11.26667, "x": 71.5, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Ngalan", "latitude": 2.93333, "longitude": 11.11667, "x": 43.8, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Ngalane", "latitude": 2.8, "longitude": 11.23333, "x": 65.4, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Ngate", "latitude": 2.93333, "longitude": 11.25, "x": 68.5, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Ngoakele", "latitude": 3.05, "longitude": 11.21667, "x": 62.3, "y": 13.8, "source": "geonames", "type": "PPL" }, { "name": "Ngoazip I", "latitude": 3.05, "longitude": 11.03333, "x": 28.5, "y": 13.8, "source": "geonames", "type": "PPL" }, { "name": "Ngoboo", "latitude": 2.96667, "longitude": 11.33333, "x": 83.8, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Ngoulessaman", "latitude": 2.95, "longitude": 11.31667, "x": 80.8, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Njafob", "latitude": 2.81667, "longitude": 11.2, "x": 59.2, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Nkan", "latitude": 2.93333, "longitude": 11.21667, "x": 62.3, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Nkoadjap", "latitude": 2.95, "longitude": 10.96667, "x": 16.2, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Nkoavos", "latitude": 2.91667, "longitude": 11.13333, "x": 46.9, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Nkoemvom", "latitude": 2.8, "longitude": 11.13333, "x": 46.9, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Nkoetye", "latitude": 2.86667, "longitude": 11.31667, "x": 80.8, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Nkolendom", "latitude": 2.78333, "longitude": 11.16667, "x": 53.1, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Nkolmvon", "latitude": 2.73333, "longitude": 11.21667, "x": 62.3, "y": 86.2, "source": "geonames", "type": "PPL" }, { "name": "Nkondjom", "latitude": 2.86667, "longitude": 11.28333, "x": 74.6, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Nkoungoulou", "latitude": 2.85, "longitude": 11.35, "x": 86.9, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Nkounkout", "latitude": 2.91667, "longitude": 11.3, "x": 77.7, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Nkovos", "latitude": 2.91667, "longitude": 11.36667, "x": 90, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Obang I", "latitude": 3.06667, "longitude": 11.23333, "x": 65.4, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Olem", "latitude": 2.93333, "longitude": 11.08333, "x": 37.7, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Sonkote", "latitude": 2.86667, "longitude": 11.01667, "x": 25.4, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Sonkoue", "latitude": 2.76667, "longitude": 11.31667, "x": 80.8, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Tchangue", "latitude": 2.96667, "longitude": 10.95, "x": 13.1, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Yem", "latitude": 2.83333, "longitude": 11.03333, "x": 28.5, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Zalom", "latitude": 2.95, "longitude": 11.03333, "x": 28.5, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Zingui", "latitude": 2.81667, "longitude": 10.96667, "x": 16.2, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Zouameyong", "latitude": 2.88333, "longitude": 11.21667, "x": 62.3, "y": 51.9, "source": "geonames", "type": "PPL" } ], "Edea": [ { "name": "Abou", "latitude": 3.61667, "longitude": 10.11667, "x": 24.1, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Betombe", "latitude": 3.8, "longitude": 10.16667, "x": 38.2, "y": 53.3, "source": "geonames", "type": "PPL" }, { "name": "Binkou", "latitude": 3.96667, "longitude": 10.16667, "x": 38.2, "y": 20, "source": "geonames", "type": "PPL" }, { "name": "Bisombe", "latitude": 4.01667, "longitude": 10.1, "x": 19.4, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Bitoutouk", "latitude": 3.65, "longitude": 10.25, "x": 61.8, "y": 83.3, "source": "geonames", "type": "PPL" }, { "name": "Daniel", "latitude": 3.83333, "longitude": 10.11667, "x": 24.1, "y": 46.7, "source": "geonames", "type": "PPL" }, { "name": "Diboumbou", "latitude": 3.78333, "longitude": 10.1, "x": 19.4, "y": 56.7, "source": "geonames", "type": "PPL" }, { "name": "Digombi", "latitude": 3.66667, "longitude": 10.28333, "x": 71.2, "y": 80, "source": "geonames", "type": "PPL" }, { "name": "Eboga", "latitude": 3.76667, "longitude": 10.13333, "x": 28.8, "y": 60, "source": "geonames", "type": "PPL" }, { "name": "Ekite", "latitude": 3.78333, "longitude": 10.06667, "x": 10, "y": 56.7, "source": "geonames", "type": "PPL" }, { "name": "Ekouyak", "latitude": 3.65, "longitude": 10.11667, "x": 24.1, "y": 83.3, "source": "geonames", "type": "PPL" }, { "name": "Elobi", "latitude": 3.88333, "longitude": 10.28333, "x": 71.2, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Etouha", "latitude": 3.66667, "longitude": 10.3, "x": 75.9, "y": 80, "source": "geonames", "type": "PPL" }, { "name": "Kopongo", "latitude": 3.95, "longitude": 10.1, "x": 19.4, "y": 23.3, "source": "geonames", "type": "PPL" }, { "name": "Lebnyok", "latitude": 3.81667, "longitude": 10.23333, "x": 57.1, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Libombe", "latitude": 3.81667, "longitude": 10.35, "x": 90, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Lougan", "latitude": 3.91667, "longitude": 10.13333, "x": 28.8, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Malimba", "latitude": 3.9, "longitude": 10.1, "x": 19.4, "y": 33.3, "source": "geonames", "type": "PPL" }, { "name": "Mapan", "latitude": 3.73333, "longitude": 10.21667, "x": 52.4, "y": 66.7, "source": "geonames", "type": "PPL" }, { "name": "Mason", "latitude": 3.86667, "longitude": 10.16667, "x": 38.2, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Mitounga", "latitude": 3.8, "longitude": 10.2, "x": 47.6, "y": 53.3, "source": "geonames", "type": "PPL" }, { "name": "Mongombe", "latitude": 3.81667, "longitude": 10.11667, "x": 24.1, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Ndogbenan", "latitude": 3.76667, "longitude": 10.1, "x": 19.4, "y": 60, "source": "geonames", "type": "PPL" }, { "name": "Ndokok", "latitude": 3.83333, "longitude": 10.28333, "x": 71.2, "y": 46.7, "source": "geonames", "type": "PPL" }, { "name": "Nkom", "latitude": 4, "longitude": 10.18333, "x": 42.9, "y": 13.3, "source": "geonames", "type": "PPL" }, { "name": "Nkomenan", "latitude": 3.86667, "longitude": 10.13333, "x": 28.8, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Nkoukoue", "latitude": 3.71667, "longitude": 10.1, "x": 19.4, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Ouingok", "latitude": 3.96667, "longitude": 10.11667, "x": 24.1, "y": 20, "source": "geonames", "type": "PPL" }, { "name": "So Lopa", "latitude": 3.68333, "longitude": 10.25, "x": 61.8, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Song Ndong", "latitude": 3.83333, "longitude": 10.25, "x": 61.8, "y": 46.7, "source": "geonames", "type": "PPL" }, { "name": "Songndong", "latitude": 3.88333, "longitude": 10.28333, "x": 71.2, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Soundjouk", "latitude": 3.98333, "longitude": 10.16667, "x": 38.2, "y": 16.7, "source": "geonames", "type": "PPL" } ], "Ekondo-Titi": [ { "name": "Baba", "latitude": 4.5051, "longitude": 9.1335, "x": 66.7, "y": 68.2, "source": "geonames", "type": "PPL" }, { "name": "Bafaka", "latitude": 4.7271, "longitude": 9.1625, "x": 73.7, "y": 20.8, "source": "geonames", "type": "PPL" }, { "name": "Bai Dieka", "latitude": 4.48333, "longitude": 9.13333, "x": 66.6, "y": 72.8, "source": "geonames", "type": "PPL" }, { "name": "Bai Estate", "latitude": 4.4762, "longitude": 9.1371, "x": 67.5, "y": 74.3, "source": "geonames", "type": "PPL" }, { "name": "Bai Foe", "latitude": 4.4748, "longitude": 9.2124, "x": 85.9, "y": 74.6, "source": "geonames", "type": "PPL" }, { "name": "Bai Grass", "latitude": 4.4681, "longitude": 9.1967, "x": 82.1, "y": 76.1, "source": "geonames", "type": "PPL" }, { "name": "Bai Kuke", "latitude": 4.4589, "longitude": 9.1449, "x": 69.4, "y": 78, "source": "geonames", "type": "PPL" }, { "name": "Bai Sombe", "latitude": 4.45214, "longitude": 9.1526, "x": 71.3, "y": 79.5, "source": "geonames", "type": "PPL" }, { "name": "Bakundu Foe", "latitude": 4.4721, "longitude": 9.2235, "x": 88.7, "y": 75.2, "source": "geonames", "type": "PPL" }, { "name": "Bambuko", "latitude": 4.4406, "longitude": 9.1225, "x": 64, "y": 81.9, "source": "geonames", "type": "PPL" }, { "name": "Barombi Mokoko", "latitude": 4.4909, "longitude": 9.0781, "x": 53.1, "y": 71.2, "source": "geonames", "type": "PPL" }, { "name": "Beach", "latitude": 4.6066, "longitude": 9.0065, "x": 35.6, "y": 46.5, "source": "geonames", "type": "PPL" }, { "name": "Bekatako", "latitude": 4.6507, "longitude": 9.095, "x": 57.2, "y": 37.1, "source": "geonames", "type": "PPL" }, { "name": "Bekura", "latitude": 4.6051, "longitude": 9.0978, "x": 57.9, "y": 46.9, "source": "geonames", "type": "PPL" }, { "name": "Bissoro", "latitude": 4.7555, "longitude": 9.1736, "x": 76.5, "y": 14.8, "source": "geonames", "type": "PPL" }, { "name": "Blask Bush", "latitude": 4.6407, "longitude": 8.9018, "x": 10, "y": 39.3, "source": "geonames", "type": "PPL" }, { "name": "Bogongo", "latitude": 4.5804, "longitude": 9.1041, "x": 59.5, "y": 52.1, "source": "geonames", "type": "PPL" }, { "name": "Bokosso", "latitude": 4.4178, "longitude": 9.1445, "x": 69.3, "y": 86.8, "source": "geonames", "type": "PPL" }, { "name": "Bonja", "latitude": 4.4671, "longitude": 9.1085, "x": 60.5, "y": 76.3, "source": "geonames", "type": "PPL" }, { "name": "Diboki Balue", "latitude": 4.7779, "longitude": 9.1764, "x": 77.1, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Dienyi", "latitude": 4.6291, "longitude": 9.2179, "x": 87.3, "y": 41.7, "source": "geonames", "type": "PPL" }, { "name": "Dora", "latitude": 4.6359, "longitude": 9.1891, "x": 80.2, "y": 40.3, "source": "geonames", "type": "PPL" }, { "name": "Ebie", "latitude": 4.4201, "longitude": 9.1535, "x": 71.5, "y": 86.3, "source": "geonames", "type": "PPL" }, { "name": "Ekondo Nene", "latitude": 4.6884, "longitude": 8.96506, "x": 25.5, "y": 29.1, "source": "geonames", "type": "PPL" }, { "name": "Ekumbe Ekundunene", "latitude": 4.4835, "longitude": 9.1557, "x": 72.1, "y": 72.8, "source": "geonames", "type": "PPL" }, { "name": "Ekumbe Liongo", "latitude": 4.4982, "longitude": 9.1061, "x": 60, "y": 69.7, "source": "geonames", "type": "PPL" }, { "name": "Ekumbe Mofako", "latitude": 4.478, "longitude": 9.0945, "x": 57.1, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Ekwe", "latitude": 4.6906, "longitude": 9.1656, "x": 74.5, "y": 28.6, "source": "geonames", "type": "PPL" }, { "name": "Iloani", "latitude": 4.5053, "longitude": 9.0251, "x": 40.1, "y": 68.1, "source": "geonames", "type": "PPL" }, { "name": "Kita", "latitude": 4.6252, "longitude": 8.9729, "x": 27.4, "y": 42.6, "source": "geonames", "type": "PPL" }, { "name": "Koto", "latitude": 4.6902, "longitude": 9.1986, "x": 82.6, "y": 28.7, "source": "geonames", "type": "PPL" }, { "name": "Kumbe Balondo", "latitude": 4.5816, "longitude": 9.0509, "x": 46.5, "y": 51.9, "source": "geonames", "type": "PPL" }, { "name": "Kumbe Balue", "latitude": 4.6921, "longitude": 9.2097, "x": 85.3, "y": 28.3, "source": "geonames", "type": "PPL" }, { "name": "Lipenja Camp", "latitude": 4.6233, "longitude": 9.0996, "x": 58.4, "y": 43, "source": "geonames", "type": "PPL" }, { "name": "Lipenja Village", "latitude": 4.6446, "longitude": 9.0935, "x": 56.9, "y": 38.4, "source": "geonames", "type": "PPL" }, { "name": "Lobe", "latitude": 4.6014, "longitude": 9.0802, "x": 53.6, "y": 47.6, "source": "geonames", "type": "PPL" }, { "name": "Lobe Village", "latitude": 4.5893, "longitude": 9.0425, "x": 44.4, "y": 50.2, "source": "geonames", "type": "PPL" }, { "name": "Loe", "latitude": 4.7297, "longitude": 8.9382, "x": 18.9, "y": 20.3, "source": "geonames", "type": "PPL" }, { "name": "Mabonji", "latitude": 4.566, "longitude": 9.1986, "x": 82.6, "y": 55.2, "source": "geonames", "type": "PPL" }, { "name": "Marumba", "latitude": 4.58333, "longitude": 9.08333, "x": 54.4, "y": 51.5, "source": "geonames", "type": "PPL" }, { "name": "Masore", "latitude": 4.65481, "longitude": 8.99404, "x": 32.6, "y": 36.3, "source": "geonames", "type": "PPL" }, { "name": "Matutu", "latitude": 4.5858, "longitude": 8.9481, "x": 21.3, "y": 51, "source": "geonames", "type": "PPL" }, { "name": "Mbonge", "latitude": 4.5343, "longitude": 9.1069, "x": 60.1, "y": 62, "source": "geonames", "type": "PPL" }, { "name": "Meme", "latitude": 4.5316, "longitude": 9.0798, "x": 53.5, "y": 62.5, "source": "geonames", "type": "PPL" }, { "name": "Metoko", "latitude": 4.65, "longitude": 8.96667, "x": 25.9, "y": 37.3, "source": "geonames", "type": "PPL" }, { "name": "Metoko Ma Bekondo", "latitude": 4.6113, "longitude": 9.2002, "x": 83, "y": 45.5, "source": "geonames", "type": "PPL" }, { "name": "Mokoko", "latitude": 4.5429, "longitude": 9.0443, "x": 44.8, "y": 60.1, "source": "geonames", "type": "PPL" }, { "name": "Mokona", "latitude": 4.6283, "longitude": 9.1658, "x": 74.5, "y": 41.9, "source": "geonames", "type": "PPL" }, { "name": "Mongossi", "latitude": 4.5757, "longitude": 9.0117, "x": 36.9, "y": 53.1, "source": "geonames", "type": "PPL" }, { "name": "Monyange", "latitude": 4.7522, "longitude": 8.9975, "x": 33.4, "y": 15.5, "source": "geonames", "type": "PPL" }, { "name": "Mukoro", "latitude": 4.7367, "longitude": 9.1953, "x": 81.8, "y": 18.8, "source": "geonames", "type": "PPL" }, { "name": "Mundongo", "latitude": 4.4056, "longitude": 9.1297, "x": 65.7, "y": 89.4, "source": "geonames", "type": "PPL" }, { "name": "Munyange", "latitude": 4.4028, "longitude": 9.1185, "x": 63, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Narendi", "latitude": 4.7474, "longitude": 9.0914, "x": 56.4, "y": 16.5, "source": "geonames", "type": "PPL" }, { "name": "Nganjo", "latitude": 4.57435, "longitude": 9.12894, "x": 65.5, "y": 53.4, "source": "geonames", "type": "PPL" }, { "name": "Nganjo Bolende", "latitude": 4.577, "longitude": 9.1565, "x": 72.3, "y": 52.8, "source": "geonames", "type": "PPL" }, { "name": "Ngolo Metoko", "latitude": 4.6547, "longitude": 9.0511, "x": 46.5, "y": 36.3, "source": "geonames", "type": "PPL" }, { "name": "Ngongo", "latitude": 4.5446, "longitude": 9.229, "x": 90, "y": 59.8, "source": "geonames", "type": "PPL" }, { "name": "Ngwenge", "latitude": 4.6646, "longitude": 9.0964, "x": 57.6, "y": 34.2, "source": "geonames", "type": "PPL" }, { "name": "Nyange", "latitude": 4.6498, "longitude": 9.2155, "x": 86.7, "y": 37.3, "source": "geonames", "type": "PPL" }, { "name": "Pundu", "latitude": 4.5149, "longitude": 9.1313, "x": 66.1, "y": 66.1, "source": "geonames", "type": "PPL" } ], "Eseka": [ { "name": "Badjob", "latitude": 3.68333, "longitude": 10.68333, "x": 32.4, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Bangsombi", "latitude": 3.75, "longitude": 10.7, "x": 35.6, "y": 35.5, "source": "geonames", "type": "PPL" }, { "name": "Batigui", "latitude": 3.63333, "longitude": 10.58333, "x": 13.2, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Bidjoka", "latitude": 3.7, "longitude": 10.6, "x": 16.4, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Bihouya", "latitude": 3.83333, "longitude": 10.73333, "x": 42, "y": 17.3, "source": "geonames", "type": "PPL" }, { "name": "Bilagal", "latitude": 3.73333, "longitude": 10.73333, "x": 42, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Bodi", "latitude": 3.51667, "longitude": 10.66667, "x": 29.2, "y": 86.4, "source": "geonames", "type": "PPL" }, { "name": "Bogso", "latitude": 3.73333, "longitude": 10.81667, "x": 58, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Ekokboum", "latitude": 3.66667, "longitude": 10.56667, "x": 10, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Makas", "latitude": 3.76667, "longitude": 10.86667, "x": 67.6, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Makomol", "latitude": 3.58333, "longitude": 10.8, "x": 54.8, "y": 71.8, "source": "geonames", "type": "PPL" }, { "name": "Makot", "latitude": 3.5, "longitude": 10.6, "x": 16.4, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Mandjok", "latitude": 3.55, "longitude": 10.88333, "x": 70.8, "y": 79.1, "source": "geonames", "type": "PPL" }, { "name": "Mangenges", "latitude": 3.56667, "longitude": 10.7, "x": 35.6, "y": 75.5, "source": "geonames", "type": "PPL" }, { "name": "Mapan III", "latitude": 3.8, "longitude": 10.81667, "x": 58, "y": 24.5, "source": "geonames", "type": "PPL" }, { "name": "Mbandjouk", "latitude": 3.7, "longitude": 10.93333, "x": 80.4, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Mbanga", "latitude": 3.86667, "longitude": 10.81667, "x": 58, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Mboglom", "latitude": 3.66667, "longitude": 10.98333, "x": 90, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Memel", "latitude": 3.8, "longitude": 10.75, "x": 45.2, "y": 24.5, "source": "geonames", "type": "PPL" }, { "name": "Ngibasal", "latitude": 3.58333, "longitude": 10.8, "x": 54.8, "y": 71.8, "source": "geonames", "type": "PPL" }, { "name": "Njok", "latitude": 3.61667, "longitude": 10.8, "x": 54.8, "y": 64.5, "source": "geonames", "type": "PPL" }, { "name": "Nkoloumba", "latitude": 3.65, "longitude": 10.73333, "x": 42, "y": 57.3, "source": "geonames", "type": "PPL" }, { "name": "Nkosim", "latitude": 3.83333, "longitude": 10.8, "x": 54.8, "y": 17.3, "source": "geonames", "type": "PPL" }, { "name": "Nsimekele", "latitude": 3.83333, "longitude": 10.83333, "x": 61.2, "y": 17.3, "source": "geonames", "type": "PPL" }, { "name": "Penanhouat", "latitude": 3.7, "longitude": 10.66667, "x": 29.2, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Sipoue", "latitude": 3.5, "longitude": 10.75, "x": 45.2, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Sogol", "latitude": 3.8, "longitude": 10.63333, "x": 22.8, "y": 24.5, "source": "geonames", "type": "PPL" }, { "name": "Song Bayang", "latitude": 3.76667, "longitude": 10.73333, "x": 42, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Song Poa", "latitude": 3.75, "longitude": 10.66667, "x": 29.2, "y": 35.5, "source": "geonames", "type": "PPL" }, { "name": "Songndeng", "latitude": 3.68333, "longitude": 10.9, "x": 74, "y": 50, "source": "geonames", "type": "PPL" } ], "Evodoula": [ { "name": "Adjap", "latitude": 3.98333, "longitude": 11.23333, "x": 90, "y": 65, "source": "geonames", "type": "PPL" }, { "name": "Egba", "latitude": 3.98333, "longitude": 11.08333, "x": 34.6, "y": 65, "source": "geonames", "type": "PPL" }, { "name": "Ekoangombe", "latitude": 3.96667, "longitude": 11.03333, "x": 16.2, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Elale", "latitude": 4.06667, "longitude": 11.13333, "x": 53.1, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Elig Ndoum", "latitude": 4.05, "longitude": 11.2, "x": 77.7, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Etok", "latitude": 4.1, "longitude": 11.15, "x": 59.2, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Koudi", "latitude": 3.91667, "longitude": 11.23333, "x": 90, "y": 85, "source": "geonames", "type": "PPL" }, { "name": "Lea Ngonbi", "latitude": 4.13333, "longitude": 11.08333, "x": 34.6, "y": 20, "source": "geonames", "type": "PPL" }, { "name": "Libobi", "latitude": 4.08333, "longitude": 11.05, "x": 22.3, "y": 35, "source": "geonames", "type": "PPL" }, { "name": "Lihong", "latitude": 4.13333, "longitude": 11.05, "x": 22.3, "y": 20, "source": "geonames", "type": "PPL" }, { "name": "Lika", "latitude": 4.11667, "longitude": 11.01667, "x": 10, "y": 25, "source": "geonames", "type": "PPL" }, { "name": "Lobo", "latitude": 3.9, "longitude": 11.23333, "x": 90, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Mabobol", "latitude": 3.98333, "longitude": 11.06667, "x": 28.5, "y": 65, "source": "geonames", "type": "PPL" }, { "name": "Makak", "latitude": 3.98333, "longitude": 11.2, "x": 77.7, "y": 65, "source": "geonames", "type": "PPL" }, { "name": "Mandjandja I", "latitude": 4.05, "longitude": 11.06667, "x": 28.5, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Mandjandja II", "latitude": 4.05, "longitude": 11.1, "x": 40.8, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Mandoi", "latitude": 3.98333, "longitude": 11.08333, "x": 34.6, "y": 65, "source": "geonames", "type": "PPL" }, { "name": "Manganga", "latitude": 4.1, "longitude": 11.05, "x": 22.3, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Manginda I", "latitude": 4.13333, "longitude": 11.01667, "x": 10, "y": 20, "source": "geonames", "type": "PPL" }, { "name": "Mango", "latitude": 4.15, "longitude": 11.08333, "x": 34.6, "y": 15, "source": "geonames", "type": "PPL" }, { "name": "Mebem", "latitude": 3.98333, "longitude": 11.23333, "x": 90, "y": 65, "source": "geonames", "type": "PPL" }, { "name": "Mekak", "latitude": 4.01667, "longitude": 11.2, "x": 77.7, "y": 55, "source": "geonames", "type": "PPL" }, { "name": "Meyos", "latitude": 4.13333, "longitude": 11.13333, "x": 53.1, "y": 20, "source": "geonames", "type": "PPL" }, { "name": "Ngibasal", "latitude": 4.06667, "longitude": 11.05, "x": 22.3, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Nkodasa", "latitude": 4.05, "longitude": 11.21667, "x": 83.8, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Nkolasa", "latitude": 4.15, "longitude": 11.2, "x": 77.7, "y": 15, "source": "geonames", "type": "PPL" }, { "name": "Nkolekotking", "latitude": 4.06667, "longitude": 11.16667, "x": 65.4, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Nkolfem", "latitude": 4.08333, "longitude": 11.2, "x": 77.7, "y": 35, "source": "geonames", "type": "PPL" }, { "name": "Nkolkougda", "latitude": 4.1, "longitude": 11.21667, "x": 83.8, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Nkolmeyos I", "latitude": 4.15, "longitude": 11.13333, "x": 53.1, "y": 15, "source": "geonames", "type": "PPL" }, { "name": "Nkolmvanze", "latitude": 4.03333, "longitude": 11.2, "x": 77.7, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Nkolpoblo", "latitude": 4.06667, "longitude": 11.21667, "x": 83.8, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Nkolsing", "latitude": 4.13333, "longitude": 11.15, "x": 59.2, "y": 20, "source": "geonames", "type": "PPL" }, { "name": "Nkong", "latitude": 4.15, "longitude": 11.06667, "x": 28.5, "y": 15, "source": "geonames", "type": "PPL" }, { "name": "Nkongngok", "latitude": 4.15, "longitude": 11.01667, "x": 10, "y": 15, "source": "geonames", "type": "PPL" }, { "name": "Nkotabel", "latitude": 4.13333, "longitude": 11.21667, "x": 83.8, "y": 20, "source": "geonames", "type": "PPL" }, { "name": "Nlongmenanga", "latitude": 4.11667, "longitude": 11.2, "x": 77.7, "y": 25, "source": "geonames", "type": "PPL" }, { "name": "Nloudou", "latitude": 4.13333, "longitude": 11.13333, "x": 53.1, "y": 20, "source": "geonames", "type": "PPL" }, { "name": "Ntouda", "latitude": 4.16667, "longitude": 11.21667, "x": 83.8, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Tala", "latitude": 4.16667, "longitude": 11.23333, "x": 90, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Tombi", "latitude": 4.15, "longitude": 11.03333, "x": 16.2, "y": 15, "source": "geonames", "type": "PPL" }, { "name": "Voua II", "latitude": 4.01667, "longitude": 11.2, "x": 77.7, "y": 55, "source": "geonames", "type": "PPL" }, { "name": "Voua III", "latitude": 4, "longitude": 11.23333, "x": 90, "y": 60, "source": "geonames", "type": "PPL" } ], "Figuil": [ { "name": "Assana", "latitude": 9.56667, "longitude": 13.86667, "x": 36.6, "y": 81.6, "source": "geonames", "type": "PPL" }, { "name": "Babadje", "latitude": 9.69337, "longitude": 13.99931, "x": 66.6, "y": 49.8, "source": "geonames", "type": "PPL" }, { "name": "Babouri", "latitude": 9.79017, "longitude": 13.77741, "x": 16.4, "y": 25.6, "source": "geonames", "type": "PPL" }, { "name": "Badessi", "latitude": 9.70076, "longitude": 13.78132, "x": 17.3, "y": 48, "source": "geonames", "type": "PPL" }, { "name": "Bafouni", "latitude": 9.7377, "longitude": 13.96651, "x": 59.2, "y": 38.7, "source": "geonames", "type": "PPL" }, { "name": "Baila", "latitude": 9.68333, "longitude": 13.83333, "x": 29, "y": 52.4, "source": "geonames", "type": "PPL" }, { "name": "Balasso Wele Soho", "latitude": 9.6959, "longitude": 13.98238, "x": 62.8, "y": 49.2, "source": "geonames", "type": "PPL" }, { "name": "Bering", "latitude": 9.80966, "longitude": 13.74922, "x": 10, "y": 20.7, "source": "geonames", "type": "PPL" }, { "name": "Bikollet", "latitude": 9.53333, "longitude": 13.98333, "x": 63, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Boh", "latitude": 9.81826, "longitude": 13.8036, "x": 22.3, "y": 18.5, "source": "geonames", "type": "PPL" }, { "name": "Bohong", "latitude": 9.84539, "longitude": 13.96737, "x": 59.4, "y": 11.7, "source": "geonames", "type": "PPL" }, { "name": "Bokire-Balda", "latitude": 9.62612, "longitude": 13.89615, "x": 43.3, "y": 66.7, "source": "geonames", "type": "PPL" }, { "name": "Bolgui", "latitude": 9.63333, "longitude": 13.95, "x": 55.5, "y": 64.9, "source": "geonames", "type": "PPL" }, { "name": "Bousa", "latitude": 9.61667, "longitude": 13.98333, "x": 63, "y": 69.1, "source": "geonames", "type": "PPL" }, { "name": "Ciment", "latitude": 9.75627, "longitude": 13.97895, "x": 62, "y": 34.1, "source": "geonames", "type": "PPL" }, { "name": "Daksi", "latitude": 9.61236, "longitude": 13.8916, "x": 42.2, "y": 70.2, "source": "geonames", "type": "PPL" }, { "name": "Dalami", "latitude": 9.71114, "longitude": 13.75663, "x": 11.7, "y": 45.4, "source": "geonames", "type": "PPL" }, { "name": "Djabbi", "latitude": 9.7982, "longitude": 14.0531, "x": 78.8, "y": 23.5, "source": "geonames", "type": "PPL" }, { "name": "Djabi-Boussa", "latitude": 9.62232, "longitude": 13.7994, "x": 21.4, "y": 67.7, "source": "geonames", "type": "PPL" }, { "name": "Djabili", "latitude": 9.75517, "longitude": 13.98971, "x": 64.4, "y": 34.3, "source": "geonames", "type": "PPL" }, { "name": "Djaloume", "latitude": 9.6038, "longitude": 14.0167, "x": 70.6, "y": 72.3, "source": "geonames", "type": "PPL" }, { "name": "Djamtari", "latitude": 9.73811, "longitude": 13.95146, "x": 55.8, "y": 38.6, "source": "geonames", "type": "PPL" }, { "name": "Djarengol", "latitude": 9.77121, "longitude": 13.86733, "x": 36.7, "y": 30.3, "source": "geonames", "type": "PPL" }, { "name": "Douing", "latitude": 9.81667, "longitude": 13.75, "x": 10.2, "y": 18.9, "source": "geonames", "type": "PPL" }, { "name": "Gadavou", "latitude": 9.68466, "longitude": 13.82689, "x": 27.6, "y": 52, "source": "geonames", "type": "PPL" }, { "name": "Ganda", "latitude": 9.76141, "longitude": 13.83379, "x": 29.1, "y": 32.8, "source": "geonames", "type": "PPL" }, { "name": "Ganda Lelel", "latitude": 9.75677, "longitude": 13.74973, "x": 10.1, "y": 33.9, "source": "geonames", "type": "PPL" }, { "name": "Ganda Mango", "latitude": 9.78499, "longitude": 13.82409, "x": 26.9, "y": 26.9, "source": "geonames", "type": "PPL" }, { "name": "Gatouguel", "latitude": 9.80582, "longitude": 13.87972, "x": 39.5, "y": 21.6, "source": "geonames", "type": "PPL" }, { "name": "Golombe", "latitude": 9.64365, "longitude": 13.86816, "x": 36.9, "y": 62.3, "source": "geonames", "type": "PPL" }, { "name": "Golomon", "latitude": 9.8522, "longitude": 14.1026, "x": 90, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Heri", "latitude": 9.82025, "longitude": 13.85564, "x": 34.1, "y": 18, "source": "geonames", "type": "PPL" }, { "name": "Kafinarou", "latitude": 9.66859, "longitude": 13.96238, "x": 58.3, "y": 56.1, "source": "geonames", "type": "PPL" }, { "name": "Kaftan", "latitude": 9.72528, "longitude": 13.99635, "x": 65.9, "y": 41.8, "source": "geonames", "type": "PPL" }, { "name": "Kagouma", "latitude": 9.66743, "longitude": 13.92104, "x": 48.9, "y": 56.4, "source": "geonames", "type": "PPL" }, { "name": "Kailleran", "latitude": 9.66667, "longitude": 13.98333, "x": 63, "y": 56.5, "source": "geonames", "type": "PPL" }, { "name": "Kakala", "latitude": 9.66451, "longitude": 13.94605, "x": 54.6, "y": 57.1, "source": "geonames", "type": "PPL" }, { "name": "Kakou", "latitude": 9.62688, "longitude": 13.95997, "x": 57.7, "y": 66.5, "source": "geonames", "type": "PPL" }, { "name": "Kalao", "latitude": 9.58947, "longitude": 13.88576, "x": 40.9, "y": 75.9, "source": "geonames", "type": "PPL" }, { "name": "Kangay", "latitude": 9.56959, "longitude": 13.91397, "x": 47.3, "y": 80.9, "source": "geonames", "type": "PPL" }, { "name": "Kara", "latitude": 9.67437, "longitude": 13.94446, "x": 54.2, "y": 54.6, "source": "geonames", "type": "PPL" }, { "name": "Karewa", "latitude": 9.78452, "longitude": 13.97192, "x": 60.4, "y": 27, "source": "geonames", "type": "PPL" }, { "name": "Katcheo", "latitude": 9.63422, "longitude": 13.91913, "x": 48.5, "y": 64.7, "source": "geonames", "type": "PPL" }, { "name": "Keou", "latitude": 9.79686, "longitude": 13.75936, "x": 12.3, "y": 23.9, "source": "geonames", "type": "PPL" }, { "name": "Kering", "latitude": 9.80863, "longitude": 13.98935, "x": 64.4, "y": 20.9, "source": "geonames", "type": "PPL" }, { "name": "Koina", "latitude": 9.81776, "longitude": 13.9286, "x": 50.6, "y": 18.6, "source": "geonames", "type": "PPL" }, { "name": "Kolle", "latitude": 9.8022, "longitude": 13.97266, "x": 60.6, "y": 22.5, "source": "geonames", "type": "PPL" }, { "name": "Kona", "latitude": 9.80568, "longitude": 13.84726, "x": 32.2, "y": 21.7, "source": "geonames", "type": "PPL" }, { "name": "Kossi", "latitude": 9.59441, "longitude": 13.84223, "x": 31.1, "y": 74.7, "source": "geonames", "type": "PPL" }, { "name": "Lombel", "latitude": 9.72991, "longitude": 13.8389, "x": 30.3, "y": 40.7, "source": "geonames", "type": "PPL" }, { "name": "Mboudja", "latitude": 9.62118, "longitude": 13.96861, "x": 59.7, "y": 68, "source": "geonames", "type": "PPL" }, { "name": "Mirodafal", "latitude": 9.74752, "longitude": 13.88088, "x": 39.8, "y": 36.3, "source": "geonames", "type": "PPL" }, { "name": "Naimain", "latitude": 9.7, "longitude": 13.78333, "x": 17.7, "y": 48.2, "source": "geonames", "type": "PPL" }, { "name": "Ouro Babouba", "latitude": 9.7966, "longitude": 13.96486, "x": 58.8, "y": 23.9, "source": "geonames", "type": "PPL" }, { "name": "Ouro Bendo", "latitude": 9.77622, "longitude": 13.96945, "x": 59.9, "y": 29.1, "source": "geonames", "type": "PPL" }, { "name": "Ouro Deri", "latitude": 9.79301, "longitude": 13.78854, "x": 18.9, "y": 24.8, "source": "geonames", "type": "PPL" }, { "name": "Ouro Kereng", "latitude": 9.81667, "longitude": 14, "x": 66.8, "y": 18.9, "source": "geonames", "type": "PPL" }, { "name": "Ouro Maloum Sidiki", "latitude": 9.81591, "longitude": 13.96152, "x": 58.1, "y": 19.1, "source": "geonames", "type": "PPL" }, { "name": "Ouro-Maray", "latitude": 9.68163, "longitude": 13.95896, "x": 57.5, "y": 52.8, "source": "geonames", "type": "PPL" }, { "name": "Pelgue", "latitude": 9.7713, "longitude": 14.0162, "x": 70.4, "y": 30.3, "source": "geonames", "type": "PPL" }, { "name": "Piaga", "latitude": 9.60982, "longitude": 13.99333, "x": 65.3, "y": 70.8, "source": "geonames", "type": "PPL" }, { "name": "Poguere", "latitude": 9.7656, "longitude": 14.0513, "x": 78.4, "y": 31.7, "source": "geonames", "type": "PPL" }, { "name": "Ponila", "latitude": 9.78333, "longitude": 13.96667, "x": 59.2, "y": 27.3, "source": "geonames", "type": "PPL" }, { "name": "Ribaou", "latitude": 9.73359, "longitude": 13.92643, "x": 50.1, "y": 39.8, "source": "geonames", "type": "PPL" }, { "name": "Rocca", "latitude": 9.76667, "longitude": 13.96667, "x": 59.2, "y": 31.5, "source": "geonames", "type": "PPL" }, { "name": "Rompo", "latitude": 9.76493, "longitude": 13.94809, "x": 55, "y": 31.9, "source": "geonames", "type": "PPL" }, { "name": "Sakande", "latitude": 9.74508, "longitude": 13.84012, "x": 30.6, "y": 36.9, "source": "geonames", "type": "PPL" }, { "name": "Sebore-Baila", "latitude": 9.70862, "longitude": 13.87117, "x": 37.6, "y": 46, "source": "geonames", "type": "PPL" }, { "name": "Sodjoy", "latitude": 9.78349, "longitude": 13.80041, "x": 21.6, "y": 27.2, "source": "geonames", "type": "PPL" }, { "name": "Soloa", "latitude": 9.66472, "longitude": 13.96794, "x": 59.5, "y": 57, "source": "geonames", "type": "PPL" }, { "name": "Sona", "latitude": 9.82073, "longitude": 13.77148, "x": 15, "y": 17.9, "source": "geonames", "type": "PPL" }, { "name": "Sorawel", "latitude": 9.78435, "longitude": 13.86242, "x": 35.6, "y": 27, "source": "geonames", "type": "PPL" }, { "name": "Tchekal", "latitude": 9.65475, "longitude": 13.81296, "x": 24.4, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Tchontchi", "latitude": 9.69352, "longitude": 13.84044, "x": 30.7, "y": 49.8, "source": "geonames", "type": "PPL" }, { "name": "Teubang", "latitude": 9.7848, "longitude": 14.0744, "x": 83.6, "y": 26.9, "source": "geonames", "type": "PPL" }, { "name": "Tisi Taram", "latitude": 9.71533, "longitude": 13.99038, "x": 64.6, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Wafango-Pomla", "latitude": 9.7646, "longitude": 13.92327, "x": 49.4, "y": 32, "source": "geonames", "type": "PPL" }, { "name": "Yambere", "latitude": 9.73333, "longitude": 13.95, "x": 55.5, "y": 39.8, "source": "geonames", "type": "PPL" }, { "name": "Zabikoroum", "latitude": 9.7781, "longitude": 14.0398, "x": 75.8, "y": 28.6, "source": "geonames", "type": "PPL" } ], "Fontem": [ { "name": "Agong", "latitude": 5.6532, "longitude": 9.8978, "x": 69.2, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Aoua", "latitude": 5.4325, "longitude": 9.95188, "x": 82.6, "y": 56.6, "source": "geonames", "type": "PPL" }, { "name": "Atebong Wire", "latitude": 5.5645, "longitude": 9.7898, "x": 42.3, "y": 28.7, "source": "geonames", "type": "PPL" }, { "name": "Bangang", "latitude": 5.6061, "longitude": 9.8876, "x": 66.6, "y": 19.9, "source": "geonames", "type": "PPL" }, { "name": "Bara", "latitude": 5.53333, "longitude": 9.71667, "x": 24.2, "y": 35.3, "source": "geonames", "type": "PPL" }, { "name": "Beka", "latitude": 5.3643, "longitude": 9.703, "x": 20.8, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Bellua", "latitude": 5.482, "longitude": 9.8896, "x": 67.1, "y": 46.1, "source": "geonames", "type": "PPL" }, { "name": "Bengrum", "latitude": 5.3973, "longitude": 9.7362, "x": 29, "y": 64, "source": "geonames", "type": "PPL" }, { "name": "Besali", "latitude": 5.6337, "longitude": 9.9068, "x": 71.4, "y": 14.1, "source": "geonames", "type": "PPL" }, { "name": "Bessassem", "latitude": 5.3045, "longitude": 9.8625, "x": 60.4, "y": 83.6, "source": "geonames", "type": "PPL" }, { "name": "Chefferie de Fossong-Wentcheng", "latitude": 5.40458, "longitude": 9.93735, "x": 79, "y": 62.5, "source": "geonames", "type": "PPL" }, { "name": "Ebakong", "latitude": 5.2743, "longitude": 9.7906, "x": 42.5, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ebeagwa I", "latitude": 5.58, "longitude": 9.7272, "x": 26.8, "y": 25.5, "source": "geonames", "type": "PPL" }, { "name": "Ebeagwa II", "latitude": 5.5732, "longitude": 9.7324, "x": 28.1, "y": 26.9, "source": "geonames", "type": "PPL" }, { "name": "Ebensuk", "latitude": 5.5232, "longitude": 9.8067, "x": 46.5, "y": 37.4, "source": "geonames", "type": "PPL" }, { "name": "Edjuingang", "latitude": 5.5748, "longitude": 9.7572, "x": 34.2, "y": 26.6, "source": "geonames", "type": "PPL" }, { "name": "Ekpor", "latitude": 5.6277, "longitude": 9.7881, "x": 41.9, "y": 15.4, "source": "geonames", "type": "PPL" }, { "name": "Elumba", "latitude": 5.40472, "longitude": 9.76344, "x": 35.8, "y": 62.5, "source": "geonames", "type": "PPL" }, { "name": "Essinte", "latitude": 5.3861, "longitude": 9.9371, "x": 78.9, "y": 66.4, "source": "geonames", "type": "PPL" }, { "name": "Etawang", "latitude": 5.44164, "longitude": 9.75747, "x": 34.3, "y": 54.7, "source": "geonames", "type": "PPL" }, { "name": "Famila", "latitude": 5.4131, "longitude": 9.8503, "x": 57.4, "y": 60.7, "source": "geonames", "type": "PPL" }, { "name": "Fasimombin", "latitude": 5.6304, "longitude": 9.9817, "x": 90, "y": 14.8, "source": "geonames", "type": "PPL" }, { "name": "Fereke-Chacha", "latitude": 5.4084, "longitude": 9.8821, "x": 65.3, "y": 61.7, "source": "geonames", "type": "PPL" }, { "name": "Foba", "latitude": 5.6156, "longitude": 9.9638, "x": 85.6, "y": 17.9, "source": "geonames", "type": "PPL" }, { "name": "Fokwe", "latitude": 5.5449, "longitude": 9.9445, "x": 80.8, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Folembe", "latitude": 5.5368, "longitude": 9.8919, "x": 67.7, "y": 34.6, "source": "geonames", "type": "PPL" }, { "name": "Fomoa", "latitude": 5.45, "longitude": 9.9239, "x": 75.6, "y": 52.9, "source": "geonames", "type": "PPL" }, { "name": "Fongotafo", "latitude": 5.3553, "longitude": 9.9293, "x": 77, "y": 72.9, "source": "geonames", "type": "PPL" }, { "name": "Fonjungo-Fonke", "latitude": 5.3955, "longitude": 9.8954, "x": 68.6, "y": 64.4, "source": "geonames", "type": "PPL" }, { "name": "Fonke", "latitude": 5.3815, "longitude": 9.8749, "x": 63.5, "y": 67.4, "source": "geonames", "type": "PPL" }, { "name": "Fonwen", "latitude": 5.368, "longitude": 9.8211, "x": 50.1, "y": 70.2, "source": "geonames", "type": "PPL" }, { "name": "Fonwung", "latitude": 5.3747, "longitude": 9.9126, "x": 72.8, "y": 68.8, "source": "geonames", "type": "PPL" }, { "name": "Forte Fongui", "latitude": 5.401, "longitude": 9.8392, "x": 54.6, "y": 63.2, "source": "geonames", "type": "PPL" }, { "name": "Fosongwanchen", "latitude": 5.4935, "longitude": 9.7874, "x": 41.7, "y": 43.7, "source": "geonames", "type": "PPL" }, { "name": "Fossong Eleleng", "latitude": 5.56943, "longitude": 9.97214, "x": 87.6, "y": 27.7, "source": "geonames", "type": "PPL" }, { "name": "Fosungu Wango", "latitude": 5.5886, "longitude": 9.966, "x": 86.1, "y": 23.6, "source": "geonames", "type": "PPL" }, { "name": "Fotabong I", "latitude": 5.514, "longitude": 9.903, "x": 70.5, "y": 39.4, "source": "geonames", "type": "PPL" }, { "name": "Fotabong II", "latitude": 5.4727, "longitude": 9.845, "x": 56, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Fotabong III", "latitude": 5.3898, "longitude": 9.8296, "x": 52.2, "y": 65.6, "source": "geonames", "type": "PPL" }, { "name": "Fotabong Koa", "latitude": 5.3693, "longitude": 9.7655, "x": 36.3, "y": 69.9, "source": "geonames", "type": "PPL" }, { "name": "Foto Ndonchwet", "latitude": 5.52804, "longitude": 9.93647, "x": 78.8, "y": 36.4, "source": "geonames", "type": "PPL" }, { "name": "Fowando", "latitude": 5.5548, "longitude": 9.8794, "x": 64.6, "y": 30.8, "source": "geonames", "type": "PPL" }, { "name": "Hunyampe", "latitude": 5.3266, "longitude": 9.76535, "x": 36.3, "y": 79, "source": "geonames", "type": "PPL" }, { "name": "Jilap", "latitude": 5.5606, "longitude": 9.945, "x": 80.9, "y": 29.6, "source": "geonames", "type": "PPL" }, { "name": "Kamalumpe", "latitude": 5.3406, "longitude": 9.7016, "x": 20.4, "y": 76, "source": "geonames", "type": "PPL" }, { "name": "Lebock", "latitude": 5.3765, "longitude": 9.8566, "x": 58.9, "y": 68.4, "source": "geonames", "type": "PPL" }, { "name": "Mambo", "latitude": 5.5398, "longitude": 9.8158, "x": 48.8, "y": 33.9, "source": "geonames", "type": "PPL" }, { "name": "Mbanga Pungo", "latitude": 5.6241, "longitude": 9.8201, "x": 49.9, "y": 16.1, "source": "geonames", "type": "PPL" }, { "name": "Mbetta", "latitude": 5.3517, "longitude": 9.8417, "x": 55.2, "y": 73.7, "source": "geonames", "type": "PPL" }, { "name": "Mbo", "latitude": 5.33333, "longitude": 9.88333, "x": 65.6, "y": 77.5, "source": "geonames", "type": "PPL" }, { "name": "Mbokambo", "latitude": 5.2902, "longitude": 9.8062, "x": 46.4, "y": 86.6, "source": "geonames", "type": "PPL" }, { "name": "Mechimia", "latitude": 5.315, "longitude": 9.876, "x": 63.7, "y": 81.4, "source": "geonames", "type": "PPL" }, { "name": "Meket", "latitude": 5.36667, "longitude": 9.95, "x": 82.1, "y": 70.5, "source": "geonames", "type": "PPL" }, { "name": "Michukouem", "latitude": 5.3285, "longitude": 9.8629, "x": 60.5, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Mogomba", "latitude": 5.3382, "longitude": 9.9164, "x": 73.8, "y": 76.5, "source": "geonames", "type": "PPL" }, { "name": "Ndom", "latitude": 5.3431, "longitude": 9.7228, "x": 25.7, "y": 75.5, "source": "geonames", "type": "PPL" }, { "name": "Nfa", "latitude": 5.41508, "longitude": 9.93593, "x": 78.6, "y": 60.3, "source": "geonames", "type": "PPL" }, { "name": "Ngui", "latitude": 5.4595, "longitude": 9.9548, "x": 83.3, "y": 50.9, "source": "geonames", "type": "PPL" }, { "name": "Ngwatta", "latitude": 5.2988, "longitude": 9.911, "x": 72.4, "y": 84.8, "source": "geonames", "type": "PPL" }, { "name": "Njungo", "latitude": 5.374, "longitude": 9.8715, "x": 62.6, "y": 68.9, "source": "geonames", "type": "PPL" }, { "name": "Nkang", "latitude": 5.3003, "longitude": 9.7963, "x": 44, "y": 84.5, "source": "geonames", "type": "PPL" }, { "name": "Nkong", "latitude": 5.5794, "longitude": 9.902, "x": 70.2, "y": 25.6, "source": "geonames", "type": "PPL" }, { "name": "Nsoa", "latitude": 5.4301, "longitude": 9.7517, "x": 32.9, "y": 57.1, "source": "geonames", "type": "PPL" }, { "name": "Sabes", "latitude": 5.64894, "longitude": 9.87005, "x": 62.3, "y": 10.9, "source": "geonames", "type": "PPL" }, { "name": "Sandong-Kokobuma", "latitude": 5.4367, "longitude": 9.6738, "x": 13.5, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Sandong-Ndosi", "latitude": 5.46, "longitude": 9.6596, "x": 10, "y": 50.8, "source": "geonames", "type": "PPL" }, { "name": "Taiyor I", "latitude": 5.5976, "longitude": 9.7095, "x": 22.4, "y": 21.7, "source": "geonames", "type": "PPL" }, { "name": "Taiyor II", "latitude": 5.6107, "longitude": 9.7173, "x": 24.3, "y": 19, "source": "geonames", "type": "PPL" }, { "name": "Takwai", "latitude": 5.5495, "longitude": 9.8035, "x": 45.7, "y": 31.9, "source": "geonames", "type": "PPL" }, { "name": "Tali Chang", "latitude": 5.5861, "longitude": 9.6965, "x": 19.2, "y": 24.2, "source": "geonames", "type": "PPL" }, { "name": "Tangang", "latitude": 5.4092, "longitude": 9.7167, "x": 24.2, "y": 61.5, "source": "geonames", "type": "PPL" }, { "name": "Zennela", "latitude": 5.38333, "longitude": 9.95, "x": 82.1, "y": 67, "source": "geonames", "type": "PPL" } ], "Foumban": [ { "name": "Chimbeng", "latitude": 5.68685, "longitude": 10.97494, "x": 82.8, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Femloum", "latitude": 5.73531, "longitude": 10.87861, "x": 50.2, "y": 46, "source": "geonames", "type": "PPL" }, { "name": "Fontain", "latitude": 5.73333, "longitude": 10.91667, "x": 63.1, "y": 46.6, "source": "geonames", "type": "PPL" }, { "name": "Kenyam-Keyou", "latitude": 5.61486, "longitude": 10.82818, "x": 33.1, "y": 83.2, "source": "geonames", "type": "PPL" }, { "name": "Kwetmenka'", "latitude": 5.77148, "longitude": 10.97051, "x": 81.3, "y": 34.8, "source": "geonames", "type": "PPL" }, { "name": "Macharou", "latitude": 5.61411, "longitude": 10.93115, "x": 68, "y": 83.4, "source": "geonames", "type": "PPL" }, { "name": "Machoutfen", "latitude": 5.67086, "longitude": 10.92776, "x": 66.8, "y": 65.9, "source": "geonames", "type": "PPL" }, { "name": "Machu", "latitude": 5.73983, "longitude": 10.98044, "x": 84.7, "y": 44.6, "source": "geonames", "type": "PPL" }, { "name": "Mafomghet", "latitude": 5.81199, "longitude": 10.97511, "x": 82.9, "y": 22.3, "source": "geonames", "type": "PPL" }, { "name": "Magham", "latitude": 5.7536, "longitude": 10.99598, "x": 89.9, "y": 40.3, "source": "geonames", "type": "PPL" }, { "name": "Maghet", "latitude": 5.80143, "longitude": 10.97929, "x": 84.3, "y": 25.6, "source": "geonames", "type": "PPL" }, { "name": "Makeremba", "latitude": 5.73848, "longitude": 10.99104, "x": 88.2, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Makouo'ncha", "latitude": 5.72438, "longitude": 10.78625, "x": 18.9, "y": 49.4, "source": "geonames", "type": "PPL" }, { "name": "Mamakeka", "latitude": 5.78217, "longitude": 10.86672, "x": 46.1, "y": 31.5, "source": "geonames", "type": "PPL" }, { "name": "Mamatie", "latitude": 5.81933, "longitude": 10.95945, "x": 77.5, "y": 20.1, "source": "geonames", "type": "PPL" }, { "name": "Mamben", "latitude": 5.7105, "longitude": 10.89795, "x": 56.7, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Mamboe", "latitude": 5.68372, "longitude": 10.95357, "x": 75.6, "y": 61.9, "source": "geonames", "type": "PPL" }, { "name": "Mamfu", "latitude": 5.65201, "longitude": 10.88204, "x": 51.3, "y": 71.7, "source": "geonames", "type": "PPL" }, { "name": "Manchinmben", "latitude": 5.69397, "longitude": 10.88616, "x": 52.7, "y": 58.7, "source": "geonames", "type": "PPL" }, { "name": "Mandekane", "latitude": 5.77242, "longitude": 10.93137, "x": 68, "y": 34.5, "source": "geonames", "type": "PPL" }, { "name": "Manga", "latitude": 5.63413, "longitude": 10.92215, "x": 64.9, "y": 77.2, "source": "geonames", "type": "PPL" }, { "name": "Manganou", "latitude": 5.6478, "longitude": 10.84013, "x": 37.1, "y": 73, "source": "geonames", "type": "PPL" }, { "name": "Manka'", "latitude": 5.73399, "longitude": 10.90697, "x": 59.8, "y": 46.4, "source": "geonames", "type": "PPLX" }, { "name": "Mansen", "latitude": 5.78024, "longitude": 10.98164, "x": 85.1, "y": 32.1, "source": "geonames", "type": "PPL" }, { "name": "Mantama", "latitude": 5.81069, "longitude": 10.90187, "x": 58, "y": 22.7, "source": "geonames", "type": "PPL" }, { "name": "Manyitngwen", "latitude": 5.66701, "longitude": 10.84117, "x": 37.5, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Mapouo'che", "latitude": 5.76511, "longitude": 10.9877, "x": 87.1, "y": 36.8, "source": "geonames", "type": "PPL" }, { "name": "Marom", "latitude": 5.7217, "longitude": 10.95815, "x": 77.1, "y": 50.2, "source": "geonames", "type": "PPL" }, { "name": "Mawo", "latitude": 5.68095, "longitude": 10.82044, "x": 30.4, "y": 62.8, "source": "geonames", "type": "PPL" }, { "name": "Mbouo'nka", "latitude": 5.72545, "longitude": 10.76009, "x": 10, "y": 49, "source": "geonames", "type": "PPL" }, { "name": "Mbouongouom", "latitude": 5.77367, "longitude": 10.81183, "x": 27.5, "y": 34.1, "source": "geonames", "type": "PPL" }, { "name": "Mechouemfu", "latitude": 5.69733, "longitude": 10.81596, "x": 28.9, "y": 57.7, "source": "geonames", "type": "PPL" }, { "name": "Mefie", "latitude": 5.73113, "longitude": 10.96536, "x": 79.5, "y": 47.3, "source": "geonames", "type": "PPL" }, { "name": "Mekwene", "latitude": 5.76043, "longitude": 10.93982, "x": 70.9, "y": 38.2, "source": "geonames", "type": "PPL" }, { "name": "Mevoue", "latitude": 5.66728, "longitude": 10.82374, "x": 31.6, "y": 67, "source": "geonames", "type": "PPL" }, { "name": "Meyama", "latitude": 5.82244, "longitude": 10.97932, "x": 84.3, "y": 19.1, "source": "geonames", "type": "PPL" }, { "name": "Mfekom", "latitude": 5.82635, "longitude": 10.93184, "x": 68.2, "y": 17.9, "source": "geonames", "type": "PPL" }, { "name": "Mfelap", "latitude": 5.82435, "longitude": 10.90399, "x": 58.8, "y": 18.5, "source": "geonames", "type": "PPL" }, { "name": "Mfemetie", "latitude": 5.8154, "longitude": 10.95664, "x": 76.6, "y": 21.3, "source": "geonames", "type": "PPL" }, { "name": "Mfengou", "latitude": 5.82919, "longitude": 10.94024, "x": 71, "y": 17, "source": "geonames", "type": "PPL" }, { "name": "Mfenguen", "latitude": 5.80051, "longitude": 10.84802, "x": 39.8, "y": 25.9, "source": "geonames", "type": "PPL" }, { "name": "Mfensu", "latitude": 5.77824, "longitude": 10.96948, "x": 80.9, "y": 32.7, "source": "geonames", "type": "PPL" }, { "name": "Mfeten", "latitude": 5.72605, "longitude": 10.90889, "x": 60.4, "y": 48.8, "source": "geonames", "type": "PPLX" }, { "name": "Mfeyap", "latitude": 5.78754, "longitude": 10.81782, "x": 29.6, "y": 29.9, "source": "geonames", "type": "PPL" }, { "name": "Mfeyet", "latitude": 5.8443, "longitude": 10.88444, "x": 52.1, "y": 12.4, "source": "geonames", "type": "PPL" }, { "name": "Mfeyouom", "latitude": 5.72702, "longitude": 10.8931, "x": 55.1, "y": 48.5, "source": "geonames", "type": "PPLX" }, { "name": "Mfopet", "latitude": 5.80701, "longitude": 10.9334, "x": 68.7, "y": 23.9, "source": "geonames", "type": "PPL" }, { "name": "Mgbetmoufong", "latitude": 5.69275, "longitude": 10.78111, "x": 17.1, "y": 59.1, "source": "geonames", "type": "PPL" }, { "name": "Mokouono", "latitude": 5.75, "longitude": 10.93333, "x": 68.7, "y": 41.5, "source": "geonames", "type": "PPL" }, { "name": "Nchoutfen", "latitude": 5.65212, "longitude": 10.90311, "x": 58.5, "y": 71.7, "source": "geonames", "type": "PPL" }, { "name": "Nchoutmfa", "latitude": 5.64207, "longitude": 10.81864, "x": 29.8, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Ndoumbwet", "latitude": 5.63247, "longitude": 10.85339, "x": 41.6, "y": 77.7, "source": "geonames", "type": "PPL" }, { "name": "Ngala'", "latitude": 5.79166, "longitude": 10.87141, "x": 47.7, "y": 28.6, "source": "geonames", "type": "PPL" }, { "name": "Ngamanzem", "latitude": 5.71187, "longitude": 10.77135, "x": 13.8, "y": 53.2, "source": "geonames", "type": "PPL" }, { "name": "Ngbassa'", "latitude": 5.71647, "longitude": 10.96701, "x": 80.1, "y": 51.8, "source": "geonames", "type": "PPL" }, { "name": "Ngwanesso", "latitude": 5.85192, "longitude": 10.84223, "x": 37.8, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Njiboure", "latitude": 5.77705, "longitude": 10.80223, "x": 24.3, "y": 33.1, "source": "geonames", "type": "PPL" }, { "name": "Njichom", "latitude": 5.79515, "longitude": 10.94139, "x": 71.4, "y": 27.5, "source": "geonames", "type": "PPL" }, { "name": "Njidare", "latitude": 5.7301, "longitude": 10.94198, "x": 71.6, "y": 47.6, "source": "geonames", "type": "PPL" }, { "name": "Njifen", "latitude": 5.72218, "longitude": 10.97333, "x": 82.2, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Njigwassi", "latitude": 5.82252, "longitude": 10.80728, "x": 26, "y": 19.1, "source": "geonames", "type": "PPL" }, { "name": "Njiketnkie", "latitude": 5.75728, "longitude": 10.91265, "x": 61.7, "y": 39.2, "source": "geonames", "type": "PPL" }, { "name": "Njikompani", "latitude": 5.70044, "longitude": 10.76623, "x": 12.1, "y": 56.7, "source": "geonames", "type": "PPL" }, { "name": "Njikwet", "latitude": 5.70235, "longitude": 10.88619, "x": 52.7, "y": 56.2, "source": "geonames", "type": "PPL" }, { "name": "Njikwet I", "latitude": 5.79488, "longitude": 10.92209, "x": 64.9, "y": 27.6, "source": "geonames", "type": "PPL" }, { "name": "Njikwet II", "latitude": 5.77005, "longitude": 10.92622, "x": 66.3, "y": 35.3, "source": "geonames", "type": "PPL" }, { "name": "Njilam", "latitude": 5.78996, "longitude": 10.98148, "x": 85, "y": 29.1, "source": "geonames", "type": "PPL" }, { "name": "Njiloum II", "latitude": 5.80037, "longitude": 10.88745, "x": 53.2, "y": 25.9, "source": "geonames", "type": "PPL" }, { "name": "Njimbam", "latitude": 5.71764, "longitude": 10.90633, "x": 59.5, "y": 51.4, "source": "geonames", "type": "PPLX" }, { "name": "Njimiepwen", "latitude": 5.79756, "longitude": 10.9537, "x": 75.6, "y": 26.8, "source": "geonames", "type": "PPL" }, { "name": "Njimom", "latitude": 5.82983, "longitude": 10.95896, "x": 77.4, "y": 16.8, "source": "geonames", "type": "PPL" }, { "name": "Njimve", "latitude": 5.62556, "longitude": 10.89958, "x": 57.3, "y": 79.9, "source": "geonames", "type": "PPL" }, { "name": "Njindare", "latitude": 5.72897, "longitude": 10.91838, "x": 63.6, "y": 47.9, "source": "geonames", "type": "PPLX" }, { "name": "Njinka", "latitude": 5.7401, "longitude": 10.9017, "x": 58, "y": 44.5, "source": "geonames", "type": "PPL" }, { "name": "Njinkoup", "latitude": 5.64015, "longitude": 10.79856, "x": 23, "y": 75.4, "source": "geonames", "type": "PPL" }, { "name": "Njinkwenmenka'", "latitude": 5.7316, "longitude": 10.95355, "x": 75.5, "y": 47.1, "source": "geonames", "type": "PPL" }, { "name": "Njinsen", "latitude": 5.7828, "longitude": 10.93612, "x": 69.6, "y": 31.3, "source": "geonames", "type": "PPL" }, { "name": "Njinso", "latitude": 5.81459, "longitude": 10.99621, "x": 90, "y": 21.5, "source": "geonames", "type": "PPL" }, { "name": "Njintout", "latitude": 5.73727, "longitude": 10.88678, "x": 52.9, "y": 45.4, "source": "geonames", "type": "PPL" }, { "name": "Njipe'touen", "latitude": 5.69981, "longitude": 10.77569, "x": 15.3, "y": 56.9, "source": "geonames", "type": "PPL" }, { "name": "Njisse I", "latitude": 5.72658, "longitude": 10.89001, "x": 54, "y": 48.7, "source": "geonames", "type": "PPL" }, { "name": "Njisse II", "latitude": 5.70819, "longitude": 10.883, "x": 51.6, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "Njisse III", "latitude": 5.71577, "longitude": 10.87716, "x": 49.7, "y": 52, "source": "geonames", "type": "PPL" }, { "name": "Njitam", "latitude": 5.72835, "longitude": 10.77875, "x": 16.3, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Njitekwet", "latitude": 5.81335, "longitude": 10.96227, "x": 78.5, "y": 21.9, "source": "geonames", "type": "PPL" }, { "name": "Njitoukouop", "latitude": 5.77635, "longitude": 10.83316, "x": 34.8, "y": 33.3, "source": "geonames", "type": "PPL" }, { "name": "Njiyawa", "latitude": 5.77439, "longitude": 10.95654, "x": 76.6, "y": 33.9, "source": "geonames", "type": "PPL" }, { "name": "Njiyouom", "latitude": 5.63214, "longitude": 10.91436, "x": 62.3, "y": 77.8, "source": "geonames", "type": "PPL" }, { "name": "Nka'nyam", "latitude": 5.72775, "longitude": 10.76788, "x": 12.6, "y": 48.3, "source": "geonames", "type": "PPL" }, { "name": "Nkengou", "latitude": 5.77242, "longitude": 10.76953, "x": 13.2, "y": 34.5, "source": "geonames", "type": "PPL" }, { "name": "Nkouchou'nsen", "latitude": 5.68061, "longitude": 10.8787, "x": 50.2, "y": 62.9, "source": "geonames", "type": "PPL" }, { "name": "Nkoufa'pche", "latitude": 5.63041, "longitude": 10.86399, "x": 45.2, "y": 78.4, "source": "geonames", "type": "PPL" }, { "name": "Nkoufen", "latitude": 5.64337, "longitude": 10.90321, "x": 58.5, "y": 74.4, "source": "geonames", "type": "PPL" }, { "name": "Nkoufomloum", "latitude": 5.83355, "longitude": 10.81165, "x": 27.5, "y": 15.7, "source": "geonames", "type": "PPL" }, { "name": "Nkougoumbe", "latitude": 5.77256, "longitude": 10.90781, "x": 60, "y": 34.5, "source": "geonames", "type": "PPL" }, { "name": "Nkougwennji", "latitude": 5.78903, "longitude": 10.79942, "x": 23.3, "y": 29.4, "source": "geonames", "type": "PPL" }, { "name": "Nkoukwet", "latitude": 5.72665, "longitude": 10.956, "x": 76.4, "y": 48.7, "source": "geonames", "type": "PPL" }, { "name": "Nkoumekwene", "latitude": 5.74596, "longitude": 10.92713, "x": 66.6, "y": 42.7, "source": "geonames", "type": "PPL" }, { "name": "Nkounchimpa", "latitude": 5.59269, "longitude": 10.9006, "x": 57.6, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Nkoundem", "latitude": 5.64483, "longitude": 10.8879, "x": 53.3, "y": 73.9, "source": "geonames", "type": "PPL" }, { "name": "Nkounga", "latitude": 5.72967, "longitude": 10.88378, "x": 51.9, "y": 47.7, "source": "geonames", "type": "PPLX" }, { "name": "Nkounka'ndi", "latitude": 5.77526, "longitude": 10.78849, "x": 19.6, "y": 33.7, "source": "geonames", "type": "PPL" }, { "name": "Nkoupa Matapit", "latitude": 5.76893, "longitude": 10.79952, "x": 23.4, "y": 35.6, "source": "geonames", "type": "PPL" }, { "name": "Nkoupa Nganou", "latitude": 5.77983, "longitude": 10.8452, "x": 38.8, "y": 32.2, "source": "geonames", "type": "PPL" }, { "name": "Nkoupou'", "latitude": 5.8106, "longitude": 10.91844, "x": 63.7, "y": 22.8, "source": "geonames", "type": "PPL" }, { "name": "Nkoupouo'loum", "latitude": 5.67524, "longitude": 10.81015, "x": 27, "y": 64.5, "source": "geonames", "type": "PPL" }, { "name": "Nkoussam", "latitude": 5.82022, "longitude": 10.9291, "x": 67.3, "y": 19.8, "source": "geonames", "type": "PPL" }, { "name": "Nkoutaba", "latitude": 5.6854, "longitude": 10.81034, "x": 27, "y": 61.4, "source": "geonames", "type": "PPL" }, { "name": "Nkoutam", "latitude": 5.77847, "longitude": 10.92464, "x": 65.8, "y": 32.7, "source": "geonames", "type": "PPL" }, { "name": "Nkoutekwet", "latitude": 5.70981, "longitude": 10.78697, "x": 19.1, "y": 53.9, "source": "geonames", "type": "PPL" }, { "name": "Nkoutenke", "latitude": 5.65754, "longitude": 10.81709, "x": 29.3, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Nkoutentoum", "latitude": 5.74041, "longitude": 10.77721, "x": 15.8, "y": 44.4, "source": "geonames", "type": "PPL" }, { "name": "Nkoutie", "latitude": 5.63673, "longitude": 10.80485, "x": 25.2, "y": 76.4, "source": "geonames", "type": "PPL" }, { "name": "Nkoutoukouop", "latitude": 5.74047, "longitude": 10.76852, "x": 12.9, "y": 44.4, "source": "geonames", "type": "PPL" }, { "name": "Nkoutounga", "latitude": 5.72793, "longitude": 10.87448, "x": 48.8, "y": 48.3, "source": "geonames", "type": "PPLX" }, { "name": "Nsiepa", "latitude": 5.80579, "longitude": 10.97772, "x": 83.7, "y": 24.2, "source": "geonames", "type": "PPL" }, { "name": "Pondimoun", "latitude": 5.69381, "longitude": 10.81053, "x": 27.1, "y": 58.8, "source": "geonames", "type": "PPL" }, { "name": "Tamkene", "latitude": 5.77063, "longitude": 10.83125, "x": 34.1, "y": 35.1, "source": "geonames", "type": "PPL" }, { "name": "Vetvoue", "latitude": 5.69547, "longitude": 10.79889, "x": 23.1, "y": 58.3, "source": "geonames", "type": "PPL" }, { "name": "Yolo-Nkoundem", "latitude": 5.60922, "longitude": 10.87901, "x": 50.3, "y": 84.9, "source": "geonames", "type": "PPL" } ], "Foumbot": [ { "name": "Bafole", "latitude": 5.65, "longitude": 10.68333, "x": 58.7, "y": 15.6, "source": "geonames", "type": "PPL" }, { "name": "Bafoussam II", "latitude": 5.45, "longitude": 10.56667, "x": 24.6, "y": 65.7, "source": "geonames", "type": "PPL" }, { "name": "Baham III", "latitude": 5.61965, "longitude": 10.58774, "x": 30.7, "y": 23.2, "source": "geonames", "type": "PPL" }, { "name": "Bameka", "latitude": 5.63333, "longitude": 10.55, "x": 19.7, "y": 19.8, "source": "geonames", "type": "PPL" }, { "name": "Bamougoum II", "latitude": 5.46667, "longitude": 10.56667, "x": 24.6, "y": 61.6, "source": "geonames", "type": "PPL" }, { "name": "Bangou II", "latitude": 5.45509, "longitude": 10.56487, "x": 24, "y": 64.5, "source": "geonames", "type": "PPL" }, { "name": "Famghang", "latitude": 5.53115, "longitude": 10.53979, "x": 16.7, "y": 45.4, "source": "geonames", "type": "PPL" }, { "name": "Fossang", "latitude": 5.38365, "longitude": 10.65654, "x": 50.9, "y": 82.4, "source": "geonames", "type": "PPL" }, { "name": "Foto", "latitude": 5.50494, "longitude": 10.55992, "x": 22.6, "y": 52, "source": "geonames", "type": "PPL" }, { "name": "Fouyiela", "latitude": 5.51828, "longitude": 10.59632, "x": 33.3, "y": 48.6, "source": "geonames", "type": "PPL" }, { "name": "Kechuentim", "latitude": 5.61567, "longitude": 10.63566, "x": 44.8, "y": 24.2, "source": "geonames", "type": "PPL" }, { "name": "Kobikong", "latitude": 5.4761, "longitude": 10.53163, "x": 14.3, "y": 59.2, "source": "geonames", "type": "PPL" }, { "name": "Kompani", "latitude": 5.50228, "longitude": 10.61924, "x": 40, "y": 52.6, "source": "geonames", "type": "PPL" }, { "name": "Kou", "latitude": 5.54866, "longitude": 10.54603, "x": 18.5, "y": 41, "source": "geonames", "type": "PPL" }, { "name": "Kouop Njot", "latitude": 5.64987, "longitude": 10.60071, "x": 34.5, "y": 15.7, "source": "geonames", "type": "PPL" }, { "name": "Kouopkaare", "latitude": 5.51106, "longitude": 10.58019, "x": 28.5, "y": 50.4, "source": "geonames", "type": "PPL" }, { "name": "Kouoptame", "latitude": 5.6486, "longitude": 10.60787, "x": 36.6, "y": 16, "source": "geonames", "type": "PPL" }, { "name": "Kuetvu", "latitude": 5.46617, "longitude": 10.57126, "x": 25.9, "y": 61.7, "source": "geonames", "type": "PPL" }, { "name": "Lafog", "latitude": 5.52335, "longitude": 10.56351, "x": 23.6, "y": 47.4, "source": "geonames", "type": "PPL" }, { "name": "Larentame", "latitude": 5.56484, "longitude": 10.73265, "x": 73.2, "y": 37, "source": "geonames", "type": "PPL" }, { "name": "Loum", "latitude": 5.37089, "longitude": 10.57895, "x": 28.2, "y": 85.6, "source": "geonames", "type": "PPL" }, { "name": "Loumbout", "latitude": 5.56988, "longitude": 10.65596, "x": 50.7, "y": 35.7, "source": "geonames", "type": "PPL" }, { "name": "Maakouop", "latitude": 5.55368, "longitude": 10.64542, "x": 47.6, "y": 39.8, "source": "geonames", "type": "PPL" }, { "name": "Maatam", "latitude": 5.61647, "longitude": 10.73115, "x": 72.7, "y": 24, "source": "geonames", "type": "PPL" }, { "name": "Makam", "latitude": 5.63911, "longitude": 10.54173, "x": 17.3, "y": 18.4, "source": "geonames", "type": "PPL" }, { "name": "Mako", "latitude": 5.3659, "longitude": 10.66194, "x": 52.5, "y": 86.8, "source": "geonames", "type": "PPL" }, { "name": "Mambwot", "latitude": 5.6713, "longitude": 10.57272, "x": 26.3, "y": 10.3, "source": "geonames", "type": "PPL" }, { "name": "Manganja", "latitude": 5.64951, "longitude": 10.71118, "x": 66.9, "y": 15.8, "source": "geonames", "type": "PPL" }, { "name": "Mangoum", "latitude": 5.4807, "longitude": 10.58873, "x": 31, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Manja", "latitude": 5.55774, "longitude": 10.79005, "x": 90, "y": 38.7, "source": "geonames", "type": "PPL" }, { "name": "Manket", "latitude": 5.63629, "longitude": 10.52003, "x": 10.9, "y": 19.1, "source": "geonames", "type": "PPL" }, { "name": "Mankopmou", "latitude": 5.63765, "longitude": 10.61528, "x": 38.8, "y": 18.7, "source": "geonames", "type": "PPL" }, { "name": "Mansen", "latitude": 5.63564, "longitude": 10.69258, "x": 61.5, "y": 19.2, "source": "geonames", "type": "PPL" }, { "name": "Mapa're", "latitude": 5.62231, "longitude": 10.65661, "x": 50.9, "y": 22.6, "source": "geonames", "type": "PPL" }, { "name": "Mapou'oche", "latitude": 5.57213, "longitude": 10.69377, "x": 61.8, "y": 35.1, "source": "geonames", "type": "PPL" }, { "name": "Maripa", "latitude": 5.42954, "longitude": 10.76426, "x": 82.4, "y": 70.9, "source": "geonames", "type": "PPL" }, { "name": "Massang", "latitude": 5.40684, "longitude": 10.65374, "x": 50.1, "y": 76.5, "source": "geonames", "type": "PPL" }, { "name": "Massiessie", "latitude": 5.60052, "longitude": 10.64672, "x": 48, "y": 28, "source": "geonames", "type": "PPL" }, { "name": "Matie", "latitude": 5.37641, "longitude": 10.66034, "x": 52, "y": 84.2, "source": "geonames", "type": "PPL" }, { "name": "Mbamkouop", "latitude": 5.65516, "longitude": 10.61123, "x": 37.6, "y": 14.3, "source": "geonames", "type": "PPL" }, { "name": "Mbanjou", "latitude": 5.5316, "longitude": 10.62729, "x": 42.3, "y": 45.3, "source": "geonames", "type": "PPL" }, { "name": "Mbeyana", "latitude": 5.57855, "longitude": 10.62945, "x": 43, "y": 33.5, "source": "geonames", "type": "PPL" }, { "name": "Meleng", "latitude": 5.6557, "longitude": 10.58244, "x": 29.2, "y": 14.2, "source": "geonames", "type": "PPL" }, { "name": "Menkechoum", "latitude": 5.5397, "longitude": 10.63369, "x": 44.2, "y": 43.3, "source": "geonames", "type": "PPL" }, { "name": "Menoup", "latitude": 5.55562, "longitude": 10.53691, "x": 15.9, "y": 39.3, "source": "geonames", "type": "PPL" }, { "name": "Mfa'chekwet", "latitude": 5.58714, "longitude": 10.68503, "x": 59.2, "y": 31.4, "source": "geonames", "type": "PPL" }, { "name": "Mfechieya", "latitude": 5.50907, "longitude": 10.60338, "x": 35.3, "y": 50.9, "source": "geonames", "type": "PPL" }, { "name": "Mfeleng", "latitude": 5.39254, "longitude": 10.59283, "x": 32.2, "y": 80.1, "source": "geonames", "type": "PPL" }, { "name": "Mfessang", "latitude": 5.43921, "longitude": 10.64921, "x": 48.7, "y": 68.4, "source": "geonames", "type": "PPL" }, { "name": "Mfesset", "latitude": 5.50182, "longitude": 10.67163, "x": 55.3, "y": 52.8, "source": "geonames", "type": "PPL" }, { "name": "Mfewouon", "latitude": 5.48295, "longitude": 10.71544, "x": 68.1, "y": 57.5, "source": "geonames", "type": "PPL" }, { "name": "Mgbankoup", "latitude": 5.57704, "longitude": 10.67866, "x": 57.4, "y": 33.9, "source": "geonames", "type": "PPL" }, { "name": "Momo", "latitude": 5.44594, "longitude": 10.55202, "x": 20.3, "y": 66.8, "source": "geonames", "type": "PPL" }, { "name": "Moripa", "latitude": 5.60339, "longitude": 10.52166, "x": 11.4, "y": 27.3, "source": "geonames", "type": "PPL" }, { "name": "Mum", "latitude": 5.49689, "longitude": 10.77339, "x": 85.1, "y": 54, "source": "geonames", "type": "PPL" }, { "name": "Nchout-Lablab", "latitude": 5.61378, "longitude": 10.59877, "x": 34, "y": 24.7, "source": "geonames", "type": "PPL" }, { "name": "Nchout-Mambouo", "latitude": 5.63842, "longitude": 10.71701, "x": 68.6, "y": 18.5, "source": "geonames", "type": "PPL" }, { "name": "Nchout-Mou", "latitude": 5.61269, "longitude": 10.58799, "x": 30.8, "y": 25, "source": "geonames", "type": "PPL" }, { "name": "Nchoutkeke", "latitude": 5.60863, "longitude": 10.70562, "x": 65.3, "y": 26, "source": "geonames", "type": "PPL" }, { "name": "Ndachi", "latitude": 5.57326, "longitude": 10.51692, "x": 10, "y": 34.9, "source": "geonames", "type": "PPL" }, { "name": "Ndoube", "latitude": 5.50802, "longitude": 10.5375, "x": 16, "y": 51.2, "source": "geonames", "type": "PPL" }, { "name": "Ndoumben", "latitude": 5.5184, "longitude": 10.61324, "x": 38.2, "y": 48.6, "source": "geonames", "type": "PPL" }, { "name": "Ngantou", "latitude": 5.49238, "longitude": 10.63294, "x": 44, "y": 55.1, "source": "geonames", "type": "PPL" }, { "name": "Ngbetswen", "latitude": 5.63488, "longitude": 10.53191, "x": 14.4, "y": 19.4, "source": "geonames", "type": "PPL" }, { "name": "Ngoundoup", "latitude": 5.62047, "longitude": 10.73597, "x": 74.2, "y": 23, "source": "geonames", "type": "PPL" }, { "name": "Ngouondam I", "latitude": 5.61306, "longitude": 10.57273, "x": 26.3, "y": 24.9, "source": "geonames", "type": "PPL" }, { "name": "Ngouondam II", "latitude": 5.6182, "longitude": 10.56064, "x": 22.8, "y": 23.6, "source": "geonames", "type": "PPL" }, { "name": "Ngouondam III", "latitude": 5.6361, "longitude": 10.56488, "x": 24, "y": 19.1, "source": "geonames", "type": "PPL" }, { "name": "Nguemou", "latitude": 5.63636, "longitude": 10.60665, "x": 36.3, "y": 19, "source": "geonames", "type": "PPL" }, { "name": "Ngwepang", "latitude": 5.56455, "longitude": 10.52291, "x": 11.8, "y": 37, "source": "geonames", "type": "PPL" }, { "name": "Ngwetsieu", "latitude": 5.53252, "longitude": 10.68272, "x": 58.6, "y": 45.1, "source": "geonames", "type": "PPL" }, { "name": "Njemka'", "latitude": 5.5613, "longitude": 10.64287, "x": 46.9, "y": 37.9, "source": "geonames", "type": "PPL" }, { "name": "Njiloum", "latitude": 5.59175, "longitude": 10.61918, "x": 40, "y": 30.2, "source": "geonames", "type": "PPL" }, { "name": "Njimbouo", "latitude": 5.65776, "longitude": 10.71053, "x": 66.7, "y": 13.7, "source": "geonames", "type": "PPL" }, { "name": "Njimbouot I", "latitude": 5.50108, "longitude": 10.64103, "x": 46.4, "y": 52.9, "source": "geonames", "type": "PPL" }, { "name": "Njimbouot II", "latitude": 5.52361, "longitude": 10.64529, "x": 47.6, "y": 47.3, "source": "geonames", "type": "PPL" }, { "name": "Njincha", "latitude": 5.48247, "longitude": 10.61295, "x": 38.1, "y": 57.6, "source": "geonames", "type": "PPL" }, { "name": "Njindoun", "latitude": 5.59562, "longitude": 10.58902, "x": 31.1, "y": 29.3, "source": "geonames", "type": "PPL" }, { "name": "Njingbamou", "latitude": 5.6467, "longitude": 10.62339, "x": 41.2, "y": 16.5, "source": "geonames", "type": "PPL" }, { "name": "Njingwen", "latitude": 5.54096, "longitude": 10.75585, "x": 80, "y": 42.9, "source": "geonames", "type": "PPL" }, { "name": "Njinkwen", "latitude": 5.67248, "longitude": 10.61665, "x": 39.2, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Njiripa", "latitude": 5.56748, "longitude": 10.62678, "x": 42.2, "y": 36.3, "source": "geonames", "type": "PPL" }, { "name": "Njitande", "latitude": 5.53035, "longitude": 10.60317, "x": 35.3, "y": 45.6, "source": "geonames", "type": "PPL" }, { "name": "Njitekwet", "latitude": 5.61186, "longitude": 10.60611, "x": 36.1, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Njiyen", "latitude": 5.64395, "longitude": 10.69977, "x": 63.6, "y": 17.1, "source": "geonames", "type": "PPL" }, { "name": "Nkemawou", "latitude": 5.57477, "longitude": 10.61969, "x": 40.1, "y": 34.5, "source": "geonames", "type": "PPL" }, { "name": "Nketloum", "latitude": 5.51876, "longitude": 10.68273, "x": 58.6, "y": 48.5, "source": "geonames", "type": "PPL" }, { "name": "Nkoambeng", "latitude": 5.61303, "longitude": 10.62003, "x": 40.2, "y": 24.9, "source": "geonames", "type": "PPL" }, { "name": "Nkou'omboum", "latitude": 5.60507, "longitude": 10.59533, "x": 33, "y": 26.9, "source": "geonames", "type": "PPL" }, { "name": "Nkoufen", "latitude": 5.35314, "longitude": 10.6293, "x": 42.9, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Nkougham-Ndoumnken", "latitude": 5.65494, "longitude": 10.56095, "x": 22.9, "y": 14.4, "source": "geonames", "type": "PPL" }, { "name": "Nkoukpa", "latitude": 5.42466, "longitude": 10.54629, "x": 18.6, "y": 72.1, "source": "geonames", "type": "PPL" }, { "name": "Nkoukwenkeng", "latitude": 5.5417, "longitude": 10.77885, "x": 86.7, "y": 42.8, "source": "geonames", "type": "PPL" }, { "name": "Nkoumbabele", "latitude": 5.63945, "longitude": 10.62488, "x": 41.6, "y": 18.3, "source": "geonames", "type": "PPL" }, { "name": "Nkoun Nten", "latitude": 5.65007, "longitude": 10.55394, "x": 20.8, "y": 15.6, "source": "geonames", "type": "PPL" }, { "name": "Nkounja", "latitude": 5.63597, "longitude": 10.74364, "x": 76.4, "y": 19.1, "source": "geonames", "type": "PPL" }, { "name": "Nkouon Nke", "latitude": 5.62546, "longitude": 10.65678, "x": 51, "y": 21.8, "source": "geonames", "type": "PPL" }, { "name": "Nkouonja", "latitude": 5.53423, "longitude": 10.66418, "x": 53.1, "y": 44.6, "source": "geonames", "type": "PPL" }, { "name": "Nkoupa're", "latitude": 5.57565, "longitude": 10.64378, "x": 47.2, "y": 34.3, "source": "geonames", "type": "PPL" }, { "name": "Nkoutaba", "latitude": 5.63095, "longitude": 10.73613, "x": 74.2, "y": 20.4, "source": "geonames", "type": "PPL" }, { "name": "Nkoutekwet", "latitude": 5.5577, "longitude": 10.76058, "x": 81.4, "y": 38.8, "source": "geonames", "type": "PPL" }, { "name": "Nkoutou Landen", "latitude": 5.48159, "longitude": 10.78075, "x": 87.3, "y": 57.8, "source": "geonames", "type": "PPL" }, { "name": "Nkoutoungouen", "latitude": 5.47236, "longitude": 10.71837, "x": 69, "y": 60.1, "source": "geonames", "type": "PPL" }, { "name": "Nkoutoungwen", "latitude": 5.61435, "longitude": 10.69483, "x": 62.1, "y": 24.6, "source": "geonames", "type": "PPL" }, { "name": "Nkoutounkwen", "latitude": 5.66336, "longitude": 10.61219, "x": 37.9, "y": 12.3, "source": "geonames", "type": "PPL" }, { "name": "Nkwetkwet", "latitude": 5.54383, "longitude": 10.62216, "x": 40.8, "y": 42.2, "source": "geonames", "type": "PPL" }, { "name": "Ntamkouop", "latitude": 5.58469, "longitude": 10.63997, "x": 46, "y": 32, "source": "geonames", "type": "PPL" }, { "name": "Ntanyet", "latitude": 5.50329, "longitude": 10.68927, "x": 60.5, "y": 52.4, "source": "geonames", "type": "PPL" }, { "name": "Ntwopouo'che", "latitude": 5.62936, "longitude": 10.75245, "x": 79, "y": 20.8, "source": "geonames", "type": "PPL" }, { "name": "Pamfuetle", "latitude": 5.63919, "longitude": 10.70234, "x": 64.3, "y": 18.3, "source": "geonames", "type": "PPL" }, { "name": "Panke", "latitude": 5.59741, "longitude": 10.64202, "x": 46.6, "y": 28.8, "source": "geonames", "type": "PPL" }, { "name": "Panket", "latitude": 5.63015, "longitude": 10.52163, "x": 11.4, "y": 20.6, "source": "geonames", "type": "PPL" }, { "name": "Panzou", "latitude": 5.4147, "longitude": 10.60861, "x": 36.9, "y": 74.6, "source": "geonames", "type": "PPL" }, { "name": "Petghom", "latitude": 5.56332, "longitude": 10.66826, "x": 54.3, "y": 37.3, "source": "geonames", "type": "PPL" }, { "name": "Pou'oloum", "latitude": 5.53894, "longitude": 10.68755, "x": 60, "y": 43.5, "source": "geonames", "type": "PPL" }, { "name": "Pounjiem", "latitude": 5.58135, "longitude": 10.65882, "x": 51.6, "y": 32.8, "source": "geonames", "type": "PPL" }, { "name": "Sapsi", "latitude": 5.52354, "longitude": 10.53128, "x": 14.2, "y": 47.3, "source": "geonames", "type": "PPL" }, { "name": "Tchitchap II", "latitude": 5.54649, "longitude": 10.52892, "x": 13.5, "y": 41.6, "source": "geonames", "type": "PPL" }, { "name": "Toukwop", "latitude": 5.48876, "longitude": 10.6799, "x": 57.7, "y": 56, "source": "geonames", "type": "PPL" }, { "name": "Toulaanden", "latitude": 5.52628, "longitude": 10.78035, "x": 87.2, "y": 46.6, "source": "geonames", "type": "PPL" } ], "Fundong": [ { "name": "Ajung", "latitude": 6.31667, "longitude": 10.41667, "x": 90, "y": 71.5, "source": "geonames", "type": "PPL" }, { "name": "Bwabwa", "latitude": 6.43333, "longitude": 10.33333, "x": 56.7, "y": 28.5, "source": "geonames", "type": "PPL" }, { "name": "Fujua", "latitude": 6.28333, "longitude": 10.28333, "x": 36.7, "y": 83.8, "source": "geonames", "type": "PPL" }, { "name": "Kam", "latitude": 6.46667, "longitude": 10.28333, "x": 36.7, "y": 16.2, "source": "geonames", "type": "PPL" }, { "name": "Konene", "latitude": 6.41667, "longitude": 10.36667, "x": 70, "y": 34.6, "source": "geonames", "type": "PPL" }, { "name": "Mbulom", "latitude": 6.38333, "longitude": 10.21667, "x": 10, "y": 46.9, "source": "geonames", "type": "PPL" }, { "name": "Mejung", "latitude": 6.31667, "longitude": 10.31667, "x": 50, "y": 71.5, "source": "geonames", "type": "PPL" }, { "name": "Meli", "latitude": 6.26667, "longitude": 10.23333, "x": 16.7, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Mme-Bafumen", "latitude": 6.33333, "longitude": 10.23333, "x": 16.7, "y": 65.4, "source": "geonames", "type": "PPL" }, { "name": "Munji", "latitude": 6.48333, "longitude": 10.31667, "x": 50, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Nchang", "latitude": 6.35, "longitude": 10.4, "x": 83.3, "y": 59.2, "source": "geonames", "type": "PPL" }, { "name": "Nyos", "latitude": 6.43333, "longitude": 10.26667, "x": 30, "y": 28.5, "source": "geonames", "type": "PPL" }, { "name": "Nyos-Acha", "latitude": 6.45, "longitude": 10.26667, "x": 30, "y": 22.3, "source": "geonames", "type": "PPL" }, { "name": "Sowe", "latitude": 6.45, "longitude": 10.36667, "x": 70, "y": 22.3, "source": "geonames", "type": "PPL" }, { "name": "Tsoka", "latitude": 6.48333, "longitude": 10.35, "x": 63.3, "y": 10, "source": "geonames", "type": "PPL" } ], "Galim": [ { "name": "Amadjoda", "latitude": 6.94505, "longitude": 12.64411, "x": 84.6, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Djalingo", "latitude": 6.96557, "longitude": 12.62437, "x": 79.8, "y": 85.1, "source": "geonames", "type": "PPL" }, { "name": "Djaoro Sakanadje", "latitude": 6.98857, "longitude": 12.55664, "x": 63.6, "y": 79.7, "source": "geonames", "type": "PPL" }, { "name": "Doualayel", "latitude": 7.01667, "longitude": 12.66667, "x": 90, "y": 73.1, "source": "geonames", "type": "PPL" }, { "name": "Garba", "latitude": 6.96177, "longitude": 12.48537, "x": 46.5, "y": 86, "source": "geonames", "type": "PPL" }, { "name": "Garbaia", "latitude": 7.28333, "longitude": 12.48333, "x": 46, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Guassanguel", "latitude": 7.2, "longitude": 12.53333, "x": 58, "y": 29.7, "source": "geonames", "type": "PPL" }, { "name": "Lainde Goudda", "latitude": 6.99038, "longitude": 12.61489, "x": 77.6, "y": 79.3, "source": "geonames", "type": "PPL" }, { "name": "Lompta", "latitude": 7.05, "longitude": 12.53333, "x": 58, "y": 65.2, "source": "geonames", "type": "PPL" }, { "name": "Mayo Dankali", "latitude": 6.98867, "longitude": 12.36185, "x": 16.8, "y": 79.7, "source": "geonames", "type": "PPL" }, { "name": "Ngouri", "latitude": 7.23333, "longitude": 12.45, "x": 38, "y": 21.8, "source": "geonames", "type": "PPL" }, { "name": "Serki", "latitude": 7.00017, "longitude": 12.50114, "x": 50.3, "y": 77, "source": "geonames", "type": "PPL" }, { "name": "Wagamdou", "latitude": 7.25, "longitude": 12.33333, "x": 10, "y": 17.9, "source": "geonames", "type": "PPL" } ], "Garoua": [ { "name": "Badoudi", "latitude": 9.25794, "longitude": 13.40309, "x": 65.5, "y": 61, "source": "geonames", "type": "PPL" }, { "name": "Bangli-Lainde", "latitude": 9.25987, "longitude": 13.19768, "x": 13.6, "y": 60.5, "source": "geonames", "type": "PPL" }, { "name": "Bangli-Maloum", "latitude": 9.24701, "longitude": 13.20387, "x": 15.2, "y": 63.5, "source": "geonames", "type": "PPL" }, { "name": "Baroumi", "latitude": 9.19589, "longitude": 13.27005, "x": 31.9, "y": 75.4, "source": "geonames", "type": "PPL" }, { "name": "Bile", "latitude": 9.30379, "longitude": 13.34824, "x": 51.7, "y": 50.3, "source": "geonames", "type": "PPL" }, { "name": "Bockle", "latitude": 9.2941, "longitude": 13.43913, "x": 74.6, "y": 52.5, "source": "geonames", "type": "PPL" }, { "name": "Bokle", "latitude": 9.22166, "longitude": 13.40608, "x": 66.3, "y": 69.4, "source": "geonames", "type": "PPL" }, { "name": "Daledje", "latitude": 9.47145, "longitude": 13.25374, "x": 27.8, "y": 11.2, "source": "geonames", "type": "PPL" }, { "name": "Djalingo", "latitude": 9.22853, "longitude": 13.44414, "x": 75.9, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Djamboutou", "latitude": 9.21408, "longitude": 13.36996, "x": 57.1, "y": 71.2, "source": "geonames", "type": "PPL" }, { "name": "Djamboutou Manga", "latitude": 9.31906, "longitude": 13.35817, "x": 54.2, "y": 46.7, "source": "geonames", "type": "PPL" }, { "name": "Djamboutou Petel", "latitude": 9.3147, "longitude": 13.35048, "x": 52.2, "y": 47.7, "source": "geonames", "type": "PPL" }, { "name": "Djamtari", "latitude": 9.2742, "longitude": 13.28125, "x": 34.7, "y": 57.2, "source": "geonames", "type": "PPL" }, { "name": "Djim", "latitude": 9.20538, "longitude": 13.21869, "x": 18.9, "y": 73.2, "source": "geonames", "type": "PPL" }, { "name": "Djirladje", "latitude": 9.28752, "longitude": 13.33953, "x": 49.5, "y": 54.1, "source": "geonames", "type": "PPL" }, { "name": "Djola", "latitude": 9.21667, "longitude": 13.5, "x": 90, "y": 70.6, "source": "geonames", "type": "PPL" }, { "name": "Djoumassi", "latitude": 9.40256, "longitude": 13.37892, "x": 59.4, "y": 27.3, "source": "geonames", "type": "PPL" }, { "name": "Falire", "latitude": 9.37234, "longitude": 13.36621, "x": 56.2, "y": 34.3, "source": "geonames", "type": "PPL" }, { "name": "Garoua Vinde", "latitude": 9.26751, "longitude": 13.34274, "x": 50.3, "y": 58.7, "source": "geonames", "type": "PPL" }, { "name": "Gaschiga", "latitude": 9.42838, "longitude": 13.36627, "x": 56.2, "y": 21.3, "source": "geonames", "type": "PPL" }, { "name": "Goundjiki-Kolere", "latitude": 9.39055, "longitude": 13.22994, "x": 21.8, "y": 30.1, "source": "geonames", "type": "PPL" }, { "name": "Guibdjol", "latitude": 9.3291, "longitude": 13.19398, "x": 12.7, "y": 44.4, "source": "geonames", "type": "PPL" }, { "name": "Hossere Faourou", "latitude": 9.30424, "longitude": 13.3204, "x": 44.6, "y": 50.2, "source": "geonames", "type": "PPL" }, { "name": "Kalgue", "latitude": 9.13333, "longitude": 13.28333, "x": 35.3, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Kokoumi", "latitude": 9.20692, "longitude": 13.26338, "x": 30.2, "y": 72.9, "source": "geonames", "type": "PPL" }, { "name": "Lainde", "latitude": 9.34327, "longitude": 13.41916, "x": 69.6, "y": 41.1, "source": "geonames", "type": "PPL" }, { "name": "Lobi", "latitude": 9.31632, "longitude": 13.46964, "x": 82.3, "y": 47.4, "source": "geonames", "type": "PPL" }, { "name": "Lougahori", "latitude": 9.45406, "longitude": 13.258, "x": 28.9, "y": 15.3, "source": "geonames", "type": "PPL" }, { "name": "Mayo Babaguido", "latitude": 9.45426, "longitude": 13.31364, "x": 42.9, "y": 15.2, "source": "geonames", "type": "PPL" }, { "name": "Mayo Dadi", "latitude": 9.20427, "longitude": 13.40327, "x": 65.6, "y": 73.5, "source": "geonames", "type": "PPL" }, { "name": "Mayo Kila", "latitude": 9.46147, "longitude": 13.34127, "x": 49.9, "y": 13.6, "source": "geonames", "type": "PPL" }, { "name": "Mbere", "latitude": 9.24911, "longitude": 13.26259, "x": 30, "y": 63, "source": "geonames", "type": "PPL" }, { "name": "Nakong", "latitude": 9.30973, "longitude": 13.25516, "x": 28.1, "y": 48.9, "source": "geonames", "type": "PPL" }, { "name": "Nassarao", "latitude": 9.33333, "longitude": 13.18333, "x": 10, "y": 43.4, "source": "geonames", "type": "PPL" }, { "name": "Ngalbi", "latitude": 9.45068, "longitude": 13.34411, "x": 50.6, "y": 16.1, "source": "geonames", "type": "PPL" }, { "name": "Nibango-Kossoumo", "latitude": 9.27164, "longitude": 13.31666, "x": 43.7, "y": 57.8, "source": "geonames", "type": "PPL" }, { "name": "Ouro Aba", "latitude": 9.2624, "longitude": 13.3472, "x": 51.4, "y": 59.9, "source": "geonames", "type": "PPL" }, { "name": "Ouro Ardo Rey", "latitude": 9.25534, "longitude": 13.27485, "x": 33.1, "y": 61.6, "source": "geonames", "type": "PPL" }, { "name": "Ouro Belo", "latitude": 9.26633, "longitude": 13.42849, "x": 71.9, "y": 59, "source": "geonames", "type": "PPL" }, { "name": "Ouro Bobbo", "latitude": 9.34215, "longitude": 13.31235, "x": 42.6, "y": 41.4, "source": "geonames", "type": "PPL" }, { "name": "Ouro Gadji", "latitude": 9.35186, "longitude": 13.30684, "x": 41.2, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Ouro Galoko", "latitude": 9.33743, "longitude": 13.20523, "x": 15.5, "y": 42.5, "source": "geonames", "type": "PPL" }, { "name": "Ouro Gone", "latitude": 9.3344, "longitude": 13.32912, "x": 46.8, "y": 43.2, "source": "geonames", "type": "PPL" }, { "name": "Ouro Harissou", "latitude": 9.34225, "longitude": 13.31974, "x": 44.5, "y": 41.3, "source": "geonames", "type": "PPL" }, { "name": "Ouro Kessoum", "latitude": 9.34585, "longitude": 13.38511, "x": 61, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Ouro Labo", "latitude": 9.29753, "longitude": 13.34723, "x": 51.4, "y": 51.7, "source": "geonames", "type": "PPL" }, { "name": "Ouro Lainde", "latitude": 9.25, "longitude": 13.2, "x": 14.2, "y": 62.8, "source": "geonames", "type": "PPL" }, { "name": "Ouro Malangao", "latitude": 9.35749, "longitude": 13.36111, "x": 54.9, "y": 37.8, "source": "geonames", "type": "PPL" }, { "name": "Ouro Malou", "latitude": 9.35107, "longitude": 13.36037, "x": 54.7, "y": 39.3, "source": "geonames", "type": "PPL" }, { "name": "Ouro Mayami", "latitude": 9.32286, "longitude": 13.32457, "x": 45.7, "y": 45.8, "source": "geonames", "type": "PPL" }, { "name": "Ouro Taka", "latitude": 9.38325, "longitude": 13.34209, "x": 50.1, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Ouro Tchaka", "latitude": 9.26396, "longitude": 13.30941, "x": 41.9, "y": 59.6, "source": "geonames", "type": "PPL" }, { "name": "Pomla Mango", "latitude": 9.39895, "longitude": 13.3479, "x": 51.6, "y": 28.1, "source": "geonames", "type": "PPL" }, { "name": "Pomla Petel", "latitude": 9.37065, "longitude": 13.34017, "x": 49.6, "y": 34.7, "source": "geonames", "type": "PPL" }, { "name": "Poumpoumre", "latitude": 9.33288, "longitude": 13.41428, "x": 68.3, "y": 43.5, "source": "geonames", "type": "PPL" }, { "name": "Roumde Lamido", "latitude": 9.32457, "longitude": 13.37117, "x": 57.5, "y": 45.5, "source": "geonames", "type": "PPL" }, { "name": "Saint Paul", "latitude": 9.27261, "longitude": 13.45972, "x": 79.8, "y": 57.6, "source": "geonames", "type": "PPL" }, { "name": "Sanguere", "latitude": 9.26667, "longitude": 13.46667, "x": 81.6, "y": 58.9, "source": "geonames", "type": "PPL" }, { "name": "Sanguere-Carrefour", "latitude": 9.24598, "longitude": 13.46403, "x": 80.9, "y": 63.8, "source": "geonames", "type": "PPL" }, { "name": "Sanguere-Ndjoi", "latitude": 9.22261, "longitude": 13.47885, "x": 84.7, "y": 69.2, "source": "geonames", "type": "PPL" }, { "name": "Sanguere-Ngal", "latitude": 9.20871, "longitude": 13.4944, "x": 88.6, "y": 72.4, "source": "geonames", "type": "PPL" }, { "name": "Sanguere-Paul", "latitude": 9.28164, "longitude": 13.45958, "x": 79.8, "y": 55.5, "source": "geonames", "type": "PPL" }, { "name": "Sanguere-Sara", "latitude": 9.21398, "longitude": 13.4899, "x": 87.4, "y": 71.2, "source": "geonames", "type": "PPL" }, { "name": "Souki", "latitude": 9.28307, "longitude": 13.34557, "x": 51, "y": 55.1, "source": "geonames", "type": "PPL" }, { "name": "Takaskou", "latitude": 9.35788, "longitude": 13.41785, "x": 69.2, "y": 37.7, "source": "geonames", "type": "PPL" }, { "name": "Tapare", "latitude": 9.47675, "longitude": 13.32035, "x": 44.6, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Tinguelin", "latitude": 9.37187, "longitude": 13.40455, "x": 65.9, "y": 34.4, "source": "geonames", "type": "PPL" }, { "name": "Tondire", "latitude": 9.23506, "longitude": 13.39063, "x": 62.4, "y": 66.3, "source": "geonames", "type": "PPL" }, { "name": "Tongo", "latitude": 9.35019, "longitude": 13.43949, "x": 74.7, "y": 39.5, "source": "geonames", "type": "PPL" } ], "Garoua-Boulai": [ { "name": "Babio", "latitude": 5.78333, "longitude": 14.55, "x": 82, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Badan", "latitude": 5.78333, "longitude": 14.5, "x": 58, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Badzere", "latitude": 5.75, "longitude": 14.43333, "x": 26, "y": 82, "source": "geonames", "type": "PPL" }, { "name": "Bakoissie", "latitude": 5.98333, "longitude": 14.43333, "x": 26, "y": 26, "source": "geonames", "type": "PPL" }, { "name": "Boutila", "latitude": 5.71667, "longitude": 14.45, "x": 34, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Saubang", "latitude": 5.91667, "longitude": 14.46667, "x": 42, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Tapare", "latitude": 6.05, "longitude": 14.4, "x": 10, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Yokosire", "latitude": 5.81667, "longitude": 14.56667, "x": 90, "y": 66, "source": "geonames", "type": "PPL" } ], "Guidiguis": [ { "name": "Babamari", "latitude": 10.22709, "longitude": 14.783, "x": 53.3, "y": 33.5, "source": "geonames", "type": "PPL" }, { "name": "Balani", "latitude": 10.28864, "longitude": 14.82838, "x": 64.7, "y": 20.8, "source": "geonames", "type": "PPL" }, { "name": "Bardouki", "latitude": 9.9713, "longitude": 14.8105, "x": 60.2, "y": 85.9, "source": "geonames", "type": "PPL" }, { "name": "Barlang", "latitude": 10.05986, "longitude": 14.85232, "x": 70.6, "y": 67.7, "source": "geonames", "type": "PPL" }, { "name": "Belel Kakou", "latitude": 10.00965, "longitude": 14.66783, "x": 24.5, "y": 78, "source": "geonames", "type": "PPL" }, { "name": "Bisseo", "latitude": 10.16161, "longitude": 14.84524, "x": 68.9, "y": 46.9, "source": "geonames", "type": "PPL" }, { "name": "Bissoue", "latitude": 9.9901, "longitude": 14.7909, "x": 55.3, "y": 82, "source": "geonames", "type": "PPL" }, { "name": "Bitchare", "latitude": 10.13446, "longitude": 14.77684, "x": 51.8, "y": 52.4, "source": "geonames", "type": "PPL" }, { "name": "Bogo", "latitude": 10.05344, "longitude": 14.82271, "x": 63.2, "y": 69, "source": "geonames", "type": "PPL" }, { "name": "Bouzou", "latitude": 10.21814, "longitude": 14.62384, "x": 13.5, "y": 35.3, "source": "geonames", "type": "PPL" }, { "name": "Dadjamka", "latitude": 10.02098, "longitude": 14.88314, "x": 78.3, "y": 75.7, "source": "geonames", "type": "PPL" }, { "name": "Dambay", "latitude": 10.3416, "longitude": 14.76328, "x": 48.4, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Dana", "latitude": 10.1681, "longitude": 14.89393, "x": 81, "y": 45.5, "source": "geonames", "type": "PPL" }, { "name": "Dang Houni", "latitude": 10.17687, "longitude": 14.79286, "x": 55.8, "y": 43.7, "source": "geonames", "type": "PPL" }, { "name": "Danhou", "latitude": 9.9775, "longitude": 14.7826, "x": 53.2, "y": 84.6, "source": "geonames", "type": "PPL" }, { "name": "Dargala", "latitude": 10.19436, "longitude": 14.86772, "x": 74.5, "y": 40.2, "source": "geonames", "type": "PPL" }, { "name": "Datcheka-Takreo", "latitude": 10.14054, "longitude": 14.89513, "x": 81.3, "y": 51.2, "source": "geonames", "type": "PPL" }, { "name": "Dawarika", "latitude": 10.088, "longitude": 14.7885, "x": 54.7, "y": 62, "source": "geonames", "type": "PPL" }, { "name": "Defin", "latitude": 10.09967, "longitude": 14.83164, "x": 65.5, "y": 59.6, "source": "geonames", "type": "PPL" }, { "name": "Djakjin", "latitude": 10.05341, "longitude": 14.6676, "x": 24.5, "y": 69, "source": "geonames", "type": "PPL" }, { "name": "Djaolani", "latitude": 10.04226, "longitude": 14.90324, "x": 83.4, "y": 71.3, "source": "geonames", "type": "PPL" }, { "name": "Djarninga", "latitude": 10.16865, "longitude": 14.80437, "x": 58.7, "y": 45.4, "source": "geonames", "type": "PPL" }, { "name": "Djoalani", "latitude": 10.29485, "longitude": 14.77807, "x": 52.1, "y": 19.6, "source": "geonames", "type": "PPL" }, { "name": "Domba", "latitude": 10.04028, "longitude": 14.76378, "x": 48.5, "y": 71.7, "source": "geonames", "type": "PPL" }, { "name": "Donrosse", "latitude": 10.08102, "longitude": 14.88793, "x": 79.5, "y": 63.4, "source": "geonames", "type": "PPL" }, { "name": "Doubane", "latitude": 10.14117, "longitude": 14.75771, "x": 47, "y": 51.1, "source": "geonames", "type": "PPL" }, { "name": "Doudoula", "latitude": 10.00214, "longitude": 14.88506, "x": 78.8, "y": 79.5, "source": "geonames", "type": "PPL" }, { "name": "Doumrou", "latitude": 10.09132, "longitude": 14.8741, "x": 76.1, "y": 61.3, "source": "geonames", "type": "PPL" }, { "name": "Dziguilao", "latitude": 10.01614, "longitude": 14.79182, "x": 55.5, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Fadere", "latitude": 10.19523, "longitude": 14.81925, "x": 62.4, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Fago", "latitude": 10.16989, "longitude": 14.87197, "x": 75.5, "y": 45.2, "source": "geonames", "type": "PPL" }, { "name": "Gadjia", "latitude": 10.34035, "longitude": 14.6969, "x": 31.8, "y": 10.3, "source": "geonames", "type": "PPL" }, { "name": "Gamloum", "latitude": 10.17327, "longitude": 14.92979, "x": 90, "y": 44.5, "source": "geonames", "type": "PPL" }, { "name": "Garmas", "latitude": 10.06239, "longitude": 14.6097, "x": 10, "y": 67.2, "source": "geonames", "type": "PPL" }, { "name": "Garoua", "latitude": 10.2281, "longitude": 14.81743, "x": 61.9, "y": 33.3, "source": "geonames", "type": "PPL" }, { "name": "Gayodje", "latitude": 10.29375, "longitude": 14.68886, "x": 29.8, "y": 19.8, "source": "geonames", "type": "PPL" }, { "name": "Gazawa Bizili", "latitude": 10.21286, "longitude": 14.85356, "x": 70.9, "y": 36.4, "source": "geonames", "type": "PPL" }, { "name": "Gazouel", "latitude": 10.26015, "longitude": 14.86235, "x": 73.1, "y": 26.7, "source": "geonames", "type": "PPL" }, { "name": "Goh", "latitude": 10.13159, "longitude": 14.83917, "x": 67.4, "y": 53, "source": "geonames", "type": "PPL" }, { "name": "Golombe", "latitude": 10.11123, "longitude": 14.80955, "x": 59.9, "y": 57.2, "source": "geonames", "type": "PPL" }, { "name": "Golondakri", "latitude": 10.12485, "longitude": 14.88189, "x": 78, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "Golonghini", "latitude": 9.9511, "longitude": 14.7817, "x": 53, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Golongreon", "latitude": 10.24737, "longitude": 14.79259, "x": 55.7, "y": 29.3, "source": "geonames", "type": "PPL" }, { "name": "Gordjo", "latitude": 10.20417, "longitude": 14.83176, "x": 65.5, "y": 38.2, "source": "geonames", "type": "PPL" }, { "name": "Goulong Agou", "latitude": 10.26398, "longitude": 14.74511, "x": 43.8, "y": 25.9, "source": "geonames", "type": "PPL" }, { "name": "Goundey", "latitude": 10.02735, "longitude": 14.69969, "x": 32.5, "y": 74.4, "source": "geonames", "type": "PPL" }, { "name": "Goundey Mapore", "latitude": 10.03397, "longitude": 14.66126, "x": 22.9, "y": 73, "source": "geonames", "type": "PPL" }, { "name": "Goussouloum", "latitude": 10.24216, "longitude": 14.84802, "x": 69.6, "y": 30.4, "source": "geonames", "type": "PPL" }, { "name": "Guegueleke", "latitude": 10.25211, "longitude": 14.82628, "x": 64.1, "y": 28.3, "source": "geonames", "type": "PPL" }, { "name": "Guirling", "latitude": 10.27878, "longitude": 14.81067, "x": 60.2, "y": 22.9, "source": "geonames", "type": "PPL" }, { "name": "Horlong", "latitude": 10.21878, "longitude": 14.6506, "x": 20.2, "y": 35.2, "source": "geonames", "type": "PPL" }, { "name": "Houwang", "latitude": 9.9605, "longitude": 14.7431, "x": 43.3, "y": 88.1, "source": "geonames", "type": "PPL" }, { "name": "Kabla", "latitude": 10.27987, "longitude": 14.85368, "x": 71, "y": 22.6, "source": "geonames", "type": "PPL" }, { "name": "Kambrake", "latitude": 10.21611, "longitude": 14.91948, "x": 87.4, "y": 35.7, "source": "geonames", "type": "PPL" }, { "name": "Keda", "latitude": 10.0878, "longitude": 14.92095, "x": 87.8, "y": 62, "source": "geonames", "type": "PPL" }, { "name": "Kerbale", "latitude": 10.32708, "longitude": 14.81478, "x": 61.3, "y": 13, "source": "geonames", "type": "PPL" }, { "name": "Kobro", "latitude": 10.11693, "longitude": 14.82656, "x": 64.2, "y": 56, "source": "geonames", "type": "PPL" }, { "name": "Kodogui", "latitude": 10.17733, "longitude": 14.7315, "x": 40.4, "y": 43.7, "source": "geonames", "type": "PPL" }, { "name": "Kofide", "latitude": 10.2155, "longitude": 14.77725, "x": 51.9, "y": 35.8, "source": "geonames", "type": "PPL" }, { "name": "Kolara", "latitude": 10.26724, "longitude": 14.64702, "x": 19.3, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Kourbi", "latitude": 10.13651, "longitude": 14.63473, "x": 16.3, "y": 52, "source": "geonames", "type": "PPL" }, { "name": "Lawan Gaori", "latitude": 10.21924, "longitude": 14.82574, "x": 64, "y": 35.1, "source": "geonames", "type": "PPL" }, { "name": "Lawang", "latitude": 10.28754, "longitude": 14.86938, "x": 74.9, "y": 21.1, "source": "geonames", "type": "PPL" }, { "name": "Lay", "latitude": 10.27138, "longitude": 14.86728, "x": 74.4, "y": 24.4, "source": "geonames", "type": "PPL" }, { "name": "Louga Mbouli", "latitude": 10.19112, "longitude": 14.62083, "x": 12.8, "y": 40.8, "source": "geonames", "type": "PPL" }, { "name": "Louguere", "latitude": 10.09781, "longitude": 14.67874, "x": 27.3, "y": 59.9, "source": "geonames", "type": "PPL" }, { "name": "Mandaigoum", "latitude": 10.07438, "longitude": 14.76275, "x": 48.3, "y": 64.7, "source": "geonames", "type": "PPL" }, { "name": "Mandara", "latitude": 10.28621, "longitude": 14.66628, "x": 24.1, "y": 21.3, "source": "geonames", "type": "PPL" }, { "name": "Mapore", "latitude": 10.04221, "longitude": 14.68348, "x": 28.4, "y": 71.3, "source": "geonames", "type": "PPL" }, { "name": "Massinkou", "latitude": 10.17103, "longitude": 14.61937, "x": 12.4, "y": 44.9, "source": "geonames", "type": "PPL" }, { "name": "Mayel Badji", "latitude": 10.30151, "longitude": 14.73908, "x": 42.3, "y": 18.2, "source": "geonames", "type": "PPL" }, { "name": "Mayo Houdo", "latitude": 10.22974, "longitude": 14.69169, "x": 30.5, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Mbiinore", "latitude": 10.23834, "longitude": 14.88563, "x": 79, "y": 31.2, "source": "geonames", "type": "PPL" }, { "name": "Mbrodong", "latitude": 9.9622, "longitude": 14.7002, "x": 32.6, "y": 87.7, "source": "geonames", "type": "PPL" }, { "name": "Mogom", "latitude": 10.06333, "longitude": 14.92117, "x": 87.8, "y": 67, "source": "geonames", "type": "PPL" }, { "name": "Ndakla", "latitude": 10.12266, "longitude": 14.76722, "x": 49.4, "y": 54.9, "source": "geonames", "type": "PPL" }, { "name": "Ndere", "latitude": 10.14969, "longitude": 14.84993, "x": 70, "y": 49.3, "source": "geonames", "type": "PPL" }, { "name": "Ndoumga", "latitude": 10.31466, "longitude": 14.80693, "x": 59.3, "y": 15.5, "source": "geonames", "type": "PPL" }, { "name": "Nem Bakri", "latitude": 10.22122, "longitude": 14.89665, "x": 81.7, "y": 34.7, "source": "geonames", "type": "PPL" }, { "name": "Ngoro", "latitude": 10.06695, "longitude": 14.83439, "x": 66.2, "y": 66.3, "source": "geonames", "type": "PPL" }, { "name": "Ourare", "latitude": 10.17833, "longitude": 14.75789, "x": 47, "y": 43.4, "source": "geonames", "type": "PPL" }, { "name": "Ouro Largo", "latitude": 10.07942, "longitude": 14.80132, "x": 57.9, "y": 63.7, "source": "geonames", "type": "PPL" }, { "name": "Padala", "latitude": 10.20564, "longitude": 14.69726, "x": 31.9, "y": 37.9, "source": "geonames", "type": "PPL" }, { "name": "Pade", "latitude": 10.02998, "longitude": 14.80383, "x": 58.5, "y": 73.8, "source": "geonames", "type": "PPL" }, { "name": "Patalao", "latitude": 10.13973, "longitude": 14.81224, "x": 60.6, "y": 51.4, "source": "geonames", "type": "PPL" }, { "name": "Pitchotonguel", "latitude": 10.1014, "longitude": 14.71179, "x": 35.5, "y": 59.2, "source": "geonames", "type": "PPL" }, { "name": "Sabodjiga", "latitude": 10.22746, "longitude": 14.66384, "x": 23.5, "y": 33.4, "source": "geonames", "type": "PPL" }, { "name": "Sak Diele", "latitude": 10.16667, "longitude": 14.78333, "x": 53.4, "y": 45.8, "source": "geonames", "type": "PPL" }, { "name": "Salmay", "latitude": 10.1881, "longitude": 14.76629, "x": 49.1, "y": 41.4, "source": "geonames", "type": "PPL" }, { "name": "Saotsay", "latitude": 10.1505, "longitude": 14.81511, "x": 61.3, "y": 49.1, "source": "geonames", "type": "PPL" }, { "name": "Saouringwa", "latitude": 10.03463, "longitude": 14.8736, "x": 76, "y": 72.9, "source": "geonames", "type": "PPL" }, { "name": "Sarman", "latitude": 10.26572, "longitude": 14.69215, "x": 30.6, "y": 25.5, "source": "geonames", "type": "PPL" }, { "name": "Sikeore", "latitude": 10.20153, "longitude": 14.79807, "x": 57.1, "y": 38.7, "source": "geonames", "type": "PPL" }, { "name": "Sossikreo", "latitude": 10.11328, "longitude": 14.66231, "x": 23.1, "y": 56.8, "source": "geonames", "type": "PPL" }, { "name": "Soulkandou", "latitude": 10.24773, "longitude": 14.73926, "x": 42.4, "y": 29.2, "source": "geonames", "type": "PPL" }, { "name": "Tchitcheo", "latitude": 10.31201, "longitude": 14.845, "x": 68.8, "y": 16.1, "source": "geonames", "type": "PPL" }, { "name": "Tchoffi", "latitude": 10.19769, "longitude": 14.7294, "x": 39.9, "y": 39.5, "source": "geonames", "type": "PPL" }, { "name": "Tiliga", "latitude": 10.07193, "longitude": 14.90604, "x": 84.1, "y": 65.2, "source": "geonames", "type": "PPL" }, { "name": "Titima", "latitude": 10.16667, "longitude": 14.81667, "x": 61.7, "y": 45.8, "source": "geonames", "type": "PPL" }, { "name": "Touloum", "latitude": 10.17971, "longitude": 14.8355, "x": 66.4, "y": 43.2, "source": "geonames", "type": "PPL" }, { "name": "Waharou", "latitude": 10.28745, "longitude": 14.70785, "x": 34.5, "y": 21.1, "source": "geonames", "type": "PPL" }, { "name": "Waila", "latitude": 10.15194, "longitude": 14.73251, "x": 40.7, "y": 48.9, "source": "geonames", "type": "PPL" }, { "name": "Werfao", "latitude": 9.9747, "longitude": 14.8472, "x": 69.4, "y": 85.2, "source": "geonames", "type": "PPL" }, { "name": "Wiribaki", "latitude": 10.03976, "longitude": 14.84601, "x": 69.1, "y": 71.8, "source": "geonames", "type": "PPL" }, { "name": "Yoldeo", "latitude": 10.27275, "longitude": 14.70155, "x": 33, "y": 24.1, "source": "geonames", "type": "PPL" }, { "name": "Zouey", "latitude": 10.02666, "longitude": 14.74925, "x": 44.9, "y": 74.5, "source": "geonames", "type": "PPL" } ], "Guider": [ { "name": "Badya", "latitude": 10.09874, "longitude": 14.0704, "x": 77.5, "y": 19.8, "source": "geonames", "type": "PPL" }, { "name": "Bainga", "latitude": 9.84929, "longitude": 13.91114, "x": 44, "y": 85.3, "source": "geonames", "type": "PPL" }, { "name": "Bama", "latitude": 10.1253, "longitude": 14.0268, "x": 68.3, "y": 12.9, "source": "geonames", "type": "PPL" }, { "name": "Banbadja", "latitude": 10.05292, "longitude": 14.02805, "x": 68.6, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Bang", "latitude": 9.87083, "longitude": 13.98248, "x": 59, "y": 79.6, "source": "geonames", "type": "PPL" }, { "name": "Bedeve", "latitude": 10.10593, "longitude": 13.98228, "x": 59, "y": 17.9, "source": "geonames", "type": "PPL" }, { "name": "Bidzar", "latitude": 9.9015, "longitude": 14.1195, "x": 87.8, "y": 71.6, "source": "geonames", "type": "PPL" }, { "name": "Biou", "latitude": 9.8705, "longitude": 14.1036, "x": 84.5, "y": 79.7, "source": "geonames", "type": "PPL" }, { "name": "Birhingue", "latitude": 10.03186, "longitude": 14.1298, "x": 90, "y": 37.4, "source": "geonames", "type": "PPL" }, { "name": "Bodong", "latitude": 10.02974, "longitude": 13.82601, "x": 26.1, "y": 37.9, "source": "geonames", "type": "PPL" }, { "name": "Bontazi-Kola", "latitude": 9.8646, "longitude": 13.96183, "x": 54.7, "y": 81.3, "source": "geonames", "type": "PPL" }, { "name": "Boulou", "latitude": 10.06917, "longitude": 14.0284, "x": 68.7, "y": 27.6, "source": "geonames", "type": "PPL" }, { "name": "Bourra", "latitude": 9.85, "longitude": 13.76667, "x": 13.7, "y": 85.1, "source": "geonames", "type": "PPL" }, { "name": "Bourwoy", "latitude": 10.0875, "longitude": 13.86647, "x": 34.6, "y": 22.8, "source": "geonames", "type": "PPL" }, { "name": "Casier", "latitude": 10.09535, "longitude": 13.926, "x": 47.2, "y": 20.7, "source": "geonames", "type": "PPL" }, { "name": "Chibret", "latitude": 9.89771, "longitude": 13.89294, "x": 40.2, "y": 72.6, "source": "geonames", "type": "PPL" }, { "name": "Daba-Daba", "latitude": 10.02522, "longitude": 13.80007, "x": 20.7, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Dafa", "latitude": 9.97978, "longitude": 13.75457, "x": 11.1, "y": 51, "source": "geonames", "type": "PPL" }, { "name": "Daguen", "latitude": 9.98686, "longitude": 13.76992, "x": 14.3, "y": 49.2, "source": "geonames", "type": "PPL" }, { "name": "Dale", "latitude": 9.96375, "longitude": 13.77833, "x": 16.1, "y": 55.2, "source": "geonames", "type": "PPL" }, { "name": "Degueri", "latitude": 10.01073, "longitude": 13.78193, "x": 16.9, "y": 42.9, "source": "geonames", "type": "PPL" }, { "name": "Dem", "latitude": 9.93333, "longitude": 13.85, "x": 31.2, "y": 63.2, "source": "geonames", "type": "PPL" }, { "name": "Dibi", "latitude": 9.921, "longitude": 14.03, "x": 69, "y": 66.5, "source": "geonames", "type": "PPL" }, { "name": "Dinhini", "latitude": 10.04245, "longitude": 14.08554, "x": 80.7, "y": 34.6, "source": "geonames", "type": "PPL" }, { "name": "Djamboutou", "latitude": 9.85305, "longitude": 13.93885, "x": 49.9, "y": 84.3, "source": "geonames", "type": "PPL" }, { "name": "Djedjengue", "latitude": 9.93333, "longitude": 13.95, "x": 52.2, "y": 63.2, "source": "geonames", "type": "PPL" }, { "name": "Djedjingue", "latitude": 9.89454, "longitude": 13.95339, "x": 52.9, "y": 73.4, "source": "geonames", "type": "PPL" }, { "name": "Djougui", "latitude": 10.02059, "longitude": 14.09819, "x": 83.4, "y": 40.3, "source": "geonames", "type": "PPL" }, { "name": "Djouvoure", "latitude": 10.06194, "longitude": 13.90495, "x": 42.7, "y": 29.5, "source": "geonames", "type": "PPL" }, { "name": "Doubas", "latitude": 10.09116, "longitude": 14.06626, "x": 76.6, "y": 21.8, "source": "geonames", "type": "PPL" }, { "name": "Doumoult", "latitude": 10.10926, "longitude": 14.04152, "x": 71.4, "y": 17.1, "source": "geonames", "type": "PPL" }, { "name": "Fitim", "latitude": 9.95213, "longitude": 13.7605, "x": 12.4, "y": 58.3, "source": "geonames", "type": "PPL" }, { "name": "Galaou", "latitude": 10.02071, "longitude": 13.93222, "x": 48.5, "y": 40.3, "source": "geonames", "type": "PPL" }, { "name": "Galbanki", "latitude": 10.05797, "longitude": 14.06028, "x": 75.4, "y": 30.5, "source": "geonames", "type": "PPL" }, { "name": "Gandarma", "latitude": 10.08324, "longitude": 14.04815, "x": 72.8, "y": 23.9, "source": "geonames", "type": "PPL" }, { "name": "Gane", "latitude": 9.85584, "longitude": 13.82128, "x": 25.1, "y": 83.5, "source": "geonames", "type": "PPL" }, { "name": "Gara", "latitude": 9.83424, "longitude": 13.84085, "x": 29.3, "y": 89.2, "source": "geonames", "type": "PPL" }, { "name": "Gardama", "latitude": 10.04835, "longitude": 14.0675, "x": 76.9, "y": 33, "source": "geonames", "type": "PPL" }, { "name": "Gobriniam", "latitude": 9.89423, "longitude": 13.75968, "x": 12.2, "y": 73.5, "source": "geonames", "type": "PPL" }, { "name": "Golomo", "latitude": 9.88884, "longitude": 13.98362, "x": 59.3, "y": 74.9, "source": "geonames", "type": "PPL" }, { "name": "Golvong", "latitude": 10.03596, "longitude": 14.01791, "x": 66.5, "y": 36.3, "source": "geonames", "type": "PPL" }, { "name": "Gorom", "latitude": 10.05213, "longitude": 13.84213, "x": 29.5, "y": 32.1, "source": "geonames", "type": "PPL" }, { "name": "Gortong", "latitude": 9.97202, "longitude": 13.82211, "x": 25.3, "y": 53.1, "source": "geonames", "type": "PPL" }, { "name": "Goudak", "latitude": 10.10871, "longitude": 13.90765, "x": 43.3, "y": 17.2, "source": "geonames", "type": "PPL" }, { "name": "Goudou", "latitude": 9.87111, "longitude": 13.97215, "x": 56.9, "y": 79.5, "source": "geonames", "type": "PPL" }, { "name": "Goudougoul", "latitude": 10.01516, "longitude": 13.97094, "x": 56.6, "y": 41.8, "source": "geonames", "type": "PPL" }, { "name": "Goulong Fali", "latitude": 10.03214, "longitude": 13.88086, "x": 37.7, "y": 37.3, "source": "geonames", "type": "PPL" }, { "name": "Guereme", "latitude": 9.934, "longitude": 14.0995, "x": 83.6, "y": 63, "source": "geonames", "type": "PPL" }, { "name": "Guetker", "latitude": 10.06432, "longitude": 14.03806, "x": 70.7, "y": 28.9, "source": "geonames", "type": "PPL" }, { "name": "Houmbal", "latitude": 10.07185, "longitude": 14.10118, "x": 84, "y": 26.9, "source": "geonames", "type": "PPL" }, { "name": "Irguilang", "latitude": 10.10404, "longitude": 13.87418, "x": 36.3, "y": 18.4, "source": "geonames", "type": "PPL" }, { "name": "Kapta", "latitude": 10.08269, "longitude": 13.99153, "x": 60.9, "y": 24, "source": "geonames", "type": "PPL" }, { "name": "Kaptaossa", "latitude": 10.05818, "longitude": 14.04753, "x": 72.7, "y": 30.5, "source": "geonames", "type": "PPL" }, { "name": "Kirirambo", "latitude": 9.96806, "longitude": 13.8431, "x": 29.7, "y": 54.1, "source": "geonames", "type": "PPL" }, { "name": "Korake", "latitude": 9.89578, "longitude": 13.8092, "x": 22.6, "y": 73.1, "source": "geonames", "type": "PPL" }, { "name": "Korloko", "latitude": 10.01691, "longitude": 14.11914, "x": 87.8, "y": 41.3, "source": "geonames", "type": "PPL" }, { "name": "Kosseldje", "latitude": 9.9099, "longitude": 14.0065, "x": 64.1, "y": 69.4, "source": "geonames", "type": "PPL" }, { "name": "Koumkeoudji", "latitude": 9.87246, "longitude": 13.79397, "x": 19.4, "y": 79.2, "source": "geonames", "type": "PPL" }, { "name": "Koussou", "latitude": 10.09594, "longitude": 14.04523, "x": 72.2, "y": 20.6, "source": "geonames", "type": "PPL" }, { "name": "Lakawar", "latitude": 10.05, "longitude": 13.88333, "x": 38.2, "y": 32.6, "source": "geonames", "type": "PPL" }, { "name": "Laminguel", "latitude": 9.91042, "longitude": 13.77728, "x": 15.9, "y": 69.2, "source": "geonames", "type": "PPL" }, { "name": "Larbak", "latitude": 10.03697, "longitude": 13.91182, "x": 44.2, "y": 36, "source": "geonames", "type": "PPL" }, { "name": "Larma", "latitude": 10.0309, "longitude": 13.78646, "x": 17.8, "y": 37.6, "source": "geonames", "type": "PPL" }, { "name": "Libe", "latitude": 10.01164, "longitude": 13.79655, "x": 19.9, "y": 42.7, "source": "geonames", "type": "PPL" }, { "name": "Lougga Djabe", "latitude": 9.93842, "longitude": 13.75206, "x": 10.6, "y": 61.9, "source": "geonames", "type": "PPL" }, { "name": "Lougguere", "latitude": 9.93333, "longitude": 13.81667, "x": 24.2, "y": 63.2, "source": "geonames", "type": "PPL" }, { "name": "Lougguere-Foulbe", "latitude": 9.93617, "longitude": 13.84034, "x": 29.2, "y": 62.5, "source": "geonames", "type": "PPL" }, { "name": "Lougguere-Wala", "latitude": 9.93861, "longitude": 13.79857, "x": 20.4, "y": 61.8, "source": "geonames", "type": "PPL" }, { "name": "Maegari", "latitude": 10.03199, "longitude": 13.91797, "x": 45.5, "y": 37.3, "source": "geonames", "type": "PPL" }, { "name": "Malmaya", "latitude": 10.0075, "longitude": 14.08572, "x": 80.7, "y": 43.8, "source": "geonames", "type": "PPL" }, { "name": "Malouey", "latitude": 10.02911, "longitude": 14.00731, "x": 64.3, "y": 38.1, "source": "geonames", "type": "PPL" }, { "name": "Mambaza", "latitude": 10.04216, "longitude": 14.01677, "x": 66.2, "y": 34.7, "source": "geonames", "type": "PPL" }, { "name": "Masga", "latitude": 10.02912, "longitude": 13.99705, "x": 62.1, "y": 38.1, "source": "geonames", "type": "PPL" }, { "name": "Masgamkotokoto", "latitude": 10.06972, "longitude": 14.00468, "x": 63.7, "y": 27.4, "source": "geonames", "type": "PPL" }, { "name": "Matafal", "latitude": 10.10075, "longitude": 14.03004, "x": 69, "y": 19.3, "source": "geonames", "type": "PPL" }, { "name": "Mataibo", "latitude": 9.98679, "longitude": 13.85816, "x": 32.9, "y": 49.2, "source": "geonames", "type": "PPL" }, { "name": "Matalfare", "latitude": 10.00816, "longitude": 13.95965, "x": 54.2, "y": 43.6, "source": "geonames", "type": "PPL" }, { "name": "Mayel Daledje", "latitude": 10.00059, "longitude": 14.05866, "x": 75, "y": 45.6, "source": "geonames", "type": "PPL" }, { "name": "Mayo Loue", "latitude": 9.95333, "longitude": 13.97404, "x": 57.3, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Mayo Sanganare", "latitude": 10.11209, "longitude": 13.92612, "x": 47.2, "y": 16.3, "source": "geonames", "type": "PPL" }, { "name": "Mbro", "latitude": 9.8504, "longitude": 13.75727, "x": 11.7, "y": 85, "source": "geonames", "type": "PPL" }, { "name": "Mindjiwa", "latitude": 9.9767, "longitude": 14.0244, "x": 67.8, "y": 51.8, "source": "geonames", "type": "PPL" }, { "name": "Modjongo", "latitude": 10.02022, "longitude": 13.94999, "x": 52.2, "y": 40.4, "source": "geonames", "type": "PPL" }, { "name": "Mokorvong", "latitude": 9.95282, "longitude": 14.02931, "x": 68.9, "y": 58.1, "source": "geonames", "type": "PPL" }, { "name": "Monbaza", "latitude": 10.044, "longitude": 14.03202, "x": 69.4, "y": 34.2, "source": "geonames", "type": "PPL" }, { "name": "Naouka", "latitude": 10.0295, "longitude": 13.92826, "x": 47.6, "y": 38, "source": "geonames", "type": "PPL" }, { "name": "Ndiam Letinam", "latitude": 10.00528, "longitude": 13.87815, "x": 37.1, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Ndjada", "latitude": 10.02764, "longitude": 13.76962, "x": 14.3, "y": 38.5, "source": "geonames", "type": "PPL" }, { "name": "Ouro Abdou", "latitude": 9.96415, "longitude": 13.85573, "x": 32.4, "y": 55.1, "source": "geonames", "type": "PPL" }, { "name": "Ouro Dama", "latitude": 10.05887, "longitude": 14.0192, "x": 66.8, "y": 30.3, "source": "geonames", "type": "PPL" }, { "name": "Ouro Dangar", "latitude": 9.98333, "longitude": 13.98333, "x": 59.2, "y": 50.1, "source": "geonames", "type": "PPL" }, { "name": "Ouro Masgam", "latitude": 10.01822, "longitude": 14.00086, "x": 62.9, "y": 40.9, "source": "geonames", "type": "PPL" }, { "name": "Ouro Ngabou", "latitude": 9.94025, "longitude": 13.89717, "x": 41.1, "y": 61.4, "source": "geonames", "type": "PPL" }, { "name": "Ouro Saday", "latitude": 10.07391, "longitude": 13.81839, "x": 24.5, "y": 26.3, "source": "geonames", "type": "PPL" }, { "name": "Ouro Tara", "latitude": 9.91348, "longitude": 13.98129, "x": 58.8, "y": 68.4, "source": "geonames", "type": "PPL" }, { "name": "OuroHoue", "latitude": 10.06155, "longitude": 14.01947, "x": 66.8, "y": 29.6, "source": "geonames", "type": "PPL" }, { "name": "Parkin", "latitude": 9.94807, "longitude": 13.90859, "x": 43.5, "y": 59.4, "source": "geonames", "type": "PPL" }, { "name": "Poka", "latitude": 9.98803, "longitude": 13.75763, "x": 11.8, "y": 48.9, "source": "geonames", "type": "PPL" }, { "name": "Sanguere", "latitude": 9.91743, "longitude": 13.94284, "x": 50.7, "y": 67.4, "source": "geonames", "type": "PPL" }, { "name": "Sarawa", "latitude": 10.05376, "longitude": 13.95203, "x": 52.6, "y": 31.6, "source": "geonames", "type": "PPL" }, { "name": "Sombetcho", "latitude": 9.83125, "longitude": 13.77123, "x": 14.6, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Soukoundou", "latitude": 9.85665, "longitude": 13.88993, "x": 39.6, "y": 83.3, "source": "geonames", "type": "PPL" }, { "name": "Soupta-Bani", "latitude": 9.91074, "longitude": 13.86799, "x": 35, "y": 69.1, "source": "geonames", "type": "PPL" }, { "name": "Tawan", "latitude": 10.10764, "longitude": 13.95077, "x": 52.4, "y": 17.5, "source": "geonames", "type": "PPL" }, { "name": "Tchakadjanwa", "latitude": 9.98074, "longitude": 13.91425, "x": 44.7, "y": 50.8, "source": "geonames", "type": "PPL" }, { "name": "Tchapka-Passiri", "latitude": 9.87576, "longitude": 13.74923, "x": 10, "y": 78.3, "source": "geonames", "type": "PPL" }, { "name": "Tchekei", "latitude": 10.02345, "longitude": 13.92679, "x": 47.3, "y": 39.6, "source": "geonames", "type": "PPL" }, { "name": "Tchikaf", "latitude": 10.02272, "longitude": 13.91802, "x": 45.5, "y": 39.8, "source": "geonames", "type": "PPL" }, { "name": "Tekele", "latitude": 10.0191, "longitude": 13.84092, "x": 29.3, "y": 40.7, "source": "geonames", "type": "PPL" }, { "name": "Tikelke", "latitude": 9.9795, "longitude": 13.7916, "x": 18.9, "y": 51.1, "source": "geonames", "type": "PPL" }, { "name": "Tirlao", "latitude": 10.04494, "longitude": 13.80891, "x": 22.5, "y": 33.9, "source": "geonames", "type": "PPL" }, { "name": "Tonkolo", "latitude": 9.88695, "longitude": 13.93635, "x": 49.3, "y": 75.4, "source": "geonames", "type": "PPL" }, { "name": "Torkin", "latitude": 9.95023, "longitude": 13.779, "x": 16.3, "y": 58.8, "source": "geonames", "type": "PPL" }, { "name": "Tra", "latitude": 10.05591, "longitude": 13.79011, "x": 18.6, "y": 31.1, "source": "geonames", "type": "PPL" }, { "name": "Vagama", "latitude": 10.04636, "longitude": 14.01803, "x": 66.5, "y": 33.6, "source": "geonames", "type": "PPL" }, { "name": "Vinde Yola", "latitude": 10.08478, "longitude": 13.80579, "x": 21.9, "y": 23.5, "source": "geonames", "type": "PPL" }, { "name": "VindeLoue", "latitude": 10.01796, "longitude": 14.01624, "x": 66.1, "y": 41, "source": "geonames", "type": "PPL" }, { "name": "Vourmoutch", "latitude": 10.04778, "longitude": 13.77295, "x": 15, "y": 33.2, "source": "geonames", "type": "PPL" }, { "name": "Yapere", "latitude": 10.00532, "longitude": 13.83916, "x": 28.9, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Zevene", "latitude": 10.13619, "longitude": 13.94792, "x": 51.8, "y": 10, "source": "geonames", "type": "PPL" } ], "Jakiri": [ { "name": "Bamessi", "latitude": 6.01667, "longitude": 10.56667, "x": 10, "y": 73.6, "source": "geonames", "type": "PPL" }, { "name": "Bunto", "latitude": 5.98668, "longitude": 10.59857, "x": 31.9, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Koubokam", "latitude": 6.05, "longitude": 10.68333, "x": 90, "y": 55.5, "source": "geonames", "type": "PPL" }, { "name": "Mambin", "latitude": 5.99516, "longitude": 10.61744, "x": 44.8, "y": 85.4, "source": "geonames", "type": "PPL" }, { "name": "Melim", "latitude": 6.13333, "longitude": 10.6, "x": 32.9, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Tabessob", "latitude": 6.11667, "longitude": 10.61667, "x": 44.3, "y": 19.1, "source": "geonames", "type": "PPL" } ], "Kaele": [ { "name": "Bahibi", "latitude": 10.01612, "longitude": 14.48562, "x": 67.5, "y": 85.1, "source": "geonames", "type": "PPL" }, { "name": "Balsale", "latitude": 10.00111, "longitude": 14.33233, "x": 21, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Banaoua", "latitude": 10.1852, "longitude": 14.34549, "x": 25, "y": 29.8, "source": "geonames", "type": "PPL" }, { "name": "Barkoy", "latitude": 10.06967, "longitude": 14.46595, "x": 61.5, "y": 67.6, "source": "geonames", "type": "PPL" }, { "name": "Berkede", "latitude": 10.06991, "longitude": 14.45101, "x": 57, "y": 67.5, "source": "geonames", "type": "PPL" }, { "name": "Bibale", "latitude": 10.19733, "longitude": 14.52495, "x": 79.4, "y": 25.8, "source": "geonames", "type": "PPL" }, { "name": "Bidoubi", "latitude": 10.20335, "longitude": 14.45055, "x": 56.9, "y": 23.9, "source": "geonames", "type": "PPLX" }, { "name": "Biguila", "latitude": 10.02486, "longitude": 14.51803, "x": 77.3, "y": 82.2, "source": "geonames", "type": "PPL" }, { "name": "Bilao", "latitude": 10.07394, "longitude": 14.52485, "x": 79.4, "y": 66.2, "source": "geonames", "type": "PPL" }, { "name": "Bipain", "latitude": 10.18984, "longitude": 14.55965, "x": 90, "y": 28.3, "source": "geonames", "type": "PPL" }, { "name": "Bissele", "latitude": 10.09708, "longitude": 14.48848, "x": 68.4, "y": 58.6, "source": "geonames", "type": "PPL" }, { "name": "Bissie", "latitude": 10.17128, "longitude": 14.47652, "x": 64.7, "y": 34.4, "source": "geonames", "type": "PPL" }, { "name": "Bololo", "latitude": 10.18131, "longitude": 14.32756, "x": 19.6, "y": 31.1, "source": "geonames", "type": "PPL" }, { "name": "Boulang", "latitude": 10.0879, "longitude": 14.53869, "x": 83.6, "y": 61.6, "source": "geonames", "type": "PPL" }, { "name": "Bouloum", "latitude": 10.15792, "longitude": 14.38874, "x": 38.1, "y": 38.7, "source": "geonames", "type": "PPL" }, { "name": "Bourgou", "latitude": 10.08478, "longitude": 14.33674, "x": 22.3, "y": 62.6, "source": "geonames", "type": "PPL" }, { "name": "Cochou", "latitude": 10.05582, "longitude": 14.48643, "x": 67.7, "y": 72.1, "source": "geonames", "type": "PPL" }, { "name": "Dassile", "latitude": 10.21574, "longitude": 14.41743, "x": 46.8, "y": 19.8, "source": "geonames", "type": "PPL" }, { "name": "Datchiabe", "latitude": 10.12002, "longitude": 14.49612, "x": 70.7, "y": 51.1, "source": "geonames", "type": "PPL" }, { "name": "Derdao", "latitude": 10.07424, "longitude": 14.42457, "x": 49, "y": 66.1, "source": "geonames", "type": "PPL" }, { "name": "Dervoum", "latitude": 10.09362, "longitude": 14.5498, "x": 87, "y": 59.8, "source": "geonames", "type": "PPL" }, { "name": "Dibanwa", "latitude": 10.17461, "longitude": 14.30876, "x": 13.9, "y": 33.3, "source": "geonames", "type": "PPL" }, { "name": "Diguin", "latitude": 10.18016, "longitude": 14.30913, "x": 14, "y": 31.5, "source": "geonames", "type": "PPL" }, { "name": "Dingding", "latitude": 10.15024, "longitude": 14.53323, "x": 81.9, "y": 41.2, "source": "geonames", "type": "PPL" }, { "name": "Djafado", "latitude": 10.17557, "longitude": 14.44983, "x": 56.6, "y": 33, "source": "geonames", "type": "PPL" }, { "name": "Djamboutou", "latitude": 10.0379, "longitude": 14.45171, "x": 57.2, "y": 78, "source": "geonames", "type": "PPL" }, { "name": "Djidjoma", "latitude": 10.0787, "longitude": 14.43328, "x": 51.6, "y": 64.6, "source": "geonames", "type": "PPL" }, { "name": "Djoringal", "latitude": 10.05903, "longitude": 14.48383, "x": 67, "y": 71.1, "source": "geonames", "type": "PPL" }, { "name": "Douhayra", "latitude": 10.04894, "longitude": 14.49439, "x": 70.2, "y": 74.4, "source": "geonames", "type": "PPL" }, { "name": "Doukalaro", "latitude": 10.02885, "longitude": 14.47016, "x": 62.8, "y": 80.9, "source": "geonames", "type": "PPL" }, { "name": "Doumrou", "latitude": 10.04065, "longitude": 14.48223, "x": 66.5, "y": 77.1, "source": "geonames", "type": "PPL" }, { "name": "Doungoy", "latitude": 10.11671, "longitude": 14.42273, "x": 48.4, "y": 52.2, "source": "geonames", "type": "PPL" }, { "name": "Drame", "latitude": 10.08106, "longitude": 14.42253, "x": 48.4, "y": 63.9, "source": "geonames", "type": "PPL" }, { "name": "Fossile", "latitude": 10.18744, "longitude": 14.43521, "x": 52.2, "y": 29.1, "source": "geonames", "type": "PPL" }, { "name": "Fouli", "latitude": 10.00675, "longitude": 14.31555, "x": 15.9, "y": 88.2, "source": "geonames", "type": "PPL" }, { "name": "Gaban", "latitude": 10.22424, "longitude": 14.54818, "x": 86.5, "y": 17, "source": "geonames", "type": "PPL" }, { "name": "Gadas", "latitude": 10.19189, "longitude": 14.43895, "x": 53.3, "y": 27.6, "source": "geonames", "type": "PPL" }, { "name": "Gambour", "latitude": 10.2124, "longitude": 14.52102, "x": 78.2, "y": 20.9, "source": "geonames", "type": "PPLX" }, { "name": "Gapring", "latitude": 10.13273, "longitude": 14.54078, "x": 84.2, "y": 47, "source": "geonames", "type": "PPL" }, { "name": "Garey", "latitude": 10.03337, "longitude": 14.33312, "x": 21.2, "y": 79.5, "source": "geonames", "type": "PPL" }, { "name": "Goda", "latitude": 10.1568, "longitude": 14.36491, "x": 30.9, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Gohing", "latitude": 10.07634, "longitude": 14.55849, "x": 89.6, "y": 65.4, "source": "geonames", "type": "PPL" }, { "name": "Goudjouwing", "latitude": 10.15264, "longitude": 14.40454, "x": 42.9, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Goudoum-Goudoum", "latitude": 10.13016, "longitude": 14.43593, "x": 52.4, "y": 47.8, "source": "geonames", "type": "PPL" }, { "name": "Goujougoui", "latitude": 10.10145, "longitude": 14.43874, "x": 53.3, "y": 57.2, "source": "geonames", "type": "PPL" }, { "name": "Goussor", "latitude": 10.12769, "longitude": 14.55978, "x": 90, "y": 48.6, "source": "geonames", "type": "PPL" }, { "name": "Guebare", "latitude": 10.04398, "longitude": 14.49452, "x": 70.2, "y": 76, "source": "geonames", "type": "PPL" }, { "name": "Guedame", "latitude": 10.0189, "longitude": 14.332, "x": 20.9, "y": 84.2, "source": "geonames", "type": "PPL" }, { "name": "Guereme", "latitude": 10.04679, "longitude": 14.52925, "x": 80.7, "y": 75.1, "source": "geonames", "type": "PPL" }, { "name": "Guetale", "latitude": 10.06483, "longitude": 14.49527, "x": 70.4, "y": 69.2, "source": "geonames", "type": "PPL" }, { "name": "Guissiga", "latitude": 10.11746, "longitude": 14.43869, "x": 53.3, "y": 52, "source": "geonames", "type": "PPL" }, { "name": "Hodango", "latitude": 10.06456, "longitude": 14.50002, "x": 71.9, "y": 69.3, "source": "geonames", "type": "PPL" }, { "name": "Kani", "latitude": 10.12464, "longitude": 14.43657, "x": 52.6, "y": 49.6, "source": "geonames", "type": "PPL" }, { "name": "Kassile", "latitude": 10.1714, "longitude": 14.4162, "x": 46.4, "y": 34.3, "source": "geonames", "type": "PPLX" }, { "name": "Kazarao", "latitude": 10.24578, "longitude": 14.43295, "x": 51.5, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Kichi", "latitude": 10.18256, "longitude": 14.34616, "x": 25.2, "y": 30.7, "source": "geonames", "type": "PPL" }, { "name": "Kidikan", "latitude": 10.0847, "longitude": 14.49133, "x": 69.2, "y": 62.7, "source": "geonames", "type": "PPL" }, { "name": "Kilguim", "latitude": 10.20683, "longitude": 14.49661, "x": 70.8, "y": 22.7, "source": "geonames", "type": "PPL" }, { "name": "Kourong", "latitude": 10.02008, "longitude": 14.44166, "x": 54.2, "y": 83.8, "source": "geonames", "type": "PPL" }, { "name": "Kyo-Kyo", "latitude": 10.10732, "longitude": 14.46921, "x": 62.5, "y": 55.3, "source": "geonames", "type": "PPL" }, { "name": "Laleouo", "latitude": 10.03447, "longitude": 14.4763, "x": 64.7, "y": 79.1, "source": "geonames", "type": "PPL" }, { "name": "Lamtari", "latitude": 10.00693, "longitude": 14.38326, "x": 36.5, "y": 88.1, "source": "geonames", "type": "PPL" }, { "name": "Lara", "latitude": 10.17815, "longitude": 14.51007, "x": 74.9, "y": 32.1, "source": "geonames", "type": "PPL" }, { "name": "Lepros", "latitude": 10.15159, "longitude": 14.48911, "x": 68.6, "y": 40.8, "source": "geonames", "type": "PPL" }, { "name": "Lera", "latitude": 10.16646, "longitude": 14.5455, "x": 85.7, "y": 35.9, "source": "geonames", "type": "PPL" }, { "name": "Ligazang", "latitude": 10.17002, "longitude": 14.37135, "x": 32.8, "y": 34.8, "source": "geonames", "type": "PPL" }, { "name": "Lirinde", "latitude": 10.03996, "longitude": 14.55542, "x": 88.7, "y": 77.3, "source": "geonames", "type": "PPL" }, { "name": "Louka", "latitude": 10.11842, "longitude": 14.49062, "x": 69, "y": 51.6, "source": "geonames", "type": "PPL" }, { "name": "Magrongong", "latitude": 10.11236, "longitude": 14.50611, "x": 73.7, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Mahay", "latitude": 10.19954, "longitude": 14.31514, "x": 15.8, "y": 25.1, "source": "geonames", "type": "PPL" }, { "name": "Makebi", "latitude": 10.13467, "longitude": 14.51114, "x": 75.2, "y": 46.3, "source": "geonames", "type": "PPL" }, { "name": "Malanegone", "latitude": 10.13467, "longitude": 14.5267, "x": 80, "y": 46.3, "source": "geonames", "type": "PPL" }, { "name": "Mangouron", "latitude": 10.10019, "longitude": 14.45927, "x": 59.5, "y": 57.6, "source": "geonames", "type": "PPL" }, { "name": "Massanye", "latitude": 10.11204, "longitude": 14.48793, "x": 68.2, "y": 53.7, "source": "geonames", "type": "PPL" }, { "name": "Matonri", "latitude": 10.14245, "longitude": 14.48666, "x": 67.8, "y": 43.8, "source": "geonames", "type": "PPL" }, { "name": "Mazang", "latitude": 10.1311, "longitude": 14.48686, "x": 67.9, "y": 47.5, "source": "geonames", "type": "PPL" }, { "name": "Mbororo", "latitude": 10.16588, "longitude": 14.34238, "x": 24.1, "y": 36.1, "source": "geonames", "type": "PPL" }, { "name": "Midjivin", "latitude": 10.16756, "longitude": 14.32531, "x": 18.9, "y": 35.6, "source": "geonames", "type": "PPL" }, { "name": "Modjem", "latitude": 10.24294, "longitude": 14.3788, "x": 35.1, "y": 10.9, "source": "geonames", "type": "PPL" }, { "name": "Moumour", "latitude": 10.11917, "longitude": 14.32439, "x": 18.6, "y": 51.4, "source": "geonames", "type": "PPL" }, { "name": "Moundjoui", "latitude": 10.18574, "longitude": 14.35114, "x": 26.7, "y": 29.6, "source": "geonames", "type": "PPL" }, { "name": "Mourbare", "latitude": 10.13144, "longitude": 14.38475, "x": 36.9, "y": 47.4, "source": "geonames", "type": "PPL" }, { "name": "Mouzao", "latitude": 10.09466, "longitude": 14.31216, "x": 14.9, "y": 59.4, "source": "geonames", "type": "PPL" }, { "name": "OuroBadiafe", "latitude": 10.0804, "longitude": 14.49231, "x": 69.5, "y": 64.1, "source": "geonames", "type": "PPL" }, { "name": "Pakana", "latitude": 10.00925, "longitude": 14.48206, "x": 66.4, "y": 87.3, "source": "geonames", "type": "PPL" }, { "name": "Paroue", "latitude": 10.13804, "longitude": 14.39748, "x": 40.8, "y": 45.2, "source": "geonames", "type": "PPL" }, { "name": "Pazani", "latitude": 10.02516, "longitude": 14.46994, "x": 62.7, "y": 82.1, "source": "geonames", "type": "PPL" }, { "name": "Pilzimiri", "latitude": 10.03574, "longitude": 14.4939, "x": 70, "y": 78.7, "source": "geonames", "type": "PPL" }, { "name": "Piwa", "latitude": 10.06132, "longitude": 14.42116, "x": 48, "y": 70.3, "source": "geonames", "type": "PPL" }, { "name": "Poguere", "latitude": 10.17404, "longitude": 14.54915, "x": 86.8, "y": 33.5, "source": "geonames", "type": "PPL" }, { "name": "Pomla", "latitude": 10.06352, "longitude": 14.48536, "x": 67.4, "y": 69.6, "source": "geonames", "type": "PPL" }, { "name": "Poudama", "latitude": 10.13149, "longitude": 14.37314, "x": 33.4, "y": 47.4, "source": "geonames", "type": "PPL" }, { "name": "Pougo", "latitude": 10.10345, "longitude": 14.48041, "x": 65.9, "y": 56.5, "source": "geonames", "type": "PPL" }, { "name": "Pouhoure", "latitude": 10.20257, "longitude": 14.41839, "x": 47.1, "y": 24.1, "source": "geonames", "type": "PPL" }, { "name": "Poukebi", "latitude": 10.11994, "longitude": 14.4044, "x": 42.9, "y": 51.1, "source": "geonames", "type": "PPL" }, { "name": "Roumde", "latitude": 10.01374, "longitude": 14.46943, "x": 62.6, "y": 85.9, "source": "geonames", "type": "PPL" }, { "name": "Saraoua", "latitude": 10.16862, "longitude": 14.31673, "x": 16.3, "y": 35.2, "source": "geonames", "type": "PPL" }, { "name": "Sildiguere", "latitude": 10.19779, "longitude": 14.43996, "x": 53.7, "y": 25.7, "source": "geonames", "type": "PPL" }, { "name": "Sokoy", "latitude": 10.02449, "longitude": 14.48129, "x": 66.2, "y": 82.4, "source": "geonames", "type": "PPL" }, { "name": "Tematoulbo", "latitude": 10.00742, "longitude": 14.33783, "x": 22.7, "y": 87.9, "source": "geonames", "type": "PPL" }, { "name": "Tibili", "latitude": 10.0381, "longitude": 14.33474, "x": 21.7, "y": 77.9, "source": "geonames", "type": "PPL" }, { "name": "Tibiri", "latitude": 10.09644, "longitude": 14.47756, "x": 65.1, "y": 58.8, "source": "geonames", "type": "PPL" }, { "name": "Titguiari", "latitude": 10.02793, "longitude": 14.33939, "x": 23.1, "y": 81.2, "source": "geonames", "type": "PPL" }, { "name": "Tonde", "latitude": 10.06181, "longitude": 14.52737, "x": 80.2, "y": 70.2, "source": "geonames", "type": "PPL" }, { "name": "Torok", "latitude": 10.04865, "longitude": 14.55403, "x": 88.3, "y": 74.5, "source": "geonames", "type": "PPL" }, { "name": "Toupoulsile", "latitude": 10.19632, "longitude": 14.42277, "x": 48.4, "y": 26.2, "source": "geonames", "type": "PPLX" }, { "name": "Tourkouri", "latitude": 10.1707, "longitude": 14.42641, "x": 49.5, "y": 34.5, "source": "geonames", "type": "PPL" }, { "name": "Tsabel", "latitude": 10.10325, "longitude": 14.46784, "x": 62.1, "y": 56.6, "source": "geonames", "type": "PPL" }, { "name": "WafangoLaoual", "latitude": 10.05079, "longitude": 14.47672, "x": 64.8, "y": 73.8, "source": "geonames", "type": "PPL" }, { "name": "Windeo", "latitude": 10.05275, "longitude": 14.4849, "x": 67.3, "y": 73.1, "source": "geonames", "type": "PPL" }, { "name": "Yolde", "latitude": 10.07197, "longitude": 14.48422, "x": 67.1, "y": 66.8, "source": "geonames", "type": "PPL" }, { "name": "Yoldeo", "latitude": 10.05302, "longitude": 14.47893, "x": 65.5, "y": 73, "source": "geonames", "type": "PPL" }, { "name": "Zaguere", "latitude": 10.12483, "longitude": 14.50805, "x": 74.3, "y": 49.5, "source": "geonames", "type": "PPL" }, { "name": "Zaklang", "latitude": 10.22696, "longitude": 14.41052, "x": 44.7, "y": 16.2, "source": "geonames", "type": "PPL" }, { "name": "Zale", "latitude": 10.06623, "longitude": 14.4253, "x": 49.2, "y": 68.7, "source": "geonames", "type": "PPL" }, { "name": "Zamakossou", "latitude": 10.06665, "longitude": 14.29605, "x": 10, "y": 68.6, "source": "geonames", "type": "PPL" }, { "name": "Zapili", "latitude": 10.15686, "longitude": 14.42219, "x": 48.3, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Zapotoksi", "latitude": 10.17891, "longitude": 14.37206, "x": 33.1, "y": 31.9, "source": "geonames", "type": "PPL" }, { "name": "Zassimgri", "latitude": 10.19859, "longitude": 14.48822, "x": 68.3, "y": 25.4, "source": "geonames", "type": "PPL" }, { "name": "Zassinkou", "latitude": 10.06604, "longitude": 14.41469, "x": 46, "y": 68.8, "source": "geonames", "type": "PPL" } ], "Kekem": [ { "name": "Bale", "latitude": 5.25633, "longitude": 10.0862, "x": 83.5, "y": 22.3, "source": "geonames", "type": "PPL" }, { "name": "Banwa Centre", "latitude": 5.10253, "longitude": 10.07057, "x": 74, "y": 65.5, "source": "geonames", "type": "PPL" }, { "name": "Banwa Fonti", "latitude": 5.12012, "longitude": 10.05853, "x": 66.7, "y": 60.5, "source": "geonames", "type": "PPL" }, { "name": "Bongouo", "latitude": 5.2739, "longitude": 9.9929, "x": 26.6, "y": 17.3, "source": "geonames", "type": "PPL" }, { "name": "Boutcha Fongam", "latitude": 5.01525, "longitude": 10.09414, "x": 88.4, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Chwinou", "latitude": 5.03866, "longitude": 10.0968, "x": 90, "y": 83.4, "source": "geonames", "type": "PPL" }, { "name": "Fomessa I", "latitude": 5.07483, "longitude": 10.06288, "x": 69.3, "y": 73.3, "source": "geonames", "type": "PPL" }, { "name": "Fondjomoko", "latitude": 5.16685, "longitude": 10.0897, "x": 85.7, "y": 47.4, "source": "geonames", "type": "PPL" }, { "name": "Fongouan", "latitude": 5.2344, "longitude": 9.9876, "x": 23.4, "y": 28.4, "source": "geonames", "type": "PPL" }, { "name": "Fonkhe", "latitude": 5.3, "longitude": 9.96667, "x": 10.7, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Foyemtcha", "latitude": 5.20913, "longitude": 10.07372, "x": 75.9, "y": 35.5, "source": "geonames", "type": "PPL" }, { "name": "Kambo", "latitude": 5.18318, "longitude": 10.03727, "x": 53.7, "y": 42.8, "source": "geonames", "type": "PPL" }, { "name": "Lele", "latitude": 5.25935, "longitude": 10.08767, "x": 84.4, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Lelem-Moatong", "latitude": 5.1995, "longitude": 9.9918, "x": 26, "y": 38.2, "source": "geonames", "type": "PPL" }, { "name": "Mankang", "latitude": 5.25889, "longitude": 10.06652, "x": 71.5, "y": 21.5, "source": "geonames", "type": "PPL" }, { "name": "Mbafam", "latitude": 5.10884, "longitude": 10.02333, "x": 45.2, "y": 63.7, "source": "geonames", "type": "PPL" }, { "name": "Mbomi", "latitude": 5.2539, "longitude": 9.9656, "x": 10, "y": 23, "source": "geonames", "type": "PPL" }, { "name": "Mengwi", "latitude": 5.23195, "longitude": 10.07636, "x": 77.5, "y": 29.1, "source": "geonames", "type": "PPL" }, { "name": "Mfouri", "latitude": 5.18156, "longitude": 10.00841, "x": 36.1, "y": 43.3, "source": "geonames", "type": "PPL" }, { "name": "Moumme", "latitude": 5.16158, "longitude": 10.0557, "x": 64.9, "y": 48.9, "source": "geonames", "type": "PPL" }, { "name": "Ngang", "latitude": 5.1798, "longitude": 10.02319, "x": 45.1, "y": 43.8, "source": "geonames", "type": "PPL" }, { "name": "Nganso", "latitude": 5.20786, "longitude": 10.01252, "x": 38.6, "y": 35.9, "source": "geonames", "type": "PPL" }, { "name": "Nkongsoung", "latitude": 5.13333, "longitude": 10.01667, "x": 41.1, "y": 56.8, "source": "geonames", "type": "PPL" }, { "name": "Poungue", "latitude": 5.13222, "longitude": 10.0748, "x": 76.6, "y": 57.1, "source": "geonames", "type": "PPL" }, { "name": "Santchou", "latitude": 5.2794, "longitude": 9.9787, "x": 18, "y": 15.8, "source": "geonames", "type": "PPL" }, { "name": "Tenga", "latitude": 5.06716, "longitude": 10.06926, "x": 73.2, "y": 75.4, "source": "geonames", "type": "PPL" }, { "name": "Yong", "latitude": 5.09565, "longitude": 10.0427, "x": 57, "y": 67.4, "source": "geonames", "type": "PPL" } ], "Kette": [ { "name": "Bedobo", "latitude": 4.8, "longitude": 14.56667, "x": 78.6, "y": 83.6, "source": "geonames", "type": "PPL" }, { "name": "Bengue Tiko", "latitude": 5.06667, "longitude": 14.51667, "x": 61.4, "y": 32.4, "source": "geonames", "type": "PPL" }, { "name": "Boden", "latitude": 4.98333, "longitude": 14.43333, "x": 32.9, "y": 48.4, "source": "geonames", "type": "PPL" }, { "name": "Bongoya", "latitude": 4.96667, "longitude": 14.43333, "x": 32.9, "y": 51.6, "source": "geonames", "type": "PPL" }, { "name": "Borongokou", "latitude": 5.16667, "longitude": 14.55, "x": 72.9, "y": 13.2, "source": "geonames", "type": "PPL" }, { "name": "Bousia", "latitude": 4.93333, "longitude": 14.56667, "x": 78.6, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Dilling", "latitude": 4.8, "longitude": 14.58333, "x": 84.3, "y": 83.6, "source": "geonames", "type": "PPL" }, { "name": "Gogazi", "latitude": 4.91667, "longitude": 14.36667, "x": 10, "y": 61.2, "source": "geonames", "type": "PPL" }, { "name": "Gogoboua", "latitude": 4.96667, "longitude": 14.55, "x": 72.9, "y": 51.6, "source": "geonames", "type": "PPL" }, { "name": "Gonkoura", "latitude": 4.86667, "longitude": 14.55, "x": 72.9, "y": 70.8, "source": "geonames", "type": "PPL" }, { "name": "Lingbim", "latitude": 4.98333, "longitude": 14.5, "x": 55.7, "y": 48.4, "source": "geonames", "type": "PPL" }, { "name": "Mbelebina", "latitude": 5.15, "longitude": 14.53333, "x": 67.1, "y": 16.4, "source": "geonames", "type": "PPL" }, { "name": "Nambaya", "latitude": 4.76667, "longitude": 14.6, "x": 90, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Nambora", "latitude": 4.95, "longitude": 14.56667, "x": 78.6, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Ndambi", "latitude": 4.88333, "longitude": 14.55, "x": 72.9, "y": 67.6, "source": "geonames", "type": "PPL" }, { "name": "Ngabana", "latitude": 4.91667, "longitude": 14.6, "x": 90, "y": 61.2, "source": "geonames", "type": "PPL" }, { "name": "Ngasa", "latitude": 4.95, "longitude": 14.56667, "x": 78.6, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Oussounou", "latitude": 5.18333, "longitude": 14.55, "x": 72.9, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Tezoukpe", "latitude": 4.88333, "longitude": 14.55, "x": 72.9, "y": 67.6, "source": "geonames", "type": "PPL" }, { "name": "Tikila", "latitude": 4.91667, "longitude": 14.56667, "x": 78.6, "y": 61.2, "source": "geonames", "type": "PPL" } ], "Konye": [ { "name": "Babensi I", "latitude": 5.1162, "longitude": 9.4043, "x": 39.9, "y": 11.8, "source": "geonames", "type": "PPL" }, { "name": "Babensi II", "latitude": 5.1236, "longitude": 9.3933, "x": 37.7, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Baduma", "latitude": 4.83137, "longitude": 9.45873, "x": 50.7, "y": 81, "source": "geonames", "type": "PPL" }, { "name": "Badun", "latitude": 5.09543, "longitude": 9.43657, "x": 46.3, "y": 16.8, "source": "geonames", "type": "PPL" }, { "name": "Bakole", "latitude": 4.962, "longitude": 9.5124, "x": 61.4, "y": 49.3, "source": "geonames", "type": "PPL" }, { "name": "Bangone", "latitude": 4.8433, "longitude": 9.5749, "x": 73.8, "y": 78.1, "source": "geonames", "type": "PPL" }, { "name": "Bekoli", "latitude": 4.8551, "longitude": 9.3577, "x": 30.6, "y": 75.3, "source": "geonames", "type": "PPL" }, { "name": "Bolo Moboka", "latitude": 4.8633, "longitude": 9.4606, "x": 51.1, "y": 73.3, "source": "geonames", "type": "PPL" }, { "name": "Boubaji", "latitude": 5.0717, "longitude": 9.488, "x": 56.6, "y": 22.6, "source": "geonames", "type": "PPL" }, { "name": "Buka", "latitude": 5.0865, "longitude": 9.5322, "x": 65.3, "y": 19, "source": "geonames", "type": "PPL" }, { "name": "Dikome Bafaw", "latitude": 4.95443, "longitude": 9.46879, "x": 52.7, "y": 51.1, "source": "geonames", "type": "PPL" }, { "name": "Dikome Balue", "latitude": 4.9025, "longitude": 9.254, "x": 10, "y": 63.7, "source": "geonames", "type": "PPL" }, { "name": "Dipenda", "latitude": 5.0669, "longitude": 9.3822, "x": 35.5, "y": 23.8, "source": "geonames", "type": "PPL" }, { "name": "Dubange", "latitude": 4.9757, "longitude": 9.4335, "x": 45.7, "y": 46, "source": "geonames", "type": "PPL" }, { "name": "Ebemi Bakundu", "latitude": 4.9986, "longitude": 9.2807, "x": 15.3, "y": 40.4, "source": "geonames", "type": "PPL" }, { "name": "Ebemi II", "latitude": 5.05007, "longitude": 9.34398, "x": 27.9, "y": 27.9, "source": "geonames", "type": "PPL" }, { "name": "Eboko Bajok", "latitude": 4.9713, "longitude": 9.5124, "x": 61.4, "y": 47, "source": "geonames", "type": "PPL" }, { "name": "Ebota", "latitude": 4.8174, "longitude": 9.3608, "x": 31.2, "y": 84.4, "source": "geonames", "type": "PPL" }, { "name": "Ediki Mbonge", "latitude": 4.7996, "longitude": 9.4321, "x": 45.4, "y": 88.8, "source": "geonames", "type": "PPL" }, { "name": "Ekeb", "latitude": 4.9004, "longitude": 9.5925, "x": 77.3, "y": 64.3, "source": "geonames", "type": "PPL" }, { "name": "Ekobum", "latitude": 5.06667, "longitude": 9.41667, "x": 42.4, "y": 23.8, "source": "geonames", "type": "PPL" }, { "name": "Ekona", "latitude": 5.0288, "longitude": 9.4924, "x": 57.4, "y": 33, "source": "geonames", "type": "PPL" }, { "name": "Ifanga Nalende", "latitude": 4.9264, "longitude": 9.3407, "x": 27.2, "y": 57.9, "source": "geonames", "type": "PPL" }, { "name": "Ifanga-Ya-Onya", "latitude": 4.9371, "longitude": 9.3056, "x": 20.3, "y": 55.3, "source": "geonames", "type": "PPL" }, { "name": "Ile", "latitude": 4.9538, "longitude": 9.4169, "x": 42.4, "y": 51.3, "source": "geonames", "type": "PPL" }, { "name": "Koba", "latitude": 5.0582, "longitude": 9.3694, "x": 33, "y": 25.9, "source": "geonames", "type": "PPL" }, { "name": "Kokobuma", "latitude": 5.07732, "longitude": 9.40142, "x": 39.3, "y": 21.3, "source": "geonames", "type": "PPL" }, { "name": "Kombone Bafaw", "latitude": 4.9866, "longitude": 9.4344, "x": 45.9, "y": 43.3, "source": "geonames", "type": "PPL" }, { "name": "Kukaka", "latitude": 4.91641, "longitude": 9.47561, "x": 54.1, "y": 60.4, "source": "geonames", "type": "PPL" }, { "name": "Kumbe", "latitude": 5.0425, "longitude": 9.4083, "x": 40.7, "y": 29.7, "source": "geonames", "type": "PPL" }, { "name": "Kurume", "latitude": 4.9001, "longitude": 9.4728, "x": 53.5, "y": 64.3, "source": "geonames", "type": "PPL" }, { "name": "Lokando", "latitude": 4.8521, "longitude": 9.284, "x": 16, "y": 76, "source": "geonames", "type": "PPL" }, { "name": "Lubangi", "latitude": 4.9363, "longitude": 9.366, "x": 32.3, "y": 55.5, "source": "geonames", "type": "PPL" }, { "name": "Mahusom", "latitude": 5.0156, "longitude": 9.6561, "x": 90, "y": 36.3, "source": "geonames", "type": "PPL" }, { "name": "Matondo I", "latitude": 4.81975, "longitude": 9.46216, "x": 51.4, "y": 83.9, "source": "geonames", "type": "PPL" }, { "name": "Matondo II", "latitude": 4.8662, "longitude": 9.391, "x": 37.3, "y": 72.6, "source": "geonames", "type": "PPL" }, { "name": "Mbenge", "latitude": 4.9787, "longitude": 9.5942, "x": 77.7, "y": 45.2, "source": "geonames", "type": "PPL" }, { "name": "Mbombe", "latitude": 4.9788, "longitude": 9.3572, "x": 30.5, "y": 45.2, "source": "geonames", "type": "PPL" }, { "name": "Mbonge Meteke I", "latitude": 4.7945, "longitude": 9.3869, "x": 36.4, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Mbu", "latitude": 5.0389, "longitude": 9.3196, "x": 23.1, "y": 30.6, "source": "geonames", "type": "PPL" }, { "name": "Mekoli", "latitude": 4.91667, "longitude": 9.28333, "x": 15.8, "y": 60.3, "source": "geonames", "type": "PPL" }, { "name": "Mekom", "latitude": 4.9439, "longitude": 9.5351, "x": 65.9, "y": 53.7, "source": "geonames", "type": "PPL" }, { "name": "Menyum", "latitude": 5.0088, "longitude": 9.6136, "x": 81.5, "y": 37.9, "source": "geonames", "type": "PPL" }, { "name": "Messaka", "latitude": 4.9799, "longitude": 9.6052, "x": 79.9, "y": 44.9, "source": "geonames", "type": "PPL" }, { "name": "Moboka Village", "latitude": 4.8502, "longitude": 9.43215, "x": 45.4, "y": 76.5, "source": "geonames", "type": "PPL" }, { "name": "Molongo", "latitude": 4.9224, "longitude": 9.5653, "x": 71.9, "y": 58.9, "source": "geonames", "type": "PPL" }, { "name": "Ndikolo", "latitude": 4.9375, "longitude": 9.3825, "x": 35.6, "y": 55.2, "source": "geonames", "type": "PPL" }, { "name": "Ndoi Bakundu", "latitude": 4.9281, "longitude": 9.4751, "x": 54, "y": 57.5, "source": "geonames", "type": "PPL" }, { "name": "Ndor", "latitude": 4.88333, "longitude": 9.43333, "x": 45.7, "y": 68.4, "source": "geonames", "type": "PPL" }, { "name": "New Difenda", "latitude": 4.9393, "longitude": 9.373, "x": 33.7, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Nkwentor", "latitude": 5.0736, "longitude": 9.3955, "x": 38.2, "y": 22.2, "source": "geonames", "type": "PPL" }, { "name": "Nongomadiba", "latitude": 5.0227, "longitude": 9.444, "x": 47.8, "y": 34.5, "source": "geonames", "type": "PPL" }, { "name": "Nyale", "latitude": 4.9957, "longitude": 9.643, "x": 87.4, "y": 41.1, "source": "geonames", "type": "PPL" }, { "name": "Nyandong", "latitude": 4.9627, "longitude": 9.579, "x": 74.7, "y": 49.1, "source": "geonames", "type": "PPL" }, { "name": "Old Ifanga", "latitude": 4.9402, "longitude": 9.2818, "x": 15.5, "y": 54.6, "source": "geonames", "type": "PPL" }, { "name": "Small Sofo", "latitude": 4.9274, "longitude": 9.3144, "x": 22, "y": 57.7, "source": "geonames", "type": "PPL" }, { "name": "Supe", "latitude": 5.01299, "longitude": 9.42069, "x": 43.2, "y": 36.9, "source": "geonames", "type": "PPL" }, { "name": "Weme", "latitude": 4.8763, "longitude": 9.4337, "x": 45.8, "y": 70.1, "source": "geonames", "type": "PPL" }, { "name": "Wone", "latitude": 5.0549, "longitude": 9.3937, "x": 37.8, "y": 26.7, "source": "geonames", "type": "PPL" } ], "Kousseri": [ { "name": "Abendourwa", "latitude": 11.98889, "longitude": 14.98028, "x": 60.7, "y": 63.9, "source": "geonames", "type": "PPL" }, { "name": "Aboukanou", "latitude": 12.10136, "longitude": 14.90112, "x": 36.6, "y": 38, "source": "geonames", "type": "PPL" }, { "name": "Adjain", "latitude": 12.08333, "longitude": 14.93333, "x": 46.4, "y": 42.1, "source": "geonames", "type": "PPL" }, { "name": "Alarke", "latitude": 11.87555, "longitude": 14.99195, "x": 64.3, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Alaya", "latitude": 12.04298, "longitude": 14.93183, "x": 46, "y": 51.4, "source": "geonames", "type": "PPL" }, { "name": "Am Chedire", "latitude": 12.05648, "longitude": 15.01742, "x": 72.1, "y": 48.3, "source": "geonames", "type": "PPL" }, { "name": "Amalgoss", "latitude": 11.95, "longitude": 15.01667, "x": 71.8, "y": 72.8, "source": "geonames", "type": "PPL" }, { "name": "Androuman", "latitude": 12.2002, "longitude": 14.87777, "x": 29.5, "y": 15.2, "source": "geonames", "type": "PPL" }, { "name": "Ardebe", "latitude": 12.05086, "longitude": 15.05089, "x": 82.3, "y": 49.6, "source": "geonames", "type": "PPL" }, { "name": "Bala", "latitude": 12.12499, "longitude": 14.86997, "x": 27.1, "y": 32.5, "source": "geonames", "type": "PPL" }, { "name": "Bamcherafa", "latitude": 12.08821, "longitude": 14.88717, "x": 32.4, "y": 41, "source": "geonames", "type": "PPL" }, { "name": "Blabline", "latitude": 11.91193, "longitude": 15.00971, "x": 69.7, "y": 81.6, "source": "geonames", "type": "PPL" }, { "name": "Danguerchem", "latitude": 11.95194, "longitude": 14.95082, "x": 51.8, "y": 72.4, "source": "geonames", "type": "PPL" }, { "name": "Djidat", "latitude": 12.02042, "longitude": 14.99068, "x": 63.9, "y": 56.6, "source": "geonames", "type": "PPL" }, { "name": "El Birke", "latitude": 12.00647, "longitude": 14.9874, "x": 62.9, "y": 59.8, "source": "geonames", "type": "PPL" }, { "name": "Frohiri", "latitude": 12.11077, "longitude": 14.84451, "x": 19.4, "y": 35.8, "source": "geonames", "type": "PPL" }, { "name": "Gobrem", "latitude": 12.06282, "longitude": 14.95483, "x": 53, "y": 46.9, "source": "geonames", "type": "PPL" }, { "name": "Herazaya", "latitude": 12.08017, "longitude": 15.0169, "x": 71.9, "y": 42.9, "source": "geonames", "type": "PPL" }, { "name": "Hileguim", "latitude": 12.19567, "longitude": 14.88775, "x": 32.6, "y": 16.3, "source": "geonames", "type": "PPL" }, { "name": "Ibou", "latitude": 12.0501, "longitude": 15.01591, "x": 71.6, "y": 49.8, "source": "geonames", "type": "PPL" }, { "name": "Kabe", "latitude": 12.00142, "longitude": 15.07633, "x": 90, "y": 61, "source": "geonames", "type": "PPL" }, { "name": "Kala Kafra", "latitude": 12.09334, "longitude": 14.84061, "x": 18.2, "y": 39.8, "source": "geonames", "type": "PPL" }, { "name": "Kala-Mouloue", "latitude": 12.16842, "longitude": 14.88104, "x": 30.5, "y": 22.5, "source": "geonames", "type": "PPL" }, { "name": "Karena", "latitude": 12.10781, "longitude": 14.83925, "x": 17.8, "y": 36.5, "source": "geonames", "type": "PPL" }, { "name": "Koulkoule", "latitude": 11.97529, "longitude": 14.94624, "x": 50.4, "y": 67, "source": "geonames", "type": "PPL" }, { "name": "Koumboula", "latitude": 12.02739, "longitude": 15.02407, "x": 74.1, "y": 55, "source": "geonames", "type": "PPL" }, { "name": "Lakta", "latitude": 12.10158, "longitude": 14.90426, "x": 37.6, "y": 37.9, "source": "geonames", "type": "PPL" }, { "name": "Madaf", "latitude": 12.02134, "longitude": 14.93349, "x": 46.5, "y": 56.4, "source": "geonames", "type": "PPL" }, { "name": "Madardem", "latitude": 12.13204, "longitude": 14.8906, "x": 33.4, "y": 30.9, "source": "geonames", "type": "PPL" }, { "name": "Madouba", "latitude": 12.2, "longitude": 14.86667, "x": 26.1, "y": 15.3, "source": "geonames", "type": "PPL" }, { "name": "Magourde I", "latitude": 12.02402, "longitude": 14.94642, "x": 50.4, "y": 55.8, "source": "geonames", "type": "PPL" }, { "name": "Mahana", "latitude": 12.04649, "longitude": 14.91448, "x": 40.7, "y": 50.6, "source": "geonames", "type": "PPL" }, { "name": "Malal", "latitude": 12.11667, "longitude": 14.85, "x": 21.1, "y": 34.5, "source": "geonames", "type": "PPL" }, { "name": "Mara I", "latitude": 12.22004, "longitude": 14.90391, "x": 37.5, "y": 10.6, "source": "geonames", "type": "PPL" }, { "name": "Mara II", "latitude": 12.22058, "longitude": 14.89851, "x": 35.8, "y": 10.5, "source": "geonames", "type": "PPL" }, { "name": "Marafin", "latitude": 12.00673, "longitude": 15.00291, "x": 67.6, "y": 59.8, "source": "geonames", "type": "PPL" }, { "name": "Marako", "latitude": 12.08937, "longitude": 14.96113, "x": 54.9, "y": 40.7, "source": "geonames", "type": "PPL" }, { "name": "Massaki", "latitude": 12.08315, "longitude": 14.98788, "x": 63.1, "y": 42.2, "source": "geonames", "type": "PPL" }, { "name": "Massilal Er", "latitude": 12.12146, "longitude": 14.91174, "x": 39.9, "y": 33.4, "source": "geonames", "type": "PPL" }, { "name": "Mildi", "latitude": 11.9114, "longitude": 14.95291, "x": 52.4, "y": 81.7, "source": "geonames", "type": "PPL" }, { "name": "Mouladok", "latitude": 12.05995, "longitude": 14.82922, "x": 14.7, "y": 47.5, "source": "geonames", "type": "PPL" }, { "name": "Mounkeu", "latitude": 11.88971, "longitude": 14.99652, "x": 65.7, "y": 86.7, "source": "geonames", "type": "PPL" }, { "name": "Mour", "latitude": 12.09108, "longitude": 14.84434, "x": 19.3, "y": 40.4, "source": "geonames", "type": "PPL" }, { "name": "Naga", "latitude": 12.09895, "longitude": 14.94033, "x": 48.6, "y": 38.5, "source": "geonames", "type": "PPL" }, { "name": "Ndouf", "latitude": 12.03721, "longitude": 15.04369, "x": 80.1, "y": 52.8, "source": "geonames", "type": "PPL" }, { "name": "Ndoum", "latitude": 12.05, "longitude": 14.88333, "x": 31.2, "y": 49.8, "source": "geonames", "type": "PPL" }, { "name": "Ngouama", "latitude": 11.92666, "longitude": 15.05407, "x": 83.2, "y": 78.2, "source": "geonames", "type": "PPL" }, { "name": "Ngoumati", "latitude": 11.94526, "longitude": 14.93333, "x": 46.4, "y": 73.9, "source": "geonames", "type": "PPL" }, { "name": "Ngout", "latitude": 12.12513, "longitude": 14.81372, "x": 10, "y": 32.5, "source": "geonames", "type": "PPL" }, { "name": "Ngree", "latitude": 12.13741, "longitude": 14.84361, "x": 19.1, "y": 29.7, "source": "geonames", "type": "PPL" }, { "name": "Njagare", "latitude": 12.03485, "longitude": 15.0149, "x": 71.3, "y": 53.3, "source": "geonames", "type": "PPL" }, { "name": "Ouadje Touna", "latitude": 12.03333, "longitude": 14.96667, "x": 56.6, "y": 53.7, "source": "geonames", "type": "PPL" }, { "name": "Oulouf", "latitude": 12.03602, "longitude": 14.94449, "x": 49.8, "y": 53, "source": "geonames", "type": "PPL" }, { "name": "Razat", "latitude": 12.21413, "longitude": 14.87227, "x": 27.8, "y": 12, "source": "geonames", "type": "PPL" }, { "name": "Sabla", "latitude": 11.92194, "longitude": 14.96611, "x": 56.4, "y": 79.3, "source": "geonames", "type": "PPL" }, { "name": "Sedik", "latitude": 11.9264, "longitude": 14.94304, "x": 49.4, "y": 78.3, "source": "geonames", "type": "PPL" }, { "name": "Sero", "latitude": 12.1016, "longitude": 14.86654, "x": 26.1, "y": 37.9, "source": "geonames", "type": "PPL" }, { "name": "Tamraya", "latitude": 12.07526, "longitude": 14.98284, "x": 61.5, "y": 44, "source": "geonames", "type": "PPL" }, { "name": "Toue", "latitude": 12.22283, "longitude": 14.85734, "x": 23.3, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Wadjetouna", "latitude": 12.03741, "longitude": 14.99114, "x": 64, "y": 52.7, "source": "geonames", "type": "PPL" } ], "Kribi": [ { "name": "Akom", "latitude": 3.05, "longitude": 10.05, "x": 72.9, "y": 15.6, "source": "geonames", "type": "PPL" }, { "name": "Allende", "latitude": 2.91667, "longitude": 9.95, "x": 38.6, "y": 45.3, "source": "geonames", "type": "PPL" }, { "name": "Bac", "latitude": 2.88333, "longitude": 9.9, "x": 21.4, "y": 52.8, "source": "geonames", "type": "PPL" }, { "name": "Bebamboue", "latitude": 3.05, "longitude": 9.96667, "x": 44.3, "y": 15.6, "source": "geonames", "type": "PPL" }, { "name": "Beka", "latitude": 2.86667, "longitude": 9.91667, "x": 27.1, "y": 56.5, "source": "geonames", "type": "PPL" }, { "name": "Bidou", "latitude": 2.85, "longitude": 9.98333, "x": 50, "y": 60.2, "source": "geonames", "type": "PPL" }, { "name": "Bidou I", "latitude": 3.01667, "longitude": 10.1, "x": 90, "y": 23, "source": "geonames", "type": "PPL" }, { "name": "Bidou II", "latitude": 2.85, "longitude": 10.01667, "x": 61.4, "y": 60.2, "source": "geonames", "type": "PPL" }, { "name": "Bikondo", "latitude": 2.98333, "longitude": 9.95, "x": 38.6, "y": 30.4, "source": "geonames", "type": "PPL" }, { "name": "Bisyang", "latitude": 3, "longitude": 10.01667, "x": 61.4, "y": 26.7, "source": "geonames", "type": "PPL" }, { "name": "Bouambe", "latitude": 2.88333, "longitude": 9.9, "x": 21.4, "y": 52.8, "source": "geonames", "type": "PPL" }, { "name": "Diboune", "latitude": 2.83333, "longitude": 9.91667, "x": 27.1, "y": 63.9, "source": "geonames", "type": "PPL" }, { "name": "Dombe", "latitude": 2.95, "longitude": 9.91667, "x": 27.1, "y": 37.9, "source": "geonames", "type": "PPL" }, { "name": "Ebome", "latitude": 2.9, "longitude": 9.9, "x": 21.4, "y": 49.1, "source": "geonames", "type": "PPL" }, { "name": "Ebounja", "latitude": 2.8, "longitude": 9.9, "x": 21.4, "y": 71.4, "source": "geonames", "type": "PPL" }, { "name": "Elabi", "latitude": 2.98217, "longitude": 9.9216, "x": 28.8, "y": 30.7, "source": "geonames", "type": "PPL" }, { "name": "Grand Batanga", "latitude": 2.84909, "longitude": 9.88761, "x": 17.2, "y": 60.4, "source": "geonames", "type": "PPL" }, { "name": "Ilende", "latitude": 2.76667, "longitude": 9.88333, "x": 15.7, "y": 78.8, "source": "geonames", "type": "PPL" }, { "name": "Linde", "latitude": 2.91667, "longitude": 9.93333, "x": 32.9, "y": 45.3, "source": "geonames", "type": "PPL" }, { "name": "Lobe", "latitude": 2.88333, "longitude": 9.88333, "x": 15.7, "y": 52.8, "source": "geonames", "type": "PPL" }, { "name": "Longji", "latitude": 3.07487, "longitude": 9.97396, "x": 46.8, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Mabenange", "latitude": 2.8, "longitude": 9.95, "x": 38.6, "y": 71.4, "source": "geonames", "type": "PPL" }, { "name": "Mbode", "latitude": 2.71667, "longitude": 9.86667, "x": 10, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Miangadjo", "latitude": 2.9, "longitude": 9.95, "x": 38.6, "y": 49.1, "source": "geonames", "type": "PPL" }, { "name": "Mokonjo", "latitude": 3.05944, "longitude": 9.97194, "x": 46.1, "y": 13.4, "source": "geonames", "type": "PPL" }, { "name": "Ndaumale", "latitude": 2.85, "longitude": 9.9, "x": 21.4, "y": 60.2, "source": "geonames", "type": "PPL" }, { "name": "Ngole", "latitude": 2.85, "longitude": 10.08333, "x": 84.3, "y": 60.2, "source": "geonames", "type": "PPL" }, { "name": "Nkolbonda", "latitude": 2.8, "longitude": 10.03333, "x": 67.1, "y": 71.4, "source": "geonames", "type": "PPL" }, { "name": "Nkolong", "latitude": 2.85, "longitude": 10.05, "x": 72.9, "y": 60.2, "source": "geonames", "type": "PPL" }, { "name": "Pongo", "latitude": 2.86667, "longitude": 9.98333, "x": 50, "y": 56.5, "source": "geonames", "type": "PPL" }, { "name": "Talaa", "latitude": 2.93333, "longitude": 9.91667, "x": 27.1, "y": 41.6, "source": "geonames", "type": "PPL" } ], "Kumba": [ { "name": "Bai Bikom", "latitude": 4.5551, "longitude": 9.3344, "x": 36.7, "y": 72.2, "source": "geonames", "type": "PPL" }, { "name": "Bai Manya", "latitude": 4.5211, "longitude": 9.3005, "x": 26.7, "y": 80.9, "source": "geonames", "type": "PPL" }, { "name": "Bai Panya", "latitude": 4.5397, "longitude": 9.3308, "x": 35.6, "y": 76.1, "source": "geonames", "type": "PPL" }, { "name": "Bakossi", "latitude": 4.6604, "longitude": 9.5122, "x": 89, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Bakumba", "latitude": 4.7729, "longitude": 9.2816, "x": 21.2, "y": 16, "source": "geonames", "type": "PPL" }, { "name": "Bangale", "latitude": 4.6697, "longitude": 9.2504, "x": 12, "y": 42.6, "source": "geonames", "type": "PPL" }, { "name": "Barombi Kang", "latitude": 4.6074, "longitude": 9.4577, "x": 73, "y": 58.7, "source": "geonames", "type": "PPL" }, { "name": "Barombi Koto", "latitude": 4.4971, "longitude": 9.278, "x": 20.1, "y": 87.1, "source": "geonames", "type": "PPL" }, { "name": "Barombi Mbo", "latitude": 4.6709, "longitude": 9.3924, "x": 53.8, "y": 42.3, "source": "geonames", "type": "PPL" }, { "name": "Bekili", "latitude": 4.6172, "longitude": 9.5114, "x": 88.8, "y": 56.2, "source": "geonames", "type": "PPL" }, { "name": "Bekondo", "latitude": 4.6819, "longitude": 9.3214, "x": 32.9, "y": 39.5, "source": "geonames", "type": "PPL" }, { "name": "Bikoki", "latitude": 4.7459, "longitude": 9.2851, "x": 22.2, "y": 23, "source": "geonames", "type": "PPL" }, { "name": "Boa", "latitude": 4.6249, "longitude": 9.3017, "x": 27.1, "y": 54.2, "source": "geonames", "type": "PPL" }, { "name": "Bole", "latitude": 4.54967, "longitude": 9.25413, "x": 13.1, "y": 73.6, "source": "geonames", "type": "PPL" }, { "name": "Bombanda", "latitude": 4.6343, "longitude": 9.2755, "x": 19.4, "y": 51.7, "source": "geonames", "type": "PPL" }, { "name": "Bombele", "latitude": 4.6507, "longitude": 9.2602, "x": 14.9, "y": 47.5, "source": "geonames", "type": "PPL" }, { "name": "Bopo", "latitude": 4.486, "longitude": 9.3889, "x": 52.7, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Butu", "latitude": 4.7812, "longitude": 9.3254, "x": 34, "y": 13.8, "source": "geonames", "type": "PPL" }, { "name": "Dchang", "latitude": 4.6178, "longitude": 9.4804, "x": 79.6, "y": 56, "source": "geonames", "type": "PPL" }, { "name": "Dissoni", "latitude": 4.7006, "longitude": 9.2898, "x": 23.6, "y": 34.6, "source": "geonames", "type": "PPL" }, { "name": "Dissosso", "latitude": 4.7319, "longitude": 9.2573, "x": 14, "y": 26.6, "source": "geonames", "type": "PPL" }, { "name": "Ediki", "latitude": 4.54067, "longitude": 9.46422, "x": 74.9, "y": 75.9, "source": "geonames", "type": "PPL" }, { "name": "Ekombe Bonji", "latitude": 4.6055, "longitude": 9.3589, "x": 43.9, "y": 59.2, "source": "geonames", "type": "PPL" }, { "name": "Fiango", "latitude": 4.6273, "longitude": 9.45, "x": 70.7, "y": 53.5, "source": "geonames", "type": "PPL" }, { "name": "Ikiliwindi", "latitude": 4.7317, "longitude": 9.4881, "x": 81.9, "y": 26.6, "source": "geonames", "type": "PPL" }, { "name": "Kake", "latitude": 4.6187, "longitude": 9.4128, "x": 59.8, "y": 55.8, "source": "geonames", "type": "PPL" }, { "name": "Kendonge", "latitude": 4.5387, "longitude": 9.4228, "x": 62.7, "y": 76.4, "source": "geonames", "type": "PPL" }, { "name": "Kombone", "latitude": 4.5832, "longitude": 9.3061, "x": 28.4, "y": 64.9, "source": "geonames", "type": "PPL" }, { "name": "Kombone Mission", "latitude": 4.606, "longitude": 9.3318, "x": 35.9, "y": 59, "source": "geonames", "type": "PPL" }, { "name": "Kumukumu", "latitude": 4.6541, "longitude": 9.2437, "x": 10, "y": 46.6, "source": "geonames", "type": "PPL" }, { "name": "Lipenja", "latitude": 4.7961, "longitude": 9.3254, "x": 34, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Mabonji", "latitude": 4.5715, "longitude": 9.4785, "x": 79.1, "y": 67.9, "source": "geonames", "type": "PPL" }, { "name": "Makobe", "latitude": 4.7802, "longitude": 9.3945, "x": 54.4, "y": 14.1, "source": "geonames", "type": "PPL" }, { "name": "Malende", "latitude": 4.5991, "longitude": 9.4922, "x": 83.1, "y": 60.8, "source": "geonames", "type": "PPL" }, { "name": "Mambanda", "latitude": 4.6442, "longitude": 9.4881, "x": 81.9, "y": 49.2, "source": "geonames", "type": "PPL" }, { "name": "Mambanda Camp", "latitude": 4.6357, "longitude": 9.5078, "x": 87.7, "y": 51.4, "source": "geonames", "type": "PPL" }, { "name": "Marumba I", "latitude": 4.5899, "longitude": 9.3438, "x": 39.5, "y": 63.2, "source": "geonames", "type": "PPL" }, { "name": "Marumba II", "latitude": 4.5709, "longitude": 9.343, "x": 39.2, "y": 68.1, "source": "geonames", "type": "PPL" }, { "name": "Massaka I", "latitude": 4.6911, "longitude": 9.2931, "x": 24.5, "y": 37.1, "source": "geonames", "type": "PPL" }, { "name": "Mator Butu", "latitude": 4.7627, "longitude": 9.3991, "x": 55.7, "y": 18.6, "source": "geonames", "type": "PPL" }, { "name": "Mbonge-Meteke II", "latitude": 4.7443, "longitude": 9.4272, "x": 64, "y": 23.4, "source": "geonames", "type": "PPL" }, { "name": "Mofako Bekondo", "latitude": 4.6293, "longitude": 9.338, "x": 37.7, "y": 53, "source": "geonames", "type": "PPL" }, { "name": "Mukonje", "latitude": 4.5776, "longitude": 9.5067, "x": 87.4, "y": 66.4, "source": "geonames", "type": "PPL" }, { "name": "Nake Bongwana", "latitude": 4.5687, "longitude": 9.2846, "x": 22, "y": 68.7, "source": "geonames", "type": "PPL" }, { "name": "New Butu", "latitude": 4.7363, "longitude": 9.3244, "x": 33.7, "y": 25.4, "source": "geonames", "type": "PPL" }, { "name": "Palabongo", "latitude": 4.5921, "longitude": 9.5156, "x": 90, "y": 62.6, "source": "geonames", "type": "PPL" }, { "name": "Pesikombe", "latitude": 4.7181, "longitude": 9.4738, "x": 77.7, "y": 30.1, "source": "geonames", "type": "PPL" }, { "name": "Pete", "latitude": 4.5382, "longitude": 9.3666, "x": 46.2, "y": 76.5, "source": "geonames", "type": "PPL" }, { "name": "Small Ekombe", "latitude": 4.6126, "longitude": 9.3814, "x": 50.5, "y": 57.3, "source": "geonames", "type": "PPL" }, { "name": "Small Ngwandi", "latitude": 4.7171, "longitude": 9.2475, "x": 11.1, "y": 30.4, "source": "geonames", "type": "PPL" }, { "name": "Three Corners", "latitude": 4.6332, "longitude": 9.4718, "x": 77.1, "y": 52, "source": "geonames", "type": "PPL" }, { "name": "Three Corners Bekondo", "latitude": 4.6575, "longitude": 9.3238, "x": 33.6, "y": 45.8, "source": "geonames", "type": "PPL" } ], "Kumbo": [ { "name": "Elom", "latitude": 6.3, "longitude": 10.6, "x": 31.3, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Kishong", "latitude": 6.23333, "longitude": 10.73333, "x": 74, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Mbiam", "latitude": 6.16667, "longitude": 10.78333, "x": 90, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ndun", "latitude": 6.28333, "longitude": 10.75, "x": 79.3, "y": 20, "source": "geonames", "type": "PPL" }, { "name": "Njotin", "latitude": 6.25, "longitude": 10.53333, "x": 10, "y": 40, "source": "geonames", "type": "PPL" } ], "Kye-Ossi": [ { "name": "Adjou", "latitude": 2.26667, "longitude": 11.36667, "x": 30, "y": 31.1, "source": "geonames", "type": "PPL" }, { "name": "Akom Bong", "latitude": 2.18333, "longitude": 11.35, "x": 23.3, "y": 84, "source": "geonames", "type": "PPL" }, { "name": "Akonangui", "latitude": 2.2, "longitude": 11.35, "x": 23.3, "y": 73.4, "source": "geonames", "type": "PPL" }, { "name": "Alen", "latitude": 2.21667, "longitude": 11.36667, "x": 30, "y": 62.8, "source": "geonames", "type": "PPL" }, { "name": "Bindong", "latitude": 2.18333, "longitude": 11.31667, "x": 10, "y": 84, "source": "geonames", "type": "PPL" }, { "name": "Douanes", "latitude": 2.2, "longitude": 11.35, "x": 23.3, "y": 73.4, "source": "geonames", "type": "PPL" }, { "name": "Eking", "latitude": 2.3, "longitude": 11.45, "x": 63.3, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Fenete", "latitude": 2.2, "longitude": 11.35, "x": 23.3, "y": 73.4, "source": "geonames", "type": "PPL" }, { "name": "Kanafounoussi", "latitude": 2.28333, "longitude": 11.33333, "x": 16.7, "y": 20.6, "source": "geonames", "type": "PPL" }, { "name": "Mefoup", "latitude": 2.23333, "longitude": 11.35, "x": 23.3, "y": 52.3, "source": "geonames", "type": "PPL" }, { "name": "Mekok", "latitude": 2.25, "longitude": 11.35, "x": 23.3, "y": 41.7, "source": "geonames", "type": "PPL" }, { "name": "Messi Messi", "latitude": 2.3, "longitude": 11.51667, "x": 90, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Metet", "latitude": 2.18333, "longitude": 11.33333, "x": 16.7, "y": 84, "source": "geonames", "type": "PPL" }, { "name": "Meyo-Mengo", "latitude": 2.26667, "longitude": 11.35, "x": 23.3, "y": 31.1, "source": "geonames", "type": "PPL" }, { "name": "Ngoazik", "latitude": 2.28333, "longitude": 11.33333, "x": 16.7, "y": 20.6, "source": "geonames", "type": "PPL" } ], "Lagdo": [ { "name": "Adoumri", "latitude": 9.25296, "longitude": 13.77435, "x": 66.7, "y": 14, "source": "geonames", "type": "PPL" }, { "name": "Badeo", "latitude": 9.25062, "longitude": 13.69766, "x": 50.5, "y": 14.4, "source": "geonames", "type": "PPL" }, { "name": "Bakatouge", "latitude": 8.90839, "longitude": 13.56795, "x": 23.3, "y": 81.8, "source": "geonames", "type": "PPL" }, { "name": "Bakka", "latitude": 9.27323, "longitude": 13.7093, "x": 53, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Bakona", "latitude": 8.97794, "longitude": 13.59568, "x": 29.1, "y": 68.1, "source": "geonames", "type": "PPL" }, { "name": "Balda", "latitude": 9.13221, "longitude": 13.84634, "x": 81.8, "y": 37.7, "source": "geonames", "type": "PPL" }, { "name": "Bame", "latitude": 9.07754, "longitude": 13.59794, "x": 29.6, "y": 48.5, "source": "geonames", "type": "PPL" }, { "name": "Bamtsi", "latitude": 8.95, "longitude": 13.71667, "x": 54.5, "y": 73.6, "source": "geonames", "type": "PPL" }, { "name": "Bessoum", "latitude": 9.13561, "longitude": 13.71521, "x": 54.2, "y": 37.1, "source": "geonames", "type": "PPL" }, { "name": "Bira Hammadou", "latitude": 8.94659, "longitude": 13.85818, "x": 84.3, "y": 74.3, "source": "geonames", "type": "PPL" }, { "name": "Bongi", "latitude": 8.9, "longitude": 13.81667, "x": 75.6, "y": 83.4, "source": "geonames", "type": "PPL" }, { "name": "Boukadji", "latitude": 9.03415, "longitude": 13.83406, "x": 79.2, "y": 57, "source": "geonames", "type": "PPL" }, { "name": "Dangdere", "latitude": 9.09588, "longitude": 13.86247, "x": 85.2, "y": 44.9, "source": "geonames", "type": "PPL" }, { "name": "Djabire", "latitude": 8.97831, "longitude": 13.8074, "x": 73.6, "y": 68, "source": "geonames", "type": "PPL" }, { "name": "Djanga", "latitude": 9.12309, "longitude": 13.67892, "x": 46.6, "y": 39.5, "source": "geonames", "type": "PPL" }, { "name": "Djeneo", "latitude": 9.19647, "longitude": 13.75362, "x": 62.3, "y": 25.1, "source": "geonames", "type": "PPL" }, { "name": "Djougoundou", "latitude": 9.15856, "longitude": 13.86485, "x": 85.7, "y": 32.6, "source": "geonames", "type": "PPL" }, { "name": "Djoungoundou", "latitude": 9.21465, "longitude": 13.80737, "x": 73.6, "y": 21.5, "source": "geonames", "type": "PPL" }, { "name": "Dougue", "latitude": 9.24155, "longitude": 13.74126, "x": 59.7, "y": 16.2, "source": "geonames", "type": "PPL" }, { "name": "Douloumi", "latitude": 9.21094, "longitude": 13.66029, "x": 42.7, "y": 22.3, "source": "geonames", "type": "PPL" }, { "name": "Gamsargou", "latitude": 9.06314, "longitude": 13.82857, "x": 78.1, "y": 51.3, "source": "geonames", "type": "PPL" }, { "name": "Gangoy", "latitude": 8.99353, "longitude": 13.81797, "x": 75.8, "y": 65, "source": "geonames", "type": "PPL" }, { "name": "Gaobara", "latitude": 9.15257, "longitude": 13.50756, "x": 10.6, "y": 33.7, "source": "geonames", "type": "PPL" }, { "name": "Garwawo", "latitude": 8.95452, "longitude": 13.52546, "x": 14.3, "y": 72.7, "source": "geonames", "type": "PPL" }, { "name": "Gonougou", "latitude": 9.08501, "longitude": 13.65403, "x": 41.4, "y": 47, "source": "geonames", "type": "PPL" }, { "name": "Gonougou-Labare", "latitude": 9.07525, "longitude": 13.69596, "x": 50.2, "y": 49, "source": "geonames", "type": "PPL" }, { "name": "Gore", "latitude": 9.17752, "longitude": 13.82844, "x": 78, "y": 28.8, "source": "geonames", "type": "PPL" }, { "name": "Karewa", "latitude": 9.18185, "longitude": 13.56, "x": 21.6, "y": 28, "source": "geonames", "type": "PPL" }, { "name": "Keini", "latitude": 9.21677, "longitude": 13.6214, "x": 34.5, "y": 21.1, "source": "geonames", "type": "PPL" }, { "name": "Kilbao", "latitude": 9.26659, "longitude": 13.72536, "x": 56.4, "y": 11.3, "source": "geonames", "type": "PPL" }, { "name": "Konere", "latitude": 9.04346, "longitude": 13.51033, "x": 11.1, "y": 55.2, "source": "geonames", "type": "PPL" }, { "name": "Labare", "latitude": 9.09016, "longitude": 13.71839, "x": 54.9, "y": 46, "source": "geonames", "type": "PPL" }, { "name": "Lamoudam", "latitude": 8.98137, "longitude": 13.53269, "x": 15.8, "y": 67.4, "source": "geonames", "type": "PPL" }, { "name": "Manawassi", "latitude": 8.99931, "longitude": 13.5198, "x": 13.1, "y": 63.9, "source": "geonames", "type": "PPL" }, { "name": "Mayel Tapare", "latitude": 9.15392, "longitude": 13.80586, "x": 73.3, "y": 33.5, "source": "geonames", "type": "PPL" }, { "name": "Mbara", "latitude": 9.06983, "longitude": 13.88459, "x": 89.8, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Mbengui", "latitude": 8.98939, "longitude": 13.77536, "x": 66.9, "y": 65.9, "source": "geonames", "type": "PPL" }, { "name": "Napanlang", "latitude": 9.08302, "longitude": 13.63358, "x": 37.1, "y": 47.4, "source": "geonames", "type": "PPL" }, { "name": "Nassarao", "latitude": 9.09105, "longitude": 13.88533, "x": 90, "y": 45.8, "source": "geonames", "type": "PPL" }, { "name": "Ndiambadi", "latitude": 9.12023, "longitude": 13.82379, "x": 77.1, "y": 40.1, "source": "geonames", "type": "PPL" }, { "name": "Ndjola", "latitude": 9.09552, "longitude": 13.50491, "x": 10, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Ngong Haoussari", "latitude": 9.02511, "longitude": 13.50967, "x": 11, "y": 58.8, "source": "geonames", "type": "PPL" }, { "name": "Ngongiel", "latitude": 8.86667, "longitude": 13.71667, "x": 54.5, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Oura Bagoni", "latitude": 9.0392, "longitude": 13.88138, "x": 89.2, "y": 56.1, "source": "geonames", "type": "PPL" }, { "name": "Ouro Bah", "latitude": 9.13232, "longitude": 13.83451, "x": 79.3, "y": 37.7, "source": "geonames", "type": "PPL" }, { "name": "Ouro Bali", "latitude": 9.03161, "longitude": 13.86926, "x": 86.6, "y": 57.5, "source": "geonames", "type": "PPL" }, { "name": "Ouro Bobboy", "latitude": 9.15127, "longitude": 13.67991, "x": 46.8, "y": 34, "source": "geonames", "type": "PPL" }, { "name": "Ouro Boro", "latitude": 8.94176, "longitude": 13.73672, "x": 58.7, "y": 75.2, "source": "geonames", "type": "PPL" }, { "name": "Ouro Diggale", "latitude": 9.12554, "longitude": 13.74484, "x": 60.5, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Ouro Donka", "latitude": 9.18428, "longitude": 13.69342, "x": 49.6, "y": 27.5, "source": "geonames", "type": "PPL" }, { "name": "Ouro Doukoudje", "latitude": 9.09765, "longitude": 13.72432, "x": 56.1, "y": 44.5, "source": "geonames", "type": "PPL" }, { "name": "Ouro Douri", "latitude": 9.1342, "longitude": 13.77473, "x": 66.7, "y": 37.4, "source": "geonames", "type": "PPL" }, { "name": "Ouro Gertogal", "latitude": 8.88333, "longitude": 13.71667, "x": 54.5, "y": 86.7, "source": "geonames", "type": "PPL" }, { "name": "Ouro Kio", "latitude": 9.22977, "longitude": 13.72048, "x": 55.3, "y": 18.6, "source": "geonames", "type": "PPL" }, { "name": "Ouro Labo", "latitude": 9.14927, "longitude": 13.57593, "x": 24.9, "y": 34.4, "source": "geonames", "type": "PPL" }, { "name": "Ouro Saidou", "latitude": 8.9775, "longitude": 13.673, "x": 45.3, "y": 68.2, "source": "geonames", "type": "PPL" }, { "name": "Ouro Sala", "latitude": 9.07188, "longitude": 13.83083, "x": 78.5, "y": 49.6, "source": "geonames", "type": "PPL" }, { "name": "Pitoa", "latitude": 9.17869, "longitude": 13.72106, "x": 55.5, "y": 28.6, "source": "geonames", "type": "PPL" }, { "name": "Riao", "latitude": 9.10586, "longitude": 13.6855, "x": 48, "y": 42.9, "source": "geonames", "type": "PPL" }, { "name": "Souki", "latitude": 9.20417, "longitude": 13.74343, "x": 60.2, "y": 23.6, "source": "geonames", "type": "PPL" }, { "name": "Soulmaki", "latitude": 9.24732, "longitude": 13.713, "x": 53.8, "y": 15.1, "source": "geonames", "type": "PPL" }, { "name": "Tchapoma", "latitude": 8.9831, "longitude": 13.84821, "x": 82.2, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Tchikali", "latitude": 9.06421, "longitude": 13.84528, "x": 81.6, "y": 51.1, "source": "geonames", "type": "PPL" }, { "name": "Teware", "latitude": 9.07368, "longitude": 13.84604, "x": 81.7, "y": 49.3, "source": "geonames", "type": "PPL" }, { "name": "Tongo", "latitude": 8.91837, "longitude": 13.51012, "x": 11.1, "y": 79.8, "source": "geonames", "type": "PPL" }, { "name": "Wafango", "latitude": 8.9839, "longitude": 13.69857, "x": 50.7, "y": 66.9, "source": "geonames", "type": "PPL" }, { "name": "Yalango", "latitude": 9.11586, "longitude": 13.86258, "x": 85.2, "y": 41, "source": "geonames", "type": "PPL" } ], "Limbe": [ { "name": "Ambas Bay", "latitude": 4.017, "longitude": 9.2042856, "x": 60.8, "y": 44.7, "source": "osm", "type": "suburb" }, { "name": "Aminatou", "latitude": 4.0170483, "longitude": 9.1687891, "x": 51.9, "y": 44.7, "source": "osm", "type": "locality" }, { "name": "Bahai Land", "latitude": 4.0277066, "longitude": 9.214663, "x": 63.3, "y": 39.1, "source": "osm", "type": "locality" }, { "name": "Bakingili", "latitude": 4.06517, "longitude": 9.03414, "x": 18.5, "y": 19.5, "source": "geonames", "type": "PPL" }, { "name": "Batoke", "latitude": 4.02836, "longitude": 9.09994, "x": 34.8, "y": 38.8, "source": "geonames", "type": "PPL" }, { "name": "Bimbia", "latitude": 3.95787, "longitude": 9.25152, "x": 72.5, "y": 75.7, "source": "geonames", "type": "PPL" }, { "name": "Boanda", "latitude": 4.0522, "longitude": 9.1533, "x": 48.1, "y": 26.3, "source": "geonames", "type": "PPL" }, { "name": "Bobende", "latitude": 4.0143941, "longitude": 9.1515262, "x": 47.6, "y": 46.1, "source": "osm", "type": "locality" }, { "name": "Bobende Layaout", "latitude": 4.0201237, "longitude": 9.153677, "x": 48.2, "y": 43.1, "source": "osm", "type": "locality" }, { "name": "Bona Ngombe", "latitude": 3.9661652, "longitude": 9.2554742, "x": 73.5, "y": 71.3, "source": "osm", "type": "locality" }, { "name": "Bonabile", "latitude": 3.9588204, "longitude": 9.2515874, "x": 72.5, "y": 75.2, "source": "osm", "type": "village" }, { "name": "Bonadikombo", "latitude": 4.0579829, "longitude": 9.2240578, "x": 65.7, "y": 23.3, "source": "osm", "type": "locality" }, { "name": "Bonenza", "latitude": 4.0459, "longitude": 9.0782, "x": 29.4, "y": 29.6, "source": "geonames", "type": "PPL" }, { "name": "Bonjo", "latitude": 3.9948494, "longitude": 9.216789, "x": 63.9, "y": 56.3, "source": "osm", "type": "village" }, { "name": "Bonjongo", "latitude": 4.0659442, "longitude": 9.1764685, "x": 53.8, "y": 19.1, "source": "osm", "type": "village" }, { "name": "Bota", "latitude": 4.0124181, "longitude": 9.1879428, "x": 56.7, "y": 47.1, "source": "osm", "type": "locality" }, { "name": "Bota Land", "latitude": 4.0106732, "longitude": 9.1763407, "x": 53.8, "y": 48, "source": "osm", "type": "suburb" }, { "name": "Bota Layout", "latitude": 4.0120061, "longitude": 9.1743818, "x": 53.3, "y": 47.3, "source": "osm", "type": "locality" }, { "name": "Botacantin", "latitude": 4.0130454, "longitude": 9.1933035, "x": 58, "y": 46.8, "source": "osm", "type": "village" }, { "name": "Botaland Layout", "latitude": 4.0248203, "longitude": 9.1704918, "x": 52.4, "y": 40.6, "source": "osm", "type": "locality" }, { "name": "Bovia", "latitude": 4.0087, "longitude": 9.1693, "x": 52.1, "y": 49.1, "source": "geonames", "type": "PPL" }, { "name": "Bussumbu", "latitude": 4.0411, "longitude": 9.2122, "x": 62.7, "y": 32.1, "source": "geonames", "type": "PPL" }, { "name": "Bussumbu Layout", "latitude": 4.0410029, "longitude": 9.1970167, "x": 58.9, "y": 32.2, "source": "osm", "type": "locality" }, { "name": "Bussumbu native", "latitude": 4.0432093, "longitude": 9.1910657, "x": 57.5, "y": 31, "source": "osm", "type": "locality" }, { "name": "Camp saker", "latitude": 3.9650007, "longitude": 9.2591344, "x": 74.4, "y": 71.9, "source": "osm", "type": "locality" }, { "name": "Carrefour Alpha Club", "latitude": 4.0248132, "longitude": 9.2147726, "x": 63.4, "y": 40.6, "source": "osm", "type": "neighbourhood" }, { "name": "Casava Farm", "latitude": 4.0195595, "longitude": 9.2098244, "x": 62.1, "y": 43.4, "source": "osm", "type": "locality" }, { "name": "CDC Bota Camp", "latitude": 4.0138479, "longitude": 9.1935348, "x": 58.1, "y": 46.4, "source": "osm", "type": "locality" }, { "name": "CDC Camp 2", "latitude": 3.9911116, "longitude": 9.2748467, "x": 78.3, "y": 58.3, "source": "osm", "type": "locality" }, { "name": "CDC Camp 3", "latitude": 4.0063991, "longitude": 9.2792659, "x": 79.4, "y": 50.3, "source": "osm", "type": "locality" }, { "name": "CDC Camp 4", "latitude": 4.0111291, "longitude": 9.2648101, "x": 75.8, "y": 47.8, "source": "osm", "type": "locality" }, { "name": "Chop Farm", "latitude": 3.9648738, "longitude": 9.2417423, "x": 70.1, "y": 72, "source": "osm", "type": "locality" }, { "name": "Church Street", "latitude": 4.0150145, "longitude": 9.209452, "x": 62, "y": 45.8, "source": "osm", "type": "locality" }, { "name": "Cite SIC", "latitude": 4.0283174, "longitude": 9.1928792, "x": 57.9, "y": 38.8, "source": "osm", "type": "locality" }, { "name": "Cite Sonara", "latitude": 4.020256, "longitude": 9.186129, "x": 56.2, "y": 43, "source": "osm", "type": "locality" }, { "name": "Clerry Quaters", "latitude": 4.0084105, "longitude": 9.2086396, "x": 61.8, "y": 49.2, "source": "osm", "type": "locality" }, { "name": "Coconut Island", "latitude": 4.0137489, "longitude": 9.205578, "x": 61.1, "y": 46.4, "source": "osm", "type": "locality" }, { "name": "Custum House", "latitude": 4.012713, "longitude": 9.1938687, "x": 58.2, "y": 47, "source": "osm", "type": "locality" }, { "name": "Custusm Quarters", "latitude": 4.0161214, "longitude": 9.1941155, "x": 58.2, "y": 45.2, "source": "osm", "type": "locality" }, { "name": "Dikolo", "latitude": 3.9692259, "longitude": 9.2694393, "x": 76.9, "y": 69.7, "source": "osm", "type": "village" }, { "name": "Dikulu", "latitude": 3.96921, "longitude": 9.26879, "x": 76.8, "y": 69.7, "source": "geonames", "type": "PPL" }, { "name": "Dockyard", "latitude": 3.9991412, "longitude": 9.2111779, "x": 62.5, "y": 54.1, "source": "osm", "type": "locality" }, { "name": "Dockyard Creek", "latitude": 4.0004682, "longitude": 9.2148134, "x": 63.4, "y": 53.4, "source": "osm", "type": "locality" }, { "name": "Down Beach", "latitude": 4.000006, "longitude": 9.2114118, "x": 62.5, "y": 53.6, "source": "osm", "type": "locality" }, { "name": "Ebongo", "latitude": 4.0738173, "longitude": 9.2149628, "x": 63.4, "y": 15, "source": "osm", "type": "locality" }, { "name": "Ekonjo", "latitude": 4.0622585, "longitude": 9.1617702, "x": 50.2, "y": 21, "source": "osm", "type": "village" }, { "name": "Essele village", "latitude": 3.9699584, "longitude": 9.2209711, "x": 64.9, "y": 69.3, "source": "osm", "type": "village" }, { "name": "Etumba", "latitude": 4.0409, "longitude": 9.1179, "x": 39.3, "y": 32.2, "source": "geonames", "type": "PPL" }, { "name": "G R A", "latitude": 4.021533, "longitude": 9.190085, "x": 57.2, "y": 42.4, "source": "osm", "type": "locality" }, { "name": "Gardens", "latitude": 4.0145465, "longitude": 9.2025474, "x": 60.3, "y": 46, "source": "osm", "type": "locality" }, { "name": "Gare Routiere de Limbe", "latitude": 4.0590846, "longitude": 9.2329453, "x": 67.9, "y": 22.7, "source": "osm", "type": "bus_station" }, { "name": "half mile", "latitude": 4.0152795, "longitude": 9.2068848, "x": 61.4, "y": 45.6, "source": "osm", "type": "locality" }, { "name": "Hospital Layout", "latitude": 4.026662, "longitude": 9.210428, "x": 62.3, "y": 39.7, "source": "osm", "type": "locality" }, { "name": "Isongo", "latitude": 4.0721, "longitude": 9.0173, "x": 14.3, "y": 15.9, "source": "geonames", "type": "PPL" }, { "name": "Kie Village", "latitude": 4.0123385, "longitude": 9.1795858, "x": 54.6, "y": 47.2, "source": "osm", "type": "village" }, { "name": "King William Town", "latitude": 3.9541725, "longitude": 9.2502254, "x": 72.2, "y": 77.6, "source": "osm", "type": "village" }, { "name": "Krata Native Village", "latitude": 4.0151692, "longitude": 9.1790515, "x": 54.5, "y": 45.7, "source": "osm", "type": "locality" }, { "name": "Ladja", "latitude": 3.9990962, "longitude": 9.2109274, "x": 62.4, "y": 54.1, "source": "osm", "type": "village" }, { "name": "Limbe Camp", "latitude": 4.0250605, "longitude": 9.1992803, "x": 59.5, "y": 40.5, "source": "osm", "type": "locality" }, { "name": "Livanda", "latitude": 4.0283317, "longitude": 9.2129617, "x": 62.9, "y": 38.8, "source": "osm", "type": "neighbourhood" }, { "name": "Lower Boando", "latitude": 4.0410009, "longitude": 9.1472552, "x": 46.6, "y": 32.2, "source": "osm", "type": "hamlet" }, { "name": "Lower Bonjongo", "latitude": 4.0567961, "longitude": 9.1810388, "x": 55, "y": 23.9, "source": "osm", "type": "locality" }, { "name": "Mabeta", "latitude": 4.00222, "longitude": 9.28556, "x": 80.9, "y": 52.5, "source": "geonames", "type": "PPL" }, { "name": "Mabeta Layout", "latitude": 4.0144681, "longitude": 9.223145, "x": 65.4, "y": 46, "source": "osm", "type": "locality" }, { "name": "Man O'War", "latitude": 3.96547, "longitude": 9.22223, "x": 65.2, "y": 71.7, "source": "geonames", "type": "PPL" }, { "name": "Mapanja", "latitude": 4.0806, "longitude": 9.1747, "x": 53.4, "y": 11.4, "source": "geonames", "type": "PPL" }, { "name": "Mawoh", "latitude": 4.0097641, "longitude": 9.2214321, "x": 65, "y": 48.5, "source": "osm", "type": "locality" }, { "name": "Mbasse", "latitude": 4.03333, "longitude": 9.11667, "x": 39, "y": 36.2, "source": "geonames", "type": "PPL" }, { "name": "Mbende", "latitude": 4.0194843, "longitude": 9.2068182, "x": 61.4, "y": 43.4, "source": "osm", "type": "locality" }, { "name": "Mbofi", "latitude": 3.9703093, "longitude": 9.2586252, "x": 74.3, "y": 69.2, "source": "osm", "type": "locality" }, { "name": "Mboku", "latitude": 3.96744, "longitude": 9.2951, "x": 83.3, "y": 70.7, "source": "geonames", "type": "PPL" }, { "name": "Mbomo", "latitude": 3.93051, "longitude": 9.322, "x": 90, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Middle Bonjongo", "latitude": 4.0676887, "longitude": 9.1830879, "x": 55.5, "y": 18.2, "source": "osm", "type": "village" }, { "name": "Middle Farm Camp", "latitude": 4.0230471, "longitude": 9.19332, "x": 58, "y": 41.6, "source": "osm", "type": "locality" }, { "name": "Mile I", "latitude": 4.0225783, "longitude": 9.2103512, "x": 62.3, "y": 41.8, "source": "osm", "type": "locality" }, { "name": "Mile II", "latitude": 4.0338314, "longitude": 9.2113487, "x": 62.5, "y": 35.9, "source": "osm", "type": "locality" }, { "name": "Mile II Extension Layout", "latitude": 4.032838, "longitude": 9.2041414, "x": 60.7, "y": 36.4, "source": "osm", "type": "locality" }, { "name": "Mitondo", "latitude": 3.9917295, "longitude": 9.2177474, "x": 64.1, "y": 58, "source": "osm", "type": "village" }, { "name": "Mobange", "latitude": 4.08333, "longitude": 9, "x": 10, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Mokindi", "latitude": 4.0133956, "longitude": 9.1684623, "x": 51.9, "y": 46.6, "source": "osm", "type": "locality" }, { "name": "Mokindi chief palace", "latitude": 4.0140584, "longitude": 9.1680607, "x": 51.8, "y": 46.3, "source": "osm", "type": "village" }, { "name": "Mokindi Isokolo", "latitude": 4.0196375, "longitude": 9.1704455, "x": 52.3, "y": 43.3, "source": "osm", "type": "locality" }, { "name": "Mokunda", "latitude": 4.048, "longitude": 9.1684, "x": 51.8, "y": 28.5, "source": "geonames", "type": "PPL" }, { "name": "Mokundange", "latitude": 4.016476, "longitude": 9.14504, "x": 46, "y": 45, "source": "osm", "type": "locality" }, { "name": "Moliwe", "latitude": 4.0643727, "longitude": 9.2468741, "x": 71.3, "y": 19.9, "source": "osm", "type": "locality" }, { "name": "Morton Point", "latitude": 3.9980803, "longitude": 9.2046756, "x": 60.9, "y": 54.6, "source": "osm", "type": "locality" }, { "name": "Motowa", "latitude": 4.0038844, "longitude": 9.2189983, "x": 64.4, "y": 51.6, "source": "osm", "type": "locality" }, { "name": "Mukunda", "latitude": 4.0468127, "longitude": 9.1643513, "x": 50.8, "y": 29.1, "source": "osm", "type": "village" }, { "name": "Nambeke Street", "latitude": 4.0221337, "longitude": 9.2091996, "x": 62, "y": 42, "source": "osm", "type": "locality" }, { "name": "Nbamba", "latitude": 3.96667, "longitude": 9.25, "x": 72.1, "y": 71.1, "source": "geonames", "type": "PPL" }, { "name": "New Town", "latitude": 4.0111223, "longitude": 9.2171984, "x": 64, "y": 47.8, "source": "osm", "type": "locality" }, { "name": "Ngeme", "latitude": 4.0154, "longitude": 9.1736, "x": 53.1, "y": 45.6, "source": "geonames", "type": "PPL" }, { "name": "Ngeme Camp", "latitude": 4.0118591, "longitude": 9.1610572, "x": 50, "y": 47.4, "source": "osm", "type": "locality" }, { "name": "Ngeme Native", "latitude": 4.0086948, "longitude": 9.150354, "x": 47.4, "y": 49.1, "source": "osm", "type": "locality" }, { "name": "Ngeme village", "latitude": 4.0131266, "longitude": 9.1563907, "x": 48.9, "y": 46.8, "source": "osm", "type": "village" }, { "name": "Nurse Quarters", "latitude": 4.025011, "longitude": 9.2122446, "x": 62.7, "y": 40.5, "source": "osm", "type": "locality" }, { "name": "Port Authority", "latitude": 4.0019543, "longitude": 9.2116692, "x": 62.6, "y": 52.6, "source": "osm", "type": "locality" }, { "name": "Shipping Office", "latitude": 4.0121529, "longitude": 9.1936463, "x": 58.1, "y": 47.3, "source": "osm", "type": "locality" }, { "name": "Site For RCM", "latitude": 4.000969, "longitude": 9.2120927, "x": 62.7, "y": 53.1, "source": "osm", "type": "locality" }, { "name": "Towe", "latitude": 4.0252349, "longitude": 9.2186436, "x": 64.3, "y": 40.4, "source": "osm", "type": "locality" }, { "name": "Unity Quarters", "latitude": 4.0242705, "longitude": 9.207175, "x": 61.5, "y": 40.9, "source": "osm", "type": "locality" }, { "name": "Upper Boando", "latitude": 4.051894, "longitude": 9.1500674, "x": 47.3, "y": 26.5, "source": "osm", "type": "village" }, { "name": "Upper Bonjongo", "latitude": 4.0666799, "longitude": 9.17496, "x": 53.5, "y": 18.7, "source": "osm", "type": "locality" }, { "name": "Victoria", "latitude": 4.0068, "longitude": 9.20953, "x": 62.1, "y": 50.1, "source": "geonames", "type": "PPLH" }, { "name": "Wharf", "latitude": 4.0114902, "longitude": 9.1934158, "x": 58.1, "y": 47.6, "source": "osm", "type": "locality" }, { "name": "Wovia", "latitude": 4.0088228, "longitude": 9.1677514, "x": 51.7, "y": 49, "source": "osm", "type": "suburb" }, { "name": "Wututu", "latitude": 4.0785209, "longitude": 9.2174867, "x": 64, "y": 12.5, "source": "osm", "type": "locality" } ], "Lolodorf": [ { "name": "Adjap", "latitude": 3.06667, "longitude": 10.75, "x": 50, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Bibia", "latitude": 3.23333, "longitude": 10.7, "x": 39.1, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Bibondi", "latitude": 3.28333, "longitude": 10.66667, "x": 31.8, "y": 42.7, "source": "geonames", "type": "PPL" }, { "name": "Bigambo", "latitude": 3.16667, "longitude": 10.6, "x": 17.3, "y": 68.2, "source": "geonames", "type": "PPL" }, { "name": "Bikala", "latitude": 3.18333, "longitude": 10.56667, "x": 10, "y": 64.5, "source": "geonames", "type": "PPL" }, { "name": "Bikoka", "latitude": 3.26667, "longitude": 10.7, "x": 39.1, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Bikouba", "latitude": 3.15, "longitude": 10.83333, "x": 68.2, "y": 71.8, "source": "geonames", "type": "PPL" }, { "name": "Bikoue", "latitude": 3.33333, "longitude": 10.85, "x": 71.8, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Bikoui", "latitude": 3.25, "longitude": 10.71667, "x": 42.7, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Bongolo I", "latitude": 3.06667, "longitude": 10.88333, "x": 79.1, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Bongolo II", "latitude": 3.08333, "longitude": 10.88333, "x": 79.1, "y": 86.4, "source": "geonames", "type": "PPL" }, { "name": "Ebom", "latitude": 3.08333, "longitude": 10.71667, "x": 42.7, "y": 86.4, "source": "geonames", "type": "PPL" }, { "name": "Elon", "latitude": 3.23333, "longitude": 10.93333, "x": 90, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Elone", "latitude": 3.2, "longitude": 10.81667, "x": 64.5, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Fouloudje", "latitude": 3.4, "longitude": 10.76667, "x": 53.6, "y": 17.3, "source": "geonames", "type": "PPL" }, { "name": "Kaba", "latitude": 3.23333, "longitude": 10.75, "x": 50, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Koubinzik", "latitude": 3.23333, "longitude": 10.83333, "x": 68.2, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Maamenyin", "latitude": 3.06667, "longitude": 10.8, "x": 60.9, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Madong", "latitude": 3.28333, "longitude": 10.76667, "x": 53.6, "y": 42.7, "source": "geonames", "type": "PPL" }, { "name": "Makalate", "latitude": 3.11667, "longitude": 10.68333, "x": 35.5, "y": 79.1, "source": "geonames", "type": "PPL" }, { "name": "Man", "latitude": 3.38333, "longitude": 10.76667, "x": 53.6, "y": 20.9, "source": "geonames", "type": "PPL" }, { "name": "Mbango", "latitude": 3.21667, "longitude": 10.8, "x": 60.9, "y": 57.3, "source": "geonames", "type": "PPL" }, { "name": "Mebande", "latitude": 3.11667, "longitude": 10.85, "x": 71.8, "y": 79.1, "source": "geonames", "type": "PPL" }, { "name": "Melange", "latitude": 3.18333, "longitude": 10.83333, "x": 68.2, "y": 64.5, "source": "geonames", "type": "PPL" }, { "name": "Melombo", "latitude": 3.33333, "longitude": 10.6, "x": 17.3, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Mengale", "latitude": 3.1, "longitude": 10.86667, "x": 75.5, "y": 82.7, "source": "geonames", "type": "PPL" }, { "name": "Mialkougou", "latitude": 3.23333, "longitude": 10.86667, "x": 75.5, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Mili", "latitude": 3.26667, "longitude": 10.75, "x": 50, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Minkan I", "latitude": 3.31667, "longitude": 10.93333, "x": 90, "y": 35.5, "source": "geonames", "type": "PPL" }, { "name": "Minkan II", "latitude": 3.33333, "longitude": 10.9, "x": 82.7, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Mouge", "latitude": 3.23333, "longitude": 10.63333, "x": 24.5, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Mvile", "latitude": 3.2, "longitude": 10.58333, "x": 13.6, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Ngovayang", "latitude": 3.25, "longitude": 10.61667, "x": 20.9, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Ngoyang", "latitude": 3.31667, "longitude": 10.75, "x": 50, "y": 35.5, "source": "geonames", "type": "PPL" }, { "name": "Nkoatom", "latitude": 3.28333, "longitude": 10.86667, "x": 75.5, "y": 42.7, "source": "geonames", "type": "PPL" }, { "name": "Nkoekouk", "latitude": 3.06667, "longitude": 10.71667, "x": 42.7, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Nkoutou", "latitude": 3.11667, "longitude": 10.61667, "x": 20.9, "y": 79.1, "source": "geonames", "type": "PPL" }, { "name": "Nkwambo", "latitude": 3.43333, "longitude": 10.76667, "x": 53.6, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Ondogadjab", "latitude": 3.15, "longitude": 10.85, "x": 71.8, "y": 71.8, "source": "geonames", "type": "PPL" }, { "name": "Yang", "latitude": 3.15, "longitude": 10.63333, "x": 24.5, "y": 71.8, "source": "geonames", "type": "PPL" } ], "Lomie": [ { "name": "Abakoum", "latitude": 3.23333, "longitude": 13.6, "x": 26, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Adjela I", "latitude": 3.15, "longitude": 13.61667, "x": 31.3, "y": 53.3, "source": "geonames", "type": "PPL" }, { "name": "Akorenjong", "latitude": 3.13333, "longitude": 13.61667, "x": 31.3, "y": 56.7, "source": "geonames", "type": "PPL" }, { "name": "Bapile", "latitude": 3.36667, "longitude": 13.58333, "x": 20.7, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Biba I", "latitude": 3.01667, "longitude": 13.68333, "x": 52.7, "y": 80, "source": "geonames", "type": "PPL" }, { "name": "Biba II", "latitude": 3.11667, "longitude": 13.61667, "x": 31.3, "y": 60, "source": "geonames", "type": "PPL" }, { "name": "Bingongol", "latitude": 3.2, "longitude": 13.63333, "x": 36.7, "y": 43.3, "source": "geonames", "type": "PPL" }, { "name": "Djebe", "latitude": 3.25, "longitude": 13.6, "x": 26, "y": 33.3, "source": "geonames", "type": "PPL" }, { "name": "Djenoun", "latitude": 3.26667, "longitude": 13.6, "x": 26, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Djoandjila", "latitude": 3.36667, "longitude": 13.58333, "x": 20.7, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Djomedjo", "latitude": 3.1, "longitude": 13.6, "x": 26, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Dountam", "latitude": 3.18333, "longitude": 13.61667, "x": 31.3, "y": 46.7, "source": "geonames", "type": "PPL" }, { "name": "Dounzo", "latitude": 3.13333, "longitude": 13.8, "x": 90, "y": 56.7, "source": "geonames", "type": "PPL" }, { "name": "Dounzo I", "latitude": 3.13333, "longitude": 13.71667, "x": 63.3, "y": 56.7, "source": "geonames", "type": "PPL" }, { "name": "Ekom", "latitude": 3.15, "longitude": 13.61667, "x": 31.3, "y": 53.3, "source": "geonames", "type": "PPL" }, { "name": "Eschienbot", "latitude": 3.13333, "longitude": 13.76667, "x": 79.3, "y": 56.7, "source": "geonames", "type": "PPL" }, { "name": "Esomo", "latitude": 3.18333, "longitude": 13.63333, "x": 36.7, "y": 46.7, "source": "geonames", "type": "PPL" }, { "name": "Malen", "latitude": 3.16667, "longitude": 13.8, "x": 90, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Manpale", "latitude": 2.96667, "longitude": 13.55, "x": 10, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Medju", "latitude": 3.18333, "longitude": 13.61667, "x": 31.3, "y": 46.7, "source": "geonames", "type": "PPL" }, { "name": "Mintoum", "latitude": 3.21667, "longitude": 13.61667, "x": 31.3, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Nemeyong", "latitude": 3.3, "longitude": 13.58333, "x": 20.7, "y": 23.3, "source": "geonames", "type": "PPL" }, { "name": "Ngoulmokong", "latitude": 3.16667, "longitude": 13.61667, "x": 31.3, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Nkolounge", "latitude": 3.23333, "longitude": 13.6, "x": 26, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Poempoum", "latitude": 3.13333, "longitude": 13.65, "x": 42, "y": 56.7, "source": "geonames", "type": "PPL" }, { "name": "Sembe", "latitude": 3.15, "longitude": 13.61667, "x": 31.3, "y": 53.3, "source": "geonames", "type": "PPL" } ], "Loum": [ { "name": "Bonandam", "latitude": 4.7235, "longitude": 9.8384, "x": 65.7, "y": 39, "source": "geonames", "type": "PPL" }, { "name": "Bonao", "latitude": 4.7094, "longitude": 9.7536, "x": 31.2, "y": 48.4, "source": "geonames", "type": "PPL" }, { "name": "Bonaolo", "latitude": 4.671, "longitude": 9.7506, "x": 30, "y": 74.1, "source": "geonames", "type": "PPL" }, { "name": "Dibombe", "latitude": 4.68599, "longitude": 9.80727, "x": 53, "y": 64.1, "source": "geonames", "type": "PPL" }, { "name": "Lamba", "latitude": 4.6471, "longitude": 9.8808, "x": 82.9, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Loum-Chantiers", "latitude": 4.6997, "longitude": 9.7196, "x": 17.4, "y": 54.9, "source": "geonames", "type": "PPL" }, { "name": "Mbete", "latitude": 4.7312, "longitude": 9.7418, "x": 26.4, "y": 33.9, "source": "geonames", "type": "PPL" }, { "name": "Mbonzie", "latitude": 4.724, "longitude": 9.7015, "x": 10, "y": 38.7, "source": "geonames", "type": "PPL" }, { "name": "Ndokmeka", "latitude": 4.6552, "longitude": 9.888, "x": 85.9, "y": 84.6, "source": "geonames", "type": "PPL" }, { "name": "Ndoknak", "latitude": 4.656, "longitude": 9.8982, "x": 90, "y": 84.1, "source": "geonames", "type": "PPL" }, { "name": "Ngodi", "latitude": 4.7042, "longitude": 9.7228, "x": 18.7, "y": 51.9, "source": "geonames", "type": "PPL" }, { "name": "Nloe", "latitude": 4.767, "longitude": 9.7498, "x": 29.6, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Ntabako", "latitude": 4.659, "longitude": 9.8128, "x": 55.3, "y": 82.1, "source": "geonames", "type": "PPL" } ], "Maga": [ { "name": "Adibe", "latitude": 10.86614, "longitude": 14.781, "x": 19.8, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Akordoy", "latitude": 10.97928, "longitude": 14.77777, "x": 19.1, "y": 25.7, "source": "geonames", "type": "PPL" }, { "name": "Alvakay I", "latitude": 11.03715, "longitude": 14.99268, "x": 66.8, "y": 15.1, "source": "geonames", "type": "PPL" }, { "name": "Alvakay II", "latitude": 11.02296, "longitude": 14.99219, "x": 66.7, "y": 17.7, "source": "geonames", "type": "PPL" }, { "name": "Bara", "latitude": 10.94745, "longitude": 14.81669, "x": 27.7, "y": 31.5, "source": "geonames", "type": "PPL" }, { "name": "Baria Godio", "latitude": 10.71301, "longitude": 15.01886, "x": 72.6, "y": 74.5, "source": "geonames", "type": "PPL" }, { "name": "Barkaya", "latitude": 10.62817, "longitude": 14.97701, "x": 63.3, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Bla", "latitude": 10.82164, "longitude": 15.06032, "x": 81.8, "y": 54.6, "source": "geonames", "type": "PPL" }, { "name": "Bla Matoko", "latitude": 10.91901, "longitude": 14.81109, "x": 26.5, "y": 36.8, "source": "geonames", "type": "PPL" }, { "name": "Boko", "latitude": 10.73692, "longitude": 14.81524, "x": 27.4, "y": 70.1, "source": "geonames", "type": "PPL" }, { "name": "Bourmi", "latitude": 10.93788, "longitude": 14.92788, "x": 52.4, "y": 33.3, "source": "geonames", "type": "PPL" }, { "name": "Dawaya", "latitude": 11.0007, "longitude": 14.97638, "x": 63.2, "y": 21.8, "source": "geonames", "type": "PPL" }, { "name": "Dega", "latitude": 10.94017, "longitude": 14.89575, "x": 45.3, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Djamboutou", "latitude": 10.6479, "longitude": 14.85951, "x": 37.2, "y": 86.4, "source": "geonames", "type": "PPL" }, { "name": "Djekadei", "latitude": 10.69966, "longitude": 14.91589, "x": 49.8, "y": 76.9, "source": "geonames", "type": "PPL" }, { "name": "Djiddel Assoulay", "latitude": 10.7975, "longitude": 14.78028, "x": 19.6, "y": 59, "source": "geonames", "type": "PPL" }, { "name": "Djidere Saoudjo", "latitude": 10.89395, "longitude": 14.77928, "x": 19.4, "y": 41.3, "source": "geonames", "type": "PPL" }, { "name": "Djinabalam", "latitude": 11.01278, "longitude": 14.79778, "x": 23.5, "y": 19.6, "source": "geonames", "type": "PPL" }, { "name": "Dokney", "latitude": 10.81146, "longitude": 14.81198, "x": 26.7, "y": 56.4, "source": "geonames", "type": "PPL" }, { "name": "Dougui", "latitude": 10.72778, "longitude": 15.09704, "x": 90, "y": 71.8, "source": "geonames", "type": "PPL" }, { "name": "Gaba", "latitude": 11.05348, "longitude": 14.89652, "x": 45.5, "y": 12.1, "source": "geonames", "type": "PPL" }, { "name": "Galang", "latitude": 10.92424, "longitude": 15.06303, "x": 82.4, "y": 35.8, "source": "geonames", "type": "PPL" }, { "name": "Gamak", "latitude": 10.80951, "longitude": 14.9421, "x": 55.6, "y": 56.8, "source": "geonames", "type": "PPL" }, { "name": "Gaskafourou", "latitude": 10.98219, "longitude": 14.79507, "x": 22.9, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Gassouay", "latitude": 10.70275, "longitude": 14.99085, "x": 66.4, "y": 76.3, "source": "geonames", "type": "PPL" }, { "name": "Gogom", "latitude": 10.82496, "longitude": 14.7915, "x": 22.1, "y": 54, "source": "geonames", "type": "PPL" }, { "name": "Golomba", "latitude": 10.70123, "longitude": 14.82414, "x": 29.4, "y": 76.6, "source": "geonames", "type": "PPL" }, { "name": "Goulmoun", "latitude": 10.96461, "longitude": 15.00618, "x": 69.8, "y": 28.4, "source": "geonames", "type": "PPL" }, { "name": "Gourgouley", "latitude": 10.87701, "longitude": 14.82658, "x": 29.9, "y": 44.4, "source": "geonames", "type": "PPL" }, { "name": "Gouriki", "latitude": 10.86969, "longitude": 14.76555, "x": 16.4, "y": 45.8, "source": "geonames", "type": "PPL" }, { "name": "Graha", "latitude": 11.0489, "longitude": 14.95213, "x": 57.8, "y": 13, "source": "geonames", "type": "PPL" }, { "name": "Guirvidig", "latitude": 10.88047, "longitude": 14.83441, "x": 31.7, "y": 43.8, "source": "geonames", "type": "PPL" }, { "name": "Hotoye", "latitude": 10.65531, "longitude": 15.00854, "x": 70.3, "y": 85, "source": "geonames", "type": "PPL" }, { "name": "Houra", "latitude": 10.90832, "longitude": 14.85957, "x": 37.2, "y": 38.7, "source": "geonames", "type": "PPL" }, { "name": "Kai-Kai", "latitude": 10.67279, "longitude": 15.02086, "x": 73.1, "y": 81.8, "source": "geonames", "type": "PPL" }, { "name": "Kalang", "latitude": 10.82073, "longitude": 14.8882, "x": 43.6, "y": 54.7, "source": "geonames", "type": "PPL" }, { "name": "Kay Kay", "latitude": 10.85781, "longitude": 14.82972, "x": 30.6, "y": 48, "source": "geonames", "type": "PPL" }, { "name": "Kay Kay Mousgom", "latitude": 10.84156, "longitude": 14.82434, "x": 29.4, "y": 50.9, "source": "geonames", "type": "PPL" }, { "name": "Kay-Kay II", "latitude": 10.66768, "longitude": 15.03506, "x": 76.2, "y": 82.8, "source": "geonames", "type": "PPL" }, { "name": "Kayam", "latitude": 10.9808, "longitude": 14.82426, "x": 29.4, "y": 25.4, "source": "geonames", "type": "PPL" }, { "name": "Kayang", "latitude": 10.83172, "longitude": 14.86359, "x": 38.1, "y": 52.7, "source": "geonames", "type": "PPL" }, { "name": "Keleo", "latitude": 10.73708, "longitude": 14.96647, "x": 61, "y": 70.1, "source": "geonames", "type": "PPL" }, { "name": "Keraha", "latitude": 10.96181, "longitude": 14.95277, "x": 57.9, "y": 28.9, "source": "geonames", "type": "PPL" }, { "name": "Lougoye", "latitude": 10.67056, "longitude": 14.98012, "x": 64, "y": 82.2, "source": "geonames", "type": "PPL" }, { "name": "Mabou", "latitude": 10.88693, "longitude": 14.85609, "x": 36.5, "y": 42.6, "source": "geonames", "type": "PPL" }, { "name": "Mahiria", "latitude": 10.98889, "longitude": 15.04278, "x": 77.9, "y": 24, "source": "geonames", "type": "PPL" }, { "name": "Malame", "latitude": 11.01264, "longitude": 14.84901, "x": 34.9, "y": 19.6, "source": "geonames", "type": "PPL" }, { "name": "Manaka", "latitude": 11.0209, "longitude": 14.97918, "x": 63.8, "y": 18.1, "source": "geonames", "type": "PPL" }, { "name": "Maola", "latitude": 10.8624, "longitude": 14.92862, "x": 52.6, "y": 47.1, "source": "geonames", "type": "PPL" }, { "name": "Massa", "latitude": 11.01542, "longitude": 14.92179, "x": 51.1, "y": 19.1, "source": "geonames", "type": "PPL" }, { "name": "Mavala", "latitude": 10.75344, "longitude": 14.95503, "x": 58.5, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Mazera", "latitude": 11.06513, "longitude": 14.90333, "x": 47, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Mbororo", "latitude": 10.68486, "longitude": 14.84028, "x": 33, "y": 79.6, "source": "geonames", "type": "PPL" }, { "name": "Medere Mendji", "latitude": 10.95775, "longitude": 14.75056, "x": 13, "y": 29.7, "source": "geonames", "type": "PPL" }, { "name": "Merba", "latitude": 10.90708, "longitude": 14.73693, "x": 10, "y": 38.9, "source": "geonames", "type": "PPL" }, { "name": "Mewi", "latitude": 10.88414, "longitude": 14.87639, "x": 41, "y": 43.1, "source": "geonames", "type": "PPL" }, { "name": "Mongossi", "latitude": 10.94814, "longitude": 14.79036, "x": 21.9, "y": 31.4, "source": "geonames", "type": "PPL" }, { "name": "Morbouna", "latitude": 10.89712, "longitude": 14.85415, "x": 36, "y": 40.8, "source": "geonames", "type": "PPL" }, { "name": "Morgoy", "latitude": 10.92548, "longitude": 14.74581, "x": 12, "y": 35.6, "source": "geonames", "type": "PPL" }, { "name": "Mougou", "latitude": 10.89238, "longitude": 14.89708, "x": 45.6, "y": 41.6, "source": "geonames", "type": "PPL" }, { "name": "Mourla", "latitude": 10.92001, "longitude": 15.07101, "x": 84.2, "y": 36.6, "source": "geonames", "type": "PPL" }, { "name": "Ngoulmoun", "latitude": 10.91912, "longitude": 14.98437, "x": 65, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Oudouk", "latitude": 10.75, "longitude": 15.08333, "x": 87, "y": 67.7, "source": "geonames", "type": "PPL" }, { "name": "Pidimie", "latitude": 10.77747, "longitude": 14.92825, "x": 52.5, "y": 62.7, "source": "geonames", "type": "PPL" }, { "name": "Pinelvi", "latitude": 11.03333, "longitude": 14.98333, "x": 64.7, "y": 15.8, "source": "geonames", "type": "PPL" }, { "name": "Pouss", "latitude": 10.8472, "longitude": 15.05557, "x": 80.8, "y": 49.9, "source": "geonames", "type": "PPL" }, { "name": "Varay", "latitude": 10.79046, "longitude": 15.06573, "x": 83, "y": 60.3, "source": "geonames", "type": "PPL" }, { "name": "Yanga", "latitude": 10.73971, "longitude": 14.97654, "x": 63.2, "y": 69.6, "source": "geonames", "type": "PPL" }, { "name": "Yiho", "latitude": 10.89458, "longitude": 15.03628, "x": 76.5, "y": 41.2, "source": "geonames", "type": "PPL" }, { "name": "Zarbay", "latitude": 10.76157, "longitude": 14.89289, "x": 44.6, "y": 65.6, "source": "geonames", "type": "PPL" }, { "name": "Ziam", "latitude": 10.9093, "longitude": 14.95284, "x": 58, "y": 38.5, "source": "geonames", "type": "PPL" } ], "Magba": [ { "name": "Batoakoum", "latitude": 6.13333, "longitude": 11.28333, "x": 71.5, "y": 13.5, "source": "geonames", "type": "PPL" }, { "name": "Gbatok", "latitude": 5.76667, "longitude": 11.3, "x": 77.7, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Mambongam", "latitude": 5.93333, "longitude": 11.25, "x": 59.2, "y": 55.2, "source": "geonames", "type": "PPL" }, { "name": "Mambonkon", "latitude": 6.13333, "longitude": 11.25, "x": 59.2, "y": 13.5, "source": "geonames", "type": "PPL" }, { "name": "Manda", "latitude": 6.15, "longitude": 11.2, "x": 40.8, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Mangamban", "latitude": 6.08333, "longitude": 11.13333, "x": 16.2, "y": 23.9, "source": "geonames", "type": "PPL" }, { "name": "Mankoumvi", "latitude": 5.88333, "longitude": 11.11667, "x": 10, "y": 65.7, "source": "geonames", "type": "PPL" }, { "name": "Mante", "latitude": 6.05, "longitude": 11.18333, "x": 34.6, "y": 30.9, "source": "geonames", "type": "PPL" }, { "name": "Mba", "latitude": 6, "longitude": 11.3, "x": 77.7, "y": 41.3, "source": "geonames", "type": "PPL" }, { "name": "Moussafon", "latitude": 6.15, "longitude": 11.13333, "x": 16.2, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Mwa", "latitude": 6.03333, "longitude": 11.33333, "x": 90, "y": 34.3, "source": "geonames", "type": "PPL" }, { "name": "Patakou", "latitude": 6.13333, "longitude": 11.26667, "x": 65.4, "y": 13.5, "source": "geonames", "type": "PPL" }, { "name": "Tche", "latitude": 6.03333, "longitude": 11.26667, "x": 65.4, "y": 34.3, "source": "geonames", "type": "PPL" } ], "Mamfe": [ { "name": "Abawesong", "latitude": 5.6683, "longitude": 9.2593, "x": 36.6, "y": 66.3, "source": "geonames", "type": "PPL" }, { "name": "Abwayortang", "latitude": 5.6578, "longitude": 9.2514, "x": 35, "y": 68.3, "source": "geonames", "type": "PPL" }, { "name": "Afap", "latitude": 5.6736, "longitude": 9.1762, "x": 19.4, "y": 65.3, "source": "geonames", "type": "PPL" }, { "name": "Agborkem", "latitude": 5.6334, "longitude": 9.2769, "x": 40.3, "y": 72.9, "source": "geonames", "type": "PPL" }, { "name": "Aiyewawa", "latitude": 5.75, "longitude": 9.13333, "x": 10.6, "y": 50.8, "source": "geonames", "type": "PPL" }, { "name": "Ajayuk Ndip", "latitude": 5.6423, "longitude": 9.1455, "x": 13.1, "y": 71.2, "source": "geonames", "type": "PPL" }, { "name": "Ashum", "latitude": 5.57943, "longitude": 9.35208, "x": 55.8, "y": 83.1, "source": "geonames", "type": "PPL" }, { "name": "Ayukaba", "latitude": 5.707, "longitude": 9.1441, "x": 12.8, "y": 58.9, "source": "geonames", "type": "PPL" }, { "name": "Bache", "latitude": 5.94648, "longitude": 9.30689, "x": 46.5, "y": 13.5, "source": "geonames", "type": "PPL" }, { "name": "Bachuo Akagbe", "latitude": 5.69105, "longitude": 9.4433, "x": 74.6, "y": 62, "source": "geonames", "type": "PPL" }, { "name": "Bachuo Ntai", "latitude": 5.6991, "longitude": 9.3954, "x": 64.7, "y": 60.4, "source": "geonames", "type": "PPL" }, { "name": "Bah Eyong", "latitude": 5.7097, "longitude": 9.4641, "x": 78.9, "y": 58.4, "source": "geonames", "type": "PPL" }, { "name": "Baku", "latitude": 5.8054, "longitude": 9.3028, "x": 45.6, "y": 40.3, "source": "geonames", "type": "PPL" }, { "name": "Bakwelle", "latitude": 5.6949, "longitude": 9.1826, "x": 20.8, "y": 61.2, "source": "geonames", "type": "PPL" }, { "name": "Baro", "latitude": 5.6679, "longitude": 9.2091, "x": 26.2, "y": 66.3, "source": "geonames", "type": "PPL" }, { "name": "Besingue", "latitude": 5.7016, "longitude": 9.2873, "x": 42.4, "y": 60, "source": "geonames", "type": "PPL" }, { "name": "Besongabang", "latitude": 5.70981, "longitude": 9.30109, "x": 45.3, "y": 58.4, "source": "geonames", "type": "PPL" }, { "name": "Beteme", "latitude": 5.89651, "longitude": 9.16232, "x": 16.6, "y": 23, "source": "geonames", "type": "PPL" }, { "name": "Bololo", "latitude": 5.7729, "longitude": 9.3471, "x": 54.8, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Bombe", "latitude": 5.8177, "longitude": 9.382, "x": 62, "y": 37.9, "source": "geonames", "type": "PPL" }, { "name": "Ebinsi", "latitude": 5.6903, "longitude": 9.1646, "x": 17, "y": 62.1, "source": "geonames", "type": "PPL" }, { "name": "Echitako", "latitude": 5.65, "longitude": 9.1546, "x": 15, "y": 69.7, "source": "geonames", "type": "PPL" }, { "name": "Egbekaw", "latitude": 5.7636, "longitude": 9.3078, "x": 46.6, "y": 48.2, "source": "geonames", "type": "PPL" }, { "name": "Eham", "latitude": 5.7041, "longitude": 9.2195, "x": 28.4, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Esaguem", "latitude": 5.8567, "longitude": 9.2184, "x": 28.2, "y": 30.5, "source": "geonames", "type": "PPL" }, { "name": "Eshobi", "latitude": 5.78583, "longitude": 9.36027, "x": 57.5, "y": 44, "source": "geonames", "type": "PPL" }, { "name": "Etemetek", "latitude": 5.6967, "longitude": 9.2459, "x": 33.8, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Ewelle I", "latitude": 5.63517, "longitude": 9.19711, "x": 23.8, "y": 72.6, "source": "geonames", "type": "PPL" }, { "name": "Ewelle II", "latitude": 5.6469, "longitude": 9.201, "x": 24.6, "y": 70.3, "source": "geonames", "type": "PPL" }, { "name": "Eyang", "latitude": 5.6435, "longitude": 9.1629, "x": 16.7, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Eyang Nchang", "latitude": 5.6894, "longitude": 9.2413, "x": 32.9, "y": 62.3, "source": "geonames", "type": "PPL" }, { "name": "Eyang Ntui", "latitude": 5.7687, "longitude": 9.4072, "x": 67.2, "y": 47.2, "source": "geonames", "type": "PPL" }, { "name": "Kembong", "latitude": 5.62957, "longitude": 9.23445, "x": 31.5, "y": 73.6, "source": "geonames", "type": "PPL" }, { "name": "Kesham", "latitude": 5.86604, "longitude": 9.29194, "x": 43.4, "y": 28.8, "source": "geonames", "type": "PPL" }, { "name": "Mbakang", "latitude": 5.6766, "longitude": 9.14971, "x": 14, "y": 64.7, "source": "geonames", "type": "PPL" }, { "name": "Mbakem", "latitude": 5.94692, "longitude": 9.24032, "x": 32.7, "y": 13.4, "source": "geonames", "type": "PPL" }, { "name": "Mbinjong", "latitude": 5.65151, "longitude": 9.47389, "x": 81, "y": 69.5, "source": "geonames", "type": "PPL" }, { "name": "Melala", "latitude": 5.765, "longitude": 9.3356, "x": 52.4, "y": 47.9, "source": "geonames", "type": "PPL" }, { "name": "Mfuni", "latitude": 5.65561, "longitude": 9.25782, "x": 36.3, "y": 68.7, "source": "geonames", "type": "PPL" }, { "name": "Mukoyong", "latitude": 5.843, "longitude": 9.3962, "x": 64.9, "y": 33.1, "source": "geonames", "type": "PPL" }, { "name": "Nchang", "latitude": 5.6433, "longitude": 9.2165, "x": 27.8, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Nchemba II", "latitude": 5.7263, "longitude": 9.5176, "x": 90, "y": 55.3, "source": "geonames", "type": "PPL" }, { "name": "Ndekwai", "latitude": 5.6653, "longitude": 9.30056, "x": 45.1, "y": 66.8, "source": "geonames", "type": "PPL" }, { "name": "Nfeitok", "latitude": 5.54318, "longitude": 9.37295, "x": 60.1, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Nfeitok II", "latitude": 5.7238, "longitude": 9.5044, "x": 87.3, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Njeke Jalek", "latitude": 5.6148, "longitude": 9.2011, "x": 24.6, "y": 76.4, "source": "geonames", "type": "PPL" }, { "name": "Njeke Okonyene", "latitude": 5.6235, "longitude": 9.1904, "x": 22.4, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Njongsi", "latitude": 5.641, "longitude": 9.2737, "x": 39.6, "y": 71.4, "source": "geonames", "type": "PPL" }, { "name": "Nkimichi", "latitude": 5.6854, "longitude": 9.15567, "x": 15.2, "y": 63, "source": "geonames", "type": "PPL" }, { "name": "Nkpot", "latitude": 5.6826, "longitude": 9.2191, "x": 28.3, "y": 63.6, "source": "geonames", "type": "PPL" }, { "name": "Ntenako", "latitude": 5.67633, "longitude": 9.29016, "x": 43, "y": 64.7, "source": "geonames", "type": "PPL" }, { "name": "Obang", "latitude": 5.657, "longitude": 9.4053, "x": 66.8, "y": 68.4, "source": "geonames", "type": "PPL" }, { "name": "Ogomoko", "latitude": 5.665, "longitude": 9.1897, "x": 22.2, "y": 66.9, "source": "geonames", "type": "PPL" }, { "name": "Okoyong", "latitude": 5.7249, "longitude": 9.3499, "x": 55.3, "y": 55.5, "source": "geonames", "type": "PPL" }, { "name": "Okpambe", "latitude": 5.965, "longitude": 9.3437, "x": 54.1, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Ossing", "latitude": 5.63176, "longitude": 9.29577, "x": 44.2, "y": 73.2, "source": "geonames", "type": "PPL" }, { "name": "Tabo", "latitude": 5.74746, "longitude": 9.13051, "x": 10, "y": 51.3, "source": "geonames", "type": "PPL" }, { "name": "Takubiya", "latitude": 5.6426, "longitude": 9.49, "x": 84.3, "y": 71.1, "source": "geonames", "type": "PPL" }, { "name": "Talangaye", "latitude": 5.6291, "longitude": 9.3108, "x": 47.3, "y": 73.7, "source": "geonames", "type": "PPL" }, { "name": "Tawi", "latitude": 5.95, "longitude": 9.38333, "x": 62.3, "y": 12.8, "source": "geonames", "type": "PPL" }, { "name": "Tokpwa", "latitude": 5.64673, "longitude": 9.29997, "x": 45, "y": 70.4, "source": "geonames", "type": "PPL" } ], "Malantouen": [ { "name": "Makoutem", "latitude": 5.75, "longitude": 11.03333, "x": 19.9, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Malouro", "latitude": 5.88333, "longitude": 11.06667, "x": 29.3, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Mandjoum", "latitude": 5.65, "longitude": 11.06667, "x": 29.3, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Mange-Koutou", "latitude": 5.78333, "longitude": 11.1, "x": 38.6, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Manguiem", "latitude": 5.51667, "longitude": 11.1, "x": 38.6, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Manki I", "latitude": 5.82402, "longitude": 10.99783, "x": 10, "y": 22.9, "source": "geonames", "type": "PPL" }, { "name": "Manki II", "latitude": 5.85, "longitude": 11.1, "x": 38.6, "y": 17.3, "source": "geonames", "type": "PPL" }, { "name": "Mantoum Palais", "latitude": 5.6, "longitude": 11.15, "x": 52.6, "y": 71.8, "source": "geonames", "type": "PPL" }, { "name": "Mapou", "latitude": 5.75, "longitude": 11.23333, "x": 76, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Matachon", "latitude": 5.75, "longitude": 11.18333, "x": 62, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Mayouom", "latitude": 5.83578, "longitude": 10.99835, "x": 10.1, "y": 20.4, "source": "geonames", "type": "PPL" }, { "name": "Monbianko", "latitude": 5.75, "longitude": 11.28333, "x": 90, "y": 39.1, "source": "geonames", "type": "PPL" } ], "Manjo": [ { "name": "Abang", "latitude": 4.9466, "longitude": 9.7749, "x": 34.8, "y": 31.1, "source": "geonames", "type": "PPL" }, { "name": "Adjong", "latitude": 4.8, "longitude": 9.95, "x": 77.7, "y": 65.2, "source": "geonames", "type": "PPL" }, { "name": "Badjoki", "latitude": 4.7, "longitude": 9.93333, "x": 73.6, "y": 88.5, "source": "geonames", "type": "PPL" }, { "name": "Bakakte", "latitude": 4.8933, "longitude": 9.8757, "x": 59.5, "y": 43.5, "source": "geonames", "type": "PPL" }, { "name": "Bakwat", "latitude": 4.9098, "longitude": 9.8371, "x": 50, "y": 39.7, "source": "geonames", "type": "PPL" }, { "name": "Balondo", "latitude": 4.7277, "longitude": 9.864, "x": 56.6, "y": 82, "source": "geonames", "type": "PPL" }, { "name": "Banika", "latitude": 4.72656, "longitude": 9.926, "x": 71.8, "y": 82.3, "source": "geonames", "type": "PPL" }, { "name": "Basseng", "latitude": 4.9241, "longitude": 9.7065, "x": 18, "y": 36.4, "source": "geonames", "type": "PPL" }, { "name": "Bigmop", "latitude": 4.9164, "longitude": 9.8267, "x": 47.5, "y": 38.2, "source": "geonames", "type": "PPL" }, { "name": "Bonalebe", "latitude": 4.7096, "longitude": 9.8859, "x": 62, "y": 86.2, "source": "geonames", "type": "PPL" }, { "name": "Bonandam", "latitude": 4.7296, "longitude": 9.8699, "x": 58.1, "y": 81.6, "source": "geonames", "type": "PPL" }, { "name": "Bwanenbwa", "latitude": 4.7971, "longitude": 9.8603, "x": 55.7, "y": 65.9, "source": "geonames", "type": "PPL" }, { "name": "Ebako", "latitude": 4.8412, "longitude": 9.866, "x": 57.1, "y": 55.6, "source": "geonames", "type": "PPL" }, { "name": "Ebone", "latitude": 4.8775, "longitude": 9.8991, "x": 65.2, "y": 47.2, "source": "geonames", "type": "PPL" }, { "name": "Ebonemin", "latitude": 5.0134, "longitude": 9.7601, "x": 31.1, "y": 15.6, "source": "geonames", "type": "PPL" }, { "name": "Edib", "latitude": 4.9615, "longitude": 9.674, "x": 10, "y": 27.7, "source": "geonames", "type": "PPL" }, { "name": "Ekangte Mpaka", "latitude": 4.8924, "longitude": 9.8371, "x": 50, "y": 43.7, "source": "geonames", "type": "PPL" }, { "name": "Ekom", "latitude": 4.7816, "longitude": 9.8828, "x": 61.2, "y": 69.5, "source": "geonames", "type": "PPL" }, { "name": "Ekomdjong", "latitude": 4.8042, "longitude": 9.8978, "x": 64.9, "y": 64.2, "source": "geonames", "type": "PPL" }, { "name": "Ekomtolo", "latitude": 4.8309, "longitude": 9.9106, "x": 68.1, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Elase", "latitude": 5.0122, "longitude": 9.7214, "x": 21.6, "y": 15.9, "source": "geonames", "type": "PPL" }, { "name": "Eloum", "latitude": 5.03757, "longitude": 9.73456, "x": 24.9, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Epuke", "latitude": 4.8792, "longitude": 9.7414, "x": 26.5, "y": 46.8, "source": "geonames", "type": "PPL" }, { "name": "Essossong", "latitude": 4.8585, "longitude": 9.7288, "x": 23.4, "y": 51.6, "source": "geonames", "type": "PPL" }, { "name": "Kak", "latitude": 4.8828, "longitude": 9.7118, "x": 19.3, "y": 46, "source": "geonames", "type": "PPL" }, { "name": "Kola", "latitude": 4.8143, "longitude": 9.7595, "x": 31, "y": 61.9, "source": "geonames", "type": "PPL" }, { "name": "Konga", "latitude": 4.7724, "longitude": 9.94, "x": 75.3, "y": 71.6, "source": "geonames", "type": "PPL" }, { "name": "Kumin", "latitude": 4.9909, "longitude": 9.7037, "x": 17.3, "y": 20.8, "source": "geonames", "type": "PPL" }, { "name": "Lala", "latitude": 4.7948, "longitude": 9.7618, "x": 31.5, "y": 66.4, "source": "geonames", "type": "PPL" }, { "name": "Loumsi", "latitude": 4.8543, "longitude": 9.8011, "x": 41.2, "y": 52.6, "source": "geonames", "type": "PPL" }, { "name": "Mabom", "latitude": 4.6934, "longitude": 9.9755, "x": 84, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Makouma", "latitude": 4.8004, "longitude": 9.9312, "x": 73.1, "y": 65.1, "source": "geonames", "type": "PPL" }, { "name": "Manengole", "latitude": 4.8724, "longitude": 9.8595, "x": 55.5, "y": 48.4, "source": "geonames", "type": "PPL" }, { "name": "Manengoteng", "latitude": 4.8156, "longitude": 9.8013, "x": 41.2, "y": 61.6, "source": "geonames", "type": "PPL" }, { "name": "Mangamba", "latitude": 4.7641, "longitude": 9.8759, "x": 59.5, "y": 73.6, "source": "geonames", "type": "PPL" }, { "name": "Manta", "latitude": 4.9745, "longitude": 9.7371, "x": 25.5, "y": 24.7, "source": "geonames", "type": "PPL" }, { "name": "Mantem I", "latitude": 4.8247, "longitude": 9.7738, "x": 34.5, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Mantem II", "latitude": 4.8717, "longitude": 9.8103, "x": 43.4, "y": 48.6, "source": "geonames", "type": "PPL" }, { "name": "Mbaka", "latitude": 4.7493, "longitude": 9.8491, "x": 53, "y": 77, "source": "geonames", "type": "PPL" }, { "name": "Mbogmut", "latitude": 4.9467, "longitude": 9.7008, "x": 16.6, "y": 31.1, "source": "geonames", "type": "PPL" }, { "name": "Mekedaku", "latitude": 4.9063, "longitude": 9.6833, "x": 12.3, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Moumba", "latitude": 4.76667, "longitude": 10, "x": 90, "y": 73, "source": "geonames", "type": "PPL" }, { "name": "Moumekeng", "latitude": 4.984, "longitude": 9.796, "x": 39.9, "y": 22.5, "source": "geonames", "type": "PPL" }, { "name": "Muambong I", "latitude": 4.9457, "longitude": 9.7225, "x": 21.9, "y": 31.4, "source": "geonames", "type": "PPL" }, { "name": "Muambong II", "latitude": 4.9595, "longitude": 9.7232, "x": 22.1, "y": 28.1, "source": "geonames", "type": "PPL" }, { "name": "Muekam", "latitude": 4.9854, "longitude": 9.7545, "x": 29.8, "y": 22.1, "source": "geonames", "type": "PPL" }, { "name": "Muetanaku", "latitude": 5.0293, "longitude": 9.7233, "x": 22.1, "y": 11.9, "source": "geonames", "type": "PPL" }, { "name": "Muydip", "latitude": 4.9767, "longitude": 9.7484, "x": 28.3, "y": 24.1, "source": "geonames", "type": "PPL" }, { "name": "Mwakoumel", "latitude": 4.9739, "longitude": 9.7952, "x": 39.7, "y": 24.8, "source": "geonames", "type": "PPL" }, { "name": "Mwandong", "latitude": 5.00019, "longitude": 9.78169, "x": 36.4, "y": 18.7, "source": "geonames", "type": "PPL" }, { "name": "Namba", "latitude": 4.8633, "longitude": 9.8296, "x": 48.2, "y": 50.5, "source": "geonames", "type": "PPL" }, { "name": "Ndiang", "latitude": 4.7697, "longitude": 9.8719, "x": 58.6, "y": 72.3, "source": "geonames", "type": "PPL" }, { "name": "Ndiong", "latitude": 4.93333, "longitude": 9.83333, "x": 49.1, "y": 34.2, "source": "geonames", "type": "PPL" }, { "name": "Ndum", "latitude": 4.8736, "longitude": 9.7144, "x": 19.9, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Ngol", "latitude": 4.8841, "longitude": 9.795, "x": 39.7, "y": 45.7, "source": "geonames", "type": "PPL" }, { "name": "Ngomboku", "latitude": 4.9205, "longitude": 9.7194, "x": 21.1, "y": 37.2, "source": "geonames", "type": "PPL" }, { "name": "Ngombombeng", "latitude": 4.9053, "longitude": 9.7123, "x": 19.4, "y": 40.7, "source": "geonames", "type": "PPL" }, { "name": "Ngomuin", "latitude": 4.9836, "longitude": 9.73208, "x": 24.3, "y": 22.5, "source": "geonames", "type": "PPL" }, { "name": "Ngondo", "latitude": 4.7149, "longitude": 9.9383, "x": 74.9, "y": 85, "source": "geonames", "type": "PPL" }, { "name": "Njimbeng", "latitude": 5.0108, "longitude": 9.713, "x": 19.6, "y": 16.2, "source": "geonames", "type": "PPL" }, { "name": "Njoumbeng", "latitude": 4.9338, "longitude": 9.8125, "x": 44, "y": 34.1, "source": "geonames", "type": "PPL" }, { "name": "Nkongbass", "latitude": 4.71667, "longitude": 9.91667, "x": 69.6, "y": 84.6, "source": "geonames", "type": "PPL" }, { "name": "Nkosse", "latitude": 4.8807, "longitude": 9.7788, "x": 35.7, "y": 46.5, "source": "geonames", "type": "PPL" }, { "name": "Nlog", "latitude": 4.879, "longitude": 9.6942, "x": 15, "y": 46.9, "source": "geonames", "type": "PPL" }, { "name": "Nyamsa", "latitude": 4.7541, "longitude": 9.8741, "x": 59.1, "y": 75.9, "source": "geonames", "type": "PPL" }, { "name": "Nyang", "latitude": 4.8863, "longitude": 9.7589, "x": 30.8, "y": 45.2, "source": "geonames", "type": "PPL" }, { "name": "Salaka", "latitude": 4.7084, "longitude": 9.9439, "x": 76.2, "y": 86.5, "source": "geonames", "type": "PPL" }, { "name": "Salmoa", "latitude": 4.9207, "longitude": 9.8599, "x": 55.6, "y": 37.2, "source": "geonames", "type": "PPL" }, { "name": "Singuendiang", "latitude": 4.7421, "longitude": 9.8664, "x": 57.2, "y": 78.7, "source": "geonames", "type": "PPL" }, { "name": "Tape", "latitude": 4.8796, "longitude": 9.7352, "x": 25, "y": 46.7, "source": "geonames", "type": "PPL" }, { "name": "Teipe", "latitude": 4.85015, "longitude": 9.71616, "x": 20.3, "y": 53.6, "source": "geonames", "type": "PPL" } ], "Maroua": [ { "name": "Adya", "latitude": 10.6903, "longitude": 14.35472, "x": 64.3, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Assideouo", "latitude": 10.68276, "longitude": 14.35135, "x": 63.1, "y": 12.9, "source": "geonames", "type": "PPL" }, { "name": "Banana", "latitude": 10.59793, "longitude": 14.34075, "x": 59.1, "y": 45.3, "source": "geonames", "type": "PPL" }, { "name": "Bao Hossere", "latitude": 10.5382, "longitude": 14.28261, "x": 37.4, "y": 68.2, "source": "geonames", "type": "PPL" }, { "name": "Bapa", "latitude": 10.5702, "longitude": 14.27673, "x": 35.2, "y": 55.9, "source": "geonames", "type": "PPL" }, { "name": "Bellare", "latitude": 10.56592, "longitude": 14.26147, "x": 29.5, "y": 57.6, "source": "geonames", "type": "PPL" }, { "name": "Bilmiti", "latitude": 10.59685, "longitude": 14.22211, "x": 14.8, "y": 45.7, "source": "geonames", "type": "PPL" }, { "name": "Bomyo", "latitude": 10.66135, "longitude": 14.34342, "x": 60.1, "y": 21.1, "source": "geonames", "type": "PPL" }, { "name": "Bornouang", "latitude": 10.57596, "longitude": 14.29861, "x": 43.4, "y": 53.7, "source": "geonames", "type": "PPL" }, { "name": "Boudougou", "latitude": 10.52377, "longitude": 14.24038, "x": 21.6, "y": 73.7, "source": "geonames", "type": "PPL" }, { "name": "Boulore", "latitude": 10.65367, "longitude": 14.29606, "x": 42.4, "y": 24, "source": "geonames", "type": "PPL" }, { "name": "Boutom Fedem", "latitude": 10.6611, "longitude": 14.26864, "x": 32.1, "y": 21.2, "source": "geonames", "type": "PPL" }, { "name": "Dakar", "latitude": 10.57733, "longitude": 14.24801, "x": 24.4, "y": 53.2, "source": "geonames", "type": "PPL" }, { "name": "Dandeo", "latitude": 10.67059, "longitude": 14.37475, "x": 71.8, "y": 17.5, "source": "geonames", "type": "PPL" }, { "name": "Dangar", "latitude": 10.68271, "longitude": 14.28967, "x": 40, "y": 12.9, "source": "geonames", "type": "PPL" }, { "name": "Dengui", "latitude": 10.55, "longitude": 14.25, "x": 25.2, "y": 63.6, "source": "geonames", "type": "PPL" }, { "name": "Diguirouo", "latitude": 10.60563, "longitude": 14.32151, "x": 51.9, "y": 42.4, "source": "geonames", "type": "PPL" }, { "name": "Dingui", "latitude": 10.563, "longitude": 14.28167, "x": 37, "y": 58.7, "source": "geonames", "type": "PPL" }, { "name": "Djaguinadje", "latitude": 10.67686, "longitude": 14.26536, "x": 30.9, "y": 15.1, "source": "geonames", "type": "PPL" }, { "name": "Djamnay", "latitude": 10.57487, "longitude": 14.28848, "x": 39.6, "y": 54.1, "source": "geonames", "type": "PPL" }, { "name": "Djarengol", "latitude": 10.58875, "longitude": 14.30995, "x": 47.6, "y": 48.8, "source": "geonames", "type": "PPL" }, { "name": "Djideo", "latitude": 10.60281, "longitude": 14.38231, "x": 74.6, "y": 43.5, "source": "geonames", "type": "PPL" }, { "name": "Djodifere", "latitude": 10.57553, "longitude": 14.26106, "x": 29.3, "y": 53.9, "source": "geonames", "type": "PPL" }, { "name": "Djodjong", "latitude": 10.48107, "longitude": 14.33188, "x": 55.8, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Djodjong Babou", "latitude": 10.48206, "longitude": 14.32041, "x": 51.5, "y": 89.6, "source": "geonames", "type": "PPL" }, { "name": "Djola", "latitude": 10.6094, "longitude": 14.40141, "x": 81.8, "y": 40.9, "source": "geonames", "type": "PPL" }, { "name": "Djountgo", "latitude": 10.57437, "longitude": 14.26834, "x": 32, "y": 54.3, "source": "geonames", "type": "PPL" }, { "name": "Djourngo", "latitude": 10.63115, "longitude": 14.40304, "x": 82.4, "y": 32.6, "source": "geonames", "type": "PPL" }, { "name": "Domayo", "latitude": 10.57484, "longitude": 14.3362, "x": 57.4, "y": 54.1, "source": "geonames", "type": "PPL" }, { "name": "Dougoy", "latitude": 10.61243, "longitude": 14.35325, "x": 63.8, "y": 39.8, "source": "geonames", "type": "PPL" }, { "name": "Doursoungo", "latitude": 10.60088, "longitude": 14.35715, "x": 65.2, "y": 44.2, "source": "geonames", "type": "PPL" }, { "name": "Douvangar", "latitude": 10.54749, "longitude": 14.25195, "x": 25.9, "y": 64.6, "source": "geonames", "type": "PPL" }, { "name": "Doyang", "latitude": 10.59063, "longitude": 14.27567, "x": 34.8, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Founangue", "latitude": 10.60067, "longitude": 14.3324, "x": 56, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Gadamayo", "latitude": 10.59224, "longitude": 14.23747, "x": 20.5, "y": 47.5, "source": "geonames", "type": "PPL" }, { "name": "Gakle", "latitude": 10.52254, "longitude": 14.26505, "x": 30.8, "y": 74.1, "source": "geonames", "type": "PPL" }, { "name": "Gare", "latitude": 10.60657, "longitude": 14.38866, "x": 77, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Gayak", "latitude": 10.66222, "longitude": 14.35278, "x": 63.6, "y": 20.7, "source": "geonames", "type": "PPL" }, { "name": "GoniBelo", "latitude": 10.58834, "longitude": 14.25451, "x": 26.9, "y": 49, "source": "geonames", "type": "PPL" }, { "name": "Goubea", "latitude": 10.57599, "longitude": 14.2094, "x": 10, "y": 53.7, "source": "geonames", "type": "PPL" }, { "name": "Goubeo", "latitude": 10.60499, "longitude": 14.37452, "x": 71.7, "y": 42.6, "source": "geonames", "type": "PPL" }, { "name": "GourelDjokapaidi", "latitude": 10.59154, "longitude": 14.39511, "x": 79.4, "y": 47.8, "source": "geonames", "type": "PPL" }, { "name": "Guiring", "latitude": 10.61979, "longitude": 14.37058, "x": 70.3, "y": 37, "source": "geonames", "type": "PPL" }, { "name": "Harde", "latitude": 10.63396, "longitude": 14.40944, "x": 84.8, "y": 31.5, "source": "geonames", "type": "PPL" }, { "name": "Hardeo", "latitude": 10.52398, "longitude": 14.28126, "x": 36.9, "y": 73.6, "source": "geonames", "type": "PPL" }, { "name": "Hedjer", "latitude": 10.5467, "longitude": 14.24291, "x": 22.5, "y": 64.9, "source": "geonames", "type": "PPL" }, { "name": "Hodango", "latitude": 10.67302, "longitude": 14.27003, "x": 32.7, "y": 16.6, "source": "geonames", "type": "PPL" }, { "name": "HossereMissinnguileo", "latitude": 10.62627, "longitude": 14.31675, "x": 50.1, "y": 34.5, "source": "geonames", "type": "PPL" }, { "name": "Houdjouloum", "latitude": 10.67184, "longitude": 14.24717, "x": 24.1, "y": 17.1, "source": "geonames", "type": "PPL" }, { "name": "Idao", "latitude": 10.5762, "longitude": 14.35851, "x": 65.7, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "KaIgama", "latitude": 10.59402, "longitude": 14.29955, "x": 43.7, "y": 46.8, "source": "geonames", "type": "PPL" }, { "name": "Kalliao", "latitude": 10.60944, "longitude": 14.21574, "x": 12.4, "y": 40.9, "source": "geonames", "type": "PPL" }, { "name": "Kodek", "latitude": 10.63742, "longitude": 14.39031, "x": 77.6, "y": 30.2, "source": "geonames", "type": "PPL" }, { "name": "Kodek Djarengol", "latitude": 10.62921, "longitude": 14.37858, "x": 73.2, "y": 33.4, "source": "geonames", "type": "PPL" }, { "name": "Kodjoleouo", "latitude": 10.67741, "longitude": 14.29047, "x": 40.3, "y": 14.9, "source": "geonames", "type": "PPL" }, { "name": "Kongola-Djideo", "latitude": 10.61299, "longitude": 14.37906, "x": 73.4, "y": 39.6, "source": "geonames", "type": "PPL" }, { "name": "Kongola-Djolao", "latitude": 10.61832, "longitude": 14.39624, "x": 79.8, "y": 37.5, "source": "geonames", "type": "PPL" }, { "name": "Kortogalouo", "latitude": 10.6071, "longitude": 14.3493, "x": 62.3, "y": 41.8, "source": "geonames", "type": "PPL" }, { "name": "KosseyelMade", "latitude": 10.63828, "longitude": 14.28938, "x": 39.9, "y": 29.9, "source": "geonames", "type": "PPL" }, { "name": "KosseyelToursoudje", "latitude": 10.6494, "longitude": 14.28855, "x": 39.6, "y": 25.6, "source": "geonames", "type": "PPL" }, { "name": "Koudbao", "latitude": 10.60305, "longitude": 14.3112, "x": 48.1, "y": 43.4, "source": "geonames", "type": "PPL" }, { "name": "Koudoukoum", "latitude": 10.57124, "longitude": 14.30885, "x": 47.2, "y": 55.5, "source": "geonames", "type": "PPL" }, { "name": "Koutoulmi", "latitude": 10.61011, "longitude": 14.38358, "x": 75.1, "y": 40.7, "source": "geonames", "type": "PPL" }, { "name": "Ladeo", "latitude": 10.58642, "longitude": 14.26338, "x": 30.2, "y": 49.7, "source": "geonames", "type": "PPL" }, { "name": "Lamorde", "latitude": 10.68973, "longitude": 14.3126, "x": 48.6, "y": 10.2, "source": "geonames", "type": "PPL" }, { "name": "Lougol", "latitude": 10.67376, "longitude": 14.26273, "x": 29.9, "y": 16.3, "source": "geonames", "type": "PPL" }, { "name": "Makabay", "latitude": 10.57246, "longitude": 14.30529, "x": 45.8, "y": 55.1, "source": "geonames", "type": "PPL" }, { "name": "Makoumba", "latitude": 10.56717, "longitude": 14.26738, "x": 31.7, "y": 57.1, "source": "geonames", "type": "PPL" }, { "name": "Mambang", "latitude": 10.65977, "longitude": 14.28764, "x": 39.2, "y": 21.7, "source": "geonames", "type": "PPL" }, { "name": "Manadje", "latitude": 10.48325, "longitude": 14.31312, "x": 48.8, "y": 89.2, "source": "geonames", "type": "PPL" }, { "name": "Manaoatchi", "latitude": 10.62161, "longitude": 14.39362, "x": 78.9, "y": 36.3, "source": "geonames", "type": "PPL" }, { "name": "Massinaka", "latitude": 10.59867, "longitude": 14.27776, "x": 35.6, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "MayelDenguesdjiToupouri", "latitude": 10.57536, "longitude": 14.34773, "x": 61.7, "y": 53.9, "source": "geonames", "type": "PPL" }, { "name": "Meskin", "latitude": 10.54874, "longitude": 14.24694, "x": 24, "y": 64.1, "source": "geonames", "type": "PPL" }, { "name": "Missinnguileo", "latitude": 10.60784, "longitude": 14.30229, "x": 44.7, "y": 41.5, "source": "geonames", "type": "PPL" }, { "name": "Moda", "latitude": 10.63415, "longitude": 14.41657, "x": 87.5, "y": 31.5, "source": "geonames", "type": "PPL" }, { "name": "Mogazang", "latitude": 10.67441, "longitude": 14.32109, "x": 51.8, "y": 16.1, "source": "geonames", "type": "PPL" }, { "name": "Mogordom", "latitude": 10.66844, "longitude": 14.28258, "x": 37.4, "y": 18.4, "source": "geonames", "type": "PPL" }, { "name": "Ngassao", "latitude": 10.57898, "longitude": 14.36003, "x": 66.3, "y": 52.6, "source": "geonames", "type": "PPL" }, { "name": "Ngoyang", "latitude": 10.52962, "longitude": 14.24322, "x": 22.6, "y": 71.4, "source": "geonames", "type": "PPL" }, { "name": "OuoylaMayo", "latitude": 10.59272, "longitude": 14.25671, "x": 27.7, "y": 47.3, "source": "geonames", "type": "PPL" }, { "name": "OuraKaidal", "latitude": 10.62433, "longitude": 14.42339, "x": 90, "y": 35.2, "source": "geonames", "type": "PPL" }, { "name": "Ouro Lade", "latitude": 10.53982, "longitude": 14.31829, "x": 50.7, "y": 67.5, "source": "geonames", "type": "PPL" }, { "name": "Ouro Said", "latitude": 10.62062, "longitude": 14.41379, "x": 86.4, "y": 36.6, "source": "geonames", "type": "PPL" }, { "name": "OuroBaba", "latitude": 10.62069, "longitude": 14.38252, "x": 74.7, "y": 36.6, "source": "geonames", "type": "PPL" }, { "name": "OuroDada", "latitude": 10.62776, "longitude": 14.38212, "x": 74.6, "y": 33.9, "source": "geonames", "type": "PPL" }, { "name": "OuroGaldima", "latitude": 10.62447, "longitude": 14.37462, "x": 71.8, "y": 35.2, "source": "geonames", "type": "PPL" }, { "name": "OuroGuie", "latitude": 10.67035, "longitude": 14.36638, "x": 68.7, "y": 17.6, "source": "geonames", "type": "PPL" }, { "name": "OuroKari", "latitude": 10.61452, "longitude": 14.40768, "x": 84.1, "y": 39, "source": "geonames", "type": "PPL" }, { "name": "OuroLaoann", "latitude": 10.62646, "longitude": 14.4055, "x": 83.3, "y": 34.4, "source": "geonames", "type": "PPL" }, { "name": "OuroLope", "latitude": 10.62133, "longitude": 14.35855, "x": 65.8, "y": 36.4, "source": "geonames", "type": "PPL" }, { "name": "OuroMalmana", "latitude": 10.63708, "longitude": 14.40492, "x": 83.1, "y": 30.3, "source": "geonames", "type": "PPL" }, { "name": "OuroMayina", "latitude": 10.65687, "longitude": 14.40594, "x": 83.5, "y": 22.8, "source": "geonames", "type": "PPL" }, { "name": "OuroModibo", "latitude": 10.59398, "longitude": 14.26194, "x": 29.6, "y": 46.8, "source": "geonames", "type": "PPL" }, { "name": "OuroNdjobdi", "latitude": 10.62832, "longitude": 14.41264, "x": 86, "y": 33.7, "source": "geonames", "type": "PPL" }, { "name": "OuroPole", "latitude": 10.60871, "longitude": 14.39372, "x": 78.9, "y": 41.2, "source": "geonames", "type": "PPL" }, { "name": "OuroTaba", "latitude": 10.61847, "longitude": 14.42326, "x": 90, "y": 37.5, "source": "geonames", "type": "PPL" }, { "name": "OuroYaya", "latitude": 10.64111, "longitude": 14.41119, "x": 85.4, "y": 28.8, "source": "geonames", "type": "PPL" }, { "name": "Pallar", "latitude": 10.60241, "longitude": 14.29355, "x": 41.5, "y": 43.6, "source": "geonames", "type": "PPL" }, { "name": "Paskale", "latitude": 10.58561, "longitude": 14.29566, "x": 42.2, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Sambouda", "latitude": 10.66548, "longitude": 14.36164, "x": 66.9, "y": 19.5, "source": "geonames", "type": "PPL" }, { "name": "SiraTare", "latitude": 10.62467, "longitude": 14.39082, "x": 77.8, "y": 35.1, "source": "geonames", "type": "PPL" }, { "name": "Taneo", "latitude": 10.52204, "longitude": 14.27096, "x": 33, "y": 74.3, "source": "geonames", "type": "PPL" }, { "name": "Tapadam", "latitude": 10.62381, "longitude": 14.24726, "x": 24.2, "y": 35.4, "source": "geonames", "type": "PPL" }, { "name": "Tchabaol", "latitude": 10.63332, "longitude": 14.23661, "x": 20.2, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Tchadeo", "latitude": 10.60417, "longitude": 14.36463, "x": 68, "y": 42.9, "source": "geonames", "type": "PPL" }, { "name": "Tchasdeo", "latitude": 10.59421, "longitude": 14.26967, "x": 32.5, "y": 46.7, "source": "geonames", "type": "PPL" }, { "name": "Toupere", "latitude": 10.63589, "longitude": 14.39831, "x": 80.6, "y": 30.8, "source": "geonames", "type": "PPL" }, { "name": "Tsabaouonn", "latitude": 10.66747, "longitude": 14.25782, "x": 28.1, "y": 18.7, "source": "geonames", "type": "PPL" }, { "name": "Wourmde", "latitude": 10.55299, "longitude": 14.28486, "x": 38.2, "y": 62.5, "source": "geonames", "type": "PPL" }, { "name": "Yoldeo", "latitude": 10.57387, "longitude": 14.36953, "x": 69.9, "y": 54.5, "source": "geonames", "type": "PPL" }, { "name": "Yolol", "latitude": 10.61727, "longitude": 14.37692, "x": 72.6, "y": 37.9, "source": "geonames", "type": "PPL" }, { "name": "Yong Kolle", "latitude": 10.55359, "longitude": 14.27972, "x": 36.3, "y": 62.3, "source": "geonames", "type": "PPL" }, { "name": "YonkolaNassourou", "latitude": 10.62629, "longitude": 14.41255, "x": 85.9, "y": 34.5, "source": "geonames", "type": "PPL" }, { "name": "Zakaliao", "latitude": 10.65417, "longitude": 14.24131, "x": 21.9, "y": 23.8, "source": "geonames", "type": "PPL" }, { "name": "Zakirou", "latitude": 10.57616, "longitude": 14.27441, "x": 34.3, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Zala", "latitude": 10.62428, "longitude": 14.25247, "x": 26.1, "y": 35.2, "source": "geonames", "type": "PPL" }, { "name": "Zayka", "latitude": 10.60877, "longitude": 14.28228, "x": 37.2, "y": 41.2, "source": "geonames", "type": "PPL" }, { "name": "Ziling", "latitude": 10.57277, "longitude": 14.26735, "x": 31.7, "y": 54.9, "source": "geonames", "type": "PPL" }, { "name": "Zokok", "latitude": 10.59042, "longitude": 14.29028, "x": 40.2, "y": 48.2, "source": "geonames", "type": "PPL" } ], "Mbalmayo": [ { "name": "Abang", "latitude": 3.56667, "longitude": 11.61667, "x": 63.3, "y": 35.5, "source": "geonames", "type": "PPL" }, { "name": "Abang I", "latitude": 3.63333, "longitude": 11.5, "x": 32.2, "y": 20.9, "source": "geonames", "type": "PPL" }, { "name": "Abimoa", "latitude": 3.56667, "longitude": 11.61667, "x": 63.3, "y": 35.5, "source": "geonames", "type": "PPL" }, { "name": "Akometam", "latitude": 3.46667, "longitude": 11.56667, "x": 50, "y": 57.3, "source": "geonames", "type": "PPL" }, { "name": "Akomnyada I", "latitude": 3.53333, "longitude": 11.55, "x": 45.6, "y": 42.7, "source": "geonames", "type": "PPL" }, { "name": "Akomnyada II", "latitude": 3.55, "longitude": 11.55, "x": 45.6, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Alen", "latitude": 3.6, "longitude": 11.43333, "x": 14.4, "y": 28.2, "source": "geonames", "type": "PPL" }, { "name": "Asanzoa", "latitude": 3.58333, "longitude": 11.55, "x": 45.6, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Atokzok", "latitude": 3.38333, "longitude": 11.63333, "x": 67.8, "y": 75.5, "source": "geonames", "type": "PPL" }, { "name": "Avebe", "latitude": 3.35, "longitude": 11.5, "x": 32.2, "y": 82.7, "source": "geonames", "type": "PPL" }, { "name": "Awae", "latitude": 3.6, "longitude": 11.55, "x": 45.6, "y": 28.2, "source": "geonames", "type": "PPL" }, { "name": "Ayene", "latitude": 3.4, "longitude": 11.68333, "x": 81.1, "y": 71.8, "source": "geonames", "type": "PPL" }, { "name": "Bikok", "latitude": 3.63333, "longitude": 11.43333, "x": 14.4, "y": 20.9, "source": "geonames", "type": "PPL" }, { "name": "Bikop", "latitude": 3.51667, "longitude": 11.41667, "x": 10, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Biyan", "latitude": 3.4, "longitude": 11.61667, "x": 63.3, "y": 71.8, "source": "geonames", "type": "PPL" }, { "name": "Daydo", "latitude": 3.56667, "longitude": 11.7, "x": 85.6, "y": 35.5, "source": "geonames", "type": "PPL" }, { "name": "Ebogo", "latitude": 3.4, "longitude": 11.46667, "x": 23.3, "y": 71.8, "source": "geonames", "type": "PPL" }, { "name": "Ebomsi II", "latitude": 3.46667, "longitude": 11.7, "x": 85.6, "y": 57.3, "source": "geonames", "type": "PPL" }, { "name": "Ebougmenyou", "latitude": 3.58333, "longitude": 11.45, "x": 18.9, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Ekali", "latitude": 3.63333, "longitude": 11.53333, "x": 41.1, "y": 20.9, "source": "geonames", "type": "PPL" }, { "name": "Ekidmekoe", "latitude": 3.63333, "longitude": 11.65, "x": 72.2, "y": 20.9, "source": "geonames", "type": "PPL" }, { "name": "Ekok I", "latitude": 3.68333, "longitude": 11.6, "x": 58.9, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Ekoko I", "latitude": 3.68333, "longitude": 11.53333, "x": 41.1, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Ekombitie", "latitude": 3.46667, "longitude": 11.51667, "x": 36.7, "y": 57.3, "source": "geonames", "type": "PPL" }, { "name": "Elende", "latitude": 3.46667, "longitude": 11.71667, "x": 90, "y": 57.3, "source": "geonames", "type": "PPL" }, { "name": "Esazok I", "latitude": 3.68333, "longitude": 11.53333, "x": 41.1, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Essomba", "latitude": 3.55, "longitude": 11.41667, "x": 10, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Fakele I", "latitude": 3.48333, "longitude": 11.6, "x": 58.9, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Fakele II", "latitude": 3.5, "longitude": 11.61667, "x": 63.3, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Indinge", "latitude": 3.4, "longitude": 11.65, "x": 72.2, "y": 71.8, "source": "geonames", "type": "PPL" }, { "name": "Koulboa", "latitude": 3.63333, "longitude": 11.46667, "x": 23.3, "y": 20.9, "source": "geonames", "type": "PPL" }, { "name": "Koumou", "latitude": 3.65, "longitude": 11.51667, "x": 36.7, "y": 17.3, "source": "geonames", "type": "PPL" }, { "name": "Loum I", "latitude": 3.63333, "longitude": 11.68333, "x": 81.1, "y": 20.9, "source": "geonames", "type": "PPL" }, { "name": "Loum II", "latitude": 3.61667, "longitude": 11.66667, "x": 76.7, "y": 24.5, "source": "geonames", "type": "PPL" }, { "name": "Mbadoumou", "latitude": 3.63333, "longitude": 11.41667, "x": 10, "y": 20.9, "source": "geonames", "type": "PPL" }, { "name": "Mbadoumou I", "latitude": 3.6, "longitude": 11.55, "x": 45.6, "y": 28.2, "source": "geonames", "type": "PPL" }, { "name": "Mbaka", "latitude": 3.65, "longitude": 11.45, "x": 18.9, "y": 17.3, "source": "geonames", "type": "PPL" }, { "name": "Mbalelon", "latitude": 3.56667, "longitude": 11.43333, "x": 14.4, "y": 35.5, "source": "geonames", "type": "PPL" }, { "name": "Mbega", "latitude": 3.53333, "longitude": 11.65, "x": 72.2, "y": 42.7, "source": "geonames", "type": "PPL" }, { "name": "Mekomo", "latitude": 3.4, "longitude": 11.63333, "x": 67.8, "y": 71.8, "source": "geonames", "type": "PPL" }, { "name": "Memiam", "latitude": 3.46667, "longitude": 11.6, "x": 58.9, "y": 57.3, "source": "geonames", "type": "PPL" }, { "name": "Mengeme", "latitude": 3.43333, "longitude": 11.7, "x": 85.6, "y": 64.5, "source": "geonames", "type": "PPL" }, { "name": "Mfida", "latitude": 3.68333, "longitude": 11.61667, "x": 63.3, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Mindong", "latitude": 3.53333, "longitude": 11.41667, "x": 10, "y": 42.7, "source": "geonames", "type": "PPL" }, { "name": "Ndagnan", "latitude": 3.61667, "longitude": 11.58333, "x": 54.4, "y": 24.5, "source": "geonames", "type": "PPL" }, { "name": "Ndanging", "latitude": 3.56667, "longitude": 11.6, "x": 58.9, "y": 35.5, "source": "geonames", "type": "PPL" }, { "name": "Ndanging I", "latitude": 3.63333, "longitude": 11.61667, "x": 63.3, "y": 20.9, "source": "geonames", "type": "PPL" }, { "name": "Ndziefidi", "latitude": 3.65, "longitude": 11.6, "x": 58.9, "y": 17.3, "source": "geonames", "type": "PPL" }, { "name": "Ngalan", "latitude": 3.53333, "longitude": 11.51667, "x": 36.7, "y": 42.7, "source": "geonames", "type": "PPL" }, { "name": "Ngat", "latitude": 3.41667, "longitude": 11.56667, "x": 50, "y": 68.2, "source": "geonames", "type": "PPL" }, { "name": "Ngen", "latitude": 3.36667, "longitude": 11.63333, "x": 67.8, "y": 79.1, "source": "geonames", "type": "PPL" }, { "name": "Nkilnsam", "latitude": 3.55, "longitude": 11.41667, "x": 10, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Nkilzok I", "latitude": 3.68333, "longitude": 11.63333, "x": 67.8, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Nkilzok II", "latitude": 3.66667, "longitude": 11.66667, "x": 76.7, "y": 13.6, "source": "geonames", "type": "PPL" }, { "name": "Nkoebe", "latitude": 3.58333, "longitude": 11.43333, "x": 14.4, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Nkolmefou II", "latitude": 3.56667, "longitude": 11.6, "x": 58.9, "y": 35.5, "source": "geonames", "type": "PPL" }, { "name": "Nkolmelen", "latitude": 3.6, "longitude": 11.43333, "x": 14.4, "y": 28.2, "source": "geonames", "type": "PPL" }, { "name": "Nkolmeva", "latitude": 3.58333, "longitude": 11.63333, "x": 67.8, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Nkolmeyos", "latitude": 3.31667, "longitude": 11.55, "x": 45.6, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Nkolnget", "latitude": 3.48333, "longitude": 11.51667, "x": 36.7, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Nkolngi", "latitude": 3.48333, "longitude": 11.68333, "x": 81.1, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Nkolngok", "latitude": 3.51667, "longitude": 11.53333, "x": 41.1, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Nkolnsala", "latitude": 3.66667, "longitude": 11.61667, "x": 63.3, "y": 13.6, "source": "geonames", "type": "PPL" }, { "name": "Nkolnso", "latitude": 3.68333, "longitude": 11.53333, "x": 41.1, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Nkolnyama", "latitude": 3.38333, "longitude": 11.6, "x": 58.9, "y": 75.5, "source": "geonames", "type": "PPL" }, { "name": "Nkoumadzap", "latitude": 3.36667, "longitude": 11.56667, "x": 50, "y": 79.1, "source": "geonames", "type": "PPL" }, { "name": "Nsenglong", "latitude": 3.51667, "longitude": 11.48333, "x": 27.8, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Ntoumou", "latitude": 3.61667, "longitude": 11.56667, "x": 50, "y": 24.5, "source": "geonames", "type": "PPL" }, { "name": "Okode", "latitude": 3.48333, "longitude": 11.45, "x": 18.9, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Okong", "latitude": 3.6, "longitude": 11.63333, "x": 67.8, "y": 28.2, "source": "geonames", "type": "PPL" }, { "name": "Oman I", "latitude": 3.63333, "longitude": 11.48333, "x": 27.8, "y": 20.9, "source": "geonames", "type": "PPL" }, { "name": "Oman II", "latitude": 3.61667, "longitude": 11.46667, "x": 23.3, "y": 24.5, "source": "geonames", "type": "PPL" }, { "name": "Ovangou", "latitude": 3.6, "longitude": 11.53333, "x": 41.1, "y": 28.2, "source": "geonames", "type": "PPL" }, { "name": "Oving", "latitude": 3.41667, "longitude": 11.7, "x": 85.6, "y": 68.2, "source": "geonames", "type": "PPL" }, { "name": "Oyak I", "latitude": 3.48333, "longitude": 11.5, "x": 32.2, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Zamakoe", "latitude": 3.56667, "longitude": 11.5, "x": 32.2, "y": 35.5, "source": "geonames", "type": "PPL" }, { "name": "Zatoumsi", "latitude": 3.43333, "longitude": 11.51667, "x": 36.7, "y": 64.5, "source": "geonames", "type": "PPL" }, { "name": "Zoatoupsi", "latitude": 3.61667, "longitude": 11.5, "x": 32.2, "y": 24.5, "source": "geonames", "type": "PPL" } ], "Mbankomo": [ { "name": "Abang", "latitude": 3.8, "longitude": 11.3, "x": 40.5, "y": 38.6, "source": "geonames", "type": "PPL" }, { "name": "Abili", "latitude": 3.68333, "longitude": 11.46667, "x": 78.6, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Afanoya", "latitude": 3.76667, "longitude": 11.48333, "x": 82.4, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Akono", "latitude": 3.71667, "longitude": 11.36667, "x": 55.7, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Angala", "latitude": 3.83333, "longitude": 11.31667, "x": 44.3, "y": 27.1, "source": "geonames", "type": "PPL" }, { "name": "Angon", "latitude": 3.75, "longitude": 11.35, "x": 51.9, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Angongo", "latitude": 3.78333, "longitude": 11.3, "x": 40.5, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Bibe", "latitude": 3.71667, "longitude": 11.4, "x": 63.3, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Bingela I", "latitude": 3.76667, "longitude": 11.41667, "x": 67.1, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Bingela II", "latitude": 3.73333, "longitude": 11.36667, "x": 55.7, "y": 61.4, "source": "geonames", "type": "PPL" }, { "name": "Biyan", "latitude": 3.7, "longitude": 11.41667, "x": 67.1, "y": 72.9, "source": "geonames", "type": "PPL" }, { "name": "Bondjok", "latitude": 3.73333, "longitude": 11.2, "x": 17.6, "y": 61.4, "source": "geonames", "type": "PPL" }, { "name": "Ebang", "latitude": 3.7, "longitude": 11.46667, "x": 78.6, "y": 72.9, "source": "geonames", "type": "PPL" }, { "name": "Ebeba I", "latitude": 3.8, "longitude": 11.21667, "x": 21.4, "y": 38.6, "source": "geonames", "type": "PPL" }, { "name": "Ebeba II", "latitude": 3.76667, "longitude": 11.25, "x": 29, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Ebekoe", "latitude": 3.68333, "longitude": 11.48333, "x": 82.4, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Ebondege", "latitude": 3.86667, "longitude": 11.18333, "x": 13.8, "y": 15.7, "source": "geonames", "type": "PPL" }, { "name": "Edipkonbo", "latitude": 3.85, "longitude": 11.3, "x": 40.5, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Ekekam III", "latitude": 3.88333, "longitude": 11.36667, "x": 55.7, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Ekoumtik", "latitude": 3.85, "longitude": 11.21667, "x": 21.4, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Esazok II", "latitude": 3.68333, "longitude": 11.51667, "x": 90, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Etoa", "latitude": 3.76667, "longitude": 11.48333, "x": 82.4, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Eyang", "latitude": 3.88333, "longitude": 11.38333, "x": 59.5, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Kala", "latitude": 3.85, "longitude": 11.36667, "x": 55.7, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Kele", "latitude": 3.8, "longitude": 11.16667, "x": 10, "y": 38.6, "source": "geonames", "type": "PPL" }, { "name": "Koli", "latitude": 3.68333, "longitude": 11.31667, "x": 44.3, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Madzap", "latitude": 3.8, "longitude": 11.16667, "x": 10, "y": 38.6, "source": "geonames", "type": "PPL" }, { "name": "Mbaligi", "latitude": 3.73333, "longitude": 11.45, "x": 74.8, "y": 61.4, "source": "geonames", "type": "PPL" }, { "name": "Mbeyinge", "latitude": 3.71667, "longitude": 11.31667, "x": 44.3, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Mbongo", "latitude": 3.8, "longitude": 11.23333, "x": 25.2, "y": 38.6, "source": "geonames", "type": "PPL" }, { "name": "Mefomo", "latitude": 3.85, "longitude": 11.26667, "x": 32.9, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Mekoumbou", "latitude": 3.75, "longitude": 11.43333, "x": 71, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Melen", "latitude": 3.7, "longitude": 11.33333, "x": 48.1, "y": 72.9, "source": "geonames", "type": "PPL" }, { "name": "Mengong", "latitude": 3.7, "longitude": 11.45, "x": 74.8, "y": 72.9, "source": "geonames", "type": "PPL" }, { "name": "Menvini", "latitude": 3.8, "longitude": 11.28333, "x": 36.7, "y": 38.6, "source": "geonames", "type": "PPL" }, { "name": "Menyeng Adzap", "latitude": 3.66667, "longitude": 11.35, "x": 51.9, "y": 84.3, "source": "geonames", "type": "PPL" }, { "name": "Mesok I", "latitude": 3.75, "longitude": 11.3, "x": 40.5, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Mesok II", "latitude": 3.73333, "longitude": 11.26667, "x": 32.9, "y": 61.4, "source": "geonames", "type": "PPL" }, { "name": "Mian", "latitude": 3.8, "longitude": 11.33333, "x": 48.1, "y": 38.6, "source": "geonames", "type": "PPL" }, { "name": "Mingek", "latitude": 3.86667, "longitude": 11.26667, "x": 32.9, "y": 15.7, "source": "geonames", "type": "PPL" }, { "name": "Nden", "latitude": 3.81667, "longitude": 11.3, "x": 40.5, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Ngatsongo", "latitude": 3.66667, "longitude": 11.43333, "x": 71, "y": 84.3, "source": "geonames", "type": "PPL" }, { "name": "Ngoulemakong", "latitude": 3.71667, "longitude": 11.5, "x": 86.2, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Ngoumou", "latitude": 3.7, "longitude": 11.43333, "x": 71, "y": 72.9, "source": "geonames", "type": "PPL" }, { "name": "Nkadip", "latitude": 3.83333, "longitude": 11.28333, "x": 36.7, "y": 27.1, "source": "geonames", "type": "PPL" }, { "name": "Nkoabang", "latitude": 3.68333, "longitude": 11.41667, "x": 67.1, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Nkolbiyen", "latitude": 3.71667, "longitude": 11.41667, "x": 67.1, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Nkolfon", "latitude": 3.76667, "longitude": 11.43333, "x": 71, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Nkolkoumou", "latitude": 3.86667, "longitude": 11.4, "x": 63.3, "y": 15.7, "source": "geonames", "type": "PPL" }, { "name": "Nkolmenyinge", "latitude": 3.76667, "longitude": 11.31667, "x": 44.3, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Nkolmesing I", "latitude": 3.73333, "longitude": 11.46667, "x": 78.6, "y": 61.4, "source": "geonames", "type": "PPL" }, { "name": "Nkolmewout", "latitude": 3.81667, "longitude": 11.23333, "x": 25.2, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Nkolngele", "latitude": 3.83333, "longitude": 11.2, "x": 17.6, "y": 27.1, "source": "geonames", "type": "PPL" }, { "name": "Nkolnget", "latitude": 3.83333, "longitude": 11.2, "x": 17.6, "y": 27.1, "source": "geonames", "type": "PPL" }, { "name": "Nkolngok", "latitude": 3.7, "longitude": 11.38333, "x": 59.5, "y": 72.9, "source": "geonames", "type": "PPL" }, { "name": "Nkolnsam", "latitude": 3.81667, "longitude": 11.36667, "x": 55.7, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Nkolnso", "latitude": 3.71667, "longitude": 11.46667, "x": 78.6, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Nkolyege", "latitude": 3.78333, "longitude": 11.26667, "x": 32.9, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Nkomekou", "latitude": 3.85, "longitude": 11.33333, "x": 48.1, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Nkondougou I", "latitude": 3.65, "longitude": 11.4, "x": 63.3, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Nkong Bingela", "latitude": 3.73333, "longitude": 11.41667, "x": 67.1, "y": 61.4, "source": "geonames", "type": "PPL" }, { "name": "Nkongabok I", "latitude": 3.7, "longitude": 11.26667, "x": 32.9, "y": 72.9, "source": "geonames", "type": "PPL" }, { "name": "Nkongabok II", "latitude": 3.71667, "longitude": 11.25, "x": 29, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Nkongmesa", "latitude": 3.88333, "longitude": 11.31667, "x": 44.3, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Nkotnkong", "latitude": 3.78333, "longitude": 11.26667, "x": 32.9, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Nkoumadzap", "latitude": 3.75, "longitude": 11.41667, "x": 67.1, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Nlong", "latitude": 3.81667, "longitude": 11.2, "x": 17.6, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Nomayos", "latitude": 3.78333, "longitude": 11.43333, "x": 71, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Ntang", "latitude": 3.81667, "longitude": 11.25, "x": 29, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Ntouesong", "latitude": 3.71667, "longitude": 11.43333, "x": 71, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Ntoun", "latitude": 3.73333, "longitude": 11.5, "x": 86.2, "y": 61.4, "source": "geonames", "type": "PPL" }, { "name": "Nyomo", "latitude": 3.75, "longitude": 11.48333, "x": 82.4, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Obokoe", "latitude": 3.65, "longitude": 11.33333, "x": 48.1, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Okoa", "latitude": 3.8, "longitude": 11.35, "x": 51.9, "y": 38.6, "source": "geonames", "type": "PPL" }, { "name": "Okong", "latitude": 3.83333, "longitude": 11.26667, "x": 32.9, "y": 27.1, "source": "geonames", "type": "PPL" }, { "name": "Oloa", "latitude": 3.71667, "longitude": 11.5, "x": 86.2, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Ongot", "latitude": 3.85, "longitude": 11.38333, "x": 59.5, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Osokoe", "latitude": 3.66667, "longitude": 11.35, "x": 51.9, "y": 84.3, "source": "geonames", "type": "PPL" }, { "name": "Osonkia", "latitude": 3.86667, "longitude": 11.3, "x": 40.5, "y": 15.7, "source": "geonames", "type": "PPL" }, { "name": "Ototomo", "latitude": 3.65, "longitude": 11.31667, "x": 44.3, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Oveng", "latitude": 3.7, "longitude": 11.36667, "x": 55.7, "y": 72.9, "source": "geonames", "type": "PPL" }, { "name": "Ozom", "latitude": 3.68333, "longitude": 11.46667, "x": 78.6, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Sibekon", "latitude": 3.66667, "longitude": 11.25, "x": 29, "y": 84.3, "source": "geonames", "type": "PPL" }, { "name": "Tikong", "latitude": 3.86667, "longitude": 11.26667, "x": 32.9, "y": 15.7, "source": "geonames", "type": "PPL" }, { "name": "Tsek", "latitude": 3.88333, "longitude": 11.25, "x": 29, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Vegbe", "latitude": 3.73333, "longitude": 11.46667, "x": 78.6, "y": 61.4, "source": "geonames", "type": "PPL" }, { "name": "Yege", "latitude": 3.73333, "longitude": 11.26667, "x": 32.9, "y": 61.4, "source": "geonames", "type": "PPL" }, { "name": "Zoasel I", "latitude": 3.66667, "longitude": 11.4, "x": 63.3, "y": 84.3, "source": "geonames", "type": "PPL" }, { "name": "Zoatoupsi", "latitude": 3.8, "longitude": 11.4, "x": 63.3, "y": 38.6, "source": "geonames", "type": "PPL" } ], "Mbanga": [ { "name": "Alobzok", "latitude": 4.397, "longitude": 9.5712, "x": 45.8, "y": 70.9, "source": "geonames", "type": "PPL" }, { "name": "Baduma", "latitude": 4.5706, "longitude": 9.5095, "x": 27.6, "y": 15.3, "source": "geonames", "type": "PPL" }, { "name": "Beach", "latitude": 4.4374, "longitude": 9.4937, "x": 22.9, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Bombe", "latitude": 4.44202, "longitude": 9.46544, "x": 14.6, "y": 56.5, "source": "geonames", "type": "PPL" }, { "name": "Dikouma", "latitude": 4.4868, "longitude": 9.5544, "x": 40.8, "y": 42.1, "source": "geonames", "type": "PPL" }, { "name": "Kombe", "latitude": 4.4085, "longitude": 9.5687, "x": 45, "y": 67.2, "source": "geonames", "type": "PPL" }, { "name": "Kompina", "latitude": 4.354, "longitude": 9.5851, "x": 49.9, "y": 84.7, "source": "geonames", "type": "PPL" }, { "name": "Koto II", "latitude": 4.3689, "longitude": 9.5443, "x": 37.8, "y": 79.9, "source": "geonames", "type": "PPL" }, { "name": "Mbalangi", "latitude": 4.501, "longitude": 9.4632, "x": 13.9, "y": 37.6, "source": "geonames", "type": "PPL" }, { "name": "Miang", "latitude": 4.4, "longitude": 9.58333, "x": 49.4, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Mombo Beach", "latitude": 4.587, "longitude": 9.557, "x": 41.6, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Mombo Gare", "latitude": 4.5699, "longitude": 9.5845, "x": 49.7, "y": 15.5, "source": "geonames", "type": "PPL" }, { "name": "Moundek", "latitude": 4.553, "longitude": 9.5532, "x": 40.5, "y": 20.9, "source": "geonames", "type": "PPL" }, { "name": "Mouyouka I", "latitude": 4.4637, "longitude": 9.5676, "x": 44.7, "y": 49.5, "source": "geonames", "type": "PPL" }, { "name": "Mouyouka II", "latitude": 4.43723, "longitude": 9.56677, "x": 44.5, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Mundame", "latitude": 4.5559, "longitude": 9.5234, "x": 31.7, "y": 20, "source": "geonames", "type": "PPL" }, { "name": "Mwanyambe", "latitude": 4.3859, "longitude": 9.721, "x": 90, "y": 74.5, "source": "geonames", "type": "PPL" }, { "name": "Ndifo", "latitude": 4.43333, "longitude": 9.45, "x": 10, "y": 59.3, "source": "geonames", "type": "PPL" }, { "name": "Ndoh Beach", "latitude": 4.5146, "longitude": 9.5185, "x": 30.2, "y": 33.2, "source": "geonames", "type": "PPL" }, { "name": "Ndoh I", "latitude": 4.4752, "longitude": 9.5446, "x": 37.9, "y": 45.8, "source": "geonames", "type": "PPL" }, { "name": "Ndoh II", "latitude": 4.4924, "longitude": 9.5348, "x": 35, "y": 40.3, "source": "geonames", "type": "PPL" }, { "name": "Ndom", "latitude": 4.4978, "longitude": 9.5628, "x": 43.3, "y": 38.6, "source": "geonames", "type": "PPL" }, { "name": "Nkongminom", "latitude": 4.3375, "longitude": 9.5946, "x": 52.7, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Nkwangsi I", "latitude": 4.5181, "longitude": 9.5855, "x": 50, "y": 32.1, "source": "geonames", "type": "PPL" }, { "name": "Nkwangsi II", "latitude": 4.5377, "longitude": 9.5659, "x": 44.2, "y": 25.8, "source": "geonames", "type": "PPL" }, { "name": "Tangui", "latitude": 4.5388, "longitude": 9.6092, "x": 57, "y": 25.5, "source": "geonames", "type": "PPL" } ], "Mbengwi": [ { "name": "Acha Tugui", "latitude": 5.9909, "longitude": 9.943, "x": 56.5, "y": 75.7, "source": "geonames", "type": "PPL" }, { "name": "Atvebe", "latitude": 6.09511, "longitude": 9.86677, "x": 34.2, "y": 42.7, "source": "geonames", "type": "PPL" }, { "name": "Bakwa", "latitude": 6.1309, "longitude": 9.8392, "x": 26.2, "y": 31.3, "source": "geonames", "type": "PPL" }, { "name": "Baminje", "latitude": 6.05687, "longitude": 9.81492, "x": 19.1, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Bamundum", "latitude": 6.16181, "longitude": 9.94836, "x": 58, "y": 21.5, "source": "geonames", "type": "PPL" }, { "name": "Barimbong", "latitude": 6.1981, "longitude": 9.9015, "x": 44.4, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Batebi", "latitude": 6.08564, "longitude": 9.80835, "x": 17.2, "y": 45.7, "source": "geonames", "type": "PPL" }, { "name": "Bechebat", "latitude": 6.1038, "longitude": 9.96664, "x": 63.4, "y": 39.9, "source": "geonames", "type": "PPL" }, { "name": "Bome", "latitude": 5.97591, "longitude": 10.00104, "x": 73.4, "y": 80.5, "source": "geonames", "type": "PPL" }, { "name": "Chup", "latitude": 6.1425, "longitude": 9.9263, "x": 51.6, "y": 27.6, "source": "geonames", "type": "PPL" }, { "name": "Djindong", "latitude": 6.0323, "longitude": 9.9925, "x": 70.9, "y": 62.6, "source": "geonames", "type": "PPL" }, { "name": "Djlmbi", "latitude": 6.0053, "longitude": 9.9696, "x": 64.2, "y": 71.1, "source": "geonames", "type": "PPL" }, { "name": "Ekpiri", "latitude": 6.1274, "longitude": 9.8328, "x": 24.3, "y": 32.4, "source": "geonames", "type": "PPL" }, { "name": "Etwii", "latitude": 5.9911, "longitude": 9.8574, "x": 31.5, "y": 75.6, "source": "geonames", "type": "PPL" }, { "name": "Fonam", "latitude": 5.98046, "longitude": 9.99174, "x": 70.7, "y": 79, "source": "geonames", "type": "PPL" }, { "name": "Fringen", "latitude": 6.1747, "longitude": 9.9309, "x": 52.9, "y": 17.4, "source": "geonames", "type": "PPL" }, { "name": "Gangeu", "latitude": 6.12104, "longitude": 9.94021, "x": 55.7, "y": 34.4, "source": "geonames", "type": "PPL" }, { "name": "Gonoku", "latitude": 6.0292, "longitude": 9.9807, "x": 67.5, "y": 63.6, "source": "geonames", "type": "PPL" }, { "name": "Gumben", "latitude": 6.11524, "longitude": 9.99631, "x": 72, "y": 36.3, "source": "geonames", "type": "PPL" }, { "name": "Gundom", "latitude": 5.9969, "longitude": 9.9654, "x": 63, "y": 73.8, "source": "geonames", "type": "PPL" }, { "name": "Jimda", "latitude": 5.9625, "longitude": 9.961, "x": 61.7, "y": 84.7, "source": "geonames", "type": "PPL" }, { "name": "Kondo", "latitude": 6.1693, "longitude": 9.8666, "x": 34.2, "y": 19.1, "source": "geonames", "type": "PPL" }, { "name": "Kopi Yang", "latitude": 5.97543, "longitude": 10.02053, "x": 79.1, "y": 80.6, "source": "geonames", "type": "PPL" }, { "name": "Kutin", "latitude": 6.04387, "longitude": 9.7836, "x": 10, "y": 58.9, "source": "geonames", "type": "PPL" }, { "name": "Mana", "latitude": 6.1378, "longitude": 9.9821, "x": 67.9, "y": 29.1, "source": "geonames", "type": "PPL" }, { "name": "Mankon", "latitude": 5.97473, "longitude": 10.05804, "x": 90, "y": 80.8, "source": "geonames", "type": "PPL" }, { "name": "Mbot", "latitude": 6.1695, "longitude": 9.9618, "x": 61.9, "y": 19.1, "source": "geonames", "type": "PPL" }, { "name": "Mengen Mbo", "latitude": 5.95, "longitude": 9.96667, "x": 63.4, "y": 88.7, "source": "geonames", "type": "PPL" }, { "name": "Nebeba", "latitude": 6.1504, "longitude": 9.9741, "x": 65.5, "y": 25.1, "source": "geonames", "type": "PPL" }, { "name": "Ngembo", "latitude": 5.96387, "longitude": 10.03763, "x": 84.1, "y": 84.3, "source": "geonames", "type": "PPL" }, { "name": "Ngonu", "latitude": 6.07026, "longitude": 9.79324, "x": 12.8, "y": 50.5, "source": "geonames", "type": "PPL" }, { "name": "Ngwo", "latitude": 6.0953, "longitude": 9.8103, "x": 17.8, "y": 42.6, "source": "geonames", "type": "PPL" }, { "name": "Ngwokwong", "latitude": 5.9747, "longitude": 9.912, "x": 47.4, "y": 80.8, "source": "geonames", "type": "PPL" }, { "name": "Niko", "latitude": 6.1383, "longitude": 9.8868, "x": 40.1, "y": 29, "source": "geonames", "type": "PPL" }, { "name": "Ntah Etuh", "latitude": 5.9544, "longitude": 9.9497, "x": 58.4, "y": 87.3, "source": "geonames", "type": "PPL" }, { "name": "Ntali", "latitude": 5.9641, "longitude": 9.9394, "x": 55.4, "y": 84.2, "source": "geonames", "type": "PPL" }, { "name": "Nyen", "latitude": 6.0044, "longitude": 9.9857, "x": 68.9, "y": 71.4, "source": "geonames", "type": "PPL" }, { "name": "Oshie", "latitude": 6.1273, "longitude": 9.8681, "x": 34.6, "y": 32.4, "source": "geonames", "type": "PPL" }, { "name": "Sabri", "latitude": 6.0758, "longitude": 9.7911, "x": 12.2, "y": 48.8, "source": "geonames", "type": "PPL" }, { "name": "Tinachong", "latitude": 6.03636, "longitude": 9.87612, "x": 37, "y": 61.3, "source": "geonames", "type": "PPL" }, { "name": "Tinaku", "latitude": 5.9899, "longitude": 9.8887, "x": 40.6, "y": 76, "source": "geonames", "type": "PPL" }, { "name": "Tonenja", "latitude": 5.9915, "longitude": 9.9224, "x": 50.5, "y": 75.5, "source": "geonames", "type": "PPL" }, { "name": "Tudig", "latitude": 5.9769, "longitude": 9.9521, "x": 59.1, "y": 80.1, "source": "geonames", "type": "PPL" }, { "name": "Tugyi", "latitude": 6.0831, "longitude": 9.8977, "x": 43.3, "y": 46.5, "source": "geonames", "type": "PPL" }, { "name": "Zang Tabi", "latitude": 5.9458, "longitude": 9.9136, "x": 47.9, "y": 90, "source": "geonames", "type": "PPL" } ], "Mbouda": [ { "name": "Baamong", "latitude": 5.68132, "longitude": 10.30372, "x": 70.9, "y": 28.7, "source": "geonames", "type": "PPL" }, { "name": "Babadjou", "latitude": 5.67153, "longitude": 10.17918, "x": 22.8, "y": 33.8, "source": "geonames", "type": "PPL" }, { "name": "Babete", "latitude": 5.6, "longitude": 10.26667, "x": 56.6, "y": 71.6, "source": "geonames", "type": "PPL" }, { "name": "Bafomgha", "latitude": 5.67318, "longitude": 10.25299, "x": 51.3, "y": 33, "source": "geonames", "type": "PPL" }, { "name": "Bagam", "latitude": 5.69093, "longitude": 10.29323, "x": 66.8, "y": 23.6, "source": "geonames", "type": "PPL" }, { "name": "Bagombo I", "latitude": 5.58707, "longitude": 10.25257, "x": 51.1, "y": 78.4, "source": "geonames", "type": "PPL" }, { "name": "Bagombo II", "latitude": 5.59274, "longitude": 10.26362, "x": 55.4, "y": 75.4, "source": "geonames", "type": "PPL" }, { "name": "Bagombo III", "latitude": 5.59249, "longitude": 10.25485, "x": 52, "y": 75.6, "source": "geonames", "type": "PPL" }, { "name": "Bakoga", "latitude": 5.63228, "longitude": 10.29958, "x": 69.3, "y": 54.6, "source": "geonames", "type": "PPL" }, { "name": "Balaafi", "latitude": 5.63669, "longitude": 10.32369, "x": 78.6, "y": 52.2, "source": "geonames", "type": "PPL" }, { "name": "Balatet", "latitude": 5.66036, "longitude": 10.34721, "x": 87.7, "y": 39.7, "source": "geonames", "type": "PPL" }, { "name": "Bali-Bagam", "latitude": 5.71667, "longitude": 10.26667, "x": 56.6, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Baloum", "latitude": 5.63012, "longitude": 10.35327, "x": 90, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Bamefa", "latitude": 5.62353, "longitude": 10.34375, "x": 86.3, "y": 59.2, "source": "geonames", "type": "PPL" }, { "name": "Bamekoupere", "latitude": 5.67107, "longitude": 10.27285, "x": 59, "y": 34.1, "source": "geonames", "type": "PPL" }, { "name": "Bamelong", "latitude": 5.63284, "longitude": 10.25818, "x": 53.3, "y": 54.3, "source": "geonames", "type": "PPLX" }, { "name": "Bamendjinda", "latitude": 5.60888, "longitude": 10.29418, "x": 67.2, "y": 66.9, "source": "geonames", "type": "PPL" }, { "name": "Bamenka", "latitude": 5.63431, "longitude": 10.33574, "x": 83.2, "y": 53.5, "source": "geonames", "type": "PPL" }, { "name": "Bamenkombo", "latitude": 5.61993, "longitude": 10.31956, "x": 77, "y": 61.1, "source": "geonames", "type": "PPL" }, { "name": "Bamessingoe", "latitude": 5.65454, "longitude": 10.23491, "x": 44.3, "y": 42.8, "source": "geonames", "type": "PPL" }, { "name": "Bamesso", "latitude": 5.6422, "longitude": 10.32978, "x": 80.9, "y": 49.3, "source": "geonames", "type": "PPL" }, { "name": "Bametchela", "latitude": 5.60703, "longitude": 10.29161, "x": 66.2, "y": 67.9, "source": "geonames", "type": "PPL" }, { "name": "Bameteu", "latitude": 5.59001, "longitude": 10.27723, "x": 60.7, "y": 76.9, "source": "geonames", "type": "PPL" }, { "name": "Bametiem", "latitude": 5.60825, "longitude": 10.3273, "x": 80, "y": 67.3, "source": "geonames", "type": "PPL" }, { "name": "Bamougong", "latitude": 5.59546, "longitude": 10.21463, "x": 36.5, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Bandjeudi", "latitude": 5.57475, "longitude": 10.32747, "x": 80, "y": 84.9, "source": "geonames", "type": "PPL" }, { "name": "Bandjisso I", "latitude": 5.58465, "longitude": 10.31581, "x": 75.5, "y": 79.7, "source": "geonames", "type": "PPL" }, { "name": "Bandjisso II", "latitude": 5.59143, "longitude": 10.31605, "x": 75.6, "y": 76.1, "source": "geonames", "type": "PPL" }, { "name": "Bandjitsi", "latitude": 5.56518, "longitude": 10.33913, "x": 84.5, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Bandjouvang", "latitude": 5.56875, "longitude": 10.30592, "x": 71.7, "y": 88.1, "source": "geonames", "type": "PPL" }, { "name": "Bangakou", "latitude": 5.63786, "longitude": 10.29296, "x": 66.7, "y": 51.6, "source": "geonames", "type": "PPL" }, { "name": "Bankassang", "latitude": 5.59626, "longitude": 10.28242, "x": 62.7, "y": 73.6, "source": "geonames", "type": "PPL" }, { "name": "Banok", "latitude": 5.61356, "longitude": 10.28385, "x": 63.2, "y": 64.5, "source": "geonames", "type": "PPL" }, { "name": "Bantang", "latitude": 5.60693, "longitude": 10.25701, "x": 52.9, "y": 68, "source": "geonames", "type": "PPL" }, { "name": "Bantchiepa", "latitude": 5.63372, "longitude": 10.31786, "x": 76.3, "y": 53.8, "source": "geonames", "type": "PPL" }, { "name": "Batangoua", "latitude": 5.63141, "longitude": 10.32641, "x": 79.6, "y": 55, "source": "geonames", "type": "PPL" }, { "name": "Batchicha", "latitude": 5.59609, "longitude": 10.31214, "x": 74.1, "y": 73.7, "source": "geonames", "type": "PPL" }, { "name": "Bato", "latitude": 5.6364, "longitude": 10.31056, "x": 73.5, "y": 52.4, "source": "geonames", "type": "PPL" }, { "name": "Batoula I", "latitude": 5.57884, "longitude": 10.29854, "x": 68.9, "y": 82.8, "source": "geonames", "type": "PPL" }, { "name": "Batoula II", "latitude": 5.57214, "longitude": 10.3064, "x": 71.9, "y": 86.3, "source": "geonames", "type": "PPL" }, { "name": "Batounda", "latitude": 5.56831, "longitude": 10.32124, "x": 77.6, "y": 88.3, "source": "geonames", "type": "PPL" }, { "name": "Batoundja", "latitude": 5.56612, "longitude": 10.31183, "x": 74, "y": 89.5, "source": "geonames", "type": "PPL" }, { "name": "Batoussi", "latitude": 5.63152, "longitude": 10.2884, "x": 65, "y": 55, "source": "geonames", "type": "PPL" }, { "name": "Batsela II", "latitude": 5.57064, "longitude": 10.29151, "x": 66.2, "y": 87.1, "source": "geonames", "type": "PPL" }, { "name": "Bawa", "latitude": 5.66573, "longitude": 10.1711, "x": 19.7, "y": 36.9, "source": "geonames", "type": "PPL" }, { "name": "Bodjinle", "latitude": 5.62925, "longitude": 10.31194, "x": 74, "y": 56.2, "source": "geonames", "type": "PPL" }, { "name": "Bondjifeu", "latitude": 5.64674, "longitude": 10.32043, "x": 77.3, "y": 46.9, "source": "geonames", "type": "PPL" }, { "name": "Botchiepa", "latitude": 5.61474, "longitude": 10.30687, "x": 72.1, "y": 63.8, "source": "geonames", "type": "PPL" }, { "name": "Djitcha", "latitude": 5.66696, "longitude": 10.16056, "x": 15.6, "y": 36.3, "source": "geonames", "type": "PPL" }, { "name": "Faghui", "latitude": 5.69451, "longitude": 10.23659, "x": 45, "y": 21.7, "source": "geonames", "type": "PPL" }, { "name": "Fanzui", "latitude": 5.6633, "longitude": 10.23601, "x": 44.7, "y": 38.2, "source": "geonames", "type": "PPL" }, { "name": "Fela", "latitude": 5.65181, "longitude": 10.24797, "x": 49.4, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Fomgha", "latitude": 5.67642, "longitude": 10.24056, "x": 46.5, "y": 31.3, "source": "geonames", "type": "PPL" }, { "name": "Kamsie", "latitude": 5.61827, "longitude": 10.3094, "x": 73.1, "y": 62, "source": "geonames", "type": "PPL" }, { "name": "Kie", "latitude": 5.63645, "longitude": 10.16084, "x": 15.7, "y": 52.4, "source": "geonames", "type": "PPL" }, { "name": "King-Place", "latitude": 5.60965, "longitude": 10.28942, "x": 65.4, "y": 66.5, "source": "geonames", "type": "PPL" }, { "name": "Koum", "latitude": 5.60023, "longitude": 10.22501, "x": 40.5, "y": 71.5, "source": "geonames", "type": "PPL" }, { "name": "La'tchue", "latitude": 5.60171, "longitude": 10.23816, "x": 45.6, "y": 70.7, "source": "geonames", "type": "PPL" }, { "name": "Laatet I", "latitude": 5.61416, "longitude": 10.2548, "x": 52, "y": 64.1, "source": "geonames", "type": "PPLX" }, { "name": "Laatet II", "latitude": 5.61118, "longitude": 10.2699, "x": 57.8, "y": 65.7, "source": "geonames", "type": "PPL" }, { "name": "Laatet III", "latitude": 5.60905, "longitude": 10.27324, "x": 59.1, "y": 66.8, "source": "geonames", "type": "PPL" }, { "name": "Ladjeuti", "latitude": 5.6274, "longitude": 10.14598, "x": 10, "y": 57.1, "source": "geonames", "type": "PPL" }, { "name": "Ladjeutsa", "latitude": 5.62665, "longitude": 10.20411, "x": 32.4, "y": 57.5, "source": "geonames", "type": "PPL" }, { "name": "Lafotio", "latitude": 5.61667, "longitude": 10.21667, "x": 37.3, "y": 62.8, "source": "geonames", "type": "PPL" }, { "name": "Leng", "latitude": 5.68545, "longitude": 10.21959, "x": 38.4, "y": 26.5, "source": "geonames", "type": "PPL" }, { "name": "Maka", "latitude": 5.64079, "longitude": 10.15168, "x": 12.2, "y": 50.1, "source": "geonames", "type": "PPL" }, { "name": "Manka", "latitude": 5.67594, "longitude": 10.22898, "x": 42, "y": 31.5, "source": "geonames", "type": "PPL" }, { "name": "Mantcha", "latitude": 5.58727, "longitude": 10.28887, "x": 65.1, "y": 78.3, "source": "geonames", "type": "PPL" }, { "name": "Mbatap", "latitude": 5.61808, "longitude": 10.3166, "x": 75.8, "y": 62.1, "source": "geonames", "type": "PPL" }, { "name": "Mbefo", "latitude": 5.70309, "longitude": 10.31602, "x": 75.6, "y": 17.2, "source": "geonames", "type": "PPL" }, { "name": "Mbepehe", "latitude": 5.68482, "longitude": 10.28835, "x": 64.9, "y": 26.8, "source": "geonames", "type": "PPL" }, { "name": "Mbessa", "latitude": 5.70938, "longitude": 10.3033, "x": 70.7, "y": 13.8, "source": "geonames", "type": "PPL" }, { "name": "Mbetsouo", "latitude": 5.69985, "longitude": 10.28978, "x": 65.5, "y": 18.9, "source": "geonames", "type": "PPL" }, { "name": "Mbeve", "latitude": 5.70536, "longitude": 10.29164, "x": 66.2, "y": 16, "source": "geonames", "type": "PPL" }, { "name": "Mbotim", "latitude": 5.63946, "longitude": 10.32661, "x": 79.7, "y": 50.8, "source": "geonames", "type": "PPL" }, { "name": "Mboumetio", "latitude": 5.62564, "longitude": 10.29404, "x": 67.1, "y": 58.1, "source": "geonames", "type": "PPL" }, { "name": "Mbwe", "latitude": 5.62462, "longitude": 10.23772, "x": 45.4, "y": 58.6, "source": "geonames", "type": "PPL" }, { "name": "Mekagok", "latitude": 5.69154, "longitude": 10.32316, "x": 78.4, "y": 23.3, "source": "geonames", "type": "PPL" }, { "name": "Melo", "latitude": 5.67357, "longitude": 10.16803, "x": 18.5, "y": 32.8, "source": "geonames", "type": "PPL" }, { "name": "Mendou", "latitude": 5.66815, "longitude": 10.22052, "x": 38.8, "y": 35.6, "source": "geonames", "type": "PPL" }, { "name": "Mendousso", "latitude": 5.68864, "longitude": 10.19221, "x": 27.8, "y": 24.8, "source": "geonames", "type": "PPL" }, { "name": "Menkwe", "latitude": 5.69705, "longitude": 10.20343, "x": 32.2, "y": 20.4, "source": "geonames", "type": "PPL" }, { "name": "Mentsa", "latitude": 5.59947, "longitude": 10.20437, "x": 32.5, "y": 71.9, "source": "geonames", "type": "PPL" }, { "name": "Mepa", "latitude": 5.69465, "longitude": 10.18647, "x": 25.6, "y": 21.6, "source": "geonames", "type": "PPL" }, { "name": "Metap", "latitude": 5.64824, "longitude": 10.23883, "x": 45.8, "y": 46.1, "source": "geonames", "type": "PPL" }, { "name": "Metio", "latitude": 5.61704, "longitude": 10.24155, "x": 46.9, "y": 62.6, "source": "geonames", "type": "PPL" }, { "name": "Mifi", "latitude": 5.69032, "longitude": 10.33563, "x": 83.2, "y": 23.9, "source": "geonames", "type": "PPL" }, { "name": "Mogatchio", "latitude": 5.60809, "longitude": 10.31587, "x": 75.6, "y": 67.3, "source": "geonames", "type": "PPL" }, { "name": "Monbap", "latitude": 5.69932, "longitude": 10.30278, "x": 70.5, "y": 19.2, "source": "geonames", "type": "PPL" }, { "name": "Montchio", "latitude": 5.61739, "longitude": 10.27147, "x": 58.4, "y": 62.4, "source": "geonames", "type": "PPLX" }, { "name": "Monye", "latitude": 5.65688, "longitude": 10.20649, "x": 33.4, "y": 41.6, "source": "geonames", "type": "PPL" }, { "name": "Motchitcha", "latitude": 5.63122, "longitude": 10.27402, "x": 59.4, "y": 55.1, "source": "geonames", "type": "PPL" }, { "name": "Nenegou I", "latitude": 5.5865, "longitude": 10.26932, "x": 57.6, "y": 78.7, "source": "geonames", "type": "PPL" }, { "name": "Nenegou II", "latitude": 5.5756, "longitude": 10.27712, "x": 60.6, "y": 84.5, "source": "geonames", "type": "PPL" }, { "name": "Ngamotchui", "latitude": 5.60717, "longitude": 10.27982, "x": 61.7, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Ngouadeng", "latitude": 5.64593, "longitude": 10.17185, "x": 20, "y": 47.4, "source": "geonames", "type": "PPL" }, { "name": "Nguekong", "latitude": 5.65943, "longitude": 10.18199, "x": 23.9, "y": 40.2, "source": "geonames", "type": "PPL" }, { "name": "Ntialong", "latitude": 5.61131, "longitude": 10.18252, "x": 24.1, "y": 65.6, "source": "geonames", "type": "PPL" }, { "name": "Ntim", "latitude": 5.62449, "longitude": 10.21892, "x": 38.1, "y": 58.7, "source": "geonames", "type": "PPL" }, { "name": "Ntsa", "latitude": 5.6021, "longitude": 10.1951, "x": 29, "y": 70.5, "source": "geonames", "type": "PPL" }, { "name": "Nzem-Tendzing", "latitude": 5.6163, "longitude": 10.337, "x": 83.7, "y": 63, "source": "geonames", "type": "PPL" }, { "name": "Nzenepa", "latitude": 5.57239, "longitude": 10.28135, "x": 62.2, "y": 86.2, "source": "geonames", "type": "PPL" }, { "name": "Panteng", "latitude": 5.61343, "longitude": 10.26002, "x": 54, "y": 64.5, "source": "geonames", "type": "PPL" }, { "name": "Poneki", "latitude": 5.64513, "longitude": 10.28822, "x": 64.9, "y": 47.8, "source": "geonames", "type": "PPL" }, { "name": "Seo", "latitude": 5.67285, "longitude": 10.19641, "x": 29.5, "y": 33.1, "source": "geonames", "type": "PPL" }, { "name": "Siguem", "latitude": 5.61307, "longitude": 10.32981, "x": 80.9, "y": 64.7, "source": "geonames", "type": "PPL" }, { "name": "Singue", "latitude": 5.63804, "longitude": 10.24309, "x": 47.5, "y": 51.5, "source": "geonames", "type": "PPL" }, { "name": "Tcheusso I", "latitude": 5.59563, "longitude": 10.29868, "x": 68.9, "y": 73.9, "source": "geonames", "type": "PPL" }, { "name": "Tchinla", "latitude": 5.68108, "longitude": 10.24188, "x": 47, "y": 28.8, "source": "geonames", "type": "PPL" }, { "name": "Tchuentim", "latitude": 5.61479, "longitude": 10.2349, "x": 44.3, "y": 63.8, "source": "geonames", "type": "PPL" }, { "name": "Tchuesso II", "latitude": 5.60087, "longitude": 10.32742, "x": 80, "y": 71.2, "source": "geonames", "type": "PPL" }, { "name": "Tio", "latitude": 5.58847, "longitude": 10.26494, "x": 55.9, "y": 77.7, "source": "geonames", "type": "PPL" }, { "name": "To'ochoussong", "latitude": 5.61262, "longitude": 10.21985, "x": 38.5, "y": 64.9, "source": "geonames", "type": "PPL" }, { "name": "Tomene", "latitude": 5.63401, "longitude": 10.15104, "x": 12, "y": 53.7, "source": "geonames", "type": "PPL" }, { "name": "Toumanka", "latitude": 5.67879, "longitude": 10.20918, "x": 34.4, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Tounga", "latitude": 5.6561, "longitude": 10.18897, "x": 26.6, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Tousso", "latitude": 5.60355, "longitude": 10.30327, "x": 70.7, "y": 69.7, "source": "geonames", "type": "PPL" }, { "name": "Toussongle", "latitude": 5.64775, "longitude": 10.19807, "x": 30.1, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Tsingla'", "latitude": 5.59525, "longitude": 10.23134, "x": 42.9, "y": 74.1, "source": "geonames", "type": "PPL" }, { "name": "Zintchue", "latitude": 5.63032, "longitude": 10.18416, "x": 24.7, "y": 55.6, "source": "geonames", "type": "PPL" } ], "Meiganga": [ { "name": "Amadzili", "latitude": 6.65, "longitude": 14.33333, "x": 72.9, "y": 20.4, "source": "geonames", "type": "PPL" }, { "name": "Baina", "latitude": 6.4, "longitude": 14.28333, "x": 55.7, "y": 72.6, "source": "geonames", "type": "PPL" }, { "name": "Dana", "latitude": 6.4, "longitude": 14.3, "x": 61.4, "y": 72.6, "source": "geonames", "type": "PPL" }, { "name": "Danboura", "latitude": 6.63333, "longitude": 14.28333, "x": 55.7, "y": 23.9, "source": "geonames", "type": "PPL" }, { "name": "Dankali", "latitude": 6.4, "longitude": 14.25, "x": 44.3, "y": 72.6, "source": "geonames", "type": "PPL" }, { "name": "Dengin", "latitude": 6.33333, "longitude": 14.33333, "x": 72.9, "y": 86.5, "source": "geonames", "type": "PPL" }, { "name": "Dir", "latitude": 6.31667, "longitude": 14.35, "x": 78.6, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Gangi", "latitude": 6.58333, "longitude": 14.25, "x": 44.3, "y": 34.3, "source": "geonames", "type": "PPL" }, { "name": "Gazi", "latitude": 6.35, "longitude": 14.33333, "x": 72.9, "y": 83, "source": "geonames", "type": "PPL" }, { "name": "Gbata", "latitude": 6.7, "longitude": 14.38333, "x": 90, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Goro", "latitude": 6.4, "longitude": 14.28333, "x": 55.7, "y": 72.6, "source": "geonames", "type": "PPL" }, { "name": "Goumbela", "latitude": 6.66667, "longitude": 14.2, "x": 27.1, "y": 17, "source": "geonames", "type": "PPL" }, { "name": "Kpagounge", "latitude": 6.48333, "longitude": 14.28333, "x": 55.7, "y": 55.2, "source": "geonames", "type": "PPL" }, { "name": "Laka", "latitude": 6.33333, "longitude": 14.35, "x": 78.6, "y": 86.5, "source": "geonames", "type": "PPL" }, { "name": "Lokoti", "latitude": 6.36667, "longitude": 14.33333, "x": 72.9, "y": 79.6, "source": "geonames", "type": "PPL" }, { "name": "Mbarang", "latitude": 6.7, "longitude": 14.36667, "x": 84.3, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Meidougou", "latitude": 6.41667, "longitude": 14.21667, "x": 32.9, "y": 69.1, "source": "geonames", "type": "PPL" }, { "name": "Nandeke", "latitude": 6.46667, "longitude": 14.26667, "x": 50, "y": 58.7, "source": "geonames", "type": "PPL" }, { "name": "Nandeke Barki", "latitude": 6.68333, "longitude": 14.38333, "x": 90, "y": 13.5, "source": "geonames", "type": "PPL" }, { "name": "Samaki", "latitude": 6.46667, "longitude": 14.15, "x": 10, "y": 58.7, "source": "geonames", "type": "PPL" }, { "name": "Yende", "latitude": 6.31667, "longitude": 14.35, "x": 78.6, "y": 90, "source": "geonames", "type": "PPL" } ], "Melong": [ { "name": "Banguem", "latitude": 5.0856, "longitude": 9.7696, "x": 17.6, "y": 71.6, "source": "geonames", "type": "PPL" }, { "name": "Bayon", "latitude": 5.05163, "longitude": 9.96118, "x": 69.9, "y": 84.9, "source": "geonames", "type": "PPL" }, { "name": "Eboukou", "latitude": 5.04659, "longitude": 9.96984, "x": 72.3, "y": 86.9, "source": "geonames", "type": "PPL" }, { "name": "Ediango", "latitude": 5.20775, "longitude": 9.77507, "x": 19.1, "y": 23.8, "source": "geonames", "type": "PPL" }, { "name": "Ekambeng", "latitude": 5.0639, "longitude": 9.7933, "x": 24.1, "y": 80.1, "source": "geonames", "type": "PPL" }, { "name": "Ekangte", "latitude": 5.06298, "longitude": 9.81314, "x": 29.5, "y": 80.5, "source": "geonames", "type": "PPL" }, { "name": "Ekanjok", "latitude": 5.0994, "longitude": 9.7416, "x": 10, "y": 66.2, "source": "geonames", "type": "PPL" }, { "name": "Ekolbouni", "latitude": 5.11362, "longitude": 9.86271, "x": 43, "y": 60.6, "source": "geonames", "type": "PPL" }, { "name": "Ekolkang", "latitude": 5.062, "longitude": 9.9131, "x": 56.8, "y": 80.8, "source": "geonames", "type": "PPL" }, { "name": "Ekom Nkam", "latitude": 5.06365, "longitude": 10.01593, "x": 84.9, "y": 80.2, "source": "geonames", "type": "PPL" }, { "name": "Essekou", "latitude": 5.2198, "longitude": 9.9168, "x": 57.8, "y": 19, "source": "geonames", "type": "PPL" }, { "name": "Lelem Mangwete", "latitude": 5.15054, "longitude": 9.971, "x": 72.6, "y": 46.2, "source": "geonames", "type": "PPL" }, { "name": "Makim", "latitude": 5.107, "longitude": 9.758, "x": 14.5, "y": 63.2, "source": "geonames", "type": "PPL" }, { "name": "Mama", "latitude": 5.2429, "longitude": 9.8175, "x": 30.7, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Mangwete", "latitude": 5.23437, "longitude": 9.93109, "x": 61.7, "y": 13.3, "source": "geonames", "type": "PPL" }, { "name": "Mankouat", "latitude": 5.13981, "longitude": 9.86619, "x": 44, "y": 50.4, "source": "geonames", "type": "PPL" }, { "name": "Manyet", "latitude": 5.134, "longitude": 9.7876, "x": 22.6, "y": 52.6, "source": "geonames", "type": "PPL" }, { "name": "Mbat", "latitude": 5.0598, "longitude": 9.8104, "x": 28.8, "y": 81.7, "source": "geonames", "type": "PPL" }, { "name": "Mbila", "latitude": 5.1425, "longitude": 9.7627, "x": 15.8, "y": 49.3, "source": "geonames", "type": "PPL" }, { "name": "Mboango", "latitude": 5.0745, "longitude": 9.88997, "x": 50.5, "y": 75.9, "source": "geonames", "type": "PPL" }, { "name": "Mboendang", "latitude": 5.1, "longitude": 9.95, "x": 66.9, "y": 66, "source": "geonames", "type": "PPL" }, { "name": "Mboko", "latitude": 5.0941, "longitude": 9.7808, "x": 20.7, "y": 68.3, "source": "geonames", "type": "PPL" }, { "name": "Mbokola", "latitude": 5.09798, "longitude": 9.93782, "x": 63.5, "y": 66.7, "source": "geonames", "type": "PPL" }, { "name": "Mbomouango", "latitude": 5.09593, "longitude": 9.95134, "x": 67.2, "y": 67.6, "source": "geonames", "type": "PPL" }, { "name": "Mbouassoum", "latitude": 5.13155, "longitude": 9.81553, "x": 30.2, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Mboue", "latitude": 5.06667, "longitude": 10, "x": 80.5, "y": 79, "source": "geonames", "type": "PPL" }, { "name": "Mbouendoum", "latitude": 5.14142, "longitude": 9.91658, "x": 57.8, "y": 49.7, "source": "geonames", "type": "PPL" }, { "name": "Mbouroukou", "latitude": 5.0572, "longitude": 9.89242, "x": 51.2, "y": 82.7, "source": "geonames", "type": "PPL" }, { "name": "Melong II", "latitude": 5.0571, "longitude": 9.9629, "x": 70.4, "y": 82.8, "source": "geonames", "type": "PPL" }, { "name": "Mouanguel", "latitude": 5.1, "longitude": 9.85, "x": 39.6, "y": 66, "source": "geonames", "type": "PPL" }, { "name": "Mounko", "latitude": 5.04126, "longitude": 10.03476, "x": 90, "y": 89, "source": "geonames", "type": "PPL" }, { "name": "Muabi", "latitude": 5.0548, "longitude": 9.8235, "x": 32.3, "y": 83.7, "source": "geonames", "type": "PPL" }, { "name": "Muanjekan", "latitude": 5.15275, "longitude": 9.7801, "x": 20.5, "y": 45.3, "source": "geonames", "type": "PPL" }, { "name": "Ndidiang", "latitude": 5.08499, "longitude": 9.92204, "x": 59.2, "y": 71.8, "source": "geonames", "type": "PPL" }, { "name": "Ndjop", "latitude": 5.15, "longitude": 9.78333, "x": 21.4, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Ndokou", "latitude": 5.11283, "longitude": 9.82558, "x": 32.9, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Ndung", "latitude": 5.0811, "longitude": 9.8053, "x": 27.4, "y": 73.4, "source": "geonames", "type": "PPL" }, { "name": "New Melong", "latitude": 5.1099, "longitude": 9.9402, "x": 64.2, "y": 62.1, "source": "geonames", "type": "PPL" }, { "name": "Ngal", "latitude": 5.06667, "longitude": 9.9, "x": 53.2, "y": 79, "source": "geonames", "type": "PPL" }, { "name": "Ninong", "latitude": 5.18517, "longitude": 9.79764, "x": 25.3, "y": 32.6, "source": "geonames", "type": "PPL" }, { "name": "Njijno I", "latitude": 5.11623, "longitude": 9.88471, "x": 49.1, "y": 59.6, "source": "geonames", "type": "PPL" }, { "name": "Njinjo II", "latitude": 5.113, "longitude": 9.90413, "x": 54.4, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Njom", "latitude": 5.0738, "longitude": 9.7632, "x": 15.9, "y": 76.2, "source": "geonames", "type": "PPL" }, { "name": "Nkiko", "latitude": 5.08776, "longitude": 9.80449, "x": 27.2, "y": 70.7, "source": "geonames", "type": "PPL" }, { "name": "Nkongsoung", "latitude": 5.1352, "longitude": 9.9874, "x": 77.1, "y": 52.2, "source": "geonames", "type": "PPL" }, { "name": "Nlonkou", "latitude": 5.07187, "longitude": 9.95612, "x": 68.5, "y": 77, "source": "geonames", "type": "PPL" }, { "name": "Nteho", "latitude": 5.1166, "longitude": 9.7433, "x": 10.5, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Nyabang", "latitude": 5.0697, "longitude": 9.934, "x": 62.5, "y": 77.8, "source": "geonames", "type": "PPL" }, { "name": "Nyam", "latitude": 5.1028, "longitude": 9.7983, "x": 25.5, "y": 64.9, "source": "geonames", "type": "PPL" }, { "name": "Passim", "latitude": 5.0386, "longitude": 9.9523, "x": 67.5, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Soundop", "latitude": 5.04103, "longitude": 9.99144, "x": 78.2, "y": 89, "source": "geonames", "type": "PPL" } ], "Mfou": [ { "name": "Abang I", "latitude": 3.76667, "longitude": 11.86667, "x": 37.7, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Abanga", "latitude": 3.83333, "longitude": 11.8, "x": 25.4, "y": 77.2, "source": "geonames", "type": "PPL" }, { "name": "Adzenzoumou", "latitude": 3.9, "longitude": 11.86667, "x": 37.7, "y": 64.4, "source": "geonames", "type": "PPL" }, { "name": "Akak", "latitude": 3.88333, "longitude": 11.95, "x": 53.1, "y": 67.6, "source": "geonames", "type": "PPL" }, { "name": "Akekela", "latitude": 3.9, "longitude": 11.83333, "x": 31.5, "y": 64.4, "source": "geonames", "type": "PPL" }, { "name": "Akomkada", "latitude": 3.98333, "longitude": 11.98333, "x": 59.2, "y": 48.4, "source": "geonames", "type": "PPL" }, { "name": "Akondo", "latitude": 3.98333, "longitude": 11.93333, "x": 50, "y": 48.4, "source": "geonames", "type": "PPL" }, { "name": "Akpak", "latitude": 4.11667, "longitude": 11.96667, "x": 56.2, "y": 22.8, "source": "geonames", "type": "PPL" }, { "name": "Awae", "latitude": 3.88333, "longitude": 11.88333, "x": 40.8, "y": 67.6, "source": "geonames", "type": "PPL" }, { "name": "Ayene", "latitude": 3.9, "longitude": 11.98333, "x": 59.2, "y": 64.4, "source": "geonames", "type": "PPL" }, { "name": "Ayos", "latitude": 3.83333, "longitude": 11.85, "x": 34.6, "y": 77.2, "source": "geonames", "type": "PPL" }, { "name": "Bekoudou", "latitude": 3.95, "longitude": 11.81667, "x": 28.5, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Bikong", "latitude": 4.01667, "longitude": 11.78333, "x": 22.3, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Bikoto", "latitude": 4.01667, "longitude": 12.11667, "x": 83.8, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Bikoue", "latitude": 4.11667, "longitude": 11.91667, "x": 46.9, "y": 22.8, "source": "geonames", "type": "PPL" }, { "name": "Biviang", "latitude": 3.86667, "longitude": 11.9, "x": 43.8, "y": 70.8, "source": "geonames", "type": "PPL" }, { "name": "Ebabot", "latitude": 3.76667, "longitude": 11.9, "x": 43.8, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ebodinkou", "latitude": 3.85, "longitude": 11.96667, "x": 56.2, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Ebodnkou", "latitude": 3.78333, "longitude": 11.88333, "x": 40.8, "y": 86.8, "source": "geonames", "type": "PPL" }, { "name": "Ebodoumou", "latitude": 4.08333, "longitude": 12, "x": 62.3, "y": 29.2, "source": "geonames", "type": "PPL" }, { "name": "Ebogo", "latitude": 4.15, "longitude": 11.86667, "x": 37.7, "y": 16.4, "source": "geonames", "type": "PPL" }, { "name": "Ebok", "latitude": 3.78333, "longitude": 11.83333, "x": 31.5, "y": 86.8, "source": "geonames", "type": "PPL" }, { "name": "Ebolakounou", "latitude": 3.93333, "longitude": 12.13333, "x": 86.9, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Ebolsi", "latitude": 3.91667, "longitude": 11.8, "x": 25.4, "y": 61.2, "source": "geonames", "type": "PPL" }, { "name": "Ebolzock", "latitude": 4.05, "longitude": 12, "x": 62.3, "y": 35.6, "source": "geonames", "type": "PPL" }, { "name": "Efandi", "latitude": 3.83333, "longitude": 11.76667, "x": 19.2, "y": 77.2, "source": "geonames", "type": "PPL" }, { "name": "Ekiembie", "latitude": 3.88333, "longitude": 11.83333, "x": 31.5, "y": 67.6, "source": "geonames", "type": "PPL" }, { "name": "Ekodongo", "latitude": 3.81667, "longitude": 11.85, "x": 34.6, "y": 80.4, "source": "geonames", "type": "PPL" }, { "name": "Ekoum Douma", "latitude": 4.15, "longitude": 12.03333, "x": 68.5, "y": 16.4, "source": "geonames", "type": "PPL" }, { "name": "Emini", "latitude": 4.1, "longitude": 12.01667, "x": 65.4, "y": 26, "source": "geonames", "type": "PPL" }, { "name": "Emvan", "latitude": 4.03333, "longitude": 12.11667, "x": 83.8, "y": 38.8, "source": "geonames", "type": "PPL" }, { "name": "Endoum", "latitude": 3.78333, "longitude": 11.85, "x": 34.6, "y": 86.8, "source": "geonames", "type": "PPL" }, { "name": "Esaminsang", "latitude": 3.91667, "longitude": 11.76667, "x": 19.2, "y": 61.2, "source": "geonames", "type": "PPL" }, { "name": "Esse", "latitude": 4.08333, "longitude": 11.88333, "x": 40.8, "y": 29.2, "source": "geonames", "type": "PPL" }, { "name": "Etetana", "latitude": 4, "longitude": 11.86667, "x": 37.7, "y": 45.2, "source": "geonames", "type": "PPL" }, { "name": "Etoutoua", "latitude": 4.08333, "longitude": 11.81667, "x": 28.5, "y": 29.2, "source": "geonames", "type": "PPL" }, { "name": "Ewot", "latitude": 4.13333, "longitude": 11.95, "x": 53.1, "y": 19.6, "source": "geonames", "type": "PPL" }, { "name": "Fon", "latitude": 3.78333, "longitude": 11.81667, "x": 28.5, "y": 86.8, "source": "geonames", "type": "PPL" }, { "name": "Kaa", "latitude": 4.05, "longitude": 12.08333, "x": 77.7, "y": 35.6, "source": "geonames", "type": "PPL" }, { "name": "Kokoa", "latitude": 4.1, "longitude": 12.1, "x": 80.8, "y": 26, "source": "geonames", "type": "PPL" }, { "name": "Kombo", "latitude": 3.98333, "longitude": 11.96667, "x": 56.2, "y": 48.4, "source": "geonames", "type": "PPL" }, { "name": "Koutou", "latitude": 4.08333, "longitude": 11.8, "x": 25.4, "y": 29.2, "source": "geonames", "type": "PPL" }, { "name": "Libi", "latitude": 3.93333, "longitude": 11.86667, "x": 37.7, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Longo", "latitude": 4.16667, "longitude": 11.85, "x": 34.6, "y": 13.2, "source": "geonames", "type": "PPL" }, { "name": "Mbamayok", "latitude": 4.11667, "longitude": 11.86667, "x": 37.7, "y": 22.8, "source": "geonames", "type": "PPL" }, { "name": "Mbelalen", "latitude": 3.88333, "longitude": 11.86667, "x": 37.7, "y": 67.6, "source": "geonames", "type": "PPL" }, { "name": "Mbenoa", "latitude": 4.06667, "longitude": 11.9, "x": 43.8, "y": 32.4, "source": "geonames", "type": "PPL" }, { "name": "Mbesi", "latitude": 4.11667, "longitude": 11.8, "x": 25.4, "y": 22.8, "source": "geonames", "type": "PPL" }, { "name": "Mbok", "latitude": 3.8, "longitude": 11.95, "x": 53.1, "y": 83.6, "source": "geonames", "type": "PPL" }, { "name": "Mboun", "latitude": 3.83333, "longitude": 11.93333, "x": 50, "y": 77.2, "source": "geonames", "type": "PPL" }, { "name": "Meban", "latitude": 4.03333, "longitude": 12, "x": 62.3, "y": 38.8, "source": "geonames", "type": "PPL" }, { "name": "Mebang", "latitude": 3.86667, "longitude": 11.78333, "x": 22.3, "y": 70.8, "source": "geonames", "type": "PPL" }, { "name": "Meboe", "latitude": 4.08333, "longitude": 11.76667, "x": 19.2, "y": 29.2, "source": "geonames", "type": "PPL" }, { "name": "Mengala", "latitude": 4.15, "longitude": 11.95, "x": 53.1, "y": 16.4, "source": "geonames", "type": "PPL" }, { "name": "Mengang", "latitude": 3.88333, "longitude": 12.05, "x": 71.5, "y": 67.6, "source": "geonames", "type": "PPL" }, { "name": "Mengossa", "latitude": 4.15, "longitude": 12.05, "x": 71.5, "y": 16.4, "source": "geonames", "type": "PPL" }, { "name": "Menyoumekombo", "latitude": 3.96667, "longitude": 11.78333, "x": 22.3, "y": 51.6, "source": "geonames", "type": "PPL" }, { "name": "Meven", "latitude": 3.83333, "longitude": 11.76667, "x": 19.2, "y": 77.2, "source": "geonames", "type": "PPL" }, { "name": "Mevo Mevo", "latitude": 4.01667, "longitude": 12.01667, "x": 65.4, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Mewoulou", "latitude": 3.85, "longitude": 11.95, "x": 53.1, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Mfandena", "latitude": 4.08333, "longitude": 11.88333, "x": 40.8, "y": 29.2, "source": "geonames", "type": "PPL" }, { "name": "Mimbang", "latitude": 3.9, "longitude": 12.01667, "x": 65.4, "y": 64.4, "source": "geonames", "type": "PPL" }, { "name": "Minbang", "latitude": 3.85, "longitude": 12.06667, "x": 74.6, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Mindombo", "latitude": 4, "longitude": 11.81667, "x": 28.5, "y": 45.2, "source": "geonames", "type": "PPL" }, { "name": "Mindzie", "latitude": 3.83333, "longitude": 11.83333, "x": 31.5, "y": 77.2, "source": "geonames", "type": "PPL" }, { "name": "Mingeme", "latitude": 4.08333, "longitude": 11.93333, "x": 50, "y": 29.2, "source": "geonames", "type": "PPL" }, { "name": "Minkomilala", "latitude": 4.03333, "longitude": 11.8, "x": 25.4, "y": 38.8, "source": "geonames", "type": "PPL" }, { "name": "Minlaba", "latitude": 3.83333, "longitude": 11.86667, "x": 37.7, "y": 77.2, "source": "geonames", "type": "PPL" }, { "name": "Moumou", "latitude": 3.78333, "longitude": 11.98333, "x": 59.2, "y": 86.8, "source": "geonames", "type": "PPL" }, { "name": "Mvolo", "latitude": 3.98333, "longitude": 12.03333, "x": 68.5, "y": 48.4, "source": "geonames", "type": "PPL" }, { "name": "Mvom", "latitude": 4.18333, "longitude": 11.91667, "x": 46.9, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Mying", "latitude": 4.05, "longitude": 11.91667, "x": 46.9, "y": 35.6, "source": "geonames", "type": "PPL" }, { "name": "Ndibesong", "latitude": 3.88333, "longitude": 11.76667, "x": 19.2, "y": 67.6, "source": "geonames", "type": "PPL" }, { "name": "Nditsi", "latitude": 4.13333, "longitude": 12.01667, "x": 65.4, "y": 19.6, "source": "geonames", "type": "PPL" }, { "name": "Ndombo", "latitude": 4.01667, "longitude": 11.91667, "x": 46.9, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Ngat", "latitude": 3.85, "longitude": 11.98333, "x": 59.2, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Ngat I", "latitude": 4.05, "longitude": 11.85, "x": 34.6, "y": 35.6, "source": "geonames", "type": "PPL" }, { "name": "Ngat II", "latitude": 4.01667, "longitude": 11.88333, "x": 40.8, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Nginda", "latitude": 3.85, "longitude": 11.91667, "x": 46.9, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Ngoantet", "latitude": 3.98333, "longitude": 11.88333, "x": 40.8, "y": 48.4, "source": "geonames", "type": "PPL" }, { "name": "Ngona", "latitude": 3.91667, "longitude": 11.96667, "x": 56.2, "y": 61.2, "source": "geonames", "type": "PPL" }, { "name": "Ngonwa", "latitude": 4.03333, "longitude": 11.96667, "x": 56.2, "y": 38.8, "source": "geonames", "type": "PPL" }, { "name": "Ngose", "latitude": 3.95, "longitude": 11.91667, "x": 46.9, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Ngougoumou", "latitude": 4.13333, "longitude": 12.03333, "x": 68.5, "y": 19.6, "source": "geonames", "type": "PPL" }, { "name": "Ngoulminanga", "latitude": 3.76667, "longitude": 11.93333, "x": 50, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ngoundou", "latitude": 4.06667, "longitude": 11.85, "x": 34.6, "y": 32.4, "source": "geonames", "type": "PPL" }, { "name": "Nguinda", "latitude": 4.05, "longitude": 12.1, "x": 80.8, "y": 35.6, "source": "geonames", "type": "PPL" }, { "name": "Niato", "latitude": 3.81667, "longitude": 11.98333, "x": 59.2, "y": 80.4, "source": "geonames", "type": "PPL" }, { "name": "Nkolato", "latitude": 3.91667, "longitude": 11.78333, "x": 22.3, "y": 61.2, "source": "geonames", "type": "PPL" }, { "name": "Nkolavolo I", "latitude": 4.06667, "longitude": 11.86667, "x": 37.7, "y": 32.4, "source": "geonames", "type": "PPL" }, { "name": "Nkolavolo II", "latitude": 4.05, "longitude": 11.83333, "x": 31.5, "y": 35.6, "source": "geonames", "type": "PPL" }, { "name": "Nkolba", "latitude": 3.98333, "longitude": 11.76667, "x": 19.2, "y": 48.4, "source": "geonames", "type": "PPL" }, { "name": "Nkolbebe", "latitude": 3.76667, "longitude": 11.95, "x": 53.1, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Nkolbek", "latitude": 3.98333, "longitude": 12.08333, "x": 77.7, "y": 48.4, "source": "geonames", "type": "PPL" }, { "name": "Nkolesson", "latitude": 4.08333, "longitude": 12.08333, "x": 77.7, "y": 29.2, "source": "geonames", "type": "PPL" }, { "name": "Nkolnget", "latitude": 3.88333, "longitude": 11.91667, "x": 46.9, "y": 67.6, "source": "geonames", "type": "PPL" }, { "name": "Nkolombonde", "latitude": 4.1, "longitude": 11.96667, "x": 56.2, "y": 26, "source": "geonames", "type": "PPL" }, { "name": "Nkomesebe", "latitude": 3.88333, "longitude": 11.78333, "x": 22.3, "y": 67.6, "source": "geonames", "type": "PPL" }, { "name": "Nkomeyo", "latitude": 4.08333, "longitude": 11.98333, "x": 59.2, "y": 29.2, "source": "geonames", "type": "PPL" }, { "name": "Nkongmelen", "latitude": 3.85, "longitude": 11.76667, "x": 19.2, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Nlong", "latitude": 3.95, "longitude": 11.75, "x": 16.2, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Nomayos", "latitude": 4.03333, "longitude": 11.88333, "x": 40.8, "y": 38.8, "source": "geonames", "type": "PPL" }, { "name": "Nsebe", "latitude": 3.78333, "longitude": 11.96667, "x": 56.2, "y": 86.8, "source": "geonames", "type": "PPL" }, { "name": "Nseosou", "latitude": 3.8, "longitude": 11.96667, "x": 56.2, "y": 83.6, "source": "geonames", "type": "PPL" }, { "name": "Nsimi", "latitude": 4.13333, "longitude": 11.88333, "x": 40.8, "y": 19.6, "source": "geonames", "type": "PPL" }, { "name": "Obok", "latitude": 3.95, "longitude": 12.01667, "x": 65.4, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Odoudouma", "latitude": 3.95, "longitude": 11.86667, "x": 37.7, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Ofoumselek", "latitude": 3.98333, "longitude": 11.71667, "x": 10, "y": 48.4, "source": "geonames", "type": "PPL" }, { "name": "Okoyen", "latitude": 3.8, "longitude": 11.81667, "x": 28.5, "y": 83.6, "source": "geonames", "type": "PPL" }, { "name": "Olela", "latitude": 3.98333, "longitude": 11.81667, "x": 28.5, "y": 48.4, "source": "geonames", "type": "PPL" }, { "name": "Olengina", "latitude": 3.96667, "longitude": 12.01667, "x": 65.4, "y": 51.6, "source": "geonames", "type": "PPL" }, { "name": "Olom", "latitude": 3.78333, "longitude": 11.9, "x": 43.8, "y": 86.8, "source": "geonames", "type": "PPL" }, { "name": "Omgbang", "latitude": 3.8, "longitude": 11.9, "x": 43.8, "y": 83.6, "source": "geonames", "type": "PPL" }, { "name": "Ongandi", "latitude": 4.01667, "longitude": 11.76667, "x": 19.2, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Ongbwang", "latitude": 3.98333, "longitude": 12.15, "x": 90, "y": 48.4, "source": "geonames", "type": "PPL" }, { "name": "Sangela", "latitude": 3.96667, "longitude": 11.98333, "x": 59.2, "y": 51.6, "source": "geonames", "type": "PPL" }, { "name": "Tom", "latitude": 3.9, "longitude": 11.75, "x": 16.2, "y": 64.4, "source": "geonames", "type": "PPL" }, { "name": "Top", "latitude": 3.9, "longitude": 12, "x": 62.3, "y": 64.4, "source": "geonames", "type": "PPL" }, { "name": "Zili", "latitude": 3.91667, "longitude": 11.81667, "x": 28.5, "y": 61.2, "source": "geonames", "type": "PPL" }, { "name": "Zoankom", "latitude": 3.91667, "longitude": 11.86667, "x": 37.7, "y": 61.2, "source": "geonames", "type": "PPL" } ], "Mindif": [ { "name": "Balbemba", "latitude": 10.47006, "longitude": 14.3585, "x": 30.1, "y": 32.2, "source": "geonames", "type": "PPL" }, { "name": "Baldaouar", "latitude": 10.33884, "longitude": 14.29342, "x": 13, "y": 69.2, "source": "geonames", "type": "PPL" }, { "name": "Bani-Bani", "latitude": 10.42569, "longitude": 14.28401, "x": 10.6, "y": 44.7, "source": "geonames", "type": "PPL" }, { "name": "Bantao", "latitude": 10.28278, "longitude": 14.48364, "x": 63, "y": 85, "source": "geonames", "type": "PPL" }, { "name": "Bembel", "latitude": 10.44715, "longitude": 14.47741, "x": 61.4, "y": 38.7, "source": "geonames", "type": "PPL" }, { "name": "Boloa", "latitude": 10.41546, "longitude": 14.44442, "x": 52.7, "y": 47.6, "source": "geonames", "type": "PPL" }, { "name": "BongorFoulbe", "latitude": 10.41187, "longitude": 14.42665, "x": 48, "y": 48.6, "source": "geonames", "type": "PPL" }, { "name": "Bonjouroho", "latitude": 10.40121, "longitude": 14.33846, "x": 24.9, "y": 51.6, "source": "geonames", "type": "PPL" }, { "name": "Bouloungol", "latitude": 10.48777, "longitude": 14.39011, "x": 38.4, "y": 27.2, "source": "geonames", "type": "PPL" }, { "name": "Bourleo", "latitude": 10.30611, "longitude": 14.48707, "x": 63.9, "y": 78.4, "source": "geonames", "type": "PPL" }, { "name": "Bridjeo", "latitude": 10.40792, "longitude": 14.34418, "x": 26.4, "y": 49.7, "source": "geonames", "type": "PPL" }, { "name": "Dambay", "latitude": 10.512, "longitude": 14.53743, "x": 77.1, "y": 20.4, "source": "geonames", "type": "PPL" }, { "name": "Debazao", "latitude": 10.33874, "longitude": 14.29718, "x": 14, "y": 69.2, "source": "geonames", "type": "PPL" }, { "name": "Didiou", "latitude": 10.44513, "longitude": 14.36319, "x": 31.4, "y": 39.2, "source": "geonames", "type": "PPL" }, { "name": "Dir Bagarmire", "latitude": 10.3551, "longitude": 14.48279, "x": 62.8, "y": 64.6, "source": "geonames", "type": "PPL" }, { "name": "Dir Irlagare", "latitude": 10.33244, "longitude": 14.46507, "x": 58.1, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Djagroum", "latitude": 10.47045, "longitude": 14.39653, "x": 40.1, "y": 32.1, "source": "geonames", "type": "PPL" }, { "name": "Djamboutou", "latitude": 10.3741, "longitude": 14.34321, "x": 26.1, "y": 59.3, "source": "geonames", "type": "PPL" }, { "name": "Djamhoura", "latitude": 10.32548, "longitude": 14.42369, "x": 47.2, "y": 73, "source": "geonames", "type": "PPL" }, { "name": "Djapay", "latitude": 10.43946, "longitude": 14.32149, "x": 20.4, "y": 40.8, "source": "geonames", "type": "PPL" }, { "name": "Djengal", "latitude": 10.44278, "longitude": 14.29635, "x": 13.8, "y": 39.9, "source": "geonames", "type": "PPL" }, { "name": "DjodjonOuawoudje", "latitude": 10.47805, "longitude": 14.35179, "x": 28.4, "y": 29.9, "source": "geonames", "type": "PPL" }, { "name": "Djogobe", "latitude": 10.49111, "longitude": 14.39758, "x": 40.4, "y": 26.3, "source": "geonames", "type": "PPL" }, { "name": "Djokole", "latitude": 10.41915, "longitude": 14.42892, "x": 48.6, "y": 46.6, "source": "geonames", "type": "PPL" }, { "name": "Djoubao", "latitude": 10.45432, "longitude": 14.36578, "x": 32, "y": 36.6, "source": "geonames", "type": "PPL" }, { "name": "Djoutaouande", "latitude": 10.50432, "longitude": 14.41034, "x": 43.7, "y": 22.5, "source": "geonames", "type": "PPL" }, { "name": "Dolandio", "latitude": 10.31731, "longitude": 14.48527, "x": 63.4, "y": 75.3, "source": "geonames", "type": "PPL" }, { "name": "Domayo", "latitude": 10.32916, "longitude": 14.4601, "x": 56.8, "y": 71.9, "source": "geonames", "type": "PPL" }, { "name": "Domayo Matfay", "latitude": 10.48702, "longitude": 14.40234, "x": 41.6, "y": 27.4, "source": "geonames", "type": "PPL" }, { "name": "Doubazao", "latitude": 10.33694, "longitude": 14.45722, "x": 56.1, "y": 69.7, "source": "geonames", "type": "PPL" }, { "name": "Doulda", "latitude": 10.44287, "longitude": 14.32791, "x": 22.1, "y": 39.9, "source": "geonames", "type": "PPL" }, { "name": "Douldalade", "latitude": 10.43117, "longitude": 14.34412, "x": 26.3, "y": 43.2, "source": "geonames", "type": "PPL" }, { "name": "Douyera", "latitude": 10.40167, "longitude": 14.49001, "x": 64.7, "y": 51.5, "source": "geonames", "type": "PPL" }, { "name": "Doyang", "latitude": 10.30236, "longitude": 14.48792, "x": 64.1, "y": 79.5, "source": "geonames", "type": "PPL" }, { "name": "Fakabodjyel", "latitude": 10.29538, "longitude": 14.4867, "x": 63.8, "y": 81.5, "source": "geonames", "type": "PPL" }, { "name": "Fakakourou", "latitude": 10.35426, "longitude": 14.46312, "x": 57.6, "y": 64.9, "source": "geonames", "type": "PPL" }, { "name": "Foftouroho", "latitude": 10.40827, "longitude": 14.33497, "x": 23.9, "y": 49.6, "source": "geonames", "type": "PPL" }, { "name": "Fouguere", "latitude": 10.29499, "longitude": 14.48147, "x": 62.4, "y": 81.6, "source": "geonames", "type": "PPL" }, { "name": "Gabani", "latitude": 10.52946, "longitude": 14.43747, "x": 50.9, "y": 15.5, "source": "geonames", "type": "PPL" }, { "name": "Gadamayo", "latitude": 10.38942, "longitude": 14.33978, "x": 25.2, "y": 54.9, "source": "geonames", "type": "PPL" }, { "name": "Gadibo", "latitude": 10.43468, "longitude": 14.28188, "x": 10, "y": 42.2, "source": "geonames", "type": "PPL" }, { "name": "Gadore", "latitude": 10.47414, "longitude": 14.3827, "x": 36.5, "y": 31.1, "source": "geonames", "type": "PPL" }, { "name": "Gagadje", "latitude": 10.38942, "longitude": 14.55581, "x": 82, "y": 54.9, "source": "geonames", "type": "PPL" }, { "name": "Galare", "latitude": 10.52536, "longitude": 14.43202, "x": 49.4, "y": 16.6, "source": "geonames", "type": "PPL" }, { "name": "Gaviang", "latitude": 10.37671, "longitude": 14.28691, "x": 11.3, "y": 58.5, "source": "geonames", "type": "PPL" }, { "name": "Gay Gay", "latitude": 10.43581, "longitude": 14.51895, "x": 72.3, "y": 41.9, "source": "geonames", "type": "PPL" }, { "name": "Gay Gay Koumaire", "latitude": 10.48454, "longitude": 14.56681, "x": 84.8, "y": 28.1, "source": "geonames", "type": "PPL" }, { "name": "Gay Gay Maoundire", "latitude": 10.45876, "longitude": 14.53883, "x": 77.5, "y": 35.4, "source": "geonames", "type": "PPL" }, { "name": "Goray", "latitude": 10.52696, "longitude": 14.48071, "x": 62.2, "y": 16.2, "source": "geonames", "type": "PPL" }, { "name": "Gouroum", "latitude": 10.52261, "longitude": 14.48756, "x": 64, "y": 17.4, "source": "geonames", "type": "PPL" }, { "name": "Gramjiao", "latitude": 10.43991, "longitude": 14.34495, "x": 26.6, "y": 40.7, "source": "geonames", "type": "PPL" }, { "name": "Guegueray", "latitude": 10.45434, "longitude": 14.49456, "x": 65.9, "y": 36.6, "source": "geonames", "type": "PPL" }, { "name": "Guileroho", "latitude": 10.38891, "longitude": 14.43903, "x": 51.3, "y": 55.1, "source": "geonames", "type": "PPL" }, { "name": "Hardeo", "latitude": 10.52239, "longitude": 14.4358, "x": 50.4, "y": 17.4, "source": "geonames", "type": "PPL" }, { "name": "Hopo", "latitude": 10.39108, "longitude": 14.34745, "x": 27.2, "y": 54.5, "source": "geonames", "type": "PPL" }, { "name": "Kaheo", "latitude": 10.54881, "longitude": 14.4694, "x": 59.3, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Kataki", "latitude": 10.51382, "longitude": 14.49381, "x": 65.7, "y": 19.9, "source": "geonames", "type": "PPL" }, { "name": "Katchel", "latitude": 10.43652, "longitude": 14.44783, "x": 53.6, "y": 41.7, "source": "geonames", "type": "PPL" }, { "name": "Kessouo", "latitude": 10.41404, "longitude": 14.48998, "x": 64.7, "y": 48, "source": "geonames", "type": "PPL" }, { "name": "Larie", "latitude": 10.48039, "longitude": 14.43202, "x": 49.4, "y": 29.3, "source": "geonames", "type": "PPL" }, { "name": "Lelkay", "latitude": 10.31155, "longitude": 14.48503, "x": 63.4, "y": 76.9, "source": "geonames", "type": "PPL" }, { "name": "Loubour", "latitude": 10.43768, "longitude": 14.35181, "x": 28.4, "y": 41.3, "source": "geonames", "type": "PPL" }, { "name": "Louga", "latitude": 10.40488, "longitude": 14.45483, "x": 55.4, "y": 50.6, "source": "geonames", "type": "PPL" }, { "name": "Loupeo", "latitude": 10.41511, "longitude": 14.46616, "x": 58.4, "y": 47.7, "source": "geonames", "type": "PPL" }, { "name": "Madjawoula", "latitude": 10.50806, "longitude": 14.42691, "x": 48.1, "y": 21.5, "source": "geonames", "type": "PPL" }, { "name": "Matfay", "latitude": 10.48134, "longitude": 14.39007, "x": 38.4, "y": 29, "source": "geonames", "type": "PPL" }, { "name": "Mayel Modibo", "latitude": 10.37378, "longitude": 14.43131, "x": 49.3, "y": 59.3, "source": "geonames", "type": "PPL" }, { "name": "Mayo Bahehel", "latitude": 10.44033, "longitude": 14.3616, "x": 30.9, "y": 40.6, "source": "geonames", "type": "PPL" }, { "name": "Mborora", "latitude": 10.29161, "longitude": 14.38612, "x": 37.4, "y": 82.5, "source": "geonames", "type": "PPL" }, { "name": "Medjin", "latitude": 10.43806, "longitude": 14.34668, "x": 27, "y": 41.2, "source": "geonames", "type": "PPL" }, { "name": "Meleme", "latitude": 10.50164, "longitude": 14.50674, "x": 69.1, "y": 23.3, "source": "geonames", "type": "PPL" }, { "name": "Memeyel", "latitude": 10.4989, "longitude": 14.40272, "x": 41.7, "y": 24.1, "source": "geonames", "type": "PPL" }, { "name": "Metched", "latitude": 10.38024, "longitude": 14.34705, "x": 27.1, "y": 57.5, "source": "geonames", "type": "PPL" }, { "name": "Mindio", "latitude": 10.33742, "longitude": 14.44041, "x": 51.6, "y": 69.6, "source": "geonames", "type": "PPL" }, { "name": "Modjom Bodi", "latitude": 10.31413, "longitude": 14.39911, "x": 40.8, "y": 76.2, "source": "geonames", "type": "PPL" }, { "name": "Mogom", "latitude": 10.51684, "longitude": 14.42537, "x": 47.7, "y": 19, "source": "geonames", "type": "PPL" }, { "name": "Mondi", "latitude": 10.4217, "longitude": 14.46039, "x": 56.9, "y": 45.8, "source": "geonames", "type": "PPL" }, { "name": "Nanikalou", "latitude": 10.4988, "longitude": 14.38471, "x": 37, "y": 24.1, "source": "geonames", "type": "PPL" }, { "name": "Ndomayo", "latitude": 10.3934, "longitude": 14.44516, "x": 52.9, "y": 53.8, "source": "geonames", "type": "PPL" }, { "name": "Ngaba", "latitude": 10.465, "longitude": 14.37213, "x": 33.7, "y": 33.6, "source": "geonames", "type": "PPL" }, { "name": "Ngaroua", "latitude": 10.43004, "longitude": 14.58645, "x": 90, "y": 43.5, "source": "geonames", "type": "PPL" }, { "name": "Nya-Nya", "latitude": 10.47219, "longitude": 14.36057, "x": 30.7, "y": 31.6, "source": "geonames", "type": "PPL" }, { "name": "Oakaldou", "latitude": 10.53659, "longitude": 14.48335, "x": 62.9, "y": 13.4, "source": "geonames", "type": "PPL" }, { "name": "Olango", "latitude": 10.44864, "longitude": 14.36804, "x": 32.6, "y": 38.2, "source": "geonames", "type": "PPL" }, { "name": "Ouokandou", "latitude": 10.3055, "longitude": 14.48308, "x": 62.8, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Ouro Dole", "latitude": 10.41716, "longitude": 14.35811, "x": 30, "y": 47.1, "source": "geonames", "type": "PPL" }, { "name": "Ouro Guilaho", "latitude": 10.37948, "longitude": 14.45539, "x": 55.6, "y": 57.7, "source": "geonames", "type": "PPL" }, { "name": "Ouro Mala", "latitude": 10.49157, "longitude": 14.51198, "x": 70.4, "y": 26.1, "source": "geonames", "type": "PPL" }, { "name": "Ouro Zangui", "latitude": 10.51119, "longitude": 14.52203, "x": 73.1, "y": 20.6, "source": "geonames", "type": "PPL" }, { "name": "OuroBeli", "latitude": 10.5297, "longitude": 14.48184, "x": 62.5, "y": 15.4, "source": "geonames", "type": "PPL" }, { "name": "OuroKaIgama", "latitude": 10.51, "longitude": 14.41524, "x": 45, "y": 20.9, "source": "geonames", "type": "PPL" }, { "name": "OuroMansour", "latitude": 10.35349, "longitude": 14.41905, "x": 46, "y": 65.1, "source": "geonames", "type": "PPL" }, { "name": "Palakonde", "latitude": 10.44092, "longitude": 14.29477, "x": 13.4, "y": 40.4, "source": "geonames", "type": "PPL" }, { "name": "Petene", "latitude": 10.26506, "longitude": 14.39322, "x": 39.2, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Poleo", "latitude": 10.4471, "longitude": 14.32315, "x": 20.8, "y": 38.7, "source": "geonames", "type": "PPL" }, { "name": "Sabongari", "latitude": 10.36715, "longitude": 14.38761, "x": 37.8, "y": 61.2, "source": "geonames", "type": "PPL" }, { "name": "Sarma", "latitude": 10.41133, "longitude": 14.28443, "x": 10.7, "y": 48.8, "source": "geonames", "type": "PPL" }, { "name": "Soumaki", "latitude": 10.31397, "longitude": 14.47832, "x": 61.6, "y": 76.2, "source": "geonames", "type": "PPL" }, { "name": "Talie", "latitude": 10.44193, "longitude": 14.31429, "x": 18.5, "y": 40.1, "source": "geonames", "type": "PPL" }, { "name": "Talindou", "latitude": 10.51969, "longitude": 14.49337, "x": 65.6, "y": 18.2, "source": "geonames", "type": "PPL" }, { "name": "Tapareo", "latitude": 10.42867, "longitude": 14.48024, "x": 62.1, "y": 43.9, "source": "geonames", "type": "PPL" }, { "name": "Tchakadjao", "latitude": 10.41559, "longitude": 14.34971, "x": 27.8, "y": 47.6, "source": "geonames", "type": "PPL" }, { "name": "Tchaouake", "latitude": 10.41149, "longitude": 14.47772, "x": 61.4, "y": 48.7, "source": "geonames", "type": "PPL" }, { "name": "Tchokola", "latitude": 10.5386, "longitude": 14.49657, "x": 66.4, "y": 12.9, "source": "geonames", "type": "PPL" }, { "name": "Toango", "latitude": 10.50411, "longitude": 14.42101, "x": 46.5, "y": 22.6, "source": "geonames", "type": "PPL" }, { "name": "Tondewo", "latitude": 10.47957, "longitude": 14.36906, "x": 32.9, "y": 29.5, "source": "geonames", "type": "PPL" }, { "name": "Tondjio", "latitude": 10.41808, "longitude": 14.48226, "x": 62.6, "y": 46.9, "source": "geonames", "type": "PPL" }, { "name": "Torou", "latitude": 10.43501, "longitude": 14.36604, "x": 32.1, "y": 42.1, "source": "geonames", "type": "PPL" }, { "name": "Toupeouo", "latitude": 10.54831, "longitude": 14.46663, "x": 58.5, "y": 10.1, "source": "geonames", "type": "PPL" }, { "name": "Vaza", "latitude": 10.35832, "longitude": 14.32902, "x": 22.4, "y": 63.7, "source": "geonames", "type": "PPL" }, { "name": "Walassin", "latitude": 10.44883, "longitude": 14.30493, "x": 16.1, "y": 38.2, "source": "geonames", "type": "PPL" }, { "name": "Walde Tapare", "latitude": 10.35819, "longitude": 14.39196, "x": 38.9, "y": 63.7, "source": "geonames", "type": "PPL" }, { "name": "Walidjom", "latitude": 10.52777, "longitude": 14.48572, "x": 63.5, "y": 15.9, "source": "geonames", "type": "PPL" }, { "name": "Yakang", "latitude": 10.42959, "longitude": 14.28893, "x": 11.9, "y": 43.6, "source": "geonames", "type": "PPL" }, { "name": "Yaragoto", "latitude": 10.47406, "longitude": 14.36304, "x": 31.3, "y": 31.1, "source": "geonames", "type": "PPL" }, { "name": "Yolelbe", "latitude": 10.43021, "longitude": 14.28542, "x": 10.9, "y": 43.4, "source": "geonames", "type": "PPL" }, { "name": "Zalao", "latitude": 10.44703, "longitude": 14.35552, "x": 29.3, "y": 38.7, "source": "geonames", "type": "PPL" }, { "name": "Zokole", "latitude": 10.4287, "longitude": 14.44349, "x": 52.4, "y": 43.9, "source": "geonames", "type": "PPL" }, { "name": "Zouenkne", "latitude": 10.32727, "longitude": 14.45392, "x": 55.2, "y": 72.5, "source": "geonames", "type": "PPL" }, { "name": "Zouzoui", "latitude": 10.33563, "longitude": 14.29696, "x": 14, "y": 70.1, "source": "geonames", "type": "PPL" } ], "Minta": [ { "name": "Afanoveng", "latitude": 4.73333, "longitude": 12.83333, "x": 54.8, "y": 22.3, "source": "geonames", "type": "PPL" }, { "name": "Badoume II", "latitude": 4.5, "longitude": 12.93333, "x": 74, "y": 65.4, "source": "geonames", "type": "PPL" }, { "name": "Bamelap", "latitude": 4.51667, "longitude": 12.95, "x": 77.2, "y": 62.3, "source": "geonames", "type": "PPL" }, { "name": "Berkong", "latitude": 4.65, "longitude": 12.6, "x": 10, "y": 37.7, "source": "geonames", "type": "PPL" }, { "name": "Bika", "latitude": 4.56667, "longitude": 13, "x": 86.8, "y": 53.1, "source": "geonames", "type": "PPL" }, { "name": "Ebah", "latitude": 4.48333, "longitude": 12.91667, "x": 70.8, "y": 68.5, "source": "geonames", "type": "PPL" }, { "name": "Ebangal", "latitude": 4.6, "longitude": 12.93333, "x": 74, "y": 46.9, "source": "geonames", "type": "PPL" }, { "name": "Efoulane", "latitude": 4.56667, "longitude": 12.75, "x": 38.8, "y": 53.1, "source": "geonames", "type": "PPL" }, { "name": "Ekak", "latitude": 4.55, "longitude": 12.85, "x": 58, "y": 56.2, "source": "geonames", "type": "PPL" }, { "name": "Elote", "latitude": 4.58333, "longitude": 12.73333, "x": 35.6, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Essimeyong", "latitude": 4.5, "longitude": 12.68333, "x": 26, "y": 65.4, "source": "geonames", "type": "PPL" }, { "name": "Etole", "latitude": 4.58333, "longitude": 12.7, "x": 29.2, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Eyen", "latitude": 4.61667, "longitude": 13, "x": 86.8, "y": 43.8, "source": "geonames", "type": "PPL" }, { "name": "Kagbane", "latitude": 4.58333, "longitude": 12.93333, "x": 74, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Kak III", "latitude": 4.53333, "longitude": 12.98333, "x": 83.6, "y": 59.2, "source": "geonames", "type": "PPL" }, { "name": "Kidimebe", "latitude": 4.8, "longitude": 12.8, "x": 48.4, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Kouambang II", "latitude": 4.43333, "longitude": 12.91667, "x": 70.8, "y": 77.7, "source": "geonames", "type": "PPL" }, { "name": "Lembe", "latitude": 4.61667, "longitude": 12.66667, "x": 22.8, "y": 43.8, "source": "geonames", "type": "PPL" }, { "name": "Loulou", "latitude": 4.43333, "longitude": 12.95, "x": 77.2, "y": 77.7, "source": "geonames", "type": "PPL" }, { "name": "Loum", "latitude": 4.36667, "longitude": 12.76667, "x": 42, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Mbele", "latitude": 4.76667, "longitude": 12.81667, "x": 51.6, "y": 16.2, "source": "geonames", "type": "PPL" }, { "name": "Mbet", "latitude": 4.61667, "longitude": 13.01667, "x": 90, "y": 43.8, "source": "geonames", "type": "PPL" }, { "name": "Mbomenjok", "latitude": 4.68333, "longitude": 12.68333, "x": 26, "y": 31.5, "source": "geonames", "type": "PPL" }, { "name": "Meba", "latitude": 4.61667, "longitude": 12.98333, "x": 83.6, "y": 43.8, "source": "geonames", "type": "PPL" }, { "name": "Mebang", "latitude": 4.46667, "longitude": 12.8, "x": 48.4, "y": 71.5, "source": "geonames", "type": "PPL" }, { "name": "Medalmbom", "latitude": 4.76667, "longitude": 12.83333, "x": 54.8, "y": 16.2, "source": "geonames", "type": "PPL" }, { "name": "Mendoumbe", "latitude": 4.55, "longitude": 12.68333, "x": 26, "y": 56.2, "source": "geonames", "type": "PPL" }, { "name": "Mendoume", "latitude": 4.58333, "longitude": 12.91667, "x": 70.8, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Mengaa", "latitude": 4.43333, "longitude": 12.71667, "x": 32.4, "y": 77.7, "source": "geonames", "type": "PPL" }, { "name": "Mengoa", "latitude": 4.75, "longitude": 12.75, "x": 38.8, "y": 19.2, "source": "geonames", "type": "PPL" }, { "name": "Messague", "latitude": 4.45, "longitude": 12.91667, "x": 70.8, "y": 74.6, "source": "geonames", "type": "PPL" }, { "name": "Mgbaga", "latitude": 4.6, "longitude": 12.96667, "x": 80.4, "y": 46.9, "source": "geonames", "type": "PPL" }, { "name": "Mimbang", "latitude": 4.4, "longitude": 12.75, "x": 38.8, "y": 83.8, "source": "geonames", "type": "PPL" }, { "name": "Ndandouk", "latitude": 4.6, "longitude": 12.7, "x": 29.2, "y": 46.9, "source": "geonames", "type": "PPL" }, { "name": "Nding", "latitude": 4.61667, "longitude": 12.65, "x": 19.6, "y": 43.8, "source": "geonames", "type": "PPL" }, { "name": "Ndjoumbe", "latitude": 4.48333, "longitude": 12.71667, "x": 32.4, "y": 68.5, "source": "geonames", "type": "PPL" }, { "name": "Ngoakomba", "latitude": 4.43333, "longitude": 12.65, "x": 19.6, "y": 77.7, "source": "geonames", "type": "PPL" }, { "name": "Ngoo", "latitude": 4.38333, "longitude": 12.83333, "x": 54.8, "y": 86.9, "source": "geonames", "type": "PPL" }, { "name": "Nguiniouma", "latitude": 4.78333, "longitude": 12.76667, "x": 42, "y": 13.1, "source": "geonames", "type": "PPL" }, { "name": "Niayesse", "latitude": 4.38333, "longitude": 12.71667, "x": 32.4, "y": 86.9, "source": "geonames", "type": "PPL" }, { "name": "Nie", "latitude": 4.56667, "longitude": 12.9, "x": 67.6, "y": 53.1, "source": "geonames", "type": "PPL" }, { "name": "Samba II", "latitude": 4.43333, "longitude": 12.96667, "x": 80.4, "y": 77.7, "source": "geonames", "type": "PPL" }, { "name": "Samba III", "latitude": 4.45, "longitude": 12.93333, "x": 74, "y": 74.6, "source": "geonames", "type": "PPL" }, { "name": "Vela", "latitude": 4.7, "longitude": 12.81667, "x": 51.6, "y": 28.5, "source": "geonames", "type": "PPL" }, { "name": "Zembe", "latitude": 4.45, "longitude": 12.9, "x": 67.6, "y": 74.6, "source": "geonames", "type": "PPL" }, { "name": "Zengoaga", "latitude": 4.63333, "longitude": 12.63333, "x": 16.4, "y": 40.8, "source": "geonames", "type": "PPL" } ], "Mintom": [ { "name": "Badekok", "latitude": 3.21667, "longitude": 15.01667, "x": 90, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Diemba", "latitude": 3.33333, "longitude": 15.01667, "x": 90, "y": 52.7, "source": "geonames", "type": "PPL" }, { "name": "Madjoue", "latitude": 3.21667, "longitude": 14.96667, "x": 42, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Momjepom", "latitude": 3.36667, "longitude": 15, "x": 74, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Ndiou", "latitude": 3.33333, "longitude": 14.98333, "x": 58, "y": 52.7, "source": "geonames", "type": "PPL" }, { "name": "Ndjoue", "latitude": 3.46667, "longitude": 14.93333, "x": 10, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Ngato", "latitude": 3.25, "longitude": 14.96667, "x": 42, "y": 79.3, "source": "geonames", "type": "PPL" }, { "name": "Ngola", "latitude": 3.38333, "longitude": 15.01667, "x": 90, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Parni", "latitude": 3.45, "longitude": 15.01667, "x": 90, "y": 15.3, "source": "geonames", "type": "PPL" } ], "Moloundou": [ { "name": "Adjala", "latitude": 2.03333, "longitude": 15.15, "x": 40.5, "y": 80, "source": "geonames", "type": "PPL" }, { "name": "Banana", "latitude": 2.08333, "longitude": 15.28333, "x": 71, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Banana Keka", "latitude": 2.08333, "longitude": 15.28333, "x": 71, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Bangoy I", "latitude": 2.1, "longitude": 15.31667, "x": 78.6, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Bangoy I Bonbang", "latitude": 2.1, "longitude": 15.3, "x": 74.8, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Bangoy I Molongodiba", "latitude": 2.08333, "longitude": 15.3, "x": 74.8, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Bangoy II", "latitude": 2.11667, "longitude": 15.31667, "x": 78.6, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Bateka Malembe", "latitude": 2.15, "longitude": 15.35, "x": 86.2, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Bateka Malen", "latitude": 2.15, "longitude": 15.18333, "x": 48.1, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Bateka Ngoli", "latitude": 2.13333, "longitude": 15.35, "x": 86.2, "y": 20, "source": "geonames", "type": "PPL" }, { "name": "Lobila", "latitude": 2.1, "longitude": 15.36667, "x": 90, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Makoka I", "latitude": 2.06667, "longitude": 15.25, "x": 63.3, "y": 60, "source": "geonames", "type": "PPL" }, { "name": "Makoka I Tembe", "latitude": 2.05, "longitude": 15.23333, "x": 59.5, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Makoka II", "latitude": 2.06667, "longitude": 15.26667, "x": 67.1, "y": 60, "source": "geonames", "type": "PPL" }, { "name": "Makoka II Bellevue", "latitude": 2.06667, "longitude": 15.25, "x": 63.3, "y": 60, "source": "geonames", "type": "PPL" }, { "name": "Malapa", "latitude": 2.01667, "longitude": 15.25, "x": 63.3, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Malen", "latitude": 2.05, "longitude": 15.01667, "x": 10, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "New Town", "latitude": 2.03333, "longitude": 15.21667, "x": 55.7, "y": 80, "source": "geonames", "type": "PPL" }, { "name": "Ngilili II", "latitude": 2.11667, "longitude": 15.01667, "x": 10, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Nola", "latitude": 2.05, "longitude": 15.18333, "x": 48.1, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Pezam", "latitude": 2.05, "longitude": 15.21667, "x": 55.7, "y": 70, "source": "geonames", "type": "PPL" } ], "Monatele": [ { "name": "Abangnang", "latitude": 4.31667, "longitude": 11.31667, "x": 82, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "Avo", "latitude": 4.26667, "longitude": 11.23333, "x": 62, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Balamba", "latitude": 4.43333, "longitude": 11.23333, "x": 62, "y": 23.3, "source": "geonames", "type": "PPL" }, { "name": "Basolo", "latitude": 4.48333, "longitude": 11.21667, "x": 58, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Bilik Bindik", "latitude": 4.28333, "longitude": 11.31667, "x": 82, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Binoum", "latitude": 4.23333, "longitude": 11.03333, "x": 14, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Boalondo", "latitude": 4.41667, "longitude": 11.23333, "x": 62, "y": 27.8, "source": "geonames", "type": "PPL" }, { "name": "Boimotah", "latitude": 4.25, "longitude": 11.15, "x": 42, "y": 72.2, "source": "geonames", "type": "PPL" }, { "name": "Bombato", "latitude": 4.46667, "longitude": 11.25, "x": 66, "y": 14.4, "source": "geonames", "type": "PPL" }, { "name": "Bongando", "latitude": 4.48333, "longitude": 11.15, "x": 42, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Bongo", "latitude": 4.35, "longitude": 11.06667, "x": 22, "y": 45.6, "source": "geonames", "type": "PPL" }, { "name": "Botombo", "latitude": 4.48333, "longitude": 11.2, "x": 54, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Djounyat", "latitude": 4.36667, "longitude": 11.28333, "x": 74, "y": 41.1, "source": "geonames", "type": "PPL" }, { "name": "Ebanga", "latitude": 4.25, "longitude": 11.35, "x": 90, "y": 72.2, "source": "geonames", "type": "PPL" }, { "name": "Ebebda I", "latitude": 4.33333, "longitude": 11.28333, "x": 74, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Ebebda II", "latitude": 4.35, "longitude": 11.26667, "x": 70, "y": 45.6, "source": "geonames", "type": "PPL" }, { "name": "Ebolmongo", "latitude": 4.25, "longitude": 11.26667, "x": 70, "y": 72.2, "source": "geonames", "type": "PPL" }, { "name": "Ekouda", "latitude": 4.18333, "longitude": 11.18333, "x": 50, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Elong", "latitude": 4.33333, "longitude": 11.31667, "x": 82, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Emana", "latitude": 4.26667, "longitude": 11.33333, "x": 86, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Eyenmeyong", "latitude": 4.18333, "longitude": 11.21667, "x": 58, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Kananga", "latitude": 4.36667, "longitude": 11.18333, "x": 50, "y": 41.1, "source": "geonames", "type": "PPL" }, { "name": "Keling", "latitude": 4.26667, "longitude": 11.01667, "x": 10, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Kelkoto", "latitude": 4.43333, "longitude": 11.16667, "x": 46, "y": 23.3, "source": "geonames", "type": "PPL" }, { "name": "Kikot", "latitude": 4.2, "longitude": 11.01667, "x": 10, "y": 85.6, "source": "geonames", "type": "PPL" }, { "name": "Kougouda", "latitude": 4.23333, "longitude": 11.31667, "x": 82, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Lekoun", "latitude": 4.21667, "longitude": 11.23333, "x": 62, "y": 81.1, "source": "geonames", "type": "PPL" }, { "name": "Lenouk", "latitude": 4.21667, "longitude": 11.15, "x": 42, "y": 81.1, "source": "geonames", "type": "PPL" }, { "name": "Levem", "latitude": 4.23333, "longitude": 11.18333, "x": 50, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Lindong", "latitude": 4.31667, "longitude": 11.28333, "x": 74, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "Melen", "latitude": 4.3, "longitude": 11.31667, "x": 82, "y": 58.9, "source": "geonames", "type": "PPL" }, { "name": "Monabo", "latitude": 4.23333, "longitude": 11.26667, "x": 70, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Monatele II", "latitude": 4.26667, "longitude": 11.26667, "x": 70, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Mvomekak", "latitude": 4.23333, "longitude": 11.23333, "x": 62, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Ndomnjinge", "latitude": 4.2, "longitude": 11.05, "x": 18, "y": 85.6, "source": "geonames", "type": "PPL" }, { "name": "Ndoup", "latitude": 4.25, "longitude": 11.18333, "x": 50, "y": 72.2, "source": "geonames", "type": "PPL" }, { "name": "Nega", "latitude": 4.33333, "longitude": 11.28333, "x": 74, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Ngbaba I", "latitude": 4.2, "longitude": 11.25, "x": 66, "y": 85.6, "source": "geonames", "type": "PPL" }, { "name": "Ngbaba II", "latitude": 4.23333, "longitude": 11.28333, "x": 74, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Niamanga I", "latitude": 4.35, "longitude": 11.13333, "x": 38, "y": 45.6, "source": "geonames", "type": "PPL" }, { "name": "Nkang", "latitude": 4.26667, "longitude": 11.31667, "x": 82, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Nkoabong", "latitude": 4.23333, "longitude": 11.35, "x": 90, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Nkolkose", "latitude": 4.28333, "longitude": 11.28333, "x": 74, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Nkolmebel", "latitude": 4.2, "longitude": 11.2, "x": 54, "y": 85.6, "source": "geonames", "type": "PPL" }, { "name": "Nkolmetolo", "latitude": 4.26667, "longitude": 11.31667, "x": 82, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Nkolngal", "latitude": 4.23333, "longitude": 11.16667, "x": 46, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Nkolosanga", "latitude": 4.28333, "longitude": 11.23333, "x": 62, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Nkombibam", "latitude": 4.23333, "longitude": 11.2, "x": 54, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Nkongmesa", "latitude": 4.23333, "longitude": 11.21667, "x": 58, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Nlongbon", "latitude": 4.2, "longitude": 11.16667, "x": 46, "y": 85.6, "source": "geonames", "type": "PPL" }, { "name": "Ntol", "latitude": 4.2, "longitude": 11.11667, "x": 34, "y": 85.6, "source": "geonames", "type": "PPL" }, { "name": "Nyambat", "latitude": 4.3, "longitude": 11.01667, "x": 10, "y": 58.9, "source": "geonames", "type": "PPL" }, { "name": "Ominde", "latitude": 4.38333, "longitude": 11.06667, "x": 22, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Osebe", "latitude": 4.18333, "longitude": 11.08333, "x": 26, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Poupouma", "latitude": 4.23333, "longitude": 11.31667, "x": 82, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Tsang", "latitude": 4.31667, "longitude": 11.23333, "x": 62, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "Yangben", "latitude": 4.43333, "longitude": 11.06667, "x": 22, "y": 23.3, "source": "geonames", "type": "PPL" } ], "Mokolo": [ { "name": "Aladjaba", "latitude": 10.73333, "longitude": 13.73333, "x": 21.8, "y": 61.6, "source": "geonames", "type": "PPL" }, { "name": "Baou", "latitude": 10.75, "longitude": 13.88333, "x": 85.1, "y": 53, "source": "geonames", "type": "PPL" }, { "name": "Bawayam", "latitude": 10.75994, "longitude": 13.88061, "x": 83.9, "y": 47.8, "source": "geonames", "type": "PPL" }, { "name": "Bazouda", "latitude": 10.81577, "longitude": 13.83515, "x": 64.7, "y": 19.1, "source": "geonames", "type": "PPL" }, { "name": "Biskavay", "latitude": 10.72528, "longitude": 13.8636, "x": 76.7, "y": 65.7, "source": "geonames", "type": "PPL" }, { "name": "Bokoules", "latitude": 10.77438, "longitude": 13.87101, "x": 79.9, "y": 40.4, "source": "geonames", "type": "PPL" }, { "name": "Borogoua", "latitude": 10.76463, "longitude": 13.81573, "x": 56.5, "y": 45.4, "source": "geonames", "type": "PPL" }, { "name": "Boufgay", "latitude": 10.77267, "longitude": 13.81338, "x": 55.6, "y": 41.3, "source": "geonames", "type": "PPL" }, { "name": "Chetok", "latitude": 10.80265, "longitude": 13.86263, "x": 76.3, "y": 25.8, "source": "geonames", "type": "PPL" }, { "name": "Chichiem", "latitude": 10.72966, "longitude": 13.88235, "x": 84.7, "y": 63.4, "source": "geonames", "type": "PPL" }, { "name": "Childem", "latitude": 10.71252, "longitude": 13.79305, "x": 47, "y": 72.3, "source": "geonames", "type": "PPL" }, { "name": "Dalan", "latitude": 10.70468, "longitude": 13.79819, "x": 49.1, "y": 76.3, "source": "geonames", "type": "PPL" }, { "name": "Damnay", "latitude": 10.78979, "longitude": 13.88211, "x": 84.6, "y": 32.5, "source": "geonames", "type": "PPL" }, { "name": "Daouatche", "latitude": 10.75923, "longitude": 13.83761, "x": 65.8, "y": 48.2, "source": "geonames", "type": "PPL" }, { "name": "Daoudere", "latitude": 10.7933, "longitude": 13.86078, "x": 75.6, "y": 30.6, "source": "geonames", "type": "PPL" }, { "name": "Davay", "latitude": 10.77638, "longitude": 13.87872, "x": 83.1, "y": 39.4, "source": "geonames", "type": "PPL" }, { "name": "Dayak", "latitude": 10.7822, "longitude": 13.84281, "x": 68, "y": 36.4, "source": "geonames", "type": "PPL" }, { "name": "Dibouloum", "latitude": 10.77102, "longitude": 13.81887, "x": 57.9, "y": 42.1, "source": "geonames", "type": "PPL" }, { "name": "Ditsnat", "latitude": 10.77697, "longitude": 13.812, "x": 55, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Djaodfaya", "latitude": 10.81383, "longitude": 13.84116, "x": 67.3, "y": 20.1, "source": "geonames", "type": "PPL" }, { "name": "Djegoue", "latitude": 10.8094, "longitude": 13.76337, "x": 34.4, "y": 22.3, "source": "geonames", "type": "PPL" }, { "name": "Djegueme", "latitude": 10.80777, "longitude": 13.86363, "x": 76.8, "y": 23.2, "source": "geonames", "type": "PPL" }, { "name": "Djele", "latitude": 10.75945, "longitude": 13.73838, "x": 23.9, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Djembele", "latitude": 10.79888, "longitude": 13.82059, "x": 58.6, "y": 27.8, "source": "geonames", "type": "PPL" }, { "name": "Djibilogoua", "latitude": 10.72381, "longitude": 13.79184, "x": 46.5, "y": 66.5, "source": "geonames", "type": "PPL" }, { "name": "Djobtoro", "latitude": 10.80113, "longitude": 13.86955, "x": 79.3, "y": 26.6, "source": "geonames", "type": "PPL" }, { "name": "Dorva", "latitude": 10.72944, "longitude": 13.78769, "x": 44.7, "y": 63.6, "source": "geonames", "type": "PPL" }, { "name": "Douatche", "latitude": 10.75205, "longitude": 13.85708, "x": 74, "y": 51.9, "source": "geonames", "type": "PPL" }, { "name": "Doubkangaz", "latitude": 10.81307, "longitude": 13.83457, "x": 64.5, "y": 20.5, "source": "geonames", "type": "PPL" }, { "name": "Doubmazangoua", "latitude": 10.80429, "longitude": 13.84421, "x": 68.6, "y": 25, "source": "geonames", "type": "PPL" }, { "name": "Douvar", "latitude": 10.77811, "longitude": 13.79225, "x": 46.6, "y": 38.5, "source": "geonames", "type": "PPL" }, { "name": "Dzagouaya", "latitude": 10.77526, "longitude": 13.8493, "x": 70.7, "y": 39.9, "source": "geonames", "type": "PPL" }, { "name": "Gadabak", "latitude": 10.80377, "longitude": 13.85437, "x": 72.9, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Gadamayo", "latitude": 10.73675, "longitude": 13.79909, "x": 49.5, "y": 59.8, "source": "geonames", "type": "PPL" }, { "name": "Goda", "latitude": 10.74681, "longitude": 13.78443, "x": 43.3, "y": 54.6, "source": "geonames", "type": "PPL" }, { "name": "Godokoum", "latitude": 10.79532, "longitude": 13.86655, "x": 78, "y": 29.6, "source": "geonames", "type": "PPL" }, { "name": "Golibay", "latitude": 10.82039, "longitude": 13.84428, "x": 68.6, "y": 16.7, "source": "geonames", "type": "PPL" }, { "name": "Goray", "latitude": 10.70143, "longitude": 13.77729, "x": 40.3, "y": 78, "source": "geonames", "type": "PPL" }, { "name": "Goualdak", "latitude": 10.75444, "longitude": 13.89201, "x": 88.7, "y": 50.7, "source": "geonames", "type": "PPL" }, { "name": "Gourdyak", "latitude": 10.79185, "longitude": 13.81732, "x": 57.2, "y": 31.4, "source": "geonames", "type": "PPL" }, { "name": "Grechay", "latitude": 10.72151, "longitude": 13.8128, "x": 55.3, "y": 67.6, "source": "geonames", "type": "PPL" }, { "name": "Grikoulam", "latitude": 10.76093, "longitude": 13.74306, "x": 25.9, "y": 47.3, "source": "geonames", "type": "PPL" }, { "name": "Grisway", "latitude": 10.71113, "longitude": 13.82829, "x": 61.8, "y": 73, "source": "geonames", "type": "PPL" }, { "name": "Guedouayam", "latitude": 10.75245, "longitude": 13.88655, "x": 86.4, "y": 51.7, "source": "geonames", "type": "PPL" }, { "name": "Gueleleje", "latitude": 10.78851, "longitude": 13.86757, "x": 78.4, "y": 33.1, "source": "geonames", "type": "PPL" }, { "name": "Gueouandar", "latitude": 10.80681, "longitude": 13.83718, "x": 65.6, "y": 23.7, "source": "geonames", "type": "PPL" }, { "name": "Guidkouala", "latitude": 10.76357, "longitude": 13.895, "x": 90, "y": 46, "source": "geonames", "type": "PPL" }, { "name": "Guidmagay", "latitude": 10.80828, "longitude": 13.82465, "x": 60.3, "y": 22.9, "source": "geonames", "type": "PPL" }, { "name": "Guidprat", "latitude": 10.81227, "longitude": 13.81728, "x": 57.2, "y": 20.9, "source": "geonames", "type": "PPL" }, { "name": "Guimsak", "latitude": 10.71797, "longitude": 13.83302, "x": 63.8, "y": 69.5, "source": "geonames", "type": "PPL" }, { "name": "Houva", "latitude": 10.80003, "longitude": 13.81098, "x": 54.5, "y": 27.2, "source": "geonames", "type": "PPL" }, { "name": "Idilang", "latitude": 10.71105, "longitude": 13.79494, "x": 47.8, "y": 73, "source": "geonames", "type": "PPL" }, { "name": "Jaoue", "latitude": 10.77291, "longitude": 13.84003, "x": 66.8, "y": 41.2, "source": "geonames", "type": "PPL" }, { "name": "Jejegle", "latitude": 10.78608, "longitude": 13.80421, "x": 51.7, "y": 34.4, "source": "geonames", "type": "PPL" }, { "name": "Koudeld", "latitude": 10.80669, "longitude": 13.87023, "x": 79.5, "y": 23.7, "source": "geonames", "type": "PPL" }, { "name": "Ksa", "latitude": 10.73532, "longitude": 13.82015, "x": 58.4, "y": 60.5, "source": "geonames", "type": "PPL" }, { "name": "Ldama", "latitude": 10.83336, "longitude": 13.8161, "x": 56.7, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Ldamsay", "latitude": 10.7665, "longitude": 13.7897, "x": 45.6, "y": 44.5, "source": "geonames", "type": "PPL" }, { "name": "Mada", "latitude": 10.74739, "longitude": 13.86905, "x": 79, "y": 54.3, "source": "geonames", "type": "PPL" }, { "name": "Madama", "latitude": 10.7204, "longitude": 13.79664, "x": 48.5, "y": 68.2, "source": "geonames", "type": "PPL" }, { "name": "Madoufnagay", "latitude": 10.73062, "longitude": 13.8088, "x": 53.6, "y": 62.9, "source": "geonames", "type": "PPL" }, { "name": "Mafaoli", "latitude": 10.72839, "longitude": 13.82106, "x": 58.8, "y": 64.1, "source": "geonames", "type": "PPL" }, { "name": "Malay", "latitude": 10.82644, "longitude": 13.84027, "x": 66.9, "y": 13.6, "source": "geonames", "type": "PPL" }, { "name": "Mandaka", "latitude": 10.74051, "longitude": 13.83989, "x": 66.7, "y": 57.9, "source": "geonames", "type": "PPL" }, { "name": "Mandoz", "latitude": 10.71043, "longitude": 13.85616, "x": 73.6, "y": 73.4, "source": "geonames", "type": "PPL" }, { "name": "Mapoldokou", "latitude": 10.75002, "longitude": 13.8687, "x": 78.9, "y": 53, "source": "geonames", "type": "PPL" }, { "name": "Markanday", "latitude": 10.81875, "longitude": 13.841, "x": 67.2, "y": 17.5, "source": "geonames", "type": "PPL" }, { "name": "Matzay", "latitude": 10.70339, "longitude": 13.88083, "x": 84, "y": 77, "source": "geonames", "type": "PPL" }, { "name": "Mavoumay", "latitude": 10.75867, "longitude": 13.76702, "x": 36, "y": 48.5, "source": "geonames", "type": "PPL" }, { "name": "Mayo Lega", "latitude": 10.72619, "longitude": 13.70545, "x": 10, "y": 65.2, "source": "geonames", "type": "PPL" }, { "name": "Mayo Sanganare", "latitude": 10.73616, "longitude": 13.73193, "x": 21.2, "y": 60.1, "source": "geonames", "type": "PPL" }, { "name": "Mboua", "latitude": 10.75385, "longitude": 13.79382, "x": 47.3, "y": 51, "source": "geonames", "type": "PPL" }, { "name": "Mbuken", "latitude": 10.73661, "longitude": 13.79091, "x": 46.1, "y": 59.9, "source": "geonames", "type": "PPL" }, { "name": "Meldere", "latitude": 10.83253, "longitude": 13.8285, "x": 61.9, "y": 10.4, "source": "geonames", "type": "PPL" }, { "name": "Mendeje", "latitude": 10.76593, "longitude": 13.88824, "x": 87.1, "y": 44.8, "source": "geonames", "type": "PPL" }, { "name": "Mendeze", "latitude": 10.75295, "longitude": 13.81472, "x": 56.1, "y": 51.4, "source": "geonames", "type": "PPL" }, { "name": "Metere", "latitude": 10.79651, "longitude": 13.82291, "x": 59.6, "y": 29, "source": "geonames", "type": "PPL" }, { "name": "Mlay", "latitude": 10.82907, "longitude": 13.8327, "x": 63.7, "y": 12.2, "source": "geonames", "type": "PPL" }, { "name": "Mofole", "latitude": 10.73398, "longitude": 13.80046, "x": 50.1, "y": 61.2, "source": "geonames", "type": "PPL" }, { "name": "Mofouele", "latitude": 10.71373, "longitude": 13.81124, "x": 54.6, "y": 71.7, "source": "geonames", "type": "PPL" }, { "name": "Mogoumaz", "latitude": 10.82099, "longitude": 13.76239, "x": 34, "y": 16.4, "source": "geonames", "type": "PPL" }, { "name": "Mokola", "latitude": 10.75893, "longitude": 13.83447, "x": 64.5, "y": 48.4, "source": "geonames", "type": "PPL" }, { "name": "Moulda", "latitude": 10.81224, "longitude": 13.85841, "x": 74.6, "y": 20.9, "source": "geonames", "type": "PPL" }, { "name": "Mouzal", "latitude": 10.79554, "longitude": 13.83113, "x": 63, "y": 29.5, "source": "geonames", "type": "PPL" }, { "name": "Mouzoday", "latitude": 10.79627, "longitude": 13.85495, "x": 73.1, "y": 29.1, "source": "geonames", "type": "PPL" }, { "name": "Ndodolom", "latitude": 10.74057, "longitude": 13.8176, "x": 57.3, "y": 57.8, "source": "geonames", "type": "PPL" }, { "name": "Ndoufgay", "latitude": 10.76125, "longitude": 13.78936, "x": 45.4, "y": 47.2, "source": "geonames", "type": "PPL" }, { "name": "Ouadazekda", "latitude": 10.76034, "longitude": 13.79403, "x": 47.4, "y": 47.6, "source": "geonames", "type": "PPL" }, { "name": "Ouda Mokola", "latitude": 10.79276, "longitude": 13.83172, "x": 63.3, "y": 30.9, "source": "geonames", "type": "PPL" }, { "name": "Oudahay", "latitude": 10.79027, "longitude": 13.81319, "x": 55.5, "y": 32.2, "source": "geonames", "type": "PPL" }, { "name": "Oudamadza", "latitude": 10.79415, "longitude": 13.82325, "x": 59.7, "y": 30.2, "source": "geonames", "type": "PPL" }, { "name": "Oudaray", "latitude": 10.81477, "longitude": 13.82673, "x": 61.2, "y": 19.6, "source": "geonames", "type": "PPL" }, { "name": "Ourdawa", "latitude": 10.75073, "longitude": 13.79625, "x": 48.3, "y": 52.6, "source": "geonames", "type": "PPL" }, { "name": "Ourloum", "latitude": 10.77863, "longitude": 13.87512, "x": 81.6, "y": 38.2, "source": "geonames", "type": "PPL" }, { "name": "Ouro Borga", "latitude": 10.77491, "longitude": 13.72956, "x": 20.2, "y": 40.1, "source": "geonames", "type": "PPL" }, { "name": "Ouro Magadji", "latitude": 10.73106, "longitude": 13.7675, "x": 36.2, "y": 62.7, "source": "geonames", "type": "PPL" }, { "name": "Ouro Tada", "latitude": 10.736, "longitude": 13.77587, "x": 39.7, "y": 60.2, "source": "geonames", "type": "PPL" }, { "name": "Ouro Vamay", "latitude": 10.77366, "longitude": 13.73958, "x": 24.4, "y": 40.8, "source": "geonames", "type": "PPL" }, { "name": "Paldok", "latitude": 10.76091, "longitude": 13.86562, "x": 77.6, "y": 47.3, "source": "geonames", "type": "PPL" }, { "name": "Pradbay", "latitude": 10.78118, "longitude": 13.85127, "x": 71.5, "y": 36.9, "source": "geonames", "type": "PPL" }, { "name": "Rouva", "latitude": 10.81929, "longitude": 13.82354, "x": 59.8, "y": 17.3, "source": "geonames", "type": "PPL" }, { "name": "Sarvak", "latitude": 10.70657, "longitude": 13.79125, "x": 46.2, "y": 75.3, "source": "geonames", "type": "PPL" }, { "name": "Semboa", "latitude": 10.74779, "longitude": 13.80126, "x": 50.4, "y": 54.1, "source": "geonames", "type": "PPL" }, { "name": "Shougoule", "latitude": 10.78779, "longitude": 13.75928, "x": 32.7, "y": 33.5, "source": "geonames", "type": "PPL" }, { "name": "Sirak", "latitude": 10.69374, "longitude": 13.79583, "x": 48.1, "y": 82, "source": "geonames", "type": "PPL" }, { "name": "Tangam", "latitude": 10.78218, "longitude": 13.84383, "x": 68.4, "y": 36.4, "source": "geonames", "type": "PPL" }, { "name": "Tassay", "latitude": 10.7717, "longitude": 13.86948, "x": 79.2, "y": 41.8, "source": "geonames", "type": "PPL" }, { "name": "Tchedeme", "latitude": 10.78839, "longitude": 13.83889, "x": 66.3, "y": 33.2, "source": "geonames", "type": "PPL" }, { "name": "Tchere", "latitude": 10.82353, "longitude": 13.83081, "x": 62.9, "y": 15.1, "source": "geonames", "type": "PPL" }, { "name": "Tcholta", "latitude": 10.76816, "longitude": 13.80744, "x": 53, "y": 43.6, "source": "geonames", "type": "PPL" }, { "name": "Vara", "latitude": 10.75818, "longitude": 13.88969, "x": 87.8, "y": 48.7, "source": "geonames", "type": "PPL" }, { "name": "Vaydavay", "latitude": 10.79982, "longitude": 13.84074, "x": 67.1, "y": 27.3, "source": "geonames", "type": "PPL" }, { "name": "Vok", "latitude": 10.78188, "longitude": 13.87177, "x": 80.2, "y": 36.5, "source": "geonames", "type": "PPL" }, { "name": "Vouyak", "latitude": 10.80611, "longitude": 13.84106, "x": 67.2, "y": 24, "source": "geonames", "type": "PPL" }, { "name": "Vouzad", "latitude": 10.81522, "longitude": 13.8106, "x": 54.4, "y": 19.3, "source": "geonames", "type": "PPL" }, { "name": "Vouzod", "latitude": 10.82973, "longitude": 13.79378, "x": 47.3, "y": 11.9, "source": "geonames", "type": "PPL" }, { "name": "Zamaroua", "latitude": 10.69796, "longitude": 13.83524, "x": 64.8, "y": 79.8, "source": "geonames", "type": "PPL" }, { "name": "Zileng", "latitude": 10.71355, "longitude": 13.84115, "x": 67.3, "y": 71.7, "source": "geonames", "type": "PPL" }, { "name": "Zivid", "latitude": 10.67813, "longitude": 13.85017, "x": 71.1, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Zoguilaway", "latitude": 10.81626, "longitude": 13.84325, "x": 68.2, "y": 18.8, "source": "geonames", "type": "PPL" } ], "Mora": [ { "name": "Adakele", "latitude": 11.03333, "longitude": 14.18333, "x": 71, "y": 53.3, "source": "geonames", "type": "PPL" }, { "name": "Adatz", "latitude": 10.97227, "longitude": 14.07139, "x": 19.9, "y": 79.1, "source": "geonames", "type": "PPL" }, { "name": "Aissa Karde", "latitude": 11.09168, "longitude": 14.22478, "x": 90, "y": 28.5, "source": "geonames", "type": "PPL" }, { "name": "AissaTarmon", "latitude": 11.08045, "longitude": 14.22374, "x": 89.5, "y": 33.3, "source": "geonames", "type": "PPL" }, { "name": "Baldama", "latitude": 10.98884, "longitude": 14.06643, "x": 17.6, "y": 72.1, "source": "geonames", "type": "PPL" }, { "name": "Baoua", "latitude": 11.0404, "longitude": 14.08932, "x": 28.1, "y": 50.3, "source": "geonames", "type": "PPL" }, { "name": "Bela", "latitude": 11.03012, "longitude": 14.05536, "x": 12.5, "y": 54.6, "source": "geonames", "type": "PPL" }, { "name": "Bester", "latitude": 10.97969, "longitude": 14.10873, "x": 36.9, "y": 76, "source": "geonames", "type": "PPL" }, { "name": "Boda", "latitude": 11.07444, "longitude": 14.0758, "x": 21.9, "y": 35.8, "source": "geonames", "type": "PPL" }, { "name": "Bola", "latitude": 11.01956, "longitude": 14.0962, "x": 31.2, "y": 59.1, "source": "geonames", "type": "PPL" }, { "name": "Daboaya", "latitude": 11.03333, "longitude": 14.06667, "x": 17.7, "y": 53.3, "source": "geonames", "type": "PPL" }, { "name": "Dalaouara", "latitude": 11.041, "longitude": 14.07753, "x": 22.7, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Dargala", "latitude": 10.97295, "longitude": 14.19981, "x": 78.6, "y": 78.8, "source": "geonames", "type": "PPL" }, { "name": "Dibon", "latitude": 10.9585, "longitude": 14.13467, "x": 48.8, "y": 85, "source": "geonames", "type": "PPL" }, { "name": "Dida", "latitude": 11.05, "longitude": 14.08333, "x": 25.3, "y": 46.2, "source": "geonames", "type": "PPL" }, { "name": "Diyan", "latitude": 11.04021, "longitude": 14.05867, "x": 14.1, "y": 50.3, "source": "geonames", "type": "PPL" }, { "name": "Djegoulele", "latitude": 11.02924, "longitude": 14.09764, "x": 31.9, "y": 55, "source": "geonames", "type": "PPL" }, { "name": "Djoe", "latitude": 11.0282, "longitude": 14.14455, "x": 53.3, "y": 55.4, "source": "geonames", "type": "PPL" }, { "name": "Dougoudje", "latitude": 11.10072, "longitude": 14.13133, "x": 47.3, "y": 24.7, "source": "geonames", "type": "PPL" }, { "name": "Doulo", "latitude": 11.10529, "longitude": 14.17345, "x": 66.5, "y": 22.8, "source": "geonames", "type": "PPL" }, { "name": "Dzala", "latitude": 10.98763, "longitude": 14.07691, "x": 22.4, "y": 72.6, "source": "geonames", "type": "PPL" }, { "name": "Dzanbola", "latitude": 11.03465, "longitude": 14.08473, "x": 26, "y": 52.7, "source": "geonames", "type": "PPL" }, { "name": "Dzandekere", "latitude": 11.06694, "longitude": 14.09017, "x": 28.5, "y": 39, "source": "geonames", "type": "PPL" }, { "name": "Dzanmadidi", "latitude": 11.05505, "longitude": 14.07138, "x": 19.9, "y": 44.1, "source": "geonames", "type": "PPL" }, { "name": "Dzanouala", "latitude": 11.10733, "longitude": 14.06357, "x": 16.3, "y": 21.9, "source": "geonames", "type": "PPL" }, { "name": "Dzanvouye", "latitude": 11.08514, "longitude": 14.04997, "x": 10.1, "y": 31.3, "source": "geonames", "type": "PPL" }, { "name": "Dzogdzama", "latitude": 11.0376, "longitude": 14.0785, "x": 23.1, "y": 51.5, "source": "geonames", "type": "PPL" }, { "name": "Erbayou", "latitude": 11.05971, "longitude": 14.05024, "x": 10.2, "y": 42.1, "source": "geonames", "type": "PPL" }, { "name": "Figui", "latitude": 11.09727, "longitude": 14.11558, "x": 40.1, "y": 26.2, "source": "geonames", "type": "PPL" }, { "name": "Gadzaoua", "latitude": 11.03249, "longitude": 14.11373, "x": 39.2, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Gagadama", "latitude": 11.01238, "longitude": 14.15555, "x": 58.3, "y": 62.1, "source": "geonames", "type": "PPL" }, { "name": "Gardwatchi", "latitude": 11.13547, "longitude": 14.13439, "x": 48.7, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Gayekourou", "latitude": 10.97622, "longitude": 14.0662, "x": 17.5, "y": 77.5, "source": "geonames", "type": "PPL" }, { "name": "Goldoubere", "latitude": 11.02281, "longitude": 14.15609, "x": 58.6, "y": 57.7, "source": "geonames", "type": "PPL" }, { "name": "Gouede-Gouede", "latitude": 10.9959, "longitude": 14.05149, "x": 10.8, "y": 69.1, "source": "geonames", "type": "PPL" }, { "name": "Gouendele", "latitude": 10.97743, "longitude": 14.15002, "x": 55.8, "y": 76.9, "source": "geonames", "type": "PPL" }, { "name": "GouendeleFla", "latitude": 10.95963, "longitude": 14.13888, "x": 50.7, "y": 84.5, "source": "geonames", "type": "PPL" }, { "name": "GouendeleGoua", "latitude": 10.96425, "longitude": 14.13452, "x": 48.7, "y": 82.5, "source": "geonames", "type": "PPL" }, { "name": "Goujimdele", "latitude": 11.00306, "longitude": 14.04981, "x": 10, "y": 66.1, "source": "geonames", "type": "PPL" }, { "name": "Gounoko", "latitude": 11.04257, "longitude": 14.15484, "x": 58, "y": 49.3, "source": "geonames", "type": "PPL" }, { "name": "Gouvaka", "latitude": 11.06635, "longitude": 14.08487, "x": 26, "y": 39.3, "source": "geonames", "type": "PPL" }, { "name": "Gueouele", "latitude": 11.03108, "longitude": 14.05942, "x": 14.4, "y": 54.2, "source": "geonames", "type": "PPL" }, { "name": "Hayouzeka", "latitude": 11.04842, "longitude": 14.09011, "x": 28.4, "y": 46.9, "source": "geonames", "type": "PPL" }, { "name": "Here", "latitude": 10.98719, "longitude": 14.20477, "x": 80.9, "y": 72.8, "source": "geonames", "type": "PPL" }, { "name": "Hodogoy", "latitude": 10.95203, "longitude": 14.13612, "x": 49.5, "y": 87.7, "source": "geonames", "type": "PPL" }, { "name": "Iboroua", "latitude": 11.01727, "longitude": 14.11272, "x": 38.8, "y": 60.1, "source": "geonames", "type": "PPL" }, { "name": "Igabata", "latitude": 11.02628, "longitude": 14.15732, "x": 59.2, "y": 56.2, "source": "geonames", "type": "PPL" }, { "name": "Igawa", "latitude": 11.13295, "longitude": 14.14161, "x": 52, "y": 11.1, "source": "geonames", "type": "PPL" }, { "name": "Jilve", "latitude": 10.96894, "longitude": 14.16693, "x": 63.5, "y": 80.5, "source": "geonames", "type": "PPL" }, { "name": "Kaledjian", "latitude": 11.00409, "longitude": 14.06751, "x": 18.1, "y": 65.6, "source": "geonames", "type": "PPL" }, { "name": "Kassa", "latitude": 11.04107, "longitude": 14.06463, "x": 16.8, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Keouede", "latitude": 11.0058, "longitude": 14.0994, "x": 32.7, "y": 64.9, "source": "geonames", "type": "PPL" }, { "name": "Kiriao", "latitude": 10.99533, "longitude": 14.07017, "x": 19.3, "y": 69.4, "source": "geonames", "type": "PPL" }, { "name": "Kouelguen", "latitude": 11.0003, "longitude": 14.0545, "x": 12.1, "y": 67.3, "source": "geonames", "type": "PPL" }, { "name": "Koueple", "latitude": 11.01223, "longitude": 14.09074, "x": 28.7, "y": 62.2, "source": "geonames", "type": "PPL" }, { "name": "Koura-Gondje", "latitude": 11.02964, "longitude": 14.08422, "x": 25.7, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Kourgui", "latitude": 11.08695, "longitude": 14.11063, "x": 37.8, "y": 30.6, "source": "geonames", "type": "PPL" }, { "name": "Koursouva", "latitude": 11.02231, "longitude": 14.11616, "x": 40.3, "y": 57.9, "source": "geonames", "type": "PPL" }, { "name": "Leloude", "latitude": 11.08285, "longitude": 14.11153, "x": 38.2, "y": 32.3, "source": "geonames", "type": "PPL" }, { "name": "Mahoula", "latitude": 11.047, "longitude": 14.20916, "x": 82.9, "y": 47.5, "source": "geonames", "type": "PPL" }, { "name": "Makara", "latitude": 10.99785, "longitude": 14.09028, "x": 28.5, "y": 68.3, "source": "geonames", "type": "PPL" }, { "name": "Massava", "latitude": 10.9849, "longitude": 14.15126, "x": 56.4, "y": 73.8, "source": "geonames", "type": "PPL" }, { "name": "Matsanga", "latitude": 10.96557, "longitude": 14.07792, "x": 22.9, "y": 82, "source": "geonames", "type": "PPL" }, { "name": "Mbreme", "latitude": 10.99172, "longitude": 14.15104, "x": 56.3, "y": 70.9, "source": "geonames", "type": "PPL" }, { "name": "Meche", "latitude": 10.9997, "longitude": 14.08868, "x": 27.8, "y": 67.5, "source": "geonames", "type": "PPL" }, { "name": "Megwede", "latitude": 11.00588, "longitude": 14.08701, "x": 27, "y": 64.9, "source": "geonames", "type": "PPL" }, { "name": "Meje", "latitude": 11.0234, "longitude": 14.07762, "x": 22.7, "y": 57.5, "source": "geonames", "type": "PPL" }, { "name": "Meke", "latitude": 11.00183, "longitude": 14.09472, "x": 30.5, "y": 66.6, "source": "geonames", "type": "PPL" }, { "name": "MelakaPlat", "latitude": 10.94659, "longitude": 14.13419, "x": 48.6, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Mindrav", "latitude": 10.97821, "longitude": 14.12068, "x": 42.4, "y": 76.6, "source": "geonames", "type": "PPL" }, { "name": "Mokoulbe", "latitude": 11.0094, "longitude": 14.04993, "x": 10.1, "y": 63.4, "source": "geonames", "type": "PPL" }, { "name": "Mondaya", "latitude": 11.01029, "longitude": 14.07211, "x": 20.2, "y": 63, "source": "geonames", "type": "PPL" }, { "name": "Moudye", "latitude": 11.0694, "longitude": 14.07949, "x": 23.6, "y": 38, "source": "geonames", "type": "PPL" }, { "name": "Mouktele", "latitude": 11.02346, "longitude": 14.0831, "x": 25.2, "y": 57.4, "source": "geonames", "type": "PPL" }, { "name": "Mouvane", "latitude": 10.98839, "longitude": 14.21003, "x": 83.3, "y": 72.3, "source": "geonames", "type": "PPL" }, { "name": "Mouvar", "latitude": 10.98368, "longitude": 14.12494, "x": 44.4, "y": 74.3, "source": "geonames", "type": "PPL" }, { "name": "Mtchelye", "latitude": 11.04528, "longitude": 14.11022, "x": 37.6, "y": 48.2, "source": "geonames", "type": "PPL" }, { "name": "Narmada", "latitude": 11.05, "longitude": 14.06667, "x": 17.7, "y": 46.2, "source": "geonames", "type": "PPL" }, { "name": "Ndananba", "latitude": 11.03333, "longitude": 14.08333, "x": 25.3, "y": 53.3, "source": "geonames", "type": "PPL" }, { "name": "Ndaza", "latitude": 11.05819, "longitude": 14.06507, "x": 17, "y": 42.7, "source": "geonames", "type": "PPL" }, { "name": "Ndemeha", "latitude": 11.02112, "longitude": 14.14096, "x": 51.7, "y": 58.4, "source": "geonames", "type": "PPL" }, { "name": "Ndogba", "latitude": 10.9954, "longitude": 14.08245, "x": 24.9, "y": 69.3, "source": "geonames", "type": "PPL" }, { "name": "Ouandjile", "latitude": 11.07306, "longitude": 14.10417, "x": 34.9, "y": 36.4, "source": "geonames", "type": "PPL" }, { "name": "Ouara", "latitude": 11.06396, "longitude": 14.06466, "x": 16.8, "y": 40.3, "source": "geonames", "type": "PPL" }, { "name": "Oudjila", "latitude": 11.0109, "longitude": 14.09688, "x": 31.5, "y": 62.8, "source": "geonames", "type": "PPL" }, { "name": "Oujdede", "latitude": 10.97231, "longitude": 14.11359, "x": 39.2, "y": 79.1, "source": "geonames", "type": "PPL" }, { "name": "Ouraga", "latitude": 11.08195, "longitude": 14.07397, "x": 21, "y": 32.7, "source": "geonames", "type": "PPL" }, { "name": "Ouzada", "latitude": 11.01789, "longitude": 14.08202, "x": 24.7, "y": 59.8, "source": "geonames", "type": "PPL" }, { "name": "Ozangadava", "latitude": 11.06995, "longitude": 14.07456, "x": 21.3, "y": 37.8, "source": "geonames", "type": "PPL" }, { "name": "Ozogaya", "latitude": 11.01618, "longitude": 14.09219, "x": 29.4, "y": 60.5, "source": "geonames", "type": "PPL" }, { "name": "Padarmbooua", "latitude": 11.02811, "longitude": 14.11501, "x": 39.8, "y": 55.5, "source": "geonames", "type": "PPL" }, { "name": "PardaMbokotye", "latitude": 11.03125, "longitude": 14.16063, "x": 60.7, "y": 54.1, "source": "geonames", "type": "PPL" }, { "name": "Pivou", "latitude": 11.11562, "longitude": 14.13078, "x": 47, "y": 18.4, "source": "geonames", "type": "PPL" }, { "name": "Podar Matkoza", "latitude": 11.0356, "longitude": 14.08807, "x": 27.5, "y": 52.3, "source": "geonames", "type": "PPL" }, { "name": "Salabire", "latitude": 11.07306, "longitude": 14.10417, "x": 34.9, "y": 36.4, "source": "geonames", "type": "PPL" }, { "name": "Salamdaha", "latitude": 11.01667, "longitude": 14.1, "x": 32.9, "y": 60.3, "source": "geonames", "type": "PPL" }, { "name": "Salaovele", "latitude": 11.04402, "longitude": 14.08928, "x": 28, "y": 48.7, "source": "geonames", "type": "PPL" }, { "name": "Sama", "latitude": 10.97163, "longitude": 14.11755, "x": 41, "y": 79.4, "source": "geonames", "type": "PPL" }, { "name": "Sava", "latitude": 11.01313, "longitude": 14.19903, "x": 78.2, "y": 61.8, "source": "geonames", "type": "PPL" }, { "name": "Sera Doumda", "latitude": 10.98767, "longitude": 14.19319, "x": 75.6, "y": 72.6, "source": "geonames", "type": "PPL" }, { "name": "Sera Hadya", "latitude": 10.99374, "longitude": 14.19568, "x": 76.7, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Sidue", "latitude": 11.00701, "longitude": 14.14348, "x": 52.8, "y": 64.4, "source": "geonames", "type": "PPL" }, { "name": "Skouala", "latitude": 11.0855, "longitude": 14.05988, "x": 14.6, "y": 31.2, "source": "geonames", "type": "PPL" }, { "name": "Sokoyeba", "latitude": 11.03833, "longitude": 14.18996, "x": 74.1, "y": 51.1, "source": "geonames", "type": "PPL" }, { "name": "Sra Warda", "latitude": 11.11722, "longitude": 14.15235, "x": 56.9, "y": 17.7, "source": "geonames", "type": "PPL" }, { "name": "Tagoudele", "latitude": 11.07306, "longitude": 14.10417, "x": 34.9, "y": 36.4, "source": "geonames", "type": "PPL" }, { "name": "Taladabara", "latitude": 11.01667, "longitude": 14.11667, "x": 40.6, "y": 60.3, "source": "geonames", "type": "PPL" }, { "name": "Talake", "latitude": 11.09333, "longitude": 14.18342, "x": 71.1, "y": 27.8, "source": "geonames", "type": "PPL" }, { "name": "Touval", "latitude": 11.00885, "longitude": 14.06028, "x": 14.8, "y": 63.6, "source": "geonames", "type": "PPL" }, { "name": "Vadigzeoua", "latitude": 11.08767, "longitude": 14.07133, "x": 19.8, "y": 30.2, "source": "geonames", "type": "PPL" }, { "name": "Vadikouatsa", "latitude": 11.02648, "longitude": 14.1241, "x": 44, "y": 56.2, "source": "geonames", "type": "PPL" }, { "name": "Vadimeteke", "latitude": 11.0229, "longitude": 14.11024, "x": 37.6, "y": 57.7, "source": "geonames", "type": "PPL" }, { "name": "Vadissa", "latitude": 11.0648, "longitude": 14.07273, "x": 20.5, "y": 39.9, "source": "geonames", "type": "PPL" }, { "name": "Vame", "latitude": 10.99669, "longitude": 14.1551, "x": 58.1, "y": 68.8, "source": "geonames", "type": "PPL" }, { "name": "Vindelar", "latitude": 10.96138, "longitude": 14.12312, "x": 43.5, "y": 83.7, "source": "geonames", "type": "PPL" }, { "name": "Warba", "latitude": 10.95738, "longitude": 14.17067, "x": 65.3, "y": 85.4, "source": "geonames", "type": "PPL" }, { "name": "Wilda", "latitude": 11.09879, "longitude": 14.08117, "x": 24.3, "y": 25.5, "source": "geonames", "type": "PPL" }, { "name": "Yondoula", "latitude": 11.06076, "longitude": 14.08571, "x": 26.4, "y": 41.6, "source": "geonames", "type": "PPL" }, { "name": "Zagama", "latitude": 11.00035, "longitude": 14.0869, "x": 27, "y": 67.2, "source": "geonames", "type": "PPL" }, { "name": "Zama", "latitude": 10.97018, "longitude": 14.07374, "x": 20.9, "y": 80, "source": "geonames", "type": "PPL" } ], "Mouanko": [ { "name": "Abe", "latitude": 3.65, "longitude": 9.81667, "x": 61.2, "y": 44.7, "source": "geonames", "type": "PPL" }, { "name": "Baleklongo", "latitude": 3.41611, "longitude": 9.79556, "x": 56.1, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ballip", "latitude": 3.69972, "longitude": 9.73778, "x": 42.3, "y": 35.1, "source": "geonames", "type": "PPL" }, { "name": "Bembo", "latitude": 3.57028, "longitude": 9.70417, "x": 34.3, "y": 60.2, "source": "geonames", "type": "PPL" }, { "name": "Bemenge", "latitude": 3.66, "longitude": 9.79278, "x": 55.5, "y": 42.8, "source": "geonames", "type": "PPL" }, { "name": "Bidale", "latitude": 3.66194, "longitude": 9.7725, "x": 50.6, "y": 42.4, "source": "geonames", "type": "PPL" }, { "name": "Bingando", "latitude": 3.67667, "longitude": 9.74111, "x": 43.1, "y": 39.6, "source": "geonames", "type": "PPL" }, { "name": "Binjame", "latitude": 3.76667, "longitude": 9.73333, "x": 41.3, "y": 22.1, "source": "geonames", "type": "PPL" }, { "name": "Bogninga", "latitude": 3.58333, "longitude": 9.68333, "x": 29.3, "y": 57.6, "source": "geonames", "type": "PPL" }, { "name": "Bohengue", "latitude": 3.57222, "longitude": 9.71972, "x": 38, "y": 59.8, "source": "geonames", "type": "PPL" }, { "name": "Bolounga", "latitude": 3.61667, "longitude": 9.76667, "x": 49.2, "y": 51.2, "source": "geonames", "type": "PPL" }, { "name": "Boloy Boleo", "latitude": 3.57722, "longitude": 9.72611, "x": 39.5, "y": 58.8, "source": "geonames", "type": "PPL" }, { "name": "Bolunga", "latitude": 3.59667, "longitude": 9.69444, "x": 32, "y": 55.1, "source": "geonames", "type": "PPL" }, { "name": "Bonabouab", "latitude": 3.57778, "longitude": 9.64778, "x": 20.8, "y": 58.7, "source": "geonames", "type": "PPL" }, { "name": "Bonagam", "latitude": 3.77972, "longitude": 9.76944, "x": 49.9, "y": 19.6, "source": "geonames", "type": "PPL" }, { "name": "Bonangango", "latitude": 3.62556, "longitude": 9.89389, "x": 79.6, "y": 49.5, "source": "geonames", "type": "PPL" }, { "name": "Bondjadi", "latitude": 3.5925, "longitude": 9.74417, "x": 43.9, "y": 55.9, "source": "geonames", "type": "PPL" }, { "name": "Botonga", "latitude": 3.57639, "longitude": 9.69806, "x": 32.8, "y": 59, "source": "geonames", "type": "PPL" }, { "name": "Botoude", "latitude": 3.61806, "longitude": 9.77333, "x": 50.8, "y": 50.9, "source": "geonames", "type": "PPL" }, { "name": "Bouea", "latitude": 3.65861, "longitude": 9.64222, "x": 19.5, "y": 43.1, "source": "geonames", "type": "PPL" }, { "name": "Boy", "latitude": 3.58333, "longitude": 9.73333, "x": 41.3, "y": 57.6, "source": "geonames", "type": "PPL" }, { "name": "Eben", "latitude": 3.60222, "longitude": 9.73139, "x": 40.8, "y": 54, "source": "geonames", "type": "PPL" }, { "name": "Eboga", "latitude": 3.74139, "longitude": 9.78972, "x": 54.7, "y": 27, "source": "geonames", "type": "PPL" }, { "name": "Elog Banag", "latitude": 3.69083, "longitude": 9.73111, "x": 40.7, "y": 36.8, "source": "geonames", "type": "PPL" }, { "name": "Elog Nsogwouti", "latitude": 3.675, "longitude": 9.73556, "x": 41.8, "y": 39.9, "source": "geonames", "type": "PPL" }, { "name": "Eloumba", "latitude": 3.56667, "longitude": 9.7, "x": 33.3, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Epasi", "latitude": 3.82944, "longitude": 9.69083, "x": 31.1, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Epolo", "latitude": 3.64278, "longitude": 9.79944, "x": 57.1, "y": 46.1, "source": "geonames", "type": "PPL" }, { "name": "Etalombo", "latitude": 3.50806, "longitude": 9.69417, "x": 31.9, "y": 72.2, "source": "geonames", "type": "PPL" }, { "name": "Etoba-Bibongo", "latitude": 3.66778, "longitude": 9.63972, "x": 18.9, "y": 41.3, "source": "geonames", "type": "PPL" }, { "name": "Itanda", "latitude": 3.41667, "longitude": 9.8, "x": 57.2, "y": 89.9, "source": "geonames", "type": "PPL" }, { "name": "Kamba", "latitude": 3.56111, "longitude": 9.65833, "x": 23.3, "y": 61.9, "source": "geonames", "type": "PPL" }, { "name": "Kombo Epaka", "latitude": 3.77278, "longitude": 9.61333, "x": 12.6, "y": 21, "source": "geonames", "type": "PPL" }, { "name": "Kombo Lote", "latitude": 3.48306, "longitude": 9.72806, "x": 40, "y": 77, "source": "geonames", "type": "PPL" }, { "name": "Konbe", "latitude": 3.575, "longitude": 9.92111, "x": 86.1, "y": 59.2, "source": "geonames", "type": "PPL" }, { "name": "Konbe I", "latitude": 3.585, "longitude": 9.92444, "x": 86.9, "y": 57.3, "source": "geonames", "type": "PPL" }, { "name": "Konbe II", "latitude": 3.58083, "longitude": 9.93167, "x": 88.7, "y": 58.1, "source": "geonames", "type": "PPL" }, { "name": "Kostom", "latitude": 3.52667, "longitude": 9.67389, "x": 27.1, "y": 68.6, "source": "geonames", "type": "PPL" }, { "name": "Lobetal", "latitude": 3.64472, "longitude": 9.78694, "x": 54.1, "y": 45.8, "source": "geonames", "type": "PPL" }, { "name": "Logoto", "latitude": 3.70833, "longitude": 9.74444, "x": 43.9, "y": 33.4, "source": "geonames", "type": "PPL" }, { "name": "Lote", "latitude": 3.42722, "longitude": 9.78556, "x": 53.8, "y": 87.8, "source": "geonames", "type": "PPL" }, { "name": "Makouma - Mounanga", "latitude": 3.49167, "longitude": 9.71944, "x": 37.9, "y": 75.4, "source": "geonames", "type": "PPL" }, { "name": "Malimba", "latitude": 3.53889, "longitude": 9.65056, "x": 21.5, "y": 66.2, "source": "geonames", "type": "PPL" }, { "name": "Mam", "latitude": 3.77278, "longitude": 9.78389, "x": 53.4, "y": 21, "source": "geonames", "type": "PPL" }, { "name": "Manye", "latitude": 3.58333, "longitude": 9.68333, "x": 29.3, "y": 57.6, "source": "geonames", "type": "PPL" }, { "name": "Marienberg", "latitude": 3.62222, "longitude": 9.8625, "x": 72.1, "y": 50.1, "source": "geonames", "type": "PPL" }, { "name": "Matanda I", "latitude": 3.45083, "longitude": 9.76222, "x": 48.2, "y": 83.3, "source": "geonames", "type": "PPL" }, { "name": "Mbalie", "latitude": 3.44306, "longitude": 9.77083, "x": 50.2, "y": 84.8, "source": "geonames", "type": "PPL" }, { "name": "Mbanga", "latitude": 3.65, "longitude": 9.85, "x": 69.2, "y": 44.7, "source": "geonames", "type": "PPL" }, { "name": "Mbango", "latitude": 3.60889, "longitude": 9.82944, "x": 64.2, "y": 52.7, "source": "geonames", "type": "PPL" }, { "name": "Mbiako", "latitude": 3.59056, "longitude": 9.64167, "x": 19.4, "y": 56.2, "source": "geonames", "type": "PPL" }, { "name": "Mbode", "latitude": 3.46972, "longitude": 9.74111, "x": 43.1, "y": 79.6, "source": "geonames", "type": "PPL" }, { "name": "Moukouke", "latitude": 3.75222, "longitude": 9.6025, "x": 10, "y": 24.9, "source": "geonames", "type": "PPL" }, { "name": "Moulongo", "latitude": 3.59889, "longitude": 9.72528, "x": 39.3, "y": 54.6, "source": "geonames", "type": "PPL" }, { "name": "Ndog Bong", "latitude": 3.71556, "longitude": 9.85917, "x": 71.3, "y": 32, "source": "geonames", "type": "PPL" }, { "name": "Ndog Mongo", "latitude": 3.65333, "longitude": 9.79139, "x": 55.1, "y": 44.1, "source": "geonames", "type": "PPL" }, { "name": "Ngog Woute", "latitude": 3.56333, "longitude": 9.93722, "x": 90, "y": 61.5, "source": "geonames", "type": "PPL" }, { "name": "Ngola", "latitude": 3.56528, "longitude": 9.71278, "x": 36.4, "y": 61.1, "source": "geonames", "type": "PPL" }, { "name": "Nkaganzogou", "latitude": 3.63333, "longitude": 9.78333, "x": 53.2, "y": 48, "source": "geonames", "type": "PPL" }, { "name": "Nkamba", "latitude": 3.74167, "longitude": 9.65889, "x": 23.5, "y": 27, "source": "geonames", "type": "PPL" }, { "name": "Nkangaak", "latitude": 3.64472, "longitude": 9.7675, "x": 49.4, "y": 45.8, "source": "geonames", "type": "PPL" }, { "name": "Nkouabaye", "latitude": 3.63, "longitude": 9.91389, "x": 84.4, "y": 48.6, "source": "geonames", "type": "PPL" }, { "name": "Npombo", "latitude": 3.61667, "longitude": 9.76667, "x": 49.2, "y": 51.2, "source": "geonames", "type": "PPL" }, { "name": "Nya Mben", "latitude": 3.71667, "longitude": 9.76667, "x": 49.2, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Nya Mbong", "latitude": 3.7, "longitude": 9.76667, "x": 49.2, "y": 35.1, "source": "geonames", "type": "PPL" }, { "name": "Okokong", "latitude": 3.6125, "longitude": 9.86528, "x": 72.8, "y": 52, "source": "geonames", "type": "PPL" }, { "name": "Okola", "latitude": 3.64056, "longitude": 9.81639, "x": 61.1, "y": 46.6, "source": "geonames", "type": "PPL" }, { "name": "Oniangue", "latitude": 3.75056, "longitude": 9.66667, "x": 25.3, "y": 25.3, "source": "geonames", "type": "PPL" }, { "name": "Pongo-Songo", "latitude": 3.61667, "longitude": 9.93333, "x": 89.1, "y": 51.2, "source": "geonames", "type": "PPL" }, { "name": "Samaboua", "latitude": 3.64778, "longitude": 9.64528, "x": 20.2, "y": 45.2, "source": "geonames", "type": "PPL" }, { "name": "Timbo", "latitude": 3.46306, "longitude": 9.74917, "x": 45.1, "y": 80.9, "source": "geonames", "type": "PPL" }, { "name": "Yadibo I", "latitude": 3.66667, "longitude": 9.81667, "x": 61.2, "y": 41.5, "source": "geonames", "type": "PPL" }, { "name": "Yadibo II", "latitude": 3.66778, "longitude": 9.78167, "x": 52.8, "y": 41.3, "source": "geonames", "type": "PPL" }, { "name": "Yalou", "latitude": 3.68333, "longitude": 9.78333, "x": 53.2, "y": 38.3, "source": "geonames", "type": "PPL" }, { "name": "Yankuzok", "latitude": 3.59333, "longitude": 9.82, "x": 62, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Yatou", "latitude": 3.61972, "longitude": 9.81694, "x": 61.3, "y": 50.6, "source": "geonames", "type": "PPL" }, { "name": "Yavi", "latitude": 3.62139, "longitude": 9.79472, "x": 55.9, "y": 50.3, "source": "geonames", "type": "PPL" } ], "Mundemba": [ { "name": "Akpasang", "latitude": 5.0024, "longitude": 8.7082, "x": 17.1, "y": 39.5, "source": "geonames", "type": "PPL" }, { "name": "Akpassang", "latitude": 4.9819, "longitude": 8.7063, "x": 16.7, "y": 43.7, "source": "geonames", "type": "PPL" }, { "name": "Bareka", "latitude": 4.8581, "longitude": 8.9299, "x": 60.3, "y": 68.9, "source": "geonames", "type": "PPL" }, { "name": "Beboka", "latitude": 5.0484, "longitude": 9.0625, "x": 86.2, "y": 30.1, "source": "geonames", "type": "PPL" }, { "name": "Bekoko", "latitude": 4.92333, "longitude": 8.85182, "x": 45.1, "y": 55.6, "source": "geonames", "type": "PPL" }, { "name": "Beoko", "latitude": 4.84689, "longitude": 8.94943, "x": 64.1, "y": 71.2, "source": "geonames", "type": "PPL" }, { "name": "Besingi", "latitude": 4.9143, "longitude": 8.9211, "x": 58.6, "y": 57.5, "source": "geonames", "type": "PPL" }, { "name": "Bilema", "latitude": 5.0756, "longitude": 9.0402, "x": 81.8, "y": 24.6, "source": "geonames", "type": "PPL" }, { "name": "Boa", "latitude": 5.027, "longitude": 8.9979, "x": 73.6, "y": 34.5, "source": "geonames", "type": "PPL" }, { "name": "Boa Ngolo", "latitude": 4.8059, "longitude": 8.9774, "x": 69.6, "y": 79.6, "source": "geonames", "type": "PPL" }, { "name": "Boku", "latitude": 4.83333, "longitude": 8.86667, "x": 48, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Boso", "latitude": 5.0865, "longitude": 9.0394, "x": 81.7, "y": 22.4, "source": "geonames", "type": "PPL" }, { "name": "Dibonda", "latitude": 4.8636, "longitude": 8.8941, "x": 53.4, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Dikuma", "latitude": 4.8907, "longitude": 9.082, "x": 90, "y": 62.3, "source": "geonames", "type": "PPL" }, { "name": "Ekumbako", "latitude": 4.8994, "longitude": 8.8739, "x": 49.4, "y": 60.5, "source": "geonames", "type": "PPL" }, { "name": "Ekundu-Kundu", "latitude": 5.1471, "longitude": 8.8877, "x": 52.1, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Erat", "latitude": 5.0601, "longitude": 8.7507, "x": 25.4, "y": 27.7, "source": "geonames", "type": "PPL" }, { "name": "Fabe", "latitude": 5.07755, "longitude": 8.96605, "x": 67.4, "y": 24.2, "source": "geonames", "type": "PPL" }, { "name": "Funge", "latitude": 4.75469, "longitude": 8.91102, "x": 56.7, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ikassa", "latitude": 4.88352, "longitude": 8.78925, "x": 32.9, "y": 63.7, "source": "geonames", "type": "PPL" }, { "name": "Ilor", "latitude": 4.80416, "longitude": 8.90438, "x": 55.4, "y": 79.9, "source": "geonames", "type": "PPL" }, { "name": "Itoki", "latitude": 4.83003, "longitude": 8.94229, "x": 62.8, "y": 74.6, "source": "geonames", "type": "PPL" }, { "name": "Ituka", "latitude": 5.016, "longitude": 8.9603, "x": 66.3, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Iwai", "latitude": 5.0538, "longitude": 9.0496, "x": 83.7, "y": 29, "source": "geonames", "type": "PPL" }, { "name": "Mabalebele", "latitude": 4.7806, "longitude": 8.9612, "x": 66.4, "y": 84.7, "source": "geonames", "type": "PPL" }, { "name": "Manja", "latitude": 4.9767, "longitude": 8.9158, "x": 57.6, "y": 44.7, "source": "geonames", "type": "PPL" }, { "name": "Matamani", "latitude": 4.99821, "longitude": 8.94668, "x": 63.6, "y": 40.4, "source": "geonames", "type": "PPL" }, { "name": "Meka", "latitude": 4.9234, "longitude": 8.9404, "x": 62.4, "y": 55.6, "source": "geonames", "type": "PPL" }, { "name": "Meta", "latitude": 5.0468, "longitude": 9.0221, "x": 78.3, "y": 30.4, "source": "geonames", "type": "PPL" }, { "name": "Miangwe II", "latitude": 5.0672, "longitude": 9.0037, "x": 74.7, "y": 26.3, "source": "geonames", "type": "PPL" }, { "name": "Mokangue", "latitude": 5.0976, "longitude": 9.0064, "x": 75.3, "y": 20.1, "source": "geonames", "type": "PPL" }, { "name": "Moki", "latitude": 4.86667, "longitude": 8.95, "x": 64.3, "y": 67.2, "source": "geonames", "type": "PPL" }, { "name": "Moko", "latitude": 4.864, "longitude": 8.8964, "x": 53.8, "y": 67.7, "source": "geonames", "type": "PPL" }, { "name": "Molonga", "latitude": 4.93333, "longitude": 9.03333, "x": 80.5, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Mosongo", "latitude": 5.0456, "longitude": 9.0398, "x": 81.8, "y": 30.7, "source": "geonames", "type": "PPL" }, { "name": "Mosongosele", "latitude": 4.8996, "longitude": 8.7605, "x": 27.3, "y": 60.5, "source": "geonames", "type": "PPL" }, { "name": "Mosongosele Ngolo", "latitude": 4.7992, "longitude": 8.9712, "x": 68.4, "y": 80.9, "source": "geonames", "type": "PPL" }, { "name": "Ndiba", "latitude": 5.0643, "longitude": 9.0381, "x": 81.4, "y": 26.9, "source": "geonames", "type": "PPL" }, { "name": "Ndjia", "latitude": 4.88333, "longitude": 8.98333, "x": 70.8, "y": 63.8, "source": "geonames", "type": "PPL" }, { "name": "Ngumu", "latitude": 4.87852, "longitude": 8.80926, "x": 36.8, "y": 64.8, "source": "geonames", "type": "PPL" }, { "name": "Okobo", "latitude": 4.9948, "longitude": 8.6962, "x": 14.8, "y": 41, "source": "geonames", "type": "PPL" }, { "name": "Weke", "latitude": 4.9519, "longitude": 8.6718, "x": 10, "y": 49.8, "source": "geonames", "type": "PPL" } ], "Mutengene": [ { "name": "Bolifamba", "latitude": 4.142, "longitude": 9.3061, "x": 39.3, "y": 28, "source": "geonames", "type": "PPL" }, { "name": "Bomaka", "latitude": 4.1578, "longitude": 9.3164, "x": 55.2, "y": 19.5, "source": "geonames", "type": "PPL" }, { "name": "Boniamavio", "latitude": 4.1518, "longitude": 9.3233, "x": 65.8, "y": 22.7, "source": "geonames", "type": "PPL" }, { "name": "Bowanda", "latitude": 4.1442, "longitude": 9.3205, "x": 61.5, "y": 26.8, "source": "geonames", "type": "PPL" }, { "name": "Dibanda", "latitude": 4.1273, "longitude": 9.3056, "x": 38.5, "y": 35.8, "source": "geonames", "type": "PPL" }, { "name": "Essuke", "latitude": 4.0637, "longitude": 9.3104, "x": 45.9, "y": 69.7, "source": "geonames", "type": "PPL" }, { "name": "Likomba", "latitude": 4.0935, "longitude": 9.3319, "x": 79.1, "y": 53.8, "source": "geonames", "type": "PPL" }, { "name": "Mussaka Village", "latitude": 4.1757, "longitude": 9.3317, "x": 78.7, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Ombe", "latitude": 4.084, "longitude": 9.2871, "x": 10, "y": 58.9, "source": "geonames", "type": "PPL" }, { "name": "Ombe Rein", "latitude": 4.0256, "longitude": 9.3101, "x": 45.5, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Tamben", "latitude": 4.116, "longitude": 9.339, "x": 90, "y": 41.8, "source": "geonames", "type": "PPL" } ], "Muyuka": [ { "name": "Bafia", "latitude": 4.3557, "longitude": 9.3161, "x": 25.4, "y": 37.3, "source": "geonames", "type": "PPL" }, { "name": "Balungu", "latitude": 4.2724, "longitude": 9.322, "x": 27.3, "y": 63.4, "source": "geonames", "type": "PPL" }, { "name": "Banga Bekele", "latitude": 4.40226, "longitude": 9.44293, "x": 66.9, "y": 22.7, "source": "geonames", "type": "PPL" }, { "name": "Bavenga", "latitude": 4.326, "longitude": 9.3301, "x": 29.9, "y": 46.6, "source": "geonames", "type": "PPL" }, { "name": "Bonadoumbe", "latitude": 4.2357, "longitude": 9.4301, "x": 62.7, "y": 74.9, "source": "geonames", "type": "PPL" }, { "name": "Diongo", "latitude": 4.2, "longitude": 9.46667, "x": 74.6, "y": 86.1, "source": "geonames", "type": "PPL" }, { "name": "Djopongo", "latitude": 4.1927, "longitude": 9.5136, "x": 90, "y": 88.3, "source": "geonames", "type": "PPL" }, { "name": "Efote", "latitude": 4.38333, "longitude": 9.3, "x": 20.1, "y": 28.7, "source": "geonames", "type": "PPL" }, { "name": "Ekona Lelu", "latitude": 4.2715, "longitude": 9.3015, "x": 20.6, "y": 63.7, "source": "geonames", "type": "PPL" }, { "name": "Ekona Mbenge", "latitude": 4.2281, "longitude": 9.3369, "x": 32.2, "y": 77.3, "source": "geonames", "type": "PPL" }, { "name": "Ekona Yard", "latitude": 4.2147, "longitude": 9.3392, "x": 32.9, "y": 81.5, "source": "geonames", "type": "PPL" }, { "name": "Ikata", "latitude": 4.3307, "longitude": 9.3594, "x": 39.5, "y": 45.1, "source": "geonames", "type": "PPL" }, { "name": "Koto I", "latitude": 4.3826, "longitude": 9.4863, "x": 81.1, "y": 28.9, "source": "geonames", "type": "PPL" }, { "name": "Koto III", "latitude": 4.3359, "longitude": 9.4929, "x": 83.2, "y": 43.5, "source": "geonames", "type": "PPL" }, { "name": "Likoki", "latitude": 4.1874, "longitude": 9.4475, "x": 68.4, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Likoko", "latitude": 4.3972, "longitude": 9.3204, "x": 26.8, "y": 24.3, "source": "geonames", "type": "PPL" }, { "name": "Lilale", "latitude": 4.4037, "longitude": 9.2855, "x": 15.3, "y": 22.3, "source": "geonames", "type": "PPL" }, { "name": "Malende", "latitude": 4.3406, "longitude": 9.4365, "x": 64.8, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Mandese", "latitude": 4.3862, "longitude": 9.46822, "x": 75.1, "y": 27.8, "source": "geonames", "type": "PPL" }, { "name": "Mangamba", "latitude": 4.2456, "longitude": 9.4344, "x": 64.1, "y": 71.8, "source": "geonames", "type": "PPL" }, { "name": "Mangundu", "latitude": 4.265, "longitude": 9.3009, "x": 20.4, "y": 65.7, "source": "geonames", "type": "PPL" }, { "name": "Masuma", "latitude": 4.28076, "longitude": 9.3605, "x": 39.9, "y": 60.8, "source": "geonames", "type": "PPL" }, { "name": "Matango", "latitude": 4.2551, "longitude": 9.3292, "x": 29.6, "y": 68.8, "source": "geonames", "type": "PPL" }, { "name": "Matouke", "latitude": 4.2818, "longitude": 9.4549, "x": 70.8, "y": 60.4, "source": "geonames", "type": "PPL" }, { "name": "Meanja", "latitude": 4.265, "longitude": 9.3967, "x": 51.7, "y": 65.7, "source": "geonames", "type": "PPL" }, { "name": "Mondoni", "latitude": 4.1883, "longitude": 9.4719, "x": 76.4, "y": 89.7, "source": "geonames", "type": "PPL" }, { "name": "Mowutu", "latitude": 4.2614, "longitude": 9.3651, "x": 41.4, "y": 66.8, "source": "geonames", "type": "PPL" }, { "name": "Mpundu", "latitude": 4.2356, "longitude": 9.4108, "x": 56.4, "y": 74.9, "source": "geonames", "type": "PPL" }, { "name": "Mundame", "latitude": 4.2535, "longitude": 9.313, "x": 24.3, "y": 69.3, "source": "geonames", "type": "PPL" }, { "name": "Munyenge", "latitude": 4.405, "longitude": 9.2699, "x": 10.2, "y": 21.9, "source": "geonames", "type": "PPL" }, { "name": "Musone", "latitude": 4.4429, "longitude": 9.2692, "x": 10, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Nachtigal Bekeke", "latitude": 4.356, "longitude": 9.51, "x": 88.8, "y": 37.2, "source": "geonames", "type": "PPL" }, { "name": "Owe", "latitude": 4.2986, "longitude": 9.3806, "x": 46.5, "y": 55.2, "source": "geonames", "type": "PPL" }, { "name": "Penda Mboko", "latitude": 4.2733, "longitude": 9.4505, "x": 69.3, "y": 63.1, "source": "geonames", "type": "PPL" }, { "name": "Puwo Camp", "latitude": 4.2376, "longitude": 9.3628, "x": 40.6, "y": 74.3, "source": "geonames", "type": "PPL" }, { "name": "Small Mpundu", "latitude": 4.2487, "longitude": 9.3954, "x": 51.3, "y": 70.8, "source": "geonames", "type": "PPL" }, { "name": "Yoke", "latitude": 4.31267, "longitude": 9.43099, "x": 63, "y": 50.8, "source": "geonames", "type": "PPL" } ], "Nanga-Eboko": [ { "name": "Akak", "latitude": 4.65, "longitude": 12.35, "x": 45.2, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Akouta", "latitude": 4.5, "longitude": 12.48333, "x": 70.8, "y": 83.6, "source": "geonames", "type": "PPL" }, { "name": "Avangane", "latitude": 4.63333, "longitude": 12.25, "x": 26, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Bidougou", "latitude": 4.46667, "longitude": 12.38333, "x": 51.6, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Bikaga", "latitude": 4.53333, "longitude": 12.46667, "x": 67.6, "y": 77.2, "source": "geonames", "type": "PPL" }, { "name": "Bikoada", "latitude": 4.55, "longitude": 12.26667, "x": 29.2, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Bipan", "latitude": 4.68333, "longitude": 12.33333, "x": 42, "y": 48.4, "source": "geonames", "type": "PPL" }, { "name": "Bipoga", "latitude": 4.71667, "longitude": 12.38333, "x": 51.6, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Boundjou", "latitude": 4.7, "longitude": 12.41667, "x": 58, "y": 45.2, "source": "geonames", "type": "PPL" }, { "name": "Darosa", "latitude": 4.8, "longitude": 12.3, "x": 35.6, "y": 26, "source": "geonames", "type": "PPL" }, { "name": "Dontchua", "latitude": 4.88333, "longitude": 12.41667, "x": 58, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Edoudoumwon", "latitude": 4.63333, "longitude": 12.36667, "x": 48.4, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Ekanga", "latitude": 4.76667, "longitude": 12.48333, "x": 70.8, "y": 32.4, "source": "geonames", "type": "PPL" }, { "name": "Ekomaye", "latitude": 4.56667, "longitude": 12.31667, "x": 38.8, "y": 70.8, "source": "geonames", "type": "PPL" }, { "name": "Ekomba", "latitude": 4.66667, "longitude": 12.35, "x": 45.2, "y": 51.6, "source": "geonames", "type": "PPL" }, { "name": "Ekondong", "latitude": 4.81667, "longitude": 12.2, "x": 16.4, "y": 22.8, "source": "geonames", "type": "PPL" }, { "name": "Ekpwakoula", "latitude": 4.56667, "longitude": 12.36667, "x": 48.4, "y": 70.8, "source": "geonames", "type": "PPL" }, { "name": "Elone", "latitude": 4.56667, "longitude": 12.46667, "x": 67.6, "y": 70.8, "source": "geonames", "type": "PPL" }, { "name": "Emtse", "latitude": 4.81667, "longitude": 12.28333, "x": 32.4, "y": 22.8, "source": "geonames", "type": "PPL" }, { "name": "Engane", "latitude": 4.63333, "longitude": 12.3, "x": 35.6, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Essamesso", "latitude": 4.58333, "longitude": 12.46667, "x": 67.6, "y": 67.6, "source": "geonames", "type": "PPL" }, { "name": "Etognang", "latitude": 4.66667, "longitude": 12.43333, "x": 61.2, "y": 51.6, "source": "geonames", "type": "PPL" }, { "name": "Evounzi", "latitude": 4.65, "longitude": 12.41667, "x": 58, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Kaa", "latitude": 4.76667, "longitude": 12.48333, "x": 70.8, "y": 32.4, "source": "geonames", "type": "PPL" }, { "name": "Kokoum", "latitude": 4.63333, "longitude": 12.4, "x": 54.8, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Kono", "latitude": 4.66667, "longitude": 12.16667, "x": 10, "y": 51.6, "source": "geonames", "type": "PPL" }, { "name": "Loum", "latitude": 4.63333, "longitude": 12.31667, "x": 38.8, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Mbiam", "latitude": 4.53333, "longitude": 12.51667, "x": 77.2, "y": 77.2, "source": "geonames", "type": "PPL" }, { "name": "Mbong", "latitude": 4.83333, "longitude": 12.43333, "x": 61.2, "y": 19.6, "source": "geonames", "type": "PPL" }, { "name": "Mebole", "latitude": 4.76667, "longitude": 12.18333, "x": 13.2, "y": 32.4, "source": "geonames", "type": "PPL" }, { "name": "Megangme", "latitude": 4.6, "longitude": 12.23333, "x": 22.8, "y": 64.4, "source": "geonames", "type": "PPL" }, { "name": "Mekak", "latitude": 4.5, "longitude": 12.33333, "x": 42, "y": 83.6, "source": "geonames", "type": "PPL" }, { "name": "Mekomo", "latitude": 4.58333, "longitude": 12.38333, "x": 51.6, "y": 67.6, "source": "geonames", "type": "PPL" }, { "name": "Memvounga", "latitude": 4.81667, "longitude": 12.53333, "x": 80.4, "y": 22.8, "source": "geonames", "type": "PPL" }, { "name": "Mengae", "latitude": 4.85, "longitude": 12.43333, "x": 61.2, "y": 16.4, "source": "geonames", "type": "PPL" }, { "name": "Mengane", "latitude": 4.55, "longitude": 12.31667, "x": 38.8, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Mengang", "latitude": 4.65, "longitude": 12.45, "x": 64.4, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Messibigui", "latitude": 4.53333, "longitude": 12.36667, "x": 48.4, "y": 77.2, "source": "geonames", "type": "PPL" }, { "name": "Mewout", "latitude": 4.48333, "longitude": 12.35, "x": 45.2, "y": 86.8, "source": "geonames", "type": "PPL" }, { "name": "Meyane", "latitude": 4.65, "longitude": 12.18333, "x": 13.2, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Meyosso", "latitude": 4.5, "longitude": 12.33333, "x": 42, "y": 83.6, "source": "geonames", "type": "PPL" }, { "name": "Mezassa", "latitude": 4.46667, "longitude": 12.36667, "x": 48.4, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Mfomalene", "latitude": 4.63333, "longitude": 12.55, "x": 83.6, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Miba", "latitude": 4.73333, "longitude": 12.45, "x": 64.4, "y": 38.8, "source": "geonames", "type": "PPL" }, { "name": "Minkom", "latitude": 4.73333, "longitude": 12.45, "x": 64.4, "y": 38.8, "source": "geonames", "type": "PPL" }, { "name": "Mpang", "latitude": 4.66667, "longitude": 12.2, "x": 16.4, "y": 51.6, "source": "geonames", "type": "PPL" }, { "name": "Mvono", "latitude": 4.51667, "longitude": 12.36667, "x": 48.4, "y": 80.4, "source": "geonames", "type": "PPL" }, { "name": "Nang-Ekak", "latitude": 4.65, "longitude": 12.58333, "x": 90, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Nangmane", "latitude": 4.83333, "longitude": 12.38333, "x": 51.6, "y": 19.6, "source": "geonames", "type": "PPL" }, { "name": "Ndemba", "latitude": 4.5, "longitude": 12.36667, "x": 48.4, "y": 83.6, "source": "geonames", "type": "PPL" }, { "name": "Ndombam", "latitude": 4.61667, "longitude": 12.25, "x": 26, "y": 61.2, "source": "geonames", "type": "PPL" }, { "name": "Ndoumba", "latitude": 4.6, "longitude": 12.28333, "x": 32.4, "y": 64.4, "source": "geonames", "type": "PPL" }, { "name": "Ngamba", "latitude": 4.61667, "longitude": 12.33333, "x": 42, "y": 61.2, "source": "geonames", "type": "PPL" }, { "name": "Ngara", "latitude": 4.8, "longitude": 12.18333, "x": 13.2, "y": 26, "source": "geonames", "type": "PPL" }, { "name": "Nguinda", "latitude": 4.65, "longitude": 12.36667, "x": 48.4, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Nkoambang", "latitude": 4.63333, "longitude": 12.51667, "x": 77.2, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Nkolsele", "latitude": 4.51667, "longitude": 12.26667, "x": 29.2, "y": 80.4, "source": "geonames", "type": "PPL" }, { "name": "Nkomeye", "latitude": 4.63333, "longitude": 12.55, "x": 83.6, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Okassang", "latitude": 4.51667, "longitude": 12.28333, "x": 32.4, "y": 80.4, "source": "geonames", "type": "PPL" }, { "name": "Omgbangdouma", "latitude": 4.53333, "longitude": 12.28333, "x": 32.4, "y": 77.2, "source": "geonames", "type": "PPL" }, { "name": "Sandza", "latitude": 4.76667, "longitude": 12.35, "x": 45.2, "y": 32.4, "source": "geonames", "type": "PPL" }, { "name": "Sanga", "latitude": 4.58333, "longitude": 12.51667, "x": 77.2, "y": 67.6, "source": "geonames", "type": "PPL" }, { "name": "Tchaouri", "latitude": 4.88333, "longitude": 12.43333, "x": 61.2, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Wala", "latitude": 4.63333, "longitude": 12.38333, "x": 51.6, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Yelle", "latitude": 4.85, "longitude": 12.5, "x": 74, "y": 16.4, "source": "geonames", "type": "PPL" } ], "Ndop": [ { "name": "Baba", "latitude": 5.89545, "longitude": 10.37959, "x": 21.4, "y": 57.2, "source": "geonames", "type": "PPL" }, { "name": "Baba I", "latitude": 6.03333, "longitude": 10.46667, "x": 52.5, "y": 19.2, "source": "geonames", "type": "PPL" }, { "name": "Babungo", "latitude": 6.06667, "longitude": 10.43333, "x": 40.6, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Bafanji", "latitude": 5.86202, "longitude": 10.46748, "x": 52.8, "y": 66.4, "source": "geonames", "type": "PPL" }, { "name": "Bagam", "latitude": 5.81155, "longitude": 10.42021, "x": 35.9, "y": 80.4, "source": "geonames", "type": "PPL" }, { "name": "Bagam I", "latitude": 5.91506, "longitude": 10.35432, "x": 12.3, "y": 51.8, "source": "geonames", "type": "PPL" }, { "name": "Bagam II", "latitude": 5.89462, "longitude": 10.34782, "x": 10, "y": 57.4, "source": "geonames", "type": "PPL" }, { "name": "Balaneba", "latitude": 5.91819, "longitude": 10.39095, "x": 25.4, "y": 50.9, "source": "geonames", "type": "PPL" }, { "name": "Bali-Gangsin", "latitude": 5.82671, "longitude": 10.35844, "x": 13.8, "y": 76.2, "source": "geonames", "type": "PPL" }, { "name": "Bali-Gashu", "latitude": 5.81461, "longitude": 10.38893, "x": 24.7, "y": 79.5, "source": "geonames", "type": "PPL" }, { "name": "Balikumbat", "latitude": 5.89267, "longitude": 10.35983, "x": 14.3, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Balom", "latitude": 5.83798, "longitude": 10.35847, "x": 13.8, "y": 73.1, "source": "geonames", "type": "PPL" }, { "name": "Bamali", "latitude": 5.94547, "longitude": 10.42202, "x": 36.5, "y": 43.4, "source": "geonames", "type": "PPL" }, { "name": "Bambani", "latitude": 5.92403, "longitude": 10.48444, "x": 58.8, "y": 49.3, "source": "geonames", "type": "PPL" }, { "name": "Bamessing", "latitude": 5.97848, "longitude": 10.36149, "x": 14.9, "y": 34.3, "source": "geonames", "type": "PPL" }, { "name": "Bamumkumbit", "latitude": 5.81667, "longitude": 10.41667, "x": 34.6, "y": 78.9, "source": "geonames", "type": "PPL" }, { "name": "Bamunka", "latitude": 5.98276, "longitude": 10.45486, "x": 48.3, "y": 33.1, "source": "geonames", "type": "PPL" }, { "name": "Bangolan", "latitude": 5.90389, "longitude": 10.37816, "x": 20.8, "y": 54.9, "source": "geonames", "type": "PPL" }, { "name": "Bati", "latitude": 5.87978, "longitude": 10.36903, "x": 17.6, "y": 61.5, "source": "geonames", "type": "PPL" }, { "name": "Batono", "latitude": 5.85069, "longitude": 10.41296, "x": 33.3, "y": 69.6, "source": "geonames", "type": "PPL" }, { "name": "Bekeu", "latitude": 5.9863, "longitude": 10.46792, "x": 52.9, "y": 32.2, "source": "geonames", "type": "PPL" }, { "name": "Chamba", "latitude": 5.82074, "longitude": 10.359, "x": 14, "y": 77.8, "source": "geonames", "type": "PPL" }, { "name": "Chossi", "latitude": 5.91641, "longitude": 10.44024, "x": 43, "y": 51.4, "source": "geonames", "type": "PPL" }, { "name": "Ekwo", "latitude": 5.85127, "longitude": 10.39062, "x": 25.3, "y": 69.4, "source": "geonames", "type": "PPL" }, { "name": "Fambuo", "latitude": 5.89301, "longitude": 10.47723, "x": 56.3, "y": 57.9, "source": "geonames", "type": "PPL" }, { "name": "Fohuk", "latitude": 5.88245, "longitude": 10.49416, "x": 62.3, "y": 60.8, "source": "geonames", "type": "PPL" }, { "name": "Gashu", "latitude": 5.83164, "longitude": 10.37058, "x": 18.1, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Haussa", "latitude": 5.97769, "longitude": 10.40236, "x": 29.5, "y": 34.5, "source": "geonames", "type": "PPL" }, { "name": "Jogoru", "latitude": 5.86902, "longitude": 10.42038, "x": 35.9, "y": 64.5, "source": "geonames", "type": "PPL" }, { "name": "Kayilbi", "latitude": 5.89908, "longitude": 10.3701, "x": 18, "y": 56.2, "source": "geonames", "type": "PPL" }, { "name": "Kenkong", "latitude": 5.95132, "longitude": 10.35015, "x": 10.8, "y": 41.8, "source": "geonames", "type": "PPL" }, { "name": "Kietbe", "latitude": 5.80549, "longitude": 10.47417, "x": 55.2, "y": 82, "source": "geonames", "type": "PPL" }, { "name": "Koduma", "latitude": 5.90754, "longitude": 10.37035, "x": 18.1, "y": 53.9, "source": "geonames", "type": "PPL" }, { "name": "Komone", "latitude": 5.84164, "longitude": 10.35217, "x": 11.6, "y": 72.1, "source": "geonames", "type": "PPL" }, { "name": "Kwatang", "latitude": 5.97702, "longitude": 10.57164, "x": 90, "y": 34.7, "source": "geonames", "type": "PPL" }, { "name": "Lamessang", "latitude": 5.919, "longitude": 10.35004, "x": 10.8, "y": 50.7, "source": "geonames", "type": "PPL" }, { "name": "Man", "latitude": 5.96524, "longitude": 10.40934, "x": 32, "y": 38, "source": "geonames", "type": "PPL" }, { "name": "Manga", "latitude": 5.86384, "longitude": 10.43265, "x": 40.3, "y": 65.9, "source": "geonames", "type": "PPL" }, { "name": "Massan", "latitude": 5.79715, "longitude": 10.42722, "x": 38.4, "y": 84.3, "source": "geonames", "type": "PPL" }, { "name": "Mbagan", "latitude": 5.99342, "longitude": 10.35765, "x": 13.5, "y": 30.2, "source": "geonames", "type": "PPL" }, { "name": "Mbakwa I", "latitude": 5.84365, "longitude": 10.40298, "x": 29.7, "y": 71.5, "source": "geonames", "type": "PPL" }, { "name": "Mbakwa II", "latitude": 5.82766, "longitude": 10.40953, "x": 32.1, "y": 75.9, "source": "geonames", "type": "PPL" }, { "name": "Mbanchoro", "latitude": 5.87847, "longitude": 10.4816, "x": 57.8, "y": 61.9, "source": "geonames", "type": "PPL" }, { "name": "Mbanka", "latitude": 5.97497, "longitude": 10.45813, "x": 49.4, "y": 35.3, "source": "geonames", "type": "PPL" }, { "name": "Mbantap", "latitude": 5.85521, "longitude": 10.35348, "x": 12, "y": 68.3, "source": "geonames", "type": "PPL" }, { "name": "Mbante", "latitude": 5.89951, "longitude": 10.52806, "x": 74.4, "y": 56.1, "source": "geonames", "type": "PPL" }, { "name": "Mbasso", "latitude": 5.88953, "longitude": 10.52711, "x": 74.1, "y": 58.9, "source": "geonames", "type": "PPL" }, { "name": "Mbatono", "latitude": 5.88404, "longitude": 10.52439, "x": 73.1, "y": 60.4, "source": "geonames", "type": "PPL" }, { "name": "Mbejong", "latitude": 5.97318, "longitude": 10.3559, "x": 12.9, "y": 35.8, "source": "geonames", "type": "PPL" }, { "name": "Mbeleng", "latitude": 5.98632, "longitude": 10.35822, "x": 13.7, "y": 32.2, "source": "geonames", "type": "PPL" }, { "name": "Mbelepo", "latitude": 5.95213, "longitude": 10.42116, "x": 36.2, "y": 41.6, "source": "geonames", "type": "PPL" }, { "name": "Mbesso", "latitude": 5.97297, "longitude": 10.36233, "x": 15.2, "y": 35.8, "source": "geonames", "type": "PPL" }, { "name": "Megan", "latitude": 5.89938, "longitude": 10.46283, "x": 51.1, "y": 56.1, "source": "geonames", "type": "PPL" }, { "name": "Meli", "latitude": 5.98227, "longitude": 10.43367, "x": 40.7, "y": 33.3, "source": "geonames", "type": "PPLX" }, { "name": "Membepa I", "latitude": 5.84252, "longitude": 10.42091, "x": 36.1, "y": 71.8, "source": "geonames", "type": "PPL" }, { "name": "Mendono", "latitude": 5.854, "longitude": 10.42792, "x": 38.6, "y": 68.6, "source": "geonames", "type": "PPL" }, { "name": "Menfoung", "latitude": 5.7819, "longitude": 10.4381, "x": 42.3, "y": 88.5, "source": "geonames", "type": "PPL" }, { "name": "Menjong", "latitude": 5.84011, "longitude": 10.45363, "x": 47.8, "y": 72.5, "source": "geonames", "type": "PPL" }, { "name": "Messo", "latitude": 5.96587, "longitude": 10.50267, "x": 65.3, "y": 37.8, "source": "geonames", "type": "PPL" }, { "name": "Miedeu", "latitude": 5.79597, "longitude": 10.46094, "x": 50.4, "y": 84.7, "source": "geonames", "type": "PPL" }, { "name": "Mishie", "latitude": 5.90552, "longitude": 10.53393, "x": 76.5, "y": 54.4, "source": "geonames", "type": "PPL" }, { "name": "Missi", "latitude": 5.96182, "longitude": 10.48844, "x": 60.3, "y": 38.9, "source": "geonames", "type": "PPL" }, { "name": "Momo", "latitude": 5.97916, "longitude": 10.43987, "x": 42.9, "y": 34.1, "source": "geonames", "type": "PPL" }, { "name": "Monjoa", "latitude": 5.94976, "longitude": 10.43085, "x": 39.7, "y": 42.2, "source": "geonames", "type": "PPL" }, { "name": "Monjong", "latitude": 5.99572, "longitude": 10.43748, "x": 42, "y": 29.6, "source": "geonames", "type": "PPL" }, { "name": "Monkunlo", "latitude": 5.94026, "longitude": 10.43891, "x": 42.6, "y": 44.9, "source": "geonames", "type": "PPL" }, { "name": "Monkwo", "latitude": 5.94979, "longitude": 10.44148, "x": 43.5, "y": 42.2, "source": "geonames", "type": "PPL" }, { "name": "Mufua", "latitude": 5.98236, "longitude": 10.39231, "x": 25.9, "y": 33.3, "source": "geonames", "type": "PPL" }, { "name": "Mundwa", "latitude": 5.82962, "longitude": 10.44327, "x": 44.1, "y": 75.4, "source": "geonames", "type": "PPL" }, { "name": "Muta", "latitude": 5.93515, "longitude": 10.42902, "x": 39, "y": 46.3, "source": "geonames", "type": "PPL" }, { "name": "Naka", "latitude": 5.99902, "longitude": 10.44295, "x": 44, "y": 28.7, "source": "geonames", "type": "PPL" }, { "name": "Ndingo", "latitude": 5.81097, "longitude": 10.39344, "x": 26.3, "y": 80.5, "source": "geonames", "type": "PPL" }, { "name": "Ngogon", "latitude": 5.79733, "longitude": 10.40042, "x": 28.8, "y": 84.3, "source": "geonames", "type": "PPL" }, { "name": "Ngombo", "latitude": 5.77658, "longitude": 10.42145, "x": 36.3, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ngwala", "latitude": 5.99476, "longitude": 10.49464, "x": 62.5, "y": 29.8, "source": "geonames", "type": "PPL" }, { "name": "Njantang", "latitude": 5.82011, "longitude": 10.41755, "x": 34.9, "y": 78, "source": "geonames", "type": "PPL" }, { "name": "Njenka I", "latitude": 5.81964, "longitude": 10.37783, "x": 20.7, "y": 78.1, "source": "geonames", "type": "PPL" }, { "name": "Njenka II", "latitude": 5.82453, "longitude": 10.39593, "x": 27.2, "y": 76.8, "source": "geonames", "type": "PPL" }, { "name": "Njinling", "latitude": 5.96695, "longitude": 10.37713, "x": 20.5, "y": 37.5, "source": "geonames", "type": "PPL" }, { "name": "Njono", "latitude": 5.88068, "longitude": 10.50534, "x": 66.3, "y": 61.3, "source": "geonames", "type": "PPL" }, { "name": "Nkieve", "latitude": 5.78416, "longitude": 10.47258, "x": 54.6, "y": 87.9, "source": "geonames", "type": "PPL" }, { "name": "Ntenfon", "latitude": 5.81133, "longitude": 10.40094, "x": 29, "y": 80.4, "source": "geonames", "type": "PPL" }, { "name": "Ntenka", "latitude": 5.98315, "longitude": 10.42583, "x": 37.9, "y": 33, "source": "geonames", "type": "PPL" }, { "name": "Ntokwe", "latitude": 5.9898, "longitude": 10.37395, "x": 19.3, "y": 31.2, "source": "geonames", "type": "PPL" }, { "name": "Ntopeli", "latitude": 5.89432, "longitude": 10.50929, "x": 67.7, "y": 57.5, "source": "geonames", "type": "PPL" }, { "name": "Nugu", "latitude": 5.92014, "longitude": 10.36995, "x": 17.9, "y": 50.4, "source": "geonames", "type": "PPL" }, { "name": "Small Bambalang", "latitude": 5.915, "longitude": 10.42711, "x": 38.3, "y": 51.8, "source": "geonames", "type": "PPL" }, { "name": "Tankwo", "latitude": 5.82641, "longitude": 10.43983, "x": 42.9, "y": 76.3, "source": "geonames", "type": "PPL" }, { "name": "Tinekala", "latitude": 5.88675, "longitude": 10.36423, "x": 15.9, "y": 59.6, "source": "geonames", "type": "PPL" }, { "name": "Tola", "latitude": 5.99862, "longitude": 10.46, "x": 50.1, "y": 28.8, "source": "geonames", "type": "PPL" }, { "name": "Tula", "latitude": 5.9499, "longitude": 10.39678, "x": 27.5, "y": 42.2, "source": "geonames", "type": "PPL" }, { "name": "Wapu", "latitude": 5.89896, "longitude": 10.35103, "x": 11.1, "y": 56.3, "source": "geonames", "type": "PPL" } ], "Ndu": [ { "name": "Lassin", "latitude": 6.43333, "longitude": 10.58333, "x": 10, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Mbwat", "latitude": 6.48333, "longitude": 10.75, "x": 42.7, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Nanso", "latitude": 6.33333, "longitude": 10.83333, "x": 59, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Nsob", "latitude": 6.36667, "longitude": 10.86667, "x": 65.5, "y": 72.2, "source": "geonames", "type": "PPL" }, { "name": "Ntumbaw", "latitude": 6.36667, "longitude": 10.78333, "x": 49.2, "y": 72.2, "source": "geonames", "type": "PPL" }, { "name": "Rom", "latitude": 6.41654, "longitude": 10.99148, "x": 90, "y": 45.6, "source": "geonames", "type": "PPL" }, { "name": "Tala", "latitude": 6.48333, "longitude": 10.76667, "x": 45.9, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Wowo", "latitude": 6.40429, "longitude": 10.8395, "x": 60.2, "y": 52.2, "source": "geonames", "type": "PPL" } ], "Ngaoundere": [ { "name": "Anam", "latitude": 7.23333, "longitude": 13.4, "x": 13.8, "y": 60.5, "source": "geonames", "type": "PPL" }, { "name": "Anloua", "latitude": 7.41667, "longitude": 13.43333, "x": 21.4, "y": 14.2, "source": "geonames", "type": "PPL" }, { "name": "Beka", "latitude": 7.31105, "longitude": 13.5479, "x": 47.6, "y": 40.9, "source": "geonames", "type": "PPL" }, { "name": "Djaoro Lendjouk", "latitude": 7.25, "longitude": 13.51667, "x": 40.5, "y": 56.3, "source": "geonames", "type": "PPL" }, { "name": "Djaoro Nana", "latitude": 7.38333, "longitude": 13.51667, "x": 40.5, "y": 22.6, "source": "geonames", "type": "PPL" }, { "name": "Djaoro Sambo", "latitude": 7.38333, "longitude": 13.53333, "x": 44.3, "y": 22.6, "source": "geonames", "type": "PPL" }, { "name": "Djouroum", "latitude": 7.18333, "longitude": 13.6, "x": 59.5, "y": 73.2, "source": "geonames", "type": "PPL" }, { "name": "Hamoa Bello", "latitude": 7.21667, "longitude": 13.41667, "x": 17.6, "y": 64.7, "source": "geonames", "type": "PPL" }, { "name": "Kantalan", "latitude": 7.2, "longitude": 13.53333, "x": 44.3, "y": 68.9, "source": "geonames", "type": "PPL" }, { "name": "Korianga", "latitude": 7.43333, "longitude": 13.51667, "x": 40.5, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Longtre", "latitude": 7.25, "longitude": 13.55, "x": 48.1, "y": 56.3, "source": "geonames", "type": "PPL" }, { "name": "Malat", "latitude": 7.38333, "longitude": 13.58333, "x": 55.7, "y": 22.6, "source": "geonames", "type": "PPL" }, { "name": "Marboui", "latitude": 7.15, "longitude": 13.53333, "x": 44.3, "y": 81.6, "source": "geonames", "type": "PPL" }, { "name": "Marcol", "latitude": 7.41667, "longitude": 13.4, "x": 13.8, "y": 14.2, "source": "geonames", "type": "PPL" }, { "name": "Marko", "latitude": 7.21667, "longitude": 13.43333, "x": 21.4, "y": 64.7, "source": "geonames", "type": "PPL" }, { "name": "Marma", "latitude": 7.2, "longitude": 13.46667, "x": 29, "y": 68.9, "source": "geonames", "type": "PPL" }, { "name": "Mbalam", "latitude": 7.31667, "longitude": 13.73333, "x": 90, "y": 39.5, "source": "geonames", "type": "PPL" }, { "name": "Mbi Djoro", "latitude": 7.38333, "longitude": 13.56667, "x": 51.9, "y": 22.6, "source": "geonames", "type": "PPL" }, { "name": "Mbiassoro", "latitude": 7.33333, "longitude": 13.45, "x": 25.2, "y": 35.3, "source": "geonames", "type": "PPL" }, { "name": "Ndamagorome", "latitude": 7.11667, "longitude": 13.51667, "x": 40.5, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Rep", "latitude": 7.31667, "longitude": 13.38333, "x": 10, "y": 39.5, "source": "geonames", "type": "PPL" }, { "name": "Sarayan", "latitude": 7.16667, "longitude": 13.73333, "x": 90, "y": 77.4, "source": "geonames", "type": "PPL" }, { "name": "Wakwa", "latitude": 7.23333, "longitude": 13.58333, "x": 55.7, "y": 60.5, "source": "geonames", "type": "PPL" } ], "Ngaoundal": [ { "name": "Bagodo", "latitude": 6.41667, "longitude": 13.38333, "x": 90, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Betare Gongo", "latitude": 6.61667, "longitude": 13.21667, "x": 36.7, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Bola", "latitude": 6.68333, "longitude": 13.33333, "x": 74, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Bori", "latitude": 6.46667, "longitude": 13.36667, "x": 84.7, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Milo", "latitude": 6.5, "longitude": 13.25, "x": 47.3, "y": 51.9, "source": "geonames", "type": "PPL" }, { "name": "Sourouma Betare", "latitude": 6.33333, "longitude": 13.13333, "x": 10, "y": 90, "source": "geonames", "type": "PPL" } ], "Ngambe": [ { "name": "Dibemgi", "latitude": 4.3, "longitude": 10.51667, "x": 29.2, "y": 40.3, "source": "geonames", "type": "PPL" }, { "name": "Dikamak", "latitude": 4.13333, "longitude": 10.65, "x": 54.8, "y": 73.9, "source": "geonames", "type": "PPL" }, { "name": "Itayap", "latitude": 4.21667, "longitude": 10.78333, "x": 80.4, "y": 57.1, "source": "geonames", "type": "PPL" }, { "name": "Kan", "latitude": 4.08333, "longitude": 10.66667, "x": 58, "y": 84, "source": "geonames", "type": "PPL" }, { "name": "Kokoa", "latitude": 4.38333, "longitude": 10.7, "x": 64.4, "y": 23.5, "source": "geonames", "type": "PPL" }, { "name": "Kokoa II", "latitude": 4.18333, "longitude": 10.63333, "x": 51.6, "y": 63.8, "source": "geonames", "type": "PPL" }, { "name": "Mahohi I", "latitude": 4.36667, "longitude": 10.61667, "x": 48.4, "y": 26.8, "source": "geonames", "type": "PPL" }, { "name": "Mahonda", "latitude": 4.26667, "longitude": 10.81667, "x": 86.8, "y": 47, "source": "geonames", "type": "PPL" }, { "name": "Malohe", "latitude": 4.18333, "longitude": 10.83333, "x": 90, "y": 63.8, "source": "geonames", "type": "PPL" }, { "name": "Mandjap I", "latitude": 4.21667, "longitude": 10.58333, "x": 42, "y": 57.1, "source": "geonames", "type": "PPL" }, { "name": "Masok", "latitude": 4.13333, "longitude": 10.46667, "x": 19.6, "y": 73.9, "source": "geonames", "type": "PPL" }, { "name": "Mbanda", "latitude": 4.1, "longitude": 10.6, "x": 45.2, "y": 80.6, "source": "geonames", "type": "PPL" }, { "name": "Ndede II", "latitude": 4.2, "longitude": 10.71667, "x": 67.6, "y": 60.5, "source": "geonames", "type": "PPL" }, { "name": "Ndokbaembi", "latitude": 4.31667, "longitude": 10.41667, "x": 10, "y": 36.9, "source": "geonames", "type": "PPL" }, { "name": "Ndokbangendeng", "latitude": 4.33333, "longitude": 10.43333, "x": 13.2, "y": 33.5, "source": "geonames", "type": "PPL" }, { "name": "Ndokbasagog I", "latitude": 4.45, "longitude": 10.61667, "x": 48.4, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Ndokminokon I", "latitude": 4.36667, "longitude": 10.45, "x": 16.4, "y": 26.8, "source": "geonames", "type": "PPL" }, { "name": "Nganbe II", "latitude": 4.2, "longitude": 10.73333, "x": 70.8, "y": 60.5, "source": "geonames", "type": "PPL" }, { "name": "Ngobilo", "latitude": 4.33333, "longitude": 10.61667, "x": 48.4, "y": 33.5, "source": "geonames", "type": "PPL" }, { "name": "Ngonbe", "latitude": 4.06667, "longitude": 10.55, "x": 35.6, "y": 87.4, "source": "geonames", "type": "PPL" }, { "name": "Niel", "latitude": 4.25, "longitude": 10.51667, "x": 29.2, "y": 50.4, "source": "geonames", "type": "PPL" }, { "name": "Ninga", "latitude": 4.31667, "longitude": 10.51667, "x": 29.2, "y": 36.9, "source": "geonames", "type": "PPL" }, { "name": "Nkak Boum", "latitude": 4.2, "longitude": 10.61667, "x": 48.4, "y": 60.5, "source": "geonames", "type": "PPL" }, { "name": "Nkaka", "latitude": 4.25, "longitude": 10.75, "x": 74, "y": 50.4, "source": "geonames", "type": "PPL" }, { "name": "Nkam", "latitude": 4.35, "longitude": 10.66667, "x": 58, "y": 30.2, "source": "geonames", "type": "PPL" }, { "name": "Nsapak", "latitude": 4.23333, "longitude": 10.65, "x": 54.8, "y": 53.7, "source": "geonames", "type": "PPL" }, { "name": "Ntanbe", "latitude": 4.35, "longitude": 10.53333, "x": 32.4, "y": 30.2, "source": "geonames", "type": "PPL" }, { "name": "Nyouya", "latitude": 4.16667, "longitude": 10.56667, "x": 38.8, "y": 67.2, "source": "geonames", "type": "PPL" }, { "name": "Pendjok", "latitude": 4.11667, "longitude": 10.73333, "x": 70.8, "y": 77.3, "source": "geonames", "type": "PPL" }, { "name": "Poutkak", "latitude": 4.11667, "longitude": 10.55, "x": 35.6, "y": 77.3, "source": "geonames", "type": "PPL" }, { "name": "Sakbayeme", "latitude": 4.05362, "longitude": 10.56421, "x": 38.3, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Sanba", "latitude": 4.2, "longitude": 10.56667, "x": 38.8, "y": 60.5, "source": "geonames", "type": "PPL" }, { "name": "Songmbenge", "latitude": 4.05437, "longitude": 10.55808, "x": 37.2, "y": 89.8, "source": "geonames", "type": "PPLX" }, { "name": "Tomel", "latitude": 4.15, "longitude": 10.48333, "x": 22.8, "y": 70.5, "source": "geonames", "type": "PPL" }, { "name": "Yebel", "latitude": 4.13333, "longitude": 10.75, "x": 74, "y": 73.9, "source": "geonames", "type": "PPL" } ], "Ngomedzap": [ { "name": "Abang", "latitude": 3.28333, "longitude": 11.26667, "x": 64.4, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Abiete", "latitude": 3.1, "longitude": 11.25, "x": 61.2, "y": 86.2, "source": "geonames", "type": "PPL" }, { "name": "Abodemving", "latitude": 3.26667, "longitude": 11.26667, "x": 64.4, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Adjap", "latitude": 3.08333, "longitude": 11.13333, "x": 38.8, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Adzap", "latitude": 3.33333, "longitude": 11.21667, "x": 54.8, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Akok", "latitude": 3.26667, "longitude": 11.16667, "x": 45.2, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Akongo", "latitude": 3.38333, "longitude": 11.08333, "x": 29.2, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Akongo III", "latitude": 3.4, "longitude": 11.11667, "x": 35.6, "y": 17.6, "source": "geonames", "type": "PPL" }, { "name": "Angonfeme", "latitude": 3.41667, "longitude": 11.13333, "x": 38.8, "y": 13.8, "source": "geonames", "type": "PPL" }, { "name": "Asie", "latitude": 3.35, "longitude": 11.3, "x": 70.8, "y": 29, "source": "geonames", "type": "PPL" }, { "name": "Ayene", "latitude": 3.26667, "longitude": 11.11667, "x": 35.6, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Bilon", "latitude": 3.3, "longitude": 11.35, "x": 80.4, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Bilong", "latitude": 3.3, "longitude": 11.11667, "x": 35.6, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Biwong", "latitude": 3.13333, "longitude": 11.15, "x": 42, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Djom", "latitude": 3.15, "longitude": 11.15, "x": 42, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Doum Ola", "latitude": 3.1, "longitude": 11.35, "x": 80.4, "y": 86.2, "source": "geonames", "type": "PPL" }, { "name": "Ebaminal", "latitude": 3.31667, "longitude": 11.25, "x": 61.2, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Ebayaga", "latitude": 3.36667, "longitude": 11.01667, "x": 16.4, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Ebenewoman", "latitude": 3.08333, "longitude": 11.23333, "x": 58, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ebom", "latitude": 3.28333, "longitude": 11.05, "x": 22.8, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Ekombite", "latitude": 3.08333, "longitude": 11.3, "x": 70.8, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ekoudbesanda", "latitude": 3.38333, "longitude": 11.1, "x": 32.4, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Ekoumeyek", "latitude": 3.38333, "longitude": 11.25, "x": 61.2, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Enamingal", "latitude": 3.1, "longitude": 11.28333, "x": 67.6, "y": 86.2, "source": "geonames", "type": "PPL" }, { "name": "Esingang", "latitude": 3.11667, "longitude": 11.28333, "x": 67.6, "y": 82.4, "source": "geonames", "type": "PPL" }, { "name": "Kama", "latitude": 3.23333, "longitude": 11.2, "x": 51.6, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Kamba", "latitude": 3.23333, "longitude": 11.33333, "x": 77.2, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Koulouganga", "latitude": 3.25, "longitude": 11.1, "x": 32.4, "y": 51.9, "source": "geonames", "type": "PPL" }, { "name": "Koumasi", "latitude": 3.31667, "longitude": 11.21667, "x": 54.8, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Mbekaa I", "latitude": 3.1, "longitude": 11.26667, "x": 64.4, "y": 86.2, "source": "geonames", "type": "PPL" }, { "name": "Mbing Yelkas", "latitude": 3.31667, "longitude": 11.1, "x": 32.4, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Mebomezoa", "latitude": 3.2, "longitude": 11.3, "x": 70.8, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Mekamba", "latitude": 3.13333, "longitude": 11.33333, "x": 77.2, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Melen", "latitude": 3.31667, "longitude": 11.3, "x": 70.8, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Mengboua", "latitude": 3.08333, "longitude": 11.25, "x": 61.2, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Mengeme", "latitude": 3.25, "longitude": 11.4, "x": 90, "y": 51.9, "source": "geonames", "type": "PPL" }, { "name": "Mesok", "latitude": 3.08333, "longitude": 11.35, "x": 80.4, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Metomba", "latitude": 3.33333, "longitude": 11.33333, "x": 77.2, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Mfida", "latitude": 3.2, "longitude": 11.4, "x": 90, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Minganda", "latitude": 3.21667, "longitude": 11.03333, "x": 19.6, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Mingeme", "latitude": 3.38333, "longitude": 11.15, "x": 42, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Minkan", "latitude": 3.15, "longitude": 11.13333, "x": 38.8, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Minlaba", "latitude": 3.16667, "longitude": 11.35, "x": 80.4, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Mvamedjap", "latitude": 3.08333, "longitude": 11.35, "x": 80.4, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Mvengue", "latitude": 3.28333, "longitude": 11.01667, "x": 16.4, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Mvinge", "latitude": 3.28333, "longitude": 10.98333, "x": 10, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Nbabewa", "latitude": 3.36667, "longitude": 11.21667, "x": 54.8, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Ngoungal", "latitude": 3.36667, "longitude": 11.23333, "x": 58, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Nkoabe", "latitude": 3.3, "longitude": 11.2, "x": 51.6, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Nkoambe", "latitude": 3.3, "longitude": 11.3, "x": 70.8, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Nkolamougou", "latitude": 3.15, "longitude": 11.16667, "x": 45.2, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Nkolbewa II", "latitude": 3.28333, "longitude": 11.2, "x": 51.6, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Nkolbewa III", "latitude": 3.35, "longitude": 11.21667, "x": 54.8, "y": 29, "source": "geonames", "type": "PPL" }, { "name": "Nkolmeyang", "latitude": 3.2, "longitude": 11.21667, "x": 54.8, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Nkolmindim", "latitude": 3.2, "longitude": 11.08333, "x": 29.2, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Nkolnking", "latitude": 3.15, "longitude": 11.1, "x": 32.4, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Nkolonye", "latitude": 3.15, "longitude": 11.11667, "x": 35.6, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Nkongnen", "latitude": 3.16667, "longitude": 11.2, "x": 51.6, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Nkoulngoui", "latitude": 3.36667, "longitude": 11.1, "x": 32.4, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Nsimalen", "latitude": 3.36667, "longitude": 11.25, "x": 61.2, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Nyemeyong", "latitude": 3.18333, "longitude": 11.36667, "x": 83.6, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Nyenge", "latitude": 3.43333, "longitude": 11.13333, "x": 38.8, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Nyep", "latitude": 3.18333, "longitude": 11.06667, "x": 26, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Obang II", "latitude": 3.08333, "longitude": 11.25, "x": 61.2, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Okala", "latitude": 3.2, "longitude": 11.01667, "x": 16.4, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Okarobele", "latitude": 3.38333, "longitude": 11.03333, "x": 19.6, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Osoe Mekening", "latitude": 3.38333, "longitude": 11.25, "x": 61.2, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Osoesam", "latitude": 3.25, "longitude": 11.3, "x": 70.8, "y": 51.9, "source": "geonames", "type": "PPL" }, { "name": "Oveng", "latitude": 3.08333, "longitude": 11.31667, "x": 74, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Soumou", "latitude": 3.1, "longitude": 11.33333, "x": 77.2, "y": 86.2, "source": "geonames", "type": "PPL" }, { "name": "Tiga", "latitude": 3.26667, "longitude": 11.13333, "x": 38.8, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Wom", "latitude": 3.21667, "longitude": 11.13333, "x": 38.8, "y": 59.5, "source": "geonames", "type": "PPL" } ], "Ngoro": [ { "name": "Boko", "latitude": 4.98333, "longitude": 11.21667, "x": 10, "y": 47.6, "source": "geonames", "type": "PPL" }, { "name": "Deuk", "latitude": 4.93333, "longitude": 11.26667, "x": 20.4, "y": 61.8, "source": "geonames", "type": "PPL" }, { "name": "Diaka", "latitude": 4.88333, "longitude": 11.26667, "x": 20.4, "y": 75.9, "source": "geonames", "type": "PPL" }, { "name": "Egona", "latitude": 4.83333, "longitude": 11.33333, "x": 34.3, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Kouniakong", "latitude": 5.1, "longitude": 11.4, "x": 48.3, "y": 14.7, "source": "geonames", "type": "PPL" }, { "name": "Ngon", "latitude": 4.91667, "longitude": 11.6, "x": 90, "y": 66.5, "source": "geonames", "type": "PPL" }, { "name": "Yangba", "latitude": 5.11667, "longitude": 11.38333, "x": 44.8, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Zakan", "latitude": 4.91667, "longitude": 11.23333, "x": 13.5, "y": 66.5, "source": "geonames", "type": "PPL" } ], "Nguti": [ { "name": "Abat", "latitude": 5.37421, "longitude": 9.21413, "x": 10.5, "y": 37.5, "source": "geonames", "type": "PPL" }, { "name": "Akak", "latitude": 5.48333, "longitude": 9.36667, "x": 41.1, "y": 12.7, "source": "geonames", "type": "PPL" }, { "name": "Ashum", "latitude": 5.49543, "longitude": 9.51897, "x": 71.7, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Ayong", "latitude": 5.1975, "longitude": 9.3152, "x": 30.8, "y": 77.6, "source": "geonames", "type": "PPL" }, { "name": "Bakogo", "latitude": 5.46486, "longitude": 9.30942, "x": 29.6, "y": 16.9, "source": "geonames", "type": "PPL" }, { "name": "Baro", "latitude": 5.27454, "longitude": 9.2115, "x": 10, "y": 60.1, "source": "geonames", "type": "PPL" }, { "name": "Bayenti", "latitude": 5.3517, "longitude": 9.4101, "x": 49.8, "y": 42.6, "source": "geonames", "type": "PPL" }, { "name": "Bayib Asibong", "latitude": 5.4309, "longitude": 9.3792, "x": 43.6, "y": 24.6, "source": "geonames", "type": "PPL" }, { "name": "Bayib Ossing", "latitude": 5.3967, "longitude": 9.2522, "x": 18.2, "y": 32.4, "source": "geonames", "type": "PPL" }, { "name": "Beko", "latitude": 5.2227, "longitude": 9.4079, "x": 49.4, "y": 71.9, "source": "geonames", "type": "PPL" }, { "name": "Besing Kisen", "latitude": 5.1696, "longitude": 9.3811, "x": 44, "y": 84, "source": "geonames", "type": "PPL" }, { "name": "Betock", "latitude": 5.2408, "longitude": 9.3941, "x": 46.6, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Big Akak II", "latitude": 5.4794, "longitude": 9.3647, "x": 40.7, "y": 13.6, "source": "geonames", "type": "PPL" }, { "name": "Ebanga", "latitude": 5.2062, "longitude": 9.3896, "x": 45.7, "y": 75.7, "source": "geonames", "type": "PPL" }, { "name": "Ediango", "latitude": 5.2574, "longitude": 9.40047, "x": 47.9, "y": 64, "source": "geonames", "type": "PPL" }, { "name": "Ediensoa", "latitude": 5.2759, "longitude": 9.602, "x": 88.3, "y": 59.8, "source": "geonames", "type": "PPL" }, { "name": "Ekengue", "latitude": 5.27923, "longitude": 9.41634, "x": 51.1, "y": 59.1, "source": "geonames", "type": "PPL" }, { "name": "Ekita", "latitude": 5.143, "longitude": 9.3856, "x": 44.9, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ekowantan", "latitude": 5.467, "longitude": 9.2844, "x": 24.6, "y": 16.5, "source": "geonames", "type": "PPL" }, { "name": "Etinkem", "latitude": 5.4147, "longitude": 9.3859, "x": 45, "y": 28.3, "source": "geonames", "type": "PPL" }, { "name": "Eyang", "latitude": 5.41043, "longitude": 9.454, "x": 58.6, "y": 29.3, "source": "geonames", "type": "PPL" }, { "name": "Eyang Atem Ako", "latitude": 5.44021, "longitude": 9.48819, "x": 65.5, "y": 22.5, "source": "geonames", "type": "PPL" }, { "name": "Ifrikwabi", "latitude": 5.2555, "longitude": 9.4483, "x": 57.5, "y": 64.5, "source": "geonames", "type": "PPL" }, { "name": "Konye", "latitude": 5.2032, "longitude": 9.5104, "x": 69.9, "y": 76.3, "source": "geonames", "type": "PPL" }, { "name": "Manyemen", "latitude": 5.21274, "longitude": 9.39896, "x": 47.6, "y": 74.2, "source": "geonames", "type": "PPL" }, { "name": "Mbinda", "latitude": 5.462, "longitude": 9.2918, "x": 26.1, "y": 17.6, "source": "geonames", "type": "PPL" }, { "name": "Mboka", "latitude": 5.298, "longitude": 9.4257, "x": 53, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Mungo Ndor", "latitude": 5.2211, "longitude": 9.51625, "x": 71.1, "y": 72.3, "source": "geonames", "type": "PPL" }, { "name": "Ndebokem", "latitude": 5.4619, "longitude": 9.3781, "x": 43.4, "y": 17.6, "source": "geonames", "type": "PPL" }, { "name": "New Konye", "latitude": 5.156, "longitude": 9.4652, "x": 60.9, "y": 87, "source": "geonames", "type": "PPL" }, { "name": "Nfaitok 1a", "latitude": 5.4592, "longitude": 9.49093, "x": 66, "y": 18.2, "source": "geonames", "type": "PPL" }, { "name": "Nfaitok 1b", "latitude": 5.4161, "longitude": 9.4654, "x": 60.9, "y": 28, "source": "geonames", "type": "PPL" }, { "name": "Njongo", "latitude": 5.4218, "longitude": 9.3856, "x": 44.9, "y": 26.7, "source": "geonames", "type": "PPL" }, { "name": "Nkomeku", "latitude": 5.1867, "longitude": 9.3831, "x": 44.4, "y": 80.1, "source": "geonames", "type": "PPL" }, { "name": "Nsambo", "latitude": 5.2198, "longitude": 9.6104, "x": 90, "y": 72.6, "source": "geonames", "type": "PPL" }, { "name": "Ntale", "latitude": 5.24811, "longitude": 9.57399, "x": 82.7, "y": 66.1, "source": "geonames", "type": "PPL" }, { "name": "Ntenmbang", "latitude": 5.47213, "longitude": 9.53456, "x": 74.8, "y": 15.3, "source": "geonames", "type": "PPL" }, { "name": "Okoroba", "latitude": 5.4563, "longitude": 9.2604, "x": 19.8, "y": 18.9, "source": "geonames", "type": "PPL" }, { "name": "Sekem", "latitude": 5.18562, "longitude": 9.28156, "x": 24.1, "y": 80.3, "source": "geonames", "type": "PPL" }, { "name": "Small Akak I", "latitude": 5.4917, "longitude": 9.3724, "x": 42.3, "y": 10.8, "source": "geonames", "type": "PPL" }, { "name": "Sumbe", "latitude": 5.4835, "longitude": 9.5473, "x": 77.3, "y": 12.7, "source": "geonames", "type": "PPL" }, { "name": "Talangaye", "latitude": 5.14984, "longitude": 9.38247, "x": 44.3, "y": 88.4, "source": "geonames", "type": "PPL" } ], "Njinikom": [ { "name": "Baicham", "latitude": 6.18333, "longitude": 10.23333, "x": 42, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Balso", "latitude": 6.25, "longitude": 10.16667, "x": 10, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Laikom", "latitude": 6.25, "longitude": 10.33333, "x": 90, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Mejang", "latitude": 6.15, "longitude": 10.23333, "x": 42, "y": 90, "source": "geonames", "type": "PPL" } ], "Nkambe": [ { "name": "Abonkwa", "latitude": 6.76667, "longitude": 10.66667, "x": 57, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Ako", "latitude": 6.81667, "longitude": 10.71667, "x": 69.4, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Akoja", "latitude": 6.73333, "longitude": 10.8, "x": 90, "y": 29, "source": "geonames", "type": "PPL" }, { "name": "Akonko", "latitude": 6.7, "longitude": 10.61667, "x": 44.6, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Akwato", "latitude": 6.63333, "longitude": 10.6, "x": 40.5, "y": 51.9, "source": "geonames", "type": "PPL" }, { "name": "Amba", "latitude": 6.78333, "longitude": 10.76667, "x": 81.7, "y": 17.6, "source": "geonames", "type": "PPL" }, { "name": "Andi", "latitude": 6.66667, "longitude": 10.66667, "x": 57, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Assa", "latitude": 6.81667, "longitude": 10.78333, "x": 85.9, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Bebe Ketti", "latitude": 6.73333, "longitude": 10.6, "x": 40.5, "y": 29, "source": "geonames", "type": "PPL" }, { "name": "Benchansi", "latitude": 6.6, "longitude": 10.56667, "x": 32.2, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Berabi", "latitude": 6.75, "longitude": 10.73333, "x": 73.5, "y": 25.2, "source": "geonames", "type": "PPL" }, { "name": "Binka", "latitude": 6.56667, "longitude": 10.75, "x": 77.6, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Binshua", "latitude": 6.6, "longitude": 10.71667, "x": 69.4, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Bissa", "latitude": 6.61667, "longitude": 10.55, "x": 28.1, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Dumbo", "latitude": 6.68333, "longitude": 10.51667, "x": 19.9, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Fonfukka", "latitude": 6.5391, "longitude": 10.47682, "x": 10, "y": 73.4, "source": "geonames", "type": "PPL" }, { "name": "Komine", "latitude": 6.58333, "longitude": 10.56667, "x": 32.2, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Kwe", "latitude": 6.78333, "longitude": 10.5, "x": 15.7, "y": 17.6, "source": "geonames", "type": "PPL" }, { "name": "Lain", "latitude": 6.46667, "longitude": 10.58333, "x": 36.4, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Mbandi", "latitude": 6.7, "longitude": 10.66667, "x": 57, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Mbiribwa", "latitude": 6.7, "longitude": 10.76667, "x": 81.7, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Missaje", "latitude": 6.58303, "longitude": 10.55255, "x": 28.7, "y": 63.4, "source": "geonames", "type": "PPL" }, { "name": "Ndaka", "latitude": 6.81667, "longitude": 10.61667, "x": 44.6, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Nsungli", "latitude": 6.53333, "longitude": 10.73333, "x": 73.5, "y": 74.8, "source": "geonames", "type": "PPL" } ], "Nkoteng": [ { "name": "Baboute", "latitude": 4.5, "longitude": 12.2, "x": 79.6, "y": 53.1, "source": "geonames", "type": "PPL" }, { "name": "Betekele", "latitude": 4.65, "longitude": 12.03333, "x": 44.8, "y": 25.4, "source": "geonames", "type": "PPL" }, { "name": "Bibote", "latitude": 4.4, "longitude": 12.01667, "x": 41.3, "y": 71.5, "source": "geonames", "type": "PPL" }, { "name": "Bikonjok", "latitude": 4.58333, "longitude": 12.06667, "x": 51.7, "y": 37.7, "source": "geonames", "type": "PPL" }, { "name": "Bipogue", "latitude": 4.53333, "longitude": 12.08333, "x": 55.2, "y": 46.9, "source": "geonames", "type": "PPL" }, { "name": "Bissaga", "latitude": 4.68333, "longitude": 12.13333, "x": 65.7, "y": 19.2, "source": "geonames", "type": "PPL" }, { "name": "Biwole", "latitude": 4.35, "longitude": 12.15, "x": 69.1, "y": 80.8, "source": "geonames", "type": "PPL" }, { "name": "Ebometende", "latitude": 4.36667, "longitude": 12.06667, "x": 51.7, "y": 77.7, "source": "geonames", "type": "PPL" }, { "name": "Ekok", "latitude": 4.65, "longitude": 12.01667, "x": 41.3, "y": 25.4, "source": "geonames", "type": "PPL" }, { "name": "Ekombitie", "latitude": 4.56667, "longitude": 12.2, "x": 79.6, "y": 40.8, "source": "geonames", "type": "PPL" }, { "name": "Ekpwoe", "latitude": 4.71667, "longitude": 12.03333, "x": 44.8, "y": 13.1, "source": "geonames", "type": "PPL" }, { "name": "Endek", "latitude": 4.51667, "longitude": 12.23333, "x": 86.5, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Endoum", "latitude": 4.5, "longitude": 12.15, "x": 69.1, "y": 53.1, "source": "geonames", "type": "PPL" }, { "name": "Ewassa", "latitude": 4.33333, "longitude": 12.03333, "x": 44.8, "y": 83.8, "source": "geonames", "type": "PPL" }, { "name": "Kombo", "latitude": 4.46667, "longitude": 12.03333, "x": 44.8, "y": 59.2, "source": "geonames", "type": "PPL" }, { "name": "Latie", "latitude": 4.56667, "longitude": 12.18333, "x": 76.1, "y": 40.8, "source": "geonames", "type": "PPL" }, { "name": "Mbam", "latitude": 4.6, "longitude": 12.13333, "x": 65.7, "y": 34.6, "source": "geonames", "type": "PPL" }, { "name": "Mbandjok", "latitude": 4.45, "longitude": 11.9, "x": 17, "y": 62.3, "source": "geonames", "type": "PPL" }, { "name": "Mbendzi", "latitude": 4.73333, "longitude": 12.01667, "x": 41.3, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Meka", "latitude": 4.6, "longitude": 12.13333, "x": 65.7, "y": 34.6, "source": "geonames", "type": "PPL" }, { "name": "Membine", "latitude": 4.43333, "longitude": 12.23333, "x": 86.5, "y": 65.4, "source": "geonames", "type": "PPL" }, { "name": "Mendjuo", "latitude": 4.55, "longitude": 12.15, "x": 69.1, "y": 43.8, "source": "geonames", "type": "PPL" }, { "name": "Mendom", "latitude": 4.66667, "longitude": 12.13333, "x": 65.7, "y": 22.3, "source": "geonames", "type": "PPL" }, { "name": "Mengonde", "latitude": 4.53333, "longitude": 12.08333, "x": 55.2, "y": 46.9, "source": "geonames", "type": "PPL" }, { "name": "Messassa", "latitude": 4.3, "longitude": 12.01667, "x": 41.3, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Messeng", "latitude": 4.58333, "longitude": 12.2, "x": 79.6, "y": 37.7, "source": "geonames", "type": "PPL" }, { "name": "Metemane", "latitude": 4.65, "longitude": 12.13333, "x": 65.7, "y": 25.4, "source": "geonames", "type": "PPL" }, { "name": "Metore", "latitude": 4.66667, "longitude": 12.08333, "x": 55.2, "y": 22.3, "source": "geonames", "type": "PPL" }, { "name": "Mezass", "latitude": 4.41667, "longitude": 12.11667, "x": 62.2, "y": 68.5, "source": "geonames", "type": "PPL" }, { "name": "Mvane", "latitude": 4.51667, "longitude": 12.13333, "x": 65.7, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Nbananga", "latitude": 4.31667, "longitude": 12.1, "x": 58.7, "y": 86.9, "source": "geonames", "type": "PPL" }, { "name": "Ndameka", "latitude": 4.63333, "longitude": 12.15, "x": 69.1, "y": 28.5, "source": "geonames", "type": "PPL" }, { "name": "Ndo", "latitude": 4.35, "longitude": 11.95, "x": 27.4, "y": 80.8, "source": "geonames", "type": "PPL" }, { "name": "Ngamba", "latitude": 4.43333, "longitude": 12.01667, "x": 41.3, "y": 65.4, "source": "geonames", "type": "PPL" }, { "name": "Ngoulmekong", "latitude": 4.48333, "longitude": 12.03333, "x": 44.8, "y": 56.2, "source": "geonames", "type": "PPL" }, { "name": "Nio", "latitude": 4.43333, "longitude": 11.86667, "x": 10, "y": 65.4, "source": "geonames", "type": "PPL" }, { "name": "Niombo", "latitude": 4.5, "longitude": 12.25, "x": 90, "y": 53.1, "source": "geonames", "type": "PPL" }, { "name": "Nkolessong", "latitude": 4.51667, "longitude": 12.03333, "x": 44.8, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Nkoloboutou", "latitude": 4.5, "longitude": 12, "x": 37.8, "y": 53.1, "source": "geonames", "type": "PPL" }, { "name": "Nkolovan", "latitude": 4.38333, "longitude": 12, "x": 37.8, "y": 74.6, "source": "geonames", "type": "PPL" }, { "name": "Nkoumoutou", "latitude": 4.5, "longitude": 12.08333, "x": 55.2, "y": 53.1, "source": "geonames", "type": "PPL" }, { "name": "Ouassa Baboute", "latitude": 4.5, "longitude": 12.2, "x": 79.6, "y": 53.1, "source": "geonames", "type": "PPL" }, { "name": "Pandzo", "latitude": 4.45, "longitude": 11.91667, "x": 20.4, "y": 62.3, "source": "geonames", "type": "PPL" }, { "name": "Sandza", "latitude": 4.7, "longitude": 12.01667, "x": 41.3, "y": 16.2, "source": "geonames", "type": "PPL" }, { "name": "Simbane", "latitude": 4.35, "longitude": 12.1, "x": 58.7, "y": 80.8, "source": "geonames", "type": "PPL" }, { "name": "Tamai", "latitude": 4.6, "longitude": 12.05, "x": 48.3, "y": 34.6, "source": "geonames", "type": "PPL" }, { "name": "Tame", "latitude": 4.66667, "longitude": 12.03333, "x": 44.8, "y": 22.3, "source": "geonames", "type": "PPL" }, { "name": "Tapare", "latitude": 4.33333, "longitude": 12.11667, "x": 62.2, "y": 83.8, "source": "geonames", "type": "PPL" }, { "name": "Yanga", "latitude": 4.66667, "longitude": 12.11667, "x": 62.2, "y": 22.3, "source": "geonames", "type": "PPL" }, { "name": "Zili", "latitude": 4.48333, "longitude": 11.98333, "x": 34.3, "y": 56.2, "source": "geonames", "type": "PPL" } ], "Nkongsamba": [ { "name": "Badjoki", "latitude": 4.8365, "longitude": 9.9524, "x": 54.4, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Bakem", "latitude": 4.96667, "longitude": 10.05, "x": 75.5, "y": 43.1, "source": "geonames", "type": "PPL" }, { "name": "Bare", "latitude": 5.0068, "longitude": 9.9653, "x": 57.2, "y": 28.6, "source": "geonames", "type": "PPL" }, { "name": "Bareko", "latitude": 5.0167, "longitude": 9.9649, "x": 57.1, "y": 25, "source": "geonames", "type": "PPL" }, { "name": "Bareok", "latitude": 4.9849, "longitude": 9.9727, "x": 58.8, "y": 36.5, "source": "geonames", "type": "PPL" }, { "name": "Ebloukon", "latitude": 4.95, "longitude": 10.05, "x": 75.5, "y": 49.1, "source": "geonames", "type": "PPL" }, { "name": "Ekanbeng", "latitude": 4.9178, "longitude": 9.9888, "x": 62.3, "y": 60.7, "source": "geonames", "type": "PPL" }, { "name": "Ekangte", "latitude": 4.9918, "longitude": 9.9274, "x": 49, "y": 34, "source": "geonames", "type": "PPL" }, { "name": "Ekohok", "latitude": 4.9124, "longitude": 9.8821, "x": 39.1, "y": 62.6, "source": "geonames", "type": "PPL" }, { "name": "Epinibele", "latitude": 5.0345, "longitude": 9.7587, "x": 12.4, "y": 18.6, "source": "geonames", "type": "PPL" }, { "name": "Essel", "latitude": 4.9709, "longitude": 9.9766, "x": 59.6, "y": 41.5, "source": "geonames", "type": "PPL" }, { "name": "Makou", "latitude": 4.95, "longitude": 10.11667, "x": 90, "y": 49.1, "source": "geonames", "type": "PPL" }, { "name": "Maku", "latitude": 5.0584, "longitude": 9.7477, "x": 10, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Mandjibo", "latitude": 5.01147, "longitude": 10.03724, "x": 72.8, "y": 26.9, "source": "geonames", "type": "PPL" }, { "name": "Manengouba", "latitude": 4.9522, "longitude": 9.8678, "x": 36, "y": 48.3, "source": "geonames", "type": "PPL" }, { "name": "Mbarembeng", "latitude": 5.008, "longitude": 10.02633, "x": 70.4, "y": 28.2, "source": "geonames", "type": "PPL" }, { "name": "Mbonko", "latitude": 4.9751, "longitude": 9.9048, "x": 44.1, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Mpaka", "latitude": 5.01667, "longitude": 10.00943, "x": 66.7, "y": 25, "source": "geonames", "type": "PPL" }, { "name": "Mueba", "latitude": 5.0303, "longitude": 9.78427, "x": 17.9, "y": 20.1, "source": "geonames", "type": "PPL" }, { "name": "Ndombeng", "latitude": 4.9328, "longitude": 9.872, "x": 37, "y": 55.3, "source": "geonames", "type": "PPL" }, { "name": "Ndoungue", "latitude": 4.918, "longitude": 9.8962, "x": 42.2, "y": 60.6, "source": "geonames", "type": "PPL" }, { "name": "Ngwa", "latitude": 4.9147, "longitude": 9.922, "x": 47.8, "y": 61.8, "source": "geonames", "type": "PPL" }, { "name": "Ninong", "latitude": 5.0386, "longitude": 9.7749, "x": 15.9, "y": 17.1, "source": "geonames", "type": "PPL" }, { "name": "Nlongo", "latitude": 4.95, "longitude": 9.93333, "x": 50.2, "y": 49.1, "source": "geonames", "type": "PPL" }, { "name": "Nsong", "latitude": 4.9874, "longitude": 9.8144, "x": 24.5, "y": 35.6, "source": "geonames", "type": "PPL" }, { "name": "Ntak", "latitude": 5.0361, "longitude": 9.7832, "x": 17.7, "y": 18, "source": "geonames", "type": "PPL" }, { "name": "Poala", "latitude": 5.05029, "longitude": 9.81577, "x": 24.8, "y": 12.9, "source": "geonames", "type": "PPL" } ], "Ntui": [ { "name": "Betamba", "latitude": 4.45, "longitude": 11.56667, "x": 30, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Biakoa", "latitude": 4.55, "longitude": 11.48333, "x": 10, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Biatsota I", "latitude": 4.41667, "longitude": 11.63333, "x": 46, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Biatsota II", "latitude": 4.43333, "longitude": 11.63333, "x": 46, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Bilanga Kombe", "latitude": 4.51667, "longitude": 11.63333, "x": 46, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Bindalima", "latitude": 4.5, "longitude": 11.63333, "x": 46, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Bindalima I", "latitude": 4.45, "longitude": 11.63333, "x": 46, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Bindamongo", "latitude": 4.51667, "longitude": 11.5, "x": 14, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Bindanyenge", "latitude": 4.41667, "longitude": 11.63333, "x": 46, "y": 63.3, "source": "geonames", "type": "PPL" }, { "name": "Bivouna", "latitude": 4.53333, "longitude": 11.63333, "x": 46, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Dokoa", "latitude": 4.36667, "longitude": 11.73333, "x": 70, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Ehondo", "latitude": 4.38333, "longitude": 11.63333, "x": 46, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Elig-Mfomo", "latitude": 4.6298, "longitude": 11.7068, "x": 63.6, "y": 14.6, "source": "geonames", "type": "PPL" }, { "name": "Emana", "latitude": 4.3, "longitude": 11.63333, "x": 46, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Kbasala", "latitude": 4.36667, "longitude": 11.7, "x": 62, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Kela", "latitude": 4.46667, "longitude": 11.53333, "x": 22, "y": 51.9, "source": "geonames", "type": "PPL" }, { "name": "Koundoung", "latitude": 4.48333, "longitude": 11.63333, "x": 46, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Kouse", "latitude": 4.45, "longitude": 11.56667, "x": 30, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Mebasa", "latitude": 4.31667, "longitude": 11.63333, "x": 46, "y": 86.2, "source": "geonames", "type": "PPL" }, { "name": "Minkouma", "latitude": 4.38333, "longitude": 11.78333, "x": 82, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Nachtigal", "latitude": 4.35, "longitude": 11.63333, "x": 46, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Ndimi", "latitude": 4.63333, "longitude": 11.66667, "x": 54, "y": 13.8, "source": "geonames", "type": "PPL" }, { "name": "Ndjame", "latitude": 4.38333, "longitude": 11.63333, "x": 46, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Ndjore", "latitude": 4.4, "longitude": 11.81667, "x": 90, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Ndzi", "latitude": 4.36667, "longitude": 11.71667, "x": 66, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Ngaounde", "latitude": 4.46667, "longitude": 11.81667, "x": 90, "y": 51.9, "source": "geonames", "type": "PPL" }, { "name": "Ngete", "latitude": 4.43333, "longitude": 11.6, "x": 38, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Nkoayos", "latitude": 4.31667, "longitude": 11.76667, "x": 78, "y": 86.2, "source": "geonames", "type": "PPL" }, { "name": "Nkolevodo", "latitude": 4.31667, "longitude": 11.6, "x": 38, "y": 86.2, "source": "geonames", "type": "PPL" }, { "name": "Odon", "latitude": 4.45, "longitude": 11.61667, "x": 42, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Olembe", "latitude": 4.3, "longitude": 11.65, "x": 50, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ondondo I", "latitude": 4.35, "longitude": 11.58333, "x": 34, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Ondondo II", "latitude": 4.31667, "longitude": 11.6, "x": 38, "y": 86.2, "source": "geonames", "type": "PPL" }, { "name": "Osombe", "latitude": 4.6, "longitude": 11.65, "x": 50, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Otibili", "latitude": 4.33333, "longitude": 11.63333, "x": 46, "y": 82.4, "source": "geonames", "type": "PPL" }, { "name": "Salakounou", "latitude": 4.56667, "longitude": 11.63333, "x": 46, "y": 29, "source": "geonames", "type": "PPL" }, { "name": "Yalongo", "latitude": 4.65, "longitude": 11.68333, "x": 58, "y": 10, "source": "geonames", "type": "PPL" } ], "Obala": [ { "name": "Abondo", "latitude": 4.06667, "longitude": 11.61667, "x": 59.5, "y": 75, "source": "geonames", "type": "PPL" }, { "name": "Abono", "latitude": 4.11667, "longitude": 11.43333, "x": 17.6, "y": 60, "source": "geonames", "type": "PPL" }, { "name": "Afamve", "latitude": 4.16667, "longitude": 11.66667, "x": 71, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Afanesele", "latitude": 4.18333, "longitude": 11.6, "x": 55.7, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Afanetabini", "latitude": 4.16667, "longitude": 11.7, "x": 78.6, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Afanetouana", "latitude": 4.08333, "longitude": 11.73333, "x": 86.2, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Akondok", "latitude": 4.2, "longitude": 11.73333, "x": 86.2, "y": 35, "source": "geonames", "type": "PPL" }, { "name": "Batchenga", "latitude": 4.28333, "longitude": 11.65, "x": 67.1, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Ebalondok", "latitude": 4.16667, "longitude": 11.71667, "x": 82.4, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Ebogo II", "latitude": 4.05, "longitude": 11.61667, "x": 59.5, "y": 80, "source": "geonames", "type": "PPL" }, { "name": "Edzendouan", "latitude": 4.2, "longitude": 11.75, "x": 90, "y": 35, "source": "geonames", "type": "PPL" }, { "name": "Efok", "latitude": 4.18333, "longitude": 11.46667, "x": 25.2, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Ekoumdouma", "latitude": 4.11667, "longitude": 11.53333, "x": 40.5, "y": 60, "source": "geonames", "type": "PPL" }, { "name": "Elig Ngomo", "latitude": 4.15, "longitude": 11.45, "x": 21.4, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Elomzok", "latitude": 4.21667, "longitude": 11.6, "x": 55.7, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Elon", "latitude": 4.25, "longitude": 11.61667, "x": 59.5, "y": 20, "source": "geonames", "type": "PPL" }, { "name": "Emana", "latitude": 4.26667, "longitude": 11.61667, "x": 59.5, "y": 15, "source": "geonames", "type": "PPL" }, { "name": "Esimel", "latitude": 4.03333, "longitude": 11.63333, "x": 63.3, "y": 85, "source": "geonames", "type": "PPL" }, { "name": "Esong", "latitude": 4.16667, "longitude": 11.45, "x": 21.4, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Famenasi", "latitude": 4.26667, "longitude": 11.61667, "x": 59.5, "y": 15, "source": "geonames", "type": "PPL" }, { "name": "Fegmimbang", "latitude": 4.01667, "longitude": 11.65, "x": 67.1, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Foulasi", "latitude": 4.13333, "longitude": 11.53333, "x": 40.5, "y": 55, "source": "geonames", "type": "PPL" }, { "name": "Indama", "latitude": 4.16667, "longitude": 11.4, "x": 10, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Indinding", "latitude": 4.11667, "longitude": 11.48333, "x": 29, "y": 60, "source": "geonames", "type": "PPL" }, { "name": "Koudanding", "latitude": 4.08333, "longitude": 11.48333, "x": 29, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Koulou", "latitude": 4.08333, "longitude": 11.61667, "x": 59.5, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Lepopomo", "latitude": 4.26667, "longitude": 11.53333, "x": 40.5, "y": 15, "source": "geonames", "type": "PPL" }, { "name": "Lingom", "latitude": 4.1, "longitude": 11.48333, "x": 29, "y": 65, "source": "geonames", "type": "PPL" }, { "name": "Loua I", "latitude": 4.18333, "longitude": 11.48333, "x": 29, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Loua II", "latitude": 4.2, "longitude": 11.48333, "x": 29, "y": 35, "source": "geonames", "type": "PPL" }, { "name": "Mban", "latitude": 4.26667, "longitude": 11.53333, "x": 40.5, "y": 15, "source": "geonames", "type": "PPL" }, { "name": "Mbele", "latitude": 4.2, "longitude": 11.51667, "x": 36.7, "y": 35, "source": "geonames", "type": "PPL" }, { "name": "Mebomenyie", "latitude": 4.05, "longitude": 11.7, "x": 78.6, "y": 80, "source": "geonames", "type": "PPL" }, { "name": "Medoumbou", "latitude": 4.03333, "longitude": 11.66667, "x": 71, "y": 85, "source": "geonames", "type": "PPL" }, { "name": "Meyos", "latitude": 4.01667, "longitude": 11.56667, "x": 48.1, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Mfomakak", "latitude": 4.03333, "longitude": 11.56667, "x": 48.1, "y": 85, "source": "geonames", "type": "PPL" }, { "name": "Mingama", "latitude": 4.25, "longitude": 11.53333, "x": 40.5, "y": 20, "source": "geonames", "type": "PPL" }, { "name": "Minkama", "latitude": 4.18333, "longitude": 11.58333, "x": 51.9, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Mintsang", "latitude": 4.08333, "longitude": 11.6, "x": 55.7, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Mvomdoumba", "latitude": 4.23333, "longitude": 11.7, "x": 78.6, "y": 25, "source": "geonames", "type": "PPL" }, { "name": "Nalasi", "latitude": 4.28333, "longitude": 11.63333, "x": 63.3, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Ndzana", "latitude": 4.15, "longitude": 11.65, "x": 67.1, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Ngali I", "latitude": 4.01667, "longitude": 11.58333, "x": 51.9, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ngali II", "latitude": 4.05, "longitude": 11.6, "x": 55.7, "y": 80, "source": "geonames", "type": "PPL" }, { "name": "Ngomo", "latitude": 4.23333, "longitude": 11.41667, "x": 13.8, "y": 25, "source": "geonames", "type": "PPL" }, { "name": "Ngongo", "latitude": 4.21667, "longitude": 11.48333, "x": 29, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Ngoungoumou", "latitude": 4.05, "longitude": 11.63333, "x": 63.3, "y": 80, "source": "geonames", "type": "PPL" }, { "name": "Nkolafamba", "latitude": 4.01667, "longitude": 11.63333, "x": 63.3, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Nkoledouma", "latitude": 4.16667, "longitude": 11.53333, "x": 40.5, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Nkolfep", "latitude": 4.08333, "longitude": 11.48333, "x": 29, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Nkolfoulou", "latitude": 4.13333, "longitude": 11.6, "x": 55.7, "y": 55, "source": "geonames", "type": "PPL" }, { "name": "Nkolmbene", "latitude": 4.11667, "longitude": 11.53333, "x": 40.5, "y": 60, "source": "geonames", "type": "PPL" }, { "name": "Nkolmekok", "latitude": 4.23333, "longitude": 11.61667, "x": 59.5, "y": 25, "source": "geonames", "type": "PPL" }, { "name": "Nkolmelen", "latitude": 4.13333, "longitude": 11.53333, "x": 40.5, "y": 55, "source": "geonames", "type": "PPL" }, { "name": "Nkolmelok", "latitude": 4.25, "longitude": 11.46667, "x": 25.2, "y": 20, "source": "geonames", "type": "PPL" }, { "name": "Nkolngem", "latitude": 4.1, "longitude": 11.53333, "x": 40.5, "y": 65, "source": "geonames", "type": "PPL" }, { "name": "Nkolondogo", "latitude": 4.23333, "longitude": 11.53333, "x": 40.5, "y": 25, "source": "geonames", "type": "PPL" }, { "name": "Nkoltomo II", "latitude": 4.21667, "longitude": 11.43333, "x": 17.6, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Nkometou I", "latitude": 4.05, "longitude": 11.55, "x": 44.3, "y": 80, "source": "geonames", "type": "PPL" }, { "name": "Nkometou II", "latitude": 4.08333, "longitude": 11.55, "x": 44.3, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Nsan", "latitude": 4.15, "longitude": 11.46667, "x": 25.2, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Nto", "latitude": 4.08333, "longitude": 11.51667, "x": 36.7, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Ntouesong", "latitude": 4.08333, "longitude": 11.71667, "x": 82.4, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Ntsaekang", "latitude": 4.23333, "longitude": 11.5, "x": 32.9, "y": 25, "source": "geonames", "type": "PPL" }, { "name": "Oboa", "latitude": 4.11667, "longitude": 11.63333, "x": 63.3, "y": 60, "source": "geonames", "type": "PPL" }, { "name": "Tsek", "latitude": 4.18333, "longitude": 11.45, "x": 21.4, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Yemekout", "latitude": 4.2, "longitude": 11.45, "x": 21.4, "y": 35, "source": "geonames", "type": "PPL" }, { "name": "Yemesoa", "latitude": 4.2, "longitude": 11.4, "x": 10, "y": 35, "source": "geonames", "type": "PPL" }, { "name": "Zoatoupsi", "latitude": 4.15, "longitude": 11.46667, "x": 25.2, "y": 50, "source": "geonames", "type": "PPL" } ], "Okola": [ { "name": "Ebod", "latitude": 3.96667, "longitude": 11.46667, "x": 76.7, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Ekabita", "latitude": 4.05, "longitude": 11.46667, "x": 76.7, "y": 24.5, "source": "geonames", "type": "PPL" }, { "name": "Eligyen", "latitude": 3.98333, "longitude": 11.35, "x": 30, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Ezezang", "latitude": 4.03333, "longitude": 11.48333, "x": 83.3, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Lebot", "latitude": 3.98333, "longitude": 11.41667, "x": 56.7, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Lindom I", "latitude": 4.03333, "longitude": 11.35, "x": 30, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Lindom II", "latitude": 4.06667, "longitude": 11.38333, "x": 43.3, "y": 17.3, "source": "geonames", "type": "PPL" }, { "name": "Lingon", "latitude": 4, "longitude": 11.35, "x": 30, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Louma", "latitude": 4.06667, "longitude": 11.4, "x": 50, "y": 17.3, "source": "geonames", "type": "PPL" }, { "name": "Metak", "latitude": 3.93333, "longitude": 11.41667, "x": 56.7, "y": 75.5, "source": "geonames", "type": "PPL" }, { "name": "Minkoa", "latitude": 3.95, "longitude": 11.3, "x": 10, "y": 68.2, "source": "geonames", "type": "PPL" }, { "name": "Mvoua", "latitude": 4.06667, "longitude": 11.43333, "x": 63.3, "y": 17.3, "source": "geonames", "type": "PPL" }, { "name": "Ngoas", "latitude": 3.91667, "longitude": 11.33333, "x": 23.3, "y": 82.7, "source": "geonames", "type": "PPL" }, { "name": "Ngong", "latitude": 4.08333, "longitude": 11.38333, "x": 43.3, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Ngoya I", "latitude": 3.95, "longitude": 11.45, "x": 70, "y": 68.2, "source": "geonames", "type": "PPL" }, { "name": "Nkolakie", "latitude": 3.98333, "longitude": 11.38333, "x": 43.3, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Nkoldjobe", "latitude": 3.93333, "longitude": 11.35, "x": 30, "y": 75.5, "source": "geonames", "type": "PPL" }, { "name": "Nkolfep", "latitude": 3.98333, "longitude": 11.4, "x": 50, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Nkolmekouma", "latitude": 4, "longitude": 11.4, "x": 50, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Nkolmeyang", "latitude": 3.91667, "longitude": 11.31667, "x": 16.7, "y": 82.7, "source": "geonames", "type": "PPL" }, { "name": "Nkolndobo", "latitude": 4.08333, "longitude": 11.45, "x": 70, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Nkolnyada", "latitude": 3.98333, "longitude": 11.45, "x": 70, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Nkolyem", "latitude": 3.96667, "longitude": 11.3, "x": 10, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Nkong", "latitude": 3.96667, "longitude": 11.45, "x": 70, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Nkongzok", "latitude": 4.05, "longitude": 11.35, "x": 30, "y": 24.5, "source": "geonames", "type": "PPL" }, { "name": "Nouma", "latitude": 3.96667, "longitude": 11.43333, "x": 63.3, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Ntem", "latitude": 4.01667, "longitude": 11.31667, "x": 16.7, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Ntouiesong", "latitude": 4.01667, "longitude": 11.35, "x": 30, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Ntsama", "latitude": 4.03333, "longitude": 11.43333, "x": 63.3, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Obak", "latitude": 3.98333, "longitude": 11.46667, "x": 76.7, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Oban", "latitude": 4.05, "longitude": 11.41667, "x": 56.7, "y": 24.5, "source": "geonames", "type": "PPL" }, { "name": "Okoukouda", "latitude": 4.01667, "longitude": 11.45, "x": 70, "y": 39.1, "source": "geonames", "type": "PPL" }, { "name": "Ovang I", "latitude": 3.93333, "longitude": 11.3, "x": 10, "y": 75.5, "source": "geonames", "type": "PPL" }, { "name": "Oyama", "latitude": 4.06667, "longitude": 11.41667, "x": 56.7, "y": 17.3, "source": "geonames", "type": "PPL" }, { "name": "Ozom III", "latitude": 3.9, "longitude": 11.4, "x": 50, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Yegeasi", "latitude": 3.98333, "longitude": 11.48333, "x": 83.3, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Yemesoum", "latitude": 4.05, "longitude": 11.5, "x": 90, "y": 24.5, "source": "geonames", "type": "PPL" } ], "Penja": [ { "name": "Bonandam", "latitude": 4.5971, "longitude": 9.6783, "x": 45.6, "y": 66.9, "source": "geonames", "type": "PPL" }, { "name": "Boneko", "latitude": 4.6502, "longitude": 9.7409, "x": 67.5, "y": 33.9, "source": "geonames", "type": "PPL" }, { "name": "Boubou", "latitude": 4.6132, "longitude": 9.6002, "x": 18.2, "y": 56.9, "source": "geonames", "type": "PPL" }, { "name": "Boubou I", "latitude": 4.6406, "longitude": 9.6461, "x": 34.3, "y": 39.9, "source": "geonames", "type": "PPL" }, { "name": "Boubou II", "latitude": 4.6228, "longitude": 9.614, "x": 23, "y": 50.9, "source": "geonames", "type": "PPL" }, { "name": "Buba", "latitude": 4.6343, "longitude": 9.6122, "x": 22.4, "y": 43.8, "source": "geonames", "type": "PPL" }, { "name": "Buba I", "latitude": 4.6332, "longitude": 9.6321, "x": 29.4, "y": 44.5, "source": "geonames", "type": "PPL" }, { "name": "Buba II", "latitude": 4.6546, "longitude": 9.6355, "x": 30.6, "y": 31.2, "source": "geonames", "type": "PPL" }, { "name": "Buba Waterfall", "latitude": 4.6293, "longitude": 9.5768, "x": 10, "y": 46.9, "source": "geonames", "type": "PPL" }, { "name": "Djoungo", "latitude": 4.576, "longitude": 9.6178, "x": 24.4, "y": 80, "source": "geonames", "type": "PPL" }, { "name": "Edibinjok", "latitude": 4.6888, "longitude": 9.6412, "x": 32.6, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Malinkam", "latitude": 4.56667, "longitude": 9.66667, "x": 41.5, "y": 85.7, "source": "geonames", "type": "PPL" }, { "name": "Mbome Ngwandang", "latitude": 4.5598, "longitude": 9.6481, "x": 35, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Njombe", "latitude": 4.58056, "longitude": 9.66472, "x": 40.8, "y": 77.1, "source": "geonames", "type": "PPL" }, { "name": "Nyanga", "latitude": 4.6051, "longitude": 9.7682, "x": 77.1, "y": 61.9, "source": "geonames", "type": "PPL" }, { "name": "Sole", "latitude": 4.6075, "longitude": 9.8051, "x": 90, "y": 60.4, "source": "geonames", "type": "PPL" } ], "Pitoa": [ { "name": "Aoao", "latitude": 9.55891, "longitude": 13.41448, "x": 23.2, "y": 19.1, "source": "geonames", "type": "PPL" }, { "name": "Babandji", "latitude": 9.45535, "longitude": 13.45044, "x": 31, "y": 42.6, "source": "geonames", "type": "PPL" }, { "name": "Bacita", "latitude": 9.53333, "longitude": 13.61667, "x": 67.1, "y": 24.9, "source": "geonames", "type": "PPL" }, { "name": "Badi", "latitude": 9.53333, "longitude": 13.56667, "x": 56.3, "y": 24.9, "source": "geonames", "type": "PPL" }, { "name": "Badjengo", "latitude": 9.4937, "longitude": 13.68354, "x": 81.6, "y": 33.9, "source": "geonames", "type": "PPL" }, { "name": "Badjouma", "latitude": 9.4613, "longitude": 13.63553, "x": 71.2, "y": 41.2, "source": "geonames", "type": "PPL" }, { "name": "Ban", "latitude": 9.51667, "longitude": 13.63333, "x": 70.7, "y": 28.7, "source": "geonames", "type": "PPL" }, { "name": "Banay", "latitude": 9.45857, "longitude": 13.51086, "x": 44.1, "y": 41.8, "source": "geonames", "type": "PPL" }, { "name": "Bango", "latitude": 9.51667, "longitude": 13.43333, "x": 27.3, "y": 28.7, "source": "geonames", "type": "PPL" }, { "name": "Bassinta", "latitude": 9.53337, "longitude": 13.61902, "x": 67.6, "y": 24.9, "source": "geonames", "type": "PPL" }, { "name": "Beri", "latitude": 9.39851, "longitude": 13.47918, "x": 37.3, "y": 55.5, "source": "geonames", "type": "PPL" }, { "name": "Boulgou", "latitude": 9.44094, "longitude": 13.46943, "x": 35.2, "y": 45.8, "source": "geonames", "type": "PPL" }, { "name": "Bounga", "latitude": 9.28009, "longitude": 13.66077, "x": 76.7, "y": 82.3, "source": "geonames", "type": "PPL" }, { "name": "Bouti", "latitude": 9.33022, "longitude": 13.57676, "x": 58.5, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Dakel", "latitude": 9.31651, "longitude": 13.64496, "x": 73.3, "y": 74.1, "source": "geonames", "type": "PPL" }, { "name": "Daldal", "latitude": 9.36053, "longitude": 13.54281, "x": 51.1, "y": 64.1, "source": "geonames", "type": "PPL" }, { "name": "Delem", "latitude": 9.56086, "longitude": 13.54213, "x": 50.9, "y": 18.7, "source": "geonames", "type": "PPL" }, { "name": "Dengui", "latitude": 9.27956, "longitude": 13.53356, "x": 49.1, "y": 82.4, "source": "geonames", "type": "PPL" }, { "name": "Djallou", "latitude": 9.59796, "longitude": 13.50202, "x": 42.2, "y": 10.2, "source": "geonames", "type": "PPL" }, { "name": "Djamboutou Djougouldou", "latitude": 9.59902, "longitude": 13.46138, "x": 33.4, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Djimeta", "latitude": 9.42795, "longitude": 13.56457, "x": 55.8, "y": 48.8, "source": "geonames", "type": "PPL" }, { "name": "Djimli", "latitude": 9.27873, "longitude": 13.64725, "x": 73.8, "y": 82.6, "source": "geonames", "type": "PPL" }, { "name": "Dola", "latitude": 9.43481, "longitude": 13.58201, "x": 59.6, "y": 47.2, "source": "geonames", "type": "PPL" }, { "name": "Dolere", "latitude": 9.40691, "longitude": 13.51477, "x": 45, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Forti", "latitude": 9.38692, "longitude": 13.64613, "x": 73.5, "y": 58.1, "source": "geonames", "type": "PPL" }, { "name": "Foui", "latitude": 9.41055, "longitude": 13.66255, "x": 77.1, "y": 52.7, "source": "geonames", "type": "PPL" }, { "name": "Gebake-Haoussare", "latitude": 9.36915, "longitude": 13.54556, "x": 51.7, "y": 62.1, "source": "geonames", "type": "PPL" }, { "name": "Gotzer", "latitude": 9.55, "longitude": 13.53333, "x": 49, "y": 21.1, "source": "geonames", "type": "PPL" }, { "name": "Gounougou", "latitude": 9.4249, "longitude": 13.38668, "x": 17.2, "y": 49.5, "source": "geonames", "type": "PPL" }, { "name": "Houla", "latitude": 9.28901, "longitude": 13.70771, "x": 86.9, "y": 80.3, "source": "geonames", "type": "PPL" }, { "name": "Jeunes Pionniers", "latitude": 9.33434, "longitude": 13.6007, "x": 63.7, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Kango", "latitude": 9.49008, "longitude": 13.55548, "x": 53.8, "y": 34.7, "source": "geonames", "type": "PPL" }, { "name": "Kefero", "latitude": 9.53884, "longitude": 13.50881, "x": 43.7, "y": 23.6, "source": "geonames", "type": "PPL" }, { "name": "Kismatari", "latitude": 9.32655, "longitude": 13.49007, "x": 39.6, "y": 71.8, "source": "geonames", "type": "PPL" }, { "name": "Koyanga", "latitude": 9.2668, "longitude": 13.54632, "x": 51.8, "y": 85.3, "source": "geonames", "type": "PPL" }, { "name": "Lainguel", "latitude": 9.26133, "longitude": 13.61084, "x": 65.9, "y": 86.6, "source": "geonames", "type": "PPL" }, { "name": "Langui-Be", "latitude": 9.32533, "longitude": 13.6333, "x": 70.7, "y": 72.1, "source": "geonames", "type": "PPL" }, { "name": "Lesse", "latitude": 9.35873, "longitude": 13.53282, "x": 48.9, "y": 64.5, "source": "geonames", "type": "PPL" }, { "name": "Louguereo", "latitude": 9.5013, "longitude": 13.3749, "x": 14.6, "y": 32.2, "source": "geonames", "type": "PPL" }, { "name": "Mba Aimi", "latitude": 9.39389, "longitude": 13.71054, "x": 87.5, "y": 56.5, "source": "geonames", "type": "PPL" }, { "name": "Mboloum", "latitude": 9.33178, "longitude": 13.722, "x": 90, "y": 70.6, "source": "geonames", "type": "PPL" }, { "name": "Mbor", "latitude": 9.52054, "longitude": 13.67996, "x": 80.9, "y": 27.8, "source": "geonames", "type": "PPL" }, { "name": "Mbotilgou", "latitude": 9.56667, "longitude": 13.43333, "x": 27.3, "y": 17.3, "source": "geonames", "type": "PPL" }, { "name": "Mboura", "latitude": 9.53578, "longitude": 13.54004, "x": 50.5, "y": 24.3, "source": "geonames", "type": "PPL" }, { "name": "Mima", "latitude": 9.56667, "longitude": 13.46667, "x": 34.6, "y": 17.3, "source": "geonames", "type": "PPL" }, { "name": "Nadoura", "latitude": 9.24623, "longitude": 13.61102, "x": 65.9, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Nassarao Yermi", "latitude": 9.36217, "longitude": 13.44088, "x": 28.9, "y": 63.7, "source": "geonames", "type": "PPL" }, { "name": "Nboutoum", "latitude": 9.58333, "longitude": 13.45, "x": 30.9, "y": 13.6, "source": "geonames", "type": "PPL" }, { "name": "Ndengui", "latitude": 9.29254, "longitude": 13.59342, "x": 62.1, "y": 79.5, "source": "geonames", "type": "PPL" }, { "name": "Ndoudja", "latitude": 9.4703, "longitude": 13.47668, "x": 36.7, "y": 39.2, "source": "geonames", "type": "PPL" }, { "name": "Ngargou", "latitude": 9.28085, "longitude": 13.62469, "x": 68.9, "y": 82.1, "source": "geonames", "type": "PPL" }, { "name": "Ngourore", "latitude": 9.28677, "longitude": 13.6176, "x": 67.3, "y": 80.8, "source": "geonames", "type": "PPL" }, { "name": "Ngoutchoumi", "latitude": 9.57092, "longitude": 13.42576, "x": 25.7, "y": 16.4, "source": "geonames", "type": "PPL" }, { "name": "Niakira", "latitude": 9.38159, "longitude": 13.45016, "x": 31, "y": 59.3, "source": "geonames", "type": "PPL" }, { "name": "Niamsala", "latitude": 9.31229, "longitude": 13.68401, "x": 81.7, "y": 75, "source": "geonames", "type": "PPL" }, { "name": "Nibango", "latitude": 9.30504, "longitude": 13.51426, "x": 44.9, "y": 76.7, "source": "geonames", "type": "PPL" }, { "name": "Onia", "latitude": 9.56787, "longitude": 13.6013, "x": 63.8, "y": 17.1, "source": "geonames", "type": "PPL" }, { "name": "Ouro Aladji", "latitude": 9.4588, "longitude": 13.38688, "x": 17.2, "y": 41.8, "source": "geonames", "type": "PPL" }, { "name": "Ouro Badjouma", "latitude": 9.39804, "longitude": 13.65406, "x": 75.2, "y": 55.6, "source": "geonames", "type": "PPL" }, { "name": "Ouro Boboy", "latitude": 9.47424, "longitude": 13.36973, "x": 13.5, "y": 38.3, "source": "geonames", "type": "PPL" }, { "name": "Ouro Boki", "latitude": 9.41035, "longitude": 13.6569, "x": 75.9, "y": 52.8, "source": "geonames", "type": "PPL" }, { "name": "Ouro Bouhari", "latitude": 9.46897, "longitude": 13.39281, "x": 18.5, "y": 39.5, "source": "geonames", "type": "PPL" }, { "name": "Ouro Djidji", "latitude": 9.46727, "longitude": 13.38034, "x": 15.8, "y": 39.9, "source": "geonames", "type": "PPL" }, { "name": "Ouro Dole", "latitude": 9.4877, "longitude": 13.38177, "x": 16.1, "y": 35.2, "source": "geonames", "type": "PPL" }, { "name": "Ouro Haoussas", "latitude": 9.36667, "longitude": 13.56667, "x": 56.3, "y": 62.7, "source": "geonames", "type": "PPL" }, { "name": "Ouro Hassana", "latitude": 9.47813, "longitude": 13.39731, "x": 19.5, "y": 37.4, "source": "geonames", "type": "PPL" }, { "name": "Ouro Kessoum", "latitude": 9.45561, "longitude": 13.37735, "x": 15.2, "y": 42.5, "source": "geonames", "type": "PPL" }, { "name": "Ouro Malloum", "latitude": 9.30994, "longitude": 13.69594, "x": 84.3, "y": 75.6, "source": "geonames", "type": "PPL" }, { "name": "Pamsi", "latitude": 9.57103, "longitude": 13.41641, "x": 23.6, "y": 16.3, "source": "geonames", "type": "PPL" }, { "name": "Panse", "latitude": 9.58333, "longitude": 13.43333, "x": 27.3, "y": 13.6, "source": "geonames", "type": "PPL" }, { "name": "Pardjije", "latitude": 9.53756, "longitude": 13.6577, "x": 76, "y": 23.9, "source": "geonames", "type": "PPL" }, { "name": "Parma", "latitude": 9.32119, "longitude": 13.50409, "x": 42.7, "y": 73, "source": "geonames", "type": "PPL" }, { "name": "Patouga", "latitude": 9.27182, "longitude": 13.62272, "x": 68.4, "y": 84.2, "source": "geonames", "type": "PPL" }, { "name": "Peske", "latitude": 9.45839, "longitude": 13.69423, "x": 84, "y": 41.9, "source": "geonames", "type": "PPL" }, { "name": "Pitoa-Douli", "latitude": 9.27306, "longitude": 13.53361, "x": 49.1, "y": 83.9, "source": "geonames", "type": "PPL" }, { "name": "Pourri", "latitude": 9.58333, "longitude": 13.5, "x": 41.8, "y": 13.6, "source": "geonames", "type": "PPL" }, { "name": "Poussane", "latitude": 9.56365, "longitude": 13.51388, "x": 44.8, "y": 18, "source": "geonames", "type": "PPL" }, { "name": "Ram", "latitude": 9.50447, "longitude": 13.49626, "x": 41, "y": 31.4, "source": "geonames", "type": "PPL" }, { "name": "Rayo", "latitude": 9.59709, "longitude": 13.44156, "x": 29.1, "y": 10.4, "source": "geonames", "type": "PPL" }, { "name": "Saski", "latitude": 9.35478, "longitude": 13.56724, "x": 56.4, "y": 65.4, "source": "geonames", "type": "PPL" }, { "name": "Sekande", "latitude": 9.42393, "longitude": 13.55504, "x": 53.7, "y": 49.7, "source": "geonames", "type": "PPL" }, { "name": "Sissari", "latitude": 9.47199, "longitude": 13.53861, "x": 50.2, "y": 38.8, "source": "geonames", "type": "PPL" }, { "name": "Soare", "latitude": 9.37187, "longitude": 13.53761, "x": 50, "y": 61.5, "source": "geonames", "type": "PPL" }, { "name": "Sonayo", "latitude": 9.38849, "longitude": 13.43919, "x": 28.6, "y": 57.7, "source": "geonames", "type": "PPL" }, { "name": "Sotobouton", "latitude": 9.5, "longitude": 13.45, "x": 30.9, "y": 32.5, "source": "geonames", "type": "PPL" }, { "name": "Souari", "latitude": 9.31745, "longitude": 13.66739, "x": 78.1, "y": 73.8, "source": "geonames", "type": "PPL" }, { "name": "Soumpa", "latitude": 9.31147, "longitude": 13.50634, "x": 43.2, "y": 75.2, "source": "geonames", "type": "PPL" }, { "name": "Tifel", "latitude": 9.47642, "longitude": 13.35362, "x": 10, "y": 37.8, "source": "geonames", "type": "PPL" }, { "name": "Toro", "latitude": 9.52687, "longitude": 13.42877, "x": 26.3, "y": 26.4, "source": "geonames", "type": "PPL" }, { "name": "Tsolaram", "latitude": 9.55732, "longitude": 13.57278, "x": 57.6, "y": 19.5, "source": "geonames", "type": "PPL" }, { "name": "Yamsala", "latitude": 9.29097, "longitude": 13.60235, "x": 64, "y": 79.9, "source": "geonames", "type": "PPL" }, { "name": "Zagam", "latitude": 9.51667, "longitude": 13.66667, "x": 78, "y": 28.7, "source": "geonames", "type": "PPL" } ], "Poli": [ { "name": "Badongo", "latitude": 8.52074, "longitude": 13.25151, "x": 52.3, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Bakte", "latitude": 8.40458, "longitude": 13.22747, "x": 47.5, "y": 64.7, "source": "geonames", "type": "PPL" }, { "name": "Balche", "latitude": 8.3304, "longitude": 13.36715, "x": 75.6, "y": 80.4, "source": "geonames", "type": "PPL" }, { "name": "Balkoa", "latitude": 8.57921, "longitude": 13.0888, "x": 19.5, "y": 27.6, "source": "geonames", "type": "PPL" }, { "name": "Baningaziouto", "latitude": 8.30459, "longitude": 13.15384, "x": 32.6, "y": 85.9, "source": "geonames", "type": "PPL" }, { "name": "Bate", "latitude": 8.57222, "longitude": 13.12833, "x": 27.5, "y": 29.1, "source": "geonames", "type": "PPL" }, { "name": "Bere", "latitude": 8.39156, "longitude": 13.21838, "x": 45.6, "y": 67.4, "source": "geonames", "type": "PPL" }, { "name": "Bokare", "latitude": 8.30078, "longitude": 13.30379, "x": 62.8, "y": 86.7, "source": "geonames", "type": "PPL" }, { "name": "Boko", "latitude": 8.40722, "longitude": 13.19676, "x": 41.3, "y": 64.1, "source": "geonames", "type": "PPL" }, { "name": "Bokte", "latitude": 8.5033, "longitude": 13.18139, "x": 38.2, "y": 43.7, "source": "geonames", "type": "PPL" }, { "name": "Bolele", "latitude": 8.51572, "longitude": 13.18529, "x": 39, "y": 41.1, "source": "geonames", "type": "PPL" }, { "name": "Boude", "latitude": 8.48346, "longitude": 13.1411, "x": 30.1, "y": 47.9, "source": "geonames", "type": "PPL" }, { "name": "Bougi", "latitude": 8.43233, "longitude": 13.35408, "x": 73, "y": 58.8, "source": "geonames", "type": "PPL" }, { "name": "Boulko", "latitude": 8.54155, "longitude": 13.13151, "x": 28.1, "y": 35.6, "source": "geonames", "type": "PPL" }, { "name": "Boumba", "latitude": 8.48017, "longitude": 13.37038, "x": 76.3, "y": 48.6, "source": "geonames", "type": "PPL" }, { "name": "Boumi", "latitude": 8.42776, "longitude": 13.38736, "x": 79.7, "y": 59.7, "source": "geonames", "type": "PPL" }, { "name": "Boumse", "latitude": 8.5069, "longitude": 13.17977, "x": 37.8, "y": 42.9, "source": "geonames", "type": "PPL" }, { "name": "Boundje", "latitude": 8.44803, "longitude": 13.24759, "x": 51.5, "y": 55.4, "source": "geonames", "type": "PPL" }, { "name": "Boupi", "latitude": 8.51184, "longitude": 13.36542, "x": 75.3, "y": 41.9, "source": "geonames", "type": "PPL" }, { "name": "Dai", "latitude": 8.43448, "longitude": 13.24973, "x": 51.9, "y": 58.3, "source": "geonames", "type": "PPL" }, { "name": "Dakidongo", "latitude": 8.49848, "longitude": 13.10568, "x": 22.9, "y": 44.7, "source": "geonames", "type": "PPL" }, { "name": "Dembako", "latitude": 8.50698, "longitude": 13.16435, "x": 34.7, "y": 42.9, "source": "geonames", "type": "PPL" }, { "name": "Deta", "latitude": 8.52183, "longitude": 13.17948, "x": 37.8, "y": 39.8, "source": "geonames", "type": "PPL" }, { "name": "Dingbare", "latitude": 8.41482, "longitude": 13.40356, "x": 83, "y": 62.5, "source": "geonames", "type": "PPL" }, { "name": "Djasa", "latitude": 8.31083, "longitude": 13.31239, "x": 64.6, "y": 84.6, "source": "geonames", "type": "PPL" }, { "name": "Djogi", "latitude": 8.42289, "longitude": 13.36274, "x": 74.7, "y": 60.8, "source": "geonames", "type": "PPL" }, { "name": "Djogo", "latitude": 8.39916, "longitude": 13.21428, "x": 44.8, "y": 65.8, "source": "geonames", "type": "PPL" }, { "name": "Djougla", "latitude": 8.30076, "longitude": 13.27526, "x": 57.1, "y": 86.7, "source": "geonames", "type": "PPL" }, { "name": "Djoumte", "latitude": 8.55065, "longitude": 13.19469, "x": 40.9, "y": 33.6, "source": "geonames", "type": "PPL" }, { "name": "Dongko", "latitude": 8.60971, "longitude": 13.11723, "x": 25.2, "y": 21.1, "source": "geonames", "type": "PPL" }, { "name": "Dongo", "latitude": 8.39592, "longitude": 13.22259, "x": 46.5, "y": 66.5, "source": "geonames", "type": "PPL" }, { "name": "Dongole", "latitude": 8.47369, "longitude": 13.23977, "x": 49.9, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Doumboulko", "latitude": 8.41478, "longitude": 13.27037, "x": 56.1, "y": 62.5, "source": "geonames", "type": "PPL" }, { "name": "Gabardina", "latitude": 8.33929, "longitude": 13.15281, "x": 32.4, "y": 78.5, "source": "geonames", "type": "PPL" }, { "name": "Gagi", "latitude": 8.42558, "longitude": 13.30515, "x": 63.1, "y": 60.2, "source": "geonames", "type": "PPL" }, { "name": "Gahamba", "latitude": 8.39432, "longitude": 13.3274, "x": 67.6, "y": 66.8, "source": "geonames", "type": "PPL" }, { "name": "Gahiba", "latitude": 8.44338, "longitude": 13.35506, "x": 73.2, "y": 56.4, "source": "geonames", "type": "PPL" }, { "name": "Gandjiba", "latitude": 8.42913, "longitude": 13.33745, "x": 69.6, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Gare", "latitude": 8.49493, "longitude": 13.15198, "x": 32.2, "y": 45.5, "source": "geonames", "type": "PPL" }, { "name": "Gata", "latitude": 8.44097, "longitude": 13.25306, "x": 52.6, "y": 56.9, "source": "geonames", "type": "PPL" }, { "name": "Geri", "latitude": 8.54408, "longitude": 13.35527, "x": 73.2, "y": 35, "source": "geonames", "type": "PPL" }, { "name": "Gesou", "latitude": 8.29427, "longitude": 13.16733, "x": 35.3, "y": 88.1, "source": "geonames", "type": "PPL" }, { "name": "Gito", "latitude": 8.43319, "longitude": 13.08443, "x": 18.6, "y": 58.6, "source": "geonames", "type": "PPL" }, { "name": "Gomana", "latitude": 8.53026, "longitude": 13.17187, "x": 36.3, "y": 38, "source": "geonames", "type": "PPL" }, { "name": "Gombo", "latitude": 8.509, "longitude": 13.11212, "x": 24.2, "y": 42.5, "source": "geonames", "type": "PPL" }, { "name": "Gompou", "latitude": 8.29278, "longitude": 13.2868, "x": 59.4, "y": 88.4, "source": "geonames", "type": "PPL" }, { "name": "Gose", "latitude": 8.58935, "longitude": 13.21111, "x": 44.2, "y": 25.4, "source": "geonames", "type": "PPL" }, { "name": "Gourko", "latitude": 8.48788, "longitude": 13.18534, "x": 39, "y": 47, "source": "geonames", "type": "PPL" }, { "name": "Hati", "latitude": 8.42234, "longitude": 13.3909, "x": 80.4, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Hepa", "latitude": 8.40458, "longitude": 13.24042, "x": 50.1, "y": 64.7, "source": "geonames", "type": "PPL" }, { "name": "Herko", "latitude": 8.49949, "longitude": 13.14408, "x": 30.7, "y": 44.5, "source": "geonames", "type": "PPL" }, { "name": "Hoke", "latitude": 8.52674, "longitude": 13.30264, "x": 62.6, "y": 38.7, "source": "geonames", "type": "PPL" }, { "name": "Honle", "latitude": 8.49493, "longitude": 13.08557, "x": 18.9, "y": 45.5, "source": "geonames", "type": "PPL" }, { "name": "Hoy", "latitude": 8.47872, "longitude": 13.32713, "x": 67.5, "y": 48.9, "source": "geonames", "type": "PPL" }, { "name": "Iba", "latitude": 8.55815, "longitude": 13.34175, "x": 70.5, "y": 32, "source": "geonames", "type": "PPL" }, { "name": "Kakati", "latitude": 8.55388, "longitude": 13.397, "x": 81.6, "y": 33, "source": "geonames", "type": "PPL" }, { "name": "Kalbingto", "latitude": 8.32348, "longitude": 13.31519, "x": 65.1, "y": 81.9, "source": "geonames", "type": "PPL" }, { "name": "Kebi", "latitude": 8.37634, "longitude": 13.40362, "x": 83, "y": 70.7, "source": "geonames", "type": "PPL" }, { "name": "Kongle", "latitude": 8.4607, "longitude": 13.1837, "x": 38.6, "y": 52.7, "source": "geonames", "type": "PPL" }, { "name": "Koniakba", "latitude": 8.45996, "longitude": 13.32869, "x": 67.9, "y": 52.9, "source": "geonames", "type": "PPL" }, { "name": "Koukse", "latitude": 8.57489, "longitude": 13.233, "x": 48.6, "y": 28.5, "source": "geonames", "type": "PPL" }, { "name": "Koumdongo", "latitude": 8.56781, "longitude": 13.11364, "x": 24.5, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Kpoti", "latitude": 8.49653, "longitude": 13.3774, "x": 77.7, "y": 45.1, "source": "geonames", "type": "PPL" }, { "name": "Legi", "latitude": 8.40447, "longitude": 13.36159, "x": 74.5, "y": 64.7, "source": "geonames", "type": "PPL" }, { "name": "Longte", "latitude": 8.42655, "longitude": 13.23589, "x": 49.2, "y": 60, "source": "geonames", "type": "PPL" }, { "name": "Maa", "latitude": 8.4435, "longitude": 13.27818, "x": 57.7, "y": 56.4, "source": "geonames", "type": "PPL" }, { "name": "Mangati", "latitude": 8.41992, "longitude": 13.25624, "x": 53.3, "y": 61.4, "source": "geonames", "type": "PPL" }, { "name": "Mango", "latitude": 8.41706, "longitude": 13.24141, "x": 50.3, "y": 62, "source": "geonames", "type": "PPL" }, { "name": "Mangzean", "latitude": 8.31426, "longitude": 13.32969, "x": 68.1, "y": 83.9, "source": "geonames", "type": "PPL" }, { "name": "Marka", "latitude": 8.45772, "longitude": 13.2762, "x": 57.3, "y": 53.4, "source": "geonames", "type": "PPL" }, { "name": "Mbanha", "latitude": 8.59696, "longitude": 13.18062, "x": 38, "y": 23.8, "source": "geonames", "type": "PPL" }, { "name": "Mouto", "latitude": 8.53484, "longitude": 13.13655, "x": 29.1, "y": 37, "source": "geonames", "type": "PPL" }, { "name": "Naba", "latitude": 8.41879, "longitude": 13.34251, "x": 70.6, "y": 61.6, "source": "geonames", "type": "PPL" }, { "name": "Nagouaro", "latitude": 8.49204, "longitude": 13.23731, "x": 49.4, "y": 46.1, "source": "geonames", "type": "PPL" }, { "name": "Napsa", "latitude": 8.63252, "longitude": 13.2081, "x": 43.6, "y": 16.3, "source": "geonames", "type": "PPL" }, { "name": "Niagi", "latitude": 8.43755, "longitude": 13.27802, "x": 57.7, "y": 57.7, "source": "geonames", "type": "PPL" }, { "name": "Niegto", "latitude": 8.54955, "longitude": 13.28344, "x": 58.7, "y": 33.9, "source": "geonames", "type": "PPL" }, { "name": "Nietche", "latitude": 8.42995, "longitude": 13.20332, "x": 42.6, "y": 59.3, "source": "geonames", "type": "PPL" }, { "name": "Otere", "latitude": 8.49958, "longitude": 13.0416, "x": 10, "y": 44.5, "source": "geonames", "type": "PPL" }, { "name": "Ouro Djabi", "latitude": 8.60287, "longitude": 13.36313, "x": 74.8, "y": 22.6, "source": "geonames", "type": "PPL" }, { "name": "Pelbou", "latitude": 8.31447, "longitude": 13.16543, "x": 35, "y": 83.8, "source": "geonames", "type": "PPL" }, { "name": "Pipa", "latitude": 8.41145, "longitude": 13.23947, "x": 49.9, "y": 63.2, "source": "geonames", "type": "PPL" }, { "name": "Poli Wango", "latitude": 8.5039, "longitude": 13.20984, "x": 43.9, "y": 43.6, "source": "geonames", "type": "PPL" }, { "name": "Pouksa", "latitude": 8.289, "longitude": 13.23101, "x": 48.2, "y": 89.2, "source": "geonames", "type": "PPL" }, { "name": "Poulko", "latitude": 8.52077, "longitude": 13.29661, "x": 61.4, "y": 40, "source": "geonames", "type": "PPL" }, { "name": "Riga", "latitude": 8.59116, "longitude": 13.19786, "x": 41.5, "y": 25, "source": "geonames", "type": "PPL" }, { "name": "Ringo", "latitude": 8.40345, "longitude": 13.19817, "x": 41.6, "y": 64.9, "source": "geonames", "type": "PPL" }, { "name": "Roube", "latitude": 8.46667, "longitude": 13.08333, "x": 18.4, "y": 51.5, "source": "geonames", "type": "PPL" }, { "name": "Rouka", "latitude": 8.66196, "longitude": 13.17265, "x": 36.4, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Sadje", "latitude": 8.42144, "longitude": 13.08228, "x": 18.2, "y": 61.1, "source": "geonames", "type": "PPL" }, { "name": "Sago", "latitude": 8.42852, "longitude": 13.26208, "x": 54.4, "y": 59.6, "source": "geonames", "type": "PPL" }, { "name": "Salaki", "latitude": 8.3878, "longitude": 13.07875, "x": 17.5, "y": 68.2, "source": "geonames", "type": "PPL" }, { "name": "Samo", "latitude": 8.48927, "longitude": 13.17372, "x": 36.6, "y": 46.7, "source": "geonames", "type": "PPL" }, { "name": "Sande", "latitude": 8.3972, "longitude": 13.25403, "x": 52.8, "y": 66.2, "source": "geonames", "type": "PPL" }, { "name": "Sare", "latitude": 8.42547, "longitude": 13.26972, "x": 56, "y": 60.2, "source": "geonames", "type": "PPL" }, { "name": "Sari", "latitude": 8.44496, "longitude": 13.31693, "x": 65.5, "y": 56.1, "source": "geonames", "type": "PPL" }, { "name": "Sebi", "latitude": 8.42912, "longitude": 13.36761, "x": 75.7, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Sebore", "latitude": 8.28531, "longitude": 13.27948, "x": 57.9, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Seko", "latitude": 8.38786, "longitude": 13.19965, "x": 41.9, "y": 68.2, "source": "geonames", "type": "PPL" }, { "name": "Seksekba", "latitude": 8.48484, "longitude": 13.42478, "x": 87.2, "y": 47.6, "source": "geonames", "type": "PPL" }, { "name": "Selou", "latitude": 8.30355, "longitude": 13.19524, "x": 41, "y": 86.1, "source": "geonames", "type": "PPL" }, { "name": "Sera", "latitude": 8.55535, "longitude": 13.11972, "x": 25.7, "y": 32.6, "source": "geonames", "type": "PPL" }, { "name": "Seri", "latitude": 8.44024, "longitude": 13.33622, "x": 69.4, "y": 57.1, "source": "geonames", "type": "PPL" }, { "name": "Sigari", "latitude": 8.33944, "longitude": 13.18426, "x": 38.8, "y": 78.5, "source": "geonames", "type": "PPL" }, { "name": "Singba", "latitude": 8.32216, "longitude": 13.32432, "x": 67, "y": 82.2, "source": "geonames", "type": "PPL" }, { "name": "Siti", "latitude": 8.491, "longitude": 13.43852, "x": 90, "y": 46.3, "source": "geonames", "type": "PPL" }, { "name": "Tako", "latitude": 8.49407, "longitude": 13.10803, "x": 23.4, "y": 45.7, "source": "geonames", "type": "PPL" }, { "name": "Tchinte", "latitude": 8.44663, "longitude": 13.29937, "x": 62, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Tegi", "latitude": 8.44309, "longitude": 13.33682, "x": 69.5, "y": 56.5, "source": "geonames", "type": "PPL" }, { "name": "Tere", "latitude": 8.4948, "longitude": 13.0738, "x": 16.5, "y": 45.5, "source": "geonames", "type": "PPL" }, { "name": "Tingori", "latitude": 8.63299, "longitude": 13.25261, "x": 52.5, "y": 16.2, "source": "geonames", "type": "PPL" }, { "name": "Tirga", "latitude": 8.49885, "longitude": 13.21178, "x": 44.3, "y": 44.6, "source": "geonames", "type": "PPL" }, { "name": "Torgi", "latitude": 8.42885, "longitude": 13.31683, "x": 65.5, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Toukte", "latitude": 8.42286, "longitude": 13.26227, "x": 54.5, "y": 60.8, "source": "geonames", "type": "PPL" }, { "name": "Toungo", "latitude": 8.46053, "longitude": 13.24011, "x": 50, "y": 52.8, "source": "geonames", "type": "PPL" }, { "name": "Wagba", "latitude": 8.47096, "longitude": 13.3853, "x": 79.3, "y": 50.6, "source": "geonames", "type": "PPL" }, { "name": "Wakiri", "latitude": 8.42063, "longitude": 13.19847, "x": 41.6, "y": 61.3, "source": "geonames", "type": "PPL" }, { "name": "Wami", "latitude": 8.55495, "longitude": 13.2662, "x": 55.3, "y": 32.7, "source": "geonames", "type": "PPL" }, { "name": "Wante", "latitude": 8.46944, "longitude": 13.30906, "x": 63.9, "y": 50.9, "source": "geonames", "type": "PPL" }, { "name": "Yarnambo", "latitude": 8.58159, "longitude": 13.12879, "x": 27.6, "y": 27.1, "source": "geonames", "type": "PPL" } ], "Rey-Bouba": [ { "name": "Anina", "latitude": 8.6873, "longitude": 14.2069, "x": 61.3, "y": 57.4, "source": "geonames", "type": "PPL" }, { "name": "Babbabla", "latitude": 8.689, "longitude": 14.0083, "x": 18.4, "y": 57, "source": "geonames", "type": "PPL" }, { "name": "Badjari", "latitude": 8.839, "longitude": 14.2302, "x": 66.3, "y": 20, "source": "geonames", "type": "PPL" }, { "name": "Bagale", "latitude": 8.68647, "longitude": 13.983, "x": 12.9, "y": 57.6, "source": "geonames", "type": "PPL" }, { "name": "Balsangri", "latitude": 8.785, "longitude": 14.175, "x": 54.4, "y": 33.3, "source": "geonames", "type": "PPL" }, { "name": "Bassabar", "latitude": 8.791, "longitude": 14.2154, "x": 63.1, "y": 31.9, "source": "geonames", "type": "PPL" }, { "name": "Bassari", "latitude": 8.7091, "longitude": 14.0807, "x": 34, "y": 52, "source": "geonames", "type": "PPL" }, { "name": "Bideng", "latitude": 8.78182, "longitude": 13.99994, "x": 16.6, "y": 34.1, "source": "geonames", "type": "PPL" }, { "name": "Boromi", "latitude": 8.6438, "longitude": 14.1983, "x": 59.4, "y": 68.1, "source": "geonames", "type": "PPL" }, { "name": "Dawan", "latitude": 8.77153, "longitude": 14.13425, "x": 45.6, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Djalbang", "latitude": 8.7733, "longitude": 14.2339, "x": 67.1, "y": 36.2, "source": "geonames", "type": "PPL" }, { "name": "Djamboutou", "latitude": 8.69721, "longitude": 13.99854, "x": 16.3, "y": 54.9, "source": "geonames", "type": "PPL" }, { "name": "Djouroum", "latitude": 8.7653, "longitude": 14.0353, "x": 24.2, "y": 38.2, "source": "geonames", "type": "PPL" }, { "name": "Doudja", "latitude": 8.7176, "longitude": 14.0715, "x": 32, "y": 49.9, "source": "geonames", "type": "PPL" }, { "name": "Gabde", "latitude": 8.779, "longitude": 14.1584, "x": 50.8, "y": 34.8, "source": "geonames", "type": "PPL" }, { "name": "Gangouri", "latitude": 8.7901, "longitude": 14.1685, "x": 53, "y": 32.1, "source": "geonames", "type": "PPL" }, { "name": "Gatouguel", "latitude": 8.6166, "longitude": 14.3049, "x": 82.4, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Godi", "latitude": 8.7908, "longitude": 14.19, "x": 57.6, "y": 31.9, "source": "geonames", "type": "PPL" }, { "name": "Godji", "latitude": 8.6627, "longitude": 14.1507, "x": 49.1, "y": 63.4, "source": "geonames", "type": "PPL" }, { "name": "Hamdja", "latitude": 8.7466, "longitude": 14.0585, "x": 29.2, "y": 42.8, "source": "geonames", "type": "PPL" }, { "name": "Kamale", "latitude": 8.75186, "longitude": 13.96952, "x": 10, "y": 41.5, "source": "geonames", "type": "PPL" }, { "name": "Kongron", "latitude": 8.7565, "longitude": 14.1628, "x": 51.7, "y": 40.4, "source": "geonames", "type": "PPL" }, { "name": "Landou", "latitude": 8.5548, "longitude": 14.3399, "x": 90, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Limbatmi", "latitude": 8.8399, "longitude": 14.2404, "x": 68.5, "y": 19.8, "source": "geonames", "type": "PPL" }, { "name": "Louggue", "latitude": 8.8798, "longitude": 14.1191, "x": 42.3, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Mbila", "latitude": 8.6502, "longitude": 14.2906, "x": 79.4, "y": 66.5, "source": "geonames", "type": "PPL" }, { "name": "Natdjari", "latitude": 8.6719, "longitude": 14.0071, "x": 18.1, "y": 61.2, "source": "geonames", "type": "PPL" }, { "name": "Nyomayre", "latitude": 8.75, "longitude": 14.2723, "x": 75.4, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Ouro Mayo Ria", "latitude": 8.73361, "longitude": 14.30321, "x": 82.1, "y": 46, "source": "geonames", "type": "PPL" }, { "name": "Ouromaire", "latitude": 8.75, "longitude": 14.26667, "x": 74.2, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Rey Manga", "latitude": 8.6484, "longitude": 14.173, "x": 54, "y": 67, "source": "geonames", "type": "PPL" }, { "name": "Sadouya", "latitude": 8.71827, "longitude": 13.97255, "x": 10.7, "y": 49.8, "source": "geonames", "type": "PPL" }, { "name": "Sedou", "latitude": 8.7783, "longitude": 14.2289, "x": 66, "y": 35, "source": "geonames", "type": "PPL" }, { "name": "Tatou", "latitude": 8.70391, "longitude": 13.98962, "x": 14.3, "y": 53.3, "source": "geonames", "type": "PPL" }, { "name": "Yokode", "latitude": 8.8795, "longitude": 14.0933, "x": 36.7, "y": 10.1, "source": "geonames", "type": "PPL" } ], "Saa": [ { "name": "Abel", "latitude": 4.28333, "longitude": 11.41667, "x": 45, "y": 78, "source": "geonames", "type": "PPL" }, { "name": "Biatingena", "latitude": 4.55, "longitude": 11.45, "x": 55, "y": 14, "source": "geonames", "type": "PPL" }, { "name": "Ebogo", "latitude": 4.35, "longitude": 11.41667, "x": 45, "y": 62, "source": "geonames", "type": "PPL" }, { "name": "Ebomzoud", "latitude": 4.38333, "longitude": 11.3, "x": 10, "y": 54, "source": "geonames", "type": "PPL" }, { "name": "Ebong", "latitude": 4.35, "longitude": 11.36667, "x": 30, "y": 62, "source": "geonames", "type": "PPL" }, { "name": "Edzen", "latitude": 4.28333, "longitude": 11.46667, "x": 60, "y": 78, "source": "geonames", "type": "PPL" }, { "name": "Ekalan Minkou", "latitude": 4.3, "longitude": 11.41667, "x": 45, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Elang", "latitude": 4.46667, "longitude": 11.48333, "x": 65, "y": 34, "source": "geonames", "type": "PPL" }, { "name": "Elesoge", "latitude": 4.4, "longitude": 11.4, "x": 40, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Etam Koma", "latitude": 4.35, "longitude": 11.31667, "x": 15, "y": 62, "source": "geonames", "type": "PPL" }, { "name": "Eyene", "latitude": 4.43333, "longitude": 11.38333, "x": 35, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Ezezang", "latitude": 4.26667, "longitude": 11.38333, "x": 35, "y": 82, "source": "geonames", "type": "PPL" }, { "name": "Goura", "latitude": 4.55, "longitude": 11.4, "x": 40, "y": 14, "source": "geonames", "type": "PPL" }, { "name": "Goura II", "latitude": 4.55, "longitude": 11.45, "x": 55, "y": 14, "source": "geonames", "type": "PPL" }, { "name": "Indoum", "latitude": 4.3, "longitude": 11.38333, "x": 35, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Koan", "latitude": 4.33333, "longitude": 11.35, "x": 25, "y": 66, "source": "geonames", "type": "PPL" }, { "name": "Kokoe", "latitude": 4.46667, "longitude": 11.46667, "x": 60, "y": 34, "source": "geonames", "type": "PPL" }, { "name": "Koro", "latitude": 4.46667, "longitude": 11.48333, "x": 65, "y": 34, "source": "geonames", "type": "PPL" }, { "name": "Lebamzip I", "latitude": 4.26667, "longitude": 11.48333, "x": 65, "y": 82, "source": "geonames", "type": "PPL" }, { "name": "Lekoubek", "latitude": 4.38333, "longitude": 11.48333, "x": 65, "y": 54, "source": "geonames", "type": "PPL" }, { "name": "Man Elon", "latitude": 4.31667, "longitude": 11.4, "x": 40, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Mbangasina", "latitude": 4.56667, "longitude": 11.38333, "x": 35, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Mbasila", "latitude": 4.45, "longitude": 11.43333, "x": 50, "y": 38, "source": "geonames", "type": "PPL" }, { "name": "Mbazoa", "latitude": 4.33333, "longitude": 11.45, "x": 55, "y": 66, "source": "geonames", "type": "PPL" }, { "name": "Mbenega", "latitude": 4.46667, "longitude": 11.35, "x": 25, "y": 34, "source": "geonames", "type": "PPL" }, { "name": "Mekimebodo", "latitude": 4.35, "longitude": 11.5, "x": 70, "y": 62, "source": "geonames", "type": "PPL" }, { "name": "Melik", "latitude": 4.38333, "longitude": 11.48333, "x": 65, "y": 54, "source": "geonames", "type": "PPL" }, { "name": "Mindouga", "latitude": 4.41667, "longitude": 11.46667, "x": 60, "y": 46, "source": "geonames", "type": "PPL" }, { "name": "Mingon", "latitude": 4.4, "longitude": 11.45, "x": 55, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Mokala", "latitude": 4.41667, "longitude": 11.43333, "x": 50, "y": 46, "source": "geonames", "type": "PPL" }, { "name": "Momo", "latitude": 4.41667, "longitude": 11.48333, "x": 65, "y": 46, "source": "geonames", "type": "PPL" }, { "name": "Ndongelang", "latitude": 4.46667, "longitude": 11.41667, "x": 45, "y": 34, "source": "geonames", "type": "PPL" }, { "name": "Ndounda", "latitude": 4.45, "longitude": 11.36667, "x": 30, "y": 38, "source": "geonames", "type": "PPL" }, { "name": "Ngoksa", "latitude": 4.33333, "longitude": 11.33333, "x": 20, "y": 66, "source": "geonames", "type": "PPL" }, { "name": "Nkolang", "latitude": 4.43333, "longitude": 11.48333, "x": 65, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Nkolayos", "latitude": 4.35, "longitude": 11.5, "x": 70, "y": 62, "source": "geonames", "type": "PPL" }, { "name": "Nkolbibak", "latitude": 4.28333, "longitude": 11.5, "x": 70, "y": 78, "source": "geonames", "type": "PPL" }, { "name": "Nkolbogo I", "latitude": 4.35, "longitude": 11.48333, "x": 65, "y": 62, "source": "geonames", "type": "PPL" }, { "name": "Nkolbogo II", "latitude": 4.4, "longitude": 11.51667, "x": 75, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Nkolbogo III", "latitude": 4.33333, "longitude": 11.56667, "x": 90, "y": 66, "source": "geonames", "type": "PPL" }, { "name": "Nkoldjama II", "latitude": 4.31667, "longitude": 11.5, "x": 70, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Nkolebasimbi", "latitude": 4.48333, "longitude": 11.45, "x": 55, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Nkolebouma", "latitude": 4.43333, "longitude": 11.43333, "x": 50, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Nkolekono", "latitude": 4.3, "longitude": 11.45, "x": 55, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Nkolelouga", "latitude": 4.46667, "longitude": 11.36667, "x": 30, "y": 34, "source": "geonames", "type": "PPL" }, { "name": "Nkolesono", "latitude": 4.41667, "longitude": 11.5, "x": 70, "y": 46, "source": "geonames", "type": "PPL" }, { "name": "Nkolmebang", "latitude": 4.38333, "longitude": 11.41667, "x": 45, "y": 54, "source": "geonames", "type": "PPL" }, { "name": "Nkolmefon", "latitude": 4.33333, "longitude": 11.46667, "x": 60, "y": 66, "source": "geonames", "type": "PPL" }, { "name": "Nkolmesing", "latitude": 4.36667, "longitude": 11.45, "x": 55, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Nkolmeyos", "latitude": 4.26667, "longitude": 11.48333, "x": 65, "y": 82, "source": "geonames", "type": "PPL" }, { "name": "Nkolmgbana", "latitude": 4.4, "longitude": 11.45, "x": 55, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Nkolmvak", "latitude": 4.3, "longitude": 11.53333, "x": 80, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Nkolndzomo", "latitude": 4.48333, "longitude": 11.41667, "x": 45, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Nkolntsa", "latitude": 4.26667, "longitude": 11.48333, "x": 65, "y": 82, "source": "geonames", "type": "PPL" }, { "name": "Nkolo", "latitude": 4.41667, "longitude": 11.41667, "x": 45, "y": 46, "source": "geonames", "type": "PPL" }, { "name": "Nkoltai", "latitude": 4.23333, "longitude": 11.38333, "x": 35, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Nkoltomo I", "latitude": 4.23333, "longitude": 11.38333, "x": 35, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Nkolve", "latitude": 4.3, "longitude": 11.48333, "x": 65, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Nkolzoa", "latitude": 4.31667, "longitude": 11.45, "x": 55, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Nkolzomo", "latitude": 4.3, "longitude": 11.51667, "x": 75, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Nkom I", "latitude": 4.41667, "longitude": 11.36667, "x": 30, "y": 46, "source": "geonames", "type": "PPL" }, { "name": "Nkom II", "latitude": 4.4, "longitude": 11.36667, "x": 30, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Nlongonambele", "latitude": 4.25, "longitude": 11.41667, "x": 45, "y": 86, "source": "geonames", "type": "PPL" }, { "name": "Nlongzok", "latitude": 4.38333, "longitude": 11.31667, "x": 15, "y": 54, "source": "geonames", "type": "PPL" }, { "name": "Ntsan", "latitude": 4.41667, "longitude": 11.46667, "x": 60, "y": 46, "source": "geonames", "type": "PPL" }, { "name": "Polo", "latitude": 4.45, "longitude": 11.4, "x": 40, "y": 38, "source": "geonames", "type": "PPL" }, { "name": "Polo I", "latitude": 4.28333, "longitude": 11.4, "x": 40, "y": 78, "source": "geonames", "type": "PPL" }, { "name": "Polo II", "latitude": 4.28333, "longitude": 11.36667, "x": 30, "y": 78, "source": "geonames", "type": "PPL" }, { "name": "Song Andzanga", "latitude": 4.41667, "longitude": 11.31667, "x": 15, "y": 46, "source": "geonames", "type": "PPL" }, { "name": "Womkoa", "latitude": 4.35, "longitude": 11.53333, "x": 80, "y": 62, "source": "geonames", "type": "PPL" }, { "name": "Yebekolo", "latitude": 4.53333, "longitude": 11.31667, "x": 15, "y": 18, "source": "geonames", "type": "PPL" }, { "name": "Zokogo", "latitude": 4.31667, "longitude": 11.38333, "x": 35, "y": 70, "source": "geonames", "type": "PPL" } ], "Sangmelima": [ { "name": "Akak", "latitude": 2.96667, "longitude": 11.88333, "x": 32.4, "y": 48.3, "source": "geonames", "type": "PPL" }, { "name": "Akoaloui", "latitude": 2.9, "longitude": 11.91667, "x": 38.8, "y": 62.2, "source": "geonames", "type": "PPL" }, { "name": "Akomendibi", "latitude": 3.03333, "longitude": 12.03333, "x": 61.2, "y": 34.3, "source": "geonames", "type": "PPL" }, { "name": "Akoo", "latitude": 2.96667, "longitude": 12.05, "x": 64.4, "y": 48.3, "source": "geonames", "type": "PPL" }, { "name": "Alouma", "latitude": 2.8, "longitude": 11.86667, "x": 29.2, "y": 83, "source": "geonames", "type": "PPL" }, { "name": "Assok", "latitude": 2.76667, "longitude": 11.93333, "x": 42, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Atong", "latitude": 2.96667, "longitude": 12.1, "x": 74, "y": 48.3, "source": "geonames", "type": "PPL" }, { "name": "Avebe", "latitude": 2.93333, "longitude": 12, "x": 54.8, "y": 55.2, "source": "geonames", "type": "PPL" }, { "name": "Avoane", "latitude": 2.91667, "longitude": 11.96667, "x": 48.4, "y": 58.7, "source": "geonames", "type": "PPL" }, { "name": "Benyougou", "latitude": 2.9, "longitude": 11.81667, "x": 19.6, "y": 62.2, "source": "geonames", "type": "PPL" }, { "name": "Biboulman", "latitude": 3.08333, "longitude": 12.03333, "x": 61.2, "y": 23.9, "source": "geonames", "type": "PPL" }, { "name": "Bidjom", "latitude": 2.91667, "longitude": 12.01667, "x": 58, "y": 58.7, "source": "geonames", "type": "PPL" }, { "name": "Ekoumedoum", "latitude": 2.95, "longitude": 11.8, "x": 16.4, "y": 51.7, "source": "geonames", "type": "PPL" }, { "name": "Eminemvon", "latitude": 2.96667, "longitude": 11.78333, "x": 13.2, "y": 48.3, "source": "geonames", "type": "PPL" }, { "name": "Esam", "latitude": 2.98333, "longitude": 12.15, "x": 83.6, "y": 44.8, "source": "geonames", "type": "PPL" }, { "name": "Evindisi II", "latitude": 3.13333, "longitude": 11.9, "x": 35.6, "y": 13.5, "source": "geonames", "type": "PPL" }, { "name": "Eyee", "latitude": 2.78333, "longitude": 11.98333, "x": 51.6, "y": 86.5, "source": "geonames", "type": "PPL" }, { "name": "Foulassi", "latitude": 2.98333, "longitude": 11.96667, "x": 48.4, "y": 44.8, "source": "geonames", "type": "PPL" }, { "name": "Kamelon", "latitude": 2.9, "longitude": 12.01667, "x": 58, "y": 62.2, "source": "geonames", "type": "PPL" }, { "name": "Kondomyos", "latitude": 2.98333, "longitude": 11.96667, "x": 48.4, "y": 44.8, "source": "geonames", "type": "PPL" }, { "name": "Koum", "latitude": 2.96667, "longitude": 12.06667, "x": 67.6, "y": 48.3, "source": "geonames", "type": "PPL" }, { "name": "Maamenyin", "latitude": 2.98333, "longitude": 11.78333, "x": 13.2, "y": 44.8, "source": "geonames", "type": "PPL" }, { "name": "Mamebili", "latitude": 2.9, "longitude": 11.81667, "x": 19.6, "y": 62.2, "source": "geonames", "type": "PPL" }, { "name": "Mang", "latitude": 2.91667, "longitude": 12.01667, "x": 58, "y": 58.7, "source": "geonames", "type": "PPL" }, { "name": "Mbom", "latitude": 2.95, "longitude": 12.03333, "x": 61.2, "y": 51.7, "source": "geonames", "type": "PPL" }, { "name": "Mebem", "latitude": 3.06667, "longitude": 12.03333, "x": 61.2, "y": 27.4, "source": "geonames", "type": "PPL" }, { "name": "Mebemenko", "latitude": 2.9, "longitude": 11.95, "x": 45.2, "y": 62.2, "source": "geonames", "type": "PPL" }, { "name": "Mefo", "latitude": 2.96667, "longitude": 11.96667, "x": 48.4, "y": 48.3, "source": "geonames", "type": "PPL" }, { "name": "Mekam", "latitude": 2.9, "longitude": 11.9, "x": 35.6, "y": 62.2, "source": "geonames", "type": "PPL" }, { "name": "Mekom", "latitude": 2.9, "longitude": 11.88333, "x": 32.4, "y": 62.2, "source": "geonames", "type": "PPL" }, { "name": "Mekomo", "latitude": 2.88333, "longitude": 11.98333, "x": 51.6, "y": 65.7, "source": "geonames", "type": "PPL" }, { "name": "Melen", "latitude": 3.08333, "longitude": 11.93333, "x": 42, "y": 23.9, "source": "geonames", "type": "PPL" }, { "name": "Mengue", "latitude": 2.83333, "longitude": 11.98333, "x": 51.6, "y": 76.1, "source": "geonames", "type": "PPL" }, { "name": "Mesak", "latitude": 2.78333, "longitude": 12.03333, "x": 61.2, "y": 86.5, "source": "geonames", "type": "PPL" }, { "name": "Metet", "latitude": 2.96667, "longitude": 12.01667, "x": 58, "y": 48.3, "source": "geonames", "type": "PPL" }, { "name": "Meukaa", "latitude": 2.76667, "longitude": 12.01667, "x": 58, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Meulok", "latitude": 2.81667, "longitude": 12.08333, "x": 70.8, "y": 79.6, "source": "geonames", "type": "PPL" }, { "name": "Meyo", "latitude": 2.96667, "longitude": 11.95, "x": 45.2, "y": 48.3, "source": "geonames", "type": "PPL" }, { "name": "Meyomadjom", "latitude": 2.76667, "longitude": 11.98333, "x": 51.6, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Meyos", "latitude": 2.78333, "longitude": 12.13333, "x": 80.4, "y": 86.5, "source": "geonames", "type": "PPL" }, { "name": "Mezese", "latitude": 2.96667, "longitude": 12.15, "x": 83.6, "y": 48.3, "source": "geonames", "type": "PPL" }, { "name": "Mfouladja", "latitude": 3.06667, "longitude": 12.03333, "x": 61.2, "y": 27.4, "source": "geonames", "type": "PPL" }, { "name": "Mimbo", "latitude": 2.98333, "longitude": 11.96667, "x": 48.4, "y": 44.8, "source": "geonames", "type": "PPL" }, { "name": "Minbang", "latitude": 2.81667, "longitude": 12.06667, "x": 67.6, "y": 79.6, "source": "geonames", "type": "PPL" }, { "name": "Minkang I", "latitude": 2.81667, "longitude": 12.06667, "x": 67.6, "y": 79.6, "source": "geonames", "type": "PPL" }, { "name": "Minkang II", "latitude": 2.81667, "longitude": 12.1, "x": 74, "y": 79.6, "source": "geonames", "type": "PPL" }, { "name": "Minkwamoveng", "latitude": 2.95, "longitude": 11.85, "x": 26, "y": 51.7, "source": "geonames", "type": "PPL" }, { "name": "Mintyaminyoumin", "latitude": 3.01667, "longitude": 12.03333, "x": 61.2, "y": 37.8, "source": "geonames", "type": "PPL" }, { "name": "Mvanbison", "latitude": 3.03333, "longitude": 12.18333, "x": 90, "y": 34.3, "source": "geonames", "type": "PPL" }, { "name": "Ndjantom", "latitude": 2.9, "longitude": 12.03333, "x": 61.2, "y": 62.2, "source": "geonames", "type": "PPL" }, { "name": "Ndjom Ese", "latitude": 2.96667, "longitude": 12.18333, "x": 90, "y": 48.3, "source": "geonames", "type": "PPL" }, { "name": "Ndoup", "latitude": 3, "longitude": 11.95, "x": 45.2, "y": 41.3, "source": "geonames", "type": "PPL" }, { "name": "Ngam", "latitude": 2.78333, "longitude": 11.9, "x": 35.6, "y": 86.5, "source": "geonames", "type": "PPL" }, { "name": "Ngan", "latitude": 2.96667, "longitude": 11.96667, "x": 48.4, "y": 48.3, "source": "geonames", "type": "PPL" }, { "name": "Ngom", "latitude": 3.08333, "longitude": 12.06667, "x": 67.6, "y": 23.9, "source": "geonames", "type": "PPL" }, { "name": "Ngomyop", "latitude": 2.9, "longitude": 11.78333, "x": 13.2, "y": 62.2, "source": "geonames", "type": "PPL" }, { "name": "Ngon", "latitude": 3.01667, "longitude": 11.95, "x": 45.2, "y": 37.8, "source": "geonames", "type": "PPL" }, { "name": "Ngoulmakong", "latitude": 2.9, "longitude": 11.96667, "x": 48.4, "y": 62.2, "source": "geonames", "type": "PPL" }, { "name": "Nko-Etye", "latitude": 2.91667, "longitude": 11.76667, "x": 10, "y": 58.7, "source": "geonames", "type": "PPL" }, { "name": "Nkolebom", "latitude": 3.06667, "longitude": 12.01667, "x": 58, "y": 27.4, "source": "geonames", "type": "PPL" }, { "name": "Nkolebon", "latitude": 3.05, "longitude": 12, "x": 54.8, "y": 30.9, "source": "geonames", "type": "PPL" }, { "name": "Nkoleyop", "latitude": 3.11667, "longitude": 12.03333, "x": 61.2, "y": 17, "source": "geonames", "type": "PPL" }, { "name": "Nkolfong", "latitude": 3.15, "longitude": 11.93333, "x": 42, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Nkolotoutou", "latitude": 3.08333, "longitude": 12.03333, "x": 61.2, "y": 23.9, "source": "geonames", "type": "PPL" }, { "name": "Nkounde", "latitude": 2.98333, "longitude": 12.01667, "x": 58, "y": 44.8, "source": "geonames", "type": "PPL" }, { "name": "Nloup", "latitude": 3, "longitude": 11.95, "x": 45.2, "y": 41.3, "source": "geonames", "type": "PPL" }, { "name": "Npouang", "latitude": 3.03333, "longitude": 11.96667, "x": 48.4, "y": 34.3, "source": "geonames", "type": "PPL" }, { "name": "Nselang", "latitude": 2.95, "longitude": 11.8, "x": 16.4, "y": 51.7, "source": "geonames", "type": "PPL" }, { "name": "Nyazanga", "latitude": 2.83333, "longitude": 12.05, "x": 64.4, "y": 76.1, "source": "geonames", "type": "PPL" }, { "name": "Olounou", "latitude": 2.81667, "longitude": 12.13333, "x": 80.4, "y": 79.6, "source": "geonames", "type": "PPL" }, { "name": "Oveng", "latitude": 2.95, "longitude": 11.83333, "x": 22.8, "y": 51.7, "source": "geonames", "type": "PPL" } ], "Santa": [ { "name": "Akum", "latitude": 5.79176, "longitude": 10.1848, "x": 48, "y": 38.1, "source": "geonames", "type": "PPL" }, { "name": "Alatening", "latitude": 5.87087, "longitude": 10.12588, "x": 33.2, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Asso", "latitude": 5.78171, "longitude": 10.09773, "x": 26.1, "y": 41.7, "source": "geonames", "type": "PPL" }, { "name": "Awing Bambuluwe", "latitude": 5.84423, "longitude": 10.25933, "x": 66.7, "y": 19.5, "source": "geonames", "type": "PPL" }, { "name": "Baba", "latitude": 5.8639, "longitude": 10.1071, "x": 28.5, "y": 12.5, "source": "geonames", "type": "PPL" }, { "name": "Baba II", "latitude": 5.8623, "longitude": 10.10619, "x": 28.2, "y": 13, "source": "geonames", "type": "PPL" }, { "name": "Bafuku", "latitude": 5.79598, "longitude": 10.12506, "x": 33, "y": 36.6, "source": "geonames", "type": "PPL" }, { "name": "Bali Gha", "latitude": 5.7506, "longitude": 10.25783, "x": 66.3, "y": 52.7, "source": "geonames", "type": "PPL" }, { "name": "Bamefogou", "latitude": 5.72237, "longitude": 10.21058, "x": 54.5, "y": 62.7, "source": "geonames", "type": "PPL" }, { "name": "Bamtem Manjane", "latitude": 5.83481, "longitude": 10.25938, "x": 66.7, "y": 22.8, "source": "geonames", "type": "PPL" }, { "name": "Batchua", "latitude": 5.75775, "longitude": 10.15672, "x": 40.9, "y": 50.2, "source": "geonames", "type": "PPL" }, { "name": "Belang", "latitude": 5.66111, "longitude": 10.03497, "x": 10.3, "y": 84.5, "source": "geonames", "type": "PPLX" }, { "name": "Benjo", "latitude": 5.86847, "longitude": 10.25272, "x": 65, "y": 10.9, "source": "geonames", "type": "PPL" }, { "name": "Bonneka", "latitude": 5.74747, "longitude": 10.13386, "x": 35.2, "y": 53.8, "source": "geonames", "type": "PPL" }, { "name": "Bossaneva", "latitude": 5.75624, "longitude": 10.25318, "x": 65.2, "y": 50.7, "source": "geonames", "type": "PPL" }, { "name": "Buchi", "latitude": 5.78512, "longitude": 10.11085, "x": 29.4, "y": 40.5, "source": "geonames", "type": "PPL" }, { "name": "Chanda", "latitude": 5.79977, "longitude": 10.29358, "x": 75.3, "y": 35.2, "source": "geonames", "type": "PPL" }, { "name": "Darmagnac Ferme", "latitude": 5.73501, "longitude": 10.14317, "x": 37.5, "y": 58.2, "source": "geonames", "type": "PPL" }, { "name": "Fido", "latitude": 5.69516, "longitude": 10.16378, "x": 42.7, "y": 72.4, "source": "geonames", "type": "PPL" }, { "name": "Fochu", "latitude": 5.75954, "longitude": 10.19322, "x": 50.1, "y": 49.5, "source": "geonames", "type": "PPL" }, { "name": "Fontegang", "latitude": 5.85953, "longitude": 10.10014, "x": 26.7, "y": 14, "source": "geonames", "type": "PPL" }, { "name": "Fotang", "latitude": 5.67504, "longitude": 10.04121, "x": 11.9, "y": 79.5, "source": "geonames", "type": "PPL" }, { "name": "Gadiwala", "latitude": 5.75988, "longitude": 10.21612, "x": 55.8, "y": 49.4, "source": "geonames", "type": "PPL" }, { "name": "Gangong", "latitude": 5.70894, "longitude": 10.13876, "x": 36.4, "y": 67.5, "source": "geonames", "type": "PPL" }, { "name": "Kombou", "latitude": 5.73739, "longitude": 10.16322, "x": 42.6, "y": 57.4, "source": "geonames", "type": "PPL" }, { "name": "Kongho", "latitude": 5.64559, "longitude": 10.04468, "x": 12.8, "y": 90, "source": "geonames", "type": "PPLX" }, { "name": "Konsa", "latitude": 5.78934, "longitude": 10.1197, "x": 31.6, "y": 39, "source": "geonames", "type": "PPL" }, { "name": "Kwang", "latitude": 5.76263, "longitude": 10.06182, "x": 17.1, "y": 48.4, "source": "geonames", "type": "PPL" }, { "name": "Lepo", "latitude": 5.71261, "longitude": 10.15546, "x": 40.6, "y": 66.2, "source": "geonames", "type": "PPL" }, { "name": "Mba", "latitude": 5.8, "longitude": 10.06667, "x": 18.3, "y": 35.2, "source": "geonames", "type": "PPL" }, { "name": "Mbammuock", "latitude": 5.66679, "longitude": 10.04241, "x": 12.2, "y": 82.5, "source": "geonames", "type": "PPLX" }, { "name": "Mbe", "latitude": 5.83443, "longitude": 10.15753, "x": 41.1, "y": 22.9, "source": "geonames", "type": "PPL" }, { "name": "Mbei", "latitude": 5.80805, "longitude": 10.16115, "x": 42, "y": 32.3, "source": "geonames", "type": "PPL" }, { "name": "Mbendam", "latitude": 5.86705, "longitude": 10.11208, "x": 29.7, "y": 11.4, "source": "geonames", "type": "PPL" }, { "name": "Medji", "latitude": 5.70568, "longitude": 10.18961, "x": 49.2, "y": 68.7, "source": "geonames", "type": "PPL" }, { "name": "Melafi", "latitude": 5.83686, "longitude": 10.32682, "x": 83.7, "y": 22.1, "source": "geonames", "type": "PPL" }, { "name": "Menka", "latitude": 5.74076, "longitude": 10.09046, "x": 24.3, "y": 56.2, "source": "geonames", "type": "PPL" }, { "name": "Menya", "latitude": 5.73799, "longitude": 10.18119, "x": 47.1, "y": 57.2, "source": "geonames", "type": "PPL" }, { "name": "Meta", "latitude": 5.72289, "longitude": 10.21922, "x": 56.6, "y": 62.5, "source": "geonames", "type": "PPL" }, { "name": "Metanyen", "latitude": 5.78786, "longitude": 10.06355, "x": 17.5, "y": 39.5, "source": "geonames", "type": "PPL" }, { "name": "Mulafi", "latitude": 5.83228, "longitude": 10.32771, "x": 83.9, "y": 23.7, "source": "geonames", "type": "PPL" }, { "name": "Munugeba", "latitude": 5.7533, "longitude": 10.25602, "x": 65.9, "y": 51.8, "source": "geonames", "type": "PPL" }, { "name": "Muwa", "latitude": 5.79038, "longitude": 10.14054, "x": 36.9, "y": 38.6, "source": "geonames", "type": "PPL" }, { "name": "Mve", "latitude": 5.64602, "longitude": 10.03362, "x": 10, "y": 89.8, "source": "geonames", "type": "PPLX" }, { "name": "Ngo", "latitude": 5.86337, "longitude": 10.26749, "x": 68.8, "y": 12.7, "source": "geonames", "type": "PPL" }, { "name": "Njong", "latitude": 5.82564, "longitude": 10.17369, "x": 45.2, "y": 26.1, "source": "geonames", "type": "PPL" }, { "name": "Nshielu", "latitude": 5.85595, "longitude": 10.21762, "x": 56.2, "y": 15.3, "source": "geonames", "type": "PPL" }, { "name": "Nte", "latitude": 5.77083, "longitude": 10.25434, "x": 65.5, "y": 45.5, "source": "geonames", "type": "PPL" }, { "name": "Papa", "latitude": 5.82606, "longitude": 10.15103, "x": 39.5, "y": 25.9, "source": "geonames", "type": "PPL" }, { "name": "Pinyin", "latitude": 5.78274, "longitude": 10.0572, "x": 15.9, "y": 41.3, "source": "geonames", "type": "PPL" }, { "name": "Santa Mbu", "latitude": 5.78716, "longitude": 10.15601, "x": 40.7, "y": 39.7, "source": "geonames", "type": "PPL" }, { "name": "Santa-Coffee", "latitude": 5.80341, "longitude": 10.21916, "x": 56.6, "y": 34, "source": "geonames", "type": "PPL" }, { "name": "Shingo", "latitude": 5.81453, "longitude": 10.35205, "x": 90, "y": 30, "source": "geonames", "type": "PPL" }, { "name": "Sosso", "latitude": 5.66667, "longitude": 10.08333, "x": 22.5, "y": 82.5, "source": "geonames", "type": "PPL" }, { "name": "Tapsetsa", "latitude": 5.72335, "longitude": 10.13526, "x": 35.5, "y": 62.4, "source": "geonames", "type": "PPL" }, { "name": "Tendjang", "latitude": 5.81005, "longitude": 10.26015, "x": 66.9, "y": 31.6, "source": "geonames", "type": "PPL" }, { "name": "Tenjam", "latitude": 5.85446, "longitude": 10.26186, "x": 67.3, "y": 15.8, "source": "geonames", "type": "PPL" }, { "name": "Tiogou", "latitude": 5.77494, "longitude": 10.31084, "x": 79.6, "y": 44.1, "source": "geonames", "type": "PPL" }, { "name": "Tochi", "latitude": 5.73925, "longitude": 10.19492, "x": 50.5, "y": 56.7, "source": "geonames", "type": "PPL" }, { "name": "Togou", "latitude": 5.7174, "longitude": 10.2037, "x": 52.7, "y": 64.5, "source": "geonames", "type": "PPL" } ], "Tchollire": [ { "name": "Douffing", "latitude": 8.3739, "longitude": 14.1474, "x": 38.1, "y": 52.9, "source": "geonames", "type": "PPL" }, { "name": "Dougon", "latitude": 8.3576, "longitude": 14.1308, "x": 34.2, "y": 57.4, "source": "geonames", "type": "PPL" }, { "name": "Gaba", "latitude": 8.3824, "longitude": 14.3701, "x": 90, "y": 50.5, "source": "geonames", "type": "PPL" }, { "name": "Gandi", "latitude": 8.3953, "longitude": 14.2146, "x": 53.8, "y": 47, "source": "geonames", "type": "PPL" }, { "name": "Home Garal", "latitude": 8.3455, "longitude": 14.1179, "x": 31.2, "y": 60.8, "source": "geonames", "type": "PPL" }, { "name": "Kali", "latitude": 8.3833, "longitude": 14.3423, "x": 83.5, "y": 50.3, "source": "geonames", "type": "PPL" }, { "name": "Kourouk", "latitude": 8.3833, "longitude": 14.3273, "x": 80, "y": 50.3, "source": "geonames", "type": "PPL" }, { "name": "Laboun", "latitude": 8.3926, "longitude": 14.2745, "x": 67.7, "y": 47.7, "source": "geonames", "type": "PPL" }, { "name": "Larki", "latitude": 8.5192, "longitude": 14.0269, "x": 10, "y": 12.6, "source": "geonames", "type": "PPL" }, { "name": "Maladi", "latitude": 8.46667, "longitude": 14.13333, "x": 34.8, "y": 27.2, "source": "geonames", "type": "PPL" }, { "name": "Maradi", "latitude": 8.51305, "longitude": 14.15397, "x": 39.6, "y": 14.3, "source": "geonames", "type": "PPL" }, { "name": "Mayo Galke", "latitude": 8.3976, "longitude": 14.2341, "x": 58.3, "y": 46.3, "source": "geonames", "type": "PPL" }, { "name": "Mbip", "latitude": 8.3027, "longitude": 14.0941, "x": 25.7, "y": 72.7, "source": "geonames", "type": "PPL" }, { "name": "Ndoukia", "latitude": 8.321, "longitude": 14.1029, "x": 27.7, "y": 67.6, "source": "geonames", "type": "PPL" }, { "name": "Ouafango", "latitude": 8.5285, "longitude": 14.1072, "x": 28.7, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Pani", "latitude": 8.41667, "longitude": 14.11667, "x": 30.9, "y": 41, "source": "geonames", "type": "PPL" }, { "name": "Reyna", "latitude": 8.3871, "longitude": 14.3069, "x": 75.3, "y": 49.2, "source": "geonames", "type": "PPL" }, { "name": "Taboun", "latitude": 8.2402, "longitude": 14.0476, "x": 14.8, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Tapare", "latitude": 8.50967, "longitude": 14.18994, "x": 48, "y": 15.2, "source": "geonames", "type": "PPL" }, { "name": "Yet", "latitude": 8.28262, "longitude": 14.08036, "x": 22.5, "y": 78.2, "source": "geonames", "type": "PPL" }, { "name": "Youkout", "latitude": 8.3005, "longitude": 14.0972, "x": 26.4, "y": 73.3, "source": "geonames", "type": "PPL" } ], "Tignere": [ { "name": "Bouroumti", "latitude": 7.46667, "longitude": 12.56667, "x": 25.2, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Garamti", "latitude": 7.46667, "longitude": 12.63333, "x": 40.5, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Karedje", "latitude": 7.4, "longitude": 12.83333, "x": 86.2, "y": 65, "source": "geonames", "type": "PPL" }, { "name": "Laore de Falkoumre", "latitude": 7.31667, "longitude": 12.6, "x": 32.9, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Mba Kana", "latitude": 7.46667, "longitude": 12.85, "x": 90, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Ouadjiri", "latitude": 7.53333, "longitude": 12.5, "x": 10, "y": 25, "source": "geonames", "type": "PPL" }, { "name": "Sambolabbo", "latitude": 7.58333, "longitude": 12.61667, "x": 36.7, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Tchabal", "latitude": 7.41667, "longitude": 12.81667, "x": 82.4, "y": 60, "source": "geonames", "type": "PPL" } ], "Tiko": [ { "name": "Akra Kombo", "latitude": 3.97376, "longitude": 9.458, "x": 64.2, "y": 63.6, "source": "geonames", "type": "PPL" }, { "name": "Besoukoudou", "latitude": 3.98722, "longitude": 9.4775, "x": 73, "y": 59.7, "source": "geonames", "type": "PPL" }, { "name": "Bessoukoudou", "latitude": 3.99283, "longitude": 9.44551, "x": 58.5, "y": 58.1, "source": "geonames", "type": "PPL" }, { "name": "Big Kombo", "latitude": 4.0279, "longitude": 9.4443, "x": 57.9, "y": 47.8, "source": "geonames", "type": "PPL" }, { "name": "Bokulang", "latitude": 4.1051, "longitude": 9.489, "x": 78.3, "y": 25.3, "source": "geonames", "type": "PPL" }, { "name": "Bwenga", "latitude": 4.0435, "longitude": 9.3392, "x": 10, "y": 43.3, "source": "geonames", "type": "PPL" }, { "name": "Damagaram Takaya", "latitude": 4.13333, "longitude": 9.48333, "x": 75.7, "y": 17.1, "source": "geonames", "type": "PPL" }, { "name": "Dou-la-Poka", "latitude": 4.0208, "longitude": 9.5008, "x": 83.7, "y": 49.9, "source": "geonames", "type": "PPL" }, { "name": "Ebonji", "latitude": 4.1, "longitude": 9.43333, "x": 52.9, "y": 26.8, "source": "geonames", "type": "PPL" }, { "name": "Grand Mboka", "latitude": 3.98333, "longitude": 9.46667, "x": 68.1, "y": 60.8, "source": "geonames", "type": "PPL" }, { "name": "Holtforth", "latitude": 4.065, "longitude": 9.357, "x": 18.1, "y": 37, "source": "geonames", "type": "PPL" }, { "name": "Ikange", "latitude": 4.095, "longitude": 9.4003, "x": 37.9, "y": 28.3, "source": "geonames", "type": "PPL" }, { "name": "Kangue", "latitude": 3.90911, "longitude": 9.34732, "x": 13.7, "y": 82.5, "source": "geonames", "type": "PPL" }, { "name": "Keme", "latitude": 4.1336, "longitude": 9.4673, "x": 68.4, "y": 17, "source": "geonames", "type": "PPL" }, { "name": "Kombo Mounja", "latitude": 4.037, "longitude": 9.492, "x": 79.7, "y": 45.2, "source": "geonames", "type": "PPL" }, { "name": "Kombo Njime", "latitude": 3.88333, "longitude": 9.45, "x": 60.5, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Kombo Pongo", "latitude": 4.0502, "longitude": 9.4891, "x": 78.3, "y": 41.3, "source": "geonames", "type": "PPL" }, { "name": "Konboa Ndonge", "latitude": 3.905, "longitude": 9.47694, "x": 72.8, "y": 83.7, "source": "geonames", "type": "PPL" }, { "name": "Manga", "latitude": 4.1114, "longitude": 9.4447, "x": 58.1, "y": 23.5, "source": "geonames", "type": "PPL" }, { "name": "Manga Samba", "latitude": 4.0792, "longitude": 9.4606, "x": 65.3, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Mange Masadi", "latitude": 3.97472, "longitude": 9.39333, "x": 34.7, "y": 63.4, "source": "geonames", "type": "PPL" }, { "name": "Matute", "latitude": 4.1132, "longitude": 9.422, "x": 47.7, "y": 23, "source": "geonames", "type": "PPL" }, { "name": "Missellele", "latitude": 4.1224, "longitude": 9.4449, "x": 58.2, "y": 20.3, "source": "geonames", "type": "PPL" }, { "name": "Modeka", "latitude": 4.1343, "longitude": 9.4981, "x": 82.4, "y": 16.8, "source": "geonames", "type": "PPL" }, { "name": "Moko", "latitude": 4.1577, "longitude": 9.4747, "x": 71.8, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Mokota", "latitude": 4.0709, "longitude": 9.4732, "x": 71.1, "y": 35.3, "source": "geonames", "type": "PPL" }, { "name": "Monkey Kombo", "latitude": 4.0569, "longitude": 9.3834, "x": 30.1, "y": 39.4, "source": "geonames", "type": "PPL" }, { "name": "Moudolo", "latitude": 3.9, "longitude": 9.48333, "x": 75.7, "y": 85.1, "source": "geonames", "type": "PPL" }, { "name": "Ndongo", "latitude": 4.0792, "longitude": 9.3483, "x": 14.1, "y": 32.9, "source": "geonames", "type": "PPL" }, { "name": "Ngombawasse", "latitude": 4.0581, "longitude": 9.507, "x": 86.5, "y": 39, "source": "geonames", "type": "PPL" }, { "name": "Ngoungou", "latitude": 3.98028, "longitude": 9.48472, "x": 76.3, "y": 61.7, "source": "geonames", "type": "PPL" }, { "name": "Njouma Songo", "latitude": 3.88333, "longitude": 9.43333, "x": 52.9, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Poka", "latitude": 3.99064, "longitude": 9.4729, "x": 70.9, "y": 58.7, "source": "geonames", "type": "PPL" }, { "name": "Pungo", "latitude": 4.1154, "longitude": 9.3993, "x": 37.4, "y": 22.3, "source": "geonames", "type": "PPL" }, { "name": "Small Kombo", "latitude": 4.0338, "longitude": 9.4878, "x": 77.7, "y": 46.1, "source": "geonames", "type": "PPL" }, { "name": "Sone", "latitude": 4.104, "longitude": 9.39, "x": 33.2, "y": 25.7, "source": "geonames", "type": "PPL" }, { "name": "Tende", "latitude": 3.9425, "longitude": 9.45, "x": 60.5, "y": 72.7, "source": "geonames", "type": "PPL" }, { "name": "Tengde", "latitude": 3.94593, "longitude": 9.44835, "x": 59.8, "y": 71.7, "source": "geonames", "type": "PPL" }, { "name": "Toube", "latitude": 3.89508, "longitude": 9.44419, "x": 57.9, "y": 86.6, "source": "geonames", "type": "PPL" }, { "name": "Toumbe", "latitude": 3.895, "longitude": 9.45694, "x": 63.7, "y": 86.6, "source": "geonames", "type": "PPL" }, { "name": "Wongue", "latitude": 4.0159, "longitude": 9.5147, "x": 90, "y": 51.3, "source": "geonames", "type": "PPL" } ], "Tibati": [ { "name": "Aba", "latitude": 6.41667, "longitude": 12.73333, "x": 73.7, "y": 50.6, "source": "geonames", "type": "PPL" }, { "name": "Batoure", "latitude": 6.38333, "longitude": 12.68333, "x": 63.3, "y": 58.5, "source": "geonames", "type": "PPL" }, { "name": "Betare Goni", "latitude": 6.57368, "longitude": 12.75171, "x": 77.6, "y": 13.6, "source": "geonames", "type": "PPL" }, { "name": "Beza", "latitude": 6.3368, "longitude": 12.79271, "x": 86.1, "y": 69.5, "source": "geonames", "type": "PPL" }, { "name": "Bimbal", "latitude": 6.37911, "longitude": 12.44098, "x": 12.9, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Bonga", "latitude": 6.48333, "longitude": 12.58333, "x": 42.5, "y": 34.9, "source": "geonames", "type": "PPL" }, { "name": "Boudjou Boudjou", "latitude": 6.5888, "longitude": 12.55501, "x": 36.6, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Buenanti", "latitude": 6.3735, "longitude": 12.75398, "x": 78, "y": 60.8, "source": "geonames", "type": "PPL" }, { "name": "Dang Haoussa", "latitude": 6.31761, "longitude": 12.5537, "x": 36.3, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Danyo", "latitude": 6.52307, "longitude": 12.66435, "x": 59.4, "y": 25.5, "source": "geonames", "type": "PPL" }, { "name": "Djaoro Bello", "latitude": 6.41896, "longitude": 12.59881, "x": 45.7, "y": 50.1, "source": "geonames", "type": "PPL" }, { "name": "Djaoro Mbatim", "latitude": 6.51238, "longitude": 12.44735, "x": 14.2, "y": 28, "source": "geonames", "type": "PPL" }, { "name": "Djaoro Ndo", "latitude": 6.43296, "longitude": 12.696, "x": 66, "y": 46.8, "source": "geonames", "type": "PPL" }, { "name": "Djaoro Pororo", "latitude": 6.57763, "longitude": 12.81137, "x": 90, "y": 12.6, "source": "geonames", "type": "PPL" }, { "name": "Djarya", "latitude": 6.56705, "longitude": 12.44483, "x": 13.7, "y": 15.1, "source": "geonames", "type": "PPL" }, { "name": "Garga", "latitude": 6.25, "longitude": 12.68333, "x": 63.3, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Killa Mandi", "latitude": 6.36667, "longitude": 12.75, "x": 77.2, "y": 62.5, "source": "geonames", "type": "PPL" }, { "name": "Larbay", "latitude": 6.50718, "longitude": 12.55549, "x": 36.7, "y": 29.3, "source": "geonames", "type": "PPL" }, { "name": "Lenin", "latitude": 6.36796, "longitude": 12.77049, "x": 81.5, "y": 62.1, "source": "geonames", "type": "PPL" }, { "name": "Madamdou", "latitude": 6.40789, "longitude": 12.52705, "x": 30.8, "y": 52.7, "source": "geonames", "type": "PPL" }, { "name": "Makodo", "latitude": 6.33333, "longitude": 12.61667, "x": 49.4, "y": 70.3, "source": "geonames", "type": "PPL" }, { "name": "Makondo", "latitude": 6.4388, "longitude": 12.6419, "x": 54.7, "y": 45.4, "source": "geonames", "type": "PPL" }, { "name": "Malandi", "latitude": 6.50468, "longitude": 12.51046, "x": 27.3, "y": 29.9, "source": "geonames", "type": "PPL" }, { "name": "Malarba", "latitude": 6.5252, "longitude": 12.61533, "x": 49.2, "y": 25, "source": "geonames", "type": "PPL" }, { "name": "Malidja", "latitude": 6.56448, "longitude": 12.73836, "x": 74.8, "y": 15.7, "source": "geonames", "type": "PPL" }, { "name": "Manbarla", "latitude": 6.39223, "longitude": 12.57797, "x": 41.4, "y": 56.4, "source": "geonames", "type": "PPL" }, { "name": "Mangari", "latitude": 6.51667, "longitude": 12.55, "x": 35.6, "y": 27, "source": "geonames", "type": "PPL" }, { "name": "Maounde", "latitude": 6.41667, "longitude": 12.66667, "x": 59.9, "y": 50.6, "source": "geonames", "type": "PPL" }, { "name": "Maour", "latitude": 6.51761, "longitude": 12.70615, "x": 68.1, "y": 26.8, "source": "geonames", "type": "PPL" }, { "name": "Mbibale", "latitude": 6.40747, "longitude": 12.61473, "x": 49, "y": 52.8, "source": "geonames", "type": "PPL" }, { "name": "Mbifoui", "latitude": 6.44073, "longitude": 12.60861, "x": 47.8, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Mbipoy", "latitude": 6.43512, "longitude": 12.61004, "x": 48.1, "y": 46.3, "source": "geonames", "type": "PPL" }, { "name": "Mbon Ha", "latitude": 6.48429, "longitude": 12.58178, "x": 42.2, "y": 34.7, "source": "geonames", "type": "PPL" }, { "name": "Medjanba", "latitude": 6.3462, "longitude": 12.55809, "x": 37.2, "y": 67.3, "source": "geonames", "type": "PPL" }, { "name": "Megami", "latitude": 6.38333, "longitude": 12.43333, "x": 11.3, "y": 58.5, "source": "geonames", "type": "PPL" }, { "name": "Mengeme", "latitude": 6.41327, "longitude": 12.55764, "x": 37.2, "y": 51.4, "source": "geonames", "type": "PPL" }, { "name": "Meseki", "latitude": 6.52977, "longitude": 12.51019, "x": 27.3, "y": 23.9, "source": "geonames", "type": "PPL" }, { "name": "Ndjoa", "latitude": 6.50829, "longitude": 12.45866, "x": 16.5, "y": 29, "source": "geonames", "type": "PPL" }, { "name": "Ndouyena", "latitude": 6.49476, "longitude": 12.55666, "x": 37, "y": 32.2, "source": "geonames", "type": "PPL" }, { "name": "Ndoyo", "latitude": 6.3445, "longitude": 12.77528, "x": 82.5, "y": 67.7, "source": "geonames", "type": "PPL" }, { "name": "Ngaoubela", "latitude": 6.52405, "longitude": 12.59986, "x": 45.9, "y": 25.3, "source": "geonames", "type": "PPL" }, { "name": "Ngat", "latitude": 6.57359, "longitude": 12.80073, "x": 87.8, "y": 13.6, "source": "geonames", "type": "PPL" }, { "name": "Ngati", "latitude": 6.52362, "longitude": 12.63484, "x": 53.2, "y": 25.4, "source": "geonames", "type": "PPL" }, { "name": "Nyende", "latitude": 6.38982, "longitude": 12.48907, "x": 22.9, "y": 57, "source": "geonames", "type": "PPL" }, { "name": "Sirki Samari", "latitude": 6.5176, "longitude": 12.42726, "x": 10, "y": 26.8, "source": "geonames", "type": "PPL" }, { "name": "Sola", "latitude": 6.30965, "longitude": 12.55176, "x": 35.9, "y": 75.9, "source": "geonames", "type": "PPL" }, { "name": "Sola-Bitom", "latitude": 6.26526, "longitude": 12.53759, "x": 33, "y": 86.4, "source": "geonames", "type": "PPL" }, { "name": "Soule", "latitude": 6.36667, "longitude": 12.71667, "x": 70.3, "y": 62.5, "source": "geonames", "type": "PPL" }, { "name": "Wandin", "latitude": 6.34032, "longitude": 12.78133, "x": 83.7, "y": 68.7, "source": "geonames", "type": "PPL" } ], "Tombel": [ { "name": "Atop", "latitude": 4.8275, "longitude": 9.6374, "x": 57.7, "y": 39.2, "source": "geonames", "type": "PPL" }, { "name": "Bambele", "latitude": 4.7968, "longitude": 9.5891, "x": 32.9, "y": 53.1, "source": "geonames", "type": "PPL" }, { "name": "Bekume", "latitude": 4.8752, "longitude": 9.6697, "x": 74.2, "y": 17.6, "source": "geonames", "type": "PPL" }, { "name": "Bulutu", "latitude": 4.7841, "longitude": 9.6026, "x": 39.8, "y": 58.9, "source": "geonames", "type": "PPL" }, { "name": "Ebonji", "latitude": 4.7245, "longitude": 9.6049, "x": 41, "y": 85.9, "source": "geonames", "type": "PPL" }, { "name": "Ebubu", "latitude": 4.7154, "longitude": 9.6615, "x": 70, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ebul", "latitude": 4.7481, "longitude": 9.6247, "x": 51.1, "y": 75.2, "source": "geonames", "type": "PPL" }, { "name": "Ehom", "latitude": 4.748, "longitude": 9.6071, "x": 42.1, "y": 75.2, "source": "geonames", "type": "PPL" }, { "name": "Etam", "latitude": 4.72047, "longitude": 9.54455, "x": 10, "y": 87.7, "source": "geonames", "type": "PPL" }, { "name": "Kupe", "latitude": 4.7571, "longitude": 9.6854, "x": 82.3, "y": 71.1, "source": "geonames", "type": "PPL" }, { "name": "Mahole", "latitude": 4.8194, "longitude": 9.6071, "x": 42.1, "y": 42.9, "source": "geonames", "type": "PPL" }, { "name": "Mbabe", "latitude": 4.8147, "longitude": 9.6534, "x": 65.9, "y": 45, "source": "geonames", "type": "PPL" }, { "name": "Mbong", "latitude": 4.7827, "longitude": 9.6529, "x": 65.6, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Mbule", "latitude": 4.8039, "longitude": 9.6687, "x": 73.7, "y": 49.9, "source": "geonames", "type": "PPL" }, { "name": "Mekedembeng", "latitude": 4.89209, "longitude": 9.6774, "x": 78.2, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Mpako", "latitude": 4.865, "longitude": 9.7004, "x": 90, "y": 22.3, "source": "geonames", "type": "PPL" }, { "name": "Mundum", "latitude": 4.8519, "longitude": 9.6841, "x": 81.6, "y": 28.2, "source": "geonames", "type": "PPL" }, { "name": "Ndissi", "latitude": 4.8604, "longitude": 9.6462, "x": 62.2, "y": 24.3, "source": "geonames", "type": "PPL" }, { "name": "Ngab", "latitude": 4.7675, "longitude": 9.6593, "x": 68.9, "y": 66.4, "source": "geonames", "type": "PPL" }, { "name": "Ngussi", "latitude": 4.851, "longitude": 9.6573, "x": 67.9, "y": 28.6, "source": "geonames", "type": "PPL" }, { "name": "Nsuke", "latitude": 4.7915, "longitude": 9.6541, "x": 66.2, "y": 55.5, "source": "geonames", "type": "PPL" }, { "name": "Nyassosso", "latitude": 4.8277, "longitude": 9.6811, "x": 80.1, "y": 39.2, "source": "geonames", "type": "PPL" }, { "name": "Peng", "latitude": 4.736, "longitude": 9.629, "x": 53.3, "y": 80.7, "source": "geonames", "type": "PPL" } ], "Waza": [ { "name": "Alapitari", "latitude": 11.40639, "longitude": 14.5675, "x": 51.3, "y": 61.4, "source": "geonames", "type": "PPL" }, { "name": "Am Talia", "latitude": 11.39028, "longitude": 14.49963, "x": 37.2, "y": 65.7, "source": "geonames", "type": "PPL" }, { "name": "Ardori", "latitude": 11.33111, "longitude": 14.36957, "x": 10, "y": 81.6, "source": "geonames", "type": "PPL" }, { "name": "Barkala", "latitude": 11.37713, "longitude": 14.46676, "x": 30.3, "y": 69.3, "source": "geonames", "type": "PPL" }, { "name": "Bladeri", "latitude": 11.36571, "longitude": 14.39652, "x": 15.6, "y": 72.3, "source": "geonames", "type": "PPL" }, { "name": "Bounderi", "latitude": 11.34714, "longitude": 14.40957, "x": 18.4, "y": 77.3, "source": "geonames", "type": "PPL" }, { "name": "Doudou-Ndiyam", "latitude": 11.50351, "longitude": 14.75278, "x": 90, "y": 35.3, "source": "geonames", "type": "PPL" }, { "name": "Farfati", "latitude": 11.41694, "longitude": 14.46, "x": 28.9, "y": 58.6, "source": "geonames", "type": "PPL" }, { "name": "Galma", "latitude": 11.36459, "longitude": 14.42554, "x": 21.7, "y": 72.6, "source": "geonames", "type": "PPL" }, { "name": "Garou", "latitude": 11.3975, "longitude": 14.55528, "x": 48.8, "y": 63.8, "source": "geonames", "type": "PPL" }, { "name": "Gouloundouma", "latitude": 11.50278, "longitude": 14.72679, "x": 84.6, "y": 35.5, "source": "geonames", "type": "PPL" }, { "name": "Goulouri", "latitude": 11.38084, "longitude": 14.41832, "x": 20.2, "y": 68.3, "source": "geonames", "type": "PPL" }, { "name": "kangleri", "latitude": 11.3, "longitude": 14.38333, "x": 12.9, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Karakwy", "latitude": 11.50938, "longitude": 14.61432, "x": 61.1, "y": 33.7, "source": "geonames", "type": "PPL" }, { "name": "Kaza", "latitude": 11.36398, "longitude": 14.44231, "x": 25.2, "y": 72.8, "source": "geonames", "type": "PPL" }, { "name": "Khwaya", "latitude": 11.39028, "longitude": 14.64056, "x": 66.6, "y": 65.7, "source": "geonames", "type": "PPL" }, { "name": "Kolossale", "latitude": 11.33583, "longitude": 14.38583, "x": 13.4, "y": 80.4, "source": "geonames", "type": "PPL" }, { "name": "Lareski", "latitude": 11.39269, "longitude": 14.57259, "x": 52.4, "y": 65.1, "source": "geonames", "type": "PPL" }, { "name": "Lawa", "latitude": 11.36167, "longitude": 14.37555, "x": 11.2, "y": 73.4, "source": "geonames", "type": "PPL" }, { "name": "Mada", "latitude": 11.54556, "longitude": 14.73861, "x": 87, "y": 24, "source": "geonames", "type": "PPL" }, { "name": "Mbague", "latitude": 11.4982, "longitude": 14.69708, "x": 78.4, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Ndiguina", "latitude": 11.48724, "longitude": 14.67763, "x": 74.3, "y": 39.6, "source": "geonames", "type": "PPL" }, { "name": "Oundjoumba", "latitude": 11.37959, "longitude": 14.39249, "x": 14.8, "y": 68.6, "source": "geonames", "type": "PPL" }, { "name": "Salra", "latitude": 11.5975, "longitude": 14.65417, "x": 69.4, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Tagawa", "latitude": 11.31806, "longitude": 14.44346, "x": 25.4, "y": 85.1, "source": "geonames", "type": "PPL" }, { "name": "Touchki", "latitude": 11.3568, "longitude": 14.42556, "x": 21.7, "y": 74.7, "source": "geonames", "type": "PPL" } ], "Wum": [ { "name": "Abunde", "latitude": 6.3501, "longitude": 9.856, "x": 10.7, "y": 62.2, "source": "geonames", "type": "PPL" }, { "name": "Atvem", "latitude": 6.40875, "longitude": 9.96217, "x": 33, "y": 48.1, "source": "geonames", "type": "PPL" }, { "name": "Badu", "latitude": 6.4684, "longitude": 9.8848, "x": 16.8, "y": 33.7, "source": "geonames", "type": "PPL" }, { "name": "Befang", "latitude": 6.30731, "longitude": 9.98043, "x": 36.9, "y": 72.5, "source": "geonames", "type": "PPL" }, { "name": "Benabandjo", "latitude": 6.3644, "longitude": 9.8722, "x": 14.1, "y": 58.8, "source": "geonames", "type": "PPL" }, { "name": "Benabo", "latitude": 6.3906, "longitude": 9.909, "x": 21.9, "y": 52.5, "source": "geonames", "type": "PPL" }, { "name": "Benade", "latitude": 6.4517, "longitude": 9.8526, "x": 10, "y": 37.7, "source": "geonames", "type": "PPL" }, { "name": "Benahundu", "latitude": 6.4258, "longitude": 9.8867, "x": 17.2, "y": 44, "source": "geonames", "type": "PPL" }, { "name": "Benakuma", "latitude": 6.414, "longitude": 9.9118, "x": 22.4, "y": 46.8, "source": "geonames", "type": "PPL" }, { "name": "Benatambe", "latitude": 6.3897, "longitude": 9.8633, "x": 12.2, "y": 52.7, "source": "geonames", "type": "PPL" }, { "name": "Benatomo", "latitude": 6.4431, "longitude": 9.8943, "x": 18.8, "y": 39.8, "source": "geonames", "type": "PPL" }, { "name": "Bewentang", "latitude": 6.3493, "longitude": 9.956, "x": 31.7, "y": 62.4, "source": "geonames", "type": "PPL" }, { "name": "Bondong", "latitude": 6.3784, "longitude": 9.9691, "x": 34.5, "y": 55.4, "source": "geonames", "type": "PPL" }, { "name": "Bugri", "latitude": 6.2557, "longitude": 9.9862, "x": 38.1, "y": 85, "source": "geonames", "type": "PPL" }, { "name": "Bwegam", "latitude": 6.34875, "longitude": 9.9082, "x": 21.7, "y": 62.5, "source": "geonames", "type": "PPL" }, { "name": "Bwotong", "latitude": 6.3306, "longitude": 9.9595, "x": 32.5, "y": 66.9, "source": "geonames", "type": "PPL" }, { "name": "Cha", "latitude": 6.46667, "longitude": 10.23333, "x": 90, "y": 34.1, "source": "geonames", "type": "PPL" }, { "name": "Essu", "latitude": 6.56667, "longitude": 10.08333, "x": 58.5, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Eteoda", "latitude": 6.43562, "longitude": 9.86403, "x": 12.4, "y": 41.6, "source": "geonames", "type": "PPL" }, { "name": "Fungom", "latitude": 6.53333, "longitude": 10.2, "x": 83, "y": 18, "source": "geonames", "type": "PPL" }, { "name": "Koaw", "latitude": 6.33333, "longitude": 10.06667, "x": 55, "y": 66.3, "source": "geonames", "type": "PPL" }, { "name": "Kong", "latitude": 6.55, "longitude": 10.21667, "x": 86.5, "y": 14, "source": "geonames", "type": "PPL" }, { "name": "Kuk", "latitude": 6.38333, "longitude": 10.16667, "x": 76, "y": 54.2, "source": "geonames", "type": "PPL" }, { "name": "Kumfutu", "latitude": 6.48333, "longitude": 10.2, "x": 83, "y": 30.1, "source": "geonames", "type": "PPL" }, { "name": "Mbakong", "latitude": 6.26667, "longitude": 10.03333, "x": 48, "y": 82.3, "source": "geonames", "type": "PPL" }, { "name": "Mbguri", "latitude": 6.25, "longitude": 9.95, "x": 30.5, "y": 86.4, "source": "geonames", "type": "PPL" }, { "name": "Modele", "latitude": 6.35421, "longitude": 9.96836, "x": 34.3, "y": 61.2, "source": "geonames", "type": "PPL" }, { "name": "Modelle", "latitude": 6.40255, "longitude": 9.98076, "x": 36.9, "y": 49.6, "source": "geonames", "type": "PPL" }, { "name": "Mudum II", "latitude": 6.2349, "longitude": 9.9457, "x": 29.6, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Mukura", "latitude": 6.32535, "longitude": 9.95424, "x": 31.4, "y": 68.2, "source": "geonames", "type": "PPL" }, { "name": "Mukuru", "latitude": 6.3335, "longitude": 9.9605, "x": 32.7, "y": 66.2, "source": "geonames", "type": "PPL" }, { "name": "Mven", "latitude": 6.53333, "longitude": 10.2, "x": 83, "y": 18, "source": "geonames", "type": "PPL" }, { "name": "Ntenkissu", "latitude": 6.31667, "longitude": 10.16667, "x": 76, "y": 70.3, "source": "geonames", "type": "PPL" }, { "name": "Okoromanjang", "latitude": 6.2915, "longitude": 9.9225, "x": 24.7, "y": 76.4, "source": "geonames", "type": "PPL" }, { "name": "Owundu", "latitude": 6.41, "longitude": 9.8801, "x": 15.8, "y": 47.8, "source": "geonames", "type": "PPL" }, { "name": "Shishong", "latitude": 6.2372, "longitude": 9.9047, "x": 20.9, "y": 89.4, "source": "geonames", "type": "PPL" }, { "name": "Zoa", "latitude": 6.53333, "longitude": 10.16667, "x": 76, "y": 18, "source": "geonames", "type": "PPL" } ], "Yabassi": [ { "name": "Bangseng", "latitude": 4.4184, "longitude": 9.7755, "x": 10, "y": 57.7, "source": "geonames", "type": "PPL" }, { "name": "Banya II", "latitude": 4.5755, "longitude": 9.9646, "x": 52.3, "y": 27.5, "source": "geonames", "type": "PPL" }, { "name": "Bewang I", "latitude": 4.58333, "longitude": 10.08333, "x": 78.8, "y": 26, "source": "geonames", "type": "PPL" }, { "name": "Bobok", "latitude": 4.5673, "longitude": 9.8792, "x": 33.2, "y": 29.1, "source": "geonames", "type": "PPL" }, { "name": "Bonabeke", "latitude": 4.4699, "longitude": 9.9713, "x": 53.8, "y": 47.8, "source": "geonames", "type": "PPL" }, { "name": "Bonabondo", "latitude": 4.4217, "longitude": 9.9016, "x": 38.2, "y": 57, "source": "geonames", "type": "PPL" }, { "name": "Bonakata", "latitude": 4.4252, "longitude": 9.9322, "x": 45, "y": 56.4, "source": "geonames", "type": "PPL" }, { "name": "Bonalambo", "latitude": 4.3291, "longitude": 9.8353, "x": 23.4, "y": 74.8, "source": "geonames", "type": "PPL" }, { "name": "Bonalembe", "latitude": 4.4359, "longitude": 9.9463, "x": 48.2, "y": 54.3, "source": "geonames", "type": "PPL" }, { "name": "Bonalongso", "latitude": 4.55, "longitude": 10.01667, "x": 63.9, "y": 32.4, "source": "geonames", "type": "PPL" }, { "name": "Bonamakita", "latitude": 4.3373, "longitude": 9.8464, "x": 25.9, "y": 73.2, "source": "geonames", "type": "PPL" }, { "name": "Bonambassi", "latitude": 4.3155, "longitude": 9.8392, "x": 24.2, "y": 77.4, "source": "geonames", "type": "PPL" }, { "name": "Bonandjeng", "latitude": 4.4123, "longitude": 9.8972, "x": 37.2, "y": 58.8, "source": "geonames", "type": "PPL" }, { "name": "Bonandjoa", "latitude": 4.2995, "longitude": 9.8287, "x": 21.9, "y": 80.5, "source": "geonames", "type": "PPL" }, { "name": "Bonandolo", "latitude": 4.3543, "longitude": 9.8638, "x": 29.7, "y": 70, "source": "geonames", "type": "PPL" }, { "name": "Bonanga", "latitude": 4.3839, "longitude": 9.8735, "x": 31.9, "y": 64.3, "source": "geonames", "type": "PPL" }, { "name": "Bonangole", "latitude": 4.43333, "longitude": 9.96667, "x": 52.7, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Bonanyama", "latitude": 4.4086, "longitude": 9.902, "x": 38.3, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Bonanyamalimbe", "latitude": 4.4181, "longitude": 9.9182, "x": 41.9, "y": 57.7, "source": "geonames", "type": "PPL" }, { "name": "Bonanyamsi", "latitude": 4.3924, "longitude": 9.8759, "x": 32.4, "y": 62.7, "source": "geonames", "type": "PPL" }, { "name": "Bonapende", "latitude": 4.4097, "longitude": 9.8836, "x": 34.2, "y": 59.3, "source": "geonames", "type": "PPL" }, { "name": "Bonekoule", "latitude": 4.333, "longitude": 9.8512, "x": 26.9, "y": 74.1, "source": "geonames", "type": "PPL" }, { "name": "Boneloko", "latitude": 4.5028, "longitude": 9.9776, "x": 55.2, "y": 41.5, "source": "geonames", "type": "PPL" }, { "name": "Bonikwe", "latitude": 4.36667, "longitude": 9.88333, "x": 34.1, "y": 67.6, "source": "geonames", "type": "PPL" }, { "name": "Bwamba", "latitude": 4.4019, "longitude": 9.8737, "x": 32, "y": 60.8, "source": "geonames", "type": "PPL" }, { "name": "Dene", "latitude": 4.3049, "longitude": 9.8382, "x": 24, "y": 79.5, "source": "geonames", "type": "PPL" }, { "name": "Diang", "latitude": 4.25, "longitude": 10.01667, "x": 63.9, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Dibombe", "latitude": 4.36314, "longitude": 9.86534, "x": 30.1, "y": 68.3, "source": "geonames", "type": "PPL" }, { "name": "Dimbong", "latitude": 4.5785, "longitude": 9.8684, "x": 30.8, "y": 26.9, "source": "geonames", "type": "PPL" }, { "name": "Gongolo", "latitude": 4.31667, "longitude": 9.95, "x": 49, "y": 77.2, "source": "geonames", "type": "PPL" }, { "name": "Koum", "latitude": 4.4583, "longitude": 9.7855, "x": 12.2, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Longtoka", "latitude": 4.4884, "longitude": 9.9746, "x": 54.5, "y": 44.2, "source": "geonames", "type": "PPL" }, { "name": "Mabobe", "latitude": 4.6306, "longitude": 9.9293, "x": 44.4, "y": 16.9, "source": "geonames", "type": "PPL" }, { "name": "Mandiba", "latitude": 4.3658, "longitude": 9.8783, "x": 33, "y": 67.8, "source": "geonames", "type": "PPL" }, { "name": "Matong", "latitude": 4.48333, "longitude": 9.78333, "x": 11.8, "y": 45.2, "source": "geonames", "type": "PPL" }, { "name": "Mbanga Mpongo", "latitude": 4.66667, "longitude": 9.95, "x": 49, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Mbanjako", "latitude": 4.43333, "longitude": 9.95, "x": 49, "y": 54.8, "source": "geonames", "type": "PPL" }, { "name": "Mpobo", "latitude": 4.43944, "longitude": 9.78417, "x": 11.9, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Ndobeginge", "latitude": 4.35, "longitude": 10.13333, "x": 90, "y": 70.8, "source": "geonames", "type": "PPL" }, { "name": "Ndokak", "latitude": 4.41667, "longitude": 10.08333, "x": 78.8, "y": 58, "source": "geonames", "type": "PPL" }, { "name": "Ndokati", "latitude": 4.6, "longitude": 10.06667, "x": 75.1, "y": 22.8, "source": "geonames", "type": "PPL" }, { "name": "Ndokbele", "latitude": 4.4434, "longitude": 9.9804, "x": 55.8, "y": 52.9, "source": "geonames", "type": "PPL" }, { "name": "Ndokbong", "latitude": 4.5114, "longitude": 9.9522, "x": 49.5, "y": 39.8, "source": "geonames", "type": "PPL" }, { "name": "Ndokiamen", "latitude": 4.5283, "longitude": 9.9381, "x": 46.4, "y": 36.6, "source": "geonames", "type": "PPL" }, { "name": "Ndokoko", "latitude": 4.5, "longitude": 10.01667, "x": 63.9, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Ndokomondo", "latitude": 4.36667, "longitude": 10.01667, "x": 63.9, "y": 67.6, "source": "geonames", "type": "PPL" }, { "name": "Ndokpenda", "latitude": 4.5444, "longitude": 9.9093, "x": 39.9, "y": 33.5, "source": "geonames", "type": "PPL" }, { "name": "Ndokpo", "latitude": 4.5502, "longitude": 9.9402, "x": 46.8, "y": 32.4, "source": "geonames", "type": "PPL" }, { "name": "Ndoksom", "latitude": 4.3274, "longitude": 9.9949, "x": 59.1, "y": 75.1, "source": "geonames", "type": "PPL" }, { "name": "Nkokem", "latitude": 4.5442, "longitude": 9.8808, "x": 33.5, "y": 33.5, "source": "geonames", "type": "PPL" }, { "name": "Nkongmalan", "latitude": 4.5344, "longitude": 9.9937, "x": 58.8, "y": 35.4, "source": "geonames", "type": "PPL" }, { "name": "Nsake", "latitude": 4.4032, "longitude": 9.8967, "x": 37.1, "y": 60.6, "source": "geonames", "type": "PPL" }, { "name": "Sok", "latitude": 4.26667, "longitude": 10.08333, "x": 78.8, "y": 86.8, "source": "geonames", "type": "PPL" }, { "name": "Timte", "latitude": 4.53333, "longitude": 10.08333, "x": 78.8, "y": 35.6, "source": "geonames", "type": "PPL" }, { "name": "Yakouan", "latitude": 4.55, "longitude": 10.06667, "x": 75.1, "y": 32.4, "source": "geonames", "type": "PPL" } ], "Yagoua": [ { "name": "Agolna", "latitude": 10.389, "longitude": 15.16942, "x": 35.3, "y": 40.9, "source": "geonames", "type": "PPL" }, { "name": "Babaye", "latitude": 10.16667, "longitude": 15.21667, "x": 46.4, "y": 87, "source": "geonames", "type": "PPL" }, { "name": "Bagarao", "latitude": 10.38403, "longitude": 15.10082, "x": 19.1, "y": 42, "source": "geonames", "type": "PPL" }, { "name": "Balamata", "latitude": 10.22935, "longitude": 15.3207, "x": 70.9, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Bangana", "latitude": 10.20102, "longitude": 15.31973, "x": 70.7, "y": 79.9, "source": "geonames", "type": "PPL" }, { "name": "Bougay", "latitude": 10.25645, "longitude": 15.11022, "x": 21.4, "y": 68.4, "source": "geonames", "type": "PPL" }, { "name": "Dana", "latitude": 10.2362, "longitude": 15.29501, "x": 64.8, "y": 72.6, "source": "geonames", "type": "PPL" }, { "name": "Dangabissi", "latitude": 10.16657, "longitude": 15.31419, "x": 69.4, "y": 87, "source": "geonames", "type": "PPL" }, { "name": "Diguisi", "latitude": 10.26667, "longitude": 15.33333, "x": 73.9, "y": 66.3, "source": "geonames", "type": "PPL" }, { "name": "Djokoydi", "latitude": 10.35138, "longitude": 15.28517, "x": 62.5, "y": 48.7, "source": "geonames", "type": "PPL" }, { "name": "Domo", "latitude": 10.15205, "longitude": 15.24153, "x": 52.3, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Doriessou", "latitude": 10.53839, "longitude": 15.1335, "x": 26.8, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Gabarey Merengue", "latitude": 10.49593, "longitude": 15.07608, "x": 13.3, "y": 18.8, "source": "geonames", "type": "PPL" }, { "name": "Gabarey Widi", "latitude": 10.4503, "longitude": 15.11248, "x": 21.9, "y": 28.2, "source": "geonames", "type": "PPL" }, { "name": "Gamdama", "latitude": 10.29689, "longitude": 15.23959, "x": 51.8, "y": 60, "source": "geonames", "type": "PPL" }, { "name": "Golon Keke", "latitude": 10.36242, "longitude": 15.08481, "x": 15.4, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Golopo", "latitude": 10.21926, "longitude": 15.06196, "x": 10, "y": 76.1, "source": "geonames", "type": "PPL" }, { "name": "Goufka", "latitude": 10.18333, "longitude": 15.25, "x": 54.3, "y": 83.5, "source": "geonames", "type": "PPL" }, { "name": "Gueme", "latitude": 10.50389, "longitude": 15.17658, "x": 37, "y": 17.1, "source": "geonames", "type": "PPL" }, { "name": "Guinane I", "latitude": 10.19232, "longitude": 15.09763, "x": 18.4, "y": 81.7, "source": "geonames", "type": "PPL" }, { "name": "Irdeng", "latitude": 10.27735, "longitude": 15.35039, "x": 77.9, "y": 64.1, "source": "geonames", "type": "PPL" }, { "name": "Kertchem", "latitude": 10.28333, "longitude": 15.31667, "x": 69.9, "y": 62.8, "source": "geonames", "type": "PPL" }, { "name": "Korokoro", "latitude": 10.25, "longitude": 15.13333, "x": 26.8, "y": 69.7, "source": "geonames", "type": "PPL" }, { "name": "Marao", "latitude": 10.38333, "longitude": 15.25, "x": 54.3, "y": 42.1, "source": "geonames", "type": "PPL" }, { "name": "Miogoy", "latitude": 10.19188, "longitude": 15.4019, "x": 90, "y": 81.8, "source": "geonames", "type": "PPL" }, { "name": "Mogorfo", "latitude": 10.45, "longitude": 15.15, "x": 30.7, "y": 28.3, "source": "geonames", "type": "PPL" }, { "name": "Mouri I", "latitude": 10.19751, "longitude": 15.27834, "x": 60.9, "y": 80.6, "source": "geonames", "type": "PPL" }, { "name": "Nataye", "latitude": 10.25, "longitude": 15.31667, "x": 69.9, "y": 69.7, "source": "geonames", "type": "PPL" }, { "name": "Noultourgaye", "latitude": 10.48333, "longitude": 15.08333, "x": 15, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Oudonday", "latitude": 10.22947, "longitude": 15.11453, "x": 22.4, "y": 74, "source": "geonames", "type": "PPL" }, { "name": "Poutiki", "latitude": 10.3, "longitude": 15.16667, "x": 34.6, "y": 59.4, "source": "geonames", "type": "PPL" }, { "name": "Souana", "latitude": 10.18333, "longitude": 15.31667, "x": 69.9, "y": 83.5, "source": "geonames", "type": "PPL" }, { "name": "Tcherfeke", "latitude": 10.23537, "longitude": 15.18002, "x": 37.8, "y": 72.7, "source": "geonames", "type": "PPL" }, { "name": "Toukou", "latitude": 10.42591, "longitude": 15.20895, "x": 44.6, "y": 33.3, "source": "geonames", "type": "PPL" }, { "name": "Vata", "latitude": 10.41667, "longitude": 15.2, "x": 42.5, "y": 35.2, "source": "geonames", "type": "PPL" }, { "name": "Vele", "latitude": 10.51088, "longitude": 15.15368, "x": 31.6, "y": 15.7, "source": "geonames", "type": "PPL" }, { "name": "Wina", "latitude": 10.15361, "longitude": 15.22083, "x": 47.4, "y": 89.7, "source": "geonames", "type": "PPL" }, { "name": "Yamtoka", "latitude": 10.47495, "longitude": 15.18868, "x": 39.8, "y": 23.1, "source": "geonames", "type": "PPL" }, { "name": "Zebe", "latitude": 10.33027, "longitude": 15.26845, "x": 58.6, "y": 53.1, "source": "geonames", "type": "PPL" }, { "name": "Zoulla", "latitude": 10.38386, "longitude": 15.27273, "x": 59.6, "y": 42, "source": "geonames", "type": "PPL" } ], "Yaounde": [ { "name": "Abom", "latitude": 3.8768267, "longitude": 11.5667786, "x": 76, "y": 44.5, "source": "osm", "type": "locality" }, { "name": "Abom Plateau", "latitude": 3.8773162, "longitude": 11.5684375, "x": 76.7, "y": 44.4, "source": "osm", "type": "locality" }, { "name": "Afeme Nord 1", "latitude": 3.8943965, "longitude": 11.4665261, "x": 29.9, "y": 38.8, "source": "osm", "type": "locality" }, { "name": "Afeme Nord 2", "latitude": 3.894803, "longitude": 11.4678684, "x": 30.5, "y": 38.7, "source": "osm", "type": "locality" }, { "name": "Ahala Centre", "latitude": 3.808109, "longitude": 11.5010636, "x": 45.8, "y": 67, "source": "osm", "type": "locality" }, { "name": "Akok Ndoe", "latitude": 3.8569179, "longitude": 11.4567922, "x": 25.4, "y": 51, "source": "osm", "type": "locality" }, { "name": "Akok Ndoe 1", "latitude": 3.8528562, "longitude": 11.469602, "x": 31.3, "y": 52.4, "source": "osm", "type": "locality" }, { "name": "Akok Ndoe 2", "latitude": 3.8498644, "longitude": 11.4593599, "x": 26.6, "y": 53.3, "source": "osm", "type": "locality" }, { "name": "Ancien Stationnement de Douala", "latitude": 3.85895, "longitude": 11.521481, "x": 55.2, "y": 50.4, "source": "osm", "type": "locality" }, { "name": "Ancienne Foire de Tsinga", "latitude": 3.896229, "longitude": 11.4978258, "x": 44.3, "y": 38.2, "source": "osm", "type": "locality" }, { "name": "Ayene", "latitude": 3.8411969, "longitude": 11.519387, "x": 54.2, "y": 56.2, "source": "osm", "type": "neighbourhood" }, { "name": "Baaba", "latitude": 3.8786236, "longitude": 11.5703524, "x": 77.6, "y": 43.9, "source": "osm", "type": "locality" }, { "name": "Bilik", "latitude": 3.9313372, "longitude": 11.5098999, "x": 49.8, "y": 26.7, "source": "osm", "type": "locality" }, { "name": "Boumnyebel", "latitude": 3.8622547, "longitude": 11.5590057, "x": 72.4, "y": 49.3, "source": "osm", "type": "locality" }, { "name": "Camp Sic Messa", "latitude": 3.871539, "longitude": 11.5068765, "x": 48.4, "y": 46.3, "source": "osm", "type": "locality" }, { "name": "Camp Sic Nlongkak", "latitude": 3.8916389, "longitude": 11.5245002, "x": 56.5, "y": 39.7, "source": "osm", "type": "locality" }, { "name": "Camp Sonel", "latitude": 3.8770787, "longitude": 11.5447358, "x": 65.8, "y": 44.4, "source": "osm", "type": "locality" }, { "name": "Carefour Petit Terrain", "latitude": 3.8789867, "longitude": 11.4637163, "x": 28.6, "y": 43.8, "source": "osm", "type": "locality" }, { "name": "Carrefour Abbia", "latitude": 3.8690216, "longitude": 11.5170773, "x": 53.1, "y": 47.1, "source": "osm", "type": "locality" }, { "name": "Carrefour Accacia", "latitude": 3.8409228, "longitude": 11.4886019, "x": 40.1, "y": 56.3, "source": "osm", "type": "locality" }, { "name": "Carrefour Amadou", "latitude": 3.8328954, "longitude": 11.5345141, "x": 61.1, "y": 58.9, "source": "osm", "type": "locality" }, { "name": "Carrefour Anguissa", "latitude": 3.8615261, "longitude": 11.5374597, "x": 62.5, "y": 49.5, "source": "osm", "type": "locality" }, { "name": "Carrefour Awae", "latitude": 3.8304145, "longitude": 11.551546, "x": 69, "y": 59.7, "source": "osm", "type": "locality" }, { "name": "Carrefour Bata Nlongkak", "latitude": 3.8891106, "longitude": 11.5224098, "x": 55.6, "y": 40.5, "source": "osm", "type": "locality" }, { "name": "Carrefour Belibi", "latitude": 3.8667743, "longitude": 11.5396511, "x": 63.5, "y": 47.8, "source": "osm", "type": "locality" }, { "name": "Carrefour Ceper", "latitude": 3.8737784, "longitude": 11.5241832, "x": 56.4, "y": 45.5, "source": "osm", "type": "locality" }, { "name": "Carrefour Chapelle TKC", "latitude": 3.8376866, "longitude": 11.4747919, "x": 33.7, "y": 57.3, "source": "osm", "type": "locality" }, { "name": "Carrefour Chefferie", "latitude": 3.9259789, "longitude": 11.5296971, "x": 58.9, "y": 28.5, "source": "osm", "type": "locality" }, { "name": "Carrefour Coden", "latitude": 3.8796366, "longitude": 11.5617199, "x": 73.6, "y": 43.6, "source": "osm", "type": "locality" }, { "name": "Carrefour de l'Amitie", "latitude": 3.825418, "longitude": 11.5367127, "x": 62.1, "y": 61.3, "source": "osm", "type": "locality" }, { "name": "Carrefour De La Chapelle", "latitude": 3.9305512, "longitude": 11.5560226, "x": 71, "y": 27, "source": "osm", "type": "locality" }, { "name": "Carrefour de La Fraternite", "latitude": 3.9305615, "longitude": 11.5300895, "x": 59.1, "y": 27, "source": "osm", "type": "locality" }, { "name": "Carrefour de la Gendarmerie", "latitude": 3.9825051, "longitude": 11.5927579, "x": 87.9, "y": 10, "source": "osm", "type": "locality" }, { "name": "Carrefour du Lycee", "latitude": 3.9775492, "longitude": 11.5928999, "x": 87.9, "y": 11.6, "source": "osm", "type": "locality" }, { "name": "Carrefour Edimo", "latitude": 3.8571186, "longitude": 11.5551015, "x": 70.6, "y": 51, "source": "osm", "type": "locality" }, { "name": "Carrefour Ekounou", "latitude": 3.8444396, "longitude": 11.5415262, "x": 64.4, "y": 55.1, "source": "osm", "type": "locality" }, { "name": "Carrefour Eldorado", "latitude": 3.8639566, "longitude": 11.5282017, "x": 58.2, "y": 48.7, "source": "osm", "type": "locality" }, { "name": "Carrefour eleveur", "latitude": 3.9005899, "longitude": 11.5576303, "x": 71.7, "y": 36.8, "source": "osm", "type": "locality" }, { "name": "Carrefour Elig-Essono", "latitude": 3.8707522, "longitude": 11.5238133, "x": 56.2, "y": 46.5, "source": "osm", "type": "locality" }, { "name": "Carrefour Eloundou", "latitude": 3.8702893, "longitude": 11.5683616, "x": 76.7, "y": 46.7, "source": "osm", "type": "locality" }, { "name": "Carrefour EMIA", "latitude": 3.8623753, "longitude": 11.5040748, "x": 47.2, "y": 49.3, "source": "osm", "type": "locality" }, { "name": "Carrefour Enyeque", "latitude": 3.8802463, "longitude": 11.4789968, "x": 35.6, "y": 43.4, "source": "osm", "type": "locality" }, { "name": "Carrefour Essomba", "latitude": 3.855302, "longitude": 11.5475887, "x": 67.1, "y": 51.6, "source": "osm", "type": "locality" }, { "name": "Carrefour Essono Bar", "latitude": 3.7969839, "longitude": 11.5276748, "x": 58, "y": 70.6, "source": "osm", "type": "locality" }, { "name": "Carrefour Etoudi", "latitude": 3.9156242, "longitude": 11.5240382, "x": 56.3, "y": 31.9, "source": "osm", "type": "locality" }, { "name": "Carrefour Fouda", "latitude": 3.8735554, "longitude": 11.5293967, "x": 58.8, "y": 45.6, "source": "osm", "type": "locality" }, { "name": "Carrefour Germaine", "latitude": 3.8707231, "longitude": 11.534337, "x": 61.1, "y": 46.5, "source": "osm", "type": "locality" }, { "name": "Carrefour Golf", "latitude": 3.8970532, "longitude": 11.4971656, "x": 44, "y": 37.9, "source": "osm", "type": "locality" }, { "name": "Carrefour Hotel Hintel", "latitude": 3.9027191, "longitude": 11.545816, "x": 66.3, "y": 36.1, "source": "osm", "type": "locality" }, { "name": "Carrefour Hotel Le Paradis", "latitude": 3.8997604, "longitude": 11.5472935, "x": 67, "y": 37, "source": "osm", "type": "locality" }, { "name": "Carrefour Hysacam", "latitude": 3.926412, "longitude": 11.5608832, "x": 73.2, "y": 28.3, "source": "osm", "type": "locality" }, { "name": "Carrefour Intendance", "latitude": 3.8665145, "longitude": 11.5205145, "x": 54.7, "y": 47.9, "source": "osm", "type": "locality" }, { "name": "Carrefour Jamot", "latitude": 3.8987864, "longitude": 11.5239767, "x": 56.3, "y": 37.4, "source": "osm", "type": "locality" }, { "name": "Carrefour Jouvence", "latitude": 3.8283393, "longitude": 11.4796414, "x": 35.9, "y": 60.4, "source": "osm", "type": "locality" }, { "name": "Carrefour Kaka", "latitude": 3.8319231, "longitude": 11.4841874, "x": 38, "y": 59.2, "source": "osm", "type": "locality" }, { "name": "Carrefour Le Banquier", "latitude": 3.8850283, "longitude": 11.5672542, "x": 76.2, "y": 41.9, "source": "osm", "type": "locality" }, { "name": "Carrefour Lissouck", "latitude": 3.8720668, "longitude": 11.5051237, "x": 47.6, "y": 46.1, "source": "osm", "type": "locality" }, { "name": "Carrefour Mami Cadeau Okoui", "latitude": 3.8402939, "longitude": 11.5528877, "x": 69.6, "y": 56.5, "source": "osm", "type": "locality" }, { "name": "Carrefour Mbankolo", "latitude": 3.8964047, "longitude": 11.4913634, "x": 41.3, "y": 38.1, "source": "osm", "type": "locality" }, { "name": "Carrefour MEEC", "latitude": 3.8695699, "longitude": 11.4850665, "x": 38.4, "y": 46.9, "source": "osm", "type": "locality" }, { "name": "Carrefour Messassi", "latitude": 3.9419351, "longitude": 11.5184559, "x": 53.8, "y": 23.3, "source": "osm", "type": "locality" }, { "name": "Carrefour Messebe", "latitude": 3.891194, "longitude": 11.446818, "x": 20.9, "y": 39.8, "source": "osm", "type": "locality" }, { "name": "Carrefour Minlo Medjo Pierre", "latitude": 3.9814525, "longitude": 11.5927357, "x": 87.9, "y": 10.3, "source": "osm", "type": "locality" }, { "name": "Carrefour Mvog-Abang", "latitude": 3.8087229, "longitude": 11.5293309, "x": 58.8, "y": 66.8, "source": "osm", "type": "locality" }, { "name": "Carrefour Nkol-ewoe", "latitude": 3.8652957, "longitude": 11.5353435, "x": 61.5, "y": 48.3, "source": "osm", "type": "locality" }, { "name": "Carrefour Nkolbisson", "latitude": 3.8726726, "longitude": 11.4542006, "x": 24.3, "y": 45.9, "source": "osm", "type": "locality" }, { "name": "Carrefour Nkollo", "latitude": 3.8423549, "longitude": 11.5973781, "x": 90, "y": 55.8, "source": "osm", "type": "locality" }, { "name": "Carrefour Nkomo", "latitude": 3.8411033, "longitude": 11.5511921, "x": 68.8, "y": 56.2, "source": "osm", "type": "locality" }, { "name": "Carrefour Nkondengui", "latitude": 3.852833, "longitude": 11.5371247, "x": 62.3, "y": 52.4, "source": "osm", "type": "locality" }, { "name": "Carrefour Noah Muogo", "latitude": 3.8912816, "longitude": 11.5670048, "x": 76.1, "y": 39.8, "source": "osm", "type": "locality" }, { "name": "Carrefour Nsam", "latitude": 3.8267352, "longitude": 11.5077463, "x": 48.8, "y": 60.9, "source": "osm", "type": "neighbourhood" }, { "name": "Carrefour Obili", "latitude": 3.857338, "longitude": 11.4925322, "x": 41.9, "y": 50.9, "source": "osm", "type": "locality" }, { "name": "Carrefour Oxfort", "latitude": 3.9257702, "longitude": 11.555409, "x": 70.7, "y": 28.5, "source": "osm", "type": "locality" }, { "name": "Carrefour Pakita Mvog-Ada", "latitude": 3.8683999, "longitude": 11.5321449, "x": 60, "y": 47.3, "source": "osm", "type": "locality" }, { "name": "Carrefour Petit Marche ODZA", "latitude": 3.8015191, "longitude": 11.533932, "x": 60.9, "y": 69.1, "source": "osm", "type": "locality" }, { "name": "Carrefour Rail Ngousso", "latitude": 3.9083359, "longitude": 11.5370099, "x": 62.3, "y": 34.2, "source": "osm", "type": "locality" }, { "name": "Carrefour Regie", "latitude": 3.89557, "longitude": 11.5230674, "x": 55.9, "y": 38.4, "source": "osm", "type": "locality" }, { "name": "Carrefour Sapeur Mimboman", "latitude": 3.8690166, "longitude": 11.5496985, "x": 68.1, "y": 47.1, "source": "osm", "type": "locality" }, { "name": "Carrefour SNH", "latitude": 3.9009571, "longitude": 11.5162633, "x": 52.8, "y": 36.6, "source": "osm", "type": "locality" }, { "name": "Carrefour Sorcier", "latitude": 3.9071129, "longitude": 11.5300207, "x": 59.1, "y": 34.6, "source": "osm", "type": "locality" }, { "name": "Carrefour Sous Manguiers", "latitude": 3.8503347, "longitude": 11.5546547, "x": 70.4, "y": 53.2, "source": "osm", "type": "locality" }, { "name": "Carrefour sous-prefecture", "latitude": 3.8869237, "longitude": 11.5016607, "x": 46.1, "y": 41.2, "source": "osm", "type": "locality" }, { "name": "Carrefour Tankwa", "latitude": 3.8785071, "longitude": 11.5576313, "x": 71.8, "y": 44, "source": "osm", "type": "locality" }, { "name": "Carrefour TKC", "latitude": 3.8409673, "longitude": 11.4774212, "x": 34.9, "y": 56.2, "source": "osm", "type": "locality" }, { "name": "Carrefour Tsimi", "latitude": 3.8729381, "longitude": 11.4616068, "x": 27.7, "y": 45.8, "source": "osm", "type": "locality" }, { "name": "Carrefour Vallee Nlongkak", "latitude": 3.8928986, "longitude": 11.5228133, "x": 55.8, "y": 39.3, "source": "osm", "type": "locality" }, { "name": "Carrefour Vogt", "latitude": 3.8442558, "longitude": 11.5013896, "x": 45.9, "y": 55.2, "source": "osm", "type": "locality" }, { "name": "Carrefour Warda", "latitude": 3.8717711, "longitude": 11.514734, "x": 52.1, "y": 46.2, "source": "osm", "type": "locality" }, { "name": "Carrefour WWF", "latitude": 3.8879671, "longitude": 11.5098883, "x": 49.8, "y": 40.9, "source": "osm", "type": "locality" }, { "name": "Carrefour Yannick", "latitude": 3.9086848, "longitude": 11.528399, "x": 58.3, "y": 34.1, "source": "osm", "type": "locality" }, { "name": "Chefferie de Afanoyoa IV", "latitude": 3.7515341, "longitude": 11.4518042, "x": 23.2, "y": 85.5, "source": "osm", "type": "locality" }, { "name": "Chefferie de Troisieme Degre de Minkoameyos", "latitude": 3.8756612, "longitude": 11.4231455, "x": 10, "y": 44.9, "source": "osm", "type": "locality" }, { "name": "Chefferie Etetak", "latitude": 3.871512, "longitude": 11.4819303, "x": 37, "y": 46.3, "source": "osm", "type": "locality" }, { "name": "Chefferie Lada 1", "latitude": 3.8316529, "longitude": 11.5887407, "x": 86, "y": 59.3, "source": "osm", "type": "locality" }, { "name": "Chefferie Mekoumbou II", "latitude": 3.7449139, "longitude": 11.4548937, "x": 24.6, "y": 87.6, "source": "osm", "type": "locality" }, { "name": "Chefferie Nkolmesseng", "latitude": 3.7376742, "longitude": 11.4644327, "x": 29, "y": 90, "source": "osm", "type": "locality" }, { "name": "Chefferie Nnom-Nnam", "latitude": 3.8838519, "longitude": 11.4653485, "x": 29.4, "y": 42.2, "source": "osm", "type": "locality" }, { "name": "Chefferie Nsam Efoulan", "latitude": 3.8262803, "longitude": 11.5086551, "x": 49.3, "y": 61, "source": "osm", "type": "locality" }, { "name": "Chefferie Nsimeyong 3", "latitude": 3.8286695, "longitude": 11.4869252, "x": 39.3, "y": 60.3, "source": "osm", "type": "locality" }, { "name": "Chefferie Oyom-Abang", "latitude": 3.8742535, "longitude": 11.4769414, "x": 34.7, "y": 45.4, "source": "osm", "type": "locality" }, { "name": "Chefferie Simbok", "latitude": 3.8118948, "longitude": 11.4735891, "x": 33.2, "y": 65.7, "source": "osm", "type": "locality" }, { "name": "Chefferie Troisieme Degre de Nkolmbong", "latitude": 3.930527, "longitude": 11.541827, "x": 64.5, "y": 27, "source": "osm", "type": "locality" }, { "name": "Chefferie Tsinga Village", "latitude": 3.9337227, "longitude": 11.5538733, "x": 70, "y": 25.9, "source": "osm", "type": "locality" }, { "name": "Cimetiere de Ngoulmekong", "latitude": 3.9060587, "longitude": 11.5700969, "x": 77.5, "y": 35, "source": "osm", "type": "locality" }, { "name": "Cite de La Vierge Marie", "latitude": 3.9094099, "longitude": 11.4837368, "x": 37.8, "y": 33.9, "source": "osm", "type": "locality" }, { "name": "Cite de Mfandena", "latitude": 3.8856158, "longitude": 11.5460119, "x": 66.4, "y": 41.7, "source": "osm", "type": "locality" }, { "name": "Cite Sic Madagascar", "latitude": 3.8814889, "longitude": 11.493193, "x": 42.2, "y": 43, "source": "osm", "type": "locality" }, { "name": "Complexe Avicole de MVOG-BETSI", "latitude": 3.8643911, "longitude": 11.4793196, "x": 35.8, "y": 48.6, "source": "osm", "type": "locality" }, { "name": "Dagobert", "latitude": 3.900089, "longitude": 11.4496313, "x": 22.2, "y": 36.9, "source": "osm", "type": "locality" }, { "name": "Depot de Bois", "latitude": 3.8318125, "longitude": 11.5399674, "x": 63.6, "y": 59.2, "source": "osm", "type": "locality" }, { "name": "Dernier Poteau", "latitude": 3.8642921, "longitude": 11.5705184, "x": 77.7, "y": 48.6, "source": "osm", "type": "locality" }, { "name": "EBOGO CITY", "latitude": 3.8544843, "longitude": 11.5001398, "x": 45.4, "y": 51.8, "source": "osm", "type": "neighbourhood" }, { "name": "Entrez Fokou", "latitude": 3.8607827, "longitude": 11.5970019, "x": 89.8, "y": 49.8, "source": "osm", "type": "quarter" }, { "name": "Etetak", "latitude": 3.87548, "longitude": 11.48039, "x": 36.3, "y": 45, "source": "geonames", "type": "PPLX" }, { "name": "NKolmbong Bethanie", "latitude": 3.9321168, "longitude": 11.5428623, "x": 65, "y": 26.5, "source": "osm", "type": "neighbourhood" }, { "name": "Okoui chefferie", "latitude": 3.8427408, "longitude": 11.5596815, "x": 72.7, "y": 55.7, "source": "osm", "type": "neighbourhood" }, { "name": "Oyom Abang", "latitude": 3.8797144, "longitude": 11.4696649, "x": 31.4, "y": 43.6, "source": "osm", "type": "quarter" }, { "name": "Simbock", "latitude": 3.8209702, "longitude": 11.4731585, "x": 33, "y": 62.8, "source": "osm", "type": "neighbourhood" }, { "name": "Zone M", "latitude": 3.8746821, "longitude": 11.4892694, "x": 40.4, "y": 45.2, "source": "osm", "type": "neighbourhood" }, { "name": "Zone O", "latitude": 3.875563, "longitude": 11.4894512, "x": 40.4, "y": 44.9, "source": "osm", "type": "neighbourhood" } ], "Yingui": [ { "name": "Bahale", "latitude": 4.7, "longitude": 10.35, "x": 58, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Bidjenj II", "latitude": 4.75, "longitude": 10.28333, "x": 42, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Log Mbo", "latitude": 4.45, "longitude": 10.48333, "x": 90, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Ndem I", "latitude": 4.55, "longitude": 10.33333, "x": 54, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Ndem II", "latitude": 4.63333, "longitude": 10.38333, "x": 66, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Ndobeginge I", "latitude": 4.4, "longitude": 10.16667, "x": 14, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ndok Ngong", "latitude": 4.41667, "longitude": 10.36667, "x": 62, "y": 86.2, "source": "geonames", "type": "PPL" }, { "name": "Ndokaniak", "latitude": 4.56667, "longitude": 10.3, "x": 46, "y": 51.9, "source": "geonames", "type": "PPL" }, { "name": "Ndokbangingi", "latitude": 4.48333, "longitude": 10.26667, "x": 38, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Ndokbom II", "latitude": 4.6, "longitude": 10.26667, "x": 38, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Ndokmen I", "latitude": 4.53333, "longitude": 10.25, "x": 34, "y": 59.5, "source": "geonames", "type": "PPL" }, { "name": "Ndokminokon II", "latitude": 4.4, "longitude": 10.38333, "x": 66, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Ndokniok", "latitude": 4.48333, "longitude": 10.36667, "x": 62, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Ndokomouen", "latitude": 4.43333, "longitude": 10.15, "x": 10, "y": 82.4, "source": "geonames", "type": "PPL" }, { "name": "Ndoktouna", "latitude": 4.63333, "longitude": 10.23333, "x": 30, "y": 36.7, "source": "geonames", "type": "PPL" }, { "name": "Ndol", "latitude": 4.55, "longitude": 10.4, "x": 70, "y": 55.7, "source": "geonames", "type": "PPL" }, { "name": "Nyamtam", "latitude": 4.48333, "longitude": 10.18333, "x": 18, "y": 71, "source": "geonames", "type": "PPL" }, { "name": "Yingi II", "latitude": 4.53333, "longitude": 10.35, "x": 58, "y": 59.5, "source": "geonames", "type": "PPL" } ], "Yoko": [ { "name": "Ayem", "latitude": 5.35, "longitude": 12.41667, "x": 83.8, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Djampa", "latitude": 5.65, "longitude": 12.35, "x": 59.2, "y": 24.5, "source": "geonames", "type": "PPL" }, { "name": "Dounga", "latitude": 5.35, "longitude": 12.43333, "x": 90, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Dzere", "latitude": 5.45, "longitude": 12.33333, "x": 53.1, "y": 68.2, "source": "geonames", "type": "PPL" }, { "name": "Kounde", "latitude": 5.55, "longitude": 12.26667, "x": 28.5, "y": 46.4, "source": "geonames", "type": "PPL" }, { "name": "Koundjiri", "latitude": 5.36667, "longitude": 12.21667, "x": 10, "y": 86.4, "source": "geonames", "type": "PPL" }, { "name": "Lewou", "latitude": 5.51667, "longitude": 12.36667, "x": 65.4, "y": 53.6, "source": "geonames", "type": "PPL" }, { "name": "Lom", "latitude": 5.36667, "longitude": 12.26667, "x": 28.5, "y": 86.4, "source": "geonames", "type": "PPL" }, { "name": "Malarba", "latitude": 5.6, "longitude": 12.31667, "x": 46.9, "y": 35.5, "source": "geonames", "type": "PPL" }, { "name": "Mangan", "latitude": 5.38333, "longitude": 12.3, "x": 40.8, "y": 82.7, "source": "geonames", "type": "PPL" }, { "name": "Matsari", "latitude": 5.35, "longitude": 12.23333, "x": 16.2, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Mbambe", "latitude": 5.66667, "longitude": 12.38333, "x": 71.5, "y": 20.9, "source": "geonames", "type": "PPL" }, { "name": "Mbamdi", "latitude": 5.61667, "longitude": 12.33333, "x": 53.1, "y": 31.8, "source": "geonames", "type": "PPL" }, { "name": "Mbare", "latitude": 5.4, "longitude": 12.31667, "x": 46.9, "y": 79.1, "source": "geonames", "type": "PPL" }, { "name": "Mbengang", "latitude": 5.46667, "longitude": 12.33333, "x": 53.1, "y": 64.5, "source": "geonames", "type": "PPL" }, { "name": "Mbering", "latitude": 5.43333, "longitude": 12.31667, "x": 46.9, "y": 71.8, "source": "geonames", "type": "PPL" }, { "name": "Medjambouni", "latitude": 5.36667, "longitude": 12.43333, "x": 90, "y": 86.4, "source": "geonames", "type": "PPL" }, { "name": "Megang", "latitude": 5.5, "longitude": 12.38333, "x": 71.5, "y": 57.3, "source": "geonames", "type": "PPL" }, { "name": "Ndja", "latitude": 5.48333, "longitude": 12.35, "x": 59.2, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Ngontan", "latitude": 5.48333, "longitude": 12.33333, "x": 53.1, "y": 60.9, "source": "geonames", "type": "PPL" }, { "name": "Ngoum", "latitude": 5.71667, "longitude": 12.41667, "x": 83.8, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Nguyimi", "latitude": 5.45, "longitude": 12.31667, "x": 46.9, "y": 68.2, "source": "geonames", "type": "PPL" }, { "name": "Tiofoun", "latitude": 5.41667, "longitude": 12.31667, "x": 46.9, "y": 75.5, "source": "geonames", "type": "PPL" }, { "name": "Yondeng", "latitude": 5.5, "longitude": 12.36667, "x": 65.4, "y": 57.3, "source": "geonames", "type": "PPL" } ], "Yokadouma": [ { "name": "Bienemama", "latitude": 3.51667, "longitude": 15.01667, "x": 21.4, "y": 78.6, "source": "geonames", "type": "PPL" }, { "name": "Garesingo", "latitude": 3.55, "longitude": 15.05, "x": 32.9, "y": 67.1, "source": "geonames", "type": "PPL" }, { "name": "Gribi", "latitude": 3.71667, "longitude": 15.13333, "x": 61.4, "y": 10, "source": "geonames", "type": "PPL" }, { "name": "Kongo", "latitude": 3.68333, "longitude": 15.11667, "x": 55.7, "y": 21.4, "source": "geonames", "type": "PPL" }, { "name": "Mampele", "latitude": 3.7, "longitude": 15.13333, "x": 61.4, "y": 15.7, "source": "geonames", "type": "PPL" }, { "name": "Masiang", "latitude": 3.5, "longitude": 15.05, "x": 32.9, "y": 84.3, "source": "geonames", "type": "PPL" }, { "name": "Mbol", "latitude": 3.5, "longitude": 14.98333, "x": 10, "y": 84.3, "source": "geonames", "type": "PPL" }, { "name": "Mbyali", "latitude": 3.6, "longitude": 15.06667, "x": 38.6, "y": 50, "source": "geonames", "type": "PPL" }, { "name": "Mendounge", "latitude": 3.48333, "longitude": 15.05, "x": 32.9, "y": 90, "source": "geonames", "type": "PPL" }, { "name": "Menziong", "latitude": 3.5, "longitude": 15.21667, "x": 90, "y": 84.3, "source": "geonames", "type": "PPL" }, { "name": "Mesadjiso", "latitude": 3.7, "longitude": 15.11667, "x": 55.7, "y": 15.7, "source": "geonames", "type": "PPL" }, { "name": "Momzopya", "latitude": 3.63333, "longitude": 15.08333, "x": 44.3, "y": 38.6, "source": "geonames", "type": "PPL" }, { "name": "Mopwo", "latitude": 3.66667, "longitude": 15.08333, "x": 44.3, "y": 27.1, "source": "geonames", "type": "PPL" }, { "name": "Noumbakoe", "latitude": 3.66667, "longitude": 15.1, "x": 50, "y": 27.1, "source": "geonames", "type": "PPL" }, { "name": "Paya", "latitude": 3.56667, "longitude": 15.06667, "x": 38.6, "y": 61.4, "source": "geonames", "type": "PPL" }, { "name": "Sangha", "latitude": 3.61667, "longitude": 15.06667, "x": 38.6, "y": 44.3, "source": "geonames", "type": "PPL" }, { "name": "Zokboulabon", "latitude": 3.5, "longitude": 15.16667, "x": 72.9, "y": 84.3, "source": "geonames", "type": "PPL" } ] }; // Runtime configuration, market constants, launch economics, and static domain catalogs. function configuredRuntimeRole() { const explicitRole = typeof window === "undefined" ? "" : String(window.WAKA_RUNTIME_ROLE || "").toLowerCase(); if (explicitRole === "admin") return "admin"; if (explicitRole === "passenger") return "passenger"; if (explicitRole === "rider") return "rider"; if (explicitRole === "public") return "public"; const shellRole = typeof document === "undefined" ? "" : String(document.documentElement.dataset.wakaShell || document.body?.dataset.wakaShell || "").toLowerCase(); if (["admin", "passenger", "rider"].includes(shellRole)) return shellRole; return "public"; } const runtimeRole = configuredRuntimeRole(); function adminRuntimeAvailable() { return runtimeRole === "admin"; } function runtimeAllowsWorkspaceTab(tab) { const allowedTabs = typeof window !== "undefined" && Array.isArray(window.WAKA_ALLOWED_RUNTIME_TABS) ? window.WAKA_ALLOWED_RUNTIME_TABS : []; if (allowedTabs.length) return allowedTabs.includes(tab); if (runtimeRole === "admin") return tab === "admin"; if (runtimeRole === "passenger") return tab === "passenger"; if (runtimeRole === "rider") return tab === "rider"; return tab !== "admin"; } function defaultRuntimeTab() { if (runtimeRole === "admin") return "admin"; if (runtimeRole === "rider") return "rider"; return "passenger"; } const countryCities = { Algeria: ["Algiers", "Oran", "Constantine", "Annaba", "Blida"], Angola: ["Luanda", "Huambo", "Lobito", "Benguela", "Lubango"], Benin: ["Cotonou", "Porto-Novo", "Parakou", "Abomey-Calavi", "Djougou"], Botswana: ["Gaborone", "Francistown", "Maun", "Molepolole", "Serowe"], "Burkina Faso": ["Ouagadougou", "Bobo-Dioulasso", "Koudougou", "Banfora", "Ouahigouya"], Burundi: ["Bujumbura", "Gitega", "Ngozi", "Rumonge", "Muyinga"], "Cabo Verde": ["Praia", "Mindelo", "Santa Maria", "Assomada", "Espargos"], Cameroon: [ "Abong-Mbang", "Akom II", "Akono", "Akonolinga", "Ambam", "Ayos", "Bafang", "Bafia", "Bafoussam", "Bafut", "Baham", "Bali", "Bamenda", "Bambili", "Bambui", "Bamusso", "Bandjoun", "Bangangte", "Bangem", "Bangourain", "Bankim", "Banyo", "Batibo", "Batouri", "Batcham", "Bekoko", "Belabo", "Belo", "Belel", "Bertoua", "Betare-Oya", "Bipindi", "Bogo", "Buea", "Buea Town", "Campo", "Deido", "Dibombari", "Dimako", "Dizangue", "Djoum", "Djohong", "Douala", "Doume", "Dschang", "Ebolowa", "Edea", "Ekondo-Titi", "Eseka", "Esu", "Eyumojock", "Evodoula", "Figuil", "Fontem", "Foumban", "Foumbot", "Fundong", "Galim", "Garoua", "Garoua-Boulai", "Guidiguis", "Guider", "Idabato", "Idenau", "Jakiri", "Kaele", "Kaele-Kaele", "Kekem", "Kette", "Kom", "Konye", "Kousseri", "Kribi", "Kumba", "Kumbo", "Kye-Ossi", "Lagdo", "Limbe", "Lolodorf", "Lomie", "Loum", "Maga", "Magba", "Mamfe", "Malantouen", "Manjo", "Maroua", "Massangam", "Mbalmayo", "Mbankomo", "Mbandjock", "Mbanga", "Mbengwi", "Mbouda", "Meiganga", "Melong", "Meyomessala", "Mfou", "Mindif", "Minta", "Mintom", "Moloundou", "Monatele", "Mokolo", "Mora", "Mouanko", "Mundemba", "Mutengene", "Muyuka", "Nanga-Eboko", "Ndobian", "Ndop", "Ndu", "Ngaoundere", "Ngaoundal", "Ngambe", "Ngomedzap", "Ngoro", "Nguti", "Njinikom", "Nkambe", "Nkoteng", "Nkongsamba", "Ntui", "Obala", "Okola", "Penja", "Pitoa", "Poli", "Rey-Bouba", "Saa", "Sangmelima", "Santa", "Tchollire", "Tignere", "Tiko", "Tibati", "Tombel", "Waza", "Wum", "Yabassi", "Yagoua", "Yaounde", "Yingui", "Yoko", "Yokadouma" ], "Central African Republic": ["Bangui", "Bimbo", "Berberati", "Bambari", "Bouar"], Chad: ["N'Djamena", "Moundou", "Abeche", "Sarh", "Kelo"], Comoros: ["Moroni", "Mutsamudu", "Fomboni", "Domoni", "Ouani"], Congo: ["Brazzaville", "Pointe-Noire", "Dolisie", "Nkayi", "Owando"], "Democratic Republic of the Congo": ["Kinshasa", "Lubumbashi", "Mbuji-Mayi", "Goma", "Kisangani", "Bukavu"], "Cote d'Ivoire": ["Abidjan", "Bouake", "Yamoussoukro", "San Pedro", "Korhogo"], Djibouti: ["Djibouti City", "Ali Sabieh", "Tadjoura", "Dikhil", "Obock"], Egypt: ["Cairo", "Alexandria", "Giza", "Luxor", "Aswan", "Mansoura"], "Equatorial Guinea": ["Malabo", "Bata", "Ebebiyin", "Aconibe", "Luba"], Eritrea: ["Asmara", "Keren", "Massawa", "Assab", "Mendefera"], Eswatini: ["Mbabane", "Manzini", "Lobamba", "Nhlangano", "Siteki"], Ethiopia: ["Addis Ababa", "Dire Dawa", "Mekelle", "Gondar", "Bahir Dar", "Hawassa"], Gabon: ["Libreville", "Port-Gentil", "Franceville", "Oyem", "Moanda"], Gambia: ["Banjul", "Serekunda", "Brikama", "Bakau", "Farafenni"], Ghana: ["Accra", "Kumasi", "Tamale", "Takoradi", "Tema", "Cape Coast"], Guinea: ["Conakry", "Kankan", "Nzerekore", "Kindia", "Labe"], "Guinea-Bissau": ["Bissau", "Bafata", "Gabu", "Cacheu", "Bissora"], Kenya: ["Nairobi", "Mombasa", "Kisumu", "Nakuru", "Eldoret", "Thika"], Lesotho: ["Maseru", "Teyateyaneng", "Mafeteng", "Leribe", "Mohale's Hoek"], Liberia: ["Monrovia", "Gbarnga", "Buchanan", "Ganta", "Kakata"], Libya: ["Tripoli", "Benghazi", "Misrata", "Zawiya", "Sabha"], Madagascar: ["Antananarivo", "Toamasina", "Antsirabe", "Mahajanga", "Fianarantsoa"], Malawi: ["Lilongwe", "Blantyre", "Mzuzu", "Zomba", "Kasungu"], Mali: ["Bamako", "Sikasso", "Mopti", "Segou", "Kayes"], Mauritania: ["Nouakchott", "Nouadhibou", "Kiffa", "Kaedi", "Rosso"], Mauritius: ["Port Louis", "Beau Bassin-Rose Hill", "Vacoas-Phoenix", "Curepipe", "Quatre Bornes"], Morocco: ["Casablanca", "Rabat", "Marrakesh", "Fes", "Tangier", "Agadir"], Mozambique: ["Maputo", "Matola", "Beira", "Nampula", "Chimoio"], Namibia: ["Windhoek", "Walvis Bay", "Swakopmund", "Oshakati", "Rundu"], Niger: ["Niamey", "Zinder", "Maradi", "Agadez", "Tahoua"], Nigeria: ["Lagos", "Abuja", "Kano", "Ibadan", "Port Harcourt", "Enugu", "Kaduna"], Rwanda: ["Kigali", "Butare", "Gisenyi", "Musanze", "Rwamagana"], "Sao Tome and Principe": ["Sao Tome", "Santo Antonio", "Trindade", "Neves", "Santana"], Senegal: ["Dakar", "Thies", "Touba", "Saint-Louis", "Kaolack", "Ziguinchor"], Seychelles: ["Victoria", "Anse Boileau", "Beau Vallon", "Takamaka", "Anse Royale"], "Sierra Leone": ["Freetown", "Bo", "Kenema", "Makeni", "Koidu"], Somalia: ["Mogadishu", "Hargeisa", "Bosaso", "Kismayo", "Baidoa"], "South Africa": ["Johannesburg", "Cape Town", "Durban", "Pretoria", "Port Elizabeth", "Bloemfontein"], "South Sudan": ["Juba", "Wau", "Malakal", "Yei", "Aweil"], Sudan: ["Khartoum", "Omdurman", "Port Sudan", "Kassala", "El Obeid"], Tanzania: ["Dar es Salaam", "Dodoma", "Mwanza", "Arusha", "Zanzibar City"], Togo: ["Lome", "Sokode", "Kara", "Kpalime", "Atakpame"], Tunisia: ["Tunis", "Sfax", "Sousse", "Kairouan", "Bizerte"], Uganda: ["Kampala", "Gulu", "Mbarara", "Jinja", "Mbale"], Zambia: ["Lusaka", "Kitwe", "Ndola", "Livingstone", "Kabwe"], Zimbabwe: ["Harare", "Bulawayo", "Chitungwiza", "Mutare", "Gweru"], Argentina: ["Buenos Aires", "Cordoba", "Rosario", "Mendoza", "La Plata"], Bahamas: ["Nassau", "Freeport", "West End", "Coopers Town", "Marsh Harbour"], Barbados: ["Bridgetown", "Speightstown", "Oistins", "Holetown", "Bathsheba"], Belize: ["Belize City", "Belmopan", "San Ignacio", "Orange Walk", "Dangriga"], Bolivia: ["La Paz", "Santa Cruz", "Cochabamba", "Sucre", "El Alto"], Brazil: ["Sao Paulo", "Rio de Janeiro", "Brasilia", "Salvador", "Fortaleza", "Belo Horizonte"], Canada: ["Toronto", "Montreal", "Vancouver", "Calgary", "Ottawa", "Edmonton"], Chile: ["Santiago", "Valparaiso", "Concepcion", "La Serena", "Antofagasta"], Colombia: ["Bogota", "Medellin", "Cali", "Barranquilla", "Cartagena"], "Costa Rica": ["San Jose", "Alajuela", "Cartago", "Heredia", "Liberia"], Cuba: ["Havana", "Santiago de Cuba", "Camaguey", "Holguin", "Santa Clara"], "Dominican Republic": ["Santo Domingo", "Santiago", "La Romana", "Puerto Plata", "San Pedro de Macoris"], Ecuador: ["Quito", "Guayaquil", "Cuenca", "Santo Domingo", "Machala"], "El Salvador": ["San Salvador", "Santa Ana", "San Miguel", "Soyapango", "Mejicanos"], Guatemala: ["Guatemala City", "Quetzaltenango", "Mixco", "Villa Nueva", "Escuintla"], Guyana: ["Georgetown", "Linden", "New Amsterdam", "Anna Regina", "Bartica"], Haiti: ["Port-au-Prince", "Cap-Haitien", "Carrefour", "Delmas", "Petion-Ville"], Honduras: ["Tegucigalpa", "San Pedro Sula", "La Ceiba", "Choloma", "El Progreso"], Jamaica: ["Kingston", "Montego Bay", "Spanish Town", "Portmore", "Mandeville"], Mexico: ["Mexico City", "Guadalajara", "Monterrey", "Puebla", "Tijuana", "Merida"], Nicaragua: ["Managua", "Leon", "Masaya", "Matagalpa", "Chinandega"], Panama: ["Panama City", "San Miguelito", "Tocumen", "David", "Colon"], Paraguay: ["Asuncion", "Ciudad del Este", "San Lorenzo", "Luque", "Capiata"], Peru: ["Lima", "Arequipa", "Trujillo", "Chiclayo", "Cusco"], Suriname: ["Paramaribo", "Lelydorp", "Nieuw Nickerie", "Moengo", "Meerzorg"], "Trinidad and Tobago": ["Port of Spain", "San Fernando", "Chaguanas", "Arima", "Point Fortin"], "United States": [ "Maryland", "Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware", "District of Columbia", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana", "Maine", "Massachusetts", "Michigan", "Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming" ], Uruguay: ["Montevideo", "Salto", "Paysandu", "Las Piedras", "Maldonado"], Venezuela: ["Caracas", "Maracaibo", "Valencia", "Barquisimeto", "Maracay"], Albania: ["Tirana", "Durres", "Vlore", "Shkoder", "Fier"], Austria: ["Vienna", "Graz", "Linz", "Salzburg", "Innsbruck"], Belgium: ["Brussels", "Antwerp", "Ghent", "Charleroi", "Liege"], Bulgaria: ["Sofia", "Plovdiv", "Varna", "Burgas", "Ruse"], Croatia: ["Zagreb", "Split", "Rijeka", "Osijek", "Zadar"], Czechia: ["Prague", "Brno", "Ostrava", "Plzen", "Liberec"], Denmark: ["Copenhagen", "Aarhus", "Odense", "Aalborg", "Esbjerg"], Finland: ["Helsinki", "Espoo", "Tampere", "Vantaa", "Turku"], France: ["Paris", "Marseille", "Lyon", "Toulouse", "Nice", "Nantes"], Germany: ["Berlin", "Hamburg", "Munich", "Cologne", "Frankfurt", "Stuttgart"], Greece: ["Athens", "Thessaloniki", "Patras", "Heraklion", "Larissa"], Hungary: ["Budapest", "Debrecen", "Szeged", "Miskolc", "Pecs"], Ireland: ["Dublin", "Cork", "Limerick", "Galway", "Waterford"], Italy: ["Rome", "Milan", "Naples", "Turin", "Palermo", "Florence"], Netherlands: ["Amsterdam", "Rotterdam", "The Hague", "Utrecht", "Eindhoven"], Norway: ["Oslo", "Bergen", "Trondheim", "Stavanger", "Drammen"], Poland: ["Warsaw", "Krakow", "Lodz", "Wroclaw", "Poznan"], Portugal: ["Lisbon", "Porto", "Braga", "Coimbra", "Faro"], Romania: ["Bucharest", "Cluj-Napoca", "Timisoara", "Iasi", "Constanta"], Serbia: ["Belgrade", "Novi Sad", "Nis", "Kragujevac", "Subotica"], Spain: ["Madrid", "Barcelona", "Valencia", "Seville", "Bilbao", "Malaga"], Sweden: ["Stockholm", "Gothenburg", "Malmo", "Uppsala", "Vasteras"], Switzerland: ["Zurich", "Geneva", "Basel", "Lausanne", "Bern"], Turkey: ["Istanbul", "Ankara", "Izmir", "Bursa", "Antalya"], Ukraine: ["Kyiv", "Kharkiv", "Odesa", "Dnipro", "Lviv"], "United Kingdom": ["London", "Birmingham", "Manchester", "Glasgow", "Liverpool", "Leeds"], Bangladesh: ["Dhaka", "Chittagong", "Khulna", "Sylhet", "Rajshahi"], China: ["Shanghai", "Beijing", "Guangzhou", "Shenzhen", "Chengdu", "Wuhan"], India: ["Delhi", "Mumbai", "Bengaluru", "Hyderabad", "Chennai", "Kolkata"], Indonesia: ["Jakarta", "Surabaya", "Bandung", "Medan", "Makassar"], Iran: ["Tehran", "Mashhad", "Isfahan", "Shiraz", "Tabriz"], Iraq: ["Baghdad", "Basra", "Mosul", "Erbil", "Najaf"], Israel: ["Tel Aviv", "Jerusalem", "Haifa", "Beersheba", "Netanya"], Japan: ["Tokyo", "Osaka", "Yokohama", "Nagoya", "Sapporo", "Fukuoka"], Jordan: ["Amman", "Zarqa", "Irbid", "Aqaba", "Madaba"], Kazakhstan: ["Almaty", "Astana", "Shymkent", "Karaganda", "Aktobe"], Kuwait: ["Kuwait City", "Hawalli", "Salmiya", "Farwaniya", "Jahra"], Lebanon: ["Beirut", "Tripoli", "Sidon", "Tyre", "Zahle"], Malaysia: ["Kuala Lumpur", "George Town", "Johor Bahru", "Ipoh", "Kota Kinabalu"], Nepal: ["Kathmandu", "Pokhara", "Lalitpur", "Biratnagar", "Birgunj"], Pakistan: ["Karachi", "Lahore", "Islamabad", "Rawalpindi", "Faisalabad"], Philippines: ["Manila", "Quezon City", "Cebu City", "Davao City", "Caloocan"], Qatar: ["Doha", "Al Rayyan", "Al Wakrah", "Umm Salal", "Al Khor"], "Saudi Arabia": ["Riyadh", "Jeddah", "Mecca", "Medina", "Dammam"], Singapore: ["Singapore", "Jurong East", "Tampines", "Woodlands", "Yishun"], "South Korea": ["Seoul", "Busan", "Incheon", "Daegu", "Daejeon"], "Sri Lanka": ["Colombo", "Kandy", "Galle", "Jaffna", "Negombo"], Thailand: ["Bangkok", "Chiang Mai", "Pattaya", "Phuket", "Nonthaburi"], "United Arab Emirates": ["Dubai", "Abu Dhabi", "Sharjah", "Ajman", "Al Ain"], Uzbekistan: ["Tashkent", "Samarkand", "Bukhara", "Namangan", "Andijan"], Vietnam: ["Ho Chi Minh City", "Hanoi", "Da Nang", "Can Tho", "Hai Phong"] }; const africanRidePaymentCountries = new Set([ "Algeria", "Angola", "Benin", "Botswana", "Burkina Faso", "Burundi", "Cabo Verde", "Cameroon", "Central African Republic", "Chad", "Comoros", "Congo", "Democratic Republic of the Congo", "Cote d'Ivoire", "Djibouti", "Egypt", "Equatorial Guinea", "Eritrea", "Eswatini", "Ethiopia", "Gabon", "Gambia", "Ghana", "Guinea", "Guinea-Bissau", "Kenya", "Lesotho", "Liberia", "Libya", "Madagascar", "Malawi", "Mali", "Mauritania", "Mauritius", "Morocco", "Mozambique", "Namibia", "Niger", "Nigeria", "Rwanda", "Sao Tome and Principe", "Senegal", "Seychelles", "Sierra Leone", "Somalia", "South Africa", "South Sudan", "Sudan", "Tanzania", "Togo", "Tunisia", "Uganda", "Zambia", "Zimbabwe" ]); const onlineRidePaymentValues = new Set(["online_card", "online_wallet"]); const productionOnlineRidePaymentProviderPattern = /\b(stripe|adyen|paypal|checkout|paystack|flutterwave|rapyd)\b/i; const marylandMapBounds = { minLatitude: 37.8, maxLatitude: 39.8, minLongitude: -79.6, maxLongitude: -75.0 }; function marylandPlaceMapX(longitude) { const span = marylandMapBounds.maxLongitude - marylandMapBounds.minLongitude; return Math.max(2, Math.min(98, Math.round(((longitude - marylandMapBounds.minLongitude) / span) * 100))); } function marylandPlaceMapY(latitude) { const span = marylandMapBounds.maxLatitude - marylandMapBounds.minLatitude; return Math.max(2, Math.min(98, Math.round(((marylandMapBounds.maxLatitude - latitude) / span) * 100))); } // Maryland launches statewide: keep the pilot towns first, then include every 2024 Census place. const marylandLaunchPlaces = [ { name: "Baltimore", latitude: 39.2904, longitude: -76.6122, x: 58, y: 28 }, { name: "Towson", latitude: 39.4015, longitude: -76.6019, x: 57, y: 20 }, { name: "Pikesville", latitude: 39.3743, longitude: -76.7225, x: 50, y: 24 }, { name: "Parkville", latitude: 39.3773, longitude: -76.5397, x: 61, y: 23 }, { name: "Catonsville", latitude: 39.2721, longitude: -76.7319, x: 51, y: 32 }, { name: "Dundalk", latitude: 39.2507, longitude: -76.5205, x: 64, y: 32 }, { name: "Essex", latitude: 39.3093, longitude: -76.4747, x: 66, y: 28 }, { name: "Glen Burnie", latitude: 39.1626, longitude: -76.6247, x: 62, y: 43 }, { name: "Severn", latitude: 39.1371, longitude: -76.6983, x: 58, y: 45 }, { name: "Hanover", latitude: 39.1929, longitude: -76.7241, x: 56, y: 44 }, { name: "Columbia", latitude: 39.2037, longitude: -76.861, x: 50, y: 39 }, { name: "Ellicott City", latitude: 39.2673, longitude: -76.7983, x: 47, y: 37 }, { name: "Laurel", latitude: 39.0993, longitude: -76.8483, x: 51, y: 48 }, { name: "Beltsville", latitude: 39.0348, longitude: -76.9075, x: 48, y: 52 }, { name: "Greenbelt", latitude: 39.0046, longitude: -76.8755, x: 52, y: 56 }, { name: "College Park", latitude: 38.9807, longitude: -76.9369, x: 48, y: 58 }, { name: "Hyattsville", latitude: 38.9559, longitude: -76.9455, x: 47, y: 61 }, { name: "Riverdale Park", latitude: 38.9634, longitude: -76.9316, x: 49, y: 60 }, { name: "New Carrollton", latitude: 38.9698, longitude: -76.8797, x: 53, y: 60 }, { name: "Lanham", latitude: 38.9688, longitude: -76.8634, x: 55, y: 60 }, { name: "Landover", latitude: 38.934, longitude: -76.8966, x: 53, y: 63 }, { name: "Largo", latitude: 38.8976, longitude: -76.8303, x: 58, y: 64 }, { name: "Capitol Heights", latitude: 38.8851, longitude: -76.9158, x: 54, y: 67 }, { name: "District Heights", latitude: 38.8576, longitude: -76.8894, x: 56, y: 68 }, { name: "Suitland", latitude: 38.8487, longitude: -76.9239, x: 54, y: 70 }, { name: "Temple Hills", latitude: 38.814, longitude: -76.9458, x: 51, y: 70 }, { name: "Oxon Hill", latitude: 38.8034, longitude: -76.9897, x: 50, y: 73 }, { name: "Clinton", latitude: 38.7651, longitude: -76.8983, x: 55, y: 75 }, { name: "Upper Marlboro", latitude: 38.8159, longitude: -76.7497, x: 61, y: 69 }, { name: "Bowie", latitude: 38.9428, longitude: -76.7303, x: 62, y: 59 }, { name: "Crofton", latitude: 39.0018, longitude: -76.6875, x: 65, y: 54 }, { name: "Odenton", latitude: 39.084, longitude: -76.7002, x: 61, y: 48 }, { name: "Annapolis", latitude: 38.9784, longitude: -76.4922, x: 72, y: 55 }, { name: "Silver Spring", latitude: 38.9907, longitude: -77.0261, x: 42, y: 61 }, { name: "Takoma Park", latitude: 38.9779, longitude: -77.0075, x: 43, y: 63 }, { name: "Wheaton", latitude: 39.0398, longitude: -77.0553, x: 40, y: 58 }, { name: "Kensington", latitude: 39.0257, longitude: -77.0764, x: 38, y: 58 }, { name: "Rockville", latitude: 39.084, longitude: -77.1528, x: 33, y: 55 }, { name: "Bethesda", latitude: 38.9847, longitude: -77.0947, x: 38, y: 64 }, { name: "Gaithersburg", latitude: 39.1434, longitude: -77.2014, x: 27, y: 49 }, { name: "Germantown", latitude: 39.174252, longitude: -77.263754, x: 24, y: 46 }, { name: "Olney", latitude: 39.14434, longitude: -77.071429, x: 36, y: 48 }, { name: "Potomac", latitude: 39.013332, longitude: -77.193579, x: 32, y: 59 }, { name: "Frederick", latitude: 39.434089, longitude: -77.414539, x: 22, y: 32 }, { name: "Urbana", latitude: 39.32577, longitude: -77.341519, x: 25, y: 38 }, { name: "Mount Airy", latitude: 39.374693, longitude: -77.153807, x: 31, y: 34 }, { name: "Westminster", latitude: 39.579154, longitude: -77.006691, x: 38, y: 21 }, { name: "Bel Air", latitude: 39.534942, longitude: -76.346323, x: 70, y: 16 }, { name: "Aberdeen", latitude: 39.516403, longitude: -76.174518, x: 76, y: 18 }, { name: "Havre de Grace", latitude: 39.546572, longitude: -76.113576, x: 78, y: 15 }, { name: "Hagerstown", latitude: 39.640182, longitude: -77.722691, x: 10, y: 29 }, { name: "Waldorf", latitude: 38.609172, longitude: -76.919776, x: 55, y: 83 }, { name: "La Plata", latitude: 38.543467, longitude: -76.96979, x: 54, y: 88 }, { name: "Lexington Park", latitude: 38.249403, longitude: -76.443556, x: 70, y: 93 }, { name: "Salisbury", latitude: 38.375328, longitude: -75.585925, x: 91, y: 82 }, { name: "Ocean City", latitude: 38.393125, longitude: -75.071208, x: 98, y: 82 }, { name: "Aberdeen Proving Ground", latitude: 39.454232, longitude: -76.133542 }, { name: "Abingdon", latitude: 39.463472, longitude: -76.27314 }, { name: "Accident", latitude: 39.625688, longitude: -79.319958 }, { name: "Accokeek", latitude: 38.676533, longitude: -77.000518 }, { name: "Adamstown", latitude: 39.307001, longitude: -77.469296 }, { name: "Adelphi", latitude: 38.997067, longitude: -76.966783 }, { name: "Algonquin", latitude: 38.589853, longitude: -76.091558 }, { name: "Allen", latitude: 38.288387, longitude: -75.693051 }, { name: "Andrews AFB", latitude: 38.809803, longitude: -76.869158 }, { name: "Annapolis Neck", latitude: 38.936562, longitude: -76.498084 }, { name: "Antietam", latitude: 39.415082, longitude: -77.736484 }, { name: "Aquasco", latitude: 38.591534, longitude: -76.697125 }, { name: "Arbutus", latitude: 39.242673, longitude: -76.692133 }, { name: "Arden on the Severn", latitude: 39.06771, longitude: -76.596488 }, { name: "Arnold", latitude: 39.043227, longitude: -76.504985 }, { name: "Ashton-Sandy Spring", latitude: 39.148685, longitude: -76.999085 }, { name: "Aspen Hill", latitude: 39.093635, longitude: -77.081985 }, { name: "Baden", latitude: 38.669485, longitude: -76.744221 }, { name: "Bagtown", latitude: 39.582982, longitude: -77.613885 }, { name: "Bakersville", latitude: 39.514829, longitude: -77.757048 }, { name: "Ballenger Creek", latitude: 39.381131, longitude: -77.420179 }, { name: "Baltimore Highlands", latitude: 39.235932, longitude: -76.637402 }, { name: "Barclay", latitude: 39.146454, longitude: -75.864433 }, { name: "Barnesville", latitude: 39.22392, longitude: -77.376106 }, { name: "Barrelville", latitude: 39.702653, longitude: -78.842454 }, { name: "Barton", latitude: 39.532283, longitude: -79.016785 }, { name: "Bartonsville", latitude: 39.388795, longitude: -77.352961 }, { name: "Beaver Creek", latitude: 39.581545, longitude: -77.651338 }, { name: "Bel Air North", latitude: 39.553404, longitude: -76.372942 }, { name: "Bel Air South", latitude: 39.508061, longitude: -76.309365 }, { name: "Benedict", latitude: 38.511529, longitude: -76.67967 }, { name: "Bensville", latitude: 38.612971, longitude: -77.004735 }, { name: "Berlin", latitude: 38.331533, longitude: -75.215434 }, { name: "Berwyn Heights", latitude: 38.992854, longitude: -76.913451 }, { name: "Betterton", latitude: 39.367072, longitude: -76.072495 }, { name: "Bier", latitude: 39.55513, longitude: -78.871122 }, { name: "Big Pool", latitude: 39.625094, longitude: -78.016168 }, { name: "Big Spring", latitude: 39.625902, longitude: -77.939632 }, { name: "Bishopville", latitude: 38.438642, longitude: -75.209507 }, { name: "Bivalve", latitude: 38.306205, longitude: -75.880628 }, { name: "Bladensburg", latitude: 38.942368, longitude: -76.92591 }, { name: "Bloomington", latitude: 39.481331, longitude: -79.07851 }, { name: "Boonsboro", latitude: 39.510648, longitude: -77.664745 }, { name: "Bowleys Quarters", latitude: 39.31281, longitude: -76.38227 }, { name: "Bowling Green", latitude: 39.62713, longitude: -78.805046 }, { name: "Bowmans Addition", latitude: 39.686344, longitude: -78.755028 }, { name: "Braddock Heights", latitude: 39.409522, longitude: -77.493564 }, { name: "Brandywine", latitude: 38.682077, longitude: -76.885314 }, { name: "Breathedsville", latitude: 39.54601, longitude: -77.724548 }, { name: "Brentwood", latitude: 38.943883, longitude: -76.957046 }, { name: "Brock Hall", latitude: 38.855852, longitude: -76.742435 }, { name: "Brookeville", latitude: 39.182514, longitude: -77.059725 }, { name: "Brooklyn Park", latitude: 39.2195, longitude: -76.620921 }, { name: "Brookmont", latitude: 38.953831, longitude: -77.12902 }, { name: "Brookview", latitude: 38.574011, longitude: -75.792963 }, { name: "Broomes Island", latitude: 38.411433, longitude: -76.548803 }, { name: "Brown Station", latitude: 38.855332, longitude: -76.797065 }, { name: "Brownsville", latitude: 39.378174, longitude: -77.661555 }, { name: "Brunswick", latitude: 39.31799, longitude: -77.62528 }, { name: "Bryans Road", latitude: 38.6044, longitude: -77.092279 }, { name: "Bryantown", latitude: 38.548839, longitude: -76.842182 }, { name: "Buckeystown", latitude: 39.324138, longitude: -77.428135 }, { name: "Burkittsville", latitude: 39.393497, longitude: -77.627788 }, { name: "Burnt Mills", latitude: 39.033984, longitude: -76.998272 }, { name: "Burtonsville", latitude: 39.120921, longitude: -76.936675 }, { name: "Butlertown", latitude: 39.282194, longitude: -76.099165 }, { name: "Cabin John", latitude: 38.974014, longitude: -77.163841 }, { name: "California", latitude: 38.297599, longitude: -76.492143 }, { name: "Callaway", latitude: 38.233918, longitude: -76.528932 }, { name: "Calvert Beach", latitude: 38.472799, longitude: -76.489676 }, { name: "Calverton", latitude: 39.058085, longitude: -76.950848 }, { name: "Cambridge", latitude: 38.554063, longitude: -76.077116 }, { name: "Camp Springs", latitude: 38.805072, longitude: -76.918565 }, { name: "Cape St. Claire", latitude: 39.043634, longitude: -76.445339 }, { name: "Carlos", latitude: 39.623743, longitude: -78.95668 }, { name: "Carney", latitude: 39.404896, longitude: -76.522725 }, { name: "Cavetown", latitude: 39.642658, longitude: -77.5932 }, { name: "Cearfoss", latitude: 39.699133, longitude: -77.77634 }, { name: "Cecilton", latitude: 39.404824, longitude: -75.867412 }, { name: "Cedar Heights", latitude: 38.903738, longitude: -76.905879 }, { name: "Cedarville", latitude: 38.656912, longitude: -76.82201 }, { name: "Centreville", latitude: 39.042265, longitude: -76.062538 }, { name: "Chance", latitude: 38.17804, longitude: -75.938788 }, { name: "Charlestown", latitude: 39.582121, longitude: -75.986712 }, { name: "Charlotte Hall", latitude: 38.468236, longitude: -76.782645 }, { name: "Charlton", latitude: 39.634401, longitude: -77.894364 }, { name: "Chesapeake Beach", latitude: 38.689456, longitude: -76.554404 }, { name: "Chesapeake City", latitude: 39.527212, longitude: -75.811653 }, { name: "Chesapeake Landing", latitude: 39.269347, longitude: -76.157236 }, { name: "Chesapeake Ranch Estates", latitude: 38.357625, longitude: -76.417351 }, { name: "Chester", latitude: 38.960094, longitude: -76.287616 }, { name: "Chestertown", latitude: 39.218383, longitude: -76.074022 }, { name: "Cheverly", latitude: 38.925931, longitude: -76.913468 }, { name: "Chevy Chase", latitude: 38.992542, longitude: -77.074905 }, { name: "Chevy Chase Section Five", latitude: 38.98398, longitude: -77.074027 }, { name: "Chevy Chase Section Three", latitude: 38.979259, longitude: -77.074187 }, { name: "Chevy Chase View", latitude: 39.019201, longitude: -77.081104 }, { name: "Chevy Chase Village", latitude: 38.969781, longitude: -77.07934 }, { name: "Chewsville", latitude: 39.648415, longitude: -77.631239 }, { name: "Chillum", latitude: 38.966667, longitude: -76.978885 }, { name: "Choptank", latitude: 38.682282, longitude: -75.949398 }, { name: "Church Creek", latitude: 38.504888, longitude: -76.15403 }, { name: "Church Hill", latitude: 39.145, longitude: -75.98077 }, { name: "Clarksburg", latitude: 39.222893, longitude: -77.266079 }, { name: "Clarysville", latitude: 39.642026, longitude: -78.888947 }, { name: "Clear Spring", latitude: 39.65605, longitude: -77.930373 }, { name: "Cloverly", latitude: 39.103947, longitude: -76.994829 }, { name: "Cobb Island", latitude: 38.263698, longitude: -76.848986 }, { name: "Cockeysville", latitude: 39.478003, longitude: -76.630796 }, { name: "Colesville", latitude: 39.07284, longitude: -77.00058 }, { name: "Colmar Manor", latitude: 38.926801, longitude: -76.943876 }, { name: "Coral Hills", latitude: 38.872313, longitude: -76.920979 }, { name: "Cordova", latitude: 38.86801, longitude: -75.998885 }, { name: "Corriganville", latitude: 39.694646, longitude: -78.79726 }, { name: "Cottage City", latitude: 38.938399, longitude: -76.949445 }, { name: "Crellin", latitude: 39.388631, longitude: -79.468471 }, { name: "Cresaptown", latitude: 39.593506, longitude: -78.83505 }, { name: "Crisfield", latitude: 37.975457, longitude: -75.854542 }, { name: "Croom", latitude: 38.739799, longitude: -76.755192 }, { name: "Crownsville", latitude: 39.022462, longitude: -76.590377 }, { name: "Crumpton", latitude: 39.233174, longitude: -75.922049 }, { name: "Cumberland", latitude: 39.65131, longitude: -78.757959 }, { name: "Damascus", latitude: 39.270995, longitude: -77.196834 }, { name: "Dames Quarter", latitude: 38.17292, longitude: -75.890024 }, { name: "Danville", latitude: 39.510165, longitude: -78.917545 }, { name: "Dargan", latitude: 39.376676, longitude: -77.734045 }, { name: "Darlington", latitude: 39.642448, longitude: -76.203521 }, { name: "Darnestown", latitude: 39.088709, longitude: -77.311779 }, { name: "Dawson", latitude: 39.477083, longitude: -78.945207 }, { name: "Deale", latitude: 38.786091, longitude: -76.540708 }, { name: "Deal Island", latitude: 38.152793, longitude: -75.939954 }, { name: "Deer Park", latitude: 39.423961, longitude: -79.325986 }, { name: "Delmar", latitude: 38.442149, longitude: -75.560422 }, { name: "Denton", latitude: 38.879421, longitude: -75.824452 }, { name: "Derwood", latitude: 39.114116, longitude: -77.150228 }, { name: "Detmold", latitude: 39.557365, longitude: -78.991143 }, { name: "Downsville", latitude: 39.55482, longitude: -77.80163 }, { name: "Drum Point", latitude: 38.330395, longitude: -76.435985 }, { name: "Dunkirk", latitude: 38.718193, longitude: -76.676782 }, { name: "Eagle Harbor", latitude: 38.567577, longitude: -76.687137 }, { name: "Eakles Mill", latitude: 39.467067, longitude: -77.68596 }, { name: "East New Market", latitude: 38.597025, longitude: -75.923126 }, { name: "Easton", latitude: 38.776262, longitude: -76.070478 }, { name: "East Riverdale", latitude: 38.959762, longitude: -76.91102 }, { name: "Eckhart Mines", latitude: 39.655233, longitude: -78.89411 }, { name: "Eden", latitude: 38.278223, longitude: -75.654776 }, { name: "Edesville", latitude: 39.153861, longitude: -76.207235 }, { name: "Edgemere", latitude: 39.220671, longitude: -76.456375 }, { name: "Edgemont", latitude: 39.676611, longitude: -77.546948 }, { name: "Edgewater", latitude: 38.939653, longitude: -76.551401 }, { name: "Edgewood", latitude: 39.42029, longitude: -76.296846 }, { name: "Edmonston", latitude: 38.949965, longitude: -76.932196 }, { name: "Eldersburg", latitude: 39.404192, longitude: -76.952585 }, { name: "Eldorado", latitude: 38.58304, longitude: -75.790281 }, { name: "Elkridge", latitude: 39.195483, longitude: -76.741082 }, { name: "Elkton", latitude: 39.605776, longitude: -75.821686 }, { name: "Ellerslie", latitude: 39.713709, longitude: -78.776181 }, { name: "Elliott", latitude: 38.308141, longitude: -76.010718 }, { name: "Emmitsburg", latitude: 39.705192, longitude: -77.321301 }, { name: "Ernstville", latitude: 39.630387, longitude: -78.023955 }, { name: "Fairland", latitude: 39.08085, longitude: -76.952544 }, { name: "Fairlee", latitude: 39.22587, longitude: -76.166332 }, { name: "Fairmount", latitude: 38.109765, longitude: -75.820564 }, { name: "Fairmount Heights", latitude: 38.901591, longitude: -76.915339 }, { name: "Fairplay", latitude: 39.53567, longitude: -77.74631 }, { name: "Fairview", latitude: 39.711079, longitude: -77.840616 }, { name: "Fairwood", latitude: 38.954713, longitude: -76.776544 }, { name: "Fallston", latitude: 39.533804, longitude: -76.438545 }, { name: "Federalsburg", latitude: 38.692265, longitude: -75.772465 }, { name: "Ferndale", latitude: 39.18696, longitude: -76.633107 }, { name: "Finzel", latitude: 39.701721, longitude: -78.951997 }, { name: "Fishing Creek", latitude: 38.336953, longitude: -76.221683 }, { name: "Flintstone", latitude: 39.703531, longitude: -78.575786 }, { name: "Flower Hill", latitude: 39.168443, longitude: -77.184443 }, { name: "Forest Glen", latitude: 39.018914, longitude: -77.045085 }, { name: "Forest Heights", latitude: 38.803776, longitude: -77.011691 }, { name: "Forestville", latitude: 38.851737, longitude: -76.870765 }, { name: "Fort Meade", latitude: 39.1057, longitude: -76.743278 }, { name: "Fort Ritchie", latitude: 39.703659, longitude: -77.506389 }, { name: "Fort Washington", latitude: 38.731868, longitude: -77.008662 }, { name: "Fountainhead-Orchard Hills", latitude: 39.687838, longitude: -77.717298 }, { name: "Four Corners", latitude: 39.023312, longitude: -77.010204 }, { name: "Franklin", latitude: 39.498927, longitude: -79.051562 }, { name: "Frenchtown-Rumbly", latitude: 38.07445, longitude: -75.853323 }, { name: "Friendly", latitude: 38.760284, longitude: -76.966952 }, { name: "Friendship", latitude: 38.735845, longitude: -76.58782 }, { name: "Friendship Heights Village", latitude: 38.963311, longitude: -77.089792 }, { name: "Friendsville", latitude: 39.662448, longitude: -79.404414 }, { name: "Frostburg", latitude: 39.650523, longitude: -78.926793 }, { name: "Fruitland", latitude: 38.320393, longitude: -75.626502 }, { name: "Fulton", latitude: 39.15311, longitude: -76.911629 }, { name: "Funkstown", latitude: 39.606743, longitude: -77.705137 }, { name: "Galena", latitude: 39.342465, longitude: -75.878873 }, { name: "Galestown", latitude: 38.562606, longitude: -75.715814 }, { name: "Galesville", latitude: 38.84077, longitude: -76.554368 }, { name: "Gambrills", latitude: 39.092725, longitude: -76.651034 }, { name: "Gapland", latitude: 39.40196, longitude: -77.658288 }, { name: "Garrett Park", latitude: 39.036129, longitude: -77.093547 }, { name: "Garretts Mill", latitude: 39.353335, longitude: -77.688911 }, { name: "Garrison", latitude: 39.402274, longitude: -76.751847 }, { name: "Georgetown", latitude: 39.221577, longitude: -76.191772 }, { name: "Gilmore", latitude: 39.58326, longitude: -78.951127 }, { name: "Girdletree", latitude: 38.098879, longitude: -75.400257 }, { name: "Glassmanor", latitude: 38.818138, longitude: -76.983152 }, { name: "Glenarden", latitude: 38.929288, longitude: -76.857691 }, { name: "Glen Echo", latitude: 38.968167, longitude: -77.141101 }, { name: "Glenmont", latitude: 39.070413, longitude: -77.046628 }, { name: "Glenn Dale", latitude: 38.984395, longitude: -76.800322 }, { name: "Golden Beach", latitude: 38.489734, longitude: -76.700689 }, { name: "Goldsboro", latitude: 39.030003, longitude: -75.780955 }, { name: "Gorman", latitude: 39.292702, longitude: -79.35275 }, { name: "Graceham", latitude: 39.617423, longitude: -77.387024 }, { name: "Grahamtown", latitude: 39.644861, longitude: -78.922288 }, { name: "Grantsville", latitude: 39.696916, longitude: -79.152849 }, { name: "Grasonville", latitude: 38.957459, longitude: -76.197803 }, { name: "Greensboro", latitude: 38.976354, longitude: -75.808076 }, { name: "Greensburg", latitude: 39.68075, longitude: -77.561545 }, { name: "Green Valley", latitude: 39.341841, longitude: -77.240301 }, { name: "Halfway", latitude: 39.61567, longitude: -77.771068 }, { name: "Hampstead", latitude: 39.614597, longitude: -76.854801 }, { name: "Hampton", latitude: 39.424788, longitude: -76.566473 }, { name: "Hancock", latitude: 39.704578, longitude: -78.16327 }, { name: "Hebron", latitude: 38.42426, longitude: -75.687163 }, { name: "Henderson", latitude: 39.074942, longitude: -75.766077 }, { name: "Herald Harbor", latitude: 39.051265, longitude: -76.57492 }, { name: "Highfield-Cascade", latitude: 39.713592, longitude: -77.494566 }, { name: "Highland", latitude: 39.187223, longitude: -76.956731 }, { name: "Highland Beach", latitude: 38.931139, longitude: -76.467018 }, { name: "Hillandale", latitude: 39.026341, longitude: -76.975202 }, { name: "Hillcrest Heights", latitude: 38.836453, longitude: -76.970583 }, { name: "Hillsboro", latitude: 38.917043, longitude: -75.941779 }, { name: "Honeygo", latitude: 39.404969, longitude: -76.430016 }, { name: "Hughesville", latitude: 38.53742, longitude: -76.772789 }, { name: "Huntingtown", latitude: 38.61235, longitude: -76.62154 }, { name: "Hurlock", latitude: 38.625712, longitude: -75.867186 }, { name: "Hutton", latitude: 39.414494, longitude: -79.480044 }, { name: "Ilchester", latitude: 39.21611, longitude: -76.762058 }, { name: "Indian Head", latitude: 38.598622, longitude: -77.155654 }, { name: "Indian Springs", latitude: 39.645634, longitude: -78.007402 }, { name: "Jarrettsville", latitude: 39.600745, longitude: -76.47241 }, { name: "Jefferson", latitude: 39.365345, longitude: -77.540671 }, { name: "Jennings", latitude: 39.648657, longitude: -79.183497 }, { name: "Jessup", latitude: 39.149094, longitude: -76.776388 }, { name: "Jesterville", latitude: 38.290479, longitude: -75.889929 }, { name: "Joppatowne", latitude: 39.416594, longitude: -76.352171 }, { name: "Jugtown", latitude: 39.614277, longitude: -77.5943 }, { name: "Keedysville", latitude: 39.486338, longitude: -77.697552 }, { name: "Kemp Mill", latitude: 39.041101, longitude: -77.021995 }, { name: "Kemps Mill", latitude: 39.62687, longitude: -77.813902 }, { name: "Kennedyville", latitude: 39.303213, longitude: -75.994244 }, { name: "Kent Narrows", latitude: 38.976483, longitude: -76.243412 }, { name: "Kettering", latitude: 38.889656, longitude: -76.790302 }, { name: "Kingstown", latitude: 39.207626, longitude: -76.04391 }, { name: "Kingsville", latitude: 39.451907, longitude: -76.430345 }, { name: "Kitzmiller", latitude: 39.389159, longitude: -79.183284 }, { name: "Klondike", latitude: 39.610247, longitude: -78.96314 }, { name: "Konterra", latitude: 39.080797, longitude: -76.899575 }, { name: "Lake Arbor", latitude: 38.90957, longitude: -76.830236 }, { name: "Lake Shore", latitude: 39.091978, longitude: -76.489628 }, { name: "Landover Hills", latitude: 38.942465, longitude: -76.894505 }, { name: "Langley Park", latitude: 38.989499, longitude: -76.980742 }, { name: "Lansdowne", latitude: 39.235573, longitude: -76.664629 }, { name: "La Vale", latitude: 39.67164, longitude: -78.826759 }, { name: "Layhill", latitude: 39.089652, longitude: -77.039971 }, { name: "Laytonsville", latitude: 39.208748, longitude: -77.135313 }, { name: "Leisure World", latitude: 39.104079, longitude: -77.06894 }, { name: "Leitersburg", latitude: 39.692806, longitude: -77.620642 }, { name: "Leonardtown", latitude: 38.303891, longitude: -76.639653 }, { name: "Lewistown", latitude: 39.54021, longitude: -77.420555 }, { name: "Libertytown", latitude: 39.48911, longitude: -77.256919 }, { name: "Linganore", latitude: 39.413664, longitude: -77.301232 }, { name: "Linthicum", latitude: 39.209567, longitude: -76.664741 }, { name: "Lisbon", latitude: 39.336634, longitude: -77.070491 }, { name: "Little Orleans", latitude: 39.630509, longitude: -78.395843 }, { name: "Lochearn", latitude: 39.34793, longitude: -76.727214 }, { name: "Loch Lynn Heights", latitude: 39.391832, longitude: -79.372671 }, { name: "Lonaconing", latitude: 39.565581, longitude: -78.978791 }, { name: "Long Beach", latitude: 38.45884, longitude: -76.473643 }, { name: "Luke", latitude: 39.478781, longitude: -79.058903 }, { name: "Lusby", latitude: 38.362356, longitude: -76.437915 }, { name: "Lutherville", latitude: 39.423965, longitude: -76.61767 }, { name: "McCoole", latitude: 39.453357, longitude: -78.973088 }, { name: "Madison", latitude: 38.508928, longitude: -76.204222 }, { name: "Manchester", latitude: 39.653159, longitude: -76.886735 }, { name: "Mapleville", latitude: 39.535702, longitude: -77.646244 }, { name: "Mardela Springs", latitude: 38.457629, longitude: -75.755482 }, { name: "Marlboro Meadows", latitude: 38.841853, longitude: -76.711958 }, { name: "Marlboro Village", latitude: 38.834902, longitude: -76.768824 }, { name: "Marlow Heights", latitude: 38.821119, longitude: -76.944294 }, { name: "Marlton", latitude: 38.761088, longitude: -76.786536 }, { name: "Martin's Additions", latitude: 38.979552, longitude: -77.069235 }, { name: "Marydel", latitude: 39.113071, longitude: -75.74951 }, { name: "Maryland City", latitude: 39.101609, longitude: -76.805188 }, { name: "Maryland Park", latitude: 38.889009, longitude: -76.907551 }, { name: "Maugansville", latitude: 39.693681, longitude: -77.747151 }, { name: "Mayo", latitude: 38.904436, longitude: -76.512842 }, { name: "Mays Chapel", latitude: 39.442523, longitude: -76.658627 }, { name: "Mechanicsville", latitude: 38.42743, longitude: -76.74664 }, { name: "Melwood", latitude: 38.801893, longitude: -76.841624 }, { name: "Mercersville", latitude: 39.49921, longitude: -77.765825 }, { name: "Middleburg", latitude: 39.71768, longitude: -77.723878 }, { name: "Middle River", latitude: 39.339519, longitude: -76.428702 }, { name: "Middletown", latitude: 39.44134, longitude: -77.535216 }, { name: "Midland", latitude: 39.589623, longitude: -78.9487 }, { name: "Midlothian", latitude: 39.631868, longitude: -78.951501 }, { name: "Milford Mill", latitude: 39.348002, longitude: -76.769409 }, { name: "Millington", latitude: 39.261444, longitude: -75.84259 }, { name: "Mitchellville", latitude: 38.936432, longitude: -76.808246 }, { name: "Monrovia", latitude: 39.359462, longitude: -77.274891 }, { name: "Montgomery Village", latitude: 39.188512, longitude: -77.205143 }, { name: "Morningside", latitude: 38.826587, longitude: -76.889622 }, { name: "Moscow", latitude: 39.538292, longitude: -79.009674 }, { name: "Mount Aetna", latitude: 39.59888, longitude: -77.612768 }, { name: "Mountain Lake Park", latitude: 39.40003, longitude: -79.381051 }, { name: "Mount Briar", latitude: 39.442571, longitude: -77.687346 }, { name: "Mount Lena", latitude: 39.554029, longitude: -77.621891 }, { name: "Mount Rainier", latitude: 38.942361, longitude: -76.964578 }, { name: "Mount Savage", latitude: 39.696684, longitude: -78.876833 }, { name: "Mount Vernon", latitude: 38.239562, longitude: -75.785308 }, { name: "Myersville", latitude: 39.510233, longitude: -77.571363 }, { name: "Nanticoke", latitude: 38.265488, longitude: -75.886973 }, { name: "Nanticoke Acres", latitude: 38.257873, longitude: -75.905841 }, { name: "National", latitude: 39.611928, longitude: -78.940444 }, { name: "National Harbor", latitude: 38.783231, longitude: -77.008114 }, { name: "Naval Academy", latitude: 38.984784, longitude: -76.482628 }, { name: "Newark", latitude: 38.268109, longitude: -75.288638 }, { name: "New Market", latitude: 39.39142, longitude: -77.273431 }, { name: "New Windsor", latitude: 39.54295, longitude: -77.099157 }, { name: "Nikep", latitude: 39.551568, longitude: -78.99773 }, { name: "North Beach", latitude: 38.707736, longitude: -76.5345 }, { name: "North Bethesda", latitude: 39.036753, longitude: -77.120336 }, { name: "North Brentwood", latitude: 38.944806, longitude: -76.950742 }, { name: "North Chevy Chase", latitude: 39.002145, longitude: -77.074145 }, { name: "North East", latitude: 39.608037, longitude: -75.941655 }, { name: "North Kensington", latitude: 39.039196, longitude: -77.071562 }, { name: "North Laurel", latitude: 39.128849, longitude: -76.846597 }, { name: "North Potomac", latitude: 39.09677, longitude: -77.238503 }, { name: "Oakland", latitude: 39.417143, longitude: -79.402352 }, { name: "Ocean", latitude: 39.601533, longitude: -78.945033 }, { name: "Ocean Pines", latitude: 38.384784, longitude: -75.148009 }, { name: "Oldtown", latitude: 39.544966, longitude: -78.614743 }, { name: "Overlea", latitude: 39.364143, longitude: -76.517548 }, { name: "Owings", latitude: 38.711906, longitude: -76.603723 }, { name: "Owings Mills", latitude: 39.410494, longitude: -76.789371 }, { name: "Oxford", latitude: 38.687407, longitude: -76.168182 }, { name: "Paramount-Long Meadow", latitude: 39.679776, longitude: -77.692106 }, { name: "Parole", latitude: 38.987941, longitude: -76.552716 }, { name: "Parsonsburg", latitude: 38.391673, longitude: -75.479581 }, { name: "Pasadena", latitude: 39.155703, longitude: -76.559807 }, { name: "Pecktonville", latitude: 39.665971, longitude: -78.048184 }, { name: "Peppermill Village", latitude: 38.893998, longitude: -76.887694 }, { name: "Perry Hall", latitude: 39.406741, longitude: -76.477793 }, { name: "Perryman", latitude: 39.464679, longitude: -76.215865 }, { name: "Perryville", latitude: 39.572968, longitude: -76.064923 }, { name: "Pinesburg", latitude: 39.626891, longitude: -77.856121 }, { name: "Piney Point", latitude: 38.147854, longitude: -76.522283 }, { name: "Pittsville", latitude: 38.394002, longitude: -75.407178 }, { name: "Pleasant Grove", latitude: 39.680201, longitude: -78.690514 }, { name: "Pleasant Hills", latitude: 39.486182, longitude: -76.393933 }, { name: "Pocomoke City", latitude: 38.063177, longitude: -75.559199 }, { name: "Point of Rocks", latitude: 39.278194, longitude: -77.529101 }, { name: "Pomfret", latitude: 38.569904, longitude: -77.030372 }, { name: "Pondsville", latitude: 39.62341, longitude: -77.591619 }, { name: "Poolesville", latitude: 39.141827, longitude: -77.410508 }, { name: "Port Deposit", latitude: 39.611015, longitude: -76.099006 }, { name: "Port Tobacco Village", latitude: 38.512292, longitude: -77.020487 }, { name: "Potomac Heights", latitude: 38.598207, longitude: -77.132328 }, { name: "Potomac Park", latitude: 39.612588, longitude: -78.808251 }, { name: "Powellville", latitude: 38.330069, longitude: -75.374698 }, { name: "Preston", latitude: 38.710212, longitude: -75.907528 }, { name: "Prince Frederick", latitude: 38.543791, longitude: -76.587906 }, { name: "Princess Anne", latitude: 38.205137, longitude: -75.696384 }, { name: "Pylesville", latitude: 39.687921, longitude: -76.388427 }, { name: "Quantico", latitude: 38.379983, longitude: -75.755014 }, { name: "Queen Anne", latitude: 38.91907, longitude: -75.953567 }, { name: "Queensland", latitude: 38.795192, longitude: -76.795634 }, { name: "Queenstown", latitude: 38.986731, longitude: -76.163342 }, { name: "Randallstown", latitude: 39.371868, longitude: -76.801926 }, { name: "Rawlings", latitude: 39.539229, longitude: -78.886762 }, { name: "Redland", latitude: 39.132868, longitude: -77.145448 }, { name: "Reid", latitude: 39.712508, longitude: -77.679313 }, { name: "Reisterstown", latitude: 39.454112, longitude: -76.816023 }, { name: "Ridgely", latitude: 38.952933, longitude: -75.882704 }, { name: "Ringgold", latitude: 39.709169, longitude: -77.568892 }, { name: "Rising Sun", latitude: 39.701401, longitude: -76.060181 }, { name: "Riva", latitude: 38.945219, longitude: -76.589928 }, { name: "Riverside", latitude: 39.480652, longitude: -76.241866 }, { name: "Riviera Beach", latitude: 39.164543, longitude: -76.527418 }, { name: "Robinwood", latitude: 39.62647, longitude: -77.662668 }, { name: "Rock Hall", latitude: 39.140541, longitude: -76.240491 }, { name: "Rock Point", latitude: 38.275467, longitude: -76.84275 }, { name: "Rohrersville", latitude: 39.434897, longitude: -77.665474 }, { name: "Romancoke", latitude: 38.891825, longitude: -76.36078 }, { name: "Rosaryville", latitude: 38.767603, longitude: -76.828092 }, { name: "Rosedale", latitude: 39.327159, longitude: -76.508377 }, { name: "Rosemont", latitude: 39.333729, longitude: -77.62276 }, { name: "Rossville", latitude: 39.356566, longitude: -76.477894 }, { name: "Sabillasville", latitude: 39.699364, longitude: -77.457277 }, { name: "St. George Island", latitude: 38.11554, longitude: -76.476965 }, { name: "St. James", latitude: 39.573816, longitude: -77.748248 }, { name: "St. Leonard", latitude: 38.467369, longitude: -76.49863 }, { name: "St. Michaels", latitude: 38.788451, longitude: -76.222622 }, { name: "Sandy Hook", latitude: 39.328505, longitude: -77.705149 }, { name: "San Mar", latitude: 39.552216, longitude: -77.640901 }, { name: "Savage", latitude: 39.146463, longitude: -76.821637 }, { name: "Scaggsville", latitude: 39.140653, longitude: -76.882576 }, { name: "Seabrook", latitude: 38.982111, longitude: -76.851115 }, { name: "Seat Pleasant", latitude: 38.894689, longitude: -76.899451 }, { name: "Secretary", latitude: 38.608193, longitude: -75.946955 }, { name: "Severna Park", latitude: 39.086633, longitude: -76.564883 }, { name: "Shady Side", latitude: 38.829011, longitude: -76.519647 }, { name: "Shaft", latitude: 39.622319, longitude: -78.942893 }, { name: "Sharpsburg", latitude: 39.457618, longitude: -77.749312 }, { name: "Sharptown", latitude: 38.538188, longitude: -75.718957 }, { name: "Silver Hill", latitude: 38.839841, longitude: -76.938593 }, { name: "Smith Island", latitude: 37.97722, longitude: -76.028791 }, { name: "Smithsburg", latitude: 39.654733, longitude: -77.579941 }, { name: "Snow Hill", latitude: 38.172519, longitude: -75.390403 }, { name: "Solomons", latitude: 38.339089, longitude: -76.461901 }, { name: "Somerset", latitude: 38.96657, longitude: -77.096319 }, { name: "South Kensington", latitude: 39.016149, longitude: -77.066293 }, { name: "South Laurel", latitude: 39.061466, longitude: -76.846194 }, { name: "Spencerville", latitude: 39.120024, longitude: -76.983527 }, { name: "Springdale", latitude: 38.935802, longitude: -76.844233 }, { name: "Spring Gap", latitude: 39.565335, longitude: -78.705332 }, { name: "Spring Ridge", latitude: 39.405041, longitude: -77.33988 }, { name: "Stevensville", latitude: 38.974441, longitude: -76.318511 }, { name: "Still Pond", latitude: 39.325957, longitude: -76.042355 }, { name: "Stockton", latitude: 38.065387, longitude: -75.40842 }, { name: "Sudlersville", latitude: 39.183261, longitude: -75.85354 }, { name: "Summerfield", latitude: 38.904538, longitude: -76.868297 }, { name: "Swanton", latitude: 39.459864, longitude: -79.232695 }, { name: "Sykesville", latitude: 39.372069, longitude: -76.97172 }, { name: "Tall Timbers", latitude: 38.169777, longitude: -76.539277 }, { name: "Taneytown", latitude: 39.657516, longitude: -77.168973 }, { name: "Taylors Island", latitude: 38.471135, longitude: -76.316009 }, { name: "Templeville", latitude: 39.136364, longitude: -75.766849 }, { name: "Ten Mile Creek", latitude: 39.229361, longitude: -77.298206 }, { name: "Thurmont", latitude: 39.619976, longitude: -77.407695 }, { name: "Tilghman Island", latitude: 38.703188, longitude: -76.336282 }, { name: "Tilghmanton", latitude: 39.528604, longitude: -77.743683 }, { name: "Timonium", latitude: 39.445744, longitude: -76.600758 }, { name: "Tolchester", latitude: 39.218004, longitude: -76.232083 }, { name: "Trappe", latitude: 38.663461, longitude: -76.05192 }, { name: "Travilah", latitude: 39.059263, longitude: -77.250633 }, { name: "Trego-Rohrersville Station", latitude: 39.429107, longitude: -77.674853 }, { name: "Tyaskin", latitude: 38.320408, longitude: -75.872635 }, { name: "Union Bridge", latitude: 39.571028, longitude: -77.172753 }, { name: "University Park", latitude: 38.971867, longitude: -76.944432 }, { name: "Vale Summit", latitude: 39.615229, longitude: -78.908354 }, { name: "Vienna", latitude: 38.481132, longitude: -75.831813 }, { name: "Walker Mill", latitude: 38.875642, longitude: -76.885442 }, { name: "Walkersville", latitude: 39.474323, longitude: -77.363352 }, { name: "Washington Grove", latitude: 39.140699, longitude: -77.174533 }, { name: "Waterview", latitude: 38.248736, longitude: -75.900009 }, { name: "West Denton", latitude: 38.889605, longitude: -75.83884 }, { name: "Westernport", latitude: 39.487468, longitude: -79.042087 }, { name: "West Laurel", latitude: 39.115711, longitude: -76.892291 }, { name: "West Ocean City", latitude: 38.347321, longitude: -75.111243 }, { name: "Westphalia", latitude: 38.840142, longitude: -76.824037 }, { name: "West Pocomoke", latitude: 38.096527, longitude: -75.579197 }, { name: "Whaleyville", latitude: 38.387037, longitude: -75.297538 }, { name: "Whitehaven", latitude: 38.269761, longitude: -75.79474 }, { name: "White Marsh", latitude: 39.381546, longitude: -76.45842 }, { name: "White Oak", latitude: 39.044212, longitude: -76.987251 }, { name: "Wildewood", latitude: 38.30351, longitude: -76.547052 }, { name: "Willards", latitude: 38.392425, longitude: -75.349406 }, { name: "Williamsport", latitude: 39.597345, longitude: -77.817972 }, { name: "Williston", latitude: 38.830321, longitude: -75.851454 }, { name: "Wilson-Conococheague", latitude: 39.651247, longitude: -77.827072 }, { name: "Woodland", latitude: 39.608341, longitude: -78.950111 }, { name: "Woodlawn", latitude: 38.950327, longitude: -76.900261 }, { name: "Woodmore", latitude: 38.923412, longitude: -76.777931 }, { name: "Woodsboro", latitude: 39.534026, longitude: -77.30991 }, { name: "Worton", latitude: 39.270657, longitude: -76.091824 }, { name: "Yarrowsburg", latitude: 39.376172, longitude: -77.684346 }, { name: "Zihlman", latitude: 39.674021, longitude: -78.913129 } ]; const cityAreaOverrides = { Cameroon: { Douala: [ { name: "Akwa", x: 47, y: 48 }, { name: "Bonaberi", x: 19, y: 56 }, { name: "Bonamoussadi", x: 58, y: 25 }, { name: "Bepanda", x: 39, y: 32 }, { name: "Deido", x: 35, y: 44 }, { name: "Logbessou", x: 70, y: 19 }, { name: "Ndokoti", x: 65, y: 55 }, { name: "Makepe", x: 55, y: 36 } ], Yaounde: [ { name: "Bastos", x: 44, y: 29 }, { name: "Mvan", x: 57, y: 70 }, { name: "Biyem-Assi", x: 31, y: 61 }, { name: "Mokolo", x: 38, y: 44 }, { name: "Essos", x: 60, y: 42 }, { name: "Etoudi", x: 51, y: 20 }, { name: "Nlongkak", x: 48, y: 38 } ], Bamenda: [ { name: "Commercial Avenue", x: 46, y: 50 }, { name: "Nkwen", x: 60, y: 31 }, { name: "Mile 4", x: 36, y: 62 }, { name: "Up Station", x: 42, y: 27 }, { name: "Food Market", x: 53, y: 56 } ], Buea: [ { name: "Molyko", x: 55, y: 47 }, { name: "Mile 17", x: 42, y: 61 }, { name: "Great Soppo", x: 47, y: 37 }, { name: "Buea Town", x: 36, y: 49 }, { name: "Bonduma", x: 62, y: 33 } ], Kumba: [ { name: "Kumba Town", x: 48, y: 50 }, { name: "Fiango", x: 60, y: 43 }, { name: "Mbonge Road", x: 36, y: 60 }, { name: "Buea Road", x: 56, y: 30 }, { name: "Main Market", x: 45, y: 56 }, { name: "Three Corners", x: 66, y: 58 }, { name: "Kosala", x: 57, y: 30 }, { name: "Mambanda", x: 73, y: 35 }, { name: "Barombi Kang", x: 51, y: 72 }, { name: "Kake", x: 33, y: 64 } ], Bambui: [ { name: "Bambui Center", x: 48, y: 50 }, { name: "Bambui Village", x: 46, y: 52 }, { name: "National Polytechnic Bambui", x: 35, y: 58 } ], Bambili: [ { name: "Bambili Center", x: 48, y: 50 }, { name: "University of Bamenda", x: 51, y: 38 }, { name: "Bambili Village", x: 47, y: 54 } ], Tiko: [ { name: "Tiko Town", x: 48, y: 50 }, { name: "Mutengene", x: 27, y: 43 }, { name: "Likomba", x: 41, y: 48 }, { name: "New Quarter Junction", x: 50, y: 56 }, { name: "Tiko Airport", x: 45, y: 34 }, { name: "Missellele", x: 74, y: 32 }, { name: "Port de Tiko", x: 60, y: 68 } ], Ebolowa: [ { name: "Ebolowa Center", x: 48, y: 50 }, { name: "Ebolowa Airport", x: 62, y: 64 }, { name: "Angale", x: 61, y: 34 } ], Maroua: [ { name: "Maroua Center", x: 48, y: 50 }, { name: "Djarengol", x: 52, y: 45 }, { name: "Domayo", x: 54, y: 55 }, { name: "Palar", x: 39, y: 52 }, { name: "Salak", x: 32, y: 72 } ], Ngaoundere: [ { name: "Ngaoundere Center", x: 48, y: 50 }, { name: "Railway Station", x: 50, y: 44 }, { name: "Dang", x: 39, y: 25 }, { name: "University of Ngaoundere", x: 37, y: 27 }, { name: "Mbideng", x: 50, y: 58 }, { name: "Bamyanga", x: 42, y: 70 } ], Garoua: [ { name: "Garoua Center", x: 48, y: 50 }, { name: "Garoua Airport", x: 40, y: 34 }, { name: "Poumpoumre", x: 58, y: 37 }, { name: "Roumde Adjia", x: 55, y: 43 }, { name: "Marouare", x: 55, y: 38 }, { name: "Lainde", x: 70, y: 28 }, { name: "Nassarao", x: 75, y: 22 } ], Limbe: [ { name: "Down Beach", x: 54, y: 66 }, { name: "Mile 4", x: 43, y: 43 }, { name: "Bota", x: 36, y: 55 }, { name: "New Town", x: 59, y: 42 }, { name: "Church Street", x: 48, y: 52 } ], Bafoussam: [ { name: "Tamja", x: 45, y: 39 }, { name: "Kamkop", x: 57, y: 27 }, { name: "Banengo", x: 39, y: 57 }, { name: "Djeleng", x: 65, y: 62 }, { name: "Tougang", x: 30, y: 36 } ] }, Ghana: { Accra: [ { name: "Osu", x: 56, y: 59 }, { name: "Madina", x: 63, y: 32 }, { name: "Kaneshie", x: 36, y: 55 }, { name: "Airport", x: 51, y: 39 }, { name: "Tema", x: 78, y: 50 } ] }, Kenya: { Nairobi: [ { name: "CBD", x: 50, y: 50 }, { name: "Westlands", x: 38, y: 38 }, { name: "Kilimani", x: 44, y: 62 }, { name: "Eastleigh", x: 62, y: 39 }, { name: "Embakasi", x: 75, y: 58 } ] }, Nigeria: { Lagos: [ { name: "Ikeja", x: 48, y: 32 }, { name: "Yaba", x: 44, y: 55 }, { name: "Lekki", x: 70, y: 68 }, { name: "Surulere", x: 35, y: 54 }, { name: "Victoria Island", x: 58, y: 72 } ] }, Senegal: { Dakar: [ { name: "Plateau", x: 71, y: 66 }, { name: "Medina", x: 62, y: 58 }, { name: "Grand Yoff", x: 43, y: 44 }, { name: "Ouakam", x: 32, y: 33 }, { name: "Pikine", x: 22, y: 52 } ] }, "United States": { Maryland: marylandLaunchPlaces.map((place) => ({ name: place.name, x: place.x ?? marylandPlaceMapX(place.longitude), y: place.y ?? marylandPlaceMapY(place.latitude) })) } }; const launchGpsTownCenters = { Cameroon: { Douala: [ { name: "Akwa", latitude: 4.052544, longitude: 9.69633 }, { name: "Bonaberi", latitude: 4.08221, longitude: 9.664972 }, { name: "Bonamoussadi", latitude: 4.082635, longitude: 9.75451 }, { name: "Bepanda", latitude: 4.056542, longitude: 9.722721 }, { name: "Deido", latitude: 4.063763, longitude: 9.715183 }, { name: "Logbessou", latitude: 4.084473, longitude: 9.784229 }, { name: "Ndokoti", latitude: 4.043434, longitude: 9.743678 }, { name: "Makepe", latitude: 4.082632, longitude: 9.757033 } ], Yaounde: [ { name: "Bastos", latitude: 3.892741, longitude: 11.519771 }, { name: "Mvan", latitude: 3.827574, longitude: 11.517617 }, { name: "Biyem-Assi", latitude: 3.849114, longitude: 11.486129 }, { name: "Mokolo", latitude: 3.875265, longitude: 11.494976 }, { name: "Essos", latitude: 3.875344, longitude: 11.545841 }, { name: "Etoudi", latitude: 3.920765, longitude: 11.526946 }, { name: "Nlongkak", latitude: 3.8852, longitude: 11.5202 } ], Bamenda: [ { name: "Commercial Avenue", latitude: 5.955834, longitude: 10.151706 }, { name: "Nkwen", latitude: 5.959514, longitude: 10.167322 }, { name: "Mile 4", latitude: 5.990921, longitude: 10.18537 }, { name: "Up Station", latitude: 5.949937, longitude: 10.165564 }, { name: "Food Market", latitude: 5.961424, longitude: 10.151375 } ], Bafoussam: [ { name: "Tamja", latitude: 5.4778, longitude: 10.4048 }, { name: "Kamkop", latitude: 5.509619, longitude: 10.379387 }, { name: "Banengo", latitude: 5.46431, longitude: 10.416783 }, { name: "Djeleng", latitude: 5.478571, longitude: 10.414446 }, { name: "Tougang", latitude: 5.488508, longitude: 10.446283 } ], Limbe: [ { name: "Down Beach", latitude: 4.000006, longitude: 9.211412 }, { name: "Mile 4", latitude: 4.0365, longitude: 9.1976 }, { name: "Bota", latitude: 4.012885, longitude: 9.201177 }, { name: "New Town", latitude: 4.011122, longitude: 9.217198 }, { name: "Church Street", latitude: 4.015015, longitude: 9.209452 } ], Buea: [ { name: "Molyko", latitude: 4.159115, longitude: 9.280517 }, { name: "Mile 17", latitude: 4.150243, longitude: 9.300125 }, { name: "Great Soppo", latitude: 4.154943, longitude: 9.249581 }, { name: "Buea Town", latitude: 4.15679, longitude: 9.231592 }, { name: "Bonduma", latitude: 4.160275, longitude: 9.268683 } ], Kumba: [ { name: "Kumba Town", latitude: 4.6378368, longitude: 9.4409446 }, { name: "Fiango", latitude: 4.6273, longitude: 9.45 }, { name: "Mbonge Road", latitude: 4.6255454, longitude: 9.4201867 }, { name: "Buea Road", latitude: 4.6322132, longitude: 9.4502527 }, { name: "Main Market", latitude: 4.6339034, longitude: 9.436732 }, { name: "Three Corners", latitude: 4.6332, longitude: 9.4718 }, { name: "Kosala", latitude: 4.6519701, longitude: 9.4507631 }, { name: "Mambanda", latitude: 4.6442, longitude: 9.4881 }, { name: "Barombi Kang", latitude: 4.6074, longitude: 9.4577 }, { name: "Kake", latitude: 4.6187, longitude: 9.4128 } ], Bambui: [ { name: "Bambui Center", latitude: 6.014621, longitude: 10.2315906 }, { name: "Bambui Village", latitude: 6.05, longitude: 10.23333 }, { name: "National Polytechnic Bambui", latitude: 6.0066, longitude: 10.218 }, ], Bambili: [ { name: "Bambili Center", latitude: 6.0043318, longitude: 10.2573289 }, { name: "University of Bamenda", latitude: 6.0125129, longitude: 10.2579645 }, { name: "Bambili Village", latitude: 5.98949, longitude: 10.25064 } ], Tiko: [ { name: "Tiko Town", latitude: 4.075571, longitude: 9.3650187 }, { name: "Mutengene", latitude: 4.09011, longitude: 9.3169578 }, { name: "Likomba", latitude: 4.0885202, longitude: 9.344429 }, { name: "New Quarter Junction", latitude: 4.0679509, longitude: 9.367807 }, { name: "Tiko Airport", latitude: 4.0900417, longitude: 9.3604058 }, { name: "Missellele", latitude: 4.1213293, longitude: 9.445525 }, { name: "Port de Tiko", latitude: 4.0658314, longitude: 9.3943686 } ], Ebolowa: [ { name: "Ebolowa Center", latitude: 2.919384, longitude: 11.1516632 }, { name: "Ebolowa Airport", latitude: 2.876, longitude: 11.185 }, { name: "Angale", latitude: 2.9346362, longitude: 11.1659816 } ], Maroua: [ { name: "Maroua Center", latitude: 10.6033083, longitude: 14.3281625 }, { name: "Djarengol", latitude: 10.58875, longitude: 14.30995 }, { name: "Domayo", latitude: 10.57484, longitude: 14.3362 }, { name: "Palar", latitude: 10.601215, longitude: 14.287532 }, { name: "Salak", latitude: 10.45149, longitude: 14.23967 } ], Ngaoundere: [ { name: "Ngaoundere Center", latitude: 7.3211536, longitude: 13.5878214 }, { name: "Railway Station", latitude: 7.3386279, longitude: 13.5897227 }, { name: "Dang", latitude: 7.428828, longitude: 13.5574798 }, { name: "University of Ngaoundere", latitude: 7.4184787, longitude: 13.5433216 }, { name: "Mbideng", latitude: 7.3078993, longitude: 13.5874748 }, { name: "Bamyanga", latitude: 7.276761, longitude: 13.5735869 } ], Garoua: [ { name: "Garoua Center", latitude: 9.3070698, longitude: 13.3934527 }, { name: "Garoua Airport", latitude: 9.3372216, longitude: 13.377202 }, { name: "Poumpoumre", latitude: 9.3311876, longitude: 13.4089963 }, { name: "Roumde Adjia", latitude: 9.3263296, longitude: 13.3992124 }, { name: "Marouare", latitude: 9.3280964, longitude: 13.400272 }, { name: "Lainde", latitude: 9.34327, longitude: 13.41916 }, { name: "Nassarao", latitude: 9.36191, longitude: 13.4407901 } ] }, "United States": { Maryland: marylandLaunchPlaces.map((place) => ({ name: place.name, latitude: place.latitude, longitude: place.longitude })) } }; function mergeOpenDataLaunchCenters(baseCenters, openDataSeeds) { const countryCenters = baseCenters?.Cameroon; if (!countryCenters || !openDataSeeds) return; Object.entries(openDataSeeds).forEach(([city, seeds]) => { if (!Array.isArray(seeds)) return; if (!countryCenters[city]) countryCenters[city] = []; const existing = new Set(countryCenters[city].map((item) => String(item?.name ?? "").trim().toLowerCase())); seeds.forEach((seed) => { const name = String(seed?.name ?? "").trim(); const latitude = Number(seed?.latitude); const longitude = Number(seed?.longitude); if (!name || existing.has(name.toLowerCase()) || !Number.isFinite(latitude) || !Number.isFinite(longitude)) return; countryCenters[city].push({ name, latitude, longitude, source: seed.source || "open-data" }); existing.add(name.toLowerCase()); }); }); } mergeOpenDataLaunchCenters( launchGpsTownCenters, typeof cameroonOpenDataAreaSeeds === "object" ? cameroonOpenDataAreaSeeds : null ); function genericAreas(city) { return [ { name: `${city} Center`, x: 48, y: 50 }, { name: "Main Market", x: 37, y: 57 }, { name: "Bus Station", x: 59, y: 61 }, { name: "University Area", x: 43, y: 34 }, { name: "Airport Area", x: 68, y: 39 } ]; } function openDataAreasForCity(city) { if (typeof cameroonOpenDataAreaSeeds !== "object") return []; const seeds = cameroonOpenDataAreaSeeds?.[city]; if (!Array.isArray(seeds)) return []; return seeds.map((seed) => ({ name: seed.name, x: seed.x, y: seed.y, latitude: seed.latitude, longitude: seed.longitude, source: seed.source || "open-data" })); } function withOtherArea(city, areaList = []) { const normalized = areaList.filter((area) => area?.name); const existing = new Set(normalized.map((area) => String(area.name).trim().toLowerCase())); openDataAreasForCity(city).forEach((area) => { const key = String(area.name ?? "").trim().toLowerCase(); if (!key || existing.has(key)) return; normalized.push(area); existing.add(key); }); if (normalized.some((area) => area.name === "Other")) return normalized; return [ ...normalized, { name: "Other", x: 50, y: 50, flexible: true } ]; } function buildCountries() { return Object.fromEntries( Object.entries(countryCities).map(([country, cities]) => [ country, Object.fromEntries(cities.map((city) => [city, withOtherArea(city, cityAreaOverrides[country]?.[city] ?? genericAreas(city))])) ]) ); } const countries = buildCountries(); const riderPickupEtaSpeedKmh = { car: 48 }; const riderPickupMaxEtaMinutes = 15; const scheduledRiderPickupMaxEtaMinutes = 30; const riderDestinationFilterNeighborRadiusMiles = 12; const riderProximityLimit = { car: 9.0, bike: 7.0 }; const carMakeCatalog = { Bajaj: ["Boxer", "Pulsar", "Discover", "Other"], Yamaha: ["YBR", "FZ", "Crypton", "Other"], TVS: ["HLX", "Apache", "Star", "Other"], Haojue: ["HJ", "DK", "Other"], Suzuki: ["GN", "AX100", "Other"], Toyota: ["Camry", "Corolla", "RAV4", "Highlander", "Sienna", "Prius"], Honda: ["Accord", "Civic", "CR-V", "Pilot", "Odyssey", "HR-V"], Nissan: ["Altima", "Sentra", "Rogue", "Pathfinder", "Murano", "Versa"], Hyundai: ["Elantra", "Sonata", "Tucson", "Santa Fe", "Kona", "Venue"], Kia: ["Forte", "K5", "Sportage", "Sorento", "Soul", "Telluride"], Ford: ["Fusion", "Escape", "Explorer", "Edge", "Focus", "Taurus"], Chevrolet: ["Malibu", "Equinox", "Traverse", "Impala", "Trax", "Suburban"], Subaru: ["Outback", "Forester", "Impreza", "Legacy", "Crosstrek", "Ascent"], Mazda: ["Mazda3", "Mazda6", "CX-5", "CX-9", "CX-30", "CX-50"], Volkswagen: ["Jetta", "Passat", "Tiguan", "Atlas", "Golf", "Taos"], Tesla: ["Model 3", "Model Y", "Model S", "Model X"], Mercedes: ["C-Class", "E-Class", "GLC", "GLE", "A-Class", "Metris"], BMW: ["3 Series", "5 Series", "X1", "X3", "X5", "X7"], Audi: ["A3", "A4", "A6", "Q3", "Q5", "Q7"], Lexus: ["ES", "IS", "RX", "NX", "GX", "UX"], Acura: ["TLX", "ILX", "RDX", "MDX", "Integra"], Infiniti: ["Q50", "QX50", "QX60", "QX80"], Volvo: ["S60", "S90", "XC40", "XC60", "XC90"], Other: ["Sedan", "SUV", "Minivan", "Wagon", "Hatchback"] }; const carColors = ["Black", "White", "Silver", "Gray", "Blue", "Red", "Green", "Brown", "Gold", "Other"]; const carBodyTypeOptions = [ { value: "sedan", label: "Sedan" }, { value: "suv", label: "SUV" }, { value: "hatchback", label: "Hatchback" }, { value: "minivan", label: "Minivan" }, { value: "wagon", label: "Wagon" }, { value: "pickup", label: "Pickup" }, { value: "coupe", label: "Coupe" }, { value: "convertible", label: "Convertible" }, { value: "luxury", label: "Luxury" }, { value: "motorbike", label: "Motorbike" } ]; const carTypePreferenceOptions = [ { value: "sedan", label: "Normal" }, { value: "suv", label: "XL/Special" }, { value: "bike", label: "Bike" } ]; const riderVehicleDesignationOptions = [ { value: "normal", label: "Normal" }, { value: "xl_special", label: "XL/Special" }, { value: "both", label: "Both" } ]; const rideStopsMaxCount = 4; const rideStopMaxLength = 160; const riderOfferNoteMaxLength = 240; const minimumVehicleYear = 2008; const fareGuidanceConfig = { baseFareUsd: 500, perMileUsd: 110, perMinuteUsd: 25, perStopUsd: 300, perExtraPassengerUsd: 150, perLuggageUsd: 200, perStopMinutes: 8, stopDistanceMultiplier: 0.08, minFareUsd: 700, maxMultiplier: 1.25, minMultiplier: 0.9, fuelIndex: 1, distanceStepMiles: 0.5, minuteStep: 5, pricingStepXaf: 100, pricingStepUsd: 1, benchmarkTripMinutes: "30-36", benchmarkTripFareUsd: "2,500-3,500 FCFA" }; const insurancePricingConfig = { enabled: true, defaultPerActiveMileUsd: 0.25, defaultPickupMileAllowance: 0.8, minTripInsuranceUsd: 0.75, tripDistanceMultiplier: 1, regionalRules: { "United States|Maryland": { perActiveMileUsd: 0.25, pickupMileAllowance: 0.8, minTripInsuranceUsd: 0.75, commercialLiabilityLimitUsd: 1000000, requiresTelematicsSdk: true }, "United States|New Jersey": { perActiveMileUsd: 0.35, pickupMileAllowance: 1, minTripInsuranceUsd: 1, commercialLiabilityLimitUsd: 1000000, requiresTelematicsSdk: true }, "United States|New York": { perActiveMileUsd: 0.35, pickupMileAllowance: 1.2, minTripInsuranceUsd: 1.25, commercialLiabilityLimitUsd: 1000000, requiresTelematicsSdk: true } } }; const insuranceTelemetryPeriods = Object.freeze({ period1: "p1_app_open", period2: "p2_match_accepted", period3: "p3_passenger_in_car" }); const kmToMiles = 0.621371; const metersToMiles = 0.000621371; const riderMonthlySubscriptionFee = 15000; const riderWeeklySubscriptionFee = 0; const riderMonthlyAccessDays = 30; const riderWeeklyAccessDays = 0; const riderWalletTopupMinimum = 5000; const riderWalletLowBalanceThreshold = 1000; const riderDailyFreeRideAllowance = 5; const riderWalletCommissionRate = 0.15; const businessMonthlySubscriptionFee = 0; const businessPartnerMonthlySubscriptionFee = 120000; const businessFreeTrialDays = 30; const businessRideServiceFeeRate = 0.1; const businessStarterPlanCode = "starter_10_percent"; const businessPartnerPlanCode = "partner_monthly"; const riderFacilitationFeeRate = 0; const subscriptionRenewalNoticeDays = 3; const stripeProcessingFeeRate = 0.029; const stripeProcessingFixedUsd = 0.3; const destinationUpdateTravelFraction = 2 / 7; const routeChangeFareConfig = { minAdditionalFareUsd: 2, minStopFareUsd: 3, afterPickupSurchargeRate: 0.15, billableMinutesPerAddedMile: 2, trafficTimeChargeMultiplier: 0.35, detourReviewMiles: 15, detourReviewMinutes: 45 }; const fareProposalAttemptLimit = 3; const passengerCancellationFeeConfig = { graceMinutes: 2, matchedBaseUsd: 3, arrivedBaseUsd: 5, perMinuteUsd: 1, capFareRatio: 0.35 }; const inProgressCancellationCompensationConfig = { fallbackEstimatedMinutes: 30, minimumFareRatio: 0.25, maximumFareRatio: 0.9 }; const riderPickupEtaRoadFactor = 1.35; const riderLiveGpsFreshMinutes = 15; const riderLiveGpsMaxAccuracyMeters = 150; const riderAvailabilityInactivityTimeoutMinutes = 8 * 60; const passengerPickupGpsFreshMinutes = 3; const passengerPickupGpsMaxAccuracyMeters = 50; const marketplaceNegotiationRefreshIntervalMs = 15 * 1000; const passengerApproachRefreshIntervalMs = 15 * 1000; const passengerNearbyRiderCountsRefreshIntervalMs = 15 * 1000; const riderMarketplaceRefreshIntervalMs = 15 * 1000; const riderMarketplaceSignedInHeartbeatIntervalMs = 5 * 60 * 1000; const riderMarketplaceActivatedRecoveryIntervalMs = 60 * 1000; const marketplaceVisibleResumeRefreshStaleMs = 5 * 1000; const marketplaceRealtimeRefreshDebounceMs = 0; const marketplaceRealtimeReconnectDelayMs = 3 * 1000; const marketplaceLoadedNoticePopupMaxAgeMs = 10 * 60 * 1000; const accountNotificationActiveRefreshIntervalMs = 1 * 1000; const accountNotificationIdleRefreshIntervalMs = 15 * 1000; const riderDropoffRequestLeadMinutes = 7; const riderAutoGpsIdleSyncIntervalMs = 5 * 60 * 1000; const riderAutoGpsMovingSyncIntervalMs = 60 * 1000; const riderAutoGpsActiveRideSyncIntervalMs = 15 * 1000; const riderAutoGpsActiveRideMinElapsedMs = 5 * 1000; const riderAutoGpsIdleHeartbeatMeters = 150; const riderAutoGpsMovingMinMovementMeters = 30; const riderAutoGpsActiveRideMinMovementMeters = 15; const riderAutoGpsSyncIntervalMs = riderAutoGpsMovingSyncIntervalMs; const riderAutoGpsMinMovementMeters = riderAutoGpsMovingMinMovementMeters; const placeDetailsCacheLimit = 100; const addressSearchRateLimitPauseMs = 30 * 1000; const testingAddressSearchRateLimitPauseMs = 0; const adminSlowRpcWarningMs = 1000; const marketplaceSyncLoadLimits = { ride_requests: 100, ride_cancellation_charges: 100, ride_payment_settlements: 100, finance_adjustments: 100, ride_tips: 100, ride_offers: 250, ride_chats: 250, ride_route_changes: 100, admin_notifications: 100, business_accounts: 50, business_subscriptions: 50, rider_tax_identity_references: 50, rider_tax_documents: 100, ride_ratings: 250, insurance_telemetry_segments: 100 }; const riderMarketplacePageSize = 40; const defaultCitySpanKm = 14; const cityDistanceSpanKm = { Cameroon: { Douala: 18, Yaounde: 16, Bamenda: 10, Bafoussam: 10, Buea: 9, Limbe: 9 }, Ghana: { Accra: 20 }, Kenya: { Nairobi: 18 }, Nigeria: { Lagos: 24 }, Senegal: { Dakar: 16 }, "United States": { Maryland: 90 } }; const rideLifecycleChatStatuses = ["matched", "arrived", "in_progress"]; const rideReportStatuses = ["matched", "arrived", "in_progress", "completed"]; const preStartCancellationStatuses = ["open", "matched", "arrived"]; const storageKey = "waka-negotiated-market-v1"; const runtimeConfigStorageKey = "waka-runtime-config-v1"; const supabaseSdkUrl = "https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"; const supabaseRequestTimeoutMs = 20000; const supabaseProfileSaveTimeoutMs = 12000; const optionalSupabaseRequestTimeoutMs = 8000; const runtimeConfigTimeoutMs = 5000; const phoneOtpCooldownMs = 60 * 1000; let appConfig = { appName: "Waka Cameroon", projectName: "waka-cameroon", mode: "demo", mapsMode: "landmarks-first", routeEstimatesProvider: "manual-fare", routeEstimateFunctionName: "route-estimate", routeEstimateMaxUncachedPerHour: 6, routeEstimateMaxUncachedPerDay: 20, requireRouteEstimateBeforePublish: false, placesAutocompleteProvider: "cameroon-local", placesAutocompleteFunctionName: "place-autocomplete", placesAutocompleteMaxRequestsPerMinute: 8, placesAutocompleteMaxRequestsPerDay: 60, placesDetailsMaxRequestsPerMinute: 4, placesDetailsMaxRequestsPerDay: 30, relaxAddressSearchLimitsForTesting: false, autoPickupGpsEnabled: true, autoRiderGpsEnabled: true, mapboxAccessToken: "", mapboxAccessTokenRestrictedToOrigins: false, mapboxAllowedOrigins: [], mapboxStyleId: "mapbox/streets-v12", mapboxTileMapsEnabled: false, passengerRequestTileMapEnabled: false, passengerApproachTileMapEnabled: false, mapboxTileRequestMonthlySoftLimit: 0, mapboxTileRequestMonthlyHardLimit: 0, riderInitializeMapZoom: 13, insuranceTelemetryEnabled: false, insurancePricingEnabled: false, telematicsSdkProvider: "", runtimeConfigFile: "", strictProductionMode: false, supabaseProjectModel: "single-project", authIsolationMode: "strict-single-tenant", enablePhoneOtpSignIn: false, turnstileRequired: true, turnstileSiteKey: "", relaxSmsVerificationForTesting: true, relaxPaymentSetupForTesting: true, relaxBackgroundCheckForTesting: false, passwordResetPhoneOtpRequired: true, passwordResetPhoneOtpFunctionName: "password-reset-phone-otp", passwordResetCompleteFunctionName: "password-reset-complete", relaxPasswordResetPhoneOtpForTesting: false, firstLaunchCountry: "Cameroon", firstLaunchCity: "Bamenda", enabledLaunchCountries: ["Cameroon"], launchCities: [ "Douala", "Yaounde", "Bamenda", "Bafoussam", "Limbe", "Buea", "Kumba", "Bambui", "Bambili", "Tiko", "Ebolowa", "Maroua", "Ngaoundere", "Garoua" ], phoneVerificationMode: "supabase", secureOnboardingFunctionsEnabled: false, passengerOnboardingSubmitFunctionName: "passenger-onboarding-submit", riderOnboardingSubmitFunctionName: "rider-onboarding-submit", passwordResetRequestFunctionName: "password-reset-request", paymentProvider: "mtn-orange-manual", ridePaymentSettlementFunctionName: "manual-direct-payment", contactRelayFunctionName: "ride-contact-relay", intercityBookingFunctionName: "intercity-booking-submit", intercityOperatorActionFunctionName: "intercity-operator-action", agencyPlatformPaymentStartFunctionName: "agency-platform-payment-start", backgroundCheckProvider: "manual-admin-review", taxOnboardingProvider: "not-required-for-cameroon-mvp", taxOnboardingMode: "disabled", supportPhone: "+237600000000", pushNotificationPublicKey: "", notificationDeliveryFunctionName: "notification-delivery", subscriptionReminderFunctionName: "subscription-reminders", clientEventIngestFunctionName: "client-event-ingest", clientErrorTelemetryEnabled: true, supabaseUrl: "", supabaseAnonKey: "", buckets: { riderDocuments: "rider-documents", rideImages: "ride-images", profilePhotos: "profile-photos", agencyLogos: "agency-logos", agencyPromotions: "agency-promotions", rideVoiceNotes: "ride-voice-notes" }, ...(window.WAKA_CONFIG ?? {}) }; let runtimeConfigSource = window.WAKA_CONFIG_SOURCE ?? (window.WAKA_CONFIG ? "window" : "default"); // Translation catalogs and language application helpers. if (typeof window !== "undefined") { window.wakaAppControlsTranslation = true; } const translations = { en: { tagline: "Negotiated car rides", passenger: "Passenger", rider: "Rider", admin: "Admin", language: "Language", installApp: "Install app", ok: "OK", cancel: "Cancel", continueAction: "Continue", createPassenger: "Create passenger account", savePassenger: "Save passenger", createPassengerAccountCta: "Create passenger account", postRide: "Post ride request", publishRequest: "Publish request", riderApplication: "Rider application", createRiderAccount: "Create rider account", createRiderAccountAndSubmitApplication: "Create account and submit application", submitRiderApplication: "Submit rider application", resubmitCorrectedApplication: "Resubmit corrected application", riderWallets: "Rider wallets", riderWalletCreditPaymentAudit: "Rider wallet credit and payment audit", submitReview: "Submit for admin review", subscription: "Rider access", paySubscription: "Start rider payment", respondRequest: "Respond to selected request", sendOffer: "Send accept or counter-offer", passengerSignIn: "Passenger sign-in", riderSignIn: "Rider sign-in", signIn: "Sign in" }, fr: { tagline: "Courses negociees en voiture", passenger: "Passager", rider: "Conducteur", admin: "Admin", language: "Langue", installApp: "Installer", ok: "OK", cancel: "Annuler", continueAction: "Continuer", createPassenger: "Creer un compte passager", savePassenger: "Enregistrer", createPassengerAccountCta: "Creer le compte passager", postRide: "Publier une demande", publishRequest: "Publier", riderApplication: "Demande conducteur", createRiderAccount: "Creer un compte conducteur", createRiderAccountAndSubmitApplication: "Creer le compte et envoyer la demande", submitRiderApplication: "Envoyer la demande conducteur", resubmitCorrectedApplication: "Renvoyer la demande corrigee", riderWallets: "Portefeuilles conducteurs", riderWalletCreditPaymentAudit: "Credit portefeuille conducteur et audit des paiements", submitReview: "Envoyer pour validation", subscription: "Abonnement", paySubscription: "Demarrer le paiement conducteur", respondRequest: "Repondre a la demande", sendOffer: "Envoyer l'offre", passengerSignIn: "Connexion passager", riderSignIn: "Connexion conducteur", signIn: "Connexion" }, pcm: { tagline: "Negotiate car rides", passenger: "Passenger", rider: "Rider", admin: "Admin", language: "Language", installApp: "Install app", createPassenger: "Open passenger account", savePassenger: "Save passenger", createPassengerAccountCta: "Create passenger account", postRide: "Post ride", publishRequest: "Publish ride", riderApplication: "Rider application", submitReview: "Send for admin check", subscription: "Subscription", paySubscription: "Start rider payment", respondRequest: "Answer ride request", sendOffer: "Send offer", passengerSignIn: "Passenger sign-in", riderSignIn: "Rider sign-in", signIn: "Sign in" }, de: { tagline: "Verhandelte Autofahrten", passenger: "Fahrgast", rider: "Fahrer", admin: "Admin", language: "Sprache", installApp: "App installieren", pageTitle: "Waka Verhandelte Fahrten", installed: "Installiert", chooseAccountType: "Kontotyp waehlen", continueAsPassenger: "Als Fahrgast fortfahren", continueAsRider: "Als Fahrer fortfahren", signInOrCreate: "Anmelden oder Konto erstellen", createAccount: "Konto erstellen", createPassenger: "Fahrgastkonto erstellen", savePassenger: "Fahrgast speichern", postRide: "Fahrtanfrage erstellen", publishRequest: "Anfrage veroeffentlichen", riderApplication: "Fahrerbewerbung", submitReview: "Zur Waka-Pruefung senden", subscription: "Fahrerzugang", paySubscription: "Fahrerzugang bezahlen", respondRequest: "Auf ausgewaehlte Anfrage antworten", sendOffer: "Annehmen oder Gegenangebot senden", passengerSignIn: "Fahrgast-Anmeldung", riderSignIn: "Fahrer-Anmeldung", signIn: "Anmelden", passengerPanelSubtitle: "Fahrt anfragen und bestes Angebot waehlen", riderPanelSubtitle: "Bewerben, Zugang aktivieren und Fahrten verhandeln", email: "E-Mail", password: "Passwort", phoneNumber: "Telefonnummer", otpCode: "OTP-Code", sendOtp: "OTP senden", sendCode: "Code senden", verify: "Pruefen", signOut: "Abmelden", fullName: "Vollstaendiger Name", profilePicture: "Profilbild", phoneVerificationCode: "Telefon-Bestaetigungscode", nationalIdNumber: "Identitaetsreferenz", identityReference: "Identitaetsreferenz", driverLicenseNumber: "Fuehrerscheinnummer", dateOfBirth: "Geburtsdatum", country: "Land", city: "Stadt", passengerSignInHelp: "Melden Sie sich mit E-Mail und Passwort an, bevor Sie Fahrten anfragen.", riderSignInHelp: "Melden Sie sich mit E-Mail und Passwort an, bevor Sie auf Fahrten antworten.", passengerWorkspace: "Fahrgastbereich", riderWorkspace: "Fahrerbereich", passengerSignedIn: "Fahrgast angemeldet", riderSignedIn: "Fahrer angemeldet", readyToRequestRides: "Bereit, Fahrten anzufragen.", applicationStatusWillAppear: "Der Bewerbungsstatus erscheint hier.", noPassengerSaved: "Noch kein Fahrgast gespeichert.", noRiderApplication: "Noch keine Fahrerbewerbung gespeichert.", pickupArea: "Abholgebiet", pickupDescription: "Abholbeschreibung", destination: "Ziel", rideTiming: "Fahrtzeit", asSoonAsPossible: "So bald wie moeglich", scheduleAhead: "Vorausplanen", scheduledDateTime: "Geplantes Datum und Uhrzeit", vehicle: "Fahrzeug", vehicleType: "Fahrzeugtyp", bike: "Auto", car: "Auto", bikeOrCar: "Auto", fareOffer: "Fahrpreisangebot", paymentPreference: "Zahlungsart", cashInHand: "Barzahlung", mtnMoney: "MTN Mobile Money", orangeMoney: "Orange Money", agreeWithRider: "Vor der Fahrt mit dem Fahrer einigen", optional: "Freiwillig", record: "Speichern", clear: "Loeschen", riderAccess: "Fahrerzugang", applicationStatus: "Bewerbungsstatus", riderPlatformStatus: "Ihr Fahrerstatus erscheint hier.", operatingArea: "Einsatzgebiet", credentialNumber: "Fuehrerscheinnummer", vehicleMake: "Fahrzeugmarke", vehicleModel: "Fahrzeugmodell", bodyType: "Karosserieform", vehicleDesignation: "Fahrzeugbezeichnung", yearOfManufacture: "Baujahr", vehicleColor: "Farbe", vehicleVin: "Fahrzeug-VIN", vehicleRegistration: "Kennzeichen", driverLicenseDocument: "Fuehrerscheindokument", vehicleRegistrationDocument: "Zulassungsdokument", nationalIdDocument: "Nationaler Ausweis Upload", subscriptionIntro: "Freigegebene Fahrer erhalten 30 Tage kostenlos nach Admin-Freigabe; danach prueft Waka Wallet, MTN MoMo oder Orange Money fuer Provisionen.", paymentProvider: "Versicherer", paymentPhone: "Zahlungstelefon", transactionReference: "Versicherungspolicennummer", subscriptionPaymentHelp: "In Kamerun prueft Waka den Wallet-Status sowie MTN MoMo oder Orange Money fuer Provisionen nach der kostenlosen Zeit.", yourFare: "Ihr Fahrpreis", messageBeforeSelection: "Hinweis an den Fahrgast vor der Auswahl", openRequests: "Offene Anfragen", passengers: "Fahrgaeste", riders: "Fahrer", pendingRiders: "Ausstehende Fahrer", subscribed: "Abonniert", loadDemoMarket: "Demo-Marktplatz laden", clearDemoData: "Lokale Demo-Daten loeschen", selectOrPublish: "Anfrage auswaehlen oder veroeffentlichen", refreshMarket: "Marktplatz aktualisieren", all: "Alle", rideRequests: "Fahrtanfragen", riderOffers: "Fahrerangebote", accountDetail: "Kontodetail", postSelectionChat: "Chat nach Auswahl", locked: "Gesperrt", send: "Senden", chooseRider: "Fahrer waehlen", openFullReview: "Vollstaendige Pruefung oeffnen", approve: "Freigeben", decline: "Ablehnen", passengerNamePlaceholder: "Name des Fahrgasts", passwordPlaceholder: "Passwort", createPasswordPlaceholder: "Passwort erstellen", codePlaceholder: "6-stelliger Code", nationalIdPlaceholder: "Fuehrerschein, Ausweis oder Passreferenz", driverLicensePlaceholder: "Fuehrerscheinnummer", pickupDescriptionPlaceholder: "Orientierungspunkt, Gebaeudefarbe, Markt, Kreuzung, Ladenname", destinationPlaceholder: "Zielgebiet, Orientierungspunkt oder Adresse", riderNamePlaceholder: "Fahrername", credentialPlaceholder: "Fuehrerscheinnummer", registrationPlaceholder: "Kennzeichen oder Zulassungsnummer", transactionReferencePlaceholder: "Policennummer", counterFarePlaceholder: "Hoeheres Gegenangebot eingeben", counterNotePlaceholder: "Optional: kurzer Fahrzeug- oder Preishinweis fuer den Fahrgast", supabasePasswordPlaceholder: "Supabase-Passwort", chatPlaceholder: "Chat oeffnet erst, nachdem der Fahrgast einen Fahrer gewaehlt hat", safetyReportDetailsPlaceholder: "Beschreiben Sie, was Waka pruefen soll", offlineReady: "Offline bereit", onlineDemo: "Online-Demo", localMode: "Lokaler Modus", supabaseReady: "Supabase bereit", supabaseConfigNeeded: "Supabase-Konfiguration erforderlich", supabaseConnecting: "Supabase verbindet", supabaseSdkUnavailable: "Supabase SDK nicht verfuegbar", manualPhoneVerified: "Manueller Pilotmodus: Telefon als verifiziert markiert. SMS-OTP vor oeffentlichem Start konfigurieren.", smsVerificationRelaxedForTesting: "Testmodus: SMS-Telefonpruefung uebersprungen. Kontoerstellung per E-Mail/Passwort kann fortgesetzt werden; echte SMS-OTP vor oeffentlichem Start aktivieren.", validPhoneRequired: "Geben Sie eine gueltige Telefonnummer ein, bevor Sie einen Code anfordern.", validDateOfBirthRequired: "Geben Sie ein gueltiges Geburtsdatum im Format JJJJ-MM-TT ein. Sie koennen nur Ziffern tippen; Waka fuegt die Bindestriche hinzu.", checkingPassengerAccount: "Fahrgastkonto wird geprueft...", checkingRiderApplication: "Fahrerbewerbung wird geprueft...", accountMissingFields: "Fuellen Sie diese Felder vor dem Speichern aus: {fields}.", phoneOtpCooldown: "Bitte warten Sie {seconds}s, bevor Sie einen weiteren Telefoncode anfordern.", phoneOtpRateLimited: "Zu viele Telefoncode-Versuche. Warten Sie etwas und pruefen Sie Supabase Auth-Limits, falls dies weiter passiert.", sendingVerificationCode: "Bestaetigungscode wird gesendet...", verificationCodeSent: "Bestaetigungscode an {phone} gesendet.", demoCode: "Demo-Code: {code} fuer {phone}", freshVerificationCodeRequired: "Fordern Sie einen neuen Bestaetigungscode fuer diese Telefonnummer an.", verifyingPhoneNumber: "Telefonnummer wird geprueft...", phoneNumberVerified: "Telefonnummer verifiziert. Das prueft nur das Telefon; druecken Sie Speichern oder Senden, um das Waka-Konto fertigzustellen.", verificationCodeIncorrect: "Der Bestaetigungscode ist nicht korrekt.", phoneOtpManualSignIn: "Telefon-OTP-Anmeldung ist im manuellen Pilotmodus deaktiviert. Verwenden Sie E-Mail und Passwort.", sendingSignInCode: "Anmeldecode wird gesendet...", signInCodeSent: "Anmeldecode an {phone} gesendet.", demoSignInCode: "Demo-Anmeldecode: {code} fuer {phone}", signingInPassword: "Anmeldung mit E-Mail und Passwort...", loadingWakaProfile: "Waka-Profil wird geladen...", supabaseProfileMissing: "Anmeldung erfolgreich. Dieses Login braucht noch ein Waka-Profil; fuellen Sie dieses Formular aus und speichern Sie es, um das Konto zu verbinden.", wrongProfileRole: "Dieses Konto ist als {role} registriert, nicht als {type}.", wrongProfileRoleStrict: "Dieses Login ist als {role} registriert, nicht als {type}. In der aktuellen WakaGood-Bereitstellung mit einem Auth-Projekt kann dieselbe E-Mail nicht sicher beide Rollen offnen. Verwenden Sie jetzt eine separate {type}-E-Mail/Telefonnummer oder schliessen Sie die Fahrgast/Fahrer-Auth-Trennung ab, bevor diese E-Mail fur beide Rollen verwendet wird.", adminPublicPortalBlocked: "Admin-Konten koennen sich nicht ueber Fahrgast- oder Fahrerportale anmelden. Nutzen Sie den Admin-Bereich oder erstellen Sie ein separates Fahrgast-/Fahrerkonto zum Testen.", signedInPassengerLoaded: "Angemeldet als {email}. Fahrgastprofil geladen. Fuegen Sie vor Fahrtanfragen eine Fahrgast-Zahlungsmethode hinzu.", signedInRiderLoaded: "Angemeldet als {email}. Fahrerprofil geladen.", signedInAs: "Angemeldet als {identity}.", freshSignInCodeRequired: "Fordern Sie einen neuen Anmeldecode fuer diese Telefonnummer an.", signInCodeRequired: "Melden Sie sich mit E-Mail und Passwort an. Telefon-OTP ist nur fuer die erstmalige Telefonpruefung.", passwordSignInOnly: "Melden Sie sich mit E-Mail und Passwort an. Telefon-OTP ist nur fuer die erstmalige Telefonpruefung.", signInEmailPasswordRequired: "Geben Sie E-Mail und Passwort fuer dieses Konto ein. Telefoncode-Anmeldung ist im manuellen Pilotmodus deaktiviert.", signingIn: "Anmeldung...", signInCodeIncorrect: "Der Anmeldecode ist nicht korrekt.", localSignInAccountMissing: "Kein gespeichertes {type}-Konto passt zu dieser Telefonnummer. Erstellen und speichern Sie zuerst das {type}-Konto, dann melden Sie sich an.", signedOut: "Abgemeldet.", passengerPhoneBeforeSave: "Verifizieren Sie die Fahrgast-Telefonnummer vor dem Speichern.", riderPhoneBeforeReview: "Verifizieren Sie die Fahrer-Telefonnummer vor dem Einreichen.", passengerPaymentRequired: "Fuegen Sie unter Zahlung eine Fahrgast-Zahlungsmethode hinzu, bevor Sie Fahrten veroeffentlichen.", riderPaymentRequired: "Speichern Sie ein Fahrer-Zahlungskonto, bevor Sie Anfragen erhalten.", riderDailyRegionsRequired: "Bevorzugte Zielregionen sind fuer Fahrer optional.", riderLiveGpsRequired: "Teilen Sie Live-Fahrer-GPS, bevor Sie Anfragen erhalten.", startingPassengerSupabase: "Fahrgast in Supabase wird gespeichert...", savingPassenger: "Fahrgast wird gespeichert...", passengerCreated: "{name} Fahrgastkonto erfolgreich erstellt. Fuegen Sie als Naechstes eine Zahlungsmethode hinzu und fragen Sie dann Fahrten an.", passengerCreatedEmailPending: "{name} Fahrgastkonto erfolgreich erstellt. Fuegen Sie als Naechstes eine Zahlungsmethode hinzu; E-Mail/Passwort-Anmeldung kann Supabase-E-Mail-Bestaetigung oder Einrichtung benoetigen.", passengerAccountFailed: "Fahrgastkonto wurde nicht erstellt: {message}", passengerSyncing: "{name} Fahrgastkonto erfolgreich erstellt. Fuegen Sie als Naechstes eine Zahlungsmethode hinzu und fragen Sie dann Fahrten an.", startingRiderSupabase: "Fahrer in Supabase wird gespeichert...", savingRiderApplication: "Fahrerbewerbung wird gespeichert...", submittingRiderApplication: "Fahrerbewerbung wird zur Admin-Freigabe gesendet...", riderCreatedPending: "{name} Konto erstellt. Fahrerbewerbung wartet auf Admin-Freigabe. Bei Freigabe startet die 30-taegige Testphase vor woechentlichem oder monatlichem Waka Fahrerzugang.", riderAccountFailed: "Fahrerkonto wurde nicht eingereicht: {message}", missingRiderDocuments: "Laden Sie diese erforderlichen Fahrerdokumente vor der Admin-Pruefung hoch: {documents}.", passengerAccountRequired: "Erstellen Sie ein Fahrgastkonto, bevor Sie eine Fahrtanfrage veroeffentlichen.", passengerSignInRequired: "Fahrgast-Anmeldung ist erforderlich, bevor Fahrten veroeffentlicht werden.", passengerPhoneRequired: "Telefonverifizierung des Fahrgasts ist erforderlich, bevor Fahrten veroeffentlicht werden.", realisticFareRequired: "Geben Sie ein realistisches Fahrpreisangebot ein.", fareBelowGuidance: "Dieser Preis liegt unter der vorgeschlagenen Spanne {min}-{max}. Fahrer koennen ihn ueberspringen oder langsamer antworten. Trotzdem fortfahren?", fareOutsideGuidance: "Diese Routenschaetzung empfiehlt {min}-{max}. Niedrigere Preise koennen laengere Wartezeiten verursachen.", scheduledTimeRequired: "Waehlen Sie ein gueltiges Datum und eine gueltige Uhrzeit fuer die geplante Fahrt.", scheduleThirtyMinutes: "Planen Sie die Fahrt mindestens 30 Minuten ab jetzt.", ridePublishedSupabase: "Fahrtanfrage wurde in Supabase fuer berechtigte Fahrer veroeffentlicht.", ridePublishedLocal: "Fahrtanfrage lokal veroeffentlicht.", publishRideFailed: "Diese Fahrtanfrage konnte nicht veroeffentlicht werden: {message}", subscriptionReferenceRequired: "Anbieter-Checkout ist fuer Waka Fahrerzugang erforderlich.", subscriptionAlreadyPending: "Ein Anbieter-Checkout ist bereits aktiv.", submittingPaymentSupabase: "Waka Fahrerzugang-Checkout wird geoeffnet...", savingPaymentReference: "Waka Fahrerzugang-Checkout wird geoeffnet...", paymentReferenceSubmitted: "Waka Fahrerzugang-Checkout geoeffnet. Bezahlter Zugang startet nach Testphase oder aktuellem Zugangszeitraum.", paymentReferenceFailed: "Fahrerzugang-Checkout konnte nicht geoeffnet werden: {message}", selectRideRequestFirst: "Waehlen Sie zuerst eine Fahrtanfrage aus.", createRiderFirst: "Erstellen Sie zuerst ein Fahrerkonto.", riderSignInRequired: "Fahrer-Anmeldung ist erforderlich, bevor auf Fahrten geantwortet wird.", riderApprovalRequired: "Admin-Freigabe ist erforderlich, bevor auf Fahrten geantwortet wird.", riderAccessRequired: "Ihre Testphase oder Ihr bezahlter Fahrerzugang muss aktiv sein, bevor Sie auf Fahrten antworten.", selectNearbyRequest: "Waehlen Sie eine nahe Anfrage, die zu Ihrem freigegebenen Fahrerkonto passt.", requestClosed: "Diese Anfrage ist nicht mehr offen.", offerSendFailed: "Dieses Angebot konnte nicht gesendet werden: {message}", passengerOwnRequestRequired: "Nur der Fahrgast, der diese Anfrage erstellt hat, kann einen Fahrer waehlen.", chooseRiderFailed: "Dieser Fahrer konnte nicht gewaehlt werden: {message}", safetyReportUnavailable: "Meldungen sind verfuegbar, nachdem ein Fahrgast einen Fahrer gewaehlt hat. Waka-Kontakt oeffnet sich nach der Fahrerauswahl.", safetyReportNeedsDetail: "Fuegen Sie genug Details hinzu, damit Waka die Anfrage versteht.", safetyReportSignInRequired: "Melden Sie sich erneut an, bevor Sie Waka kontaktieren.", submittingSafetySupabase: "Nachricht an Waka wird gesendet...", savingSafetyReport: "Nachricht wird fuer Waka-Pruefung gespeichert...", safetyReportSubmitted: "Sicherheitsmeldung zur Admin-Pruefung gesendet. Nachricht an Waka gesendet.", safetyReportFailed: "Meldung konnte nicht gesendet werden: {message}", suspendRiderConfirm: "Diesen Fahrer sperren? Er sieht und akzeptiert sofort keine Fahrtanfragen mehr.", clearDemoConfirm: "Alle lokal gespeicherten Demo-Daten loeschen?", requestConfirmationFailed: "Bestaetigung konnte nicht angefordert werden: {message}", confirmScheduledFailed: "Diese geplante Fahrt konnte nicht bestaetigt werden: {message}", reopenScheduledFailed: "Diese geplante Fahrt konnte nicht wieder geoeffnet werden: {message}", stop: "Stoppen", androidInstallHelp: "Auf Android diese Seite in Chrome oeffnen, Menue antippen und Zum Startbildschirm hinzufuegen oder App installieren waehlen." }, ar: { tagline: "رحلات دراجة وسيارة قابلة للتفاوض", passenger: "راكب", rider: "سائق", admin: "مشرف", language: "اللغة", installApp: "تثبيت", createPassenger: "انشاء حساب راكب", savePassenger: "حفظ الراكب", postRide: "طلب رحلة", publishRequest: "نشر الطلب", riderApplication: "طلب السائق", submitReview: "ارسال للمراجعة", subscription: "اشتراك", paySubscription: "Open rider access checkout", respondRequest: "الرد على الطلب", sendOffer: "ارسال العرض", passengerSignIn: "دخول الراكب", riderSignIn: "دخول السائق", signIn: "دخول" }, sw: { tagline: "Safari za gari kwa maelewano", passenger: "Abiria", rider: "Dereva", admin: "Msimamizi", language: "Lugha", installApp: "Sakinisha", createPassenger: "Fungua akaunti ya abiria", savePassenger: "Hifadhi abiria", postRide: "Tuma ombi la safari", publishRequest: "Chapisha ombi", riderApplication: "Ombi la dereva", submitReview: "Tuma kwa ukaguzi", subscription: "Usajili", paySubscription: "Fungua malipo ya usajili kiotomatiki", respondRequest: "Jibu ombi", sendOffer: "Tuma ofa", passengerSignIn: "Kuingia abiria", riderSignIn: "Kuingia dereva", signIn: "Ingia" }, pt: { tagline: "Viagens negociadas de carro", passenger: "Passageiro", rider: "Motorista", admin: "Admin", language: "Idioma", installApp: "Instalar", createPassenger: "Criar conta de passageiro", savePassenger: "Guardar passageiro", postRide: "Publicar pedido", publishRequest: "Publicar", riderApplication: "Pedido de motorista", submitReview: "Enviar para revisao", subscription: "Subscricao", paySubscription: "Abrir pagamento automatico da subscricao", respondRequest: "Responder ao pedido", sendOffer: "Enviar oferta", passengerSignIn: "Entrada passageiro", riderSignIn: "Entrada motorista", signIn: "Entrar" }, es: { tagline: "Viajes en auto negociados", passenger: "Pasajero", rider: "Conductor", admin: "Admin", language: "Idioma", installApp: "Instalar", createPassenger: "Crear cuenta de pasajero", savePassenger: "Guardar pasajero", postRide: "Publicar solicitud", publishRequest: "Publicar", riderApplication: "Solicitud de conductor", submitReview: "Enviar para revision", subscription: "Suscripcion", paySubscription: "Abrir pago automatico de suscripcion", respondRequest: "Responder a solicitud", sendOffer: "Enviar oferta", passengerSignIn: "Ingreso de pasajero", riderSignIn: "Ingreso de conductor", signIn: "Ingresar" } }; const translationAdditions = { en: { pageTitle: "Waka Negotiated Rides", installed: "Installed", chooseAccountType: "Choose account type", continueAsPassenger: "Continue as passenger", continueAsRider: "Continue as rider", signInOrCreate: "Sign in or create account", createAccount: "Create account", passengerPanelSubtitle: "Request a ride and choose the best offer", riderPanelSubtitle: "Apply, subscribe, then negotiate rides", email: "Email", password: "Password", phoneNumber: "Phone number", otpCode: "OTP code", sendOtp: "Send OTP", sendCode: "Send code", verify: "Verify", signOut: "Sign out", fullName: "Full name", profilePicture: "Profile picture", phoneVerificationCode: "Phone verification code", nationalIdNumber: "Identity reference", identityReference: "Identity reference", driverLicenseNumber: "Driver's license number", dateOfBirth: "Date of birth", country: "Country", city: "City", passengerSignInHelp: "Use email and password to sign in before requesting rides.", riderSignInHelp: "Use email and password to sign in before responding to rides.", passengerWorkspace: "Passenger workspace", riderWorkspace: "Rider workspace", passengerSignedIn: "Passenger signed in", riderSignedIn: "Rider signed in", readyToRequestRides: "Ready to request rides.", applicationStatusWillAppear: "Application status will appear here.", noPassengerSaved: "No passenger saved yet.", noRiderApplication: "No rider application saved yet.", pickupArea: "Pickup area", pickupCity: "Pickup city", pickupCityHelp: "Choose where the pickup will happen. If you are booking for someone elsewhere, choose their pickup city and area, then type a clear landmark below.", pickupOtherAreaHelp: "If the exact pickup area is not listed, choose Other and type the quarter, junction, motor park, shop, school, church, or local landmark in the pickup space.", pickupDescription: "Pickup description", destination: "Destination", rideTiming: "Ride timing", asSoonAsPossible: "As soon as possible", scheduleAhead: "Schedule ahead", scheduledDateTime: "Scheduled date and time", vehicle: "Vehicle", vehicleType: "Vehicle designation", bike: "Bike", car: "Car", bikeOrCar: "Bike or car", fareOffer: "Fare offer", paymentPreference: "Payment preference", cashInHand: "Cash in hand", mtnMoney: "MTN Mobile Money", orangeMoney: "Orange Money", agreeWithRider: "Agree with rider before ride", optional: "Optional", otherOption: "Other", record: "Record", clear: "Clear", riderAccess: "Rider access", applicationStatus: "Application status", riderPlatformStatus: "Your rider platform status will appear here.", createRiderAccount: "Create rider account", createRiderAccountAndSubmitApplication: "Create account and submit application", submitRiderApplication: "Submit rider application", resubmitCorrectedApplication: "Resubmit corrected application", riderApplicationIntro: "Create one rider profile and submit it once for Waka admin review. Start with the basics; Waka admin can request extra documents later.", riderLoginOnlyIntro: "Create one rider profile and submit it once for Waka admin review.", riderOneStepApplicationIntro: "Enter the basics once: account, phone, work area, bike or car, plate, make, model, and color. Optional identity details and documents can be added later if admin asks.", riderApplicationOnlyIntro: "Your email is confirmed and your rider login is active. Complete only any missing basic rider details below; do not create a second account.", riderCorrectionIntro: "Admin has requested changes. Update only the requested rider application details and resubmit.", riderLoginOnlyStatus: "Complete the rider form once. Waka sends the email confirmation link after saving the application.", riderOneStepApplicationStatus: "Complete the basic form once, verify you are human, then Waka saves the application and sends the email confirmation link.", riderApplicationOnlyStatus: "Your rider login is active, but Waka does not yet have an application for admin review. Complete only the remaining application details below.", riderProfileLoaded: "Profile already loaded", riderFinishApplicationOnly: "Finish only the rider application details below. Your name, email, phone, and password stay with the signed-in account.", account: "Account", riderAccountSectionHelp: "These details create the rider login and attach it to this application.", identity: "Identity", riderIdentitySectionHelp: "Use the same identity details the agency or Waka admin can verify later.", riderVehicleSectionHelp: "Choose bike or car first so Waka only asks for relevant details.", vehicleSectionTitle: "Vehicle", bikeSectionTitle: "Bike", vehicleSectionHelp: "Choose bike or car first so Waka only asks for relevant details.", bikeSectionHelp: "Bike selected. Waka only asks for bike-relevant details.", vehicleCategory: "Vehicle type", bikeType: "Bike type", documents: "Documents", riderDocumentsSectionHelp: "Document uploads are optional during pilot signup. Waka admin may request any needed files later.", operatingArea: "Operating area", credentialNumber: "Driver's license number", vehicleMake: "Vehicle make", vehicleModel: "Vehicle model", bikeMake: "Bike make", bikeModel: "Bike model", bodyType: "Body type", vehicleDesignation: "Vehicle designation", yearOfManufacture: "Year of manufacture", vehicleColor: "Color", vehicleVin: "Vehicle VIN", vehicleRegistration: "Plate number", bikePlateNumber: "Bike plate number", driverLicenseDocument: "License document upload", vehicleRegistrationDocument: "Registration document", bikeRegistrationDocument: "Bike registration document", vehicleBackgroundConsent: "I authorize Waka to review my rider eligibility, identity, vehicle, permit, insurance, and safety records for admin approval.", bikeBackgroundConsent: "I authorize Waka to review my rider eligibility, identity, bike registration, permit, and safety records for admin approval.", bikeModeNote: "Bike selected. Car-only fields are skipped for this application.", carModeNote: "Car selected. Optional insurance and inspection details can be added now or later if admin requests them.", nationalIdDocument: "National Identity card upload (optional)", subscriptionIntro: "Approved riders get a 30-day free period after admin approval. After that, the first 5 completed rides per day remain free and ride 6+ uses the rider wallet unless monthly access is active.", riderFarePaymentMode: "Rider fare payment mode", riderFarePaymentModeHelp: "Cameroon riders receive negotiated ride fares directly from passengers by cash, MTN Mobile Money, or Orange Money. Wallet top-up and optional monthly access are paid separately from the Rider access panel.", riderDirectFareCollectionHelp: "Passenger fare collection remains direct cash, MTN Mobile Money, or Orange Money unless Waka later enables online ride-fare settlement.", reviewDirectFareMode: "Review direct fare mode", riderDirectFareModeActive: "Direct passenger-to-rider fare payment is active after approval.", riderPaymentChoice: "Rider payment", walletTopupBundleXaf: "Wallet top-up bundle - from 5,000 FCFA", monthlyRiderAccessXaf: "Monthly access subscription - 15,000 FCFA", walletTopupAmount: "Wallet top-up amount", paymentProviderChoice: "Payment provider", subscriptionPaymentProviderHelp: "Choose MTN Mobile Money or Orange Money for wallet top-up or the optional monthly access subscription.", paymentProvider: "Insurance provider", paymentPhone: "Payment phone", transactionReference: "Insurance policy number", subscriptionPaymentHelp: "For Cameroon, riders top up their wallet from 5,000 FCFA or pay optional 15,000 FCFA monthly access with MTN Mobile Money or Orange Money.", yourFare: "Your fare", messageBeforeSelection: "Note to passenger before selection", openRequests: "Open requests", passengers: "Passengers", riders: "Riders", pendingRiders: "Pending riders", subscribed: "Subscribed", loadDemoMarket: "Load demo market", clearDemoData: "Clear local demo data", selectOrPublish: "Select or publish a request", refreshMarket: "Refresh market", all: "All", rideRequests: "Ride requests", riderOffers: "Rider offers", accountDetail: "Account detail", postSelectionChat: "Post-selection chat", locked: "Locked", send: "Send", logoPreview: "Logo", companyLogo: "Company logo", logoDescription: "Logo description", agencyLogoVisibilityHelp: "Optional. The logo appears on public search cards, agency dashboards, booking receipts, messages, and trip reports.", heldUnavailableSeats: "Held / unavailable seats", adminAgencyPublishMessageTitle: "Publish or message on behalf of an agency", adminAgencyPublishMessageBody: "Waka admin can publish active departures for approved agencies and send agency-only platform messages without exposing another agency's booking details.", publishForAgency: "Publish for agency", adminIntercityPublishStatusHelp: "Choose an active agency to publish a public, bookable departure.", message: "Message", sendAgencyMessage: "Send agency message", agencyMessagesScopedHelp: "Agency messages are scoped to the selected agency dashboard.", travelerNameAsNationalId: "Name as on national ID", nationalIdentityNumberOptional: "National ID number (optional)", nationalIdentityInspectionHelp: "Enter the traveler name exactly as it appears on the national identity card. The physical document must be presented to agency staff before travelling.", passengerManifestTitle: "Passenger details for selected seats", passengerManifestHelp: "For group bookings, enter the name and date of birth for every traveler. National ID number is optional, but the physical identity card must be inspected before boarding.", identityDocumentNumberOptional: "Identity document number (optional)", optionalIdentityPlaceholder: "Optional ID, license, or passport", dateOfBirthOptional: "Date of birth (optional)", passengerIdentityOptionalHelp: "Identity details are optional when creating a passenger account. You can add them later; travel bookings still ask for traveler details before reserving seats.", passengerEmailConfirmationPopup: "Good news - your required passenger account details were accepted.\n\nWaka Cameroon has sent a confirmation link to {email}. Open your email and click the link to complete account creation. If you do not see it, check Spam or Junk. Also verify that the email address you entered is correct. After confirming, return here and sign in with the same email and password. Do not press Save account again for this same email.", agencyEmailConfirmationPopup: "Good news - your required agency access details were accepted.\n\nWaka Cameroon has sent a confirmation link to {email}. Open your email and click the link to complete account creation. If you do not see it, check Spam or Junk. Also verify that the email address you entered is correct. After confirming, return here and sign in with the same email and password. Do not press Save account again for this same email.", riderEmailConfirmationPopup: "Good news - your Waka Cameroon rider account and application were received.\n\nWaka Cameroon has sent a confirmation link to {email}. Open your email and click the link to complete account access. If you do not see it, check Spam or Junk. Also verify that the email address you entered is correct. After confirming, return here and sign in with the same email and password. Your application will be waiting for Waka admin review.", riderApplicationSubmittedPopup: "{name} has been submitted for Waka admin review.\n\nYou do not need to fill this form again. Waka admin will review the account, identity, vehicle or bike, and uploaded documents. If anything else is needed, Waka will request corrections from your rider workspace.", voice: "Voice", chooseRider: "Choose rider", openFullReview: "Open full review", approve: "Approve", decline: "Decline", passengerNamePlaceholder: "Passenger name", passwordPlaceholder: "Password", createPasswordPlaceholder: "Create a password", codePlaceholder: "6-digit code", nationalIdPlaceholder: "Driver license, state ID, or passport reference", driverLicensePlaceholder: "Driver's license number", pickupDescriptionPlaceholder: "Landmark, building color, market, junction, shop name", destinationPlaceholder: "Destination area, landmark, or address", luggagePieces: "Luggage pieces", luggageNote: "Luggage note", luggageNotePlaceholder: "Optional: bags, boxes, cargo, fragile items", tripDetailsRiderHint: "Riders see these trip details before accepting or negotiating. Waka Cameroon does not estimate or auto-price this request.", rideFareGuidanceManual: "Enter the fare you want to offer in FCFA. Suggestions are optional; typed pickup and destination text can still be published. Waka sends vehicle, passenger count, luggage, and stops to riders for agreement.", fareGuidanceNeedPickupDestination: "Enter pickup and destination addresses, then enter the FCFA fare you want to offer.", fareGuidancePendingRoute: "Enter a destination and either use current location or enter a pickup address to estimate the fare before publishing.", fareGuidanceTripContext: "Trip context: {routeSummary}. Enter your FCFA fare offer; riders can accept, decline, or counter when negotiation is enabled.", routeServiceFallbackEstimate: "The route service is unavailable right now, so this quick estimate is shown.", currentLocationNeedsAddressForEstimate: "Choose the pickup from Cameroon suggestions, or check Current and capture GPS, before Waka estimates fare.", confirmedDestinationRequiredForEstimate: "Choose the destination from Cameroon suggestions before Waka estimates fare.", confirmedLocationsRequiredForEstimate: "Choose pickup and destination from Cameroon suggestions before Waka estimates fare. For pickup, Current GPS is also accepted.", pickupGpsCaptureTimeUnavailable: "Exact pickup capture time is unavailable. Waka can still publish with a clear typed landmark.", pickupGpsOld: "Exact pickup location is {minutes} minutes old. Waka can still publish with a clear typed landmark.", pickupGpsAccuracyUnavailable: "Exact pickup accuracy is unavailable. Waka can still publish with the pickup address or landmark.", pickupGpsNeedsClearerSignal: "Exact pickup location needs a clearer signal. Waka can still publish with a clear typed landmark.", pickupGpsFallbackStatus: "Exact pickup GPS was not clear enough, so Waka will publish using the typed pickup address instead.", publishTypedPickupStatus: "Publishing with the typed pickup address and selected city/area.", typedPickupStatus: "Using the typed pickup address. Riders will see the pickup text and the selected city/area.", typedPickupStatusBeforePublish: "Pickup was not matched to a saved GPS place. Waka can still publish it, but add a clear landmark so the rider can find you.", typedPickupUsePhoneGpsPrompt: "This pickup was not matched to a saved GPS place. Use this phone's GPS only if this phone is physically at the pickup point, so nearby riders around that pickup can see the request. If you are booking for someone elsewhere, choose Cancel, then make sure the pickup city/area and landmark are correct. Waka will still show your typed pickup text and landmark to the rider.", typedPickupPhoneGpsCapturing: "Capturing phone GPS so nearby riders can see this request...", typedPickupPhoneGpsLinked: "Phone GPS linked for nearby rider matching. Riders will still see your typed pickup landmark.", typedPickupPhoneGpsSkipped: "Phone GPS was not added. Waka will publish using the selected pickup city/area and typed landmark; riders may clarify by chat if needed.", typedPickupPhoneGpsUnavailable: "Phone GPS could not be captured clearly. Waka will publish using the selected pickup city/area and typed landmark; riders may clarify by chat if needed.", typedPickupPhoneGpsNeedsHttps: "Phone GPS needs a secure HTTPS page. Waka will publish using the selected pickup city/area and typed landmark instead.", typedPickupPhoneGpsUnsupported: "This browser cannot share phone GPS. Waka will publish using the selected pickup city/area and typed landmark instead.", typedDestinationStatusBeforePublish: "Destination was not matched to a saved GPS place. Waka can still publish it, but riders need a clear destination landmark.", landmarkReminderPopup: "Exact GPS or a saved map point was not confirmed for this ride. Waka will still publish it. Please include a nearby landmark, quarter/neighborhood, junction, shop, school, church, motor park, or clear local description so the rider can find you easily.", landmarkReminderConfirmPickup: "The pickup was not matched to a saved GPS place. Continue only if the selected pickup city/area is correct and the pickup text includes a clear landmark, quarter, junction, shop, school, church, motor park, or local description. The selected pickup area controls which nearby riders see this request when phone GPS is not used. Choose Continue to publish, or Cancel to return and correct the pickup.", landmarkReminderConfirmDestination: "The destination was not matched to a saved GPS place. Continue only if the destination text includes a clear landmark, quarter, junction, shop, school, church, motor park, or local description. Choose Continue to publish, or Cancel to return and correct the destination.", landmarkReminderConfirmBoth: "The pickup and destination were not matched to saved GPS places. Continue only if the selected pickup city/area is correct and both typed addresses include clear landmarks, quarters, junctions, shops, schools, churches, motor parks, or local descriptions. The selected pickup area controls which nearby riders see this request when phone GPS is not used. Choose Continue to publish, or Cancel to return and correct the addresses.", pickupLandmarkRequired: "Enter the pickup address or a recognizable landmark before publishing.", pickupLandmarkMinLength: "Enter at least 3 characters for the pickup address or landmark.", confirmPickupAddressBeforePublish: "Enter the pickup address or a recognizable landmark before publishing.", currentLocationCouldNotConfirmPublish: "Exact current location could not be confirmed. Enter the pickup address or a recognizable landmark.", completePickupAddressBeforePublish: "Enter a complete pickup address or recognizable landmark before publishing.", currentPickupConfirmedStatus: "Using the confirmed pickup address from current location.", destinationRequiredBeforePublishing: "Enter a destination address before publishing.", destinationLandmarkRequiredBeforePublishing: "Enter the destination address or a recognizable landmark before publishing.", selectedPlaceStatus: "Selected: {place}", selectedPickupStatus: "Selected pickup: {place}", selectedDestinationStatus: "Selected destination: {place}", usingCurrentLocationStatus: "Using current location: {place}.", usingCurrentLocationVerifiedStatus: "Using current location: {place}. Riders will see the verified pickup point.", exactGpsNoLandmarkStatus: "Exact GPS was captured. No nearby saved landmark was found, so riders will navigate to the GPS point and see your pickup note.", exactGpsFallbackNoLandmarkStatus: "Using exact GPS pickup. No nearby saved landmark was found, so riders will navigate to the GPS point and see your typed pickup note.", searchingPickupAddresses: "Searching pickup addresses...", pickupChooseSuggestion: "Choose the matching pickup from the suggestions.", pickupNoSuggestion: "No Cameroon pickup match found. You can continue with the typed pickup address, or use Current GPS if you want exact pickup.", searchingDestinationAddresses: "Searching destination addresses...", destinationChooseSuggestion: "Choose the matching destination from the suggestions.", destinationNoSuggestion: "No destination suggestion found; continue with the full address.", pickupSuggestionOrTyped: "Choose a suggestion if one matches, or continue with the typed pickup address.", pickupTypedUnlessGps: "Pickup will use the address as typed unless exact current location is checked.", destinationSuggestionOrTyped: "Choose a suggestion if one matches, or continue with the typed destination.", destinationTypedAsEntered: "Destination text will be routed as typed.", signInPassengerPickupSuggestions: "Sign in as a passenger to use pickup suggestions.", signInPassengerPlaceSuggestions: "Sign in as a passenger to use place suggestions.", typePickupSearchMin: "Type at least 3 characters to search pickup addresses.", typeAddressSearchMin: "Type at least 3 characters to search addresses.", pickupEntryPrompt: "Enter a pickup address, or check exact current location.", destinationEntryPrompt: "Start typing a destination address.", capturingCurrentLocation: "Capturing your current location. Approve the browser location prompt if it appears.", currentLocationCaptureFailed: "Current location could not be captured. In Chrome, allow Location for this site or type the pickup address.", currentLocationNeedsHttps: "Current location requires a secure HTTPS page.", browserNoCurrentLocation: "This browser does not support current location. Type the pickup address instead.", rideNoAddedStops: "No added stops.", rideAllStopsMarked: "All {count} added stop(s) marked. Continue to destination.", rideNextStop: "Next stop {current} of {total}: {stop}.", rideRequestReopenedAfterRiderCancel: "Rider cancelled. This request is open again.", rideCancelBeforeChoosingRider: "Cancel any time before choosing a rider.", rideStepRiderMarkArrival: "Mark arrival when you are with the passenger. GPS will be recorded when available.", rideStepPassengerRiderOnWay: "Rider on the way. {fee}", rideStepRiderConfirmPickup: "Confirm pickup to start destination navigation.", rideStepPassengerRiderArrived: "Rider arrived. Meet the rider when ready. {fee}", rideStepPassengerRiderCompletesDropoff: "{progress} Rider completes at drop-off.", rideStepRiderCompleteDestination: "{progress} Complete at destination. {fee}", rideAlreadyComplete: "Ride is already marked complete. {summary}", rideCancelled: "Ride has been cancelled. {fee}", rideActionsProgress: "Ride actions update as the trip progresses.", nextRideQueued: "Next ride queued", nextRequestAvailable: "Next request available", nextRideQueuedDetail: "This pickup is saved for after the current trip. Finish the active ride before opening the next pickup navigation.", nextRequestAvailableDetail: "You can review or offer because you are near drop-off. Current trip navigation stays active until the ride is complete.", destinationNavigation: "Destination navigation", pickupNavigation: "Pickup navigation", readyAtPickup: "Ready at pickup", fromLiveLocation: "{eta} from your live location.", openPickupNavigation: "Open navigation to the verified pickup point.", pickupNavigationNeedsClarification: "Pickup GPS is not clear enough for reliable navigation. Clarify the landmark with the passenger by chat if needed.", navigateToPickup: "Navigate to pickup", riderPickupNavigationClarifyPopup: "Pickup GPS is not clear enough for reliable navigation to {pickup}. Use Contact/Open chat to clarify the exact pickup landmark with the passenger, then continue using the typed pickup address shown on this ride.", riderPickupNavigationClarifyStatus: "Ride accepted. Pickup GPS is not clear enough for navigation. Use Contact/Open chat to clarify the pickup landmark with the passenger.", trackRider: "Track rider", riderCancelled: "Rider cancelled", rideRequest: "Ride request", riderApproachWithEta: "{name}: {eta} away. {source}.", selectedRiderApproachPending: "{name} selected; approach pending.", requestOpenAgainAfterRiderCancel: "Your request is open again for other nearby riders. You do not need to create a new ride request.", requestOpenForNearbyRiders: "Request is open for nearby riders. Destination: {destination}.", continueToDestination: "Continue to destination", continueToStop: "Continue to {stop}", navigateToPlace: "Navigate to {place}", pickup: "pickup", rideActions: "Ride actions", cancelBeforeStart: "Cancel before start", cancelRide: "Cancel ride", endRideEarly: "End ride early", arrivedAtPickup: "Arrived at pickup", pickedUpPassenger: "Picked up passenger", arrivedAtStop: "Arrived at {stop}", completeRideAtDropoff: "Complete ride at drop-off", fare: "Fare", fareSummary: "Fare summary", support: "Support", supportOrReportIssue: "Support or report issue", contact: "Contacter", contactPassenger: "Contact passenger", route: "Route", acceptOrDeclineRouteChange: "Accept or decline requested route change", navigate: "Navigate", navigateNextStopOrDestination: "Navigate to next stop or destination", progress: "Progress", updateRideProgressStatus: "Update pickup, stop, or completion status", reviewRequestedRouteChange: "Review requested route change", cancelThisRide: "Cancel this ride", riderRideTools: "Rider ride tools", routeChangePending: "Route change pending", routeChangePendingDetail: "Accept or decline the passenger route change before opening navigation.", navigation: "Itineraire", openNavigationTo: "Open navigation to {place}.", rideProgress: "Ride progress", nextRideStep: "Next ride step", routeUpdate: "Route update", routeUpdateReviewBeforeChanging: "Review the passenger route-change request before changing course.", messagePassengerFromRide: "Message the passenger in WakaGood from this ride.", contactWakaOrReportRideIssue: "Contact Waka or report a ride issue.", finalFareRoute: "Final matched fare: {fare}. Route: {route}.", rideTools: "Ride tools", writeToSelectedRiderOrPassenger: "Write to the selected rider or passenger", chatOpenConfirmPickupPayment: "Chat is open. Confirm pickup details and {payment} before the ride starts.", myRideRequests: "My ride requests", yourRequest: "Your request", respondToRiderOffer: "Respond to rider offer", chooseRiderOffer: "Choose rider offer", offersForMyRequest: "Offers for my request", activeRide: "Active ride", car: "car", vehicle: "vehicle", incomingVehicleRequests: "Incoming {vehicle} requests", myVehicleOffers: "My {vehicle} offers", liveLocationActive: "Live location active", adminWorkspace: "Admin workspace", adminMarketplaceSummary: "View passengers, riders, approvals, subscriptions, and marketplace activity", adminVisibilityRequired: "Admin sign-in required for full passenger and rider visibility", scheduledRequest: "Scheduled request", incomingRequest: "Incoming request", vehicleRequestLabel: "{vehicle} {type}", scheduled: "scheduled", request: "request", pickupLabelValue: "Pickup: {pickup}", statusWithPerson: "{status} with {name}", routeFromTo: "{pickup} to {destination}", personOfferedFare: "{name} offered {fare}", fareScheduleLine: "Fare {fare} - {schedule}", personOfferedFareSchedule: "{name} offered {fare} - {schedule}", reviewRiderOffersChoose: "Review rider offers and choose when ready.", waitingForRiderToAccept: "Waiting for rider to accept...", waitingForRiderOffers: "Waiting for rider offers", offerAmount: "Offer {fare}", fareAmount: "Fare {fare}", offerCount: "{count} offer(s)", stopCount: "{count} stop(s)", cancelFeeAmount: "Cancel fee {fare}", pickupEtaPending: "Pickup ETA pending", businessRide: "Business ride", vehicleDesignation: "Vehicle designation: {vehicle}", marketplaceChooseNearbyRequest: "Marketplace: choose a nearby {vehicle} request to review or decline", publishOrSelectRideRequest: "Publish or select one of your ride requests", respondToRiderFareOffer: "Respond to {name} - {fare} offer", chooseRiderOfferCount: "Choose rider offer - {count} offer(s) available", waitingForRiderOffersPassengerOffered: "Waiting for rider offers - passenger offered {fare}", selectedRequestRouteSummary: "{pickup} to {destination} - {fareMode} - offer {fare} - {schedule}{approach}", changeDestinationOrAddStop: "Change destination or add a stop", rate: "Rate", rating: "Rating", paymentFareSummary: "Payment and fare summary", contactRider: "Contact rider", addStopOrChangeDestination: "Add stop or change destination", paymentSummary: "Payment summary", finalFarePaymentDestination: "Final matched fare: {fare}. Payment method: {method}. Destination: {destination}.", updateRoute: "Update route", routeChange: "Route change", routeChangeFareRecalculation: "Change destination or add one stop. Waka recalculates the added fare before sending it to the rider.", messageRiderFromRide: "Message the rider in WakaGood from this ride.", negotiableFare: "Negotiable fare", scheduledAtChip: "Scheduled: {time}", immediateRide: "Immediate ride", reopened: "Reopened", matched: "Matched", riderArrived: "Rider arrived", rideInProgress: "Ride in progress", completed: "Completed", cancelled: "Cancelled", unknown: "Unknown", riderCancelledReposted: "Rider cancelled; reposted to nearby riders", matchedToYou: "Matched to you", pickupEtaChip: "Pickup ETA: {eta}", riderPickupEtaChip: "Rider pickup ETA: {eta}", destinationDriveChip: "Destination drive: {distance}", passengerCancelledRideRemoved: "Passenger has canceled this ride request. It has been removed from your active marketplace.", youCancelledRideRemoved: "You cancelled this ride. It has been removed from your active trip list.", rideCancelledRemoved: "This ride was cancelled. It has been removed from your active marketplace.", bike: "Bike", xlSpecial: "XL/Special", normalVehicle: "Normal", riderStatusSignInOrApply: "Sign in or submit an application to access the rider platform.", riderStatusProfileOnly: "Your rider login is active, but Waka does not yet have a rider application for admin review. Complete only the application details on Profile; do not create a second account.", riderStatusPendingDirect: "Your rider application is waiting for Waka Cameroon admin review. Ride requests, offers, and chat unlock after the required document and safety review steps.", riderStatusPendingReviewTesting: "Your rider application is waiting for admin review. Stay on Eligibility to monitor progress and start the relaxed Checkr testing step.", riderStatusPendingReview: "Your rider application is waiting for admin document review. Checkr, Stripe, ride requests, offers, and chat unlock only after the required review steps.", riderStatusBackgroundPendingDirect: "Your rider application passed initial admin review. Complete any requested local document or permit review so admin can make the final decision.", riderStatusBackgroundPendingProvider: "Your rider application passed initial admin review. Complete the Checkr background check from Eligibility checks so admin can make the final decision.", riderStatusCorrectionsWithNote: "Admin requested rider application corrections. Update the rider form and resubmit before review continues. Note: {note}", riderStatusCorrectionsNoNote: "Admin requested rider application corrections. Update the rider form and resubmit before review continues.", riderStatusDeclined: "Your rider application was declined by admin. Contact Waka support before submitting new documents.", riderStatusApprovalRequired: "Admin approval is required before the rider platform unlocks.", riderGpsTapActivateAgain: "{summary} Tap Activate again so nearby requests refresh from your current position.", riderGpsNeededBeforeRequests: "Live GPS is still needed before nearby requests can appear. Tap Activate when you are ready to receive rides.", riderApprovedSetupActive: "Approved. {accessName} is active until {date}. Complete {gaps} before requests appear. {gps}", riderApprovedRenewalDirect: "Approved. {accessName} is active until {date}. MTN/Orange renewal opens 3 days before expiry.", riderApprovedRenewalProvider: "Approved. {accessName} is active until {date}. Payment choices open 3 days before expiry.", riderRenewalReminderDirect: "Top up the rider wallet or pay monthly access before the free period ends.", riderRenewalReminderProvider: "Renewal is due soon; choose manual payment or automatic renewal so paid access starts after this period ends.", riderCompleteSetupBeforeRequests: "Complete {gaps} before ride requests appear. {gps}", riderApprovedRemaining: "Approved. Your {label} has {remaining} left, until {date}.{reminder}{setup}", riderApprovedWalletModel: "Approved. The Cameroon wallet/free-rides model applies after the free period; top up the rider wallet or pay monthly access from Eligibility checks.", riderApprovedPaidInactive: "Approved, but paid rider access is inactive. Choose a Waka Rider Access payment before receiving and responding to ride requests.", zeroRides: "0 rides", completedRidesToday: "Completed rides paid or pending today", sundayThroughToday: "Sunday through today", monthToDateRiderEarnings: "Month-to-date rider earnings", yearToDateRiderEarnings: "Year-to-date rider earnings", completedRideCount: "{count} completed ride(s)", completedWakaMiles: "Completed Waka miles", onlyCompletedWakaRidesCounted: "Only completed Waka rides are counted.", completedRidesPayoutsAppearHere: "Completed rides and rider payouts will appear here after trips are finished.", rideEarningsStatus: "Ride earnings - {status}", amountEarned: "{amount} earned", actualWakaMileage: "Actual Waka mileage: {mileage}", mileagePending: "Mileage pending", nearbyRequestsUseAvailability: "Nearby requests use your availability and pickup radius.", optionalPreferredDestinationsSaved: "Optional preferred destinations saved: {regions}.", riderServiceAreaSummary: "Requests use your live location while you are online: immediate pickups within about {immediate} minutes, and scheduled rides within about {scheduled} minutes in the active launch market.{destinations} {status}", destinationPreferencesOnDestinationPage: "Destination preferences are on the Destination page. Nearby requests use your active location.", preferredDestinationsAndNearbyPickups: "Optional preferred destinations saved: {regions}. Showing all nearby pickups within the pickup radius. {remaining} update(s) remaining today.", showingNearbyPickups: "Showing all nearby pickups within the pickup radius.", onlineActiveRideResumeNearDropoff: "Online for this active ride. New requests resume when you are about 7 minutes from drop-off.{setup}", onlineActiveRidePausedUntilNearDropoff: "Online for this active ride. New immediate requests are paused until the ride starts and you are about 7 minutes from drop-off.{setup}", onlineAvailableGpsPrivate: "Online and available. {gps} is used privately to show only nearby requests.{setup}", locationActive: "Location active", activatedWaitingFreshLocation: "Activated. Waka is waiting for a fresh location update before nearby ride requests appear.{setup}", offlineActivateForNearbyRequests: "Offline. Activate when you are ready to receive nearby ride requests.{setup}", openCorrectionForm: "Open correction form", openEligibilityChecks: "Open eligibility checks", riderNamePlaceholder: "Rider or driver name", credentialPlaceholder: "Driver's license number", registrationPlaceholder: "Plate or registration number", transactionReferencePlaceholder: "Policy number", counterFarePlaceholder: "Enter a higher counter-offer fare", counterNotePlaceholder: "Optional: brief vehicle or fare note for the passenger", supabasePasswordPlaceholder: "Supabase password", chatPlaceholder: "Chat opens only after passenger chooses a rider", safetyReportDetailsPlaceholder: "Describe what Waka should review", offlineReady: "Offline-ready", onlineDemo: "Online demo", localMode: "Local mode", supabaseReady: "Supabase ready", supabaseConfigNeeded: "Supabase config needed", supabaseConnecting: "Supabase connecting", supabaseSdkUnavailable: "Supabase SDK unavailable", manualPhoneVerified: "Manual pilot mode: phone marked verified. Configure SMS OTP before public launch.", smsVerificationRelaxedForTesting: "Testing mode: no SMS code will be sent. Waka has temporarily accepted the registered phone for testing; enable real SMS OTP before public launch.", validPhoneRequired: "Enter a valid phone number before requesting a code.", validDateOfBirthRequired: "Enter a valid date of birth as YYYY-MM-DD, or leave it blank for now. You can type only digits and Waka will add the dashes.", checkingPassengerAccount: "Checking passenger account details...", checkingRiderApplication: "Checking rider application details...", accountMissingFields: "Complete these fields before saving: {fields}.", phoneOtpCooldown: "Please wait {seconds}s before requesting another phone code.", phoneOtpRateLimited: "Too many phone-code attempts. Wait a while before requesting another code, and check Supabase Auth rate limits if this continues.", sendingVerificationCode: "Sending verification code...", verificationCodeSent: "Verification code sent to {phone}.", demoCode: "Demo code: {code} for {phone}", freshVerificationCodeRequired: "Request a fresh verification code for this phone number.", verifyingPhoneNumber: "Verifying phone number...", phoneNumberVerified: "Phone number verified. This only verifies the phone; press Save or Submit to finish creating the Waka account.", verificationCodeIncorrect: "Verification code is not correct.", phoneOtpManualSignIn: "Phone OTP sign-in is disabled in manual pilot mode. Use email and password to sign in.", sendingSignInCode: "Sending sign-in code...", signInCodeSent: "Sign-in code sent to {phone}.", demoSignInCode: "Demo sign-in code: {code} for {phone}", signingInPassword: "Signing in with email and password...", loadingWakaProfile: "Loading Waka profile...", supabaseProfileMissing: "Sign-in worked. This login still needs a Waka profile; complete and save this form to finish linking the account.", wrongProfileRole: "This account is registered as {role}, not {type}.", wrongProfileRoleStrict: "This login is registered as {role}, not {type}. In this WakaGood deployment's single Auth project, the same email cannot safely open both roles. Use a separate {type} email/phone now, or complete the passenger/rider Auth split before reusing this email for both roles.", adminPublicPortalBlocked: "Admin accounts cannot sign in through passenger or rider portals. Use the Admin workspace, or create a separate passenger/rider account for testing.", signedInPassengerLoaded: "Signed in as {email}. Passenger profile loaded. Add a passenger payment method before requesting rides.", signedInRiderLoaded: "Signed in as {email}. Rider profile loaded.", signedInAs: "Signed in as {identity}.", freshSignInCodeRequired: "Request a fresh sign-in code for this phone number.", signInCodeRequired: "Use email and password to sign in. Phone OTP is only for first-time phone verification.", passwordSignInOnly: "Use email and password to sign in. Phone OTP is only for first-time phone verification.", signInEmailPasswordRequired: "Enter the email and password for this account. Phone-code sign-in is disabled in manual pilot mode.", signingIn: "Signing in...", signInCodeIncorrect: "Sign-in code is not correct.", localSignInAccountMissing: "No saved {type} account matches this phone number. Create and save the {type} account first, then sign in.", signedOut: "Signed out.", passengerPhoneBeforeSave: "Verify the passenger phone number before saving the account.", riderPhoneBeforeReview: "Verify the rider phone number before submitting for review.", passengerPaymentRequired: "Add a passenger payment method under Payment before publishing rides.", riderPaymentRequired: "Save a rider payment account before receiving requests.", riderDailyRegionsRequired: "Preferred destination regions are optional for riders.", riderLiveGpsRequired: "Share live rider GPS before receiving requests.", startingPassengerSupabase: "Starting passenger save in Supabase...", savingPassenger: "Saving passenger...", passengerCreated: "{name} passenger account created successfully. Add a passenger payment method next, then request rides.", passengerCreatedEmailPending: "{name} passenger account created successfully. Add a passenger payment method next; email/password sign-in may need Supabase email confirmation or setup.", passengerAccountFailed: "Passenger account was not created: {message}", passengerSyncing: "{name} passenger account created successfully. Add a passenger payment method next, then request rides.", startingRiderSupabase: "Starting rider save in Supabase...", savingRiderApplication: "Saving rider application...", submittingRiderApplication: "Submitting rider application for admin approval...", riderCreatedPending: "{name} account created. Rider application is pending admin approval. If approved, the 30-day free period starts before Waka wallet, MTN MoMo, or Orange Money commission review is required.", riderAccountFailed: "Rider account was not submitted: {message}", missingRiderDocuments: "Add these rider documents if Waka admin requested them: {documents}.", passengerAccountRequired: "Create a passenger account before publishing a ride request.", passengerSignInRequired: "Passenger sign-in is required before publishing rides.", passengerPhoneRequired: "Passenger phone verification is required before publishing rides.", realisticFareRequired: "Enter a realistic fare offer.", fareBelowGuidance: "This fare is below the suggested {min}-{max} range. Riders may skip it or respond more slowly. Continue anyway?", fareOutsideGuidance: "This route estimate suggests {min}-{max}. Lower fares may take longer to match.", scheduledTimeRequired: "Choose a valid date and time for the scheduled ride.", scheduleThirtyMinutes: "Schedule the ride at least 30 minutes from now.", ridePublishedSupabase: "Ride request published to Supabase for eligible riders.", ridePublishedLocal: "Ride request published locally.", publishRideFailed: "Could not publish this ride request: {message}", preparingPickupCityForPublish: "Preparing this request for {city} before publishing...", pickupCityCouldNotBePrepared: "Waka could not prepare the selected pickup city for publishing. Choose the pickup city and area again, then publish the ride request.", subscriptionReferenceRequired: "Provider checkout is required for rider wallet or monthly access payment.", subscriptionAlreadyPending: "A provider rider payment checkout is already in progress.", submittingPaymentSupabase: "Opening rider payment checkout...", savingPaymentReference: "Opening rider payment checkout...", paymentReferenceSubmitted: "Rider payment checkout opened. Wallet or monthly access updates after provider confirmation.", paymentReferenceFailed: "Could not open rider payment checkout: {message}", selectRideRequestFirst: "Select a ride request first.", createRiderFirst: "Create a rider account first.", riderSignInRequired: "Rider sign-in is required before responding to rides.", riderApprovalRequired: "Admin approval is required before responding to rides.", riderAccessRequired: "Your rider wallet/free-rides model or monthly access must be active before responding to rides.", selectNearbyRequest: "Select a nearby request that matches your approved rider account.", requestClosed: "This request is no longer open.", offerSendFailed: "Could not send this offer: {message}", passengerOwnRequestRequired: "Only the passenger who posted this request can choose a rider.", chooseRiderFailed: "Could not choose this rider: {message}", acceptRouteChangeFailed: "Could not accept route change: {message}", acceptRouteChangeSaveFailed: "Could not accept route change: Waka could not save the updated route. Refresh and try again.", declineRouteChangeFailed: "Could not decline route change: {message}", declineRouteChangeSaveFailed: "Could not decline route change: Waka could not save your decision. Refresh and try again.", immediateOfferLocked: "New immediate offers unlock when this ride is about 7 minutes from drop-off.", riderOfferExpired: "That rider offer has expired. Keep the request open or counter with a fresh fare so riders can respond again.", reviewRouteChangeBeforeProceeding: "Review the passenger route change before proceeding. Accept to update navigation, or decline to keep the current route.", rejectOfferFailed: "Could not reject this offer: {message}", safetyReportUnavailable: "Reports are available after a passenger chooses a rider. Contact Waka opens after a rider is selected.", safetyReportNeedsDetail: "Add enough detail for Waka to understand the request.", safetyReportSignInRequired: "Sign in again before contacting Waka.", submittingSafetySupabase: "Sending message to Waka...", savingSafetyReport: "Saving message for Waka review...", safetyReportSubmitted: "Safety report submitted for admin review. Message sent to Waka.", safetyReportFailed: "Could not submit report: {message}", suspendRiderConfirm: "Suspend this rider? They will stop seeing and accepting ride requests immediately.", clearDemoConfirm: "Clear all locally stored demo data?", requestConfirmationFailed: "Could not request confirmation: {message}", confirmScheduledFailed: "Could not confirm this scheduled ride: {message}", reopenScheduledFailed: "Could not reopen this scheduled ride: {message}", stop: "Stop", about: "About", guide: "Guide", androidInstallHelp: "On Android, open this site in Chrome, tap the menu, then choose Add to Home screen or Install app.", homeHeroKicker: "Cameroon ride agreement", homeHeroBody: "A Cameroon-first transport marketplace for city rides and inter-city seats, built around clear fares, visible choices, agency-published departures, and accountable support.", homeHeroPointOne: "Negotiated city rides", homeHeroPointTwo: "Book agency bus seats", homeHeroPointThree: "Cash, MTN MoMo, Orange Money", requestRideCta: "Request a ride", driveWithWakaCta: "Drive with Waka Cameroon", homeHeroProofTitle: "Passenger. Rider. Agency. One Cameroon marketplace.", homeHeroProofBody: "Compare offers, confirm fares, book seats, follow ride state, and keep support records organized.", homeLeadKicker: "Why WakaGood", homeLeadTitle: "Clear fares, public departures, and transport choices that feel organized.", homeLeadBody: "WakaGood helps visitors request city rides, compare rider offers, search inter-city departures, choose available seats, and reach approved agency workspaces without mixing roles or hiding the next step.", intercityTravel: "Inter-city travel", transportAgenciesNav: "Transport agencies", browseTransportAgencies: "Browse transport agencies", agencyPortalNav: "Agency portal", serviceChooserKicker: "Choose your Waka service", serviceChooserTitle: "Transport options for Cameroon, clearly separated.", serviceChooserBody: "Use WakaGood to request city rides, search inter-city departures, or sign in to an approved transport-agency workspace.", servicePassengerKicker: "Passenger rides", servicePassengerTitle: "Request city rides with a clear FCFA offer.", servicePassengerBody: "Enter pickup, destination, luggage, and passenger count before choosing a rider offer.", servicePassengerPointOne: "Landmarks and typed addresses", servicePassengerPointTwo: "Nearby rider offers", servicePassengerPointThree: "Trip lifecycle and ratings", servicePassengerCta: "Open passenger rides", serviceIntercityTitle: "Search agencies, compare departures, and book seats.", serviceIntercityBody: "Filter by agency, date, departure city, and destination city before reserving seats.", serviceIntercityPointOne: "Agency and city filters", serviceIntercityPointTwo: "Seat maps and boarding points", serviceIntercityPointThree: "Email receipts and traveler updates", serviceIntercityCta: "Search inter-city departures", serviceAgencyKicker: "Agency workspace", serviceAgencyTitle: "Approved agency access for operators.", serviceAgencyBody: "Transport companies apply first. After Waka admin approval, they can publish departures and manage bookings.", serviceAgencyPointOne: "Admin approval before publishing", serviceAgencyPointTwo: "Weekly departures and bus capacity", serviceAgencyPointThree: "Bookings, payments, and messages", serviceAgencyCta: "Agency sign in", agencyPortalKicker: "Agency portal", agencyPortalTitle: "A dedicated page for approved transport operators.", agencyPortalBody: "Agencies sign in on their own page to publish trips, manage seats, review bookings, share promotions, and keep their public page current after Waka approval.", agencyPortalPointThree: "Public page, promotions, and reports", openAgencyPortal: "Open agency portal", agencyDirectoryKicker: "Transport agencies", agencyDirectoryTitle: "Find approved transport agencies and book published departures.", agencyDirectoryBody: "Open an agency page to see its current departures, seats left, boarding point, fare, and booking option.", searchAllDepartures: "Search all departures", searchAllIntercityDepartures: "Search all inter-city departures", loadingApprovedAgencyPages: "Loading approved agency pages.", noPublicAgencyPages: "No public agency page is listed yet.", whenAdminPublishesAgencyPage: "When Waka admin publishes an agency page, it will appear here for travelers.", wakaAdminControlsPublicAgencyPages: "Approved agency pages and live departures appear here.", wakaAdminPublishedPage: "Waka admin published page", openAgencyPage: "Open agency page", bookDepartures: "Book departures", agencyDirectoryDefaultDescription: "Waka-approved public agency page with departures, seats, and booking access.", agencyDirectoryNextDeparture: "Next departure", agencyDirectoryPromotions: "Approved promotions", publicLearnMoreKicker: "Learn more", publicLearnMoreTitle: "Use WakaGood from the right doorway.", publicLearnMoreBody: "Visitors can search departures from the inter-city travel page. Passengers, riders, and agencies use their own pages so account tasks, trip work, and operator controls stay clear.", searchIntercityDepartures: "Search inter-city departures", readHowWakaWorks: "Read how Waka Cameroon works", intercityHeroTitle: "Book a seat on Cameroon bus routes through WakaGood.", intercityHeroBody: "Transport agencies can publish weekly departures, seat counts, boarding terminals, and payment instructions. Travelers can browse operators, compare cities and times, and book with or without a personal Waka account.", intercityOperatorSubscriptionMessage: "Operators subscribe through WakaGood for 25,000 FCFA per month. Travelers can pay by MTN Mobile Money, Orange Money, or pay on arrival when the agency allows it. Pay-on-arrival seat holds close one hour before departure.", intercityBookNow: "Book travel now", intercityAgencySetup: "Set up transport agency", intercityBrowseDeparturesCta: "Browse departures", intercityOperatorAccessCta: "Transport agency access", agencySetupNav: "Agency setup", transportAgencies: "Transport agencies", whatHappensNext: "What happens next", intercityStepOne: "Step 1 - choose a departure", intercityStepTwo: "Step 2 - reserve seats", selectedTripDetails: "Selected trip details", chooseDepartureSummaryPrompt: "Select a departure to review the agency, route, boarding point, payment options, fare, and seats left before booking.", travelerGuideTitle: "How travelers book", travelerGuideBody: "Choose your departure city, destination city, and travel date. Browse operators below, compare route details, review boarding point, drop-off point, seats, fare, and payment options, then continue to the booking form.", openBookingForm: "Browse operators and departures", agencyGuideTitle: "How agencies set up", agencyGuideBody: "Agencies use a separate Waka agency access flow. Existing operators sign in to open the agency workspace. New operators create an agency account first, then wait for Waka admin approval before they can publish departures, manage blocked seats, review bookings, and message travelers.", passengerIntercityNav: "Inter-city travel", passengerAgencyTravel: "Inter-city travel", passengerAgencyTravelTitle: "Search transport agencies and book inter-city departures", passengerAgencyTravelBody: "Use the same WakaGood inter-city travel search here inside your passenger account. Compare agencies, dates, route details, seat availability, and payment options, then book and later review your bus trips under My trips.", alsoOnWaka: "Also on Waka", passengerIntercityShortcutTitle: "Book inter-city agency travel", passengerIntercityShortcutBody: "Need a bus or agency trip instead of a local ride? Open Inter-city travel to search transport agencies by date, departure city, destination city, route, fare per seat, and remaining seats before booking.", openPassengerIntercityTravel: "Open inter-city travel", openAgencySetup: "Continue to agency workspace", agencySignInCta: "Agency sign in", agencyCreateCta: "Create agency account", agencyEntryHelp: "Agency sign in is for operators that already have Waka agency access. Create agency account is for a new transport company starting setup for the first time. Passenger account creation stays separate.", weeklyOperationsTitle: "Weekly travel operations", weeklyOperationsBody: "Agencies can update travel status, confirm payments, cancel bookings when necessary, and keep passengers informed. Travelers can book without an account or sign in to track seat numbers, messages, and route updates.", seeDepartures: "Choose agency and departure", intercitySearchSimpleBody: "Search by agency, travel date, departure city, and destination city. Each result shows route, boarding point, seats, fare, and payment options before booking.", availableIntercityDeparturesTitle: "Available inter-city departures", departureBrowserHelp: "Use the agency, date, From, and To filters to narrow the list, then select a matching departure.", reviewBeforeBookingTitle: "Review selected trip, then book", agencyStepOneLabel: "1. Register agency", agencyStepOneBody: "Open Agency access, create or sign in to the operator account, and save the company profile with support phone, email, terminal address, and operated cities for admin approval.", agencyStepTwoLabel: "2. Publish departures", agencyStepTwoBody: "After admin approval, use Weekly departures to enter origin city, destination city, boarding point, travel time, bus label, seats, and fare per seat.", agencyStepThreeLabel: "3. Update live availability", agencyStepThreeBody: "Block seats, pause a departure, or change travel status to boarding, departed, arrived, delayed, or cancelled.", agencyStepFourLabel: "4. Manage bookings", agencyStepFourBody: "Confirm payments, cancel passengers when needed, mark check-in or completion, and message travelers by app or email.", anyTransportAgency: "Any transport agency", anyDepartureCity: "Any departure city", anyDestinationCity: "Any destination city", fromLabel: "From", toLabel: "To", cameroonCitiesDirectoryTitle: "Cameroon cities on Waka", transportAgenciesDirectoryTitle: "Transport agencies on Waka", travelDate: "Travel date", findDepartures: "Find departures", loadingAgencyDepartures: "Loading verified agency departures.", loadingTransportAgencies: "Loading transport agencies...", refreshingAgencyDepartures: "Refreshing approved agency departures...", upcomingDepartures: "Upcoming departures", departureListSummary: "Companies, times, seats, and routes", bookByEmail: "Book by email", reserveIntercitySeat: "Reserve your inter-city seat", chooseDepartureToContinue: "Choose a departure to continue.", chooseDepartureToUnlockBooking: "Choose a departure from the list. Traveler details, seat selection, and payment open here after selection.", chooseSeatsBeforeBooking: "Choose available seats", selectSeatsFromMap: "Select seat numbers from the map before booking.", selectedSeatsLabel: "Selected seats", amountToPay: "Amount to pay", chooseDepartureShort: "Choose departure", noneSelected: "None", zeroXaf: "Aucun montant", travelerName: "Traveler name", emailReceipt: "Email receipt", shortPhone: "Phone", seats: "Seats", seatsLeftLabel: "Seats left", boardingLocationLabel: "Boarding point", dropoffLocationLabel: "Drop-off point", paymentOption: "Payment option", payOnArrival: "Pay on arrival (seat held)", payOnArrivalClosedOption: "Pay on arrival (closed within 1 hour)", bookingNote: "Booking note", bookDeparture: "Book departure", bookingReceiptMessage: "Your booking receipt will be sent by email. Pay on arrival is available only until one hour before departure, and selected seats are held for the booking.", payOnArrivalSelectionWarning: "Pay on arrival keeps the selected seats held for this booking. Arrive early and follow the agency payment instructions.", payOnArrivalClosedWarning: "Pay on arrival closes one hour before departure. Use mobile money to reserve your selected seats now.", payOnArrivalBookingSavedWarning: "Pay on arrival was selected. The chosen seats are held for this booking; arrive early and follow the agency payment instructions.", intercityOperators: "Inter-city operators", registerTransportAgency: "Register a transport agency", operatorProfileTitle: "Agency profile and public page", operatorAgencyStatusDefault: "Manage the agency identity, logo, support contacts, terminal address, payment numbers, and public page details.", operatorAccessMessage: "Sign in as a passenger, then register your company and weekly bus departures for WakaGood inter-city bookings.", operatorSetupKicker: "Inter-city operators", agencyProfileHelp: "Save the agency profile first. Waka uses this company identity, support contact, terminal address, and operated cities on the public travel website.", operatorStepTwoKicker: "Step 2 - keep service active", operatorPaymentHelp: "Agencies start with one free month. Submit the monthly fee record here so Waka can keep future departures visible after the trial period.", operatorStepThreeKicker: "Step 3 - publish departures", operatorDepartureHelp: "Passengers only see departures after Waka admin approves the agency and the agency publishes active routes with origin city, destination city, departure time, boarding point, seat count, and payment options.", operatorStepFourKicker: "Step 4 - manage travelers", agencySetupChecklistTitle: "Agency setup checklist", agencySetupChecklistBody: "This workspace is where agencies create their public company profile, wait for admin approval, publish departures, keep service active, and manage traveler bookings.", agencyProfileStatusLabel: "Agency profile", serviceVisibilityLabel: "Public visibility", publishedDeparturesLabel: "Published departures", travelerBookingsLabel: "Traveler bookings", latestOperatorPaymentLabel: "Latest operator payment", seatPublishingRuleLabel: "Passenger view", seatPublishingRuleBody: "Passengers only see active departures with route, time, boarding point, fare, and seats left.", operatorNextActionStart: "Start by saving the company profile with support contacts, operated cities, and terminal address.", operatorNextActionPublish: "Next, open Weekly departures and publish at least one route with the destination city, departure time, seats, and payment options.", operatorNextActionPayment: "Your free month can publish now. Submit the monthly fee record before the trial ends so future departures stay visible.", operatorNextActionManage: "Keep departures updated, block seats when needed, and manage traveler bookings below.", operatorConsoleKicker: "Agency control center", operatorConsoleTitle: "Run your transport agency faster", operatorConsoleBody: "Agency admins control their own profile, departures, seat holds, bookings, payments, advertising, and traveler messages. Waka admin can assist and update records on request.", operatorTabProfile: "Profile", operatorTabDepartures: "Departures", operatorTabBookings: "Bookings", operatorTabReports: "Reports", operatorTabPayments: "Payments", operatorTabAdvertising: "Advertising", operatorTabMessages: "Messagerie", operatorAgencyFilter: "Agency", operatorStatusFilter: "Filter", operatorSearchLabel: "Search", operatorSearchPlaceholder: "Search route, traveler, city, or reference", operatorReportsKicker: "Trip reports", operatorReportsTitle: "Pre-departure transaction overview", operatorReportsBody: "Review each departure before boarding: paid and pending amounts, passenger contacts, selected seats, booking references, and payment methods.", adminIntercityTripReportsTitle: "Trip reports for Waka admin", adminIntercityTripReportsBody: "Each departure report shows agency, travel date, time, From, To, seats, totals, paid and pending transactions, and passenger manifest before the trip commences.", adminIntercityReportsPageHeading: "Agency trip reports and passenger manifests", adminIntercityReportsPageBody: "Review departures, fare totals, paid and pending bookings, seat counts, and passenger manifests without crowding the agency operations page.", adminIntercityReportsPageStatus: "Choose an agency from the filter or open reports from an agency card.", operatorFilterAllAgencies: "All agencies", operatorFilterAllItems: "All items", operatorFilterActive: "Active", operatorFilterTrial: "Trial", operatorFilterPaymentDue: "Payment due", operatorFilterPendingReview: "Pending review", operatorFilterSuspended: "Suspended", operatorFilterPaused: "Paused", operatorFilterScheduled: "Scheduled", operatorFilterBoarding: "Boarding", operatorFilterDeparted: "Departed", operatorFilterArrived: "Arrived", operatorFilterCompleted: "Completed", operatorFilterDelayed: "Delayed", operatorFilterCancelled: "Cancelled", operatorFilterUpcoming: "Upcoming", operatorFilterToday: "Today", operatorFilterPendingPayment: "Pending payment", operatorFilterPaid: "Paid", operatorFilterReserved: "Reserved", operatorFilterConfirmed: "Confirmed", operatorFilterCheckedIn: "Checked in", operatorFilterFromPassenger: "From passenger", operatorFilterFromOperator: "From operator", operatorFilterFromAdmin: "From Waka admin", operatorFilterRecent: "Last 24h", operatorFilterApproved: "Approved", operatorFilterRejected: "Rejected", operatorVisibleCountLabel: "Visible now", operatorNextActionLabel: "Next action", operatorMessagesKicker: "Messagerie", operatorMessagesTitle: "Recent traveler messages", operatorMessagesBody: "Passenger and agency updates appear here so operators can reply faster, keep context, and resolve travel issues without leaving the workspace.", agencySignInKicker: "Agency access", agencySignInTitle: "Agency sign in", agencySignInBody: "Agency accounts are separate from passenger accounts. Apply first, wait for Waka admin approval, then sign in to manage departures and bookings.", agencyCreateTitle: "Create agency account", agencyCreatePrompt: "New transport company to Waka Cameroon?", guestBookingAllowed: "Guest booking allowed", guestBookingDisabled: "Guest booking disabled", guestBookingLabel: "Guest booking", travelStatusLabel: "Travel status", agencyTerminalLabel: "Agency terminal", reviewAgencyDepartureCta: "Review agency and departure", companyName: "Company name", companySlug: "Company slug", supportEmail: "Support email", supportPhone: "Support phone", headOfficeCity: "Head office city", mainTerminalAddress: "Main terminal address", operatedCities: "Operated cities", agencyDescription: "Agency description", mtnMomoAccountName: "MTN MoMo account name", mtnMomoNumber: "MTN MoMo number", orangeMoneyAccountName: "Orange Money account name", orangeMoneyNumber: "Orange Money number", saveAgencyProfile: "Save agency profile", monthlyPlatformFee: "Monthly platform fee", submitOperatorSubscription: "Submit the 25,000 FCFA operator subscription", notAvailable: "Not available", suspended: "Suspended", pendingReview: "Pending review", agency: "Agency", route: "Route", origin: "Departure city", chooseAgency: "Choose agency", billingMonth: "Billing month", paymentMethod: "Payment method", cash: "Cash", payerName: "Payer name", payerPhone: "Payer phone", shortReference: "Reference", submitMonthlyFeeRecord: "Submit monthly fee record", operatorFeeReviewMessage: "WakaGood reviews operator subscription submissions before keeping departures active.", weeklyDepartures: "Weekly departures", publishBusesTitle: "Publish buses, seats, cities, and travel times", departureDateTime: "Departure date and time", originCity: "Origin city", destinationCity: "Destination city", boardingLocation: "Boarding location", dropoffLocation: "Dropoff location", estimatedDurationMinutes: "Estimated duration (minutes)", farePerSeat: "Fare per seat (FCFA)", departureNote: "Departure note", acceptMtnMomo: "Accept MTN MoMo", acceptOrangeMoney: "Accept Orange Money", allowPayOnArrival: "Allow pay on arrival", saveDeparture: "Save departure", departureWebsiteMessage: "Passengers will see active departures, available seats, and payment options on the website.", travelerBookings: "Traveler bookings", recentReceiptsReservations: "Recent receipts and reservations", myBusBookings: "My bus bookings", travelerBookingMessageCenter: "Signed-in traveler bookings, seat numbers, route updates, and operator messages appear here.", additionalCities: "Additional cities", busVehicleLabel: "Bus / vehicle label", travelStatusLabel: "Travel status", scheduled: "Scheduled", boarding: "Boarding", departed: "Departed", arrived: "Arrived", completed: "Completed", delayed: "Delayed", cancelled: "Cancelled", statusNoteLabel: "Status note", advertisingTitle: "Advertising", paidPromotionRequests: "Paid promotion requests", agencyPromotionsKicker: "Agency promotions", agencyPromotionsTitle: "Travel offers and updates from approved agencies.", agencyPromotionsBody: "Browse Waka-approved agency announcements, route promotions, photos, and travel updates before choosing your departure.", campaignName: "Campaign name", campaignNamePlaceholder: "Holiday Douala route push", publicPromotionTitle: "Public promotion title", publicPromotionTitlePlaceholder: "Weekend seats to Douala now available", publicPromotionBody: "Public promotion details", publicPromotionBodyPlaceholder: "Describe the route, offer, bus comfort, schedule change, or passenger notice that should appear publicly.", placementLabel: "Placement", websiteBanner: "Website banner", featuredRoute: "Featured route", homepageSpotlight: "Homepage spotlight", pushNotice: "Push notice", durationDays: "Duration (days)", promotionStart: "Start showing", promotionEnd: "Stop showing", promotionCtaLabel: "Button label", promotionCtaLabelPlaceholder: "Book this route", promotionCtaUrl: "Optional link", promotionCtaUrlPlaceholder: "https://agency.example.com/offer", promotionImage: "Promotion picture", promotionImageAlt: "Picture description", promotionImageAltPlaceholder: "Bus, terminal, route, or offer image", promotionPublicActive: "Show publicly after Waka approval", viewAgencyDepartures: "View departures", startsLabel: "Starts", endsLabel: "Ends", amountXafLabel: "Amount (FCFA)", paymentReference: "Payment reference", advertisingPaymentReferencePlaceholder: "Advertising payment reference", campaignNote: "Campaign note", campaignNotePlaceholder: "Target route, audience, or schedule note", payerNamePlaceholder: "Finance officer", submitAdvertisingRequest: "Submit advertising request", advertisingSeparateFeeMessage: "Advertising is billed separately from the monthly operator fee and reviewed by WakaGood.", bookingActionGuide: "Use booking actions to confirm payment, cancel seats, send traveler updates, or mark check-in and completion.", intercityAdminLabel: "Agencies", intercityAdminTitle: "Agencies, public pages, and travel operations", intercityAdminStatus: "Agency trials, departures, bookings, seat blocks, payments, advertising, and traveler messaging appear here.", pendingAgencyApprovalsTitle: "Pending agency approvals", pendingAgencyApprovalsBody: "New transport agency applications appear here first for Waka admin approval. Approve an agency here before it can sign in, publish trips, receive bookings, or appear publicly.", agencyApprovalsAdminLabel: "Agency approvals", agencyApprovalsAdminStatus: "Review new transport agency applications before they can publish trips, receive bookings, or appear publicly.", agencyApprovalsPendingTitle: "Pending agency applications", agencyApprovalsPendingBody: "Approve agencies that meet Waka requirements, decline applications that should not proceed, or message operators for corrections before deciding.", intercityRecordCountZero: "0 inter-city records", cityBamenda: "Bamenda", cityBafoussam: "Bafoussam", cityBuea: "Buea", cityDouala: "Douala", cityYaounde: "Yaounde", cityLimbe: "Limbe", cityKumba: "Kumba", cityMamfe: "Mamfe", cityKribi: "Kribi", cityMbouda: "Mbouda" }, fr: { admin: "Administrateur", pageTitle: "Waka - Courses negociees", installed: "Installee", chooseAccountType: "Choisissez le type de compte", continueAsPassenger: "Continuer comme passager", continueAsRider: "Continuer comme conducteur", signInOrCreate: "Connexion ou creation de compte", createAccount: "Creer un compte", paySubscription: "Ouvrir le paiement automatique", passengerPanelSubtitle: "Demandez une course et choisissez la meilleure offre", riderPanelSubtitle: "Postulez, abonnez-vous, puis negociez les courses", email: "E-mail", password: "Mot de passe", phoneNumber: "Numero de telephone", otpCode: "Code OTP", sendOtp: "Envoyer OTP", sendCode: "Envoyer le code", verify: "Verifier", signOut: "Se deconnecter", fullName: "Nom complet", profilePicture: "Photo de profil", phoneVerificationCode: "Code de verification telephone", nationalIdNumber: "Numero d'identification nationale", identityReference: "Reference d'identite", driverLicenseNumber: "Numero de permis de conduire", dateOfBirth: "Date de naissance", country: "Pays", city: "Ville", passengerSignInHelp: "Connectez-vous avec l'e-mail et le mot de passe avant de demander une course.", riderSignInHelp: "Connectez-vous avec l'e-mail et le mot de passe avant de repondre aux courses.", passengerWorkspace: "Espace passager", riderWorkspace: "Espace conducteur", passengerSignedIn: "Passager connecte", riderSignedIn: "Conducteur connecte", readyToRequestRides: "Pret a demander des courses.", applicationStatusWillAppear: "Le statut de la demande apparaitra ici.", noPassengerSaved: "Aucun passager enregistre.", noRiderApplication: "Aucune demande conducteur enregistree.", pickupArea: "Zone de prise en charge", pickupCity: "Ville de prise en charge", pickupCityHelp: "Choisissez ou la prise en charge aura lieu. Si vous reservez pour quelqu'un ailleurs, choisissez sa ville et sa zone de depart, puis tapez un repere clair ci-dessous.", pickupOtherAreaHelp: "Si la zone exacte de depart n'est pas dans la liste, choisissez Autre et tapez le quartier, carrefour, parc auto, boutique, ecole, eglise ou repere local dans l'espace de depart.", pickupDescription: "Description du lieu", destination: "Lieu de destination", rideTiming: "Moment de la course", asSoonAsPossible: "Des que possible", scheduleAhead: "Planifier", scheduledDateTime: "Date et heure prevues", vehicle: "Vehicule", vehicleType: "Designation du vehicule", bike: "Moto", car: "Voiture", bikeOrCar: "Moto ou voiture", fareOffer: "Prix propose", paymentPreference: "Mode de paiement", cashInHand: "Especes", mtnMoney: "Argent mobile MTN", orangeMoney: "Argent Orange", agreeWithRider: "Accord avec le conducteur avant la course", optional: "Optionnel", otherOption: "Autre", record: "Enregistrer", clear: "Effacer", riderAccess: "Acces conducteur", applicationStatus: "Statut de la demande", riderPlatformStatus: "Le statut de votre espace conducteur apparaitra ici.", createRiderAccount: "Creer un compte conducteur", createRiderAccountAndSubmitApplication: "Creer le compte et envoyer la demande", submitRiderApplication: "Envoyer la demande conducteur", resubmitCorrectedApplication: "Renvoyer la demande corrigee", riderApplicationIntro: "Creez un seul profil conducteur et envoyez-le une seule fois pour validation admin Waka. Commencez par les informations de base; l'admin Waka peut demander des documents plus tard.", riderLoginOnlyIntro: "Creez un seul profil conducteur et envoyez-le une seule fois pour validation admin Waka.", riderOneStepApplicationIntro: "Saisissez une seule fois les informations de base: compte, telephone, zone, moto ou voiture, plaque, marque, modele et couleur. Les pieces et details d'identite optionnels peuvent etre ajoutes plus tard si l'admin les demande.", riderApplicationOnlyIntro: "Votre e-mail est confirme et votre connexion conducteur est active. Completez seulement les informations conducteur de base manquantes ci-dessous; ne creez pas un deuxieme compte.", riderCorrectionIntro: "L'admin a demande des corrections. Mettez a jour uniquement les details demandes et renvoyez la demande.", riderLoginOnlyStatus: "Completez le formulaire conducteur une seule fois. Waka envoie le lien de confirmation apres enregistrement de la demande.", riderOneStepApplicationStatus: "Completez une seule fois le formulaire de base, confirmez que vous etes humain, puis Waka enregistre la demande et envoie le lien de confirmation par e-mail.", riderApplicationOnlyStatus: "Votre connexion conducteur est active, mais Waka n'a pas encore de demande pour validation admin. Completez seulement les details restants ci-dessous.", riderProfileLoaded: "Profil deja charge", riderFinishApplicationOnly: "Completez seulement les details de la demande conducteur ci-dessous. Votre nom, e-mail, telephone et mot de passe restent lies au compte connecte.", account: "Compte", riderAccountSectionHelp: "Ces informations creent la connexion conducteur et la rattachent a cette demande.", identity: "Identite", riderIdentitySectionHelp: "Utilisez les memes informations d'identite que l'agence ou l'admin Waka pourra verifier ensuite.", riderVehicleSectionHelp: "Choisissez d'abord moto ou voiture afin que Waka ne demande que les details pertinents.", vehicleSectionTitle: "Vehicule", bikeSectionTitle: "Moto", vehicleSectionHelp: "Choisissez d'abord moto ou voiture afin que Waka ne demande que les details pertinents.", bikeSectionHelp: "Moto selectionnee. Waka demande uniquement les details utiles pour la moto.", vehicleCategory: "Type de vehicule", bikeType: "Type de moto", documents: "Pieces justificatives", riderDocumentsSectionHelp: "Les televersements de documents sont facultatifs pendant l'inscription pilote. L'admin Waka peut demander les fichiers necessaires plus tard.", operatingArea: "Zone d'activite", credentialNumber: "Numero de permis de conduire", vehicleMake: "Marque du vehicule", vehicleModel: "Modele du vehicule", bikeMake: "Marque de la moto", bikeModel: "Modele de la moto", bodyType: "Type de carrosserie", vehicleDesignation: "Designation du vehicule", yearOfManufacture: "Annee de fabrication", vehicleColor: "Couleur", vehicleVin: "VIN du vehicule", vehicleRegistration: "Numero de plaque", bikePlateNumber: "Numero de plaque de la moto", driverLicenseDocument: "Document du permis de conduire", vehicleRegistrationDocument: "Document d'immatriculation", bikeRegistrationDocument: "Document d'immatriculation de la moto", vehicleBackgroundConsent: "J'autorise Waka a examiner mon eligibilite conducteur, mon identite, mon vehicule, mon permis, mon assurance et mes dossiers de securite pour validation admin.", bikeBackgroundConsent: "J'autorise Waka a examiner mon eligibilite conducteur, mon identite, l'immatriculation de ma moto, mon permis et mes dossiers de securite pour validation admin.", bikeModeNote: "Moto selectionnee. Les champs reserves aux voitures sont ignores pour cette demande.", carModeNote: "Voiture selectionnee. Les details optionnels d'assurance et d'inspection peuvent etre ajoutes maintenant ou plus tard si l'admin les demande.", nationalIdDocument: "Televersement de la carte nationale d'identite", subscriptionIntro: "Les conducteurs approuves recoivent 30 jours gratuits apres validation admin. Ensuite, les 5 premieres courses terminees par jour restent gratuites et la course 6 et plus utilise le wallet conducteur sauf si l'acces mensuel est actif.", riderFarePaymentMode: "Mode de paiement des courses conducteur", riderFarePaymentModeHelp: "Au Cameroun, les conducteurs recoivent les courses negociees directement des passagers en especes, MTN Mobile Money ou Orange Money. Le rechargement wallet et l'acces mensuel optionnel se paient separement dans le panneau Acces conducteur.", riderDirectFareCollectionHelp: "Le paiement des courses reste direct en especes, MTN Mobile Money ou Orange Money sauf si Waka active plus tard le reglement en ligne des courses.", reviewDirectFareMode: "Verifier le mode de paiement direct", riderDirectFareModeActive: "Le paiement direct passager-vers-conducteur est actif apres approbation.", riderPaymentChoice: "Paiement conducteur", walletTopupBundleXaf: "Recharge wallet - des 5 000 FCFA", monthlyRiderAccessXaf: "Abonnement acces mensuel - 15 000 FCFA", walletTopupAmount: "Montant de recharge wallet", paymentProviderChoice: "Fournisseur de paiement", subscriptionPaymentProviderHelp: "Choisissez MTN Mobile Money ou Orange Money pour recharger le wallet ou payer l'abonnement mensuel optionnel.", paymentProvider: "Assureur", paymentPhone: "Telephone de paiement", transactionReference: "Numero de police d'assurance", subscriptionPaymentHelp: "Au Cameroun, les conducteurs rechargent leur wallet des 5 000 FCFA ou paient l'acces mensuel optionnel de 15 000 FCFA par MTN Mobile Money ou Orange Money.", yourFare: "Votre prix", messageBeforeSelection: "Message avant selection", openRequests: "Demandes ouvertes", passengers: "Passagers", riders: "Conducteurs", pendingRiders: "Conducteurs en attente", subscribed: "Abonnes", loadDemoMarket: "Charger le marche demo", clearDemoData: "Effacer les donnees demo", selectOrPublish: "Selectionnez ou publiez une demande", refreshMarket: "Actualiser le marche", all: "Tous", rideRequests: "Demandes de course", riderOffers: "Offres conducteurs", accountDetail: "Detail du compte", postSelectionChat: "Chat apres selection", locked: "Verrouille", send: "Envoyer", voice: "Vocal", chooseRider: "Choisir conducteur", openFullReview: "Ouvrir la revue complete", approve: "Approuver", decline: "Refuser", passengerNamePlaceholder: "Nom du passager", passwordPlaceholder: "Mot de passe", createPasswordPlaceholder: "Creer un mot de passe", codePlaceholder: "Code a 6 chiffres", nationalIdPlaceholder: "Numero d'identification nationale", driverLicensePlaceholder: "Numero de permis de conduire", pickupDescriptionPlaceholder: "Repere, couleur du batiment, marche, carrefour, boutique", destinationPlaceholder: "Zone, repere ou adresse de destination", luggagePieces: "Bagages", luggageNote: "Note bagages", luggageNotePlaceholder: "Facultatif: sacs, cartons, colis, objets fragiles", tripDetailsRiderHint: "Les conducteurs voient ces details de trajet avant d'accepter ou de negocier. Waka Cameroon n'estime pas et ne fixe pas automatiquement le prix de cette demande.", rideFareGuidanceManual: "Entrez le prix que vous voulez proposer en FCFA. Les suggestions sont facultatives; les adresses tapees pour le depart et la destination peuvent aussi etre publiees. Waka envoie le vehicule, le nombre de passagers, les bagages et les arrets aux conducteurs pour accord.", fareGuidanceNeedPickupDestination: "Entrez les adresses de depart et de destination, puis le prix en FCFA que vous voulez proposer.", fareGuidancePendingRoute: "Entrez une destination et utilisez la position actuelle ou une adresse de depart pour estimer le prix avant publication.", fareGuidanceTripContext: "Contexte du trajet: {routeSummary}. Entrez votre offre en FCFA; les conducteurs peuvent accepter, refuser ou contre-proposer quand la negociation est activee.", routeServiceFallbackEstimate: "Le service d'itineraire est indisponible pour le moment; cette estimation rapide est affichee.", currentLocationNeedsAddressForEstimate: "Choisissez le depart dans les suggestions Cameroun, ou cochez Actuel et capturez le GPS, avant que Waka estime le prix.", confirmedDestinationRequiredForEstimate: "Choisissez la destination dans les suggestions Cameroun avant que Waka estime le prix.", confirmedLocationsRequiredForEstimate: "Choisissez le depart et la destination dans les suggestions Cameroun avant que Waka estime le prix. Pour le depart, le GPS actuel est aussi accepte.", pickupGpsCaptureTimeUnavailable: "L'heure de capture du GPS de depart est indisponible. Waka peut quand meme publier avec un repere tape clairement.", pickupGpsOld: "La position de depart exacte date de {minutes} minutes. Waka peut quand meme publier avec un repere tape clairement.", pickupGpsAccuracyUnavailable: "La precision GPS du depart est indisponible. Waka peut quand meme publier avec l'adresse ou le repere de depart.", pickupGpsNeedsClearerSignal: "La position exacte de depart a besoin d'un signal plus clair. Waka peut quand meme publier avec un repere tape clairement.", pickupGpsFallbackStatus: "Le GPS exact du depart n'etait pas assez clair; Waka publiera avec l'adresse de depart tapee.", publishTypedPickupStatus: "Publication avec l'adresse de depart tapee et la ville/zone choisie.", typedPickupStatus: "Adresse de depart tapee utilisee. Les conducteurs verront le texte du depart et la ville/zone choisie.", typedPickupStatusBeforePublish: "Le depart n'a pas ete associe a un lieu GPS enregistre. Waka peut quand meme publier, mais ajoutez un repere clair pour aider le conducteur.", typedPickupUsePhoneGpsPrompt: "Ce depart n'a pas ete associe a un lieu GPS enregistre. Utilisez le GPS de ce telephone seulement si ce telephone se trouve physiquement au point de depart, afin que les conducteurs proches de ce depart voient la demande. Si vous reservez pour quelqu'un ailleurs, choisissez Annuler, puis verifiez que la ville/zone de depart et le repere sont corrects. Waka montrera quand meme au conducteur le texte et le repere de depart que vous avez tapes.", typedPickupPhoneGpsCapturing: "Capture du GPS du telephone pour afficher cette demande aux conducteurs proches...", typedPickupPhoneGpsLinked: "GPS du telephone lie pour la recherche de conducteurs proches. Les conducteurs verront toujours le repere de depart tape.", typedPickupPhoneGpsSkipped: "Le GPS du telephone n'a pas ete ajoute. Waka publiera avec la ville/zone de depart choisie et le repere tape; les conducteurs pourront clarifier par chat si necessaire.", typedPickupPhoneGpsUnavailable: "Le GPS du telephone n'a pas pu etre capture clairement. Waka publiera avec la ville/zone de depart choisie et le repere tape; les conducteurs pourront clarifier par chat si necessaire.", typedPickupPhoneGpsNeedsHttps: "Le GPS du telephone exige une page HTTPS securisee. Waka publiera plutot avec la ville/zone de depart choisie et le repere tape.", typedPickupPhoneGpsUnsupported: "Ce navigateur ne peut pas partager le GPS du telephone. Waka publiera plutot avec la ville/zone de depart choisie et le repere tape.", typedDestinationStatusBeforePublish: "La destination n'a pas ete associee a un lieu GPS enregistre. Waka peut quand meme publier, mais le conducteur a besoin d'un repere clair.", landmarkReminderPopup: "Le GPS exact ou un point de carte enregistre n'a pas ete confirme pour cette course. Waka va quand meme la publier. Ajoutez un repere proche, le quartier, un carrefour, une boutique, une ecole, une eglise, un parc auto ou une description locale claire pour aider le conducteur a vous retrouver.", landmarkReminderConfirmPickup: "Le depart n'a pas ete associe a un lieu GPS enregistre. Continuez seulement si la ville/zone de depart choisie est correcte et si le texte du depart contient un repere clair, quartier, carrefour, boutique, ecole, eglise, parc auto ou description locale. La zone de depart choisie determine quels conducteurs proches voient cette demande quand le GPS du telephone n'est pas utilise. Choisissez Continuer pour publier, ou Annuler pour corriger le depart.", landmarkReminderConfirmDestination: "La destination n'a pas ete associee a un lieu GPS enregistre. Continuez seulement si le texte de destination contient un repere clair, quartier, carrefour, boutique, ecole, eglise, parc auto ou description locale. Choisissez Continuer pour publier, ou Annuler pour corriger la destination.", landmarkReminderConfirmBoth: "Le depart et la destination n'ont pas ete associes a des lieux GPS enregistres. Continuez seulement si la ville/zone de depart choisie est correcte et si les deux adresses tapees contiennent des reperes clairs, quartiers, carrefours, boutiques, ecoles, eglises, parcs auto ou descriptions locales. La zone de depart choisie determine quels conducteurs proches voient cette demande quand le GPS du telephone n'est pas utilise. Choisissez Continuer pour publier, ou Annuler pour corriger les adresses.", pickupLandmarkRequired: "Entrez l'adresse de depart ou un repere reconnaissable avant publication.", pickupLandmarkMinLength: "Entrez au moins 3 caracteres pour l'adresse ou le repere de depart.", confirmPickupAddressBeforePublish: "Entrez l'adresse de depart ou un repere reconnaissable avant publication.", currentLocationCouldNotConfirmPublish: "La position actuelle exacte n'a pas pu etre confirmee. Entrez l'adresse de depart ou un repere reconnaissable.", completePickupAddressBeforePublish: "Entrez une adresse de depart complete ou un repere reconnaissable avant publication.", currentPickupConfirmedStatus: "Adresse de depart confirmee depuis la position actuelle.", destinationRequiredBeforePublishing: "Entrez une adresse de destination avant publication.", destinationLandmarkRequiredBeforePublishing: "Entrez l'adresse de destination ou un repere reconnaissable avant publication.", selectedPlaceStatus: "Selection: {place}", selectedPickupStatus: "Depart choisi: {place}", selectedDestinationStatus: "Destination choisie: {place}", usingCurrentLocationStatus: "Position actuelle utilisee: {place}.", usingCurrentLocationVerifiedStatus: "Position actuelle utilisee: {place}. Les conducteurs verront le point de depart verifie.", exactGpsNoLandmarkStatus: "Le GPS exact a ete capture. Aucun repere enregistre proche n'a ete trouve; les conducteurs verront le point GPS et votre note de depart.", exactGpsFallbackNoLandmarkStatus: "Depart par GPS exact utilise. Aucun repere enregistre proche n'a ete trouve; les conducteurs verront le point GPS et votre note de depart tapee.", searchingPickupAddresses: "Recherche des adresses de depart...", pickupChooseSuggestion: "Choisissez le depart correspondant dans les suggestions.", pickupNoSuggestion: "Aucun depart camerounais correspondant. Vous pouvez continuer avec l'adresse tapee, ou utiliser le GPS actuel pour un depart exact.", searchingDestinationAddresses: "Recherche des adresses de destination...", destinationChooseSuggestion: "Choisissez la destination correspondante dans les suggestions.", destinationNoSuggestion: "Aucune suggestion de destination; continuez avec l'adresse complete.", pickupSuggestionOrTyped: "Choisissez une suggestion si elle correspond, ou continuez avec l'adresse de depart tapee.", pickupTypedUnlessGps: "Le depart utilisera l'adresse tapee sauf si la position actuelle exacte est cochee.", destinationSuggestionOrTyped: "Choisissez une suggestion si elle correspond, ou continuez avec la destination tapee.", destinationTypedAsEntered: "La destination sera utilisee comme elle est tapee.", signInPassengerPickupSuggestions: "Connectez-vous comme passager pour utiliser les suggestions de depart.", signInPassengerPlaceSuggestions: "Connectez-vous comme passager pour utiliser les suggestions d'adresse.", typePickupSearchMin: "Tapez au moins 3 caracteres pour rechercher des adresses de depart.", typeAddressSearchMin: "Tapez au moins 3 caracteres pour rechercher des adresses.", pickupEntryPrompt: "Entrez une adresse de depart, ou cochez la position actuelle exacte.", destinationEntryPrompt: "Commencez a taper une adresse de destination.", capturingCurrentLocation: "Capture de votre position actuelle. Acceptez la demande de localisation du navigateur si elle apparait.", currentLocationCaptureFailed: "La position actuelle n'a pas pu etre capturee. Dans Chrome, autorisez la localisation pour ce site ou tapez l'adresse de depart.", currentLocationNeedsHttps: "La position actuelle exige une page HTTPS securisee.", browserNoCurrentLocation: "Ce navigateur ne prend pas en charge la position actuelle. Tapez plutot l'adresse de depart.", rideNoAddedStops: "Aucun arret ajoute.", rideAllStopsMarked: "Tous les {count} arrets ajoutes sont marques. Continuez vers la destination.", rideNextStop: "Prochain arret {current} sur {total}: {stop}.", rideRequestReopenedAfterRiderCancel: "Le conducteur a annule. Cette demande est de nouveau ouverte.", rideCancelBeforeChoosingRider: "Vous pouvez annuler avant de choisir un conducteur.", rideStepRiderMarkArrival: "Marquez l'arrivee quand vous etes avec le passager. Le GPS sera enregistre s'il est disponible.", rideStepPassengerRiderOnWay: "Le conducteur arrive. {fee}", rideStepRiderConfirmPickup: "Confirmez la prise en charge pour demarrer la navigation vers la destination.", rideStepPassengerRiderArrived: "Le conducteur est arrive. Rejoignez-le quand vous etes pret. {fee}", rideStepPassengerRiderCompletesDropoff: "{progress} Le conducteur termine a la destination.", rideStepRiderCompleteDestination: "{progress} Terminez a la destination. {fee}", rideAlreadyComplete: "Cette course est deja marquee terminee. {summary}", rideCancelled: "Cette course a ete annulee. {fee}", rideActionsProgress: "Les actions de course changent selon l'etape du trajet.", nextRideQueued: "Prochaine course en attente", nextRequestAvailable: "Prochaine demande disponible", nextRideQueuedDetail: "Cette prise en charge est gardee pour apres le trajet en cours. Terminez la course active avant d'ouvrir la prochaine navigation.", nextRequestAvailableDetail: "Vous pouvez verifier ou proposer car vous etes proche du depot. La navigation du trajet actuel reste active jusqu'a la fin.", destinationNavigation: "Navigation vers destination", pickupNavigation: "Navigation vers prise en charge", readyAtPickup: "Pret au point de prise en charge", fromLiveLocation: "{eta} depuis votre position en direct.", openPickupNavigation: "Ouvrez la navigation vers le point de prise en charge verifie.", pickupNavigationNeedsClarification: "Le GPS du depart n'est pas assez clair pour une navigation fiable. Clarifiez le repere avec le passager par chat si necessaire.", navigateToPickup: "Naviguer vers le depart", riderPickupNavigationClarifyPopup: "Le GPS du depart n'est pas assez clair pour une navigation fiable vers {pickup}. Utilisez Contact/Chat pour demander au passager le repere exact du depart, puis continuez avec l'adresse de depart tapee affichee sur cette course.", riderPickupNavigationClarifyStatus: "Course acceptee. Le GPS du depart n'est pas assez clair pour la navigation. Utilisez Contact/Chat pour demander au passager le repere de depart.", trackRider: "Suivre le conducteur", riderCancelled: "Conducteur annule", rideRequest: "Demande de course", riderApproachWithEta: "{name}: a {eta}. {source}.", selectedRiderApproachPending: "{name} selectionne; approche en attente.", requestOpenAgainAfterRiderCancel: "Votre demande est de nouveau ouverte aux conducteurs proches. Vous n'avez pas besoin de creer une nouvelle demande.", requestOpenForNearbyRiders: "Demande ouverte aux conducteurs proches. Destination: {destination}.", continueToDestination: "Continuer vers la destination", continueToStop: "Continuer vers {stop}", navigateToPlace: "Naviguer vers {place}", pickup: "depart", rideActions: "Actions de course", cancelBeforeStart: "Annuler avant depart", cancelRide: "Annuler la course", endRideEarly: "Terminer la course tot", arrivedAtPickup: "Arrive au depart", pickedUpPassenger: "Passager pris en charge", arrivedAtStop: "Arrive a {stop}", completeRideAtDropoff: "Terminer a la destination", fare: "Prix", fareSummary: "Resume du prix", support: "Assistance", supportOrReportIssue: "Assistance ou signalement", contact: "Contact", contactPassenger: "Contacter le passager", route: "Trajet", acceptOrDeclineRouteChange: "Accepter ou refuser le changement de trajet", navigate: "Naviguer", navigateNextStopOrDestination: "Naviguer vers le prochain arret ou la destination", progress: "Progression", updateRideProgressStatus: "Mettre a jour la prise en charge, l'arret ou la fin", reviewRequestedRouteChange: "Verifier le changement de trajet demande", cancelThisRide: "Annuler cette course", riderRideTools: "Outils de course conducteur", routeChangePending: "Changement de trajet en attente", routeChangePendingDetail: "Acceptez ou refusez le changement de trajet du passager avant d'ouvrir la navigation.", navigation: "Navigation", openNavigationTo: "Ouvrir la navigation vers {place}.", rideProgress: "Progression de la course", nextRideStep: "Prochaine etape", routeUpdate: "Mise a jour du trajet", routeUpdateReviewBeforeChanging: "Verifiez le changement de trajet du passager avant de changer de route.", messagePassengerFromRide: "Envoyez un message au passager dans WakaGood depuis cette course.", contactWakaOrReportRideIssue: "Contactez Waka ou signalez un probleme de course.", finalFareRoute: "Prix final accepte: {fare}. Trajet: {route}.", rideTools: "Outils de course", writeToSelectedRiderOrPassenger: "Ecrire au conducteur ou passager selectionne", chatOpenConfirmPickupPayment: "Le chat est ouvert. Confirmez le point de depart et {payment} avant le depart.", myRideRequests: "Mes demandes de course", yourRequest: "Votre demande", respondToRiderOffer: "Repondre a l'offre du conducteur", chooseRiderOffer: "Choisir une offre conducteur", offersForMyRequest: "Offres pour ma demande", activeRide: "Course active", car: "voiture", vehicle: "vehicule", incomingVehicleRequests: "Demandes {vehicle} entrantes", myVehicleOffers: "Mes offres {vehicle}", liveLocationActive: "Position en direct active", adminWorkspace: "Espace admin", adminMarketplaceSummary: "Voir passagers, conducteurs, approbations, abonnements et activite du marche", adminVisibilityRequired: "Connexion admin requise pour voir les passagers et conducteurs", scheduledRequest: "Demande programmee", incomingRequest: "Demande entrante", vehicleRequestLabel: "Demande {vehicle} {type}", scheduled: "programmee", request: "course", pickupLabelValue: "Depart: {pickup}", statusWithPerson: "{status} avec {name}", routeFromTo: "{pickup} vers {destination}", personOfferedFare: "{name} a propose {fare}", fareScheduleLine: "Prix {fare} - {schedule}", personOfferedFareSchedule: "{name} a propose {fare} - {schedule}", reviewRiderOffersChoose: "Verifiez les offres conducteurs et choisissez quand vous etes pret.", waitingForRiderToAccept: "En attente d'acceptation par un conducteur...", waitingForRiderOffers: "En attente d'offres conducteurs", offerAmount: "Offre {fare}", fareAmount: "Prix {fare}", offerCount: "{count} offre(s)", stopCount: "{count} arret(s)", cancelFeeAmount: "Frais d'annulation {fare}", pickupEtaPending: "Temps d'arrivee au depart en attente", businessRide: "Course entreprise", vehicleDesignation: "Type de vehicule: {vehicle}", marketplaceChooseNearbyRequest: "Marche: choisissez une demande {vehicle} proche a verifier ou refuser", publishOrSelectRideRequest: "Publiez ou choisissez une de vos demandes de course", respondToRiderFareOffer: "Repondre a {name} - offre de {fare}", chooseRiderOfferCount: "Choisir une offre conducteur - {count} offre(s) disponible(s)", waitingForRiderOffersPassengerOffered: "En attente d'offres conducteurs - le passager a propose {fare}", selectedRequestRouteSummary: "{pickup} vers {destination} - {fareMode} - offre {fare} - {schedule}{approach}", changeDestinationOrAddStop: "Changer la destination ou ajouter un arret", rate: "Noter", rating: "Note", paymentFareSummary: "Paiement et resume du prix", contactRider: "Contacter le conducteur", addStopOrChangeDestination: "Ajouter un arret ou changer la destination", paymentSummary: "Resume du paiement", finalFarePaymentDestination: "Prix final accepte: {fare}. Methode de paiement: {method}. Destination: {destination}.", updateRoute: "Mettre a jour le trajet", routeChange: "Changement de trajet", routeChangeFareRecalculation: "Changez la destination ou ajoutez un arret. Waka recalcule le prix ajoute avant l'envoi au conducteur.", messageRiderFromRide: "Envoyez un message au conducteur dans WakaGood depuis cette course.", negotiableFare: "Prix negociable", scheduledAtChip: "Programme: {time}", immediateRide: "Course immediate", reopened: "Rouverte", matched: "Associee", riderArrived: "Conducteur arrive", rideInProgress: "Course en cours", completed: "Terminee", cancelled: "Annulee", unknown: "Inconnu", riderCancelledReposted: "Conducteur annule; republiee aux conducteurs proches", matchedToYou: "Associee a vous", pickupEtaChip: "Arrivee au depart: {eta}", riderPickupEtaChip: "Arrivee du conducteur: {eta}", destinationDriveChip: "Trajet vers destination: {distance}", passengerCancelledRideRemoved: "Le passager a annule cette demande. Elle a ete retiree de votre marche actif.", youCancelledRideRemoved: "Vous avez annule cette course. Elle a ete retiree de votre liste active.", rideCancelledRemoved: "Cette course a ete annulee. Elle a ete retiree du marche actif.", bike: "Moto", xlSpecial: "XL/Special", normalVehicle: "Normal", riderStatusSignInOrApply: "Connectez-vous ou envoyez une demande pour acceder a l'espace conducteur.", riderStatusProfileOnly: "Votre login conducteur est actif, mais Waka n'a pas encore de demande conducteur pour revue admin. Completez seulement les details sur Profil; ne creez pas un second compte.", riderStatusPendingDirect: "Votre demande conducteur attend la revue admin Waka Cameroon. Les demandes, offres et chats s'ouvrent apres les verifications requises des documents et de securite.", riderStatusPendingReviewTesting: "Votre demande conducteur attend la revue admin. Restez sur Eligibilite pour suivre l'avancement et demarrer l'etape Checkr test si elle est active.", riderStatusPendingReview: "Votre demande conducteur attend la revue des documents. Checkr, Stripe, demandes, offres et chat s'ouvrent seulement apres les etapes requises.", riderStatusBackgroundPendingDirect: "Votre demande conducteur a passe la premiere revue admin. Completez toute verification locale de document ou autorisation demandee pour la decision finale.", riderStatusBackgroundPendingProvider: "Votre demande conducteur a passe la premiere revue admin. Completez la verification Checkr depuis Eligibilite pour la decision finale.", riderStatusCorrectionsWithNote: "L'admin a demande des corrections. Mettez a jour le formulaire conducteur et renvoyez-le avant la suite de la revue. Note: {note}", riderStatusCorrectionsNoNote: "L'admin a demande des corrections. Mettez a jour le formulaire conducteur et renvoyez-le avant la suite de la revue.", riderStatusDeclined: "Votre demande conducteur a ete refusee par l'admin. Contactez l'assistance Waka avant d'envoyer de nouveaux documents.", riderStatusApprovalRequired: "L'approbation admin est requise avant l'ouverture de l'espace conducteur.", riderGpsTapActivateAgain: "{summary} Appuyez encore sur Activer pour actualiser les demandes proches depuis votre position actuelle.", riderGpsNeededBeforeRequests: "Le GPS en direct est encore requis avant l'affichage des demandes proches. Appuyez sur Activer quand vous etes pret.", riderApprovedSetupActive: "Approuve. {accessName} est actif jusqu'au {date}. Completez {gaps} avant l'affichage des demandes. {gps}", riderApprovedRenewalDirect: "Approuve. {accessName} est actif jusqu'au {date}. Le renouvellement MTN/Orange s'ouvre 3 jours avant expiration.", riderApprovedRenewalProvider: "Approuve. {accessName} est actif jusqu'au {date}. Les choix de paiement s'ouvrent 3 jours avant expiration.", riderRenewalReminderDirect: "Rechargez le wallet conducteur ou payez l'acces mensuel avant la fin de la periode gratuite.", riderRenewalReminderProvider: "Le renouvellement approche; choisissez un paiement manuel ou automatique pour continuer l'acces payant.", riderCompleteSetupBeforeRequests: "Completez {gaps} avant l'affichage des demandes. {gps}", riderApprovedRemaining: "Approuve. Votre {label} reste {remaining}, jusqu'au {date}.{reminder}{setup}", riderApprovedWalletModel: "Approuve. Le modele camerounais wallet/courses gratuites s'applique apres la periode gratuite; rechargez le wallet ou payez l'acces mensuel depuis Eligibilite.", riderApprovedPaidInactive: "Approuve, mais l'acces conducteur payant est inactif. Choisissez un paiement Waka Rider Access avant de recevoir ou repondre aux demandes.", zeroRides: "0 course", completedRidesToday: "Courses terminees payees ou en attente aujourd'hui", sundayThroughToday: "De dimanche a aujourd'hui", monthToDateRiderEarnings: "Gains conducteur du mois", yearToDateRiderEarnings: "Gains conducteur de l'annee", completedRideCount: "{count} course(s) terminee(s)", completedWakaMiles: "Distance Waka terminee", onlyCompletedWakaRidesCounted: "Seules les courses Waka terminees sont comptees.", completedRidesPayoutsAppearHere: "Les courses terminees et paiements conducteur apparaitront ici apres la fin des trajets.", rideEarningsStatus: "Gains de course - {status}", amountEarned: "{amount} gagne", actualWakaMileage: "Distance Waka reelle: {mileage}", mileagePending: "Distance en attente", nearbyRequestsUseAvailability: "Les demandes proches utilisent votre disponibilite et rayon de prise en charge.", optionalPreferredDestinationsSaved: "Destinations preferees facultatives enregistrees: {regions}.", riderServiceAreaSummary: "Les demandes utilisent votre position en direct quand vous etes en ligne: prises en charge immediates dans environ {immediate} minutes, et courses programmees dans environ {scheduled} minutes dans le marche actif.{destinations} {status}", destinationPreferencesOnDestinationPage: "Les preferences de destination sont sur la page Destination. Les demandes proches utilisent votre position active.", preferredDestinationsAndNearbyPickups: "Destinations preferees facultatives enregistrees: {regions}. Toutes les prises en charge proches dans le rayon sont affichees. {remaining} mise(s) a jour restante(s) aujourd'hui.", showingNearbyPickups: "Toutes les prises en charge proches dans le rayon sont affichees.", onlineActiveRideResumeNearDropoff: "En ligne pour cette course active. Les nouvelles demandes reprennent quand vous etes a environ 7 minutes du depot.{setup}", onlineActiveRidePausedUntilNearDropoff: "En ligne pour cette course active. Les nouvelles demandes immediates sont en pause jusqu'au depart et jusqu'a environ 7 minutes du depot.{setup}", onlineAvailableGpsPrivate: "En ligne et disponible. {gps} est utilise priveement pour afficher seulement les demandes proches.{setup}", locationActive: "Position active", activatedWaitingFreshLocation: "Active. Waka attend une nouvelle position avant d'afficher les demandes proches.{setup}", offlineActivateForNearbyRequests: "Hors ligne. Activez quand vous etes pret a recevoir les demandes proches.{setup}", openCorrectionForm: "Ouvrir le formulaire de correction", openEligibilityChecks: "Ouvrir les controles d'eligibilite", riderNamePlaceholder: "Nom du conducteur", credentialPlaceholder: "CNI, permis ou numero d'autorisation", registrationPlaceholder: "Plaque ou numero d'immatriculation", transactionReferencePlaceholder: "Numero de police", counterFarePlaceholder: "Entrez une contre-offre plus elevee", counterNotePlaceholder: "Facultatif: courte note vehicule ou tarif pour le passager", supabasePasswordPlaceholder: "Mot de passe Supabase", chatPlaceholder: "Le chat s'ouvre apres le choix du conducteur", safetyReportDetailsPlaceholder: "Decrivez le souci pour examen admin", offlineReady: "Pret hors ligne", onlineDemo: "Demo en ligne", localMode: "Mode local", supabaseReady: "Supabase pret", supabaseConfigNeeded: "Configuration Supabase requise", supabaseConnecting: "Connexion Supabase", supabaseSdkUnavailable: "SDK Supabase indisponible", manualPhoneVerified: "Mode pilote manuel: telephone marque verifie. Configurez le SMS OTP avant le lancement public.", smsVerificationRelaxedForTesting: "Mode test: aucun code SMS ne sera envoye. Waka accepte temporairement le telephone enregistre pour les tests; activez le vrai SMS OTP avant le lancement public.", validPhoneRequired: "Entrez un numero de telephone valide avant de demander un code.", validDateOfBirthRequired: "Entrez une date de naissance valide au format AAAA-MM-JJ, ou laissez ce champ vide pour l'instant. Vous pouvez saisir seulement les chiffres et Waka ajoutera les tirets.", checkingPassengerAccount: "Verification des details du compte passager...", checkingRiderApplication: "Verification des details de la demande conducteur...", accountMissingFields: "Completez ces champs avant d'enregistrer: {fields}.", phoneOtpCooldown: "Veuillez attendre {seconds}s avant de demander un autre code telephone.", phoneOtpRateLimited: "Trop de demandes de code telephone. Attendez avant de demander un autre code et verifiez les limites Auth Supabase si cela continue.", sendingVerificationCode: "Envoi du code de verification...", verificationCodeSent: "Code de verification envoye a {phone}.", demoCode: "Code demo: {code} pour {phone}", freshVerificationCodeRequired: "Demandez un nouveau code pour ce numero.", verifyingPhoneNumber: "Verification du telephone...", phoneNumberVerified: "Numero de telephone verifie. Cela verifie seulement le telephone; appuyez sur Enregistrer ou Envoyer pour terminer la creation du compte Waka.", verificationCodeIncorrect: "Le code de verification est incorrect.", phoneOtpManualSignIn: "La connexion OTP telephone est desactivee en mode pilote manuel. Utilisez l'e-mail et le mot de passe.", sendingSignInCode: "Envoi du code de connexion...", signInCodeSent: "Code de connexion envoye a {phone}.", demoSignInCode: "Code de connexion demo: {code} pour {phone}", signingInPassword: "Connexion avec e-mail et mot de passe...", loadingWakaProfile: "Chargement du profil Waka...", supabaseProfileMissing: "Connexion reussie. Ce login a encore besoin d'un profil Waka; completez et enregistrez ce formulaire pour lier le compte.", wrongProfileRole: "Ce compte est enregistre comme {role}, pas {type}.", wrongProfileRoleStrict: "Ce login est enregistre comme {role}, pas {type}. Dans le projet Auth unique de ce deploiement WakaGood, le meme e-mail ne peut pas ouvrir les deux roles en securite. Utilisez maintenant un e-mail/telephone {type} separe, ou terminez la separation Auth passager/conducteur avant de reutiliser cet e-mail pour les deux roles.", adminPublicPortalBlocked: "Les comptes admin ne peuvent pas se connecter aux portails passager ou conducteur. Utilisez l'espace Admin, ou creez un compte passager/conducteur separe pour les tests.", signedInPassengerLoaded: "Connecte comme {email}. Profil passager charge. Ajoutez un compte de paiement avant de demander une course.", signedInRiderLoaded: "Connecte comme {email}. Profil conducteur charge.", signedInAs: "Connecte comme {identity}.", freshSignInCodeRequired: "Demandez un nouveau code de connexion pour ce numero.", signInCodeRequired: "Utilisez l'e-mail et le mot de passe pour vous connecter. L'OTP telephone sert seulement a verifier le telephone la premiere fois.", passwordSignInOnly: "Utilisez l'e-mail et le mot de passe pour vous connecter. L'OTP telephone sert seulement a verifier le telephone la premiere fois.", signInEmailPasswordRequired: "Entrez l'e-mail et le mot de passe de ce compte. La connexion par code telephone est desactivee en mode pilote manuel.", signingIn: "Connexion...", signInCodeIncorrect: "Le code de connexion est incorrect.", localSignInAccountMissing: "Aucun compte {type} enregistre ne correspond a ce telephone. Creez et enregistrez le compte {type}, puis connectez-vous.", signedOut: "Deconnecte.", passengerPhoneBeforeSave: "Verifiez le telephone du passager avant d'enregistrer le compte.", riderPhoneBeforeReview: "Verifiez le telephone du conducteur avant d'envoyer la demande.", passengerPaymentRequired: "Ajoutez une methode de paiement passager sous Paiement avant de publier des courses.", riderPaymentRequired: "Enregistrez un compte de paiement conducteur avant de recevoir des demandes.", riderDailyRegionsRequired: "Les regions de destination preferees sont facultatives pour les conducteurs.", riderLiveGpsRequired: "Partagez le GPS conducteur en direct avant de recevoir des demandes.", startingPassengerSupabase: "Enregistrement passager dans Supabase...", savingPassenger: "Enregistrement du passager...", passengerCreated: "Compte passager {name} cree avec succes. Ajoutez ensuite un compte de paiement avant de demander une course.", passengerCreatedEmailPending: "Compte passager {name} cree avec succes. Ajoutez ensuite un compte de paiement; la connexion e-mail/mot de passe peut necessiter une confirmation ou configuration Supabase.", passengerAccountFailed: "Le compte passager n'a pas ete cree: {message}", passengerSyncing: "Compte passager {name} cree avec succes. Ajoutez ensuite un compte de paiement avant de demander une course.", startingRiderSupabase: "Enregistrement conducteur dans Supabase...", savingRiderApplication: "Enregistrement de la demande conducteur...", submittingRiderApplication: "Envoi de la demande conducteur pour validation admin...", riderCreatedPending: "Compte {name} cree. La demande conducteur attend la validation admin. Si elle est approuvee, choisissez l'acces Waka hebdomadaire ou mensuel avant de recevoir des demandes.", riderAccountFailed: "Le compte conducteur n'a pas ete soumis: {message}", missingRiderDocuments: "Ajoutez ces documents conducteur si l'admin Waka les a demandes: {documents}.", passengerAccountRequired: "Creez un compte passager avant de publier une demande de course.", passengerSignInRequired: "La connexion passager est requise avant de publier des courses.", passengerPhoneRequired: "La verification du telephone passager est requise avant de publier des courses.", realisticFareRequired: "Entrez un prix propose realiste.", fareBelowGuidance: "Ce prix est inferieur a la fourchette suggeree {min}-{max}. Les conducteurs peuvent l'ignorer ou repondre plus lentement. Continuer quand meme?", fareOutsideGuidance: "Cette estimation d'itineraire suggere {min}-{max}. Les prix plus bas peuvent prendre plus de temps a trouver un conducteur.", scheduledTimeRequired: "Choisissez une date et une heure valides pour la course planifiee.", scheduleThirtyMinutes: "Planifiez la course au moins 30 minutes a l'avance.", ridePublishedSupabase: "Demande de course publiee dans Supabase pour les conducteurs eligibles.", ridePublishedLocal: "Demande de course publiee localement.", publishRideFailed: "Impossible de publier cette demande: {message}", preparingPickupCityForPublish: "Preparation de cette demande pour {city} avant publication...", pickupCityCouldNotBePrepared: "Waka n'a pas pu preparer la ville de prise en charge choisie. Choisissez encore la ville et la zone de prise en charge, puis publiez la demande.", selectRideRequestFirst: "Selectionnez d'abord une demande de course.", createRiderFirst: "Creez d'abord un compte conducteur.", riderSignInRequired: "Connexion conducteur requise avant de repondre aux courses.", riderApprovalRequired: "Validation admin requise avant de repondre aux courses.", riderAccessRequired: "Votre modele wallet/courses gratuites ou l'acces mensuel doit etre actif avant de repondre aux courses.", selectNearbyRequest: "Selectionnez une demande proche qui correspond a votre compte conducteur approuve.", requestClosed: "Cette demande n'est plus ouverte.", offerSendFailed: "Impossible d'envoyer cette offre: {message}", passengerOwnRequestRequired: "Seul le passager qui a publie cette demande peut choisir un conducteur.", chooseRiderFailed: "Impossible de choisir ce conducteur: {message}", acceptRouteChangeFailed: "Impossible d'accepter le changement d'itineraire: {message}", acceptRouteChangeSaveFailed: "Impossible d'accepter le changement d'itineraire: Waka n'a pas pu enregistrer l'itineraire mis a jour. Actualisez et reessayez.", declineRouteChangeFailed: "Impossible de refuser le changement d'itineraire: {message}", declineRouteChangeSaveFailed: "Impossible de refuser le changement d'itineraire: Waka n'a pas pu enregistrer votre decision. Actualisez et reessayez.", immediateOfferLocked: "Les nouvelles offres immediates se debloquent quand cette course est a environ 7 minutes de l'arrivee.", riderOfferExpired: "Cette offre conducteur a expire. Gardez la demande ouverte ou proposez un nouveau prix pour que les conducteurs puissent repondre.", reviewRouteChangeBeforeProceeding: "Examinez le changement d'itineraire du passager avant de continuer. Acceptez pour mettre a jour la navigation, ou refusez pour garder l'itineraire actuel.", rejectOfferFailed: "Impossible de refuser cette offre: {message}", suspendRiderConfirm: "Suspendre ce conducteur? Il ne verra plus et n'acceptera plus les demandes immediatement.", clearDemoConfirm: "Effacer toutes les donnees demo stockees localement?", requestConfirmationFailed: "Impossible de demander la confirmation: {message}", confirmScheduledFailed: "Impossible de confirmer cette course planifiee: {message}", reopenScheduledFailed: "Impossible de rouvrir cette course planifiee: {message}", stop: "Arreter", about: "A propos", guide: "Guide pratique", subscriptionReferenceRequired: "Le paiement fournisseur est requis pour le wallet conducteur ou l'acces mensuel.", subscriptionAlreadyPending: "Une session de paiement conducteur est deja en cours.", submittingPaymentSupabase: "Ouverture du paiement conducteur...", savingPaymentReference: "Ouverture du paiement conducteur...", paymentReferenceSubmitted: "Paiement conducteur ouvert. Le wallet ou l'acces mensuel se met a jour apres confirmation fournisseur.", paymentReferenceFailed: "Impossible d'ouvrir le paiement conducteur: {message}", safetyReportUnavailable: "Les signalements sont disponibles apres le choix d'un conducteur.", safetyReportNeedsDetail: "Ajoutez assez de details pour que l'admin comprenne le souci.", safetyReportSignInRequired: "Reconnectez-vous avant d'envoyer un signalement.", submittingSafetySupabase: "Envoi du signalement a Supabase...", savingSafetyReport: "Enregistrement du signalement pour examen admin...", safetyReportSubmitted: "Signalement envoye pour examen admin.", safetyReportFailed: "Impossible d'envoyer le signalement: {message}", androidInstallHelp: "Sur Android, ouvrez ce site dans Chrome, touchez le menu, puis choisissez Ajouter a l'ecran d'accueil ou Installer l'application.", homeHeroKicker: "Accord de transport au Cameroun", homeHeroBody: "Une place de marche du transport pensee pour le Cameroun, pour les courses urbaines et les places interurbaines, avec prix clairs, choix visibles, departs publies par les agences et assistance responsable.", homeHeroPointOne: "Courses urbaines negociees", homeHeroPointTwo: "Places de bus d'agence", homeHeroPointThree: "Especes, MTN MoMo, Orange Money", requestRideCta: "Demander une course", driveWithWakaCta: "Conduire avec Waka Cameroon", homeHeroProofTitle: "Passager. Conducteur. Agence. Une seule place de marche camerounaise.", homeHeroProofBody: "Comparez les offres, confirmez les prix, reservez des places, suivez l'etat du trajet et gardez l'assistance organisee.", homeLeadKicker: "Pourquoi WakaGood", homeLeadTitle: "Des prix clairs, des departs publics et des choix de transport bien organises.", homeLeadBody: "WakaGood aide les visiteurs a demander une course urbaine, comparer les offres des conducteurs, rechercher des departs interurbains, choisir les places disponibles et acceder aux espaces agence approuves sans melanger les roles.", intercityTravel: "Voyage interurbain", transportAgenciesNav: "Agences de transport", browseTransportAgencies: "Parcourir les agences de transport", agencyPortalNav: "Portail agence", serviceChooserKicker: "Choisissez votre service Waka", serviceChooserTitle: "Des options de transport au Cameroun, clairement separees.", serviceChooserBody: "Utilisez WakaGood pour demander une course urbaine, rechercher des departs interurbains ou vous connecter a un espace agence approuve.", servicePassengerKicker: "Courses passager", servicePassengerTitle: "Demandez des courses urbaines avec une offre FCFA claire.", servicePassengerBody: "Saisissez le point de depart, la destination, les bagages et le nombre de passagers avant de choisir une offre.", servicePassengerPointOne: "Reperes et adresses saisies", servicePassengerPointTwo: "Offres de riders a proximite", servicePassengerPointThree: "Cycle du trajet et notes", servicePassengerCta: "Ouvrir les courses passager", serviceIntercityTitle: "Recherchez les agences, comparez les departs et reservez des places.", serviceIntercityBody: "Filtrez par agence, date, ville de depart et ville de destination avant de reserver des places.", serviceIntercityPointOne: "Filtres agence et villes", serviceIntercityPointTwo: "Plans de sieges et points d'embarquement", serviceIntercityPointThree: "Recus par e-mail et mises a jour voyageurs", serviceIntercityCta: "Rechercher des departs interurbains", serviceAgencyKicker: "Espace agence", serviceAgencyTitle: "Acces agence approuve pour les operateurs.", serviceAgencyBody: "Les compagnies de transport font d'abord une demande. Apres approbation admin Waka, elles peuvent publier des departs et gerer les reservations.", serviceAgencyPointOne: "Approbation admin avant publication", serviceAgencyPointTwo: "Departs hebdomadaires et capacite des bus", serviceAgencyPointThree: "Reservations, paiements et messages", serviceAgencyCta: "Connexion agence", agencyPortalKicker: "Portail agence", agencyPortalTitle: "Une page dediee aux operateurs de transport approuves.", agencyPortalBody: "Les agences se connectent sur leur propre page pour publier les voyages, gerer les sieges, suivre les reservations, partager des promotions et garder leur page publique a jour apres approbation Waka.", agencyPortalPointThree: "Page publique, promotions et rapports", openAgencyPortal: "Ouvrir le portail agence", agencyDirectoryKicker: "Agences de transport", agencyDirectoryTitle: "Trouvez les agences approuvees et reservez les departs publies.", agencyDirectoryBody: "Ouvrez une page d'agence pour voir ses departs actuels, les places restantes, le point d'embarquement, le prix et l'option de reservation.", searchAllDepartures: "Rechercher tous les departs", searchAllIntercityDepartures: "Rechercher tous les departs interurbains", loadingApprovedAgencyPages: "Chargement des pages d'agences approuvees.", noPublicAgencyPages: "Aucune page publique d'agence n'est listee pour le moment.", whenAdminPublishesAgencyPage: "Quand l'admin Waka publie une page d'agence, elle apparaitra ici pour les voyageurs.", wakaAdminControlsPublicAgencyPages: "Les pages d'agences approuvees et les departs actifs apparaissent ici.", wakaAdminPublishedPage: "Page publiee par l'admin Waka", openAgencyPage: "Ouvrir la page agence", bookDepartures: "Reserver les departs", agencyDirectoryDefaultDescription: "Page publique d'agence approuvee par Waka avec departs, places et acces reservation.", agencyDirectoryNextDeparture: "Prochain depart", agencyDirectoryPromotions: "Promotions approuvees", publicLearnMoreKicker: "En savoir plus", publicLearnMoreTitle: "Utilisez WakaGood par la bonne entree.", publicLearnMoreBody: "Les visiteurs peuvent rechercher les departs depuis la page des voyages interurbains. Les passagers, conducteurs et agences utilisent leurs propres pages afin que les comptes, trajets et controles operateur restent clairs.", searchIntercityDepartures: "Rechercher des departs interurbains", readHowWakaWorks: "Lire comment Waka Cameroon fonctionne", intercityHeroTitle: "Reservez une place sur les lignes de bus camerounaises avec WakaGood.", intercityHeroBody: "Les agences de transport peuvent publier les departs hebdomadaires, le nombre de places, les gares d'embarquement et les consignes de paiement. Les voyageurs peuvent comparer les compagnies, les villes et les horaires, puis reserver avec ou sans compte Waka personnel.", intercityOperatorSubscriptionMessage: "Les operateurs s'abonnent via WakaGood pour 25 000 FCFA par mois. Les voyageurs peuvent payer par MTN Mobile Money ou Orange Money. Le paiement a l'arrivee n'est qu'une demande de voyage, ne garantit pas une place, et tout voyageur non paye devra prendre toute place restante au depart.", intercityBookNow: "Reserver un voyage", intercityAgencySetup: "Configurer une agence de transport", intercityBrowseDeparturesCta: "Parcourir les departs", intercityOperatorAccessCta: "Acces agence de transport", agencySetupNav: "Configuration agence", transportAgencies: "Agences de transport", whatHappensNext: "Ce qui suit", intercityStepOne: "Etape 1 - choisir un depart", intercityStepTwo: "Etape 2 - reserver les places", selectedTripDetails: "Details du trajet choisi", chooseDepartureSummaryPrompt: "Selectionnez un depart pour verifier l'agence, le trajet, le point d'embarquement, les options de paiement, le prix et les places restantes avant de reserver.", travelerGuideTitle: "Comment les voyageurs reservent", travelerGuideBody: "Choisissez votre ville de depart, votre ville de destination et la date du voyage. Parcourez d'abord les operateurs ci-dessous, comparez le trajet, verifiez le point d'embarquement, le point de descente, les places, le tarif et les options de paiement, puis continuez vers le formulaire de reservation.", openBookingForm: "Parcourir operateurs et departs", agencyGuideTitle: "Comment les agences se configurent", agencyGuideBody: "Les agences utilisent un acces agence Waka separe. Les operateurs existants se connectent pour ouvrir l'espace agence. Les nouveaux operateurs creent d'abord un compte agence, puis attendent l'approbation de l'admin Waka avant de publier des departs, gerer les sieges bloques, consulter les reservations et envoyer des messages aux voyageurs.", passengerIntercityNav: "Voyage interurbain", passengerAgencyTravel: "Voyage interurbain", passengerAgencyTravelTitle: "Rechercher les agences de transport et reserver des departs interurbains", passengerAgencyTravelBody: "Utilisez la meme recherche de voyage interurbain WakaGood ici dans votre compte passager. Comparez les agences, les dates, les details du trajet, les places disponibles et les options de paiement, puis reservez avant de retrouver vos voyages de bus dans Mes trajets.", alsoOnWaka: "Aussi sur Waka", passengerIntercityShortcutTitle: "Reserver un voyage interurbain en agence", passengerIntercityShortcutBody: "Besoin d'un bus ou d'un trajet d'agence au lieu d'une course locale ? Ouvrez Voyage interurbain pour rechercher les agences de transport par date, ville de depart, ville de destination, itineraire, prix par place et places restantes avant de reserver.", openPassengerIntercityTravel: "Ouvrir le voyage interurbain", openAgencySetup: "Continuer vers l'espace agence", agencySignInCta: "Connexion agence", agencyCreateCta: "Creer un compte agence", agencyEntryHelp: "Connexion agence est destinee aux operateurs qui ont deja un acces agence Waka. Creer un compte agence est pour une nouvelle compagnie de transport qui commence sa configuration pour la premiere fois. La creation du compte passager reste separee.", weeklyOperationsTitle: "Operations hebdomadaires de voyage", weeklyOperationsBody: "Les agences peuvent mettre a jour le statut du voyage, confirmer les paiements, annuler des reservations si necessaire et tenir les passagers informes. Les voyageurs peuvent reserver sans compte ou se connecter pour suivre les numeros de siege, les messages et les mises a jour du trajet.", seeDepartures: "Choisir agence et depart", intercitySearchSimpleBody: "Recherchez par agence, date de voyage, ville de depart et ville de destination. Chaque resultat montre le trajet, le point d'embarquement, les places, le prix et les options de paiement avant la reservation.", availableIntercityDeparturesTitle: "Departs interurbains disponibles", departureBrowserHelp: "Utilisez les filtres agence, date, De et Vers pour reduire la liste, puis selectionnez un depart correspondant.", reviewBeforeBookingTitle: "Verifiez le trajet choisi, puis reservez", agencyStepOneLabel: "1. Enregistrer l'agence", agencyStepOneBody: "Ouvrez l'acces Agence, creez ou connectez-vous au compte operateur, puis enregistrez le profil de la compagnie avec telephone support, e-mail, adresse du terminal et villes desservies pour approbation admin.", agencyStepTwoLabel: "2. Publier les departs", agencyStepTwoBody: "Apres approbation admin, utilisez Departs hebdomadaires pour saisir la ville d'origine, la ville de destination, le point d'embarquement, l'heure du voyage, l'identifiant du bus, le nombre de places et le prix par place.", agencyStepThreeLabel: "3. Mettre a jour la disponibilite", agencyStepThreeBody: "Bloquez des sieges, mettez un depart en pause ou changez le statut du voyage en embarquement, parti, arrive, retarde ou annule.", agencyStepFourLabel: "4. Gerer les reservations", agencyStepFourBody: "Confirmez les paiements, annulez des passagers si necessaire, marquez l'enregistrement ou la fin, et envoyez des messages aux voyageurs par application ou e-mail.", anyTransportAgency: "Toute agence de transport", anyDepartureCity: "Toute ville de depart", anyDestinationCity: "Toute ville d'arrivee", fromLabel: "De", toLabel: "Vers", cameroonCitiesDirectoryTitle: "Villes du Cameroun sur Waka", transportAgenciesDirectoryTitle: "Agences de transport sur Waka", travelDate: "Date du voyage", findDepartures: "Chercher les departs", loadingAgencyDepartures: "Chargement des departs verifies des agences.", loadingTransportAgencies: "Chargement des agences de transport...", refreshingAgencyDepartures: "Actualisation des departs approuves des agences...", upcomingDepartures: "Departs a venir", departureListSummary: "Compagnies, horaires, places et lignes", bookByEmail: "Reservation par e-mail", reserveIntercitySeat: "Reservez votre place interurbaine", chooseDepartureToContinue: "Choisissez d'abord un depart.", chooseDepartureToUnlockBooking: "Choisissez un depart dans la liste. Les details voyageurs, le choix des sieges et le paiement s'ouvrent ici apres la selection.", chooseSeatsBeforeBooking: "Choisissez les places disponibles", selectSeatsFromMap: "Selectionnez les numeros de place sur le plan avant de reserver.", selectedSeatsLabel: "Places choisies", amountToPay: "Montant a payer", chooseDepartureShort: "Choisir un depart", noneSelected: "Aucune", zeroXaf: "0 FCFA", travelerName: "Nom du voyageur", emailReceipt: "Recu par e-mail", shortPhone: "Telephone", seats: "Places", seatsLeftLabel: "Places restantes", boardingLocationLabel: "Point d'embarquement", dropoffLocationLabel: "Point de descente", paymentOption: "Option de paiement", payOnArrival: "Payer a l'arrivee (place non garantie)", payOnArrivalClosedOption: "Payer a l'arrivee (ferme a moins d'une heure)", bookingNote: "Note de reservation", bookDeparture: "Reserver le depart", bookingReceiptMessage: "Le recu de reservation sera envoye par e-mail. Le paiement a l'arrivee n'est disponible que jusqu'a une heure avant le depart, ne garantit pas une place, et le voyageur non paye devra prendre toute place restante au depart.", payOnArrivalSelectionWarning: "Le paiement a l'arrivee ne garantit pas une place. Sans paiement immediat, vous devrez prendre toute place restante quand vous arriverez pour le depart.", payOnArrivalClosedWarning: "Le paiement a l'arrivee ferme une heure avant le depart. Utilisez le mobile money pour confirmer votre reservation maintenant.", payOnArrivalBookingSavedWarning: "Le paiement a l'arrivee a ete choisi. Les places choisies sont reservees pour cette reservation; arrivez tot et suivez les instructions de paiement de l'agence.", intercityOperators: "Operateurs interurbains", registerTransportAgency: "Enregistrer une agence de transport", operatorProfileTitle: "Profil agence et page publique", operatorAgencyStatusDefault: "Gerez l'identite de l'agence, le logo, les contacts support, l'adresse du terminal, les numeros de paiement et les details de la page publique.", operatorAccessMessage: "Connectez-vous comme passager puis enregistrez votre compagnie et vos departs hebdomadaires de bus pour les reservations interurbaines WakaGood.", operatorSetupKicker: "Operateurs interurbains", agencyProfileHelp: "Enregistrez d'abord le profil de l'agence. Waka utilise cette identite de compagnie, le contact support, l'adresse du terminal et les villes desservies sur le site public de voyage.", operatorStepTwoKicker: "Etape 2 - garder le service actif", operatorPaymentHelp: "Les agences commencent avec un mois gratuit. Soumettez ici le dossier de frais mensuels pour que Waka garde les futurs departs visibles apres la periode d'essai.", operatorStepThreeKicker: "Etape 3 - publier les departs", operatorDepartureHelp: "Les passagers ne voient les departs qu'apres l'approbation de l'agence par l'admin Waka et la publication de lignes actives avec ville d'origine, ville de destination, heure de depart, point d'embarquement, nombre de places et options de paiement.", operatorStepFourKicker: "Etape 4 - gerer les voyageurs", agencySetupChecklistTitle: "Checklist de configuration agence", agencySetupChecklistBody: "Cet espace permet aux agences de creer leur profil public, attendre l'approbation admin, publier les departs, garder le service actif et gerer les reservations voyageurs.", agencyProfileStatusLabel: "Profil agence", serviceVisibilityLabel: "Visibilite publique", publishedDeparturesLabel: "Departs publies", travelerBookingsLabel: "Reservations voyageurs", latestOperatorPaymentLabel: "Dernier paiement operateur", seatPublishingRuleLabel: "Vue passager", seatPublishingRuleBody: "Les passagers ne voient que les departs actifs avec itineraire, horaire, point d'embarquement, prix et places restantes.", operatorNextActionStart: "Commencez par enregistrer le profil de la compagnie avec contacts support, villes desservies et adresse du terminal.", operatorNextActionPublish: "Ensuite, ouvrez Departs hebdomadaires et publiez au moins une ligne avec ville de destination, heure de depart, places et options de paiement.", operatorNextActionPayment: "Le mois gratuit permet de publier maintenant. Soumettez le dossier de frais mensuels avant la fin de l'essai pour garder les futurs departs visibles.", operatorNextActionManage: "Gardez les departs a jour, bloquez des sieges si necessaire et gerez les reservations voyageurs ci-dessous.", operatorConsoleKicker: "Centre de controle agence", operatorConsoleTitle: "Gerez votre agence plus rapidement", operatorConsoleBody: "Les admins d'agence controlent leur profil, les departs, les places bloquees, les reservations, les paiements, la publicite et les messages voyageurs. L'admin Waka peut aider et mettre a jour les dossiers sur demande.", operatorTabProfile: "Profil", operatorTabDepartures: "Departs", operatorTabBookings: "Reservations", operatorTabReports: "Rapports", operatorTabPayments: "Paiements", operatorTabAdvertising: "Publicite", operatorTabMessages: "Messages", operatorAgencyFilter: "Agence", operatorStatusFilter: "Filtre", operatorSearchLabel: "Recherche", operatorSearchPlaceholder: "Rechercher trajet, voyageur, ville ou reference", operatorReportsKicker: "Rapports de voyage", operatorReportsTitle: "Vue transactions avant depart", operatorReportsBody: "Verifiez chaque depart avant l'embarquement: montants payes et en attente, contacts passagers, places choisies, references de reservation et modes de paiement.", adminIntercityTripReportsTitle: "Rapports de voyage pour l'admin Waka", adminIntercityTripReportsBody: "Chaque rapport de depart affiche l'agence, la date, l'heure, De, A, les sieges, les totaux, les transactions payees et en attente, et le manifeste passagers avant le depart.", adminIntercityReportsPageHeading: "Rapports de voyage des agences et manifestes passagers", adminIntercityReportsPageBody: "Consultez les departs, les totaux des tarifs, les reservations payees et en attente, les sieges et les manifestes passagers sans encombrer la page des operations agence.", adminIntercityReportsPageStatus: "Choisissez une agence dans le filtre ou ouvrez les rapports depuis une fiche agence.", operatorFilterAllAgencies: "Toutes les agences", operatorFilterAllItems: "Tous les elements", operatorFilterActive: "Actif", operatorFilterTrial: "Essai", operatorFilterPaymentDue: "Paiement du", operatorFilterPendingReview: "En attente d'examen", operatorFilterSuspended: "Suspendu", operatorFilterPaused: "En pause", operatorFilterScheduled: "Planifie", operatorFilterBoarding: "Embarquement", operatorFilterDeparted: "Parti", operatorFilterArrived: "Arrive", operatorFilterCompleted: "Termine", operatorFilterDelayed: "Retarde", operatorFilterCancelled: "Annule", operatorFilterUpcoming: "A venir", operatorFilterToday: "Aujourd'hui", operatorFilterPendingPayment: "Paiement en attente", operatorFilterPaid: "Paye", operatorFilterReserved: "Reserve", operatorFilterConfirmed: "Confirme", operatorFilterCheckedIn: "Enregistre", operatorFilterFromPassenger: "Du passager", operatorFilterFromOperator: "De l'agence", operatorFilterFromAdmin: "De l'admin Waka", operatorFilterRecent: "Dernieres 24 h", operatorFilterApproved: "Approuve", operatorFilterRejected: "Rejete", operatorVisibleCountLabel: "Affiches", operatorNextActionLabel: "Action suivante", operatorMessagesKicker: "Messages", operatorMessagesTitle: "Messages voyageurs recents", operatorMessagesBody: "Les mises a jour passager et agence apparaissent ici pour repondre plus vite, garder le contexte et resoudre les problemes de voyage sans quitter l'espace operateur.", agencySignInKicker: "Acces agence", agencySignInTitle: "Connexion agence", agencySignInBody: "Les comptes agence sont separes des comptes passager. Faites d'abord une demande, attendez l'approbation admin Waka, puis connectez-vous pour gerer departs et reservations.", agencyCreateTitle: "Creer un compte agence", agencyCreatePrompt: "Nouvelle compagnie de transport sur Waka Cameroon ?", guestBookingAllowed: "Reservation invite autorisee", guestBookingDisabled: "Reservation invite desactivee", guestBookingLabel: "Reservation invite", travelStatusLabel: "Statut du voyage", agencyTerminalLabel: "Terminal agence", reviewAgencyDepartureCta: "Voir l'agence et le depart", companyName: "Nom de la compagnie", companySlug: "Identifiant web de la compagnie", supportEmail: "E-mail de support", supportPhone: "Telephone de support", headOfficeCity: "Ville du siege", mainTerminalAddress: "Adresse principale du terminal", operatedCities: "Villes desservies", agencyDescription: "Description de l'agence", mtnMomoAccountName: "Nom du compte MTN MoMo", mtnMomoNumber: "Numero MTN MoMo", orangeMoneyAccountName: "Nom du compte Orange Money", orangeMoneyNumber: "Numero Orange Money", saveAgencyProfile: "Enregistrer le profil de l'agence", monthlyPlatformFee: "Frais mensuels de plateforme", submitOperatorSubscription: "Soumettre l'abonnement operateur de 25 000 FCFA", notAvailable: "Non disponible", suspended: "Suspendu", pendingReview: "En attente d'examen", agency: "Agence", route: "Trajet", origin: "Ville de depart", chooseAgency: "Choisir l'agence", billingMonth: "Mois de facturation", paymentMethod: "Mode de paiement", cash: "Especes", payerName: "Nom du payeur", payerPhone: "Telephone du payeur", shortReference: "Reference", submitMonthlyFeeRecord: "Soumettre le paiement mensuel", operatorFeeReviewMessage: "WakaGood verifie les soumissions d'abonnement operateur avant de garder les departs actifs.", weeklyDepartures: "Departs hebdomadaires", publishBusesTitle: "Publier bus, places, villes et horaires", departureDateTime: "Date et heure du depart", originCity: "Ville de depart", destinationCity: "Ville d'arrivee", boardingLocation: "Lieu d'embarquement", dropoffLocation: "Lieu de descente", estimatedDurationMinutes: "Duree estimee (minutes)", farePerSeat: "Prix par place (FCFA)", departureNote: "Note de depart", acceptMtnMomo: "Accepter MTN MoMo", acceptOrangeMoney: "Accepter Orange Money", allowPayOnArrival: "Autoriser le paiement a l'arrivee", saveDeparture: "Enregistrer le depart", departureWebsiteMessage: "Les passagers verront les departs actifs, les places disponibles et les options de paiement sur le site.", travelerBookings: "Reservations voyageurs", recentReceiptsReservations: "Recus et reservations recents", myBusBookings: "Mes reservations de bus", travelerBookingMessageCenter: "Les reservations du voyageur connecte, les numeros de siege, les mises a jour du trajet et les messages de l'operateur apparaissent ici.", additionalCities: "Villes supplementaires", busVehicleLabel: "Bus / identifiant du vehicule", travelStatusLabel: "Statut du voyage", scheduled: "Programme", boarding: "Embarquement", departed: "Parti", arrived: "Arrive", completed: "Termine", delayed: "Retarde", cancelled: "Annule", statusNoteLabel: "Note de statut", advertisingTitle: "Publicite", paidPromotionRequests: "Demandes de promotion payante", agencyPromotionsKicker: "Promotions des agences", agencyPromotionsTitle: "Offres de voyage et mises a jour des agences approuvees.", agencyPromotionsBody: "Consultez les annonces, promotions de lignes, photos et mises a jour voyage approuvees par Waka avant de choisir votre depart.", campaignName: "Nom de la campagne", campaignNamePlaceholder: "Promotion vacances ligne Douala", publicPromotionTitle: "Titre public de la promotion", publicPromotionTitlePlaceholder: "Places du week-end vers Douala disponibles", publicPromotionBody: "Details publics de la promotion", publicPromotionBodyPlaceholder: "Decrivez la ligne, l'offre, le confort du bus, le changement d'horaire ou l'avis passager a afficher publiquement.", placementLabel: "Emplacement", websiteBanner: "Banniere du site", featuredRoute: "Ligne mise en avant", homepageSpotlight: "Mise en avant sur l'accueil", pushNotice: "Notification push", durationDays: "Duree (jours)", promotionStart: "Debut d'affichage", promotionEnd: "Fin d'affichage", promotionCtaLabel: "Libelle du bouton", promotionCtaLabelPlaceholder: "Reserver cette ligne", promotionCtaUrl: "Lien optionnel", promotionCtaUrlPlaceholder: "https://agence.example.com/offre", promotionImage: "Image de promotion", promotionImageAlt: "Description de l'image", promotionImageAltPlaceholder: "Image du bus, terminal, ligne ou offre", promotionPublicActive: "Afficher publiquement apres approbation Waka", viewAgencyDepartures: "Voir les departs", startsLabel: "Debut", endsLabel: "Fin", amountXafLabel: "Montant (FCFA)", paymentReference: "Reference de paiement", advertisingPaymentReferencePlaceholder: "Reference du paiement publicitaire", campaignNote: "Note de campagne", campaignNotePlaceholder: "Ligne cible, audience ou note de calendrier", payerNamePlaceholder: "Responsable financier", submitAdvertisingRequest: "Soumettre la demande publicitaire", advertisingSeparateFeeMessage: "La publicite est facturee separement des frais mensuels de l'operateur et est examinee par WakaGood.", bookingActionGuide: "Utilisez les actions de reservation pour confirmer le paiement, annuler des places, envoyer des mises a jour au voyageur ou marquer l'enregistrement et la fin du voyage.", intercityAdminLabel: "Agences", intercityAdminTitle: "Agences, pages publiques et operations de voyage", intercityAdminStatus: "Les essais d'agence, departs, reservations, blocages de sieges, paiements, publicites et messages voyageurs apparaissent ici.", pendingAgencyApprovalsTitle: "Approbations d'agences en attente", pendingAgencyApprovalsBody: "Les nouvelles demandes d'agence de transport apparaissent d'abord ici pour l'approbation de l'admin Waka. Approuvez une agence ici avant qu'elle puisse se connecter, publier des voyages, recevoir des reservations ou apparaitre publiquement.", agencyApprovalsAdminLabel: "Approbations d'agences", agencyApprovalsAdminStatus: "Examinez les nouvelles demandes d'agence de transport avant qu'elles puissent publier des voyages, recevoir des reservations ou apparaitre publiquement.", agencyApprovalsPendingTitle: "Demandes d'agence en attente", agencyApprovalsPendingBody: "Approuvez les agences qui respectent les exigences Waka, refusez les demandes qui ne doivent pas avancer ou envoyez un message aux operateurs pour demander des corrections avant de decider.", intercityRecordCountZero: "0 enregistrements interurbains", cityBamenda: "Bamenda", cityBafoussam: "Bafoussam", cityBuea: "Buea", cityDouala: "Douala", cityYaounde: "Yaounde", cityLimbe: "Limbe", cityKumba: "Kumba", cityMamfe: "Mamfe", cityKribi: "Kribi", cityMbouda: "Mbouda" }, ar: { tagline: "رحلات دراجة وسيارة قابلة للتفاوض", passenger: "راكب", rider: "سائق", admin: "مشرف", language: "اللغة", installApp: "تثبيت التطبيق", createPassenger: "إنشاء حساب راكب", savePassenger: "حفظ الراكب", postRide: "طلب رحلة", publishRequest: "نشر الطلب", riderApplication: "طلب السائق", submitReview: "إرسال للمراجعة", subscription: "اشتراك", paySubscription: "Open rider access checkout", respondRequest: "الرد على الطلب المحدد", sendOffer: "إرسال قبول أو عرض مقابل", passengerSignIn: "تسجيل دخول الراكب", riderSignIn: "تسجيل دخول السائق", signIn: "تسجيل الدخول", pageTitle: "رحلات Waka التفاوضية", installed: "مثبت", passengerPanelSubtitle: "اطلب رحلة واختر أفضل عرض", riderPanelSubtitle: "قدّم طلبك واشترك ثم تفاوض على الرحلات", email: "البريد الإلكتروني", password: "كلمة المرور", phoneNumber: "رقم الهاتف", otpCode: "رمز التحقق", sendOtp: "إرسال الرمز", sendCode: "إرسال الرمز", verify: "تحقق", signOut: "تسجيل الخروج", fullName: "الاسم الكامل", profilePicture: "صورة الملف الشخصي", phoneVerificationCode: "رمز تحقق الهاتف", nationalIdNumber: "رقم الهوية الوطنية", dateOfBirth: "تاريخ الميلاد", country: "الدولة", city: "المدينة", passengerSignInHelp: "سجل الدخول قبل طلب الرحلات.", riderSignInHelp: "سجل الدخول قبل الرد على الرحلات.", passengerWorkspace: "مساحة الراكب", riderWorkspace: "مساحة السائق", passengerSignedIn: "تم تسجيل دخول الراكب", riderSignedIn: "تم تسجيل دخول السائق", readyToRequestRides: "جاهز لطلب الرحلات.", applicationStatusWillAppear: "ستظهر حالة الطلب هنا.", noPassengerSaved: "لم يتم حفظ أي راكب بعد.", noRiderApplication: "لم يتم حفظ أي طلب سائق بعد.", pickupArea: "منطقة الالتقاء", pickupDescription: "وصف مكان الالتقاء", destination: "الوجهة", rideTiming: "وقت الرحلة", asSoonAsPossible: "في أقرب وقت ممكن", scheduleAhead: "الحجز مسبقاً", scheduledDateTime: "تاريخ ووقت الرحلة المجدولة", vehicle: "المركبة", vehicleType: "تصنيف المركبة", bike: "سيارة", car: "سيارة", bikeOrCar: "سيارة", fareOffer: "عرض الأجرة", paymentPreference: "طريقة الدفع المفضلة", cashInHand: "نقداً", mtnMoney: "MTN Mobile Money", orangeMoney: "Orange Money", agreeWithRider: "اتفق مع السائق قبل الرحلة", optional: "اختياري", record: "تسجيل", clear: "مسح", riderAccess: "وصول السائق", applicationStatus: "حالة الطلب", riderPlatformStatus: "ستظهر حالة منصة السائق هنا.", operatingArea: "منطقة العمل", credentialNumber: "رقم رخصة القيادة", vehicleRegistration: "رقم اللوحة", driverLicenseDocument: "وثيقة رخصة القيادة", vehicleRegistrationDocument: "وثيقة التسجيل", nationalIdDocument: "وثيقة التأمين", subscriptionIntro: "يحصل السائقون المعتمدون على 30 يوماً مجاناً قبل فرض رسوم شهرية للمنصة.", paymentProvider: "مزود التأمين", paymentPhone: "هاتف الدفع", transactionReference: "رقم وثيقة التأمين", subscriptionPaymentHelp: "يتحقق المشرف من مدفوعات اشتراك السائق قبل تمديد الوصول.", yourFare: "أجرتك", messageBeforeSelection: "ملاحظة للراكب قبل الاختيار", openRequests: "الطلبات المفتوحة", passengers: "الركاب", riders: "السائقون", pendingRiders: "سائقون بانتظار الاعتماد", subscribed: "مشتركون", loadDemoMarket: "تحميل سوق تجريبي", clearDemoData: "مسح بيانات التجربة المحلية", selectOrPublish: "اختر أو انشر طلباً", refreshMarket: "تحديث السوق", all: "الكل", rideRequests: "طلبات الرحلات", riderOffers: "عروض السائقين", accountDetail: "تفاصيل الحساب", postSelectionChat: "الدردشة بعد الاختيار", locked: "مقفل", send: "إرسال", chooseRider: "اختيار سائق", openFullReview: "فتح المراجعة الكاملة", approve: "اعتماد", decline: "رفض" }, pcm: { passengerPanelSubtitle: "Ask for ride and choose di best offer", paySubscription: "Open rider access checkout", riderPanelSubtitle: "Apply, pay subscription, then talk price", phoneNumber: "Phone number", sendCode: "Send code", verify: "Verify", signOut: "Sign out", fullName: "Full name", profilePicture: "Profile picture", nationalIdNumber: "National ID number", dateOfBirth: "Date of birth", passengerSignInHelp: "Sign in before you request ride.", riderSignInHelp: "Sign in before you answer ride.", pickupArea: "Place for pickup", pickupDescription: "Describe where you dey", rideTiming: "Ride time", asSoonAsPossible: "Now now", scheduleAhead: "Book ahead", fareOffer: "Money you offer", paymentPreference: "How you go pay", cashInHand: "Cash for hand", agreeWithRider: "Agree with rider before ride", optional: "If you want", vehicleType: "Vehicle designation", operatingArea: "Area wey you dey work", subscriptionIntro: "Approved riders get a 30-day free period after admin approval before Waka wallet, MTN MoMo, or Orange Money commission review is required.", paymentProvider: "Insurance provider", paymentPhone: "Payment phone", transactionReference: "Insurance policy number", refreshMarket: "Refresh market", rideRequests: "Ride requests", riderOffers: "Rider offers", accountDetail: "Account details", send: "Send" }, sw: { passengerPanelSubtitle: "Omba safari na chagua ofa bora", paySubscription: "Fungua malipo ya usajili kiotomatiki", riderPanelSubtitle: "Tuma ombi, lipa usajili, kisha jadili safari", email: "Barua pepe", password: "Nenosiri", phoneNumber: "Namba ya simu", sendCode: "Tuma msimbo", verify: "Thibitisha", signOut: "Toka", fullName: "Jina kamili", profilePicture: "Picha ya wasifu", nationalIdNumber: "Namba ya kitambulisho", dateOfBirth: "Tarehe ya kuzaliwa", country: "Nchi", city: "Mji", pickupArea: "Eneo la kuchukuliwa", pickupDescription: "Maelezo ya mahali", destination: "Unakoenda", rideTiming: "Muda wa safari", asSoonAsPossible: "Haraka iwezekanavyo", scheduleAhead: "Panga baadaye", vehicle: "Chombo", bike: "Gari", car: "Gari", fareOffer: "Nauli unayotoa", paymentPreference: "Njia ya malipo", cashInHand: "Pesa taslimu", optional: "Si lazima", record: "Rekodi", clear: "Futa", operatingArea: "Eneo la kazi", paymentProvider: "Kampuni ya bima", paymentPhone: "Simu ya malipo", transactionReference: "Namba ya bima", passengers: "Abiria", riders: "Madereva", refreshMarket: "Sasisha soko", rideRequests: "Maombi ya safari", riderOffers: "Ofa za madereva", accountDetail: "Maelezo ya akaunti", send: "Tuma" }, pt: { passengerPanelSubtitle: "Pedir viagem e escolher a melhor oferta", paySubscription: "Abrir pagamento automatico da subscricao", riderPanelSubtitle: "Candidatar, subscrever e negociar viagens", email: "Email", password: "Palavra-passe", phoneNumber: "Numero de telefone", sendCode: "Enviar codigo", verify: "Verificar", signOut: "Sair", fullName: "Nome completo", profilePicture: "Foto de perfil", nationalIdNumber: "Numero de identificacao nacional", dateOfBirth: "Data de nascimento", country: "Pais", city: "Cidade", pickupArea: "Zona de recolha", pickupDescription: "Descricao do local", destination: "Destino", rideTiming: "Hora da viagem", asSoonAsPossible: "O mais cedo possivel", scheduleAhead: "Agendar", vehicle: "Veiculo", bike: "Carro", car: "Carro", fareOffer: "Oferta de tarifa", paymentPreference: "Preferencia de pagamento", cashInHand: "Dinheiro em mao", optional: "Opcional", record: "Gravar", clear: "Limpar", operatingArea: "Area de operacao", subscriptionIntro: "Motoristas aprovados recebem 30 dias gratis antes da taxa mensal.", paymentProvider: "Seguradora", paymentPhone: "Telefone de pagamento", transactionReference: "Numero da apolice", passengers: "Passageiros", riders: "Motoristas", refreshMarket: "Atualizar mercado", rideRequests: "Pedidos de viagem", riderOffers: "Ofertas de motoristas", accountDetail: "Detalhe da conta", send: "Enviar" }, es: { passengerPanelSubtitle: "Solicita un viaje y elige la mejor oferta", riderPanelSubtitle: "Aplica, suscribete y negocia viajes", createAccount: "Crear cuenta", email: "Correo", password: "Contrasena", phoneNumber: "Numero de telefono", sendCode: "Enviar codigo", verify: "Verificar", signOut: "Salir", fullName: "Nombre completo", profilePicture: "Foto de perfil", nationalIdNumber: "Referencia de identidad", identityReference: "Referencia de identidad", driverLicenseNumber: "Numero de licencia", dateOfBirth: "Fecha de nacimiento", country: "Pais", city: "Ciudad", pickupArea: "Zona de recogida", pickupDescription: "Descripcion de recogida", destination: "Destino", rideTiming: "Horario del viaje", asSoonAsPossible: "Lo antes posible", scheduleAhead: "Programar", vehicle: "Vehiculo", car: "Auto", fareOffer: "Oferta de tarifa", paymentPreference: "Preferencia de pago", operatingArea: "Zona de operacion", paymentProvider: "Aseguradora", passengers: "Pasajeros", riders: "Conductores", refreshMarket: "Actualizar mercado", rideRequests: "Solicitudes de viaje", riderOffers: "Ofertas de conductores", accountDetail: "Detalle de cuenta", send: "Enviar" } }; Object.assign(translationAdditions.fr, { logoPreview: "Logo", companyLogo: "Logo de l'entreprise", logoDescription: "Description du logo", agencyLogoVisibilityHelp: "Facultatif. Le logo apparait sur les cartes publiques, les tableaux de bord agence, les recus de reservation, les messages et les rapports.", heldUnavailableSeats: "Places bloquees / indisponibles", adminAgencyPublishMessageTitle: "Publier ou envoyer un message pour une agence", adminAgencyPublishMessageBody: "L'admin Waka peut publier des departs actifs pour les agences approuvees et envoyer des messages limites a l'agence sans exposer les reservations d'une autre agence.", publishForAgency: "Publier pour l'agence", adminIntercityPublishStatusHelp: "Choisissez une agence active pour publier un depart public reservable.", message: "Message", sendAgencyMessage: "Envoyer un message a l'agence", agencyMessagesScopedHelp: "Les messages d'agence restent limites au tableau de bord de l'agence selectionnee.", travelerNameAsNationalId: "Nom comme sur la CNI", nationalIdentityNumberOptional: "Numero CNI (facultatif)", nationalIdentityInspectionHelp: "Saisissez le nom du voyageur exactement comme sur la carte nationale d'identite. Le document physique doit etre presente au personnel de l'agence avant le voyage.", passengerManifestTitle: "Details des passagers pour les places choisies", passengerManifestHelp: "Pour les reservations de groupe, saisissez le nom et la date de naissance de chaque voyageur. Le numero CNI est facultatif, mais la piece physique doit etre inspectee avant l'embarquement.", identityDocumentNumberOptional: "Numero du document d'identite (facultatif)", optionalIdentityPlaceholder: "CNI, permis ou passeport facultatif", dateOfBirthOptional: "Date de naissance (facultatif)", passengerIdentityOptionalHelp: "Les informations d'identite sont facultatives lors de la creation du compte passager. Vous pouvez les ajouter plus tard; les reservations de voyage demanderont toujours les details du voyageur avant de reserver les places.", passengerEmailConfirmationPopup: "Bonne nouvelle - les informations obligatoires du compte passager ont ete acceptees.\n\nWaka Cameroon a envoye un lien de confirmation a {email}. Ouvrez votre e-mail et cliquez sur ce lien pour terminer la creation du compte. Si vous ne le voyez pas, verifiez Spam ou Courrier indesirable. Verifiez aussi que l'adresse e-mail saisie est correcte. Apres confirmation, revenez ici et connectez-vous avec le meme e-mail et le meme mot de passe. Ne rappuyez pas sur Enregistrer le compte pour ce meme e-mail.", agencyEmailConfirmationPopup: "Bonne nouvelle - les informations obligatoires de l'acces agence ont ete acceptees.\n\nWaka Cameroon a envoye un lien de confirmation a {email}. Ouvrez votre e-mail et cliquez sur ce lien pour terminer la creation du compte. Si vous ne le voyez pas, verifiez Spam ou Courrier indesirable. Verifiez aussi que l'adresse e-mail saisie est correcte. Apres confirmation, revenez ici et connectez-vous avec le meme e-mail et le meme mot de passe. Ne rappuyez pas sur Enregistrer le compte pour ce meme e-mail.", riderEmailConfirmationPopup: "Bonne nouvelle - votre compte conducteur Waka Cameroon et votre demande ont ete recus.\n\nWaka Cameroon a envoye un lien de confirmation a {email}. Ouvrez votre e-mail et cliquez sur ce lien pour terminer l'acces au compte. Si vous ne le voyez pas, verifiez Spam ou Courrier indesirable. Verifiez aussi que l'adresse e-mail saisie est correcte. Apres confirmation, revenez ici et connectez-vous avec le meme e-mail et le meme mot de passe. Votre demande attendra la validation admin Waka.", riderApplicationSubmittedPopup: "{name} a ete envoye pour validation admin Waka.\n\nVous n'avez pas besoin de remplir ce formulaire a nouveau. L'admin Waka verifiera le compte, l'identite, la voiture ou moto et les documents televerses. Si autre chose est necessaire, Waka demandera des corrections dans votre espace conducteur." }); translations.en = { ...translations.en, ...translationAdditions.en }; Object.entries(translationAdditions).forEach(([language, entries]) => { if (language !== "en") translations[language] = { ...translations.en, ...(translations[language] ?? {}), ...entries }; }); const textTranslationKeys = { "Passenger": "passenger", "Rider": "rider", "Admin": "admin", "Request a ride and choose the best offer": "passengerPanelSubtitle", "Apply, subscribe, then negotiate rides": "riderPanelSubtitle", "Email": "email", "Password": "password", "Phone number": "phoneNumber", "OTP code": "otpCode", "Send OTP": "sendOtp", "Send code": "sendCode", "Verify": "verify", "Sign in": "signIn", "Sign out": "signOut", "Full name": "fullName", "Profile picture": "profilePicture", "Phone verification code": "phoneVerificationCode", "National ID number": "identityReference", "Identity reference": "identityReference", "Identity document number": "identityReference", "Identity document number (optional)": "identityDocumentNumberOptional", "Driver's license number": "driverLicenseNumber", "Date of birth": "dateOfBirth", "Date of birth (optional)": "dateOfBirthOptional", "Country": "country", "City": "city", "Use email and password to sign in before requesting rides.": "passengerSignInHelp", "Use email and password to sign in before responding to rides.": "riderSignInHelp", "Passenger workspace": "passengerWorkspace", "Rider workspace": "riderWorkspace", "Passenger signed in": "passengerSignedIn", "Rider signed in": "riderSignedIn", "Ready to request rides.": "readyToRequestRides", "Application status will appear here.": "applicationStatusWillAppear", "No passenger saved yet.": "noPassengerSaved", "No rider application saved yet.": "noRiderApplication", "Pickup area": "pickupArea", "Pickup description": "pickupDescription", "Destination": "destination", "Ride timing": "rideTiming", "As soon as possible": "asSoonAsPossible", "Schedule ahead": "scheduleAhead", "Scheduled date and time": "scheduledDateTime", "Vehicle": "vehicle", "Vehicle type": "vehicleCategory", "Vehicle class": "vehicleType", "Vehicle designation": "vehicleType", "Car": "car", "Car": "car", "Car only": "bikeOrCar", "Fare offer": "fareOffer", "Fare offer (FCFA)": "fareOffer", "Payment preference": "paymentPreference", "Cash in hand": "cashInHand", "MTN Mobile Money": "mtnMoney", "Orange Money": "orangeMoney", "Agree with rider before ride": "agreeWithRider", "Optional": "optional", "Record": "record", "Clear": "clear", "Rider access": "riderAccess", "Application status": "applicationStatus", "Your rider platform status will appear here.": "riderPlatformStatus", "Create one rider profile and submit it once for Waka admin review. Bike riders see only bike-relevant vehicle fields.": "riderApplicationIntro", "Create one rider profile and submit it once for Waka admin review. Start with the basics; Waka admin can request extra documents later.": "riderApplicationIntro", "Enter your account, identity, vehicle or bike, and document details once. Waka submits the application for admin review and sends an email confirmation link.": "riderOneStepApplicationIntro", "Enter the basics once: account, phone, work area, bike or car, plate, make, model, and color. Optional identity details and documents can be added later if admin asks.": "riderOneStepApplicationIntro", "Profile already loaded": "riderProfileLoaded", "Finish only the rider application details below. Your name, email, phone, and password stay with the signed-in account.": "riderFinishApplicationOnly", "Account": "account", "These details create the rider login and attach it to this application.": "riderAccountSectionHelp", "Identity": "identity", "Use the same identity details the agency or Waka admin can verify later.": "riderIdentitySectionHelp", "Vehicle": "vehicle", "Choose bike or car first so Waka only asks for relevant details.": "riderVehicleSectionHelp", "Documents": "documents", "Document uploads are optional during pilot signup. Waka admin may request any needed files later.": "riderDocumentsSectionHelp", "Operating area": "operatingArea", "License or professional credential number": "credentialNumber", "Vehicle make": "vehicleMake", "Vehicle model": "vehicleModel", "Body type": "bodyType", "Year of manufacture": "yearOfManufacture", "Vehicle VIN": "vehicleVin", "Vehicle registration": "vehicleRegistration", "Plate number": "vehicleRegistration", "Driver's license document": "driverLicenseDocument", "License document upload": "driverLicenseDocument", "Vehicle registration document": "vehicleRegistrationDocument", "Registration document": "vehicleRegistrationDocument", "National Identity card upload": "nationalIdDocument", "National Identity card upload (optional)": "nationalIdDocument", "Car make": "vehicleMake", "Car type/model": "vehicleModel", "Year": "yearOfManufacture", "Color": "vehicleColor", "Insurance provider": "paymentProvider", "Insurance policy number": "transactionReference", "Approved riders get a 30-day free period after admin approval. After that, the first 5 completed rides per day remain free and ride 6+ uses the rider wallet unless monthly access is active.": "subscriptionIntro", "Rider fare payment mode": "riderFarePaymentMode", "Cameroon riders receive negotiated ride fares directly from passengers by cash, MTN Mobile Money, or Orange Money. Wallet top-up and optional monthly access are paid separately from the Rider access panel.": "riderFarePaymentModeHelp", "Passenger fare collection remains direct cash, MTN Mobile Money, or Orange Money unless Waka later enables online ride-fare settlement.": "riderDirectFareCollectionHelp", "Review direct fare mode": "reviewDirectFareMode", "Direct passenger-to-rider fare payment is active after approval.": "riderDirectFareModeActive", "Rider payment": "riderPaymentChoice", "Wallet top-up bundle - from 5,000 FCFA": "walletTopupBundleXaf", "Monthly access subscription - 15,000 FCFA": "monthlyRiderAccessXaf", "Wallet top-up amount": "walletTopupAmount", "Payment provider": "paymentProviderChoice", "Choose MTN Mobile Money or Orange Money for wallet top-up or the optional monthly access subscription.": "subscriptionPaymentProviderHelp", "Payment phone": "paymentPhone", "For Cameroon, riders top up their wallet from 5,000 FCFA or pay optional 15,000 FCFA monthly access with MTN Mobile Money or Orange Money.": "subscriptionPaymentHelp", "Your fare": "yourFare", "Note to passenger before selection": "messageBeforeSelection", "Open requests": "openRequests", "Passengers": "passengers", "Riders": "riders", "Pending riders": "pendingRiders", "Subscribed": "subscribed", "Load demo market": "loadDemoMarket", "Clear local demo data": "clearDemoData", "Select or publish a request": "selectOrPublish", "Refresh market": "refreshMarket", "All": "all", "Ride requests": "rideRequests", "Rider offers": "riderOffers", "Account detail": "accountDetail", "Post-selection chat": "postSelectionChat", "Locked": "locked", "Send": "send", "Voice": "voice", "Choose rider": "chooseRider", "Open full review": "openFullReview", "Approve": "approve", "Decline": "decline", "Inter-city travel": "intercityTravel", "Book a seat on Cameroon bus routes through WakaGood.": "intercityHeroTitle", "Transport agencies can publish weekly departures, seat counts, boarding terminals, and payment instructions. Travelers can browse operators, compare cities and times, and book with or without a personal Waka account.": "intercityHeroBody", "Operators subscribe through WakaGood for 25,000 FCFA per month. Travelers can pay by MTN Mobile Money, Orange Money, or pay on arrival when the agency allows it. Pay-on-arrival seat holds close one hour before departure.": "intercityOperatorSubscriptionMessage", "Book travel now": "intercityBookNow", "Set up transport agency": "intercityAgencySetup", "Browse departures": "intercityBrowseDeparturesCta", "Transport agency access": "intercityOperatorAccessCta", "Agency setup": "agencySetupNav", "Transport agencies": "transportAgencies", "What happens next": "whatHappensNext", "Step 1 - choose a departure": "intercityStepOne", "Step 2 - reserve seats": "intercityStepTwo", "Selected trip details": "selectedTripDetails", "Select a departure to review the agency, route, boarding point, payment options, fare, and seats left before booking.": "chooseDepartureSummaryPrompt", "How travelers book": "travelerGuideTitle", "Choose your departure city, destination city, and travel date. Browse operators below, compare route details, review boarding point, drop-off point, seats, fare, and payment options, then continue to the booking form.": "travelerGuideBody", "Browse operators and departures": "openBookingForm", "How agencies set up": "agencyGuideTitle", "Agencies use a separate Waka agency access flow. Existing operators sign in to open the agency workspace. New operators create an agency account first, then Waka opens the workspace where they register their company, choose served cities, publish departures, manage blocked seats, review bookings, and message travelers.": "agencyGuideBody", "Inter-city travel": "passengerAgencyTravel", "Search transport agencies and book inter-city departures": "passengerAgencyTravelTitle", "Use the same WakaGood inter-city travel search here inside your passenger account. Compare agencies, dates, route details, seat availability, and payment options, then book and later review your bus trips under My trips.": "passengerAgencyTravelBody", "Also on Waka": "alsoOnWaka", "Book inter-city agency travel": "passengerIntercityShortcutTitle", "Need a bus or agency trip instead of a local ride? Open Inter-city travel to search transport agencies by date, departure city, destination city, route, fare per seat, and remaining seats before booking.": "passengerIntercityShortcutBody", "Open inter-city travel": "openPassengerIntercityTravel", "Continue to agency workspace": "openAgencySetup", "Agency sign in": "agencySignInCta", "Create agency account": "agencyCreateCta", "Agency sign in is for operators that already have Waka agency access. Create agency account is for a new transport company starting setup for the first time. Passenger account creation stays separate.": "agencyEntryHelp", "Weekly travel operations": "weeklyOperationsTitle", "Agencies can update travel status, confirm payments, cancel bookings when necessary, and keep passengers informed. Travelers can book without an account or sign in to track seat numbers, messages, and route updates.": "weeklyOperationsBody", "Choose agency and departure": "seeDepartures", "Search by agency, travel date, departure city, and destination city. Each result shows route, boarding point, seats, fare, and payment options before booking.": "intercitySearchSimpleBody", "Available inter-city departures": "availableIntercityDeparturesTitle", "Use the agency, date, From, and To filters to narrow the list, then select a matching departure.": "departureBrowserHelp", "Review selected trip, then book": "reviewBeforeBookingTitle", "1. Register agency": "agencyStepOneLabel", "Open Agency access, create or sign in to the operator account, and save the company profile with support phone, email, terminal address, and operated cities.": "agencyStepOneBody", "2. Publish departures": "agencyStepTwoLabel", "Use Weekly departures to enter origin city, destination city, boarding point, travel time, bus label, seats, and fare per seat.": "agencyStepTwoBody", "3. Update live availability": "agencyStepThreeLabel", "Block seats, pause a departure, or change travel status to boarding, departed, arrived, delayed, or cancelled.": "agencyStepThreeBody", "4. Manage bookings": "agencyStepFourLabel", "Confirm payments, cancel passengers when needed, mark check-in or completion, and message travelers by app or email.": "agencyStepFourBody", "Inter-city operators": "operatorSetupKicker", "Save the agency profile first. Waka uses this company identity, support contact, terminal address, and operated cities on the public travel website.": "agencyProfileHelp", "Step 2 - keep service active": "operatorStepTwoKicker", "Agencies start with one free month. Submit the monthly fee record here so Waka can keep future departures visible after the trial period.": "operatorPaymentHelp", "Step 3 - publish departures": "operatorStepThreeKicker", "Passengers only see departures that are active, tied to a live agency, and published with origin city, destination city, departure time, boarding point, seat count, and payment options.": "operatorDepartureHelp", "Step 4 - manage travelers": "operatorStepFourKicker", "Any transport agency": "anyTransportAgency", "Any departure city": "anyDepartureCity", "Any destination city": "anyDestinationCity", "Cameroon cities on Waka": "cameroonCitiesDirectoryTitle", "Transport agencies on Waka": "transportAgenciesDirectoryTitle", "Travel date": "travelDate", "Find departures": "findDepartures", "Loading verified agency departures.": "loadingAgencyDepartures", "Upcoming departures": "upcomingDepartures", "Companies, times, seats, and routes": "departureListSummary", "Book by email": "bookByEmail", "Reserve your inter-city seat": "reserveIntercitySeat", "Choose a departure to continue.": "chooseDepartureToContinue", "Choose a departure from the list. Traveler details, seat selection, and payment open here after selection.": "chooseDepartureToUnlockBooking", "Choose available seats": "chooseSeatsBeforeBooking", "Select seat numbers from the map before booking.": "selectSeatsFromMap", "Selected seats": "selectedSeatsLabel", "Amount to pay": "amountToPay", "Choose departure": "chooseDepartureShort", "None": "noneSelected", "0 FCFA": "zeroXaf", "Traveler name": "travelerName", "Email receipt": "emailReceipt", "Phone": "shortPhone", "Seats": "seats", "Payment option": "paymentOption", "Pay on arrival": "payOnArrival", "Pay on arrival (seat held)": "payOnArrival", "Booking note": "bookingNote", "Book departure": "bookDeparture", "Your booking receipt will be sent by email. Pay on arrival is available only until one hour before departure, and selected seats are held for the booking.": "bookingReceiptMessage", "Inter-city operators": "intercityOperators", "Register a transport agency": "registerTransportAgency", "Sign in as a passenger, then register your company and weekly bus departures for WakaGood inter-city bookings.": "operatorAccessMessage", "Company name": "companyName", "Company slug": "companySlug", "Support email": "supportEmail", "Support phone": "supportPhone", "Head office city": "headOfficeCity", "Bamenda": "cityBamenda", "Bafoussam": "cityBafoussam", "Buea": "cityBuea", "Douala": "cityDouala", "Yaounde": "cityYaounde", "Limbe": "cityLimbe", "Kumba": "cityKumba", "Mamfe": "cityMamfe", "Kribi": "cityKribi", "Mbouda": "cityMbouda", "Main terminal address": "mainTerminalAddress", "Operated cities": "operatedCities", "Agency description": "agencyDescription", "MTN MoMo account name": "mtnMomoAccountName", "MTN MoMo number": "mtnMomoNumber", "Orange Money account name": "orangeMoneyAccountName", "Orange Money number": "orangeMoneyNumber", "Save agency profile": "saveAgencyProfile", "Monthly platform fee": "monthlyPlatformFee", "Submit the 25,000 FCFA operator subscription": "submitOperatorSubscription", "Agency": "agency", "Choose agency": "chooseAgency", "Billing month": "billingMonth", "Payment method": "paymentMethod", "Cash": "cash", "Payer name": "payerName", "Payer phone": "payerPhone", "Reference": "shortReference", "Submit monthly fee record": "submitMonthlyFeeRecord", "WakaGood reviews operator subscription submissions before keeping departures active.": "operatorFeeReviewMessage", "Weekly departures": "weeklyDepartures", "Publish buses, seats, cities, and travel times": "publishBusesTitle", "Departure date and time": "departureDateTime", "Origin city": "originCity", "Destination city": "destinationCity", "Boarding location": "boardingLocation", "Dropoff location": "dropoffLocation", "Estimated duration (minutes)": "estimatedDurationMinutes", "Fare per seat (FCFA)": "farePerSeat", "Departure note": "departureNote", "Accept MTN MoMo": "acceptMtnMomo", "Accept Orange Money": "acceptOrangeMoney", "Allow pay on arrival": "allowPayOnArrival", "Save departure": "saveDeparture", "Passengers will see active departures, available seats, and payment options on the website.": "departureWebsiteMessage", "Traveler bookings": "travelerBookings", "Recent receipts and reservations": "recentReceiptsReservations" }; const placeholderTranslationKeys = { "Password": "passwordPlaceholder", "Create a password": "createPasswordPlaceholder", "6-digit code": "codePlaceholder", "Passenger name": "passengerNamePlaceholder", "National identification number": "nationalIdPlaceholder", "Driver license, state ID, or passport reference": "nationalIdPlaceholder", "Driver license, state ID, or passport": "nationalIdPlaceholder", "Optional ID, license, or passport": "optionalIdentityPlaceholder", "Driver's license number": "driverLicensePlaceholder", "Landmark, building color, market, junction, shop name": "pickupDescriptionPlaceholder", "Destination area, landmark, or address": "destinationPlaceholder", "Optional: bags, boxes, cargo, fragile items": "luggageNotePlaceholder", "Rider or driver name": "riderNamePlaceholder", "National ID, license, or permit number": "credentialPlaceholder", "17-character VIN": "credentialPlaceholder", "Vehicle color": "vehicle", "Plate or registration number": "registrationPlaceholder", "Plate number": "registrationPlaceholder", "Insurance company": "paymentProvider", "Policy number": "transactionReferencePlaceholder", "Enter a different counter-offer fare": "counterFarePlaceholder", "Enter a higher counter-offer fare": "counterFarePlaceholder", "Optional: brief vehicle or fare note for the passenger": "counterNotePlaceholder", "Supabase password": "supabasePasswordPlaceholder", "Chat opens only after passenger chooses a rider": "chatPlaceholder", "Describe the concern for admin review": "safetyReportDetailsPlaceholder" }; const translatedStaticTextNodes = []; const translatedStaticTextNodeSet = new WeakSet(); const productionTranslationTargetPercent = 100; const productionLaunchLanguages = ["en", "fr"]; const translationSameAsEnglishKeys = new Set([ "admin", "ok", "xlSpecial", "normalVehicle", "logoPreview", "message", "mtnMoney", "orangeMoney", "shortReference", "cityBamenda", "cityBafoussam", "cityBuea", "cityDouala", "cityYaounde", "cityLimbe", "cityKumba", "cityMamfe", "cityKribi", "cityMbouda" ]); const languageLabels = { en: "English", fr: "French", de: "German", pcm: "Pidgin", ar: "Arabic", sw: "Swahili", pt: "Portuguese", es: "Spanish" }; const staticTextTranslations = { fr: { "Waka Cameroon | Negotiated Rides for Cameroon": "Waka Cameroon | Courses negociees pour le Cameroun", "W": "W", "Waka Cameroon": "Waka Cameroon", "English": "Anglais", "Francais": "Francais", "Deutsch": "Allemand", "Pidgin": "Pidgin", "Arabic": "Arabe", "Swahili": "Swahili", "Portugues": "Portugais", "Espanol": "Espagnol", "Waka update ready": "Mise a jour Waka prete", "A verified Waka security and reliability update is ready. Updating keeps fares, privacy, and ride screens current.": "Une mise a jour Waka verifiee de securite et de fiabilite est prete. La mise a jour garde les prix, la confidentialite et les ecrans de course a jour.", "Update now": "Mettre a jour maintenant", "Project scope": "Portee du projet", "App navigation": "Navigation dans l'app", "Policies": "Politiques", "Guide": "Guide", "Privacy": "Confidentialite", "Terms": "Conditions", "Cameroon ride agreement": "Accord de course au Cameroun", "A Cameroon-first negotiated ride marketplace where passengers request landmark-based trips, riders make practical offers, and both sides confirm the fare before pickup, progress, completion, support, and ratings.": "Une place de marche de courses negociees pensee pour le Cameroun, ou les passagers demandent des trajets par reperes, les conducteurs font des offres pratiques, et les deux parties confirment le prix avant la prise en charge, le trajet, la fin, le support et les notes.", "Landmark-first rides": "Courses basees sur les reperes", "Cash and mobile money": "Especes et mobile money", "Admin-approved riders": "Conducteurs approuves par admin", "Request a ride": "Demander une course", "Drive with Waka Cameroon": "Conduire avec Waka Cameroon", "Passenger. Rider. Operations. One Cameroon marketplace.": "Passager. Conducteur. Operations. Une seule place de marche camerounaise.", "Built around landmarks, capped negotiation, direct payment, verified ride state, privacy, support, and auditable operations.": "Concu autour des reperes, de la negociation limitee, du paiement direct, des etats de course verifies, de la confidentialite, du support et des operations auditables.", "The Waka Cameroon vision": "La vision Waka Cameroon", "Make everyday transport clear before anyone starts moving.": "Rendre le transport quotidien clair avant que quelqu'un ne commence a bouger.", "Waka Cameroon turns informal price discussion into a structured marketplace. Passengers publish a landmark-based request, choose negotiation or no negotiation, and confirm the fare path before publishing. Riders review distance, timing, vehicle category, payment method, and capacity. The match begins only when the accepted fare is visible and confirmed.": "Waka Cameroon transforme la discussion informelle du prix en place de marche structuree. Les passagers publient une demande basee sur des reperes, choisissent negociation ou sans negociation, puis confirment le parcours du prix avant publication. Les conducteurs examinent distance, horaire, categorie de vehicule, mode de paiement et capacite. La mise en relation commence seulement quand le prix accepte est visible et confirme.", "Negotiation is capped at three passenger proposals and three rider proposals per ride.": "La negociation est limitee a trois propositions passager et trois propositions conducteur par course.", "Choice": "Choix", "Every Cameroon ride request uses negotiated fare agreement. Passengers enter an FCFA offer and riders can accept, decline, or counter.": "Chaque demande de course au Cameroun utilise un accord de prix negocie. Les passagers saisissent une offre en FCFA et les conducteurs peuvent accepter, refuser ou contre-proposer.", "Route": "Itineraire", "Fare guidance refreshes when pickup, destination, stops, or route details change.": "L'estimation du prix se met a jour quand la prise en charge, la destination, les arrets ou les details d'itineraire changent.", "Direct": "Direct", "Passengers pay matched riders by cash, MTN Mobile Money, or Orange Money.": "Les passagers paient le conducteur associe en especes, MTN Mobile Money ou Orange Money.", "A Cameroon transport marketplace, not just a request form.": "Une place de marche du transport au Cameroun, pas seulement un formulaire.", "Passenger platform": "Plateforme passager", "Account setup, landmark pickup and destination, passenger count, cash or mobile-money preference, capped negotiation, matched trip tools, support, ratings, and trip history.": "Creation de compte, prise en charge et destination par reperes, nombre de passagers, preference especes ou mobile money, negociation limitee, outils de trajet associe, support, notes et historique.", "Rider platform": "Plateforme conducteur", "Application, photo, vehicle category, documents, permit, emergency contact, admin approval, GPS availability, marketplace requests, fare offers, pickup navigation, wallet obligations, and ratings.": "Candidature, photo, categorie de vehicule, documents, permis, contact d'urgence, approbation admin, disponibilite GPS, demandes du marche, offres de prix, navigation vers la prise en charge, obligations wallet et notes.", "Operations platform": "Plateforme operations", "Rider approval, landmark management, support inbox, safety review, notices, broadcasts, marketplace controls, wallet visibility, diagnostics, feature flags, and production readiness.": "Approbation conducteur, gestion des reperes, boite support, examen securite, avis, diffusions, controles du marche, visibilite wallet, diagnostics, indicateurs de fonctions et preparation production.", "What the app covers": "Ce que couvre l'app", "Every major workflow is separated by role and tuned for Cameroon operations.": "Chaque grand parcours est separe par role et adapte aux operations au Cameroun.", "Ride marketplace": "Marche des courses", "Passengers publish requests, riders accept or counter, and matched requests are hidden from other riders after fare confirmation.": "Les passagers publient des demandes, les conducteurs acceptent ou contre-proposent, et les demandes associees sont cachees aux autres conducteurs apres confirmation du prix.", "Maps and route cost controls": "Cartes et controle des couts d'itineraire", "Landmarks and fallback maps work before paid map tiles. Real maps can be enabled intentionally with cache, usage alerting, and kill switches.": "Les reperes et cartes de secours fonctionnent avant les tuiles cartographiques payantes. Les vraies cartes peuvent etre activees volontairement avec cache, alertes d'usage et interrupteurs d'arret.", "Payments and rider wallet": "Paiements et wallet conducteur", "Cash, MTN Mobile Money, and Orange Money happen directly between passenger and rider. Waka commission is tracked through rider wallet operations after the free period.": "Les especes, MTN Mobile Money et Orange Money se reglent directement entre passager et conducteur. La commission Waka est suivie par les operations wallet conducteur apres la periode gratuite.", "Safety and support": "Securite et support", "Support tickets, safety reports, account notices, masked contact, cancellation context, and matched-counterparty ratings stay connected to the ride record.": "Les tickets support, signalements securite, avis de compte, contact masque, contexte d'annulation et notes de la contrepartie associee restent lies au dossier de course.", "Request with context.": "Demander avec contexte.", "Enter pickup and destination landmarks or typed addresses, enter your FCFA offer, choose negotiation or no negotiation, switch to Negotiate if a firm fare does not work, follow the rider approaching, and rate the matched rider after completion.": "Saisissez des reperes ou adresses tapees pour le depart et la destination, indiquez votre offre en FCFA, choisissez negociation ou sans negociation, passez a Negocier si un prix ferme ne convient pas, suivez l'approche du conducteur et notez-le apres la fin.", "Open Passenger": "Ouvrir Passager", "Drive on practical terms.": "Conduire avec des conditions pratiques.", "Complete eligibility, get admin approval, go active when ready, review nearby requests, accept or counter within the offer limit, navigate to pickup, and complete the ride flow.": "Completez l'eligibilite, obtenez l'approbation admin, activez-vous quand vous etes pret, examinez les demandes proches, acceptez ou contre-proposez dans la limite, naviguez vers la prise en charge et terminez le parcours de course.", "Open Rider": "Ouvrir Conducteur", "Operations": "Operations", "Operate with visibility.": "Operer avec visibilite.", "Review riders, manage landmarks, handle support and safety, monitor wallets and diagnostics, send notices, and keep platform flags ready for real-world operations.": "Examinez les conducteurs, gerez les reperes, traitez support et securite, surveillez wallets et diagnostics, envoyez des avis et gardez les fonctions pretes pour les operations reelles.", "View scope": "Voir la portee", "Navigate the app": "Naviguer dans l'app", "Each workspace has a clear menu, so users know where to go next.": "Chaque espace a un menu clair pour que les utilisateurs sachent ou aller ensuite.", "Passenger menu": "Menu passager", "Ride request for city, neighborhood, landmarks, stops, vehicle category, timing, payment, and fare.": "Demande de course pour ville, quartier, reperes, arrets, categorie de vehicule, horaire, paiement et prix.", "Enter an FCFA fare offer; riders can accept, decline, or counter within the proposal limit.": "Saisissez une offre en FCFA; les conducteurs peuvent accepter, refuser ou contre-proposer dans la limite de propositions.", "My trips for matched, active, completed, cancelled, and rating prompts.": "Mes trajets pour les courses associees, actives, terminees, annulees et les demandes de notation.", "Cash/mobile-money preference, rewards, profile, notices, and support for account operations.": "Preference especes/mobile money, recompenses, profil, avis et support pour les operations de compte.", "Rider menu": "Menu conducteur", "Overview and Initialize rider availability for readiness and GPS status.": "Vue d'ensemble et initialisation de disponibilite conducteur pour etat pret et GPS.", "Eligibility checks, ride requests, destination, earnings, wallet, and ratings.": "Verifications d'eligibilite, demandes de course, destination, gains, wallet et notes.", "Notices, support, and profile for rider account management.": "Avis, support et profil pour gerer le compte conducteur.", "Operations menu": "Menu operations", "Overview, alerts, locations, all activity, reports, controls, and messages.": "Vue d'ensemble, alertes, lieux, toute activite, rapports, controles et messages.", "Accounting, insurance mileage, rewards, tax and checks, rider approvals.": "Comptabilite, kilometrage assurance, recompenses, fiscalite et controles, approbations conducteurs.", "Passengers, riders, support inbox, audit, diagnostics, and launch readiness.": "Passagers, conducteurs, boite support, audit, diagnostics et preparation lancement.", "Why Waka Cameroon can win trust": "Pourquoi Waka Cameroon peut gagner la confiance", "The model is designed around visible agreement and accountable state.": "Le modele est concu autour d'un accord visible et d'etats responsables.", "Typical ride friction": "Frottements habituels des courses", "Prices can feel sudden or hard to question.": "Les prix peuvent sembler soudains ou difficiles a discuter.", "Riders may face requests that do not fit the trip economics.": "Les conducteurs peuvent recevoir des demandes qui ne couvrent pas l'economie du trajet.", "Passengers often commit with limited room to compare options.": "Les passagers s'engagent souvent avec peu de possibilite de comparer.", "Important ride, payment, support, and status details can feel fragmented.": "Les details importants de course, paiement, support et statut peuvent sembler disperses.", "Waka Cameroon's answer": "La reponse de Waka Cameroon", "The fare conversation is visible before match.": "La discussion du prix est visible avant la mise en relation.", "Route changes recalculate instead of keeping stale fare values.": "Les changements d'itineraire recalculent au lieu de garder des prix obsoletes.", "Accepted rides move into a controlled lifecycle with support and ratings.": "Les courses acceptees passent dans un cycle controle avec support et notes.", "Operations tools keep safety, direct-payment records, diagnostics, and notices visible.": "Les outils operations gardent visibles la securite, les traces de paiement direct, les diagnostics et les avis.", "The experience": "L'experience", "Four moments, one simple promise.": "Quatre moments, une promesse simple.", "Ask": "Demander", "The passenger publishes pickup, destination, timing, vehicle, and the correct fare mode.": "Le passager publie depart, destination, horaire, vehicule et le bon mode de prix.", "Offer": "Offrir", "Riders accept firm fares or counter negotiated requests while both sides stay within proposal limits.": "Les conducteurs acceptent les prix fermes ou contre-proposent sur les demandes negociees pendant que les deux cotes respectent les limites.", "Agree": "Accepter", "Either side accepts only after seeing the fare being confirmed.": "Chaque cote accepte seulement apres avoir vu le prix a confirmer.", "Ride": "Course", "The matched trip moves through approach, pickup, progress, completion, rating, and support.": "Le trajet associe passe par approche, prise en charge, progression, fin, note et support.", "Policies and operating principles": "Politiques et principes d'exploitation", "Clear rules support a safer marketplace.": "Des regles claires soutiennent un marche plus sur.", "Waka Cameroon limits data sharing by ride stage, uses masked contact after match, and keeps direct-payment records separate from full mobile-money credentials.": "Waka Cameroon limite le partage des donnees selon l'etape de la course, utilise un contact masque apres association et garde les traces de paiement direct separees des identifiants complets de mobile money.", "Read privacy policy": "Lire la politique de confidentialite", "Terms and conduct": "Conditions et conduite", "Passengers and riders must use accurate information, avoid unsafe or abusive behavior, and keep fare agreement, payment confirmation, and support paths inside Waka Cameroon.": "Les passagers et conducteurs doivent utiliser des informations exactes, eviter les comportements dangereux ou abusifs et garder l'accord de prix, la confirmation de paiement et le support dans Waka Cameroon.", "Read terms": "Lire les conditions", "Security and compliance": "Securite et conformite", "Rider approval, documents, wallet review, landmark management, ratings, notices, and operator actions are designed around server-side checks and auditability.": "L'approbation conducteur, les documents, la revue wallet, la gestion des reperes, les notes, les avis et les actions operateur sont concus autour de controles serveur et de l'auditabilite.", "Read project scope": "Lire la portee du projet", "Ready to move differently": "Pret a se deplacer autrement", "Start with agreement. Move with confidence.": "Commencez par l'accord. Deplacez-vous avec confiance.", "Waka Cameroon is built for people who want transport to feel fair, practical, secure, and clear before the ride starts moving.": "Waka Cameroon est concu pour les personnes qui veulent un transport juste, pratique, securise et clair avant que la course commence.", "Read how Waka Cameroon works": "Lire comment fonctionne Waka Cameroon", "Read the privacy policy": "Lire la politique de confidentialite", "Read the terms and community guidelines": "Lire les conditions et regles communautaires", "Passenger access": "Acces passager", "Move with the fare already clear.": "Avancez avec un prix deja clair.", "Request landmark-based rides, compare offers, follow pickup, and keep trip support in one secure Waka Cameroon account.": "Demandez des courses par reperes, comparez les offres, suivez la prise en charge et gardez le support de trajet dans un compte Waka Cameroon securise.", "F": "F", "Fare first": "Prix d'abord", "V": "V", "Verified riders": "Conducteurs verifies", "Support": "Support", "P": "P", "D": "D", "Then match": "Puis association", "Welcome back": "Bon retour", "Passenger sign in": "Connexion passager", "Sign in to your Waka Cameroon passenger account.": "Connectez-vous a votre compte passager Waka Cameroon.", "Forgot password?": "Mot de passe oublie ?", "Account security": "Securite du compte", "Agency account security": "Securite du compte agence", "Agency password recovery": "Recuperation du mot de passe agence", "Current password": "Mot de passe actuel", "New password": "Nouveau mot de passe", "Confirm new password": "Confirmer le nouveau mot de passe", "At least 8 characters": "Au moins 8 caracteres", "Change password": "Changer le mot de passe", "Change agency password": "Changer le mot de passe agence", "Update agency password": "Mettre a jour le mot de passe agence", "Back to agency sign in": "Retour a la connexion agence", "Change your password without leaving the passenger account.": "Changez votre mot de passe sans quitter le compte passager.", "Change your password without leaving the rider account.": "Changez votre mot de passe sans quitter le compte conducteur.", "Agency managers can update the workspace login password here.": "Les responsables d'agence peuvent mettre a jour ici le mot de passe de connexion a l'espace de travail.", "Open the reset link sent to the agency email, then enter a new password twice.": "Ouvrez le lien de reinitialisation envoye a l'e-mail de l'agence, puis saisissez deux fois le nouveau mot de passe.", "Open the reset link from the agency email first.": "Ouvrez d'abord le lien de reinitialisation depuis l'e-mail de l'agence.", "Secure password recovery": "Recuperation securisee du mot de passe", "Verify the registered phone on this passenger account before creating a new password.": "Verifiez le telephone enregistre sur ce compte passager avant de creer un nouveau mot de passe.", "Verify registered phone": "Verifier le telephone enregistre", "Send a code to the verified phone saved on this account.": "Envoyer un code au telephone verifie enregistre sur ce compte.", "Phone code": "Code telephone", "Phone verification is required before saving a new password.": "La verification du telephone est requise avant d'enregistrer un nouveau mot de passe.", "New password": "Nouveau mot de passe", "Confirm password": "Confirmer le mot de passe", "Update password": "Mettre a jour le mot de passe", "New to Waka Cameroon?": "Nouveau sur Waka Cameroon ?", "OK": "OK", "Your trip data stays protected.": "Vos donnees de trajet restent protegees.", "Waka Cameroon keeps fare, route, and support activity inside your secure account.": "Waka Cameroon garde le prix, l'itineraire et le support dans votre compte securise.", "Ride request": "Demande de course", "Menu": "Menu", "My trips": "Mes trajets", "Payment": "Paiement", "Business": "Entreprise", "Rewards": "Recompenses", "Profile": "Profil", "Notices": "Avis", "Secure payment setup": "Configuration de paiement securisee", "Cameroon MVP rides are paid directly to the matched rider by cash, MTN Mobile Money, or Orange Money. Waka does not collect passenger fare money in this package.": "Les courses MVP Cameroun sont payees directement au conducteur associe en especes, MTN Mobile Money ou Orange Money. Waka ne collecte pas l'argent du passager dans ce package.", "Direct passenger payment": "Paiement direct passager", "Choose the payment preference on the ride request. The accepted fare remains visible before pickup and completion.": "Choisissez la preference de paiement dans la demande. Le prix accepte reste visible avant la prise en charge et la fin.", "Cash or mobile money": "Especes ou mobile money", "Review direct payment mode": "Verifier le mode paiement direct", "Cash, MTN Mobile Money, and Orange Money are available on Cameroon ride requests.": "Especes, MTN Mobile Money et Orange Money sont disponibles sur les demandes de course au Cameroun.", "Passenger profile": "Profil passager", "Profile picture not uploaded.": "Photo de profil non ajoutee.", "Invite & earn": "Inviter et gagner", "Passenger rewards": "Recompenses passager", "Referral rewards appear here after setup.": "Les recompenses de parrainage apparaissent ici apres configuration.", "Code pending": "Code en attente", "Share your invite link. Passenger credits appear after the referred passenger completes a paid ride.": "Partagez votre lien d'invitation. Les credits passager apparaissent apres que le passager parraine termine une course payee.", "Copy link": "Copier le lien", "Share": "Partager", "Text": "Texte", "Current passenger state": "Etat passager actuel", "State": "Etat", "Update state": "Mettre a jour l'etat", "Ride requests use this state.": "Les demandes de course utilisent cet etat.", "Business account": "Compte entreprise", "Business name": "Nom de l'entreprise", "Billing email": "Email de facturation", "Business type": "Type d'entreprise", "Hotel / lodging": "Hotel / hebergement", "Clinic / healthcare": "Clinique / sante", "Employer / staff transport": "Employeur / transport du personnel", "School / campus": "Ecole / campus", "Venue / event": "Lieu / evenement", "Other organization": "Autre organisation", "Preferred plan": "Forfait prefere", "Starter - 30 days free, then 10% per completed ride": "Starter - 30 jours gratuits, puis 10% par course terminee", "Partner - 30 days free, then 120,000 FCFA/month, no 10% ride fee": "Partenaire - 30 jours gratuits, puis 120 000 FCFA/mois, sans frais de course 10%", "Business address": "Adresse de l'entreprise", "Contact person": "Personne contact", "Contact phone": "Telephone contact", "Referral code": "Code de parrainage", "Create business account": "Creer un compte entreprise", "Business accounts require Waka verification before ride billing. Verified businesses get 30 days free; after that Starter adds 10% per completed ride or Partner is 120,000 FCFA/month with no 10% ride fee.": "Les comptes entreprise exigent une verification Waka avant la facturation des courses. Les entreprises verifiees obtiennent 30 jours gratuits; ensuite Starter ajoute 10% par course terminee ou Partenaire coute 120 000 FCFA/mois sans frais de course 10%.", "Business invites": "Invitations entreprise", "Business referral code": "Code de parrainage entreprise", "Business referral benefits appear after setup.": "Les avantages de parrainage entreprise apparaissent apres configuration.", "Share your business invite link. Admin can track referred organizations after Waka verification.": "Partagez votre lien d'invitation entreprise. L'admin peut suivre les organisations parrainees apres verification Waka.", "FINEXS Voyages (Yaounde)": "FINEXS Voyages (Yaounde)", "Buca Voyages (Yaounde)": "Buca Voyages (Yaounde)", "Waka Starter Bamenda Connect (Bamenda)": "Waka Starter Bamenda Connect (Bamenda)", "Waka Starter Capital Link (Yaounde)": "Waka Starter Capital Link (Yaounde)", "Waka Starter Coastline (Buea)": "Waka Starter Coastline (Buea)", "Waka Starter West Link (Bafoussam)": "Waka Starter West Link (Bafoussam)", "Abong-Mbang": "Abong-Mbang", "Akonolinga": "Akonolinga", "Bafang": "Bafang", "Bafia": "Bafia", "Bafoussam": "Bafoussam", "Bali": "Bali", "Bamenda": "Bamenda", "Bandjoun": "Bandjoun", "Bangangte": "Bangangte", "Banyo": "Banyo", "Batouri": "Batouri", "Belo": "Belo", "Bertoua": "Bertoua", "Bogo": "Bogo", "Buea": "Buea", "Deido": "Deido", "Dibombari": "Dibombari", "Dimako": "Dimako", "Dizangue": "Dizangue", "Djoum": "Djoum", "Douala": "Douala", "Dschang": "Dschang", "Ebolowa": "Ebolowa", "Edea": "Edea", "Eseka": "Eseka", "Evodoula": "Evodoula", "Foumban": "Foumban", "Foumbot": "Foumbot", "Fundong": "Fundong", "Garoua": "Garoua", "Guider": "Guider", "Idenau": "Idenau", "Kaele": "Kaele", "Kousseri": "Kousseri", "Kribi": "Kribi", "Kumba": "Kumba", "Kumbo": "Kumbo", "Limbe": "Limbe", "Loum": "Loum", "Mamfe": "Mamfe", "Manjo": "Manjo", "Maroua": "Maroua", "Mbalmayo": "Mbalmayo", "Mbandjock": "Mbandjock", "Mbanga": "Mbanga", "Mbouda": "Mbouda", "Meiganga": "Meiganga", "Melong": "Melong", "Mokolo": "Mokolo", "Mora": "Mora", "Mutengene": "Mutengene", "Nanga-Eboko": "Nanga-Eboko", "Ngaoundere": "Ngaoundere", "Nguti": "Nguti", "Nkambe": "Nkambe", "Nkongsamba": "Nkongsamba", "Ntui": "Ntui", "Obala": "Obala", "Penja": "Penja", "Sangmelima": "Sangmelima", "Tiko": "Tiko", "Tibati": "Tibati", "Wum": "Wum", "Yagoua": "Yagoua", "Yaounde": "Yaounde", "Yokadouma": "Yokadouma", "Waka notices": "Avis Waka", "Enable phone notifications": "Activer les notifications telephone", "Phone notifications need browser permission on this device.": "Les notifications telephone necessitent l'autorisation du navigateur sur cet appareil.", "Contact Waka support": "Contacter le support Waka", "Category": "Categorie", "Account access or profile": "Acces compte ou profil", "Payment, card, fare, or refund": "Paiement, carte, prix ou remboursement", "Ride request or trip issue": "Demande de course ou probleme de trajet", "Safety concern": "Probleme de securite", "App or technical problem": "Probleme app ou technique", "Other support request": "Autre demande support", "Subject": "Sujet", "Send support request": "Envoyer la demande support", "Support requests go to Waka support.": "Les demandes support vont au support Waka.", "Account use": "Utilisation du compte", "Personal rides": "Courses personnelles", "Business rides": "Courses entreprise", "Business billing email": "Email de facturation entreprise", "Already have a Waka Cameroon passenger account?": "Vous avez deja un compte passager Waka Cameroon ?", "Add a passenger payment method before publishing. You can still prepare pickup and destination details here.": "Ajoutez un mode de paiement passager avant publication. Vous pouvez toujours preparer les details de depart et destination ici.", "Fare": "Prix", "Negotiate": "Negocier", "Fare choice": "Choix du prix", "Negotiated fare": "Prix negocie", "Pickup routing area": "Zone d'itineraire de depart", "Pickup": "Prise en charge", "Current": "Actuel", "Pickup GPS": "GPS de depart", "Exact pickup location is off.": "La position exacte de prise en charge est desactivee.", "Use current location": "Utiliser la position actuelle", "Refresh pickup GPS": "Actualiser le GPS de depart", "Destination routing region": "Region d'itineraire destination", "T": "T", "Timing": "Horaire", "Trip": "Trajet", "Enter pickup and destination to see fare.": "Entrez depart et destination pour voir le prix.", "Enter the fare you want to offer in FCFA. Suggestions are optional; typed pickup and destination text can still be published. Waka sends vehicle, passenger count, luggage, and stops to riders for agreement.": "Entrez le prix que vous voulez proposer en FCFA. Les suggestions sont facultatives; le depart et la destination tapes peuvent aussi etre publies. Waka envoie le vehicule, le nombre de passagers, les bagages et les arrets aux conducteurs pour accord.", "Riders see these trip details before accepting or negotiating. Waka Cameroon does not estimate or auto-price this request.": "Les conducteurs voient ces details de trajet avant d'accepter ou de negocier. Waka Cameroon n'estime pas et ne fixe pas automatiquement le prix de cette demande.", "Clear stops": "Effacer les arrets", "Stops": "Arrets", "Billing account": "Compte de facturation", "Personal ride": "Course personnelle", "Rider availability appears after pickup is set.": "La disponibilite des conducteurs apparait apres choix du depart.", "Normal": "Normal", "XL/Special": "XL/Special", "Luggage pieces": "Bagages", "Luggage note": "Note bagages", "Passenger count and luggage details are sent to riders with the fare offer. The final fare is agreed before match.": "Le nombre de passagers et les details des bagages sont envoyes aux conducteurs avec l'offre de prix. Le prix final est accepte avant la mise en relation.", "Card or online payment": "Carte ou paiement en ligne", "Online wallet or bank transfer": "Wallet en ligne ou virement bancaire", "Work from a cleaner ride queue.": "Travaillez depuis une file de courses plus claire.", "Review eligible requests, confirm fares, manage documents, and track access from one rider workspace.": "Examinez les demandes eligibles, confirmez les prix, gerez les documents et suivez l'acces depuis un seul espace conducteur.", "R": "R", "Ride queue": "File de courses", "Verified profile": "Profil verifie", "Clear fares": "Prix clairs", "Eligible trips": "Trajets eligibles", "Ready to review": "Pret a examiner", "Rider sign in": "Connexion conducteur", "Sign in to your Waka Cameroon rider account.": "Connectez-vous a votre compte conducteur Waka Cameroon.", "Verify the registered phone on this rider account before creating a new password.": "Verifiez le telephone enregistre sur ce compte conducteur avant de creer un nouveau mot de passe.", "Ready to drive with Waka Cameroon?": "Pret a conduire avec Waka Cameroon ?", "Your rider account is protected.": "Votre compte conducteur est protege.", "Documents, ride access, payments, and trip activity stay in your secure workspace.": "Documents, acces aux courses, paiements et activite de trajet restent dans votre espace securise.", "Create one rider profile and submit it once for Waka admin review. Bike riders see only bike-relevant vehicle fields.": "Creez un seul profil conducteur et soumettez-le une fois a la validation admin Waka. Les conducteurs moto ne voient que les champs utiles pour la moto.", "Enter your account, identity, vehicle or bike, and document details once. Waka submits the application for admin review and sends an email confirmation link.": "Saisissez une seule fois les informations de compte, d'identite, de voiture ou moto et les documents. Waka envoie la demande a l'admin et vous transmet un lien de confirmation par e-mail.", "Profile already loaded": "Profil deja charge", "Finish only the rider application details below. Your name, email, phone, and password stay with the signed-in account.": "Completez seulement les details de la demande conducteur ci-dessous. Votre nom, e-mail, telephone et mot de passe restent lies au compte connecte.", "Account": "Compte", "These details create the rider login and attach it to this application.": "Ces informations creent la connexion conducteur et la rattachent a cette demande.", "Identity": "Identite", "Use the same identity details the agency or Waka admin can verify later.": "Utilisez les memes informations d'identite que l'agence ou l'admin Waka pourra verifier ensuite.", "Vehicle": "Vehicule", "Choose bike or car first so Waka only asks for relevant details.": "Choisissez d'abord moto ou voiture afin que Waka ne demande que les details pertinents.", "Documents": "Pieces justificatives", "Document uploads are optional during pilot signup. Waka admin may request any needed files later.": "Les televersements de documents sont facultatifs pendant l'inscription pilote. L'admin Waka peut demander les fichiers necessaires plus tard.", "Overview": "Vue d'ensemble", "Track approval, earnings, ride access, and account setup from one rider workspace.": "Suivez l'approbation, les gains, l'acces aux courses et la configuration du compte depuis un seul espace conducteur.", "Initialize rider availability": "Initialiser la disponibilite conducteur", "Eligibility checks": "Verifications d'eligibilite", "Earnings": "Gains", "Payout": "Paiement conducteur", "Ratings": "Notes", "Navigation": "Navigation", "Google Maps": "Google Maps", "Waze": "Waze", "Renew documents": "Renouveler les documents", "Bike": "Moto", "National Identity card number": "Numero de carte nationale d'identite", "National Identity card number (optional)": "Numero de carte nationale d'identite (optionnel)", "Profile picture (optional)": "Photo de profil (optionnelle)", "Driver's license expiration date (optional)": "Date d'expiration du permis de conduire (optionnelle)", "Year and month of birth (optional)": "Annee et mois de naissance (optionnel)", "Vehicle category": "Categorie de vehicule", "Car / taxi": "Voiture / taxi", "Bike / motorbike": "Moto", "Vehicle VIN or chassis number (optional)": "VIN ou numero de chassis (optionnel)", "Insurance provider (optional)": "Assureur (optionnel)", "Insurance policy number (optional)": "Numero de police d'assurance (optionnel)", "Insurance expiration date (optional)": "Date d'expiration de l'assurance (optionnelle)", "National Identity card upload": "Televersement de la carte nationale d'identite (optionnel)", "National Identity card upload (optional)": "Televersement de la carte nationale d'identite (optionnel)", "License document upload (optional)": "Televersement du permis (optionnel)", "Registration document (optional)": "Document d'immatriculation (optionnel)", "Insurance document (optional)": "Document d'assurance (optionnel)", "New license expiration": "Nouvelle expiration du permis", "New license document": "Nouveau document de permis", "New insurance expiration": "Nouvelle expiration d'assurance", "New insurance document": "Nouveau document d'assurance", "Submit renewed documents": "Soumettre les documents renouveles", "Renew before expiration to keep service active. After suspension, admin must approve renewal before service resumes.": "Renouvelez avant expiration pour garder le service actif. Apres suspension, l'admin doit approuver le renouvellement avant reprise.", "Rider rewards": "Recompenses conducteur", "Share your invite link. Rider rewards appear after the referred rider completes the required paid rides.": "Partagez votre lien d'invitation. Les recompenses conducteur apparaissent apres que le conducteur parraine termine les courses payees requises.", "Rider wallet and commission": "Wallet conducteur et commission", "Cameroon MVP riders receive fares directly from passengers. After the free period, Waka operations tracks wallet balance and commission obligations instead of Stripe payout onboarding.": "Les conducteurs MVP Cameroun recoivent les prix directement des passagers. Apres la periode gratuite, les operations Waka suivent le solde wallet et les obligations de commission au lieu de l'onboarding Stripe.", "Rider earnings": "Gains conducteur", "Keep rider wallet details current for admin review. Passenger fare collection remains direct cash, MTN Mobile Money, or Orange Money.": "Gardez les details du wallet conducteur a jour pour revue admin. La collecte du prix passager reste directe en especes, MTN Mobile Money ou Orange Money.", "Review wallet mode": "Verifier le mode wallet", "Waka operations handles rider wallet and commission review after approval.": "Les operations Waka gerent la revue wallet conducteur et commission apres approbation.", "Starting area fallback": "Zone de depart de secours", "Preferred destination regions for today": "Regions de destination preferees aujourd'hui", "Nearby request visibility": "Visibilite des demandes proches", "Show all nearby pickups": "Afficher tous les departs proches", "Only preferred destinations": "Seulement les destinations preferees", "Rider availability": "Disponibilite conducteur", "Activate when you are ready to receive nearby ride requests.": "Activez-vous quand vous etes pret a recevoir des demandes proches.", "Activate": "Activer", "Deactivate": "Desactiver", "Save setup": "Enregistrer la configuration", "Nearby requests appear while you are activated.": "Les demandes proches apparaissent quand vous etes active.", "Destination preferences are on the Destination page. Nearby requests use your active location.": "Les preferences de destination sont sur la page Destination. Les demandes proches utilisent votre position active.", "Destination preference": "Preference de destination", "Show all nearby rides, or narrow this marketplace view by destination.": "Affichez toutes les courses proches, ou limitez cette vue par destination.", "I want to filter this marketplace view by where I prefer to go right now.": "Je veux filtrer cette vue selon l'endroit ou je prefere aller maintenant.", "State / city": "Region / ville", "Town / area": "Ville / zone", "Destination contains": "La destination contient", "Apply destination filter": "Appliquer le filtre de destination", "Payment, payout, tax, or subscription": "Paiement, versement, fiscalite ou abonnement", "Documents, vehicle, or background check": "Documents, vehicule ou controle d'antecedents", "Background check": "Controle d'antecedents", "Not requested": "Non demande", "After submitting your rider application, complete provider screening so Waka can make an approval decision.": "Apres soumission de votre candidature conducteur, completez le controle fournisseur afin que Waka prenne une decision.", "Start background check": "Demarrer le controle d'antecedents", "Background-check status will appear here.": "Le statut du controle apparaitra ici.", "Tax documents": "Documents fiscaux", "Hosted tax setup": "Configuration fiscale hebergee", "Tax-provider onboarding is disabled for the Cameroon MVP. Waka operations can add country-specific tax/compliance review before public launch.": "L'onboarding fiscal fournisseur est desactive pour le MVP Cameroun. Les operations Waka peuvent ajouter une revue fiscale/conformite locale avant le lancement public.", "Review tax status": "Verifier le statut fiscal", "No US tax-provider flow is required for this Cameroon MVP package.": "Aucun parcours fiscal americain n'est requis pour ce package MVP Cameroun.", "License expiration date": "Date d'expiration du permis", "Year and month of birth": "Annee et mois de naissance", "Operating area fallback": "Zone d'operation de secours", "Insurance expiration date": "Date d'expiration d'assurance", "I authorize Waka to review my rider eligibility, identity, vehicle, permit, insurance, and safety records for admin approval.": "J'autorise Waka a examiner mon eligibilite conducteur, identite, vehicule, permis, assurance et dossiers de securite pour approbation admin.", "Vehicle inspection document (optional)": "Document d'inspection du vehicule (optionnel)", "Already have a Waka Cameroon rider account?": "Vous avez deja un compte conducteur Waka Cameroon ?", "Approved riders get a 30-day commission-free period after admin approval.": "Les conducteurs approuves obtiennent 30 jours sans commission apres validation admin.", "After the free period, the first 5 completed rides each day remain free. Ride 6+ deducts 15% of the fare from the rider wallet. Wallet top-ups start at 5,000 FCFA and low-balance notices start below 1,000 FCFA.": "Apres la periode gratuite, les 5 premieres courses terminees chaque jour restent gratuites. La course 6 et plus deduit 15% du prix du wallet conducteur. Les recharges commencent a 5 000 FCFA et les alertes de solde bas commencent sous 1 000 FCFA.", "Rider payment": "Paiement conducteur", "Wallet top-up bundle - from 5,000 FCFA": "Recharge wallet - des 5 000 FCFA", "Monthly access subscription - 15,000 FCFA": "Abonnement acces mensuel - 15 000 FCFA", "Wallet top-up amount": "Montant de recharge wallet", "Wallet review - 5,000 FCFA minimum target": "Revue wallet - objectif minimum 5 000 FCFA", "Daily commission - 15% after 5 free rides": "Commission journaliere - 15% apres 5 courses gratuites", "Renewal": "Renouvellement", "Manual mobile-money payment": "Paiement mobile money manuel", "Waka operations reviews wallet and commission readiness after the free period.": "Les operations Waka examinent le wallet et la preparation commission apres la periode gratuite.", "Start rider payment": "Demarrer le paiement conducteur", "Choose MTN Mobile Money or Orange Money for wallet top-up or the optional monthly access subscription.": "Choisissez MTN Mobile Money ou Orange Money pour recharger le wallet ou payer l'abonnement mensuel optionnel.", "Completed rides and earnings": "Courses terminees et gains", "0 rides": "0 course", "Create first admin account": "Creer le premier compte admin", "Create and confirm the admin user in Supabase Authentication.": "Creez et confirmez l'utilisateur admin dans Supabase Authentication.", "Promote that user's profile to": "Promouvoir le profil de cet utilisateur a", "role = 'admin'": "role = 'admin'", "using the SQL in": "avec le SQL dans", "docs/ADMIN-SETUP.md": "docs/ADMIN-SETUP.md", "Sign in here to view passengers, riders, approvals, payments, support issues, audit logs, and transaction history.": "Connectez-vous ici pour voir passagers, conducteurs, approbations, paiements, problemes support, journaux d'audit et historique des transactions.", "Admin workspace": "Espace admin", "Monitor Waka activity and open the area you need.": "Surveillez l'activite Waka et ouvrez la zone voulue.", "Previous": "Precedent", "1 of 16": "1 sur 16", "Next": "Suivant", "Alerts": "Alertes", "Locations": "Lieux", "Locations, neighborhoods, and city coverage": "Lieux, quartiers et couverture des villes", "All activity": "Toute activite", "Reports": "Rapports", "Controls": "Controles", "Messages": "Messages", "Accounting": "Comptabilite", "Insurance mileage": "Kilometrage assurance", "Tax & checks": "Fiscalite et controles", "Rider approvals": "Approbations conducteurs", "Support inbox": "Boite support", "Audit": "Audit", "Diagnostics": "Diagnostics", "Search everything": "Tout rechercher", "Search": "Rechercher", "Country / state or town": "Pays / region ou ville", "All regions": "Toutes les regions", "Search accounts, rides, messages, payments, safety reports, and audit records by keyword or region.": "Rechercher comptes, courses, messages, paiements, rapports securite et audits par mot-cle ou region.", "Session": "Session", "Admin alerts": "Alertes admin", "Active rides": "Courses actives", "GPS writes 24h": "Ecritures GPS 24h", "Paid Google calls 24h": "Appels Google payants 24h", "Route cache hits": "Reutilisations du cache itineraire", "Slow RPC/API": "RPC/API lentes", "Database size": "Taille base de donnees", "n/a": "n/d", "File storage": "Stockage fichiers", "Noisy rows 24h": "Lignes bruyantes 24h", "Retention rows 30d": "Lignes retention 30j", "Admin page directory": "Repertoire des pages admin", "Every admin page is available here with live counts and quick routing.": "Chaque page admin est disponible ici avec comptes en direct et acces rapide.", "0 pages": "0 page", "Recent platform activity": "Activite recente de la plateforme", "0 activity records": "0 enregistrement d'activite", "Open full ledger": "Ouvrir le grand livre complet", "Launch readiness checks": "Verifications de preparation lancement", "These cards are backend and launch blockers. Use the Admin menu for live operations.": "Ces cartes concernent les blocages backend et lancement. Utilisez le menu Admin pour les operations en direct.", "Bamenda, Cameroon": "Bamenda, Cameroun", "North": "Nord", "Market": "Marche", "Center": "Centre", "Transit": "Transit", "Ride request detail": "Detail de demande de course", "Review pickup, fare, and route before responding.": "Examinez depart, prix et itineraire avant de repondre.", "Marketplace": "Marche", "Select a nearby request to see pickup, destination, fare, payment, and distance before offering.": "Selectionnez une demande proche pour voir depart, destination, prix, paiement et distance avant d'offrir.", "Accept passenger fare": "Accepter le prix passager", "Send counter-offer": "Envoyer une contre-offre", "Decline request": "Refuser la demande", "0 alerts": "0 alerte", "Approval workflow only: documents, local safety review, corrections, decline, and final approval. Use Search everything above to find a rider quickly.": "Parcours d'approbation seulement: documents, revue securite locale, corrections, refus et approbation finale. Utilisez Tout rechercher ci-dessus pour trouver vite un conducteur.", "Tax and background resources": "Ressources fiscalite et antecedents", "0 compliance rows": "0 ligne de conformite", "0 profiles": "0 profil", "Full rider directory: search profiles, documents, subscription state, payout setup, ride history, messages, and account controls.": "Repertoire complet conducteurs: recherchez profils, documents, etat d'abonnement, configuration de paiement, historique de courses, messages et controles de compte.", "Accounting dashboard": "Tableau de bord comptable", "0 settlements": "0 reglement", "Referral rewards": "Recompenses de parrainage", "Review invite relationships, passenger credits, rider bonuses, and reward payout or void decisions.": "Examinez les liens d'invitation, credits passager, bonus conducteur et decisions de paiement ou annulation de recompense.", "0 rewards": "0 recompense", "Audit Period 1, Period 2, and Period 3 active-mile telemetry for insurance review and exports.": "Auditez la telemetrie de miles actifs Periode 1, Periode 2 et Periode 3 pour assurance et exports.", "View": "Voir", "Ride/rider segments": "Segments course/conducteur", "Daily totals": "Totaux quotidiens", "Period": "Periode", "All periods": "Toutes les periodes", "Period 1": "Periode 1", "Period 2": "Periode 2", "Period 3": "Periode 3", "Export status": "Statut export", "All statuses": "Tous les statuts", "Pending": "En attente", "Exported": "Exporte", "Excluded": "Exclu", "From": "De", "To": "A", "Apply filters": "Appliquer les filtres", "Download page CSV": "Telecharger CSV de page", "Download filtered CSV": "Telecharger CSV filtre", "Mark page exported": "Marquer la page exportee", "Insurance telemetry loads when this page is opened.": "La telemetrie assurance se charge a l'ouverture de cette page.", "0 telemetry rows": "0 ligne de telemetrie", "Subscription payment audit": "Audit des paiements d'abonnement", "0 payment records": "0 enregistrement de paiement", "0 webhook records": "0 enregistrement webhook", "Transaction ledger": "Grand livre transactions", "0 transactions": "0 transaction", "System diagnostics": "Diagnostics systeme", "Search backend incidents, notification delivery, Edge Function, and provider events by ride ID, user ID, notification ID, request ID, or provider text.": "Recherchez incidents backend, livraison notification, Edge Function et evenements fournisseur par ID course, ID utilisateur, ID notification, ID demande ou texte fournisseur.", "Diagnostics load only when this page is opened or searched.": "Les diagnostics se chargent seulement quand cette page est ouverte ou recherchee.", "0 events": "0 evenement", "Operations controls": "Controles operations", "0 ride controls": "0 controle de course", "Country, state and town view": "Vue pays, region et ville", "0 regions": "0 region", "Download report": "Telecharger le rapport", "Download CSV": "Telecharger CSV", "Download PDF": "Telecharger PDF", "Download JSON": "Telecharger JSON", "Copy summary": "Copier le resume", "Report type": "Type de rapport", "Full platform report": "Rapport complet plateforme", "Alerts only": "Alertes seulement", "Support inbox only": "Boite support seulement", "Transactions only": "Transactions seulement", "Accounts only": "Comptes seulement", "All categories": "Toutes les categories", "Safety": "Securite", "Payments": "Paiements", "Rides": "Courses", "Tax": "Fiscalite", "Background checks": "Controles d'antecedents", "Accounts": "Comptes", "Technical": "Technique", "Status": "Statut", "Open or reviewing": "Ouvert ou en revue", "Critical/high": "Critique/eleve", "Resolved/dismissed": "Resolu/classe", "Include transaction detail": "Inclure le detail des transactions", "Generate a report from current alerts, support messages, rides, payments, accounts, and audit data.": "Generer un rapport depuis les alertes, messages support, courses, paiements, comptes et donnees d'audit actuels.", "0 report records": "0 enregistrement de rapport", "Messages and broadcasts": "Messages et diffusions", "Broadcast notification": "Diffuser une notification", "Audience": "Audience", "Passengers and riders": "Passagers et conducteurs", "Passengers only": "Passagers seulement", "Riders only": "Conducteurs seulement", "Title": "Titre", "Delivery": "Livraison", "In-app notice": "Avis dans l'app", "Phone push notification": "Notification push telephone", "SMS text": "Texte SMS", "Send broadcast": "Envoyer la diffusion", "Broadcasts create one notification per matching passenger or rider and are audit logged.": "Les diffusions creent une notification par passager ou conducteur correspondant et sont journalisees.", "0 messages": "0 message", "Admin audit log": "Journal d'audit admin", "0 audit rows": "0 ligne d'audit", "Support inbox: safety and user messages": "Boite support: securite et messages utilisateurs", "0 support items": "0 element support", "Contact Waka reason": "Motif de contact Waka", "General support question": "Question generale de support", "Fare or payment issue": "Probleme de prix ou paiement", "Unsafe route or pickup/drop-off issue": "Itineraire ou depart/arrivee dangereux", "Rider or passenger no-show": "Conducteur ou passager absent", "Wrong person or vehicle": "Mauvaise personne ou vehicule", "Urgent safety concern": "Probleme urgent de securite", "Harassment or threats": "Harcelement ou menaces", "Payment pressure or off-app demand": "Pression de paiement ou demande hors app", "Document or credential concern": "Probleme de document ou justificatif", "Severity": "Gravite", "Medium": "Moyen", "High": "Eleve", "Low": "Faible", "Send to Waka": "Envoyer a Waka", "Contact Waka opens after a rider is selected.": "Contacter Waka s'ouvre apres selection d'un conducteur.", "Overall": "General", "5 - excellent": "5 - excellent", "4 - good": "4 - bien", "3 - okay": "3 - correct", "2 - poor": "2 - mauvais", "1 - serious issue": "1 - probleme grave", "Pickup timing": "Ponctualite de prise en charge", "Communication": "Communication", "Private note to Waka": "Note privee a Waka", "Submit rating": "Envoyer la note", "Ratings open after the ride is marked complete.": "Les notes s'ouvrent apres que la course est marquee terminee." } }; function englishStaticTextKey(sourceText) { if (textTranslationKeys[sourceText]) return textTranslationKeys[sourceText]; return Object.entries(translations.en ?? {}).find(([, value]) => value === sourceText)?.[0] ?? ""; } function hasStaticTextTranslation(sourceText) { return Boolean(englishStaticTextKey(sourceText)) || Object.values(staticTextTranslations).some((dictionary) => ( Object.prototype.hasOwnProperty.call(dictionary, sourceText) )); } function translatedStaticText(sourceText) { return staticTextTranslations[state.language]?.[sourceText] ?? sourceText; } function translatedValue(key) { const dictionary = translations[state.language] ?? translations.en; return dictionary[key] ?? translations.en[key] ?? ""; } function translatedMessage(key, values = {}) { return translatedValue(key).replace(/\{([a-zA-Z0-9_]+)\}/g, (match, valueKey) => ( values[valueKey] ?? match )); } function translationCoverageFor(language) { const english = translations.en ?? {}; const dictionary = translations[language] ?? {}; const keys = Object.keys(english); const fallbackKeys = language === "en" ? [] : keys.filter((key) => ( !dictionary[key] || (dictionary[key] === english[key] && !translationSameAsEnglishKeys.has(key)) )); const reviewed = keys.length - fallbackKeys.length; return { language, label: languageLabels[language] ?? language.toUpperCase(), reviewed, fallback: fallbackKeys.length, total: keys.length, percent: keys.length ? Math.round((reviewed / keys.length) * 100) : 100, sampleFallbacks: fallbackKeys.slice(0, 4), fallbackKeys }; } function translationCoverageReport() { return Object.keys(translations).map(translationCoverageFor); } function translationCoverageGrid(report) { return `
${report.map((item) => { const ready = item.percent >= productionTranslationTargetPercent; return ` ${escapeHtml(item.label)} ${item.percent}% reviewed - ${item.fallback} fallback key${item.fallback === 1 ? "" : "s"} `; }).join("")}
`; } function setTranslatedStatus(node, key, values = {}) { if (!node) return; node.dataset.i18nDynamic = key; node.dataset.i18nValues = JSON.stringify(values); const text = translatedMessage(key, values); node.dataset.i18nRenderedText = text; node.textContent = text; } const wakaGoodDialogBrand = "WakaGood"; function ensureWakaGoodDialogStyles() { if (document.getElementById("wakaGoodDialogStyles")) return; const style = document.createElement("style"); style.id = "wakaGoodDialogStyles"; style.textContent = ` .wakagood-dialog-backdrop { position: fixed; inset: 0; z-index: 10000; display: grid; place-items: center; padding: 18px; background: rgba(23, 32, 29, 0.42); } .wakagood-dialog { width: min(420px, 100%); border: 1px solid var(--line, #d7e0dc); border-radius: 8px; box-shadow: var(--shadow, 0 18px 42px rgba(23, 32, 29, 0.12)); background: var(--surface, #fff); color: var(--ink, #17201d); overflow: hidden; } .wakagood-dialog-header { display: flex; align-items: center; gap: 10px; padding: 14px 16px; border-bottom: 1px solid var(--line, #d7e0dc); font-weight: 800; } .wakagood-dialog-mark { width: 34px; height: 34px; display: grid; place-items: center; border-radius: 8px; background: var(--green, #0f766e); color: #fff; font-weight: 900; } .wakagood-dialog-body { padding: 16px; white-space: pre-line; line-height: 1.45; color: var(--ink, #17201d); } .wakagood-dialog-input { width: calc(100% - 32px); margin: 0 16px 16px; min-height: 48px; border: 1px solid var(--line, #d7e0dc); border-radius: 8px; padding: 11px 12px; background: #fff; color: var(--ink, #17201d); } .wakagood-dialog-actions { display: flex; justify-content: flex-end; gap: 10px; padding: 0 16px 16px; } .wakagood-dialog-actions button { min-height: 42px; border-radius: 8px; padding: 9px 14px; font-weight: 800; } .wakagood-dialog-cancel { border: 1px solid var(--line, #d7e0dc); background: var(--wash, #f3f7f4); color: var(--green-dark, #0b4f4a); } .wakagood-dialog-confirm { border: 1px solid var(--green, #0f766e); background: var(--green, #0f766e); color: #fff; } `; document.head.append(style); } function showWakaGoodDialog({ message, mode = "alert", defaultValue = "" } = {}) { if (typeof document === "undefined" || !document.body) { if (mode === "confirm") return Promise.resolve(confirm(String(message ?? ""))); if (mode === "prompt") return Promise.resolve(prompt(String(message ?? ""), defaultValue)); alert(String(message ?? "")); return Promise.resolve(true); } ensureWakaGoodDialogStyles(); return new Promise((resolve) => { const backdrop = document.createElement("div"); backdrop.className = "wakagood-dialog-backdrop"; backdrop.setAttribute("role", "presentation"); const dialog = document.createElement("div"); dialog.className = "wakagood-dialog"; dialog.setAttribute("role", "dialog"); dialog.setAttribute("aria-modal", "true"); dialog.setAttribute("aria-labelledby", "wakaGoodDialogTitle"); dialog.setAttribute("aria-describedby", "wakaGoodDialogMessage"); const header = document.createElement("div"); header.className = "wakagood-dialog-header"; const mark = document.createElement("span"); mark.className = "wakagood-dialog-mark"; mark.textContent = "W"; const title = document.createElement("strong"); title.id = "wakaGoodDialogTitle"; title.textContent = wakaGoodDialogBrand; header.append(mark, title); const body = document.createElement("div"); body.className = "wakagood-dialog-body"; body.id = "wakaGoodDialogMessage"; body.textContent = String(message ?? ""); const input = document.createElement("input"); input.className = "wakagood-dialog-input"; input.value = String(defaultValue ?? ""); input.hidden = mode !== "prompt"; input.setAttribute("aria-label", String(message ?? "WakaGood input")); const actions = document.createElement("div"); actions.className = "wakagood-dialog-actions"; const cancel = document.createElement("button"); cancel.type = "button"; cancel.className = "wakagood-dialog-cancel"; cancel.textContent = translatedValue("cancel") || "Cancel"; cancel.hidden = mode === "alert"; const ok = document.createElement("button"); ok.type = "button"; ok.className = "wakagood-dialog-confirm"; ok.textContent = mode === "alert" ? (translatedValue("ok") || "OK") : (translatedValue("continueAction") || "Continue"); actions.append(cancel, ok); dialog.append(header, body); if (mode === "prompt") dialog.append(input); dialog.append(actions); backdrop.append(dialog); function finish(value) { document.removeEventListener("keydown", onKeyDown); backdrop.remove(); resolve(value); } function onKeyDown(event) { if (event.key === "Escape") { event.preventDefault(); finish(mode === "alert" ? true : null); } if (event.key === "Enter" && mode !== "alert") { event.preventDefault(); finish(mode === "prompt" ? input.value : true); } } cancel.addEventListener("click", () => finish(null)); ok.addEventListener("click", () => finish(mode === "prompt" ? input.value : true)); document.addEventListener("keydown", onKeyDown); document.body.append(backdrop); window.setTimeout(() => (mode === "prompt" ? input : ok).focus(), 0); }); } function showWakaGoodAlert(message) { return showWakaGoodDialog({ message, mode: "alert" }); } function showWakaGoodConfirm(message) { return showWakaGoodDialog({ message, mode: "confirm" }); } function showWakaGoodPrompt(message, defaultValue = "") { return showWakaGoodDialog({ message, mode: "prompt", defaultValue }); } function translatedAlert(key, values = {}) { void showWakaGoodAlert(translatedMessage(key, values)); } function translatedConfirm(key, values = {}) { return confirm(translatedMessage(key, values)); } function registerStaticTextTranslations() { const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, { acceptNode(node) { const text = node.nodeValue.trim(); if (!text || (!textTranslationKeys[text] && !hasStaticTextTranslation(text)) || translatedStaticTextNodeSet.has(node)) return NodeFilter.FILTER_REJECT; if (node.parentElement?.closest("script, style, [data-i18n]")) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } }); let node = walker.nextNode(); while (node) { translatedStaticTextNodeSet.add(node); const sourceText = node.nodeValue.trim(); translatedStaticTextNodes.push({ node, key: englishStaticTextKey(sourceText), sourceText }); node = walker.nextNode(); } } function setTranslatedTextNode(node, value) { const leading = node.nodeValue.match(/^\s*/)?.[0] ?? ""; const trailing = node.nodeValue.match(/\s*$/)?.[0] ?? ""; node.nodeValue = `${leading}${value}${trailing}`; } function syncSiteLanguageAfterAppRender() { if (typeof window === "undefined") return; if (typeof window.wakaApplySiteLanguage === "function") { window.wakaApplySiteLanguage(state.language, { source: "app-render", dispatch: false, notifyApp: false }); return; } if (window.wakaSiteLanguage?.decorateLinks) { window.wakaSiteLanguage.decorateLinks(state.language); } } function applyLanguage() { registerStaticTextTranslations(); document.documentElement.lang = state.language; document.documentElement.dir = state.language === "ar" ? "rtl" : "ltr"; document.title = translatedValue("pageTitle"); document.querySelectorAll("[data-i18n]").forEach((node) => { const value = translatedValue(node.dataset.i18n); if (value) node.textContent = value; }); translatedStaticTextNodes.forEach(({ node, key, sourceText }) => { const value = key ? translatedValue(key) : translatedStaticText(sourceText); if (value && node.isConnected) setTranslatedTextNode(node, value); }); document.querySelectorAll("[placeholder]").forEach((node) => { const original = node.dataset.i18nPlaceholder || placeholderTranslationKeys[node.getAttribute("placeholder")]; if (!original) return; node.dataset.i18nPlaceholder = original; const value = translatedValue(original); if (value) node.setAttribute("placeholder", value); }); document.querySelectorAll("[data-i18n-dynamic]").forEach((node) => { if (node.dataset.i18nRenderedText && node.textContent !== node.dataset.i18nRenderedText) { delete node.dataset.i18nDynamic; delete node.dataset.i18nValues; delete node.dataset.i18nRenderedText; return; } let values = {}; try { values = JSON.parse(node.dataset.i18nValues || "{}"); } catch { values = {}; } const value = translatedMessage(node.dataset.i18nDynamic, values); if (value) { node.dataset.i18nRenderedText = value; node.textContent = value; } }); updateInstallButton(); syncSiteLanguageAfterAppRender(); } if (typeof window !== "undefined") { window.wakaApplyAppLanguage = applyLanguage; } // Browser state, persistence scrubbing, lookup indexes, and runtime hardening. const persistedPassengerWorkspacePages = ["request", "trips", "payment", "business", "rewards", "profile", "notices", "support"]; const persistedRiderWorkspacePages = ["overview", "initialize", "checks", "requests", "destination", "earnings", "payment", "ratings", "rewards", "notices", "support", "profile"]; const defaultState = { activeTab: "passenger", showRoleEntry: true, accountMode: { passenger: "signin", rider: "signin" }, passwordReset: { role: "", active: false, startedAt: null, phoneOtpSentAt: null, phoneOtpMaskedPhone: "", phoneOtpVerified: false, phoneOtpVerifiedAt: null, recoverySessionUserId: null, recoverySessionActivatedAt: null }, passengerPage: "request", passengerFareMode: "negotiable", riderPage: "overview", workspaceUiMemory: { passenger: null, rider: null }, riderAvailabilityActivated: false, passengerMenuOpen: false, filter: "all", riderDestinationScope: "preferred", riderMarketplaceDestinationFilter: { enabled: false, consent: false, country: "", city: "", area: "", query: "", appliedAt: null }, riderWorkloadMode: "normal", riderDecisionQueue: [], riderNearbyRequestAlertedKeys: [], riderDismissedRequestKeys: [], riderClearedPrePickupCancellationKeys: [], riderNavigationPreferenceOverride: null, notificationPreferences: { passenger: { all: true, ride: true, chat: true, fare: true, admin: true }, rider: { all: true, ride: true, chat: true, fare: true, admin: true } }, pendingProfileRecovery: null, language: "en", verification: { passenger: null, rider: null, passengerSignIn: null, riderSignIn: null }, sessions: { passenger: null, rider: null }, adminSession: null, adminPage: "overview", adminDetail: null, adminIntercityReportAgencyId: "", adminDirectorySearch: "", adminDirectoryRegion: "", adminDirectoryPages: { passengers: 0, riders: 0 }, adminBoardPages: {}, adminTransactionPage: 0, adminSystemEventPage: 0, adminMessagesPage: 0, adminSupportInboxPage: 0, adminReferralRewardPage: 0, adminInsuranceTelemetryPage: 0, adminInsuranceTelemetryFilters: { view: "segments", period: "", exportStatus: "", startedFrom: "", startedTo: "" }, selectedRequestId: null, passengerSelectedOfferId: null, passenger: null, rider: null, passengers: [], requests: [], riders: [], demoSeeded: false, paymentRequests: [], paymentAccounts: [], businessAccounts: [], businessSubscriptions: [], intercityAgencies: [], intercityAgencyPayments: [], intercityDepartures: [], intercityBookings: [], intercityAdvertisingRequests: [], intercityBookingMessages: [], intercityAgencyMessages: [], rideSettlements: [], rideTips: [], riderCompletedMileageSegments: [], financeAdjustments: [], riderDayPreferences: [], backgroundChecks: [], taxIdentityReferences: [], taxDocuments: [], rideRatings: [], riderRatingSummary: null, routeChangeRequests: [], routeChangePromptedIds: [], riderDecisionQueue: [], rejectedOfferIds: [], offers: [], chats: [], notifications: [], notificationPopupIds: [], pushSubscriptions: [], supportTickets: [], safetyReports: [] }; const bundledDemoRiderIds = new Set(["rider-amina", "rider-patrick"]); const bundledDemoRequestIds = new Set(["request-akwa-bonaberi", "request-bepanda-makepe"]); const bundledDemoOfferIds = new Set(["offer-amina-akwa", "offer-patrick-bepanda"]); const bundledDemoRiderEmails = new Set(["amina@example.com", "patrick@example.com"]); const bundledDemoRiderPhones = new Set(["237690111222", "237675222333"]); const bundledDemoRiderNationalIds = new Set(["CNI-88210", "CNI-55221"]); const workspaceTabs = ["passenger", "rider", "admin"]; const websiteLanguageStorageKey = "waka-site-language"; const websiteSupportedLanguages = new Set(["en", "fr"]); const notificationPreferenceDefaults = { all: true, ride: true, chat: true, fare: true, admin: true }; let state = normalizeState(loadState()); let storageWriteWarningShown = false; let stateLookupCache = null; const phoneOtpCooldowns = new Map(); let pendingPickupGps = null; let selectedCurrentPickupGps = null; let passengerPickupGpsPromise = null; let passengerPickupGpsWatchId = null; let passengerPickupGpsWatchStartedAt = 0; let passengerPickupGpsWatchFallbackTimer = null; let selectedPickupPlace = null; let selectedDestinationPlace = null; const selectedStopPlaces = new Map(); const destinationPlaceDetailsCache = new Map(); const placeAutocompleteCache = new Map(); let placesAutocompleteRateLimitedUntil = 0; let pickupAutocompleteTimer = null; let pickupAutocompleteSessionToken = null; let pickupAutocompleteRequestId = 0; let destinationAutocompleteTimer = null; let destinationAutocompleteSessionToken = null; let destinationAutocompleteRequestId = 0; let rideStopAutocompleteTimer = null; let rideStopAutocompleteSessionToken = null; let rideStopAutocompleteRequestId = 0; let fareGuidanceTimer = null; let fareGuidanceRequestId = 0; let fareGuidanceInFlightKey = ""; let lastRouteFareGuidance = null; let lastRouteFareGuidanceKey = ""; let lastRouteEstimateError = null; let lastRouteEstimateAttemptKey = ""; let stablePassengerFareGuidance = null; let stablePassengerFareGuidanceKey = ""; let pendingLowFareOverrideKey = ""; const riderInitiatedRideCancellationRequestIds = new Set(); const passengerInitiatedRideMatchRequestIds = new Set(); let useCurrentPickupActivationInFlight = false; let passengerApproachRefreshTimer = null; let riderMarketplaceRefreshTimer = null; let riderNearbyAlertActiveId = null; let riderGpsWatchId = null; let riderGpsHeartbeatTimer = null; let riderScreenWakeLock = null; let riderScreenWakeLockRetryAfter = 0; const riderScreenWakeLockRetryDelayMs = 10 * 1000; let riderAutoGpsPaused = !state.riderAvailabilityActivated; let riderAutoGpsSyncPromise = null; let lastRiderAutoGpsSyncAt = 0; let lastRiderAutoGpsSyncPoint = null; let deferredInstallPrompt = null; let locationUpdateRpcUnavailable = { passenger: false, rider: false, liveGps: false, clearLiveGps: false }; let lastLocationUpdateSource = "not used"; let profileOnboardingRpcUnavailable = { profile: false, photo: false, riderApplication: false, riderComplianceRenewal: false }; let lastProfileOnboardingSource = "not used"; function storedWebsiteLanguage() { try { const language = String(localStorage.getItem(websiteLanguageStorageKey) || "").toLowerCase().split("-")[0]; return websiteSupportedLanguages.has(language) ? language : ""; } catch { return ""; } } function urlWebsiteLanguage() { try { const search = typeof window !== "undefined" ? window.location?.search : ""; const language = String(new URLSearchParams(search || "").get("lang") || "").toLowerCase().split("-")[0]; return websiteSupportedLanguages.has(language) ? language : ""; } catch { return ""; } } function requestedWebsiteLanguage() { return urlWebsiteLanguage() || storedWebsiteLanguage(); } function syncWebsiteLanguageStorage(language) { const normalized = String(language || "").toLowerCase().split("-")[0]; if (!websiteSupportedLanguages.has(normalized)) return; try { localStorage.setItem(websiteLanguageStorageKey, normalized); } catch { // Best effort only. The app can still run with its own in-memory state. } } function loadState() { try { const saved = JSON.parse(localStorage.getItem(storageKey)); const websiteLanguage = requestedWebsiteLanguage(); const loadedState = saved ? { ...defaultState, ...saved } : structuredClone(defaultState); if (websiteLanguage) loadedState.language = websiteLanguage; return loadedState; } catch { const fallbackState = structuredClone(defaultState); const websiteLanguage = requestedWebsiteLanguage(); if (websiteLanguage) fallbackState.language = websiteLanguage; return fallbackState; } } const storageMinimalAccountKeys = new Set([ "id", "supabaseUserId", "preferredLanguage", "country", "city", "area", "vehicle", "vehicleDesignation", "navigationPreference", "carBodyType", "status", "approvedAt", "trialEndsAt", "subscriptionPaidUntil", "rating", "backgroundCheckStatus", "backgroundCheckDecision", "createdAt" ]); function minimizedPrivateStatePatch() { return { adminDetail: null, adminPage: "overview", adminDirectorySearch: "", adminDirectoryRegion: "", adminDirectoryPages: { passengers: 0, riders: 0 }, adminBoardPages: {}, adminTransactionPage: 0, adminSystemEventPage: 0, adminMessagesPage: 0, adminSupportInboxPage: 0, adminReferralRewardPage: 0, adminInsuranceTelemetryPage: 0, adminInsuranceTelemetryFilters: { view: "segments", period: "", exportStatus: "", startedFrom: "", startedTo: "" }, requests: [], paymentRequests: [], paymentAccounts: [], businessAccounts: [], businessSubscriptions: [], intercityAgencies: [], intercityAgencyPayments: [], intercityDepartures: [], intercityBookings: [], intercityAdvertisingRequests: [], intercityBookingMessages: [], intercityAgencyMessages: [], rideSettlements: [], rideTips: [], riderCompletedMileageSegments: [], financeAdjustments: [], riderDayPreferences: [], backgroundChecks: [], taxIdentityReferences: [], taxDocuments: [], rideRatings: [], riderRatingSummary: null, offers: [], chats: [], notifications: [], supportTickets: [], safetyReports: [] }; } function shouldMinimizeStoredProfileData() { return appConfig.mode === "supabase" || strictProductionModeEnabled(); } function minimalStorageAccount(record) { return Object.fromEntries( Object.entries(record).filter(([key, value]) => storageMinimalAccountKeys.has(key) && value !== undefined) ); } function storageSafeAccount(record, options = {}) { if (!record || typeof record !== "object") return record ?? null; const copy = { ...record }; delete copy.password; delete copy.passcode; delete copy.code; delete copy.access_token; delete copy.refresh_token; return options.minimizeProfileData ? minimalStorageAccount(copy) : copy; } function storageSafeAccounts(records = [], options = {}) { return Array.isArray(records) ? records.map((record) => storageSafeAccount(record, options)).filter(Boolean) : []; } function storageSafeVerification(verification, options = {}) { if (options.minimizeProfileData) return null; if (!verification?.verifiedAt) return null; const phone = verification.phone ?? verification.verifiedPhone ?? ""; if (!phone) return null; return { phone, phoneDigits: verification.phoneDigits ?? phoneDigits(phone), verifiedPhone: verification.verifiedPhone ?? phone, verifiedAt: verification.verifiedAt, userId: verification.userId ?? null, provider: verification.provider ?? "unknown" }; } function storageSafeSession(session, options = {}) { if (!session) return null; if (options.minimizeProfileData) { const userId = session.userId ?? null; return userId ? { userId, signedInAt: session.signedInAt ?? null } : null; } const safeSession = { phone: session.phone ?? "", email: session.email ?? "", userId: session.userId ?? null, signedInAt: session.signedInAt ?? null }; return safeSession.phone || safeSession.email || safeSession.userId ? safeSession : null; } function workspaceUiRoleConfig(role) { if (role === "passenger") { return { pageKey: "passengerPage", selectedPage: "trips", pages: persistedPassengerWorkspacePages }; } if (role === "rider") { return { pageKey: "riderPage", selectedPage: "requests", pages: persistedRiderWorkspacePages }; } return null; } function workspaceUiAccountKey(role, account = state?.[role], session = state?.sessions?.[role]) { const value = account?.supabaseUserId ?? account?.id ?? session?.userId ?? account?.email ?? session?.email ?? account?.phone ?? session?.phone ?? ""; return String(value ?? "").trim().toLowerCase(); } function normalizeWorkspaceUiMemoryEntry(role, entry = null) { const config = workspaceUiRoleConfig(role); if (!config || !entry || typeof entry !== "object") return null; const page = String(entry.page ?? "").trim().toLowerCase(); if (!config.pages.includes(page)) return null; const selectedRequestId = typeof entry.selectedRequestId === "string" && entry.selectedRequestId.trim() ? entry.selectedRequestId.trim() : null; const accountKey = String(entry.accountKey ?? "").trim().toLowerCase(); const updatedAt = String(entry.updatedAt ?? "").trim(); return { accountKey, page, selectedRequestId, updatedAt: updatedAt && !Number.isNaN(new Date(updatedAt).getTime()) ? updatedAt : null }; } function normalizeWorkspaceUiMemory(value = {}) { const source = value && typeof value === "object" ? value : {}; return { passenger: normalizeWorkspaceUiMemoryEntry("passenger", source.passenger), rider: normalizeWorkspaceUiMemoryEntry("rider", source.rider) }; } function selectedRequestIdForWorkspaceMemory(role, page, explicitValue) { if (explicitValue !== undefined) { return typeof explicitValue === "string" && explicitValue.trim() ? explicitValue.trim() : null; } const config = workspaceUiRoleConfig(role); if (!config || page !== config.selectedPage) return null; return typeof state.selectedRequestId === "string" && state.selectedRequestId.trim() ? state.selectedRequestId.trim() : null; } function rememberWorkspaceUiState(role, overrides = {}) { const config = workspaceUiRoleConfig(role); if (!config) return false; const page = String(overrides.page ?? state[config.pageKey] ?? "").trim().toLowerCase(); if (!config.pages.includes(page)) return false; state.workspaceUiMemory = normalizeWorkspaceUiMemory(state.workspaceUiMemory); state.workspaceUiMemory[role] = { accountKey: workspaceUiAccountKey(role), page, selectedRequestId: selectedRequestIdForWorkspaceMemory(role, page, overrides.selectedRequestId), updatedAt: new Date().toISOString() }; return true; } function rememberActiveWorkspaceUiState() { const role = state.activeTab; if (!["passenger", "rider"].includes(role)) return false; if (!state.sessions?.[role] || !state[role]) return false; return rememberWorkspaceUiState(role); } function workspaceUiMemoryForRole(role) { state.workspaceUiMemory = normalizeWorkspaceUiMemory(state.workspaceUiMemory); const entry = state.workspaceUiMemory[role]; if (!entry) return null; const accountKey = workspaceUiAccountKey(role); if (entry.accountKey && (!accountKey || entry.accountKey !== accountKey)) return null; return entry; } function availableWorkspacePagesForUiMemory(role) { if (role === "passenger" && typeof availablePassengerWorkspacePages === "function") { return availablePassengerWorkspacePages(); } if (role === "rider" && typeof availableRiderWorkspacePages === "function") { return availableRiderWorkspacePages(typeof currentRiderRecord === "function" ? currentRiderRecord() : undefined); } return workspaceUiRoleConfig(role)?.pages ?? []; } function restoreWorkspaceUiState(role, { replaceRoute = true, preferPathRoute = true } = {}) { const config = workspaceUiRoleConfig(role); const entry = workspaceUiMemoryForRole(role); if (!config || !entry) return false; const availablePages = availableWorkspacePagesForUiMemory(role); let page = entry.page; const selectedRequestId = entry.selectedRequestId; if (selectedRequestId) page = config.selectedPage; if (!availablePages.includes(page)) return false; state.activeTab = role; state.showRoleEntry = false; state[config.pageKey] = page; state.selectedRequestId = selectedRequestId; if (role === "passenger") { if (typeof passengerWorkspacePageSelectedInSession !== "undefined") passengerWorkspacePageSelectedInSession = true; if (typeof updatePassengerWorkspaceRoute === "function") { updatePassengerWorkspaceRoute(page, { replace: replaceRoute, requestId: selectedRequestId ?? "", preferPathRoute }); } } if (role === "rider" && typeof updateRiderWorkspaceRoute === "function") { updateRiderWorkspaceRoute(page, { replace: replaceRoute, requestId: selectedRequestId ?? "" }); } return true; } function storageSafePendingProfileRecovery(recovery) { if (!recovery || typeof recovery !== "object") return null; const role = recovery.role === "rider" ? "rider" : recovery.role === "passenger" ? "passenger" : ""; const email = String(recovery.email ?? "").trim().toLowerCase(); if (!role || !email) return null; return { role, email, userId: recovery.userId ?? null, phone: recovery.phone ?? "", name: recovery.name ?? "", startedAt: recovery.startedAt ?? new Date().toISOString() }; } function storageSafePasswordReset(reset) { if (!reset || typeof reset !== "object") return { role: "", active: false, startedAt: null }; const role = reset.role === "rider" ? "rider" : reset.role === "passenger" ? "passenger" : ""; const active = role && reset.active === true; return { role: active ? role : "", active: Boolean(active), startedAt: active ? reset.startedAt || new Date().toISOString() : null, phoneOtpSentAt: active ? reset.phoneOtpSentAt || null : null, phoneOtpMaskedPhone: active ? String(reset.phoneOtpMaskedPhone || "") : "", phoneOtpVerified: false, phoneOtpVerifiedAt: null, recoverySessionUserId: active ? reset.recoverySessionUserId || null : null, recoverySessionActivatedAt: active ? reset.recoverySessionActivatedAt || null : null }; } function storageSafeAdminSession(session) { if (session?.source !== "demo" || !session.email || !demoAdminSignInAllowed()) return null; return { email: session.email, source: "demo", signedInAt: session.signedInAt ?? new Date().toISOString() }; } function storageSafeAdminPageKey(pageKey) { const normalized = String(pageKey ?? "").trim().toLowerCase(); if (normalized === "payments" || normalized === "payment" || normalized === "finance") return "accounting"; if (normalized === "support" || normalized === "support-inbox") return "safety"; if (normalized === "tax" || normalized === "checks") return "compliance"; return normalized; } function stateForStorage(options = {}) { const minimizeProfileData = options.minimizeProfileData ?? shouldMinimizeStoredProfileData(); const storageOptions = { minimizeProfileData }; const safeAdminSession = minimizeProfileData ? null : storageSafeAdminSession(state.adminSession); return { ...state, ...(minimizeProfileData ? minimizedPrivateStatePatch() : {}), verification: { passenger: storageSafeVerification(state.verification?.passenger, storageOptions), rider: storageSafeVerification(state.verification?.rider, storageOptions), passengerSignIn: null, riderSignIn: null }, sessions: { passenger: storageSafeSession(state.sessions?.passenger, storageOptions), rider: storageSafeSession(state.sessions?.rider, storageOptions) }, adminSession: safeAdminSession, adminDetail: safeAdminSession ? state.adminDetail : null, passenger: storageSafeAccount(state.passenger, storageOptions), rider: storageSafeAccount(state.rider, storageOptions), passengers: storageSafeAccounts(minimizeProfileData ? [state.passenger].filter(Boolean) : state.passengers, storageOptions), riders: storageSafeAccounts(minimizeProfileData ? [state.rider].filter(Boolean) : state.riders, storageOptions) }; } function minimizeRuntimeProfileState() { const storageOptions = { minimizeProfileData: true }; state.verification = { passenger: null, rider: null, passengerSignIn: null, riderSignIn: null }; state.sessions = { passenger: storageSafeSession(state.sessions?.passenger, storageOptions), rider: storageSafeSession(state.sessions?.rider, storageOptions) }; Object.assign(state, minimizedPrivateStatePatch()); state.passenger = storageSafeAccount(state.passenger, storageOptions); state.rider = storageSafeAccount(state.rider, storageOptions); state.passengers = storageSafeAccounts([state.passenger].filter(Boolean), storageOptions); state.riders = storageSafeAccounts([state.rider].filter(Boolean), storageOptions); clearStateLookupIndexes(); } function hardenStateForRuntime() { const safeAdminSession = storageSafeAdminSession(state.adminSession); let shouldRewriteStoredState = shouldMinimizeStoredProfileData(); if (shouldRewriteStoredState) minimizeRuntimeProfileState(); if (!safeAdminSession) { shouldRewriteStoredState ||= Boolean(state.adminSession || state.adminDetail); state.adminSession = null; state.adminDetail = null; if (typeof resetAdminData === "function") resetAdminData(); } else { state.adminSession = safeAdminSession; } if (shouldRewriteStoredState) saveState(); } function normalizeRiderMarketplaceDestinationFilter(value = {}) { const source = value && typeof value === "object" ? value : {}; const filter = { enabled: source.enabled === true, consent: source.consent === true, country: String(source.country ?? "").trim(), city: String(source.city ?? "").trim(), area: String(source.area ?? "").trim(), query: String(source.query ?? "").trim(), appliedAt: null }; if (filter.country && !countryCities[filter.country]) filter.country = ""; if (filter.country && filter.city && !countries[filter.country]?.[filter.city]) filter.city = ""; if (filter.country && filter.city && filter.area) { const areaExists = (countries[filter.country]?.[filter.city] ?? []).some((area) => area.name === filter.area); if (!areaExists) filter.area = ""; } const appliedAt = String(source.appliedAt ?? "").trim(); if (appliedAt && !Number.isNaN(new Date(appliedAt).getTime())) filter.appliedAt = appliedAt; if (!filter.consent || !(filter.country || filter.city || filter.area || filter.query)) filter.enabled = false; return filter; } function normalizeNotificationPreferenceSet(value = {}) { const source = value && typeof value === "object" ? value : {}; return Object.fromEntries( Object.entries(notificationPreferenceDefaults).map(([key, fallback]) => [key, source[key] !== false && fallback]) ); } function normalizeNotificationPreferences(value = {}) { const source = value && typeof value === "object" ? value : {}; return { passenger: normalizeNotificationPreferenceSet(source.passenger), rider: normalizeNotificationPreferenceSet(source.rider) }; } function normalizeMarketplaceFareChange(value = null) { const source = value && typeof value === "object" ? value : {}; const direction = source.direction === "down" ? "down" : source.direction === "up" ? "up" : ""; const amount = Number(source.amount); const previousFare = Number(source.previousFare ?? source.previous_fare); const currentFare = Number(source.currentFare ?? source.current_fare); const changedAt = String(source.changedAt ?? source.changed_at ?? "").trim(); if (!direction || !Number.isFinite(amount) || amount <= 0 || !Number.isFinite(previousFare) || !Number.isFinite(currentFare)) { return null; } return { direction, amount, previousFare, currentFare, changedAt: changedAt && !Number.isNaN(new Date(changedAt).getTime()) ? changedAt : new Date().toISOString() }; } function normalizeAdminInsuranceTelemetryFilters(value = {}) { const source = value && typeof value === "object" ? value : {}; return { view: ["daily", "segments"].includes(String(source.view ?? "").trim()) ? String(source.view).trim() : "segments", period: ["", "all", "p1_app_open", "p2_match_accepted", "p3_passenger_in_car"].includes(String(source.period ?? "").trim()) ? String(source.period ?? "").trim() : "", exportStatus: ["", "all", "pending", "exported", "excluded"].includes(String(source.exportStatus ?? "").trim()) ? String(source.exportStatus ?? "").trim() : "", startedFrom: /^\d{4}-\d{2}-\d{2}$/.test(String(source.startedFrom ?? "").trim()) ? String(source.startedFrom).trim() : "", startedTo: /^\d{4}-\d{2}-\d{2}$/.test(String(source.startedTo ?? "").trim()) ? String(source.startedTo).trim() : "" }; } function normalizeRiderDecisionQueue(value = []) { return (Array.isArray(value) ? value : []) .filter((item) => item && typeof item === "object" && item.requestId) .map((item) => { const createdAt = String(item.createdAt ?? new Date().toISOString()); return { id: String(item.id || ["rider-decision", item.riderId, item.requestId, item.eventType, createdAt].filter(Boolean).join(":")), riderId: String(item.riderId ?? ""), requestId: String(item.requestId), title: String(item.title ?? "Ride update"), body: String(item.body ?? ""), eventType: String(item.eventType ?? "ride_update"), fareOffer: Number.isFinite(Number(item.fareOffer)) ? Number(item.fareOffer) : null, createdAt }; }) .slice(-25); } function normalizeState(nextState) { nextState.language ||= "en"; nextState.showRoleEntry = nextState.showRoleEntry !== false; if (!["all", "car"].includes(nextState.filter)) nextState.filter = "all"; nextState.riderDestinationScope = nextState.riderDestinationScope === "all" ? "all" : "preferred"; nextState.riderMarketplaceDestinationFilter = normalizeRiderMarketplaceDestinationFilter(nextState.riderMarketplaceDestinationFilter); nextState.riderWorkloadMode = ["normal", "focus"].includes(nextState.riderWorkloadMode) ? nextState.riderWorkloadMode : "normal"; nextState.riderDecisionQueue = normalizeRiderDecisionQueue(nextState.riderDecisionQueue); nextState.riderNearbyRequestAlertedKeys = Array.isArray(nextState.riderNearbyRequestAlertedKeys) ? nextState.riderNearbyRequestAlertedKeys.filter(Boolean).slice(-200) : []; nextState.riderDismissedRequestKeys = Array.isArray(nextState.riderDismissedRequestKeys) ? nextState.riderDismissedRequestKeys.filter(Boolean).slice(-300) : []; nextState.riderClearedPrePickupCancellationKeys = Array.isArray(nextState.riderClearedPrePickupCancellationKeys) ? nextState.riderClearedPrePickupCancellationKeys.filter(Boolean).slice(-100) : []; nextState.riderNavigationPreferenceOverride = nextState.riderNavigationPreferenceOverride && typeof nextState.riderNavigationPreferenceOverride === "object" && ["google_maps", "waze"].includes(String(nextState.riderNavigationPreferenceOverride.value || "").trim().toLowerCase()) ? (() => { const riderId = String(nextState.riderNavigationPreferenceOverride.riderId || "").trim(); const riderIds = [ riderId, ...(Array.isArray(nextState.riderNavigationPreferenceOverride.riderIds) ? nextState.riderNavigationPreferenceOverride.riderIds : []) ] .map((value) => String(value ?? "").trim()) .filter(Boolean) .filter((value, index, values) => values.indexOf(value) === index) .slice(-8); return { riderId: riderIds[0] || riderId, riderIds, value: String(nextState.riderNavigationPreferenceOverride.value).trim().toLowerCase() === "waze" ? "waze" : "google_maps", updatedAt: nextState.riderNavigationPreferenceOverride.updatedAt || null }; })() : null; nextState.pendingProfileRecovery = storageSafePendingProfileRecovery(nextState.pendingProfileRecovery); nextState.notificationPreferences = normalizeNotificationPreferences(nextState.notificationPreferences); nextState.passengerPage = persistedPassengerWorkspacePages.includes(nextState.passengerPage) ? nextState.passengerPage : "request"; nextState.passengerFareMode = normalizePassengerFareMode(nextState.passengerFareMode); nextState.riderPage = persistedRiderWorkspacePages.includes(nextState.riderPage) ? nextState.riderPage : "overview"; nextState.selectedRequestId = typeof nextState.selectedRequestId === "string" && nextState.selectedRequestId.trim() ? nextState.selectedRequestId.trim() : null; nextState.workspaceUiMemory = normalizeWorkspaceUiMemory(nextState.workspaceUiMemory); nextState.riderAvailabilityActivated = nextState.riderAvailabilityActivated === true; nextState.accountMode ||= { passenger: "signin", rider: "signin" }; nextState.accountMode.passenger = ["signin", "create"].includes(nextState.accountMode.passenger) ? nextState.accountMode.passenger : "signin"; nextState.accountMode.rider = ["signin", "create"].includes(nextState.accountMode.rider) ? nextState.accountMode.rider : "signin"; nextState.passwordReset = storageSafePasswordReset(nextState.passwordReset); nextState.verification ||= { passenger: null, rider: null }; nextState.verification.passenger = storageSafeVerification(nextState.verification.passenger); nextState.verification.rider = storageSafeVerification(nextState.verification.rider); nextState.verification.passengerSignIn = null; nextState.verification.riderSignIn = null; nextState.sessions ||= { passenger: null, rider: null }; nextState.sessions.passenger = storageSafeSession(nextState.sessions.passenger); nextState.sessions.rider = storageSafeSession(nextState.sessions.rider); nextState.adminSession = storageSafeAdminSession(nextState.adminSession); nextState.adminDetail = nextState.adminSession ? nextState.adminDetail : null; const normalizedAdminPage = storageSafeAdminPageKey(nextState.adminPage); nextState.adminPage = nextState.adminSession && ["overview", "alerts", "geography", "activity", "reports", "controls", "messages", "intercity", "intercity-reports", "agency-approvals", "accounting", "telemetry", "rewards", "compliance", "approvals", "passengers", "riders", "safety", "audit", "diagnostics", "account"].includes(normalizedAdminPage) ? normalizedAdminPage : "overview"; nextState.adminIntercityReportAgencyId = typeof nextState.adminIntercityReportAgencyId === "string" ? nextState.adminIntercityReportAgencyId.trim() : ""; nextState.adminDirectorySearch ||= ""; nextState.adminDirectoryRegion ||= ""; nextState.adminDirectoryPages ||= { passengers: 0, riders: 0 }; nextState.adminDirectoryPages.passengers ||= 0; nextState.adminDirectoryPages.riders ||= 0; nextState.adminBoardPages ||= {}; Object.entries(nextState.adminBoardPages).forEach(([key, value]) => { nextState.adminBoardPages[key] = Math.max(0, Number(value) || 0); }); nextState.adminTransactionPage = Math.max(0, Number(nextState.adminTransactionPage) || 0); nextState.adminSystemEventPage = Math.max(0, Number(nextState.adminSystemEventPage) || 0); nextState.adminMessagesPage = Math.max(0, Number(nextState.adminMessagesPage) || 0); nextState.adminSupportInboxPage = Math.max(0, Number(nextState.adminSupportInboxPage) || 0); nextState.adminReferralRewardPage = Math.max(0, Number(nextState.adminReferralRewardPage) || 0); nextState.adminInsuranceTelemetryPage = Math.max(0, Number(nextState.adminInsuranceTelemetryPage) || 0); nextState.adminInsuranceTelemetryFilters = normalizeAdminInsuranceTelemetryFilters(nextState.adminInsuranceTelemetryFilters); nextState.passengerSelectedOfferId = typeof nextState.passengerSelectedOfferId === "string" ? nextState.passengerSelectedOfferId : null; nextState.passenger = storageSafeAccount(nextState.passenger); nextState.rider = storageSafeAccount(nextState.rider); nextState.passengers = storageSafeAccounts(nextState.passengers ?? []); if (nextState.passenger && !nextState.passengers.some((passenger) => passenger.id === nextState.passenger.id)) { nextState.passengers.unshift(nextState.passenger); } nextState.riders = storageSafeAccounts(nextState.riders ?? []); nextState.requests ||= []; nextState.riders = nextState.riders.map((rider) => ({ ...storageSafeAccount(rider), vehicle: normalizeRideVehicle(rider.vehicle), carBodyType: normalizeRideVehicle(rider.vehicle) === "bike" ? "motorbike" : normalizeCarBodyType(rider.carBodyType ?? rider.car_body_type) })); nextState.requests = nextState.requests.map((request) => ({ ...request, businessAccountId: request.businessAccountId ?? request.business_account_id ?? null, vehicle: normalizeRideVehicle(request.vehicle ?? request.vehicle_preference), carTypePreference: normalizeCarTypePreference(request.carTypePreference ?? request.car_type_preference), fareMode: normalizePassengerFareMode(request.fareMode ?? request.fare_mode), rideStops: normalizeRideStops(request.rideStops ?? request.ride_stops), rideStopPoints: normalizeRideStopPoints(request.rideStopPoints ?? request.ride_stop_points, request.rideStops ?? request.ride_stops), passengerCount: normalizeRidePassengerCount(request.passengerCount ?? request.passenger_count), luggageCount: normalizeRideLuggageCount(request.luggageCount ?? request.luggage_count), luggageNote: normalizeRideLuggageNote(request.luggageNote ?? request.luggage_note), currentStopIndex: Math.max(0, Number(request.currentStopIndex ?? request.current_stop_index ?? 0) || 0), lastStopArrivedAt: request.lastStopArrivedAt ?? request.last_stop_arrived_at ?? null, estimatedDistanceMiles: request.estimatedDistanceMiles ?? request.estimated_distance_miles ?? null, estimatedTravelMinutes: request.estimatedTravelMinutes ?? request.estimated_travel_minutes ?? null, routeEstimateSource: request.routeEstimateSource ?? request.route_estimate_source ?? null, routeEstimateProvider: request.routeEstimateProvider ?? request.route_estimate_provider ?? null, routeEstimateCached: Boolean(request.routeEstimateCached ?? request.route_estimate_cached), routeEstimateKey: request.routeEstimateKey ?? request.route_estimate_key ?? null, routeEstimatePolyline: request.routeEstimatePolyline ?? request.route_estimate_polyline ?? null, routeEstimateDestinationFingerprint: request.routeEstimateDestinationFingerprint ?? request.route_estimate_destination_fingerprint ?? null, routeEstimateCreatedAt: request.routeEstimateCreatedAt ?? request.route_estimate_created_at ?? null, fareHistory: Array.isArray(request.fareHistory ?? request.fare_history) ? (request.fareHistory ?? request.fare_history) : [], marketplaceFareChange: normalizeMarketplaceFareChange(request.marketplaceFareChange ?? request.marketplace_fare_change), acceptedRouteChangeFare: Number(request.acceptedRouteChangeFare ?? request.accepted_route_change_fare ?? 0) || 0, arrivedAt: request.arrivedAt ?? request.arrived_at ?? null, startedAt: request.startedAt ?? request.started_at ?? null, completedAt: request.completedAt ?? request.completed_at ?? null, cancellationFeeAmount: request.cancellationFeeAmount ?? request.cancellation_fee_amount ?? 0, cancellationFeeCurrency: request.cancellationFeeCurrency ?? request.cancellation_fee_currency ?? moneyCurrencyForCountry(request.country), cancellationFeeStatus: request.cancellationFeeStatus ?? request.cancellation_fee_status ?? "not_applicable", cancellationFeeRiderId: request.cancellationFeeRiderId ?? request.cancellation_fee_rider_id ?? null, cancellationFeeElapsedMinutes: request.cancellationFeeElapsedMinutes ?? request.cancellation_fee_elapsed_minutes ?? null })); nextState.offers ||= []; nextState.demoSeeded = Boolean(nextState.demoSeeded); stripBundledDemoData(nextState); nextState.chats ||= []; nextState.notifications ||= []; nextState.notificationPopupIds = Array.isArray(nextState.notificationPopupIds) ? nextState.notificationPopupIds.filter(Boolean).slice(-100) : []; nextState.pushSubscriptions ||= []; nextState.paymentRequests ||= []; nextState.paymentAccounts ||= []; nextState.businessAccounts ||= []; nextState.businessSubscriptions ||= []; nextState.intercityAgencies ||= []; nextState.intercityAgencyPayments ||= []; nextState.intercityDepartures ||= []; nextState.intercityBookings ||= []; nextState.intercityAdvertisingRequests ||= []; nextState.intercityBookingMessages ||= []; nextState.intercityAgencyMessages ||= []; nextState.rideSettlements ||= []; nextState.rideTips ||= []; nextState.riderCompletedMileageSegments ||= []; nextState.financeAdjustments ||= []; nextState.riderDayPreferences ||= []; nextState.backgroundChecks ||= []; nextState.taxIdentityReferences ||= []; nextState.taxDocuments ||= []; nextState.rideRatings ||= []; nextState.riderRatingSummary = nextState.riderRatingSummary && typeof nextState.riderRatingSummary === "object" ? nextState.riderRatingSummary : null; nextState.routeChangeRequests = (nextState.routeChangeRequests ?? []).map((change) => ({ ...change, status: ["pending", "accepted", "declined"].includes(change.status) ? change.status : "pending", destinationArea: change.destinationArea ?? change.destination_area ?? null, rideStops: normalizeRideStops(change.rideStops ?? change.ride_stops), rideStopPoints: normalizeRideStopPoints(change.rideStopPoints ?? change.ride_stop_points, change.rideStops ?? change.ride_stops), additionalFare: Number(change.additionalFare ?? 0) || 0, totalFare: Number(change.totalFare ?? 0) || 0 })); nextState.routeChangePromptedIds = Array.isArray(nextState.routeChangePromptedIds) ? nextState.routeChangePromptedIds.filter(Boolean) : []; nextState.rejectedOfferIds = Array.isArray(nextState.rejectedOfferIds) ? nextState.rejectedOfferIds.filter(Boolean).slice(-500) : []; nextState.supportTickets ||= []; nextState.safetyReports ||= []; return nextState; } function saveState() { clearStateLookupIndexes(); rememberActiveWorkspaceUiState(); syncWebsiteLanguageStorage(state.language); try { localStorage.setItem(storageKey, JSON.stringify(stateForStorage())); } catch (error) { if (!storageWriteWarningShown) { logClientWarning("Waka local state could not be saved. Continuing with in-memory state for this session.", error); storageWriteWarningShown = true; } } } function clearStateLookupIndexes() { stateLookupCache = null; } function stateLookupIndexes() { const requests = state.requests ?? []; const riders = state.riders ?? []; const offers = state.offers ?? []; if ( stateLookupCache && stateLookupCache.requests === requests && stateLookupCache.riders === riders && stateLookupCache.offers === offers && stateLookupCache.requestCount === requests.length && stateLookupCache.riderCount === riders.length && stateLookupCache.offerCount === offers.length ) { return stateLookupCache; } const requestMap = new Map(requests.map((request) => [request.id, request])); const riderMap = new Map(riders.map((rider) => [rider.id, rider])); const offerMap = new Map(offers.map((offer) => [offer.id, offer])); const offersByRequestId = new Map(); offers.forEach((offer) => { if (!offersByRequestId.has(offer.requestId)) offersByRequestId.set(offer.requestId, []); offersByRequestId.get(offer.requestId).push(offer); }); offersByRequestId.forEach((requestOffers) => { requestOffers.sort((a, b) => Number(a.fare) - Number(b.fare)); }); stateLookupCache = { requests, riders, offers, requestCount: requests.length, riderCount: riders.length, offerCount: offers.length, requestMap, riderMap, offerMap, offersByRequestId }; return stateLookupCache; } function isBundledDemoRider(record) { const email = String(record?.email ?? "").toLowerCase(); const phone = phoneDigits(record?.phone); const nationalId = String(record?.nationalId ?? record?.national_id_number ?? "").toUpperCase(); return bundledDemoRiderIds.has(record?.id) || bundledDemoRiderEmails.has(email) || bundledDemoRiderPhones.has(phone) || bundledDemoRiderNationalIds.has(nationalId); } function stripBundledDemoData(nextState) { if (isBundledDemoRider(nextState.rider)) { nextState.rider = null; if (nextState.sessions) nextState.sessions.rider = null; } nextState.riders = (nextState.riders ?? []).filter((rider) => !isBundledDemoRider(rider)); nextState.requests = (nextState.requests ?? []).filter((request) => !bundledDemoRequestIds.has(request.id)); nextState.offers = (nextState.offers ?? []).filter((offer) => !bundledDemoOfferIds.has(offer.id) && !bundledDemoRiderIds.has(offer.riderId)); nextState.taxIdentityReferences = (nextState.taxIdentityReferences ?? []).filter((reference) => !bundledDemoRiderIds.has(reference.riderId)); if (bundledDemoRequestIds.has(nextState.selectedRequestId)) nextState.selectedRequestId = null; return nextState; } function upsertById(items, item) { return [item, ...items.filter((existing) => existing.id !== item.id)]; } function makeId(prefix) { return crypto.randomUUID ? crypto.randomUUID() : `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`; } // Shared formatting, validation, DOM handles, routing, and UI utility helpers. function redactClientLogText(value) { return String(value ?? "") .replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[redacted-jwt]") .replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, "[redacted-email]") .replace(/\+?\d[\d\s().-]{7,}\d/g, "[redacted-phone]") .replace(/\b(access_token|refresh_token|authorization|apikey|api[_-]?key|secret|password|client[_-]?secret|card|cvc|setup_intent)=([^&\s"'<>]+)/gi, "$1=[redacted]") .slice(0, 500); } function uiText(key, fallback = "", values = {}) { const translated = typeof translatedMessage === "function" ? translatedMessage(key, values) : typeof translatedValue === "function" ? translatedValue(key) : ""; return translated || fallback; } function safeClientLogDetail(detail) { if (!detail) return detail; if (detail instanceof Error) { return { name: redactClientLogText(detail.name || "Error"), message: redactClientLogText(detail.message || ""), code: redactClientLogText(detail.code || detail.status || "") }; } if (typeof detail === "string" || typeof detail === "number" || typeof detail === "boolean") { return redactClientLogText(detail); } if (typeof detail === "object") { return { name: redactClientLogText(detail.name || detail.error || "ClientError"), message: redactClientLogText(detail.message || detail.msg || detail.error_description || ""), code: redactClientLogText(detail.code || detail.status || detail.statusCode || "") }; } return redactClientLogText(detail); } function clientLogShouldRedact() { return Boolean( (typeof strictProductionModeEnabled === "function" && strictProductionModeEnabled()) || (typeof appConfig === "object" && appConfig?.mode === "supabase") ); } function logClientWarning(message, ...details) { const safeMessage = redactClientLogText(message); const safeDetails = clientLogShouldRedact() ? details.map(safeClientLogDetail) : details; console.warn(safeMessage, ...safeDetails); } const clientRuntimeTelemetryStorageKey = "waka-client-runtime-events-v1"; const clientRuntimeTelemetryMaxStored = 20; const clientRuntimeTelemetryMaxFlush = 5; const clientRuntimeTelemetryFlushCooldownMs = 30 * 1000; const clientRuntimeTelemetryDedupeMs = 60 * 1000; let clientRuntimeTelemetryInstalled = false; let clientRuntimeTelemetryFlushInFlight = false; let clientRuntimeTelemetryLastFlushAt = 0; const clientRuntimeTelemetryRecent = new Map(); function clientEventIngestFunctionName() { return String(appConfig.clientEventIngestFunctionName || "client-event-ingest").trim(); } function clientRuntimeTelemetryEnabled() { return Boolean( typeof window !== "undefined" && appConfig.mode === "supabase" && configFlagEnabled(appConfig.clientErrorTelemetryEnabled ?? true) && hasSupabaseConfig() && clientEventIngestFunctionName() ); } function clientRuntimeTelemetryAuthToken() { return supabaseRestSession?.access_token || ""; } function clientRuntimeTelemetryId() { try { if (window.crypto?.randomUUID) return window.crypto.randomUUID(); } catch {} return `client-${Date.now()}-${Math.random().toString(36).slice(2)}`; } function clientRuntimeTelemetryPath() { try { return window.location?.pathname || "/"; } catch { return "/"; } } function clientRuntimeTelemetryScreen() { try { return `${window.screen?.width || 0}x${window.screen?.height || 0}`; } catch { return ""; } } function clientRuntimeTelemetryQueue() { try { const stored = JSON.parse(localStorage.getItem(clientRuntimeTelemetryStorageKey) || "[]"); return Array.isArray(stored) ? stored.slice(0, clientRuntimeTelemetryMaxStored) : []; } catch { return []; } } function saveClientRuntimeTelemetryQueue(events) { try { localStorage.setItem( clientRuntimeTelemetryStorageKey, JSON.stringify(events.slice(-clientRuntimeTelemetryMaxStored)) ); } catch (error) { logClientWarning("Client runtime telemetry queue could not be saved.", error); } } function forgetClientRuntimeTelemetryEvents(sentIds = []) { if (!sentIds.length) return; const sent = new Set(sentIds); saveClientRuntimeTelemetryQueue(clientRuntimeTelemetryQueue().filter((event) => !sent.has(event.id))); } function clientRuntimeTelemetryMessage(errorLike, fallback = "Client runtime event.") { if (errorLike instanceof Error) return errorLike.message || fallback; if (typeof errorLike === "string") return errorLike; if (typeof errorLike === "object" && errorLike) { return errorLike.message || errorLike.reason || errorLike.error || fallback; } return fallback; } function clientRuntimeTelemetryName(errorLike) { if (errorLike instanceof Error) return errorLike.name || "Error"; if (typeof errorLike === "object" && errorLike) return errorLike.name || errorLike.code || "ClientError"; return "ClientError"; } function clientRuntimeTelemetryRecord(eventType, errorLike, context = {}) { const message = redactClientLogText(clientRuntimeTelemetryMessage(errorLike)); const errorName = redactClientLogText(clientRuntimeTelemetryName(errorLike)); const source = redactClientLogText(context.source || ""); const line = Number.isFinite(Number(context.line)) ? Number(context.line) : null; const column = Number.isFinite(Number(context.column)) ? Number(context.column) : null; const dedupeKey = `${eventType}:${message}:${source}:${line}:${column}`; const now = Date.now(); const recentAt = clientRuntimeTelemetryRecent.get(dedupeKey) || 0; if (now - recentAt < clientRuntimeTelemetryDedupeMs) return null; clientRuntimeTelemetryRecent.set(dedupeKey, now); for (const [key, seenAt] of clientRuntimeTelemetryRecent) { if (now - seenAt > clientRuntimeTelemetryDedupeMs * 2) clientRuntimeTelemetryRecent.delete(key); } return { id: clientRuntimeTelemetryId(), occurredAt: new Date(now).toISOString(), eventType, severity: eventType === "client_warning" ? "warning" : "error", message, metadata: { runtimeRole, activeTab: state?.activeTab || "", path: clientRuntimeTelemetryPath(), visibilityState: document.visibilityState || "", online: navigator.onLine !== false, language: navigator.language || "", screen: clientRuntimeTelemetryScreen(), errorName, errorCode: redactClientLogText(errorLike?.code || errorLike?.status || ""), source, line, column } }; } function enqueueClientRuntimeTelemetry(event) { if (!event || !clientRuntimeTelemetryEnabled()) return; const queue = clientRuntimeTelemetryQueue(); queue.push(event); saveClientRuntimeTelemetryQueue(queue); void flushClientRuntimeTelemetry(); } async function flushClientRuntimeTelemetry({ force = false } = {}) { if (!clientRuntimeTelemetryEnabled()) return; const token = clientRuntimeTelemetryAuthToken(); if (!token) return; const now = Date.now(); if (!force && now - clientRuntimeTelemetryLastFlushAt < clientRuntimeTelemetryFlushCooldownMs) return; if (clientRuntimeTelemetryFlushInFlight) return; const events = clientRuntimeTelemetryQueue().slice(0, clientRuntimeTelemetryMaxFlush); if (!events.length) return; clientRuntimeTelemetryFlushInFlight = true; clientRuntimeTelemetryLastFlushAt = now; try { const response = await fetch(`${appConfig.supabaseUrl}/functions/v1/${clientEventIngestFunctionName()}`, { method: "POST", headers: { "content-type": "application/json", apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${token}` }, body: JSON.stringify({ events }), keepalive: true }); if (!response.ok) throw new Error(`Client event ingest failed with HTTP ${response.status}.`); forgetClientRuntimeTelemetryEvents(events.map((event) => event.id)); } catch (error) { logClientWarning("Client runtime telemetry flush was skipped.", error); } finally { clientRuntimeTelemetryFlushInFlight = false; } } function installClientRuntimeErrorReporting() { if (clientRuntimeTelemetryInstalled || typeof window === "undefined") return; clientRuntimeTelemetryInstalled = true; window.addEventListener("error", (event) => { enqueueClientRuntimeTelemetry(clientRuntimeTelemetryRecord("client_runtime_error", event.error || event.message, { source: event.filename, line: event.lineno, column: event.colno })); }, true); window.addEventListener("unhandledrejection", (event) => { enqueueClientRuntimeTelemetry(clientRuntimeTelemetryRecord("client_unhandled_rejection", event.reason || "Unhandled promise rejection.")); }); window.addEventListener("online", () => { void flushClientRuntimeTelemetry({ force: true }); }); document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") void flushClientRuntimeTelemetry({ force: true }); }); } const riderDocumentLabels = { nationalIdentity: "National Identity card", driverLicense: "Driver's license", vehicleRegistration: "Registration document", insurance: "Insurance document", vehicleInspection: "Vehicle inspection document" }; const riderDocumentRequestKeys = Object.keys(riderDocumentLabels); function emptyRiderDocuments() { return { nationalIdentity: "", driverLicense: "", vehicleRegistration: "", insurance: "", vehicleInspection: "", requestedDocuments: [] }; } function requiredRiderDocuments() { return []; } function parseRiderDocuments(value) { const documents = emptyRiderDocuments(); if (!value) return documents; if (typeof value === "object") { const requestedDocuments = normalizeRequestedRiderDocumentKeys(value.requestedDocuments); return { ...documents, ...value, requestedDocuments }; } const text = String(value).trim(); if (!text) return documents; try { const parsed = JSON.parse(text); if (parsed && typeof parsed === "object") { const requestedDocuments = normalizeRequestedRiderDocumentKeys(parsed.requestedDocuments); return { ...documents, ...parsed, requestedDocuments }; } } catch { return { ...documents, driverLicense: text }; } return { ...documents, driverLicense: text }; } function riderDocuments(rider) { const documents = { ...emptyRiderDocuments(), ...parseRiderDocuments(rider?.documentName), ...parseRiderDocuments(rider?.documents) }; if (rider?.driverLicenseDocumentName) documents.driverLicense = rider.driverLicenseDocumentName; if (rider?.nationalIdentityDocumentName) documents.nationalIdentity = rider.nationalIdentityDocumentName; if (rider?.vehicleRegistrationDocumentName) documents.vehicleRegistration = rider.vehicleRegistrationDocumentName; if (rider?.insuranceDocumentName) documents.insurance = rider.insuranceDocumentName; if (rider?.vehicleInspectionDocumentName) documents.vehicleInspection = rider.vehicleInspectionDocumentName; if (rider?.driverLicenseDocumentPath) documents.driverLicense = rider.driverLicenseDocumentPath; if (rider?.nationalIdentityDocumentPath) documents.nationalIdentity = rider.nationalIdentityDocumentPath; if (rider?.vehicleRegistrationDocumentPath) documents.vehicleRegistration = rider.vehicleRegistrationDocumentPath; if (rider?.insuranceDocumentPath) documents.insurance = rider.insuranceDocumentPath; if (rider?.vehicleInspectionDocumentPath) documents.vehicleInspection = rider.vehicleInspectionDocumentPath; documents.requestedDocuments = normalizeRequestedRiderDocumentKeys(documents.requestedDocuments); return documents; } function riderRequestableDocumentKeys(riderOrVehicle = null) { const vehicle = typeof riderOrVehicle === "string" ? normalizeRideVehicle(riderOrVehicle) : normalizeRideVehicle(riderOrVehicle?.vehicle); return riderDocumentRequestKeys.filter((key) => { if (vehicle === "bike" && ["insurance", "vehicleInspection"].includes(key)) return false; return true; }); } function normalizeRequestedRiderDocumentKeys(value, riderOrVehicle = null) { const allowed = new Set(riderRequestableDocumentKeys(riderOrVehicle)); const raw = Array.isArray(value) ? value : String(value ?? "") .split(/[,\n;]/) .map((item) => item.trim()) .filter(Boolean); return [...new Set(raw.map((key) => String(key).trim()).filter((key) => allowed.has(key)))]; } function riderRequestedDocumentKeys(rider) { return normalizeRequestedRiderDocumentKeys(riderDocuments(rider).requestedDocuments, rider); } function riderRequestedDocumentLabels(rider) { return riderRequestedDocumentKeys(rider).map((key) => riderDocumentLabels[key]); } function riderMissingRequestedDocumentKeys(riderOrDocuments, riderOrVehicle = null) { const documents = riderOrDocuments && ( Object.prototype.hasOwnProperty.call(riderOrDocuments, "documentName") || Object.prototype.hasOwnProperty.call(riderOrDocuments, "documents") || Object.prototype.hasOwnProperty.call(riderOrDocuments, "vehicle") ) ? riderDocuments(riderOrDocuments) : { ...emptyRiderDocuments(), ...(riderOrDocuments || {}) }; const requested = normalizeRequestedRiderDocumentKeys(documents.requestedDocuments, riderOrVehicle ?? riderOrDocuments); return requested.filter((key) => !documents[key]); } function riderMissingRequestedDocumentLabels(riderOrDocuments, riderOrVehicle = null) { return riderMissingRequestedDocumentKeys(riderOrDocuments, riderOrVehicle).map((key) => riderDocumentLabels[key]); } function selectedRiderDocumentFiles() { const vehicle = normalizeRideVehicle(els.riderVehicle?.value); const files = { nationalIdentity: els.riderNationalIdDocument?.files?.[0] ?? null, driverLicense: els.riderLicenseDocument?.files?.[0] ?? null, vehicleRegistration: els.riderRegistrationDocument?.files?.[0] ?? null, insurance: els.riderInsuranceDocument?.files?.[0] ?? null, vehicleInspection: els.riderInspectionDocument?.files?.[0] ?? null }; if (vehicle === "bike") { files.insurance = null; files.vehicleInspection = null; } return files; } function riderDocumentPayload(documents) { return JSON.stringify({ ...emptyRiderDocuments(), ...documents }); } function riderDocumentMetadata(rider) { const documents = riderDocuments(rider); return { vehicleDesignation: normalizeRiderVehicleDesignation(documents.vehicleDesignation ?? rider?.vehicleDesignation, rider?.carBodyType), navigationPreference: riderNavigationPreference(rider) }; } function riderApplicationDocumentPayload(documents, rider) { return riderDocumentPayload({ ...documents, vehicleDesignation: normalizeRiderVehicleDesignation(rider?.vehicleDesignation, rider?.carBodyType), navigationPreference: riderNavigationPreference(rider) }); } function missingRiderDocumentLabels(documents) { return requiredRiderDocuments() .filter((key) => !documents[key]) .map((key) => riderDocumentLabels[key]); } function riderDocumentSummary(rider) { const documents = riderDocuments(rider); const requiredSummary = requiredRiderDocuments() .map((key) => `${riderDocumentLabels[key]}: ${documents[key] || "missing"}`); const optionalSummary = documents.vehicleInspection ? [`${riderDocumentLabels.vehicleInspection}: ${documents.vehicleInspection}`] : ["Vehicle inspection: optional"]; return [...requiredSummary, ...optionalSummary].join(". "); } function inertElement(name = "pruned") { const classList = { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } }; const element = { name, value: "", checked: false, disabled: false, hidden: true, textContent: "", innerHTML: "", className: "", dataset: {}, style: {}, classList, children: [], files: [], options: [], selectedOptions: [], addEventListener() {}, removeEventListener() {}, append() {}, prepend() {}, replaceChildren() {}, reset() {}, focus() {}, click() {}, setAttribute() {}, removeAttribute() {}, getAttribute() { return null; }, matches() { return false; }, closest() { return null; }, querySelector() { return inertElement(`${name}:child`); }, querySelectorAll() { return []; } }; return element; } const els = { roleEntry: document.querySelector("#roleEntry"), workspace: document.querySelector("#workspace"), publicIntercityExperienceMount: document.querySelector("#publicIntercityExperienceMount"), publicAgencyDirectoryList: document.querySelector("#publicAgencyDirectoryList"), publicAgencyPagePanel: document.querySelector("#publicAgencyPagePanel"), publicAgencyPageContent: document.querySelector("#publicAgencyPageContent"), publicIntercityGuidanceGrid: document.querySelector("#publicIntercityGuidanceGrid"), publicIntercityResultsGrid: document.querySelector("#publicIntercityResultsGrid"), roleTabs: document.querySelector("#roleTabs"), connectionStatus: document.querySelector("#connectionStatus"), installApp: document.querySelector("#installApp"), deploymentUpdateNotice: document.querySelector("#deploymentUpdateNotice"), deploymentUpdateTitle: document.querySelector("#deploymentUpdateTitle"), deploymentUpdateMessage: document.querySelector("#deploymentUpdateMessage"), deploymentUpdateNow: document.querySelector("#deploymentUpdateNow"), languageSelect: document.querySelector("#languageSelect"), languageSelects: document.querySelectorAll("[data-language-select]"), passengerSignInForm: document.querySelector("#passengerSignInForm"), passengerSignInEmail: document.querySelector("#passengerSignInEmail"), passengerSignInPassword: document.querySelector("#passengerSignInPassword"), passengerSignInOtpPanel: document.querySelector("#passengerSignInOtpPanel"), passengerSignInPhone: document.querySelector("#passengerSignInPhone"), passengerSignInCode: document.querySelector("#passengerSignInCode"), sendPassengerSignInCode: document.querySelector("#sendPassengerSignInCode"), verifyPassengerSignIn: document.querySelector("#verifyPassengerSignIn"), forgotPassengerPassword: document.querySelector("#forgotPassengerPassword"), passengerPasswordResetPanel: document.querySelector("#passengerPasswordResetPanel"), passengerPasswordResetPhoneStep: document.querySelector("#passengerPasswordResetPhoneStep"), passengerPasswordResetPhoneHint: document.querySelector("#passengerPasswordResetPhoneHint"), passengerPasswordResetPhoneCode: document.querySelector("#passengerPasswordResetPhoneCode"), sendPassengerPasswordResetPhoneOtp: document.querySelector("#sendPassengerPasswordResetPhoneOtp"), verifyPassengerPasswordResetPhoneOtp: document.querySelector("#verifyPassengerPasswordResetPhoneOtp"), passengerPasswordResetPhoneStatus: document.querySelector("#passengerPasswordResetPhoneStatus"), passengerPasswordResetPasswordFields: document.querySelector("#passengerPasswordResetPasswordFields"), passengerResetPassword: document.querySelector("#passengerResetPassword"), passengerResetPasswordConfirm: document.querySelector("#passengerResetPasswordConfirm"), savePassengerResetPassword: document.querySelector("#savePassengerResetPassword"), passengerSignInStatus: document.querySelector("#passengerSignInStatus"), passengerAccountStage: document.querySelector("#passengerAccountStage"), passengerPanelHeading: document.querySelector("#passengerPanelHeading"), passengerWorkspaceHeader: document.querySelector("#passengerWorkspaceHeader"), passengerWorkspaceTitle: document.querySelector("#passengerWorkspaceTitle"), passengerWorkspaceMenu: document.querySelector("#passengerWorkspaceMenu"), passengerWorkspaceMenuToggle: document.querySelector("#passengerWorkspaceMenuToggle"), passengerSessionCard: document.querySelector("#passengerSessionCard"), passengerSessionTitle: document.querySelector("#passengerSessionTitle"), passengerSessionSummary: document.querySelector("#passengerSessionSummary"), passengerProfileAvatar: document.querySelector("#passengerProfileAvatar"), passengerProfilePhotoStatus: document.querySelector("#passengerProfilePhotoStatus"), passengerReferralPanel: document.querySelector("#passengerReferralPanel"), passengerReferralSummary: document.querySelector("#passengerReferralSummary"), passengerReferralCodeDisplay: document.querySelector("#passengerReferralCodeDisplay"), copyPassengerReferralCode: document.querySelector("#copyPassengerReferralCode"), sharePassengerReferralCode: document.querySelector("#sharePassengerReferralCode"), emailPassengerReferralCode: document.querySelector("#emailPassengerReferralCode"), textPassengerReferralCode: document.querySelector("#textPassengerReferralCode"), passengerReferralHowItWorks: document.querySelector("#passengerReferralHowItWorks"), passengerSignOut: document.querySelector("#passengerSignOut"), passengerMenuSignOut: document.querySelector("#passengerMenuSignOut"), passengerPasswordChangeForm: document.querySelector("#passengerPasswordChangeForm"), passengerCurrentPassword: document.querySelector("#passengerCurrentPassword"), passengerNewPassword: document.querySelector("#passengerNewPassword"), passengerNewPasswordConfirm: document.querySelector("#passengerNewPasswordConfirm"), passengerChangePassword: document.querySelector("#passengerChangePassword"), passengerPasswordChangeStatus: document.querySelector("#passengerPasswordChangeStatus"), passengerWorkspaceNav: document.querySelector("#passengerWorkspaceNav"), passengerPaymentForm: document.querySelector("#passengerPaymentForm"), passengerPaymentProvider: document.querySelector("#passengerPaymentProvider"), passengerBankName: document.querySelector("#passengerBankName"), passengerAccountHolder: document.querySelector("#passengerAccountHolder"), passengerAccountLast4: document.querySelector("#passengerAccountLast4"), passengerPaymentReference: document.querySelector("#passengerPaymentReference"), passengerPaymentStatus: document.querySelector("#passengerPaymentStatus"), startPassengerPaymentSetup: document.querySelector("#startPassengerPaymentSetup"), passengerLocationForm: document.querySelector("#passengerLocationForm"), passengerActiveCountry: document.querySelector("#passengerActiveCountry"), passengerActiveCity: document.querySelector("#passengerActiveCity"), passengerLocationStatus: document.querySelector("#passengerLocationStatus"), businessAccountForm: document.querySelector("#businessAccountForm"), businessName: document.querySelector("#businessName"), businessBillingEmail: document.querySelector("#businessBillingEmail"), businessCategory: document.querySelector("#businessCategory"), businessPlan: document.querySelector("#businessPlan"), businessAddress: document.querySelector("#businessAddress"), businessContactName: document.querySelector("#businessContactName"), businessContactPhone: document.querySelector("#businessContactPhone"), businessReferralCode: document.querySelector("#businessReferralCode"), businessReferralPanel: document.querySelector("#businessReferralPanel"), businessReferralSummary: document.querySelector("#businessReferralSummary"), businessReferralCodeDisplay: document.querySelector("#businessReferralCodeDisplay"), copyBusinessReferralCode: document.querySelector("#copyBusinessReferralCode"), shareBusinessReferralCode: document.querySelector("#shareBusinessReferralCode"), emailBusinessReferralCode: document.querySelector("#emailBusinessReferralCode"), textBusinessReferralCode: document.querySelector("#textBusinessReferralCode"), businessReferralHowItWorks: document.querySelector("#businessReferralHowItWorks"), businessAccountStatus: document.querySelector("#businessAccountStatus"), businessAccountList: document.querySelector("#businessAccountList"), agencyPasswordChangeForm: document.querySelector("#agencyPasswordChangeForm"), agencyCurrentPassword: document.querySelector("#agencyCurrentPassword"), agencyNewPassword: document.querySelector("#agencyNewPassword"), agencyNewPasswordConfirm: document.querySelector("#agencyNewPasswordConfirm"), agencyChangePassword: document.querySelector("#agencyChangePassword"), agencyPasswordChangeStatus: document.querySelector("#agencyPasswordChangeStatus"), publicIntercitySearchForm: document.querySelector("#publicIntercitySearchForm"), publicIntercityAgency: document.querySelector("#publicIntercityAgency"), publicIntercityOrigin: document.querySelector("#publicIntercityOrigin"), publicIntercityDestination: document.querySelector("#publicIntercityDestination"), publicIntercityDate: document.querySelector("#publicIntercityDate"), publicIntercitySearchStatus: document.querySelector("#publicIntercitySearchStatus"), publicIntercityCitiesDirectory: document.querySelector("#publicIntercityCitiesDirectory"), publicIntercityAgenciesDirectory: document.querySelector("#publicIntercityAgenciesDirectory"), publicIntercityPromotionsPanel: document.querySelector("#publicIntercityPromotionsPanel"), publicIntercityPromotionList: document.querySelector("#publicIntercityPromotionList"), publicIntercityDepartureList: document.querySelector("#publicIntercityDepartureList"), publicIntercityBookingPanel: document.querySelector("#publicIntercityBookingPanel"), publicIntercitySelectedDeparture: document.querySelector("#publicIntercitySelectedDeparture"), publicIntercityDepartureSummary: document.querySelector("#publicIntercityDepartureSummary"), publicIntercityBookingForm: document.querySelector("#publicIntercityBookingForm"), publicIntercityTravelerName: document.querySelector("#publicIntercityTravelerName"), publicIntercityTravelerEmail: document.querySelector("#publicIntercityTravelerEmail"), publicIntercityTravelerPhone: document.querySelector("#publicIntercityTravelerPhone"), publicIntercityTravelerIdentityNumber: document.querySelector("#publicIntercityTravelerIdentityNumber"), publicIntercitySeatCount: document.querySelector("#publicIntercitySeatCount"), publicIntercitySeatSelectionStatus: document.querySelector("#publicIntercitySeatSelectionStatus"), publicIntercityPassengerManifest: document.querySelector("#publicIntercityPassengerManifest"), publicIntercityPassengerManifestList: document.querySelector("#publicIntercityPassengerManifestList"), publicIntercityBookingTotalPanel: document.querySelector("#publicIntercityBookingTotalPanel"), publicIntercityFarePerSeat: document.querySelector("#publicIntercityFarePerSeat"), publicIntercitySelectedSeatsSummary: document.querySelector("#publicIntercitySelectedSeatsSummary"), publicIntercityTotalDue: document.querySelector("#publicIntercityTotalDue"), publicIntercityPaymentInstructions: document.querySelector("#publicIntercityPaymentInstructions"), publicIntercityPaymentMethod: document.querySelector("#publicIntercityPaymentMethod"), publicIntercityPaymentPhone: document.querySelector("#publicIntercityPaymentPhone"), publicIntercityBookingNotes: document.querySelector("#publicIntercityBookingNotes"), publicIntercitySeatMap: document.querySelector("#publicIntercitySeatMap"), publicIntercityBookingStatus: document.querySelector("#publicIntercityBookingStatus"), intercityAgencyForm: document.querySelector("#intercityAgencyForm"), intercityAgencyName: document.querySelector("#intercityAgencyName"), intercityAgencySlug: document.querySelector("#intercityAgencySlug"), intercityAgencyEmail: document.querySelector("#intercityAgencyEmail"), intercityAgencyPhone: document.querySelector("#intercityAgencyPhone"), intercityAgencyCity: document.querySelector("#intercityAgencyCity"), cameroonIntercityCityOptions: document.querySelector("#cameroonIntercityCityOptions"), intercityAgencyTerminal: document.querySelector("#intercityAgencyTerminal"), intercityAgencyCitiesSelect: document.querySelector("#intercityAgencyCitiesSelect"), intercityAgencyCitiesCustom: document.querySelector("#intercityAgencyCitiesCustom"), intercityAgencyDescription: document.querySelector("#intercityAgencyDescription"), intercityAgencyLogo: document.querySelector("#intercityAgencyLogo"), intercityAgencyLogoAlt: document.querySelector("#intercityAgencyLogoAlt"), intercityAgencyLogoPreview: document.querySelector("#intercityAgencyLogoPreview"), intercityAgencyLogoStatus: document.querySelector("#intercityAgencyLogoStatus"), intercityAgencyMtnName: document.querySelector("#intercityAgencyMtnName"), intercityAgencyMtnNumber: document.querySelector("#intercityAgencyMtnNumber"), intercityAgencyOrangeName: document.querySelector("#intercityAgencyOrangeName"), intercityAgencyOrangeNumber: document.querySelector("#intercityAgencyOrangeNumber"), intercityAgencyStatus: document.querySelector("#intercityAgencyStatus"), intercityAgencyChecklist: document.querySelector("#intercityAgencyChecklist"), intercityAgencyList: document.querySelector("#intercityAgencyList"), intercityOperatorTabs: document.querySelector("#intercityOperatorTabs"), intercityOperatorAgencyFilter: document.querySelector("#intercityOperatorAgencyFilter"), intercityOperatorStatusFilter: document.querySelector("#intercityOperatorStatusFilter"), intercityOperatorSearch: document.querySelector("#intercityOperatorSearch"), intercityOperatorTabSummary: document.querySelector("#intercityOperatorTabSummary"), intercityAgencyPaymentForm: document.querySelector("#intercityAgencyPaymentForm"), intercityAgencyPaymentSelect: document.querySelector("#intercityAgencyPaymentSelect"), intercityAgencyPaymentMonth: document.querySelector("#intercityAgencyPaymentMonth"), intercityAgencyPaymentProvider: document.querySelector("#intercityAgencyPaymentProvider"), intercityAgencyPaymentPayerName: document.querySelector("#intercityAgencyPaymentPayerName"), intercityAgencyPaymentPayerPhone: document.querySelector("#intercityAgencyPaymentPayerPhone"), intercityAgencyPaymentReference: document.querySelector("#intercityAgencyPaymentReference"), intercityAgencyPaymentStatus: document.querySelector("#intercityAgencyPaymentStatus"), intercityAgencyPaymentList: document.querySelector("#intercityAgencyPaymentList"), intercityDepartureForm: document.querySelector("#intercityDepartureForm"), intercityDepartureAgency: document.querySelector("#intercityDepartureAgency"), intercityDepartureBusLabel: document.querySelector("#intercityDepartureBusLabel"), intercityDepartureTravelStatus: document.querySelector("#intercityDepartureTravelStatus"), intercityDepartureOrigin: document.querySelector("#intercityDepartureOrigin"), intercityDepartureDestination: document.querySelector("#intercityDepartureDestination"), intercityDepartureBoarding: document.querySelector("#intercityDepartureBoarding"), intercityDepartureDropoff: document.querySelector("#intercityDepartureDropoff"), intercityDepartureStatusNote: document.querySelector("#intercityDepartureStatusNote"), intercityDepartureAt: document.querySelector("#intercityDepartureAt"), intercityDepartureDuration: document.querySelector("#intercityDepartureDuration"), intercityDepartureSeats: document.querySelector("#intercityDepartureSeats"), intercityDepartureFare: document.querySelector("#intercityDepartureFare"), intercityDepartureBlockedSeats: document.querySelector("#intercityDepartureBlockedSeats"), intercityDepartureNotes: document.querySelector("#intercityDepartureNotes"), intercityDepartureAllowMtn: document.querySelector("#intercityDepartureAllowMtn"), intercityDepartureAllowOrange: document.querySelector("#intercityDepartureAllowOrange"), intercityDepartureAllowPayLater: document.querySelector("#intercityDepartureAllowPayLater"), saveIntercityDeparture: document.querySelector("#saveIntercityDeparture"), intercityDepartureStatus: document.querySelector("#intercityDepartureStatus"), intercityDepartureList: document.querySelector("#intercityDepartureList"), intercityAdvertisingForm: document.querySelector("#intercityAdvertisingForm"), intercityAdvertisingAgency: document.querySelector("#intercityAdvertisingAgency"), intercityAdvertisingCampaignName: document.querySelector("#intercityAdvertisingCampaignName"), intercityAdvertisingPublicTitle: document.querySelector("#intercityAdvertisingPublicTitle"), intercityAdvertisingPublicBody: document.querySelector("#intercityAdvertisingPublicBody"), intercityAdvertisingCtaLabel: document.querySelector("#intercityAdvertisingCtaLabel"), intercityAdvertisingCtaUrl: document.querySelector("#intercityAdvertisingCtaUrl"), intercityAdvertisingImage: document.querySelector("#intercityAdvertisingImage"), intercityAdvertisingImageAlt: document.querySelector("#intercityAdvertisingImageAlt"), intercityAdvertisingStartAt: document.querySelector("#intercityAdvertisingStartAt"), intercityAdvertisingEndAt: document.querySelector("#intercityAdvertisingEndAt"), intercityAdvertisingPublicActive: document.querySelector("#intercityAdvertisingPublicActive"), intercityAdvertisingPlacement: document.querySelector("#intercityAdvertisingPlacement"), intercityAdvertisingDurationDays: document.querySelector("#intercityAdvertisingDurationDays"), intercityAdvertisingAmountXaf: document.querySelector("#intercityAdvertisingAmountXaf"), intercityAdvertisingProvider: document.querySelector("#intercityAdvertisingProvider"), intercityAdvertisingPayerName: document.querySelector("#intercityAdvertisingPayerName"), intercityAdvertisingPayerPhone: document.querySelector("#intercityAdvertisingPayerPhone"), intercityAdvertisingReference: document.querySelector("#intercityAdvertisingReference"), intercityAdvertisingNote: document.querySelector("#intercityAdvertisingNote"), intercityAdvertisingStatus: document.querySelector("#intercityAdvertisingStatus"), intercityAdvertisingList: document.querySelector("#intercityAdvertisingList"), intercityBookingStatus: document.querySelector("#intercityBookingStatus"), intercityBookingList: document.querySelector("#intercityBookingList"), intercityTripReportList: document.querySelector("#intercityTripReportList"), intercityMessageStatus: document.querySelector("#intercityMessageStatus"), intercityMessageList: document.querySelector("#intercityMessageList"), passengerIntercityBookingsPanel: document.querySelector("#passengerIntercityBookingsPanel"), passengerIntercityBookingStatus: document.querySelector("#passengerIntercityBookingStatus"), passengerIntercityBookingList: document.querySelector("#passengerIntercityBookingList"), passengerAgencyTravelPanel: document.querySelector("#passengerAgencyTravelPanel"), passengerAgencyTravelHost: document.querySelector("#passengerAgencyTravelHost"), passengerNoticePanel: document.querySelector("#passengerNoticePanel"), passengerNoticeList: document.querySelector("#passengerNoticeList"), passengerEnablePush: document.querySelector("#passengerEnablePush"), passengerPushStatus: document.querySelector("#passengerPushStatus"), passengerSupportForm: document.querySelector("#passengerSupportForm"), passengerSupportCategory: document.querySelector("#passengerSupportCategory"), passengerSupportSubject: document.querySelector("#passengerSupportSubject"), passengerSupportMessage: document.querySelector("#passengerSupportMessage"), passengerSupportStatus: document.querySelector("#passengerSupportStatus"), passengerAccountForm: document.querySelector("#passengerAccountForm"), passengerName: document.querySelector("#passengerName"), passengerEmail: document.querySelector("#passengerEmail"), passengerPassword: document.querySelector("#passengerPassword"), passengerPhoto: document.querySelector("#passengerPhoto"), passengerPhone: document.querySelector("#passengerPhone"), passengerVerificationCode: document.querySelector("#passengerVerificationCode"), sendPassengerCode: document.querySelector("#sendPassengerCode"), verifyPassengerPhone: document.querySelector("#verifyPassengerPhone"), passengerNationalId: document.querySelector("#passengerNationalId"), passengerDob: document.querySelector("#passengerDob"), passengerAccountUse: document.querySelector("#passengerAccountUse"), passengerReferralCode: document.querySelector("#passengerReferralCode"), passengerInitialBusinessFields: document.querySelector("#passengerInitialBusinessFields"), passengerInitialBusinessName: document.querySelector("#passengerInitialBusinessName"), passengerInitialBusinessBillingEmail: document.querySelector("#passengerInitialBusinessBillingEmail"), passengerInitialBusinessCategory: document.querySelector("#passengerInitialBusinessCategory"), passengerInitialBusinessPlan: document.querySelector("#passengerInitialBusinessPlan"), passengerInitialBusinessAddress: document.querySelector("#passengerInitialBusinessAddress"), passengerInitialBusinessLogo: document.querySelector("#passengerInitialBusinessLogo"), passengerInitialBusinessLogoAlt: document.querySelector("#passengerInitialBusinessLogoAlt"), passengerInitialBusinessReferralCode: document.querySelector("#passengerInitialBusinessReferralCode"), passengerCountry: document.querySelector("#passengerCountry"), passengerCity: document.querySelector("#passengerCity"), passengerStatus: document.querySelector("#passengerStatus"), passengerTurnstile: document.querySelector("#passengerTurnstile"), passengerSaveButton: document.querySelector("#passengerSaveButton"), rideRequestForm: document.querySelector("#rideRequestForm"), passengerRideGate: document.querySelector("#passengerRideGate"), pickupCity: document.querySelector("#pickupCity"), pickupArea: document.querySelector("#pickupArea"), pickupDescription: document.querySelector("#pickupDescription"), pickupUseCurrentLocation: document.querySelector("#pickupUseCurrentLocation"), pickupHistory: document.querySelector("#pickupHistory"), pickupSuggestions: document.querySelector("#pickupSuggestions"), pickupPlaceStatus: document.querySelector("#pickupPlaceStatus"), useCurrentPickup: document.querySelector("#useCurrentPickup"), capturePickupGps: document.querySelector("#capturePickupGps"), clearPickupGps: document.querySelector("#clearPickupGps"), pickupGpsStatus: document.querySelector("#pickupGpsStatus"), destinationArea: document.querySelector("#destinationArea"), destination: document.querySelector("#destination"), destinationHistory: document.querySelector("#destinationHistory"), destinationSuggestions: document.querySelector("#destinationSuggestions"), destinationPlaceStatus: document.querySelector("#destinationPlaceStatus"), rideRequestRoutePreview: document.querySelector("#rideRequestRoutePreview"), addRideStop: document.querySelector("#addRideStop"), clearRideStops: document.querySelector("#clearRideStops"), rideStopsPanel: document.querySelector("#rideStopsPanel"), rideStopsField: document.querySelector("#rideStopsField"), rideStops: document.querySelector("#rideStops"), rideStopSuggestions: document.querySelector("#rideStopSuggestions"), rideStopsStatus: document.querySelector("#rideStopsStatus"), rideBillingAccount: document.querySelector("#rideBillingAccount"), toggleRideTiming: document.querySelector("#toggleRideTiming"), rideTimingPanel: document.querySelector("#rideTimingPanel"), rideTiming: document.querySelector("#rideTiming"), scheduledAtField: document.querySelector("#scheduledAtField"), scheduledAt: document.querySelector("#scheduledAt"), passengerRiderAvailability: document.querySelector("#passengerRiderAvailability"), toggleVehiclePreference: document.querySelector("#toggleVehiclePreference"), vehiclePreferencePanel: document.querySelector("#vehiclePreferencePanel"), vehiclePreference: document.querySelector("#vehiclePreference"), ridePassengerCount: document.querySelector("#ridePassengerCount"), rideLuggageCount: document.querySelector("#rideLuggageCount"), rideLuggageNote: document.querySelector("#rideLuggageNote"), toggleFareDetails: document.querySelector("#toggleFareDetails"), fareDetailsPanel: document.querySelector("#fareDetailsPanel"), passengerFareModePanel: document.querySelector("#passengerFareModePanel"), passengerFareNegotiable: document.querySelector("#passengerFareNegotiable"), passengerFareMode: document.querySelector("#passengerFareMode"), passengerFareModeStatus: document.querySelector("#passengerFareModeStatus"), fareOffer: document.querySelector("#fareOffer"), fareGuidance: document.querySelector("#fareGuidance"), fareReviewPanel: document.querySelector("#fareReviewPanel"), paymentPreference: document.querySelector("#paymentPreference"), riderSignInForm: document.querySelector("#riderSignInForm"), riderSignInEmail: document.querySelector("#riderSignInEmail"), riderSignInPassword: document.querySelector("#riderSignInPassword"), riderSignInOtpPanel: document.querySelector("#riderSignInOtpPanel"), riderSignInPhone: document.querySelector("#riderSignInPhone"), riderSignInCode: document.querySelector("#riderSignInCode"), sendRiderSignInCode: document.querySelector("#sendRiderSignInCode"), verifyRiderSignIn: document.querySelector("#verifyRiderSignIn"), forgotRiderPassword: document.querySelector("#forgotRiderPassword"), riderPasswordResetPanel: document.querySelector("#riderPasswordResetPanel"), riderPasswordResetPhoneStep: document.querySelector("#riderPasswordResetPhoneStep"), riderPasswordResetPhoneHint: document.querySelector("#riderPasswordResetPhoneHint"), riderPasswordResetPhoneCode: document.querySelector("#riderPasswordResetPhoneCode"), sendRiderPasswordResetPhoneOtp: document.querySelector("#sendRiderPasswordResetPhoneOtp"), verifyRiderPasswordResetPhoneOtp: document.querySelector("#verifyRiderPasswordResetPhoneOtp"), riderPasswordResetPhoneStatus: document.querySelector("#riderPasswordResetPhoneStatus"), riderPasswordResetPasswordFields: document.querySelector("#riderPasswordResetPasswordFields"), riderResetPassword: document.querySelector("#riderResetPassword"), riderResetPasswordConfirm: document.querySelector("#riderResetPasswordConfirm"), saveRiderResetPassword: document.querySelector("#saveRiderResetPassword"), riderSignInStatus: document.querySelector("#riderSignInStatus"), riderAccountStage: document.querySelector("#riderAccountStage"), riderWorkspaceHeader: document.querySelector("#riderWorkspaceHeader"), riderWorkspaceTitle: document.querySelector("#riderWorkspaceTitle"), riderWorkspaceSummary: document.querySelector("#riderWorkspaceSummary"), riderWorkspaceMenu: document.querySelector("#riderWorkspaceMenu"), riderWorkspaceMenuToggle: document.querySelector("#riderWorkspaceMenuToggle"), riderWorkspaceNav: document.querySelector("#riderWorkspaceNav"), riderMenuSignOutTop: document.querySelector("#riderMenuSignOutTop"), riderOverviewGrid: document.querySelector("#riderOverviewGrid"), riderSessionCard: document.querySelector("#riderSessionCard"), riderProfileAvatar: document.querySelector("#riderProfileAvatar"), riderProfilePhotoStatus: document.querySelector("#riderProfilePhotoStatus"), riderProfileDetailList: document.querySelector("#riderProfileDetailList"), riderPasswordChangeForm: document.querySelector("#riderPasswordChangeForm"), riderCurrentPassword: document.querySelector("#riderCurrentPassword"), riderNewPassword: document.querySelector("#riderNewPassword"), riderNewPasswordConfirm: document.querySelector("#riderNewPasswordConfirm"), riderChangePassword: document.querySelector("#riderChangePassword"), riderPasswordChangeStatus: document.querySelector("#riderPasswordChangeStatus"), riderRatingsPanel: document.querySelector("#riderRatingsPanel"), riderNavigationPreference: document.querySelector("#riderNavigationPreference"), riderReferralPanel: document.querySelector("#riderReferralPanel"), riderReferralSummary: document.querySelector("#riderReferralSummary"), riderReferralCodeDisplay: document.querySelector("#riderReferralCodeDisplay"), copyRiderReferralCode: document.querySelector("#copyRiderReferralCode"), shareRiderReferralCode: document.querySelector("#shareRiderReferralCode"), emailRiderReferralCode: document.querySelector("#emailRiderReferralCode"), textRiderReferralCode: document.querySelector("#textRiderReferralCode"), riderReferralHowItWorks: document.querySelector("#riderReferralHowItWorks"), riderSessionTitle: document.querySelector("#riderSessionTitle"), riderSessionSummary: document.querySelector("#riderSessionSummary"), riderMenuSignOut: document.querySelector("#riderMenuSignOut"), riderPaymentForm: document.querySelector("#riderPaymentForm"), startRiderStripePayoutSetup: document.querySelector("#startRiderStripePayoutSetup"), riderPaymentProvider: document.querySelector("#riderPaymentProvider"), riderBankName: document.querySelector("#riderBankName"), riderAccountHolder: document.querySelector("#riderAccountHolder"), riderAccountLast4: document.querySelector("#riderAccountLast4"), riderPaymentReference: document.querySelector("#riderPaymentReference"), riderPaymentStatus: document.querySelector("#riderPaymentStatus"), riderLocationForm: document.querySelector("#riderLocationForm"), riderActiveCountry: document.querySelector("#riderActiveCountry"), riderActiveCity: document.querySelector("#riderActiveCity"), riderActiveArea: document.querySelector("#riderActiveArea"), riderDailyRegions: document.querySelector("#riderDailyRegions"), riderDestinationScope: document.querySelector("#riderDestinationScope"), captureRiderGps: document.querySelector("#captureRiderGps"), clearRiderGps: document.querySelector("#clearRiderGps"), riderGpsStatus: document.querySelector("#riderGpsStatus"), riderLocationStatus: document.querySelector("#riderLocationStatus"), riderDailyRegionStatus: document.querySelector("#riderDailyRegionStatus"), riderNoticePanel: document.querySelector("#riderNoticePanel"), riderNoticeList: document.querySelector("#riderNoticeList"), riderEnablePush: document.querySelector("#riderEnablePush"), riderPushStatus: document.querySelector("#riderPushStatus"), riderSupportForm: document.querySelector("#riderSupportForm"), riderSupportCategory: document.querySelector("#riderSupportCategory"), riderSupportSubject: document.querySelector("#riderSupportSubject"), riderSupportMessage: document.querySelector("#riderSupportMessage"), riderSupportStatus: document.querySelector("#riderSupportStatus"), riderFlowCard: document.querySelector("#riderFlowCard"), riderFlowTitle: document.querySelector("#riderFlowTitle"), riderFlowSummary: document.querySelector("#riderFlowSummary"), riderFlowSteps: document.querySelector("#riderFlowSteps"), riderFlowMeta: document.querySelector("#riderFlowMeta"), riderFlowActions: document.querySelector("#riderFlowActions"), riderSignOut: document.querySelector("#riderSignOut"), riderAccountForm: document.querySelector("#riderAccountForm"), riderApplicationModeNotice: document.querySelector("#riderApplicationModeNotice"), riderName: document.querySelector("#riderName"), riderEmail: document.querySelector("#riderEmail"), riderPassword: document.querySelector("#riderPassword"), riderPhoto: document.querySelector("#riderPhoto"), riderPhone: document.querySelector("#riderPhone"), riderReferralCode: document.querySelector("#riderReferralCode"), riderVerificationCode: document.querySelector("#riderVerificationCode"), sendRiderCode: document.querySelector("#sendRiderCode"), verifyRiderPhone: document.querySelector("#verifyRiderPhone"), riderNationalId: document.querySelector("#riderNationalId"), riderDob: document.querySelector("#riderDob"), riderVehicle: document.querySelector("#riderVehicle"), riderVehicleSectionTitle: document.querySelector("#riderVehicleSectionTitle"), riderVehicleSectionHelp: document.querySelector("#riderVehicleSectionHelp"), riderVehicleCategoryLabel: document.querySelector("#riderVehicleCategoryLabel"), riderVehicleMakeLabel: document.querySelector("#riderVehicleMakeLabel"), riderCarMake: document.querySelector("#riderCarMake"), riderVehicleModelLabel: document.querySelector("#riderVehicleModelLabel"), riderCarModel: document.querySelector("#riderCarModel"), riderCarBodyType: document.querySelector("#riderCarBodyType"), riderVehicleDesignation: document.querySelector("#riderVehicleDesignation"), riderCarYear: document.querySelector("#riderCarYear"), riderCarColor: document.querySelector("#riderCarColor"), riderCountry: document.querySelector("#riderCountry"), riderCity: document.querySelector("#riderCity"), riderArea: document.querySelector("#riderArea"), riderCredential: document.querySelector("#riderCredential"), riderLicenseExpiresOn: document.querySelector("#riderLicenseExpiresOn"), riderVehicleVin: document.querySelector("#riderVehicleVin"), riderRegistrationLabel: document.querySelector("#riderRegistrationLabel"), riderRegistration: document.querySelector("#riderRegistration"), riderVehicleModeNote: document.querySelector("#riderVehicleModeNote"), riderInsuranceProvider: document.querySelector("#riderInsuranceProvider"), riderInsuranceNumber: document.querySelector("#riderInsuranceNumber"), riderInsuranceExpiresOn: document.querySelector("#riderInsuranceExpiresOn"), riderBackgroundConsent: document.querySelector("#riderBackgroundConsent"), riderBackgroundConsentText: document.querySelector("#riderBackgroundConsentText"), riderNationalIdDocument: document.querySelector("#riderNationalIdDocument"), riderLicenseDocument: document.querySelector("#riderLicenseDocument"), riderRegistrationDocumentLabel: document.querySelector("#riderRegistrationDocumentLabel"), riderRegistrationDocument: document.querySelector("#riderRegistrationDocument"), riderInsuranceDocument: document.querySelector("#riderInsuranceDocument"), riderInspectionDocument: document.querySelector("#riderInspectionDocument"), riderComplianceRenewalForm: document.querySelector("#riderComplianceRenewalForm"), riderRenewLicenseExpiresOn: document.querySelector("#riderRenewLicenseExpiresOn"), riderRenewLicenseDocument: document.querySelector("#riderRenewLicenseDocument"), riderRenewInsuranceExpiresOn: document.querySelector("#riderRenewInsuranceExpiresOn"), riderRenewInsuranceDocument: document.querySelector("#riderRenewInsuranceDocument"), riderSubmitComplianceRenewal: document.querySelector("#riderSubmitComplianceRenewal"), riderComplianceRenewalStatus: document.querySelector("#riderComplianceRenewalStatus"), riderStatus: document.querySelector("#riderStatus"), riderTurnstile: document.querySelector("#riderTurnstile"), riderSubmitButton: document.querySelector("#riderSubmitButton"), riderBackgroundCheckPanel: document.querySelector("#riderBackgroundCheckPanel"), riderBackgroundCheckBadge: document.querySelector("#riderBackgroundCheckBadge"), riderBackgroundCheckSummary: document.querySelector("#riderBackgroundCheckSummary"), startRiderBackgroundCheck: document.querySelector("#startRiderBackgroundCheck"), riderBackgroundCheckStatus: document.querySelector("#riderBackgroundCheckStatus"), riderBackgroundCheckList: document.querySelector("#riderBackgroundCheckList"), riderTaxPanel: document.querySelector("#riderTaxPanel"), riderTaxOnboardingSummary: document.querySelector("#riderTaxOnboardingSummary"), startRiderTaxOnboarding: document.querySelector("#startRiderTaxOnboarding"), riderTaxOnboardingStatus: document.querySelector("#riderTaxOnboardingStatus"), riderTaxList: document.querySelector("#riderTaxList"), subscriptionText: document.querySelector("#subscriptionText"), subscriptionPlan: document.querySelector("#subscriptionPlan"), subscriptionRenewalMode: document.querySelector("#subscriptionRenewalMode"), subscriptionTopupAmount: document.querySelector("#subscriptionTopupAmount"), subscriptionPaymentProvider: document.querySelector("#subscriptionPaymentProvider"), subscriptionPayerPhone: document.querySelector("#subscriptionPayerPhone"), subscriptionPaymentStatus: document.querySelector("#subscriptionPaymentStatus"), paySubscription: document.querySelector("#paySubscription"), riderEarningsPanel: document.querySelector("#riderEarningsPanel"), riderEarningsCount: document.querySelector("#riderEarningsCount"), riderEarningsSummary: document.querySelector("#riderEarningsSummary"), riderEarningsList: document.querySelector("#riderEarningsList"), offerForm: document.querySelector("#offerForm"), offerRequestContext: document.querySelector("#offerRequestContext"), counterFare: document.querySelector("#counterFare"), counterNote: document.querySelector("#counterNote"), acceptFare: document.querySelector("#acceptFare"), dropRiderNegotiation: document.querySelector("#dropRiderNegotiation"), selectedSummary: document.querySelector("#selectedSummary"), riderAppliedDestinationSummary: document.querySelector("#riderAppliedDestinationSummary"), marketPanel: document.querySelector("#marketPanel"), marketLocation: document.querySelector("#marketLocation"), openRiderDestinationFilter: document.querySelector("#openRiderDestinationFilter"), refreshMarket: document.querySelector("#refreshMarket"), marketFilters: document.querySelector("#marketFilters"), riderDestinationFilterPanel: document.querySelector("#riderDestinationFilterPanel"), riderDestinationFilterConsent: document.querySelector("#riderDestinationFilterConsent"), riderDestinationFilterCountry: document.querySelector("#riderDestinationFilterCountry"), riderDestinationFilterCity: document.querySelector("#riderDestinationFilterCity"), riderDestinationFilterArea: document.querySelector("#riderDestinationFilterArea"), riderDestinationFilterText: document.querySelector("#riderDestinationFilterText"), riderDestinationFilterApply: document.querySelector("#riderDestinationFilterApply"), riderDestinationFilterClear: document.querySelector("#riderDestinationFilterClear"), riderDestinationFilterStatus: document.querySelector("#riderDestinationFilterStatus"), cityMap: document.querySelector("#cityMap"), riderRequestDetailPanel: document.querySelector("#riderRequestDetailPanel"), riderRequestDetailTitle: document.querySelector("#riderRequestDetailTitle"), riderRequestDetailStatus: document.querySelector("#riderRequestDetailStatus"), riderMarketplaceBack: document.querySelector("#riderMarketplaceBack"), boardGrid: document.querySelector("#boardGrid"), requestsBoard: document.querySelector("#requestsBoard"), requestBoardTitle: document.querySelector("#requestBoardTitle"), requestList: document.querySelector("#requestList"), offersBoard: document.querySelector("#offersBoard"), offerBoardTitle: document.querySelector("#offerBoardTitle"), offerList: document.querySelector("#offerList"), adminIssuePagination: document.querySelector("#adminIssuePagination"), adminIssuePrev: document.querySelector("#adminIssuePrev"), adminIssuePage: document.querySelector("#adminIssuePage"), adminIssueNext: document.querySelector("#adminIssueNext"), requestCount: document.querySelector("#requestCount"), offerCount: document.querySelector("#offerCount"), gpsWriteMetric: document.querySelector("#gpsWriteMetric"), googleCallMetric: document.querySelector("#googleCallMetric"), routeCacheMetric: document.querySelector("#routeCacheMetric"), slowRpcMetric: document.querySelector("#slowRpcMetric"), dbStorageMetric: document.querySelector("#dbStorageMetric"), fileStorageMetric: document.querySelector("#fileStorageMetric"), noisyRowMetric: document.querySelector("#noisyRowMetric"), retentionRowMetric: document.querySelector("#retentionRowMetric"), adminPageTitle: document.querySelector("#adminPageTitle"), adminPageDescription: document.querySelector("#adminPageDescription"), adminPageStep: document.querySelector("#adminPageStep"), adminPrevPage: document.querySelector("#adminPrevPage"), adminNextPage: document.querySelector("#adminNextPage"), adminMetricGrid: document.querySelector("#adminMetricGrid"), adminPageDirectory: document.querySelector("#adminPageDirectory"), adminWorkspacePageList: document.querySelector("#adminWorkspacePageList"), adminPageDirectoryCount: document.querySelector("#adminPageDirectoryCount"), adminActivityPanel: document.querySelector("#adminActivityPanel"), adminActivityList: document.querySelector("#adminActivityList"), adminActivityCount: document.querySelector("#adminActivityCount"), adminActivityPagination: document.querySelector("#adminActivityPagination"), adminActivityPrev: document.querySelector("#adminActivityPrev"), adminActivityPage: document.querySelector("#adminActivityPage"), adminActivityNext: document.querySelector("#adminActivityNext"), adminDirectoryTools: document.querySelector("#adminDirectoryTools"), adminCompliancePagination: document.querySelector("#adminCompliancePagination"), adminCompliancePrev: document.querySelector("#adminCompliancePrev"), adminCompliancePage: document.querySelector("#adminCompliancePage"), adminComplianceNext: document.querySelector("#adminComplianceNext"), adminFinanceSummaryPagination: document.querySelector("#adminFinanceSummaryPagination"), adminFinanceSummaryPrev: document.querySelector("#adminFinanceSummaryPrev"), adminFinanceSummaryPage: document.querySelector("#adminFinanceSummaryPage"), adminFinanceSummaryNext: document.querySelector("#adminFinanceSummaryNext"), adminPaymentPagination: document.querySelector("#adminPaymentPagination"), adminPaymentPrev: document.querySelector("#adminPaymentPrev"), adminPaymentPage: document.querySelector("#adminPaymentPage"), adminPaymentNext: document.querySelector("#adminPaymentNext"), adminControlsPagination: document.querySelector("#adminControlsPagination"), adminControlsPrev: document.querySelector("#adminControlsPrev"), adminControlsPage: document.querySelector("#adminControlsPage"), adminControlsNext: document.querySelector("#adminControlsNext"), adminIntercityMetric: document.querySelector("#adminIntercityMetric"), adminAgencyApprovalsBoard: document.querySelector("#adminAgencyApprovalsBoard"), adminAgencyApprovalsCount: document.querySelector("#adminAgencyApprovalsCount"), adminAgencyApprovalsStatus: document.querySelector("#adminAgencyApprovalsStatus"), adminAgencyApprovalsList: document.querySelector("#adminAgencyApprovalsList"), adminIntercityBoard: document.querySelector("#adminIntercityBoard"), adminIntercityPendingAgencyList: document.querySelector("#adminIntercityPendingAgencyList"), adminIntercityReportList: document.querySelector("#adminIntercityReportList"), adminIntercityReportsBoard: document.querySelector("#adminIntercityReportsBoard"), adminIntercityReportsCount: document.querySelector("#adminIntercityReportsCount"), adminIntercityReportsStatus: document.querySelector("#adminIntercityReportsStatus"), adminIntercityReportsAgencyFilter: document.querySelector("#adminIntercityReportsAgencyFilter"), adminIntercityReportsList: document.querySelector("#adminIntercityReportsList"), adminIntercityList: document.querySelector("#adminIntercityList"), adminIntercityCount: document.querySelector("#adminIntercityCount"), adminIntercityStatus: document.querySelector("#adminIntercityStatus"), adminIntercityPublishForm: document.querySelector("#adminIntercityPublishForm"), adminIntercityPublishAgency: document.querySelector("#adminIntercityPublishAgency"), adminIntercityPublishAt: document.querySelector("#adminIntercityPublishAt"), adminIntercityPublishBusLabel: document.querySelector("#adminIntercityPublishBusLabel"), adminIntercityPublishOrigin: document.querySelector("#adminIntercityPublishOrigin"), adminIntercityPublishDestination: document.querySelector("#adminIntercityPublishDestination"), adminIntercityPublishBoarding: document.querySelector("#adminIntercityPublishBoarding"), adminIntercityPublishDropoff: document.querySelector("#adminIntercityPublishDropoff"), adminIntercityPublishDuration: document.querySelector("#adminIntercityPublishDuration"), adminIntercityPublishSeats: document.querySelector("#adminIntercityPublishSeats"), adminIntercityPublishFare: document.querySelector("#adminIntercityPublishFare"), adminIntercityPublishBlockedSeats: document.querySelector("#adminIntercityPublishBlockedSeats"), adminIntercityPublishNotes: document.querySelector("#adminIntercityPublishNotes"), adminIntercityPublishAllowMtn: document.querySelector("#adminIntercityPublishAllowMtn"), adminIntercityPublishAllowOrange: document.querySelector("#adminIntercityPublishAllowOrange"), adminIntercityPublishAllowPayLater: document.querySelector("#adminIntercityPublishAllowPayLater"), adminIntercityPublishStatus: document.querySelector("#adminIntercityPublishStatus"), adminIntercityAgencyMessageForm: document.querySelector("#adminIntercityAgencyMessageForm"), adminIntercityAgencyMessageAgency: document.querySelector("#adminIntercityAgencyMessageAgency"), adminIntercityAgencyMessageSubject: document.querySelector("#adminIntercityAgencyMessageSubject"), adminIntercityAgencyMessageBody: document.querySelector("#adminIntercityAgencyMessageBody"), adminIntercityAgencyMessageStatus: document.querySelector("#adminIntercityAgencyMessageStatus"), adminIntercityPagination: document.querySelector("#adminIntercityPagination"), adminIntercityPrev: document.querySelector("#adminIntercityPrev"), adminIntercityPage: document.querySelector("#adminIntercityPage"), adminIntercityNext: document.querySelector("#adminIntercityNext"), adminGeographyPagination: document.querySelector("#adminGeographyPagination"), adminGeographyPrev: document.querySelector("#adminGeographyPrev"), adminGeographyPage: document.querySelector("#adminGeographyPage"), adminGeographyNext: document.querySelector("#adminGeographyNext"), adminReportsPagination: document.querySelector("#adminReportsPagination"), adminReportsPrev: document.querySelector("#adminReportsPrev"), adminReportsPage: document.querySelector("#adminReportsPage"), adminReportsNext: document.querySelector("#adminReportsNext"), adminAuditPagination: document.querySelector("#adminAuditPagination"), adminAuditPrev: document.querySelector("#adminAuditPrev"), adminAuditPage: document.querySelector("#adminAuditPage"), adminAuditNext: document.querySelector("#adminAuditNext"), adminStatus: inertElement("adminStatus"), seedDemo: inertElement("seedDemo"), adminDirectorySearchButton: document.querySelector("#adminDirectorySearchButton"), adminDirectoryClearSearch: document.querySelector("#adminDirectoryClearSearch"), clearDemo: inertElement("clearDemo"), chatPanel: document.querySelector("#chatPanel"), chatStatus: document.querySelector("#chatStatus"), rideActionPanel: document.querySelector("#rideActionPanel"), chatThread: document.querySelector("#chatThread"), chatForm: document.querySelector("#chatForm"), chatInput: document.querySelector("#chatInput"), chatVoiceButton: document.querySelector("#chatVoiceButton"), chatSendButton: document.querySelector("#chatSendButton"), chatVoiceStatus: document.querySelector("#chatVoiceStatus"), safetyReportForm: document.querySelector("#safetyReportForm"), safetyReportCategory: document.querySelector("#safetyReportCategory"), safetyReportSeverity: document.querySelector("#safetyReportSeverity"), safetyReportDetails: document.querySelector("#safetyReportDetails"), safetyReportStatus: document.querySelector("#safetyReportStatus"), rideRatingForm: document.querySelector("#rideRatingForm"), rideRatingScore: document.querySelector("#rideRatingScore"), rideRatingSafety: document.querySelector("#rideRatingSafety"), rideRatingPunctuality: document.querySelector("#rideRatingPunctuality"), rideRatingCommunication: document.querySelector("#rideRatingCommunication"), rideRatingVehicle: document.querySelector("#rideRatingVehicle"), rideRatingComment: document.querySelector("#rideRatingComment"), rideRatingStatus: document.querySelector("#rideRatingStatus"), requestTemplate: document.querySelector("#requestTemplate"), offerTemplate: document.querySelector("#offerTemplate"), reviewTemplate: document.querySelector("#reviewTemplate") }; function adminShellAvailable() { return false; } function availableWorkspaceTab(tab) { if (!workspaceTabs.includes(tab)) return null; if (!runtimeAllowsWorkspaceTab(tab)) return null; if (tab === "admin" && !adminShellAvailable()) return null; return tab; } function populateSelect(select, values, selectedValue) { if (!select) return; select.innerHTML = ""; values.forEach((value) => { const option = document.createElement("option"); option.value = value; option.textContent = selectOptionLabel(value); option.selected = value === selectedValue; select.append(option); }); } function selectOptionLabel(value) { const text = String(value ?? ""); if (text === "Other") { return typeof translatedValue === "function" ? translatedValue("otherOption") || text : text; } return text; } function populateMultiSelect(select, values, selectedValues = []) { if (!select) return; const selected = new Set(selectedValues); select.innerHTML = ""; values.forEach((value) => { const option = document.createElement("option"); option.value = value; option.textContent = selectOptionLabel(value); option.selected = selected.has(value); select.append(option); }); } function selectedMultiValues(select) { return [...(select?.selectedOptions ?? [])].map((option) => option.value).filter(Boolean); } function populateSelectOptions(select, options, selectedValue) { if (!select) return; select.innerHTML = ""; options.forEach((item) => { const option = document.createElement("option"); option.value = item.value; option.textContent = item.label; option.selected = item.value === selectedValue; select.append(option); }); } function daysFromNow(days) { const date = new Date(); date.setDate(date.getDate() + days); return date.toISOString(); } function daysAgo(days) { return daysFromNow(-days); } function defaultLaunchCountry() { return countryCities[appConfig.firstLaunchCountry] ? appConfig.firstLaunchCountry : "United States"; } function defaultLaunchCity(country = defaultLaunchCountry()) { return cityNames(country).includes(appConfig.firstLaunchCity) ? appConfig.firstLaunchCity : cityNames(country)[0]; } function moneyCurrencyForCountry(country = defaultLaunchCountry()) { return africanRidePaymentCountries.has(country) ? "XAF" : "USD"; } function minimumFareOffer(country = defaultLaunchCountry()) { return moneyCurrencyForCountry(country) === "USD" ? 1 : 100; } const passengerFareModeOverrideStorageKey = "waka-passenger-fare-mode-choice-v1"; const riderNavigationPreferenceChoiceStorageKey = "waka-rider-navigation-preference-choice-v1"; function readLocalJsonRecord(key) { try { const raw = localStorage.getItem(key); if (!raw) return null; const parsed = JSON.parse(raw); return parsed && typeof parsed === "object" ? parsed : null; } catch { return null; } } function writeLocalJsonRecord(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); } catch { // Ignore storage failures; in-memory state still carries the current choice. } } function normalizePassengerFareMode() { return "negotiable"; } function rememberPassengerFareModeChoice(mode) { writeLocalJsonRecord(passengerFareModeOverrideStorageKey, { value: normalizePassengerFareMode(mode), updatedAt: new Date().toISOString() }); } function storedPassengerFareModeChoice() { const stored = readLocalJsonRecord(passengerFareModeOverrideStorageKey); return stored?.value ? normalizePassengerFareMode(stored.value) : null; } function syncPassengerFareModeInputs(mode, { userSelected = false } = {}) { const normalized = normalizePassengerFareMode(mode); if (els.passengerFareMode) { els.passengerFareMode.value = normalized; els.passengerFareMode.dataset.lastSyncedFareMode = normalized; if (userSelected) els.passengerFareMode.dataset.userSelectedFareMode = normalized; } if (els.passengerFareNegotiable instanceof HTMLInputElement) { els.passengerFareNegotiable.checked = normalized !== "non_negotiable"; els.passengerFareNegotiable.setAttribute("aria-checked", normalized !== "non_negotiable" ? "true" : "false"); els.passengerFareNegotiable.dataset.lastSyncedFareMode = normalized; if (userSelected) { els.passengerFareNegotiable.dataset.userSelectedFareMode = normalized; } } if (userSelected) rememberPassengerFareModeChoice(normalized); return normalized; } function passengerFareMode() { const selectedMode = els.passengerFareMode?.dataset?.userSelectedFareMode ?? els.passengerFareNegotiable?.dataset?.userSelectedFareMode; if (selectedMode) return normalizePassengerFareMode(selectedMode); if (els.passengerFareMode instanceof HTMLSelectElement) { const currentMode = normalizePassengerFareMode(els.passengerFareMode.value); const lastSyncedMode = els.passengerFareMode.dataset.lastSyncedFareMode; if (lastSyncedMode && currentMode !== normalizePassengerFareMode(lastSyncedMode)) { els.passengerFareMode.dataset.userSelectedFareMode = currentMode; state.passengerFareMode = currentMode; rememberPassengerFareModeChoice(currentMode); return currentMode; } } if (els.passengerFareNegotiable instanceof HTMLInputElement) { const currentMode = els.passengerFareNegotiable.checked ? "negotiable" : "non_negotiable"; const lastSyncedMode = els.passengerFareNegotiable.dataset.lastSyncedFareMode; if (lastSyncedMode && currentMode !== normalizePassengerFareMode(lastSyncedMode)) { els.passengerFareNegotiable.dataset.userSelectedFareMode = currentMode; state.passengerFareMode = currentMode; rememberPassengerFareModeChoice(currentMode); return currentMode; } } const storedMode = storedPassengerFareModeChoice(); if (storedMode) return storedMode; return normalizePassengerFareMode( state.passengerFareMode ?? els.passengerFareMode?.value ?? (els.passengerFareNegotiable?.checked === false ? "non_negotiable" : "negotiable") ); } function requestFareMode(request) { return normalizePassengerFareMode(request?.fareMode ?? request?.fare_mode); } function requestIsNonNegotiableFare() { return false; } function requestIsNegotiableFare() { return true; } function fareModeChipText() { return uiText("negotiableFare", "Negotiable fare"); } function formatMoney(amount, country = defaultLaunchCountry()) { const value = Number(amount) || 0; const currency = moneyCurrencyForCountry(country); if (currency === "USD") { return new Intl.NumberFormat("en-US", { style: "currency", currency, maximumFractionDigits: 0 }).format(value); } return `${value.toLocaleString("en-US")} FCFA`; } function normalizeRidePassengerCount(value = 1) { const parsed = Number.parseInt(String(value ?? "1").replace(/[^\d]/g, ""), 10); if (!Number.isFinite(parsed)) return 1; return Math.min(Math.max(parsed, 1), 8); } function normalizeRideLuggageCount(value = 0) { const parsed = Number.parseInt(String(value ?? "0").replace(/[^\d]/g, ""), 10); if (!Number.isFinite(parsed)) return 0; return Math.min(Math.max(parsed, 0), 12); } function normalizeRideLuggageNote(value = "") { return String(value ?? "").replace(/\s+/g, " ").trim().slice(0, 160); } function normalizeRideVehicle(value) { const normalized = String(value ?? "").trim().toLowerCase().replace(/[\s/-]+/g, "_"); if (["bike", "motorbike", "motorcycle", "moto", "okada", "bendskin"].includes(normalized)) return "bike"; return "car"; } function rideVehicleLabel(value) { return normalizeRideVehicle(value) === "bike" ? "Bike" : "Car"; } function currentRideTripDetails() { return { passengerCount: normalizeRidePassengerCount(els.ridePassengerCount?.value), luggageCount: normalizeRideLuggageCount(els.rideLuggageCount?.value), luggageNote: normalizeRideLuggageNote(els.rideLuggageNote?.value) }; } function normalizeCarBodyType(value) { const normalized = String(value ?? "").trim().toLowerCase(); const allowed = ["sedan", "suv", "hatchback", "minivan", "wagon", "pickup", "coupe", "convertible", "luxury", "motorbike"]; return allowed.includes(normalized) ? normalized : "sedan"; } function carBodyTypeLabel(value) { const normalized = normalizeCarBodyType(value); const labels = { sedan: "Sedan", suv: "SUV", hatchback: "Hatchback", minivan: "Minivan", wagon: "Wagon", pickup: "Pickup", coupe: "Coupe", convertible: "Convertible", luxury: "Luxury", motorbike: "Motorbike" }; return labels[normalized] ?? "Sedan"; } function carBodyTypeAllowsXlSpecial(value) { return ["suv", "minivan", "wagon", "pickup", "luxury"].includes(normalizeCarBodyType(value)); } function normalizeCarTypePreference(value) { const normalized = String(value ?? "").trim().toLowerCase(); if (["bike", "motorbike", "motorcycle", "moto", "okada", "bendskin"].includes(normalized)) return "bike"; if (["normal", "sedan", "any", "car"].includes(normalized)) return "sedan"; if (["xl_special", "xl/special", "xl-special", "suv"].includes(normalized)) return "suv"; return "sedan"; } function carTypePreferenceLabel(value) { const normalized = normalizeCarTypePreference(value); if (normalized === "bike") return uiText("bike", "Bike"); return normalized === "suv" ? uiText("xlSpecial", "XL/Special") : uiText("normalVehicle", "Normal"); } function passengerMinimumFareFromGuidance(guidance, vehicleDesignation = els.vehiclePreference?.value) { if (!guidance) return null; const minimum = normalizeCarTypePreference(vehicleDesignation) === "suv" ? Number(guidance.max) + 1 : Number(guidance.min); return Number.isFinite(minimum) && minimum > 0 ? Math.ceil(minimum) : null; } function normalizeRiderVehicleDesignation(value, bodyType = null) { const normalized = String(value ?? "").trim().toLowerCase().replace(/[/-]/g, "_"); const desired = ["normal", "xl_special", "both"].includes(normalized) ? normalized : "normal"; if (bodyType && !carBodyTypeAllowsXlSpecial(bodyType) && desired !== "normal") return "normal"; return desired; } function riderVehicleDesignationLabel(value) { const normalized = normalizeRiderVehicleDesignation(value); if (normalized === "xl_special") return "XL/Special"; if (normalized === "both") return "Normal and XL/Special"; return "Normal"; } function riderCanServeCarTypePreference(rider, preference) { const normalizedPreference = normalizeCarTypePreference(preference); const vehicle = normalizeRideVehicle(rider?.vehicle); if (normalizedPreference === "bike") return vehicle === "bike"; if (vehicle !== "car") return false; const designation = normalizeRiderVehicleDesignation(rider?.vehicleDesignation, rider?.carBodyType); if (normalizedPreference === "sedan") return ["normal", "both"].includes(designation); if (normalizedPreference === "suv") return carBodyTypeAllowsXlSpecial(rider?.carBodyType) && ["xl_special", "both"].includes(designation); return true; } function normalizeRiderNavigationPreference(value) { const normalized = String(value || "google_maps") .trim() .toLowerCase() .replace(/[\s-]+/g, "_"); return normalized === "waze" ? "waze" : "google_maps"; } function riderNavigationPreferenceUiOverride() { if (!(els.riderNavigationPreference instanceof HTMLSelectElement)) return null; const currentPreference = normalizeRiderNavigationPreference(els.riderNavigationPreference.value); const selected = els.riderNavigationPreference.dataset.userSelectedNavigationPreference; if (selected) { const selectedPreference = normalizeRiderNavigationPreference(selected); if (selectedPreference === currentPreference) return selectedPreference; els.riderNavigationPreference.dataset.userSelectedNavigationPreference = currentPreference; rememberRiderNavigationPreferenceOverride(currentPreference); return currentPreference; } const lastSyncedPreference = els.riderNavigationPreference.dataset.lastSyncedNavigationPreference; if (lastSyncedPreference && currentPreference !== normalizeRiderNavigationPreference(lastSyncedPreference)) { els.riderNavigationPreference.dataset.userSelectedNavigationPreference = currentPreference; rememberRiderNavigationPreferenceOverride(currentPreference); return currentPreference; } return null; } function syncRiderNavigationPreferenceInput(preference, { userSelected = false } = {}) { const normalized = normalizeRiderNavigationPreference(preference); if (!els.riderNavigationPreference) return normalized; els.riderNavigationPreference.value = normalized; els.riderNavigationPreference.dataset.lastSyncedNavigationPreference = normalized; if (userSelected) { els.riderNavigationPreference.dataset.userSelectedNavigationPreference = normalized; rememberStoredRiderNavigationPreferenceChoice(normalized); } return normalized; } function riderIdentityAliasesForRecord(rider) { const aliases = []; [rider?.id, rider?.supabaseUserId, rider?.riderId, rider?.userId, rider?.accountId, rider?.profileId].forEach((value) => { const normalized = String(value ?? "").trim(); if (normalized && !aliases.includes(normalized)) aliases.push(normalized); }); return aliases; } function currentRiderIdentityAliases() { const aliases = []; const add = (value) => { const normalized = String(value ?? "").trim(); if (normalized && !aliases.includes(normalized)) aliases.push(normalized); }; riderIdentityAliasesForRecord(state.rider).forEach(add); add(state.sessions?.rider?.userId); const current = currentRiderRecord(); riderIdentityAliasesForRecord(current).forEach(add); return aliases; } function riderRecordMatchesIdentityAliases(rider, aliases = []) { const lookup = aliases .map((value) => String(value ?? "").trim()) .filter(Boolean); if (!lookup.length) return false; return riderIdentityAliasesForRecord(rider).some((id) => lookup.includes(id)); } function riderNavigationPreference(rider = currentRiderRecord()) { const explicitRider = arguments.length > 0; const uiOverride = riderNavigationPreferenceUiOverride(); if (uiOverride && (!explicitRider || !state.rider || rider === state.rider || riderRecordMatchesIdentityAliases(rider, currentRiderIdentityAliases()))) { return uiOverride; } const riderIds = riderIdentityAliasesForRecord(rider); let override = riderIds.length ? riderNavigationPreferenceOverrideForRider(riderIds) : null; if (!override && !explicitRider) override = riderNavigationPreferenceOverrideForRider(); if (!override && explicitRider && riderRecordMatchesIdentityAliases(rider, currentRiderIdentityAliases())) { override = riderNavigationPreferenceOverrideForRider(); } if (override) return override; const documents = riderDocuments(rider); return normalizeRiderNavigationPreference(documents.navigationPreference ?? rider?.navigationPreference); } function riderNavigationPreferenceOverrideForRider(riderIdOrAliases) { const storedOverride = storedRiderNavigationPreferenceChoice(); const explicitRider = arguments.length > 0; const lookupIds = (explicitRider ? (Array.isArray(riderIdOrAliases) ? riderIdOrAliases : [riderIdOrAliases]) : currentRiderIdentityAliases()) .map((value) => String(value ?? "").trim()) .filter(Boolean); return riderNavigationPreferenceOverrideValue(storedOverride, lookupIds) ?? riderNavigationPreferenceOverrideValue(state.riderNavigationPreferenceOverride, lookupIds); } function riderNavigationPreferenceOverrideValue(override, lookupIds = []) { if (!override?.value) return null; const overrideIds = [ override.riderId, ...(Array.isArray(override.riderIds) ? override.riderIds : []) ] .map((value) => String(value ?? "").trim()) .filter(Boolean); if (overrideIds.length && !lookupIds.some((id) => overrideIds.includes(id))) return null; return normalizeRiderNavigationPreference(override.value); } function storedRiderNavigationPreferenceChoice() { const stored = readLocalJsonRecord(riderNavigationPreferenceChoiceStorageKey); if (!stored?.value) return null; return { riderId: String(stored.riderId || "").trim(), riderIds: Array.isArray(stored.riderIds) ? stored.riderIds.map((value) => String(value ?? "").trim()).filter(Boolean) : [], value: normalizeRiderNavigationPreference(stored.value), updatedAt: stored.updatedAt || null }; } function rememberStoredRiderNavigationPreferenceChoice(preference, riderId = currentRiderRecord()?.id) { const riderIds = [ riderId, ...currentRiderIdentityAliases() ] .map((value) => String(value ?? "").trim()) .filter(Boolean) .filter((value, index, values) => values.indexOf(value) === index) .slice(-8); writeLocalJsonRecord(riderNavigationPreferenceChoiceStorageKey, { riderId: riderIds[0] || "", riderIds, value: normalizeRiderNavigationPreference(preference), updatedAt: new Date().toISOString() }); } function rememberRiderNavigationPreferenceOverride(preference, riderId = currentRiderRecord()?.id) { const riderIds = [ riderId, ...currentRiderIdentityAliases() ] .map((value) => String(value ?? "").trim()) .filter(Boolean) .filter((value, index, values) => values.indexOf(value) === index); state.riderNavigationPreferenceOverride = { riderId: riderIds[0] || "", riderIds, value: normalizeRiderNavigationPreference(preference), updatedAt: new Date().toISOString() }; rememberStoredRiderNavigationPreferenceChoice(preference, riderId); } function normalizeRideStops(value) { const rawStops = Array.isArray(value) ? value : String(value ?? "").split(/\r?\n|;/); return rawStops .map((stop) => String(stop ?? "").replace(/\s+/g, " ").trim()) .filter(Boolean) .slice(0, rideStopsMaxCount) .map((stop) => stop.slice(0, rideStopMaxLength)); } function rawRideStopPoints(value) { if (Array.isArray(value)) return value; if (typeof value !== "string") return []; try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed : []; } catch { return []; } } function normalizeRideStopPoint(point, fallbackLabel = "") { const label = String(point?.label ?? fallbackLabel ?? "").replace(/\s+/g, " ").trim().slice(0, rideStopMaxLength); const latitude = Number(point?.latitude ?? point?.lat); const longitude = Number(point?.longitude ?? point?.lng); if (typeof validGpsCoordinate !== "function" || !validGpsCoordinate(latitude, longitude)) return null; return { label, latitude, longitude }; } function rideStopPointKey(label) { return typeof stopPlaceKey === "function" ? stopPlaceKey(label) : String(label ?? "").replace(/\s+/g, " ").trim().toLowerCase(); } function normalizeRideStopPoints(value, stops = []) { const normalizedStops = normalizeRideStops(stops); const points = rawRideStopPoints(value); return normalizedStops.map((stop, index) => normalizeRideStopPoint(points[index], stop)); } function rideStopPointsForRoute(stops, existingPoints = []) { const normalizedStops = normalizeRideStops(stops); const existingByLabel = new Map(); rawRideStopPoints(existingPoints).forEach((point) => { const normalized = normalizeRideStopPoint(point); const key = normalized ? rideStopPointKey(normalized.label) : ""; if (key) existingByLabel.set(key, normalized); }); return normalizedStops.map((stop) => { const existing = existingByLabel.get(rideStopPointKey(stop)); if (existing) return { ...existing, label: stop }; const place = typeof stopPlaceForRoute === "function" ? stopPlaceForRoute(stop) : null; return normalizeRideStopPoint(place, stop); }); } function rideStopPointsComplete(stops, points) { const normalizedStops = normalizeRideStops(stops); if (!normalizedStops.length) return true; const normalizedPoints = normalizeRideStopPoints(points, normalizedStops); return normalizedPoints.length === normalizedStops.length && normalizedPoints.every(Boolean); } function rideStopPointAt(request, index = 0) { return normalizeRideStopPoints(request?.rideStopPoints, request?.rideStops)[index] ?? null; } function rideStopsInputEnabled() { return Boolean(els.rideStops && !els.rideStops.disabled && els.rideStops.dataset.enabled === "true"); } function rideStopsFormValue() { return rideStopsInputEnabled() ? els.rideStops.value : ""; } function rideStopsInputValue(stops) { return normalizeRideStops(stops).join("\n"); } function rideStopsSummary(stops) { const normalized = normalizeRideStops(stops); if (!normalized.length) return "No added stops"; return `${normalized.length} stop${normalized.length === 1 ? "" : "s"}: ${normalized.join("; ")}`; } function formatDate(value) { if (!value) return "Not set"; return new Intl.DateTimeFormat("en", { month: "short", day: "numeric", year: "numeric" }).format(new Date(value)); } function parseDateOnly(value) { const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(value ?? "").trim()); if (!match) return null; const year = Number(match[1]); const month = Number(match[2]); const day = Number(match[3]); const date = new Date(year, month - 1, day); if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) return null; return date; } function daysUntilDate(value) { const date = parseDateOnly(value); if (!date) return null; const today = new Date(); const todayStart = new Date(today.getFullYear(), today.getMonth(), today.getDate()); return Math.ceil((date.getTime() - todayStart.getTime()) / (24 * 60 * 60 * 1000)); } function riderComplianceDateValue(value) { const text = String(value ?? "").trim(); if (!text) return ""; if (/^(not set|not captured|not provided|n\/a|na|none|null|undefined)$/i.test(text)) return ""; return text; } function riderComplianceItems(rider) { const items = [ { key: "driverLicense", label: "Driver's license", expiresOn: riderComplianceDateValue(rider?.driverLicenseExpiresOn), required: true } ]; const insuranceExpiresOn = riderComplianceDateValue(rider?.insuranceExpiresOn); if (insuranceExpiresOn) { items.push({ key: "insurance", label: "Insurance", expiresOn: insuranceExpiresOn, required: false }); } return items; } function riderExpiredComplianceItems(rider) { return riderComplianceItems(rider).filter((item) => { if (!String(item.expiresOn ?? "").trim()) return false; const days = daysUntilDate(item.expiresOn); return days === null || days < 0; }); } function riderUpcomingComplianceItems(rider, noticeDays = 30) { return riderComplianceItems(rider).filter((item) => { const days = daysUntilDate(item.expiresOn); return days !== null && days >= 0 && days <= noticeDays; }); } function riderComplianceReady(rider) { return riderExpiredComplianceItems(rider).length === 0; } function riderComplianceStatusText(rider) { const expired = riderExpiredComplianceItems(rider); if (expired.length) { return `Rider service is blocked until current ${expired.map((item) => item.label.toLowerCase()).join(" and ")} details are reviewed.`; } const upcoming = riderUpcomingComplianceItems(rider); if (upcoming.length) { return `Renewal reminder: ${upcoming.map((item) => `${item.label} expires ${formatDate(item.expiresOn)}`).join("; ")}.`; } return "No expired optional license or insurance dates are on file."; } function maskProviderReference(value) { const text = String(value ?? "").trim(); if (!text) return ""; if (text.length <= 10) return text; return `${text.slice(0, 7)}...${text.slice(-4)}`; } function formatDateOfBirthInput(value) { const digits = String(value ?? "").replace(/\D/g, "").slice(0, 8); const parts = [ digits.slice(0, 4), digits.slice(4, 6), digits.slice(6, 8) ].filter(Boolean); return parts.join("-"); } function validDateOfBirth(value) { const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value ?? "").trim()); if (!match) return false; const year = Number(match[1]); const month = Number(match[2]); const day = Number(match[3]); if (year < 1900) return false; const parsed = new Date(Date.UTC(year, month - 1, day)); const now = new Date(); const today = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate())); return parsed.getUTCFullYear() === year && parsed.getUTCMonth() === month - 1 && parsed.getUTCDate() === day && parsed <= today; } function normalizeDateOfBirthInput(input) { if (!input) return ""; input.value = formatDateOfBirthInput(input.value); return input.value; } function formatYearMonthInput(value) { const digits = String(value ?? "").replace(/\D/g, "").slice(0, 6); const parts = [digits.slice(0, 4), digits.slice(4, 6)].filter(Boolean); return parts.join("-"); } function validYearMonthOfBirth(value) { const match = /^(\d{4})-(\d{2})$/.exec(String(value ?? "").trim()); if (!match) return false; const year = Number(match[1]); const month = Number(match[2]); if (year < 1900 || month < 1 || month > 12) return false; const now = new Date(); const parsed = new Date(Date.UTC(year, month - 1, 1)); const thisMonth = new Date(Date.UTC(now.getFullYear(), now.getMonth(), 1)); return parsed <= thisMonth; } function normalizeYearMonthOfBirthInput(input) { if (!input) return ""; input.value = formatYearMonthInput(input.value); return input.value; } function yearMonthToStoredDate(value) { return validYearMonthOfBirth(value) ? `${value}-01` : ""; } function storedDateToYearMonth(value) { return String(value ?? "").slice(0, 7); } function wireYearMonthInput(input) { if (!input) return; input.addEventListener("input", () => { const cursorWasAtEnd = input.selectionStart === input.value.length; input.value = formatYearMonthInput(input.value); if (cursorWasAtEnd && typeof input.setSelectionRange === "function") { input.setSelectionRange(input.value.length, input.value.length); } }); input.addEventListener("blur", () => { input.value = formatYearMonthInput(input.value); }); } function wireDateOfBirthInput(input) { if (!input) return; input.addEventListener("input", () => { const cursorWasAtEnd = input.selectionStart === input.value.length; input.value = formatDateOfBirthInput(input.value); if (cursorWasAtEnd && typeof input.setSelectionRange === "function") { input.setSelectionRange(input.value.length, input.value.length); } }); input.addEventListener("blur", () => { input.value = formatDateOfBirthInput(input.value); }); } function formFieldLabel(field) { const label = field.closest("label"); const explicitLabel = label?.querySelector("span")?.textContent || label?.textContent || ""; return explicitLabel .replace(/\s+/g, " ") .trim() .replace(/(Send code|Verify|Submit|Save).*$/i, "") .trim() || field.placeholder || field.id || "field"; } function invalidAccountFields(form) { return [...form.querySelectorAll("input, select, textarea")] .filter((field) => !field.disabled && field.willValidate && !field.checkValidity()); } function summarizeInvalidFields(fields) { const labels = fields.slice(0, 4).map(formFieldLabel); if (fields.length > labels.length) labels.push(`${fields.length - labels.length} more`); return labels.join(", "); } function validateAccountForm(form, statusNode) { const invalidFields = invalidAccountFields(form); if (!invalidFields.length) return true; setTranslatedStatus(statusNode, "accountMissingFields", { fields: summarizeInvalidFields(invalidFields) }); invalidFields[0].focus({ preventScroll: false }); return false; } function pendingProfileRecoveryForRole(type) { const recovery = storageSafePendingProfileRecovery(state.pendingProfileRecovery); return recovery?.role === type ? recovery : null; } function setPendingProfileRecovery(type, user, email) { const metadata = user?.user_metadata ?? {}; state.pendingProfileRecovery = storageSafePendingProfileRecovery({ role: type, userId: user?.id ?? null, email: email || authUserEmail(user), phone: user?.phone ?? metadata.phone ?? "", name: metadata.full_name ?? metadata.name ?? "", startedAt: new Date().toISOString() }); return state.pendingProfileRecovery; } function clearPendingProfileRecovery(type = null) { if (!state.pendingProfileRecovery) return; if (type && state.pendingProfileRecovery.role !== type) return; state.pendingProfileRecovery = null; } function updateAccountPhoneVerificationControls() { const relaxed = smsVerificationRelaxedForTesting(); [ els.passengerVerificationCode?.closest(".verification-row"), els.riderVerificationCode?.closest(".verification-row") ].filter(Boolean).forEach((row) => { row.hidden = relaxed; }); } function setButtonBusy(button, busy) { if (!button) return; button.disabled = busy; button.setAttribute("aria-busy", String(busy)); } function formatDateTime(value) { if (!value) return "Not scheduled"; return new Intl.DateTimeFormat("en", { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" }).format(new Date(value)); } function countryNames() { const first = defaultLaunchCountry(); return [ first, ...Object.keys(countryCities).filter((country) => country !== first) ]; } function cityNames(country = defaultLaunchCountry()) { return Object.keys(countries[country] ?? {}); } function selectedRidePickupCity() { const selectedCountry = state.passenger?.country ?? els.passengerCountry?.value ?? els.passengerActiveCountry?.value ?? defaultLaunchCountry(); const country = enabledLaunchCountries().includes(selectedCountry) ? selectedCountry : defaultLaunchCountry(); const city = els.pickupCity?.value ?? state.passenger?.city ?? els.passengerCity?.value ?? els.passengerActiveCity?.value ?? defaultLaunchCity(country); return cityNames(country).includes(city) ? city : defaultLaunchCity(country); } function locationSubdivisionLabel(country = defaultLaunchCountry()) { return country === "United States" ? "state" : "city"; } function areas(country = defaultLaunchCountry(), city = defaultLaunchCity(country)) { return countries[country]?.[city] ?? []; } function findArea(country, city, name) { return areas(country, city).find((area) => area.name === name) ?? areas(country, city)[0]; } function areaDistanceUnits(firstArea, secondArea) { if (!firstArea || !secondArea) return null; return Math.hypot(firstArea.x - secondArea.x, firstArea.y - secondArea.y); } function citySpanKm(country, city) { return cityDistanceSpanKm[country]?.[city] ?? defaultCitySpanKm; } function estimatedAreaDistanceKm(country, city, firstArea, secondArea) { const distance = areaDistanceUnits(firstArea, secondArea); if (distance == null) return null; return (distance / 100) * citySpanKm(country, city); } function formatDistanceKm(value) { if (value == null) return "distance not estimated"; if (value < 0.2) return "same pickup area"; if (value < 1) return `${Math.round(value * 1000)} m away`; return `${value.toFixed(value < 10 ? 1 : 0)} km away`; } function formatDistanceMiles(value) { if (value == null || !Number.isFinite(Number(value))) return "distance not estimated"; if (value < 0.2) return "same pickup area"; if (value < 1) return `${Math.round(value * 5280)} ft away`; return `${value.toFixed(value < 10 ? 1 : 0)} mi away`; } function pickupEtaMinutes(distanceKm, rider = currentRiderRecord()) { if (distanceKm == null || !Number.isFinite(Number(distanceKm))) return null; const speedKmh = riderPickupEtaSpeedKmh[rider?.vehicle] ?? riderPickupEtaSpeedKmh.car; return Math.max(2, Math.ceil((Number(distanceKm) * riderPickupEtaRoadFactor * 60) / speedKmh)); } function formatPickupEta(minutes) { if (minutes == null) return "pickup ETA not estimated"; if (minutes < 60) return `about ${minutes} min pickup`; const hours = Math.floor(minutes / 60); const remainder = minutes % 60; return remainder ? `about ${hours} hr ${remainder} min pickup` : `about ${hours} hr pickup`; } function populateLocationFields() { const countriesList = countryNames(); const passengerCountry = countriesList.includes(state.passenger?.country) ? state.passenger.country : defaultLaunchCountry(); populateSelect(els.passengerCountry, countriesList, passengerCountry); populateSelect(els.passengerActiveCountry, countriesList, passengerCountry); const passengerCity = state.passenger?.city ?? cityNames(passengerCountry)[0]; populateSelect(els.passengerCity, cityNames(passengerCountry), passengerCity); populateSelect(els.passengerActiveCity, cityNames(passengerCountry), passengerCity); const pickupCity = cityNames(passengerCountry).includes(els.pickupCity?.value) ? els.pickupCity.value : passengerCity; populateSelect(els.pickupCity, cityNames(passengerCountry), pickupCity); populateSelect(els.pickupArea, areas(passengerCountry, pickupCity).map((area) => area.name), areas(passengerCountry, pickupCity)[0]?.name); populateSelect(els.destinationArea, areas(passengerCountry, pickupCity).map((area) => area.name), areas(passengerCountry, pickupCity)[1]?.name ?? areas(passengerCountry, pickupCity)[0]?.name); populateSelectOptions(els.vehiclePreference, carTypePreferenceOptions, normalizeCarTypePreference(els.vehiclePreference?.value)); updateRidePaymentOptions(passengerCountry); updateFareGuidance(); const riderCountry = countriesList.includes(state.rider?.country) ? state.rider.country : passengerCountry; const riderCity = state.rider?.city ?? cityNames(riderCountry)[0]; populateSelect(els.riderCountry, countriesList, riderCountry); populateSelect(els.riderCity, cityNames(riderCountry), riderCity); populateSelect(els.riderArea, areas(riderCountry, riderCity).map((area) => area.name), state.rider?.area ?? areas(riderCountry, riderCity)[0]?.name); populateSelect(els.riderActiveCountry, countriesList, riderCountry); populateSelect(els.riderActiveCity, cityNames(riderCountry), riderCity); populateSelect(els.riderActiveArea, areas(riderCountry, riderCity).map((area) => area.name), state.rider?.area ?? areas(riderCountry, riderCity)[0]?.name); populateRiderDailyRegionOptions(riderCountry, riderCity); populateVehicleCatalogFields(state.rider); } function hydrateForms() { if (els.languageSelect) els.languageSelect.value = state.language; els.languageSelects?.forEach((select) => { select.value = state.language; }); updateAccountPhoneVerificationControls(); updatePassengerInitialBusinessFields(); const invitedReferralCode = new URLSearchParams(window.location.search).get("ref") || new URLSearchParams(String(window.location.hash).split("?")[1] || "").get("ref") || ""; if (invitedReferralCode && !state.passenger && els.passengerReferralCode && !els.passengerReferralCode.value) { els.passengerReferralCode.value = invitedReferralCode; } if (invitedReferralCode && !state.rider && els.riderReferralCode && !els.riderReferralCode.value) { els.riderReferralCode.value = invitedReferralCode; } if (invitedReferralCode && els.businessReferralCode && !els.businessReferralCode.value) { els.businessReferralCode.value = invitedReferralCode; } if (invitedReferralCode && els.passengerInitialBusinessReferralCode && !els.passengerInitialBusinessReferralCode.value) { els.passengerInitialBusinessReferralCode.value = invitedReferralCode; } if (state.sessions.passenger) { els.passengerSignInEmail.value = state.sessions.passenger.email ?? ""; els.passengerSignInPhone.value = state.sessions.passenger.phone ?? ""; setTranslatedStatus(els.passengerSignInStatus, "signedInAs", { identity: sessionDisplayIdentity("passenger") }); } if (state.passenger) { els.passengerName.value = state.passenger.name; els.passengerEmail.value = state.passenger.email ?? ""; els.passengerPhone.value = state.passenger.phone; els.passengerNationalId.value = state.passenger.nationalId ?? ""; els.passengerDob.value = state.passenger.dateOfBirth ?? ""; els.passengerCountry.value = state.passenger.country; els.passengerCity.value = state.passenger.city; els.passengerActiveCountry.value = state.passenger.country; els.passengerActiveCity.value = state.passenger.city; if (els.pickupCity && !els.pickupCity.value) els.pickupCity.value = state.passenger.city; const passengerSubdivision = locationSubdivisionLabel(state.passenger.country); const passengerPhoneStatus = smsVerificationRelaxedForTesting() ? "Phone verification is relaxed for this staging pilot." : "Phone verified."; els.passengerStatus.textContent = `${state.passenger.name} is ready to request rides. ${passengerPhoneStatus}`; els.passengerLocationStatus.textContent = `Default account ${passengerSubdivision}: ${state.passenger.city}, ${state.passenger.country}. Each ride can choose its own pickup city and area before publishing.`; const passengerPayment = paymentAccountFor("passenger", state.passenger.id); if (passengerPayment) { els.passengerPaymentProvider.value = passengerPayment.provider; els.passengerBankName.value = passengerPayment.institutionName ?? ""; els.passengerAccountHolder.value = passengerPayment.accountHolder ?? ""; els.passengerAccountLast4.value = passengerPayment.accountLast4 ?? ""; els.passengerPaymentReference.value = passengerPayment.reference ?? ""; els.passengerPaymentStatus.textContent = paymentAccountSummary("passenger", state.passenger); } } const passengerRecovery = pendingProfileRecoveryForRole("passenger"); if (!state.passenger && passengerRecovery) { if (passengerRecovery.name && !els.passengerName.value) els.passengerName.value = passengerRecovery.name; if (passengerRecovery.email) els.passengerEmail.value = passengerRecovery.email; if (passengerRecovery.phone && !els.passengerPhone.value) els.passengerPhone.value = passengerRecovery.phone; setTranslatedStatus(els.passengerStatus, "supabaseProfileMissing"); } if (typeof updatePassengerFareModeControls === "function") updatePassengerFareModeControls(); if (state.sessions.rider) { els.riderSignInEmail.value = state.sessions.rider.email ?? ""; els.riderSignInPhone.value = state.sessions.rider.phone ?? ""; setTranslatedStatus(els.riderSignInStatus, "signedInAs", { identity: sessionDisplayIdentity("rider") }); } if (state.rider) { els.riderName.value = state.rider.name; els.riderEmail.value = state.rider.email ?? ""; els.riderPhone.value = state.rider.phone; els.riderNationalId.value = state.rider.nationalId ?? ""; els.riderDob.value = storedDateToYearMonth(state.rider.dateOfBirth) || state.rider.dateOfBirth || ""; els.riderVehicle.value = normalizeRideVehicle(state.rider.vehicle); populateVehicleCatalogFields(state.rider); els.riderCarMake.value = state.rider.carMake ?? els.riderCarMake.value; populateSelect(els.riderCarModel, carMakeCatalog[els.riderCarMake.value] ?? carMakeCatalog.Other, state.rider.carModel); els.riderCarBodyType.value = normalizeCarBodyType(state.rider.carBodyType); if (els.riderVehicleDesignation) { populateRiderVehicleDesignationOptions(state.rider); els.riderVehicleDesignation.value = normalizeRiderVehicleDesignation(state.rider.vehicleDesignation, state.rider.carBodyType); } syncRiderNavigationPreferenceInput(riderNavigationPreference(state.rider)); els.riderCarYear.value = String(state.rider.carYear ?? els.riderCarYear.value); els.riderCarColor.value = state.rider.carColor ?? ""; els.riderCountry.value = state.rider.country; els.riderCity.value = state.rider.city; updateRiderAreas(); els.riderArea.value = state.rider.area; els.riderActiveCountry.value = state.rider.country; els.riderActiveCity.value = state.rider.city; updateRiderActiveAreas(); els.riderActiveArea.value = state.rider.area; if (els.riderCredential) els.riderCredential.value = state.rider.credential; if (els.riderLicenseExpiresOn) els.riderLicenseExpiresOn.value = state.rider.driverLicenseExpiresOn ?? ""; els.riderVehicleVin.value = state.rider.vehicleVin ?? ""; els.riderRegistration.value = state.rider.registration; els.riderInsuranceProvider.value = state.rider.insuranceProvider ?? ""; els.riderInsuranceNumber.value = state.rider.insuranceNumber ?? ""; if (els.riderInsuranceExpiresOn) els.riderInsuranceExpiresOn.value = state.rider.insuranceExpiresOn ?? ""; if (els.riderBackgroundConsent) els.riderBackgroundConsent.checked = state.rider.backgroundCheckConsentAt !== null; els.riderLocationStatus.textContent = riderServiceAreaSummary(state.rider); if (typeof renderRiderAvailabilityControls === "function") { renderRiderAvailabilityControls(state.rider); } else { els.riderGpsStatus.textContent = riderCurrentFreshGps(state.rider) ? "Online and available." : "Offline. Activate when ready."; } const riderPayment = paymentAccountFor("rider", state.rider.id); if (riderPayment) { if (els.riderPaymentProvider) els.riderPaymentProvider.value = riderPayment.provider; els.riderBankName.value = riderPayment.institutionName ?? ""; els.riderAccountHolder.value = riderPayment.accountHolder ?? ""; els.riderAccountLast4.value = riderPayment.accountLast4 ?? ""; if (els.riderPaymentReference) els.riderPaymentReference.value = riderPayment.reference ?? ""; els.riderPaymentStatus.textContent = paymentAccountSummary("rider", state.rider); } populateRiderDailyRegionOptions(state.rider.country, state.rider.city); renderRiderDailyRegionStatus(state.rider); } const riderRecovery = pendingProfileRecoveryForRole("rider"); if (!state.rider && riderRecovery) { if (riderRecovery.name && !els.riderName.value) els.riderName.value = riderRecovery.name; if (riderRecovery.email) els.riderEmail.value = riderRecovery.email; if (riderRecovery.phone && !els.riderPhone.value) els.riderPhone.value = riderRecovery.phone; setTranslatedStatus(els.riderStatus, "supabaseProfileMissing"); } if (typeof renderPassengerTurnstileChallenge === "function") void renderPassengerTurnstileChallenge(); if (typeof renderRiderTurnstileChallenge === "function") void renderRiderTurnstileChallenge(); } function sessionDisplayIdentity(type) { const session = state.sessions?.[type] ?? {}; const account = type === "rider" ? state.rider : state.passenger; return session.email ?? account?.email ?? session.phone ?? account?.phone ?? "this Waka account"; } function updateConnectionStatus() { const statusPill = els.connectionStatus?.closest(".status-pill"); if (statusPill) statusPill.hidden = true; if (appConfig.mode === "supabase") { if (supabaseClient) { setTranslatedStatus(els.connectionStatus, "supabaseReady"); return; } if (!appConfig.supabaseUrl || !appConfig.supabaseAnonKey) { setTranslatedStatus(els.connectionStatus, "supabaseConfigNeeded"); return; } if (!window.supabase?.createClient) { els.connectionStatus.textContent = "Supabase auth ready"; return; } setTranslatedStatus(els.connectionStatus, window.supabase?.createClient ? "supabaseConnecting" : "supabaseSdkUnavailable"); return; } setTranslatedStatus(els.connectionStatus, navigator.onLine ? "onlineDemo" : "offlineReady"); } function updateInstallButton() { const standalone = window.matchMedia("(display-mode: standalone)").matches || navigator.standalone; els.installApp.hidden = true; els.installApp.disabled = standalone; els.installApp.textContent = standalone ? translatedValue("installed") : translatedValue("installApp"); } function makeVerificationCode() { return String(Math.floor(100000 + Math.random() * 900000)); } function phoneOtpErrorMessage(error) { if (/unsupported phone provider/i.test(error.message)) { return "Phone OTP is not enabled in Supabase. Configure an SMS provider in Auth > Providers > Phone, or use manual pilot verification before public launch."; } if (error.status === 429 || /rate limit|too many/i.test(error.message)) { return translatedMessage("phoneOtpRateLimited"); } return error.message; } function profileAvailabilityErrorMessage(error) { const message = String(error?.message || error || ""); if (error?.status === 429 || /too many account checks|rate limit|too many/i.test(message)) { return "Too many account checks. Wait a few minutes before trying again."; } return ""; } function phoneOtpCooldownKey(type, phone) { return `${type}:${phone}`; } function phoneOtpCooldownSeconds(type, phone) { const availableAt = phoneOtpCooldowns.get(phoneOtpCooldownKey(type, phone)) ?? 0; return Math.max(0, Math.ceil((availableAt - Date.now()) / 1000)); } function startPhoneOtpCooldown(type, phone) { phoneOtpCooldowns.set(phoneOtpCooldownKey(type, phone), Date.now() + phoneOtpCooldownMs); } function clearPhoneOtpCooldown(type, phone) { phoneOtpCooldowns.delete(phoneOtpCooldownKey(type, phone)); } function phoneDigits(value) { return String(value ?? "").replace(/\D/g, ""); } function phoneMatches(first, second) { const firstDigits = phoneDigits(first); const secondDigits = phoneDigits(second); if (!firstDigits || !secondDigits) return false; if (firstDigits === secondDigits) return true; if (firstDigits.length < 8 || secondDigits.length < 8) return false; return firstDigits.endsWith(secondDigits) || secondDigits.endsWith(firstDigits); } async function updatePassengerFareOffer(event, requestId) { event.preventDefault(); const form = event.currentTarget; const status = form.querySelector(".fare-boost-status"); const input = form.querySelector(".fare-boost-input"); const request = state.requests.find((item) => item.id === requestId); if (!canBoostPassengerFare(request)) { status.textContent = "Only open requests from this passenger can be updated."; return; } const rawFareDraft = String(input?.value ?? passengerFareBoostDrafts.get(requestId) ?? ""); passengerFareBoostDrafts.set(requestId, rawFareDraft); const nextFare = Number(rawFareDraft.replace(/[^\d]/g, "")); if (!nextFare || nextFare <= request.fareOffer) { status.textContent = `Enter a fare higher than ${formatMoney(request.fareOffer)}.`; return; } try { status.textContent = "Updating fare..."; await updateRideRequestFareInSupabase(request.id, nextFare); state.requests = state.requests.map((item) => item.id === request.id ? { ...item, fareOffer: nextFare } : item); passengerFareBoostDrafts.delete(request.id); passengerFareBoostLastFocus = null; if (typeof passengerFareBoostOpenRequestId !== "undefined") passengerFareBoostOpenRequestId = null; if (input instanceof HTMLInputElement) input.blur(); pushSystemChat(request.id, `Passenger increased the fare offer to ${formatMoney(nextFare)}.`); saveState(); renderAll(); void refreshMarketplace({ silent: true }); } catch (error) { status.textContent = error.message; } } // Supabase runtime configuration, authentication, profile, storage, and row mapping helpers. let supabaseClient = null; let supabaseSdkPromise = null; let supabaseRestSession = null; let passwordResetRoleCapturedBeforeSupabaseInit = ""; let riderProfileHydrationInFlight = null; const riderProfileHydrationRefreshMs = 15000; let riderProfileHydrationRefreshAt = 0; let insuranceTelemetryRpcUnavailable = false; let lastInsuranceTelemetrySource = "not used"; function mapRideRequestFromDatabase(request, profileMap = new Map(), offerMap = new Map()) { const passenger = profileMap.get(request.passenger_id); const selectedOffer = offerMap.get(request.selected_offer_id); const selectedRider = selectedOffer ? profileMap.get(selectedOffer.riderId) : null; const pickupGps = gpsPointFromDatabaseLocation( request.pickup_location, request.pickup_gps_accuracy_meters, request.pickup_gps_captured_at ) ?? normalizeGpsPoint({ latitude: request.pickup_latitude ?? request.pickup_lat, longitude: request.pickup_longitude ?? request.pickup_lng ?? request.pickup_lon, accuracyMeters: request.pickup_gps_accuracy_meters, capturedAt: request.pickup_gps_captured_at }); const pickupLocationShared = Boolean(request.pickup_location || pickupGps); return { id: request.id, passengerId: request.passenger_id, passengerName: passenger?.full_name ?? state.passenger?.name ?? "Passenger", passengerPhone: "Hidden by Waka relay", businessAccountId: request.business_account_id ?? null, country: request.country, city: request.city, pickupArea: request.pickup_area, pickupDescription: request.pickup_description, destinationArea: request.destination_area ?? null, destination: request.destination, destinationPlaceId: request.destination_place_id ?? null, destinationFormattedAddress: request.destination_formatted_address ?? null, destinationLatitude: request.destination_lat ?? null, destinationLongitude: request.destination_lng ?? null, vehicle: normalizeRideVehicle(request.vehicle_preference), carTypePreference: normalizeRideVehicle(request.vehicle_preference) === "bike" ? "bike" : normalizeCarTypePreference(request.car_type_preference), rideStops: normalizeRideStops(request.ride_stops), rideStopPoints: normalizeRideStopPoints(request.ride_stop_points, request.ride_stops), passengerCount: normalizeRidePassengerCount(request.passenger_count), luggageCount: normalizeRideLuggageCount(request.luggage_count), luggageNote: normalizeRideLuggageNote(request.luggage_note), currentStopIndex: Math.max(0, Number(request.current_stop_index ?? 0) || 0), lastStopArrivedAt: request.last_stop_arrived_at ?? null, estimatedDistanceMiles: request.estimated_distance_miles ?? null, estimatedTravelMinutes: request.estimated_travel_minutes ?? null, acceptedRouteChangeFare: request.accepted_route_change_fare ?? 0, routeEstimateSource: request.route_estimate_source ?? null, routeEstimateProvider: request.route_estimate_provider ?? null, routeEstimateCached: Boolean(request.route_estimate_cached), routeEstimateKey: request.route_estimate_key ?? null, routeEstimatePolyline: request.route_estimate_polyline ?? null, routeEstimateCreatedAt: request.route_estimate_created_at ?? null, routeEstimateDestinationFingerprint: request.route_estimate_destination_fingerprint ?? null, fareOffer: request.fare_offer_xaf, fareMode: normalizePassengerFareMode(request.fare_mode), fareHistory: Array.isArray(request.fare_history) ? request.fare_history : [], paymentPreference: paymentFromDatabase(request.payment_preference), rideTiming: request.scheduled_at ? "scheduled" : "now", scheduledAt: request.scheduled_at ?? null, riderConfirmationStatus: request.rider_confirmation_status ?? null, riderConfirmationRequestedAt: request.rider_confirmation_requested_at ?? null, riderConfirmedAt: request.rider_confirmed_at ?? null, releasedAt: request.released_at ?? null, status: request.status, selectedOfferId: request.selected_offer_id, agreedFare: selectedOffer?.fare ?? null, selectedRiderId: request.selected_rider_id ?? selectedOffer?.riderId ?? null, selectedRiderName: selectedRider?.full_name ?? null, cancellationFeeAmount: request.cancellation_fee_amount ?? 0, cancellationFeeCurrency: request.cancellation_fee_currency ?? moneyCurrencyForCountry(request.country), cancellationFeeStatus: request.cancellation_fee_status ?? "not_applicable", cancellationFeeRiderId: request.cancellation_fee_rider_id ?? null, cancellationFeeElapsedMinutes: request.cancellation_fee_elapsed_minutes ?? null, createdAt: request.created_at, matchedAt: request.matched_at, arrivedAt: request.arrived_at ?? null, startedAt: request.started_at ?? null, completedAt: request.completed_at ?? null, gpsDistanceMeters: request.gps_distance_meters ?? null, matchSource: request.match_source ?? null, pickupLocationShared, pickupGps, pickupLatitude: pickupGps?.latitude ?? null, pickupLongitude: pickupGps?.longitude ?? null, pickupGpsAccuracyMeters: request.pickup_gps_accuracy_meters ?? null, pickupGpsCapturedAt: request.pickup_gps_captured_at ?? null, cancelledBy: request.cancelled_by ?? null, cancelledAt: request.cancelled_at ?? null, cancelReason: request.cancel_reason ?? null, cancellationFeeAmount: request.cancellation_fee_amount ?? 0, cancellationFeeCurrency: request.cancellation_fee_currency ?? moneyCurrencyForCountry(request.country), cancellationFeeStatus: request.cancellation_fee_status ?? "not_applicable", cancellationFeeRiderId: request.cancellation_fee_rider_id ?? null, cancellationFeeElapsedMinutes: request.cancellation_fee_elapsed_minutes ?? null }; } function mapOfferFromDatabase(offer) { return { id: offer.id, requestId: offer.ride_request_id, riderId: offer.rider_id, fare: offer.fare_xaf, type: offer.type, note: offer.public_note ?? "", pickupDistanceMeters: offer.pickup_distance_meters ?? null, distanceSource: offer.distance_source ?? null, fareHistory: Array.isArray(offer.fare_history) ? offer.fare_history : [], createdAt: offer.created_at, updatedAt: offer.updated_at ?? offer.created_at }; } function mapPassengerApproachFromDatabase(row) { const riderApproachGps = normalizeGpsPoint({ latitude: row.rider_lat, longitude: row.rider_lng, accuracyMeters: row.accuracy_meters, capturedAt: row.captured_at }); return { requestId: row.request_id, selectedRiderId: row.rider_id, selectedRiderName: firstNameOnly(row.rider_name, "Rider"), riderApproachDistanceMeters: row.pickup_distance_meters ?? null, riderApproachSource: row.distance_source ?? null, riderApproachGps, riderApproachLatitude: riderApproachGps?.latitude ?? null, riderApproachLongitude: riderApproachGps?.longitude ?? null, riderApproachAccuracyMeters: row.accuracy_meters ?? null, riderApproachCapturedAt: row.captured_at ?? null, riderApproachIsLive: Boolean(row.is_live) }; } function mapActiveRideContactFromDatabase(row) { return { requestId: row.request_id, contactUserId: row.counterparty_id, contactName: firstNameOnly(row.counterparty_name, "Matched contact"), contactProfilePhotoPath: row.counterparty_profile_photo_path ?? "", contactPhone: "", contactRelayPhone: row.relay_phone ?? "", contactRelayStatus: row.relay_status ?? "relay_not_configured", contactProviderSessionId: row.provider_session_id ?? "" }; } function mapRiderCompletedMileageSegmentFromDatabase(row = {}) { return { id: row.id, riderId: row.rider_id, requestId: row.ride_request_id, period: row.insurance_period, status: row.status, distanceMiles: Number(row.distance_miles ?? 0), startedAt: row.started_at, endedAt: row.ended_at, eventCount: Number(row.event_count ?? 0), source: row.source ?? "" }; } function mapChatFromDatabase(message) { const parsed = parseRouteChangeEventText(message.body); const systemLike = parsed || /^\[System\]/i.test(String(message.body ?? "")); const senderRole = ["passenger", "rider"].includes(message.sender_role) ? message.sender_role : ""; return { id: message.id, requestId: message.ride_request_id, senderId: message.sender_id, sender: systemLike ? "system" : senderRole || (message.sender_id === state.rider?.id ? "rider" : message.sender_id === state.passenger?.id ? "passenger" : "system"), text: parsed?.message ?? String(message.body ?? "").replace(/^\[System\]\s*/i, ""), mediaType: message.media_type ?? null, mediaBucket: message.media_bucket ?? null, mediaPath: message.media_path ?? null, mediaMimeType: message.media_mime_type ?? null, mediaDurationSeconds: message.media_duration_seconds ?? null, mediaSizeBytes: message.media_size_bytes ?? null, routeChangeEvent: parsed?.event ?? null, deliveryStatus: "sent", createdAt: message.created_at }; } function mapRideRouteChangeFromDatabase(row = {}) { const payload = row.change_payload && typeof row.change_payload === "object" ? row.change_payload : {}; const stops = normalizeRideStops(payload.rideStops ?? payload.ride_stops ?? []); const stopPoints = normalizeRideStopPoints(payload.rideStopPoints ?? payload.ride_stop_points ?? [], stops); return { id: payload.id ?? row.change_id, requestId: payload.requestId ?? row.ride_request_id, type: payload.type ?? row.change_type, country: payload.country ?? null, destinationArea: payload.destinationArea ?? payload.destination_area ?? null, destination: payload.destination ?? null, destinationPlaceId: payload.destinationPlaceId ?? null, destinationFormattedAddress: payload.destinationFormattedAddress ?? null, destinationLatitude: payload.destinationLatitude ?? null, destinationLongitude: payload.destinationLongitude ?? null, rideStops: stops, rideStopPoints: stopPoints, routeEstimate: payload.routeEstimate ?? null, routeDelta: payload.routeDelta ?? null, additionalFare: Number(payload.additionalFare ?? row.additional_fare ?? 0) || 0, totalFare: Number(payload.totalFare ?? row.total_fare ?? 0) || 0, acceptedRouteChangeFare: Number(payload.acceptedRouteChangeFare ?? 0) || 0, requestedAt: payload.requestedAt ?? row.created_at, decidedAt: payload.decidedAt ?? (row.status === "pending" ? null : row.decided_at), status: ["pending", "accepted", "declined"].includes(row.status) ? row.status : payload.status ?? "pending", passengerId: payload.passengerId ?? row.passenger_id ?? null, riderId: payload.riderId ?? row.rider_id ?? null }; } function profileToPassenger(profile) { return { id: profile.id, supabaseUserId: profile.id, name: profile.full_name, email: profile.email, phone: profile.phone, phoneVerified: Boolean(profile.phone_verified_at), phoneVerifiedAt: profile.phone_verified_at, nationalId: profile.national_id_number, dateOfBirth: profile.date_of_birth, preferredLanguage: profile.preferred_language, country: profile.country, city: profile.city, profilePhotoPath: profile.profile_photo_path, accountStatus: profile.account_status ?? "active", accountStatusReason: profile.account_status_reason ?? "", accountStatusChangedAt: profile.account_status_changed_at ?? null, accountStatusChangedBy: profile.account_status_changed_by ?? null, accountClosedAt: profile.account_closed_at ?? null, createdAt: profile.created_at }; } function mapAdminNotificationFromDatabase(notification) { const deliveryChannels = normalizeNotificationDeliveryChannels(notification.delivery_channels); return { id: notification.id, recipientId: notification.recipient_id, recipientRole: notification.recipient_role, title: notification.title ?? "Waka notice", body: notification.body, createdBy: notification.created_by, createdByRole: notification.created_by_role ?? null, requestId: notification.request_id ?? null, actionUrl: notification.action_url ?? "", eventType: notification.event_type ?? "", deliveryChannels, deliveryStatus: notification.delivery_status ?? {}, createdAt: notification.created_at, readAt: notification.read_at ?? null }; } function normalizeNotificationDeliveryChannels(value) { const raw = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : []; const channels = raw .map((item) => String(item ?? "").trim().toLowerCase()) .filter((item) => ["in_app", "push", "email", "sms"].includes(item)); if (!channels.includes("in_app")) channels.unshift("in_app"); return [...new Set(channels)]; } function notificationDeliveryLabel(channels = []) { const normalized = normalizeNotificationDeliveryChannels(channels); const labels = { in_app: "in-app", push: "phone push", email: "email", sms: "SMS" }; return normalized.map((channel) => labels[channel] ?? channel).join(", "); } function mapSupportTicketFromDatabase(ticket, profileMap = new Map()) { const account = profileMap.get(ticket.account_id); const reviewer = profileMap.get(ticket.reviewed_by); return { id: ticket.id, accountId: ticket.account_id, accountRole: ticket.account_role, accountName: account?.full_name ?? account?.email ?? "Account", category: ticket.category, subject: ticket.subject ?? "Support request", message: ticket.message, priority: ticket.priority ?? "medium", status: ticket.status ?? "open", reviewedBy: ticket.reviewed_by ?? null, reviewedByName: reviewer?.full_name ?? reviewer?.email ?? "", reviewedAt: ticket.reviewed_at ?? null, createdAt: ticket.created_at, updatedAt: ticket.updated_at ?? ticket.created_at }; } function profileAccountStatus(profile) { return profile?.account_status ?? profile?.accountStatus ?? "active"; } function profileAccountIsBlocked(profile) { return ["suspended", "closed"].includes(profileAccountStatus(profile)); } function profileAccountBlockedMessage(profile) { const status = profileAccountStatus(profile); const reason = profile?.account_status_reason ?? profile?.accountStatusReason ?? ""; const base = status === "closed" ? "This Waka account has been closed by admin." : "This Waka account activity is suspended by admin."; return reason ? `${base} Reason: ${reason}` : `${base} Contact Waka support for review.`; } function directoryRowToPassenger(row) { return profileToPassenger({ id: row.id, full_name: row.full_name, email: row.email, phone: row.phone, phone_verified_at: row.phone_verified_at, national_id_number: row.national_id_number, date_of_birth: row.date_of_birth, preferred_language: row.preferred_language, country: row.country, city: row.city, profile_photo_path: row.profile_photo_path, account_status: row.account_status, account_status_reason: row.account_status_reason, account_status_changed_at: row.account_status_changed_at, account_status_changed_by: row.account_status_changed_by, account_closed_at: row.account_closed_at, created_at: row.created_at }); } function directoryRowToRider(row) { const documents = parseRiderDocuments(row.document_path); const vehicleDesignation = normalizeRiderVehicleDesignation(row.vehicle_designation ?? documents.vehicleDesignation, row.car_body_type); return { ...directoryRowToPassenger(row), area: row.operating_area ?? "No application", vehicle: row.vehicle ?? "not set", credential: row.credential_number ?? "No application", registration: row.vehicle_registration ?? "No application", carMake: row.car_make ?? "", carModel: row.car_model ?? "", carBodyType: normalizeCarBodyType(row.car_body_type), vehicleDesignation, navigationPreference: normalizeRiderNavigationPreference(documents.navigationPreference), carYear: row.car_year ?? "", carColor: row.car_color ?? "", vehicleVin: row.vehicle_vin ?? "", insuranceProvider: row.insurance_provider ?? "", insuranceNumber: row.insurance_number ?? "", driverLicenseExpiresOn: row.driver_license_expires_on ?? "", insuranceExpiresOn: row.insurance_expires_on ?? "", complianceSuspendedAt: row.compliance_suspended_at ?? null, complianceSuspensionReason: row.compliance_suspension_reason ?? "", backgroundCheckConsentAt: row.background_check_consent_at ?? null, backgroundCheckProvider: row.background_check_consent_provider ?? row.background_check_provider ?? "", backgroundCheckConsentVersion: row.background_check_consent_version ?? "", backgroundCheckStatus: row.background_check_status ?? "not requested", backgroundCheckDecision: row.background_check_decision ?? "pending", documentName: row.document_path ?? "", documents, driverLicenseDocumentPath: documents.driverLicense, vehicleRegistrationDocumentPath: documents.vehicleRegistration, insuranceDocumentPath: documents.insurance, vehicleInspectionDocumentPath: documents.vehicleInspection, status: row.application_status ?? "profile only", approvedAt: row.reviewed_at ?? null, trialEndsAt: row.trial_ends_at ?? null, subscriptionPaidUntil: row.paid_until ?? null, rating: "new" }; } function riderTrialEndsFromApproval(application, subscription) { if (subscription?.trial_ends_at) return subscription.trial_ends_at; if (application?.status !== "approved") return null; const anchor = application?.reviewed_at || application?.updated_at || application?.created_at; if (!anchor) return null; const trialEnd = new Date(anchor); if (Number.isNaN(trialEnd.getTime())) return null; trialEnd.setDate(trialEnd.getDate() + trialDays); return trialEnd.toISOString(); } function riderApplicationWorkspaceRank(application) { const rank = { approved: 70, background_pending: 60, needs_correction: 50, pending: 40, declined: 30, suspended: 20 }; return rank[application?.status] ?? 0; } function riderApplicationWorkspaceTime(application) { const timestamp = application?.reviewed_at || application?.updated_at || application?.created_at; const value = timestamp ? new Date(timestamp).getTime() : 0; return Number.isFinite(value) ? value : 0; } function chooseRiderApplicationForWorkspace(rows = []) { const applications = (Array.isArray(rows) ? rows : [rows]).filter(Boolean); if (!applications.length) return null; return [...applications].sort((left, right) => { const rankDifference = riderApplicationWorkspaceRank(right) - riderApplicationWorkspaceRank(left); if (rankDifference) return rankDifference; return riderApplicationWorkspaceTime(right) - riderApplicationWorkspaceTime(left); })[0] ?? null; } function applySignedInProfile(type, profile, user) { activateWorkspaceRoleSession(type, { phone: profile.phone, email: profile.email, userId: user.id, signedInAt: new Date().toISOString() }); if (type === "passenger") { state.passenger = profileToPassenger(profile); state.passengers = upsertById(state.passengers, state.passenger); } if (type === "rider") { state.rider = { ...(state.rider ?? {}), ...profileToPassenger(profile), area: state.rider?.area ?? "", vehicle: state.rider?.vehicle ?? "car", credential: state.rider?.credential ?? "", registration: state.rider?.registration ?? "", carMake: state.rider?.carMake ?? "", carModel: state.rider?.carModel ?? "", carBodyType: normalizeCarBodyType(state.rider?.carBodyType), vehicleDesignation: normalizeRiderVehicleDesignation(state.rider?.vehicleDesignation, state.rider?.carBodyType), navigationPreference: riderNavigationPreference(state.rider), carYear: state.rider?.carYear ?? "", carColor: state.rider?.carColor ?? "", vehicleVin: state.rider?.vehicleVin ?? "", insuranceProvider: state.rider?.insuranceProvider ?? "", insuranceNumber: state.rider?.insuranceNumber ?? "", backgroundCheckConsentAt: state.rider?.backgroundCheckConsentAt ?? null, backgroundCheckProvider: state.rider?.backgroundCheckProvider ?? "", backgroundCheckConsentVersion: state.rider?.backgroundCheckConsentVersion ?? "", backgroundCheckStatus: state.rider?.backgroundCheckStatus ?? "not requested", backgroundCheckDecision: state.rider?.backgroundCheckDecision ?? "pending", documentName: state.rider?.documentName ?? "", documents: riderDocuments(state.rider), needsApplication: Boolean(profile.needsApplication), accountStatus: profile.account_status ?? "active", accountStatusReason: profile.account_status_reason ?? "", accountStatusChangedAt: profile.account_status_changed_at ?? null, accountStatusChangedBy: profile.account_status_changed_by ?? null, accountClosedAt: profile.account_closed_at ?? null, status: profile.needsApplication ? "profile only" : state.rider?.status ?? profile.status ?? "pending", approvedAt: state.rider?.approvedAt ?? null, trialEndsAt: state.rider?.trialEndsAt ?? null, subscriptionPaidUntil: state.rider?.subscriptionPaidUntil ?? null, rating: state.rider?.rating ?? "new" }; state.riders = upsertById(state.riders, state.rider); } } function applyRuntimeConfig(localConfig, source) { appConfig = { ...appConfig, ...localConfig, buckets: { ...appConfig.buckets, ...(localConfig.buckets ?? {}) } }; runtimeConfigSource = source; window.WAKA_CONFIG = appConfig; } function readCachedRuntimeConfig() { try { return JSON.parse(localStorage.getItem(runtimeConfigStorageKey)); } catch { return null; } } function cacheRuntimeConfig(localConfig) { try { localStorage.setItem(runtimeConfigStorageKey, JSON.stringify(localConfig)); } catch { // Local storage can be unavailable in private contexts; the live config still works. } } function isLocalDevelopmentHost(hostname = window.location.hostname) { return ["127.0.0.1", "localhost", "::1", ""].includes(hostname); } function isSecureRuntimeContext() { return window.location.protocol === "https:" || isLocalDevelopmentHost(); } function runtimeConfigFileName() { const configured = String(appConfig.runtimeConfigFile ?? "").trim(); if (configured) return configured; return isLocalDevelopmentHost() ? "config.local.json" : "config.runtime.json"; } async function fetchRuntimeConfig() { const configFile = runtimeConfigFileName(); const configUrl = new URL(configFile, window.location.href); const cacheBustUrl = new URL(configUrl.href); cacheBustUrl.searchParams.set("t", Date.now().toString()); const urls = [...new Set([configFile, configUrl.href, cacheBustUrl.href])]; const attempts = urls.flatMap((url) => [ fetchRuntimeConfigUrl(url), fetchRuntimeConfigWithXhr(url) ]); return firstRuntimeConfig(attempts); } function firstRuntimeConfig(attempts) { return new Promise((resolve) => { let settled = false; let pending = attempts.length; const timer = window.setTimeout(() => finish(null), runtimeConfigTimeoutMs + 500); function finish(config) { if (settled) return; settled = true; window.clearTimeout(timer); resolve(config); } attempts.forEach((attempt) => { attempt .then((config) => { if (config) { finish(config); return; } pending -= 1; if (pending === 0) finish(null); }) .catch(() => { pending -= 1; if (pending === 0) finish(null); }); }); }); } async function fetchRuntimeConfigUrl(url) { let timeoutId; const controller = new AbortController(); const timeout = new Promise((_, reject) => { timeoutId = window.setTimeout(() => { controller.abort(); reject(new Error("Runtime config load timed out.")); }, runtimeConfigTimeoutMs); }); try { const response = await Promise.race([ fetch(url, { cache: "no-store", credentials: "same-origin", signal: controller.signal }), timeout ]); if (!response.ok) return null; return response.json(); } finally { window.clearTimeout(timeoutId); } } function fetchRuntimeConfigWithXhr(url) { return new Promise((resolve, reject) => { const request = new XMLHttpRequest(); request.open("GET", url, true); request.responseType = "text"; request.timeout = runtimeConfigTimeoutMs; request.onload = () => { if (request.status < 200 || request.status >= 300) { resolve(null); return; } try { resolve(JSON.parse(request.responseText)); } catch (error) { reject(error); } }; request.onerror = () => reject(new Error("Runtime config XHR failed.")); request.ontimeout = () => reject(new Error("Runtime config XHR timed out.")); request.send(); }); } async function loadRuntimeConfig() { const cachedConfig = readCachedRuntimeConfig(); const configFile = runtimeConfigFileName(); try { const localConfig = await fetchRuntimeConfig(); if (!localConfig) { if (cachedConfig) applyRuntimeConfig(cachedConfig, `cached ${configFile}`); return; } applyRuntimeConfig(localConfig, configFile); cacheRuntimeConfig(localConfig); } catch { if (cachedConfig) applyRuntimeConfig(cachedConfig, `cached ${configFile}`); } } function loadSupabaseSdk() { if (window.supabase?.createClient) return Promise.resolve(true); if (supabaseSdkPromise) return supabaseSdkPromise; supabaseSdkPromise = new Promise((resolve) => { const script = document.createElement("script"); let settled = false; const timer = window.setTimeout(() => finish(false), 8000); function finish(loaded) { if (settled) return; settled = true; window.clearTimeout(timer); if (!loaded) supabaseSdkPromise = null; resolve(loaded); } script.src = supabaseSdkUrl; script.async = true; script.dataset.wakaSupabaseSdk = "true"; script.onload = () => finish(Boolean(window.supabase?.createClient)); script.onerror = () => finish(false); document.head.appendChild(script); }); return supabaseSdkPromise; } async function initSupabaseClient() { if (appConfig.mode !== "supabase") return; if (!appConfig.supabaseUrl || !appConfig.supabaseAnonKey) { updateConnectionStatus(); return; } passwordResetRoleCapturedBeforeSupabaseInit ||= passwordResetRoleFromLocation(); const passwordResetReturnPending = Boolean(passwordResetRoleCapturedBeforeSupabaseInit); const sdkReady = await loadSupabaseSdk(); if (!sdkReady) { updateConnectionStatus(); return; } supabaseClient = window.supabase.createClient(appConfig.supabaseUrl, appConfig.supabaseAnonKey, { auth: { persistSession: true, autoRefreshToken: true, detectSessionInUrl: !passwordResetReturnPending } }); updateConnectionStatus(); } function usesManualPhoneVerification() { return appConfig.phoneVerificationMode === "manual"; } function smsVerificationRelaxedForTesting() { return configFlagEnabled(appConfig.relaxSmsVerificationForTesting); } function markManualPhoneVerified(type, phone, status) { state.verification[type] = { phone, phoneDigits: phoneDigits(phone), verifiedPhone: phone, verifiedAt: new Date().toISOString(), provider: "manual-pilot" }; saveState(); setTranslatedStatus(status, "manualPhoneVerified"); return true; } function markSmsRelaxedPhoneVerified(type, phone, status) { state.verification[type] = { phone, phoneDigits: phoneDigits(phone), verifiedPhone: phone, verifiedAt: new Date().toISOString(), provider: "email-test-bypass" }; saveState(); setTranslatedStatus(status, "smsVerificationRelaxedForTesting"); return true; } function isSupabaseMode() { return appConfig.mode === "supabase" && Boolean(supabaseClient); } function hasSupabaseConfig() { return appConfig.mode === "supabase" && Boolean(appConfig.supabaseUrl && appConfig.supabaseAnonKey); } function hasSupabaseRuntime() { return isSupabaseMode() || Boolean(supabaseRestSession); } function configFlagEnabled(value) { return value === true || String(value ?? "").toLowerCase() === "true"; } function strictProductionModeEnabled() { return configFlagEnabled(appConfig.strictProductionMode); } function normalizedPublicRole(value) { const role = String(value || "").trim().toLowerCase(); return ["passenger", "rider"].includes(role) ? role : ""; } function authIsolationMode() { const mode = String(appConfig.authIsolationMode || "strict-single-tenant").trim().toLowerCase(); return ["strict-single-tenant", "separate-tenants", "shared"].includes(mode) ? mode : "strict-single-tenant"; } function strictRoleAuthIsolationEnabled() { return authIsolationMode() !== "shared"; } function separateRoleAuthTenantsRequested() { return authIsolationMode() === "separate-tenants"; } function roleAuthTenantConfig(role) { const normalizedRole = normalizedPublicRole(role); const configured = appConfig.roleAuthTenants?.[normalizedRole] || {}; return { supabaseUrl: String(configured.supabaseUrl || "").trim(), supabaseAnonKey: String(configured.supabaseAnonKey || "").trim() }; } function roleAuthTenantConfigured(role) { const tenant = roleAuthTenantConfig(role); return Boolean(tenant.supabaseUrl && tenant.supabaseAnonKey); } function signedInProfileHasPrimaryRole(profile, role) { return Boolean(profile?.id && normalizedPublicRole(profile.role) === normalizedPublicRole(role)); } async function signedInProfileCanUseWorkspaceRole(profile, role) { if (!profile?.id || !normalizedPublicRole(role)) return false; if (strictRoleAuthIsolationEnabled()) return signedInProfileHasPrimaryRole(profile, role); return signedInProfileHasRole(profile, role); } const platformFeatureFlagDefinitions = Object.freeze([ { key: "ride_publishing_enabled", label: "Ride publishing", description: "Allow passengers to publish new ride requests.", defaultEnabled: true }, { key: "rider_activation_enabled", label: "Rider activation", description: "Allow approved riders to activate availability and enter the marketplace.", defaultEnabled: true }, { key: "notification_delivery_enabled", label: "External notification delivery", description: "Allow queued push, SMS, and email delivery workers to process notifications.", defaultEnabled: true }, { key: "route_estimates_enabled", label: "Route estimates", description: "Allow paid route estimate Edge Function calls.", defaultEnabled: true }, { key: "marketplace_realtime_enabled", label: "Marketplace realtime", description: "Allow browser Supabase Realtime subscriptions for ride, offer, chat, and notice updates.", defaultEnabled: true }, { key: "client_error_reporting_enabled", label: "Client error reporting", description: "Allow signed-in browser runtime errors to be sanitized and recorded in system events.", defaultEnabled: true } ]); const platformFeatureFlagsCacheMs = 30 * 1000; let platformFeatureFlags = defaultPlatformFeatureFlags(); let platformFeatureFlagsLoadedAt = 0; let platformFeatureFlagsPromise = null; let platformFeatureFlagsWarningShown = false; function platformFeatureDefinitions() { return platformFeatureFlagDefinitions.map((definition) => ({ ...definition })); } function defaultPlatformFeatureFlags() { return Object.fromEntries(platformFeatureFlagDefinitions.map((definition) => [ definition.key, definition.defaultEnabled !== false ])); } function platformFeatureEnabled(key) { return platformFeatureFlags[String(key)] !== false; } function normalizePlatformFeatureFlagRow(row = {}) { return { key: row.flag_key ?? row.key, label: row.label ?? "", description: row.description ?? "", enabled: row.enabled !== false, reason: row.reason ?? "", updatedAt: row.updated_at ?? row.updatedAt ?? null, updatedBy: row.updated_by ?? row.updatedBy ?? null }; } async function loadPlatformFeatureFlagsFromSupabase({ force = false } = {}) { if (!hasSupabaseRuntime()) return platformFeatureFlags; const now = Date.now(); if (!force && platformFeatureFlagsLoadedAt && now - platformFeatureFlagsLoadedAt < platformFeatureFlagsCacheMs) { return platformFeatureFlags; } if (platformFeatureFlagsPromise) return platformFeatureFlagsPromise; platformFeatureFlagsPromise = (async () => { try { let rows; if (supabaseClient?.from) { const { data, error } = await withSupabaseTimeout( supabaseClient .from("platform_feature_flags") .select("flag_key,enabled,updated_at") .order("flag_key", { ascending: true }), "Loading platform feature flags", optionalSupabaseRequestTimeoutMs ); if (error) throw error; rows = data; } else { rows = await supabaseRestRequest("/rest/v1/platform_feature_flags?select=flag_key,enabled,updated_at&order=flag_key.asc"); } const next = defaultPlatformFeatureFlags(); (rows ?? []).forEach((row) => { const key = String(row?.flag_key ?? "").trim(); if (key) next[key] = row.enabled !== false; }); platformFeatureFlags = next; platformFeatureFlagsLoadedAt = Date.now(); return platformFeatureFlags; } catch (error) { if (!platformFeatureFlagsWarningShown) { platformFeatureFlagsWarningShown = true; logClientWarning("Platform feature flags could not be loaded; using enabled defaults.", error); } platformFeatureFlagsLoadedAt = Date.now(); return platformFeatureFlags; } finally { platformFeatureFlagsPromise = null; } })(); return platformFeatureFlagsPromise; } async function assertPlatformFeatureEnabled(key, label = "This feature") { await loadPlatformFeatureFlagsFromSupabase(); if (!platformFeatureEnabled(key)) { throw new Error(`${label} is temporarily paused by Waka operations.`); } } function demoToolsAllowed() { return appConfig.mode === "demo" && isLocalDevelopmentHost() && !strictProductionModeEnabled(); } function phoneOtpSignInEnabled() { return configFlagEnabled(appConfig.enablePhoneOtpSignIn); } function shouldBlockClientFallbackWrites() { return hasSupabaseRuntime() || strictProductionModeEnabled(); } function assertClientFallbackAllowed(feature, sqlFile) { if (!shouldBlockClientFallbackWrites()) return; const runtimeMode = hasSupabaseRuntime() ? "Supabase mode" : "strict production mode"; throw new Error(`${feature} requires ${sqlFile} in ${runtimeMode}. Install the SQL/RPC from docs/SUPABASE-LAUNCH-RUNBOOK.md, then retry.`); } async function withSupabaseTimeout(promise, label, timeoutMs = supabaseRequestTimeoutMs) { let timeoutId; const timeout = new Promise((_, reject) => { timeoutId = setTimeout(() => { reject(new Error(`${label} is taking too long. Check your internet connection, Supabase project, and Auth settings, then try again.`)); }, timeoutMs); }); try { return await Promise.race([promise, timeout]); } finally { clearTimeout(timeoutId); } } async function supabaseRestRequest(path, { method = "GET", body = null, accessToken = null, headers = {}, returnResponse = false } = {}) { if (!hasSupabaseConfig()) throw new Error("Supabase config is missing."); const resolvedAccessToken = accessToken || await currentSupabaseAccessToken().catch(() => "") || supabaseRestSession?.access_token || ""; const requestHeaders = { apikey: appConfig.supabaseAnonKey, Authorization: `Bearer ${resolvedAccessToken || appConfig.supabaseAnonKey}`, ...headers }; if (body !== null) requestHeaders["Content-Type"] = "application/json"; const response = await fetch(`${appConfig.supabaseUrl}${path}`, { method, headers: requestHeaders, body: body === null ? null : JSON.stringify(body) }); const text = await response.text(); let payload = null; if (text) { try { payload = JSON.parse(text); } catch { payload = text; } } if (!response.ok) { throw new Error(payload?.msg || payload?.message || text || `Supabase request failed with HTTP ${response.status}.`); } return returnResponse ? { data: payload, headers: response.headers, status: response.status } : payload; } async function currentSupabaseAccessToken() { if (supabaseRestSession?.access_token) return supabaseRestSession.access_token; if (!supabaseClient?.auth?.getSession) return ""; const { data, error } = await withSupabaseTimeout( supabaseClient.auth.getSession(), "Reading the saved Supabase session", optionalSupabaseRequestTimeoutMs ); if (error && !isMissingAuthSessionError(error)) throw error; if (data?.session) supabaseRestSession = data.session; return data?.session?.access_token || ""; } function shouldRetrySupabaseRpcWithFreshSession(error) { const message = String(error?.message || error || ""); return /permission denied for function|auth session missing|jwt|token|not authorized|401/i.test(message); } async function callSupabaseRpc(functionName, body, label, timeoutMs = optionalSupabaseRequestTimeoutMs) { if (!hasSupabaseRuntime()) return null; if (!supabaseClient) { return withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rpc/${functionName}`, { method: "POST", body, headers: { Prefer: "return=minimal" } }), label, timeoutMs ); } const { data, error } = await withSupabaseTimeout( supabaseClient.rpc(functionName, body), label, timeoutMs ); if (error) { if (shouldRetrySupabaseRpcWithFreshSession(error)) { const accessToken = await currentSupabaseAccessToken(); if (accessToken) { return withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rpc/${functionName}`, { method: "POST", body, accessToken, headers: { Prefer: "return=minimal" } }), label, timeoutMs ); } } throw error; } return data; } async function signInWithSupabasePasswordRest(email, password) { const session = await withSupabaseTimeout( supabaseRestRequest("/auth/v1/token?grant_type=password", { method: "POST", accessToken: appConfig.supabaseAnonKey, body: { email, password } }), "Signing in with Supabase Auth" ); supabaseRestSession = session; updateConnectionStatus(); return session; } async function signInAndLoadProfileForRole(email, password, role, onStage = null) { let user = null; let profile = null; reportSupabaseStep(onStage, `Signing in to the ${role} login...`); if (supabaseClient) { const { data, error } = await withSupabaseTimeout( supabaseClient.auth.signInWithPassword({ email, password }), `Signing in as ${role}`, optionalSupabaseRequestTimeoutMs ); if (error) throw error; user = data?.user ?? null; if (!user?.id) throw new Error("Supabase accepted the sign-in but did not return the account id."); reportSupabaseStep(onStage, `Loading the Waka ${role} profile...`); const { data: profileData, error: profileError } = await withSupabaseTimeout( supabaseClient .from("profiles") .select("*") .eq("id", user.id) .maybeSingle(), `Loading the ${role} profile`, supabaseProfileSaveTimeoutMs ); if (profileError) throw profileError; profile = profileData; } else { const session = await signInWithSupabasePasswordRest(email, password); user = session.user; reportSupabaseStep(onStage, `Loading the Waka ${role} profile...`); profile = await selectProfileRest(user.id, "*", session.access_token); } if (!profile) throw new Error(`Supabase sign-in worked, but the Waka ${role} profile was not found.`); if (!(await signedInProfileCanUseWorkspaceRole(profile, role))) { throw new Error(`This Waka login does not have a separate ${role} profile. Use the ${role} account or create one before signing in here.`); } return { user, profile: profileForWorkspaceRole(profile, { role }) }; } async function selectProfileRest(userId, select = "*", accessToken = supabaseRestSession?.access_token) { const params = new URLSearchParams(); params.set("id", `eq.${userId}`); params.set("select", select); const rows = await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/profiles?${params.toString()}`, { accessToken }), "Loading the Supabase profile", supabaseProfileSaveTimeoutMs ); return Array.isArray(rows) ? rows[0] ?? null : rows; } async function selectRiderApplicationRest(riderId, accessToken = supabaseRestSession?.access_token) { const params = new URLSearchParams(); params.set("rider_id", `eq.${riderId}`); params.set("select", "*"); params.set("order", "created_at.desc"); params.set("limit", "10"); const rows = await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rider_applications?${params.toString()}`, { accessToken }), "Loading the rider application", supabaseProfileSaveTimeoutMs ); return Array.isArray(rows) ? chooseRiderApplicationForWorkspace(rows) : rows; } async function selectRiderSubscriptionRest(riderId, accessToken = supabaseRestSession?.access_token) { const params = new URLSearchParams(); params.set("rider_id", `eq.${riderId}`); params.set("select", "*"); params.set("limit", "1"); const rows = await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rider_subscriptions?${params.toString()}`, { accessToken }), "Loading the rider subscription", supabaseProfileSaveTimeoutMs ); return Array.isArray(rows) ? rows[0] ?? null : rows; } async function updateProfileLocationInSupabase(profileId, country, city) { if (!hasSupabaseRuntime() || !profileId) return; const payload = { country, city }; if (supabaseClient) { const { error } = await withSupabaseTimeout( supabaseClient.from("profiles").update(payload).eq("id", profileId), "Updating profile location", supabaseProfileSaveTimeoutMs ); if (error) throw error; return; } await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/profiles?id=eq.${profileId}`, { method: "PATCH", body: payload, headers: { Prefer: "return=minimal" } }), "Updating profile location", supabaseProfileSaveTimeoutMs ); } async function updateRiderApplicationLocationInSupabase(riderId, area) { if (!hasSupabaseRuntime() || !riderId) return; const payload = { operating_area: area }; if (supabaseClient) { const { error } = await withSupabaseTimeout( supabaseClient.from("rider_applications").update(payload).eq("rider_id", riderId), "Updating rider operating area", supabaseProfileSaveTimeoutMs ); if (error) throw error; return; } await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rider_applications?rider_id=eq.${riderId}`, { method: "PATCH", body: payload, headers: { Prefer: "return=minimal" } }), "Updating rider operating area", supabaseProfileSaveTimeoutMs ); } async function updateRiderApplicationDocumentsInSupabase(riderId, documents) { if (!hasSupabaseRuntime() || !riderId) return; const payload = { document_path: riderDocumentPayload(documents) }; if (supabaseClient) { const { error } = await withSupabaseTimeout( supabaseClient.from("rider_applications").update(payload).eq("rider_id", riderId), "Updating rider application documents", supabaseProfileSaveTimeoutMs ); if (error) throw error; return; } await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rider_applications?rider_id=eq.${riderId}`, { method: "PATCH", body: payload, headers: { Prefer: "return=minimal" } }), "Updating rider application documents", supabaseProfileSaveTimeoutMs ); } async function updatePassengerCurrentCityInSupabase(profileId, country, city) { if (!hasSupabaseRuntime() || !profileId) return; if (!locationUpdateRpcUnavailable.passenger) { try { await callSupabaseRpc( "passenger_update_current_city", { p_country: country, p_city: city }, "Updating passenger city", supabaseProfileSaveTimeoutMs ); lastLocationUpdateSource = "location update RPC"; return; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; locationUpdateRpcUnavailable.passenger = true; logClientWarning("Passenger location RPC is not installed yet. Falling back to direct profile update.", error); } } assertClientFallbackAllowed("Passenger location update", "supabase-location-update-rpc.sql"); lastLocationUpdateSource = "direct location update fallback"; await updateProfileLocationInSupabase(profileId, country, city); } async function updateRiderCurrentAreaInSupabase(riderId, country, city, area) { if (!hasSupabaseRuntime() || !riderId) return; if (!locationUpdateRpcUnavailable.rider) { try { await callSupabaseRpc( "rider_update_current_area", { p_country: country, p_city: city, p_area: area }, "Updating rider area", supabaseProfileSaveTimeoutMs ); lastLocationUpdateSource = "location update RPC"; return; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; locationUpdateRpcUnavailable.rider = true; logClientWarning("Rider location RPC is not installed yet. Falling back to direct profile and rider location updates.", error); } } assertClientFallbackAllowed("Rider location update", "supabase-location-update-rpc.sql"); lastLocationUpdateSource = "direct location update fallback"; await updateProfileLocationInSupabase(riderId, country, city); await updateRiderApplicationLocationInSupabase(riderId, area); } async function updateRiderLiveGpsWithRpc(rider) { const currentGps = riderCurrentGps(rider); if (!currentGps) return false; await callSupabaseRpc( "rider_update_live_gps", { p_lat: currentGps.latitude, p_lng: currentGps.longitude, p_accuracy_meters: currentGps.accuracyMeters ?? null, p_captured_at: currentGps.capturedAt ?? null }, "Updating rider live GPS", optionalSupabaseRequestTimeoutMs ); lastLocationUpdateSource = "location update RPC"; void recordInsuranceTelemetryPointInSupabase(rider, currentGps, "rider_live_gps"); return true; } async function clearRiderLiveGpsWithRpc() { await callSupabaseRpc( "rider_clear_live_gps", {}, "Stopping rider live GPS", optionalSupabaseRequestTimeoutMs ); lastLocationUpdateSource = "location update RPC"; void closeInsuranceTelemetrySegmentInSupabase("rider_clear_live_gps"); return true; } function insuranceTelemetryEnabled() { return configFlagEnabled(appConfig.insuranceTelemetryEnabled ?? true); } async function recordInsuranceTelemetryPointInSupabase(rider, gpsPoint, eventType = "rider_live_gps") { if (!hasSupabaseRuntime() || !insuranceTelemetryEnabled() || insuranceTelemetryRpcUnavailable) return null; const point = normalizeGpsPoint(gpsPoint); if (!rider?.id || !point) return null; try { const segment = await callSupabaseRpcResult( "record_insurance_telemetry_point", { p_ride_request_id: null, p_period: null, p_lat: point.latitude, p_lng: point.longitude, p_accuracy_meters: point.accuracyMeters ?? null, p_captured_at: point.capturedAt ?? null, p_event_type: eventType }, "Recording insurance telemetry", optionalSupabaseRequestTimeoutMs ); lastInsuranceTelemetrySource = "insurance telemetry RPC"; return Array.isArray(segment) ? segment[0] ?? null : segment; } catch (error) { if (adminDirectoryRpcMissing(error)) { insuranceTelemetryRpcUnavailable = true; lastInsuranceTelemetrySource = "insurance telemetry RPC missing"; logClientWarning("Insurance telemetry RPC is not installed yet. Rider GPS still synced, but active-mile reporting is not production ready.", error); return null; } lastInsuranceTelemetrySource = "insurance telemetry warning"; logClientWarning("Insurance telemetry point could not be recorded.", error); return null; } } async function closeInsuranceTelemetrySegmentInSupabase(eventType = "manual_close") { if (!hasSupabaseRuntime() || !insuranceTelemetryEnabled() || insuranceTelemetryRpcUnavailable) return null; try { const result = await callSupabaseRpcResult( "close_insurance_telemetry_segment", { p_event_type: eventType }, "Closing insurance telemetry", optionalSupabaseRequestTimeoutMs ); lastInsuranceTelemetrySource = "insurance telemetry RPC"; return result ?? true; } catch (error) { if (adminDirectoryRpcMissing(error)) { insuranceTelemetryRpcUnavailable = true; lastInsuranceTelemetrySource = "insurance telemetry RPC missing"; logClientWarning("Insurance telemetry close RPC is not installed yet.", error); return null; } lastInsuranceTelemetrySource = "insurance telemetry warning"; logClientWarning("Insurance telemetry segment could not be closed.", error); return null; } } async function recordInsuranceTelemetryTransitionInSupabase(request, actionName, gpsPoint = null) { if (!hasSupabaseRuntime() || !insuranceTelemetryEnabled() || insuranceTelemetryRpcUnavailable) return null; if (!request?.id || !actionName) return null; const point = normalizeGpsPoint(gpsPoint); try { const segment = await callSupabaseRpcResult( "record_insurance_telemetry_transition", { p_request_id: request.id, p_action_name: actionName, p_lat: point?.latitude ?? null, p_lng: point?.longitude ?? null, p_accuracy_meters: point?.accuracyMeters ?? null, p_captured_at: point?.capturedAt ?? null }, "Recording insurance lifecycle telemetry", optionalSupabaseRequestTimeoutMs ); lastInsuranceTelemetrySource = "insurance telemetry RPC"; return Array.isArray(segment) ? segment[0] ?? null : segment; } catch (error) { if (adminDirectoryRpcMissing(error)) { insuranceTelemetryRpcUnavailable = true; lastInsuranceTelemetrySource = "insurance telemetry RPC missing"; logClientWarning("Insurance lifecycle telemetry RPC is not installed yet.", error); return null; } lastInsuranceTelemetrySource = "insurance telemetry warning"; logClientWarning("Insurance lifecycle telemetry could not be recorded.", error); return null; } } async function clearRiderLiveGpsInSupabase(rider) { if (!hasSupabaseRuntime() || !rider?.id) return; if (!locationUpdateRpcUnavailable.clearLiveGps) { try { const didClear = await clearRiderLiveGpsWithRpc(); if (didClear) return; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; locationUpdateRpcUnavailable.clearLiveGps = true; logClientWarning("Rider clear live GPS RPC is not installed yet. Falling back to direct rider location upsert.", error); } } assertClientFallbackAllowed("Rider live GPS clearing", "supabase-location-update-rpc.sql"); await updateRiderLocationPresenceInSupabase(clearRiderLiveGpsFields(rider)); } async function expireRiderLiveGpsIfNeeded() { const rider = currentRiderRecord(); if (!riderLiveGpsNeedsClearing(rider)) return false; const ageMinutes = typeof riderLiveGpsAgeMinutes === "function" ? riderLiveGpsAgeMinutes(rider) : null; const inactivityLimit = Number(typeof riderAvailabilityInactivityTimeoutMinutes !== "undefined" ? riderAvailabilityInactivityTimeoutMinutes : 8 * 60); const longInactive = ageMinutes != null && Number.isFinite(ageMinutes) && ageMinutes >= inactivityLimit; if (state.riderAvailabilityActivated === true && !longInactive) { if (activeRole() === "rider") { els.riderGpsStatus.textContent = "Activated. Waka is waiting for a fresh location update before nearby ride requests appear."; } return false; } const clearedRider = clearRiderLiveGpsFields(rider); if (longInactive && state.riderAvailabilityActivated === true) { state.riderAvailabilityActivated = false; } saveCurrentRiderRecord(clearedRider); await clearRiderLiveGpsInSupabase(clearedRider); if (activeRole() === "rider") { els.riderGpsStatus.textContent = longInactive ? "Offline after a long period without a fresh location. Activate when you are ready to receive requests again." : "Live GPS expired or became inaccurate; activate again before receiving requests."; } return true; } async function updateRiderApplicationReviewInSupabase(riderId, status, reviewedAt, reviewNote = null) { const payload = { status, reviewed_by: state.adminSession.userId, reviewed_at: reviewedAt, review_note: status === "needs_correction" ? reviewNote : null }; if (supabaseClient) { const { error } = await supabaseClient .from("rider_applications") .update(payload) .eq("rider_id", riderId); if (error) throw error; return; } await supabaseRestRequest(`/rest/v1/rider_applications?rider_id=eq.${riderId}`, { method: "PATCH", body: payload, headers: { Prefer: "return=minimal" } }); } async function updateRiderLocationPresenceInSupabase(rider) { if (!hasSupabaseRuntime() || !rider?.id) return; const currentGps = riderCurrentGps(rider); if (currentGps && !locationUpdateRpcUnavailable.liveGps) { try { const didUpdate = await updateRiderLiveGpsWithRpc(rider); if (didUpdate) return; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; locationUpdateRpcUnavailable.liveGps = true; logClientWarning("Rider live GPS RPC is not installed yet. Falling back to direct rider location upsert.", error); } } const location = gpsPointToDatabase(currentGps); const payload = { rider_id: rider.id, city: rider.city, area_label: rider.area, is_online: Boolean(currentGps && rider.status === "approved" && isSubscriptionActive(rider)), updated_at: new Date().toISOString() }; payload.location = location; payload.accuracy_meters = currentGps?.accuracyMeters ?? null; payload.captured_at = currentGps?.capturedAt ?? null; try { assertClientFallbackAllowed("Rider live GPS update", "supabase-location-update-rpc.sql"); lastLocationUpdateSource = "direct rider location upsert fallback"; if (supabaseClient) { await withSupabaseTimeout( supabaseClient.from("rider_locations").upsert(payload, { onConflict: "rider_id" }), "Updating rider live location", optionalSupabaseRequestTimeoutMs ); void recordInsuranceTelemetryPointInSupabase(rider, currentGps, "rider_live_gps_fallback"); return; } await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rider_locations?on_conflict=rider_id", { method: "POST", body: payload, headers: { Prefer: "resolution=merge-duplicates,return=minimal" } }), "Updating rider live location", optionalSupabaseRequestTimeoutMs ); void recordInsuranceTelemetryPointInSupabase(rider, currentGps, "rider_live_gps_fallback"); } catch (error) { logClientWarning("Rider live location was not updated.", error); } } function reportSupabaseStep(onStage, message) { if (typeof onStage === "function") onStage(message); } function pause(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function isMissingAuthSessionError(error) { return Boolean(error?.message && /auth session missing|session.*missing/i.test(error.message)); } function isMissingJwtUserError(error) { return Boolean(error?.message && /user from sub claim in jwt does not exist/i.test(error.message)); } function isSupabaseTimeoutError(error) { const message = String(error?.message || error || ""); return /taking too long|timed out|timeout/i.test(message); } function isInvalidLoginCredentialsError(error) { return Boolean(error?.message && /invalid login credentials/i.test(error.message)); } function isAlreadyRegisteredError(error) { return Boolean(error?.message && /already|registered|exists/i.test(error.message)); } function authUserEmail(user) { return user?.email?.toLowerCase?.() ?? ""; } function authUserMatchesVerifiedPhone(user, profile) { return Boolean(user?.phone && profile?.phone && phoneMatches(user.phone, profile.phone)); } async function attachEmailPasswordToVerifiedPhoneUser(user, profile, onStage) { if (!authUserMatchesVerifiedPhone(user, profile)) return user; const updates = {}; if (profile.email && authUserEmail(user) !== profile.email) updates.email = profile.email; if (profile.password) updates.password = profile.password; if (!Object.keys(updates).length || !supabaseClient) return user; reportSupabaseStep(onStage, "Linking email and password to the verified phone account..."); const { data, error } = await withSupabaseTimeout( supabaseClient.auth.updateUser(updates), "Linking email/password to the verified phone account", supabaseProfileSaveTimeoutMs ); if (error) { logClientWarning("Phone was verified, but email/password setup still needs attention.", error); reportSupabaseStep(onStage, `Phone verified. Email/password sign-in still needs attention: ${error.message}`); return { ...user, emailSetupPending: true, emailSetupError: error.message }; } const updatedUser = data?.user ?? user; return { ...updatedUser, emailSetupPending: Boolean(updates.email && authUserEmail(updatedUser) !== profile.email) }; } function clearStoredSupabaseAuthSession() { try { Object.keys(localStorage) .filter((key) => /^sb-.+-auth-token$/.test(key)) .forEach((key) => localStorage.removeItem(key)); } catch (error) { logClientWarning("Stored Supabase session could not be cleared.", error); } } async function clearStaleSupabaseSession() { clearStoredSupabaseAuthSession(); try { await withSupabaseTimeout( supabaseClient.auth.signOut({ scope: "local" }), "Clearing the stale Supabase session", optionalSupabaseRequestTimeoutMs ); } catch (error) { logClientWarning("Stale Supabase session could not be signed out.", error); } } async function getSupabaseUser(options = {}) { const sessionTimeoutMs = options.sessionTimeoutMs ?? optionalSupabaseRequestTimeoutMs; const userTimeoutMs = options.timeoutMs ?? supabaseRequestTimeoutMs; if (supabaseRestSession?.user && !supabaseClient) return supabaseRestSession.user; if (!isSupabaseMode()) return null; if (supabaseClient.auth.getSession) { const { data: sessionData, error: sessionError } = await withSupabaseTimeout( supabaseClient.auth.getSession(), "Reading the saved Supabase session", sessionTimeoutMs ); if (sessionError && !isMissingAuthSessionError(sessionError)) throw sessionError; if (sessionData?.session?.user) return sessionData.session.user; } const { data, error } = await withSupabaseTimeout( supabaseClient.auth.getUser(), "Checking the current Supabase session", userTimeoutMs ); if (isMissingAuthSessionError(error)) return null; if (isMissingJwtUserError(error)) { await clearStaleSupabaseSession(); return null; } if (error) throw error; return data?.user ?? null; } async function loadSupabaseProfileForUser(user, select = "*", timeoutMessage = "Loading the Waka profile") { if (!user?.id) return null; if (supabaseClient) { const { data, error } = await withSupabaseTimeout( supabaseClient .from("profiles") .select(select) .eq("id", user.id) .maybeSingle(), timeoutMessage, supabaseProfileSaveTimeoutMs ); if (error) throw error; return data; } return selectProfileRest(user.id, select, supabaseRestSession?.access_token); } async function savePhoneVerificationEvent(userId, phone, provider = "supabase-otp") { if (!isSupabaseMode() || !userId) return; try { const { error } = await withSupabaseTimeout( supabaseClient.from("phone_verification_events").insert({ user_id: userId, phone, provider }), "Saving the phone verification audit event", optionalSupabaseRequestTimeoutMs ); if (error) logClientWarning("Phone verification audit event was not saved.", error); } catch (error) { logClientWarning("Phone verification audit event was skipped.", error); } } function profileOnboardingRpcBody(profile, profilePhotoPath = null) { return { p_role: profile.role, p_full_name: profile.name, p_email: profile.email, p_phone: profile.phone, p_phone_verified_at: profile.phoneVerifiedAt, p_national_id_number: profile.nationalId, p_date_of_birth: profile.dateOfBirth, p_preferred_language: profile.preferredLanguage, p_country: profile.country, p_city: profile.city, p_profile_photo_path: profilePhotoPath, p_phone_verification_provider: profile.phoneVerificationProvider ?? "supabase-otp" }; } async function upsertProfileWithOnboardingRpc(profile, user, onStage, didRefreshSession = false) { reportSupabaseStep(onStage, didRefreshSession ? "Retrying profile onboarding RPC..." : "Syncing profile details through onboarding RPC..."); try { await callSupabaseRpc( "upsert_own_profile", profileOnboardingRpcBody(profile, profile.profilePhotoPath ?? null), "Saving the Waka profile", supabaseProfileSaveTimeoutMs ); lastProfileOnboardingSource = "profile onboarding RPC"; return user; } catch (error) { if (isMissingJwtUserError(error) && !didRefreshSession) { reportSupabaseStep(onStage, "Supabase session mismatch found. Refreshing sign-in..."); const refreshedUser = await refreshSupabaseSignIn(profile, onStage); await pause(800); return upsertProfileWithOnboardingRpc(profile, refreshedUser, onStage, true); } throw error; } } async function saveProfilePhotoPathWithRpc(profilePhotoPath) { await callSupabaseRpc( "save_profile_photo_path", { p_profile_photo_path: profilePhotoPath }, "Saving the profile photo path", supabaseProfileSaveTimeoutMs ); lastProfileOnboardingSource = "profile onboarding RPC"; } function riderInsuranceProviderForPayload(rider, vehicle = normalizeRideVehicle(rider?.vehicle)) { const provider = String(rider?.insuranceProvider || "").trim(); if (provider) return provider; return vehicle === "bike" ? "Not applicable - bike" : "Not provided"; } function riderInsuranceNumberForPayload(rider, vehicle = normalizeRideVehicle(rider?.vehicle)) { const number = String(rider?.insuranceNumber || "").trim(); if (number) return number; return vehicle === "bike" ? "Not applicable - bike" : "Not provided"; } function riderIdentityReferenceForPayload(rider) { const entered = String(rider?.credential || rider?.nationalId || "").trim(); if (entered) return entered; const digits = String(rider?.phone || "").replace(/\D/g, "").slice(-12); return `PENDING-${digits || String(Date.now()).slice(-8)}`; } function optionalSupabaseDate(value) { const normalized = String(value || "").trim(); return normalized || null; } async function submitRiderApplicationWithRpc(rider, documentPath) { const vehicle = normalizeRideVehicle(rider.vehicle); const carBodyType = vehicle === "bike" ? "motorbike" : normalizeCarBodyType(rider.carBodyType); const vehicleDesignation = vehicle === "bike" ? "normal" : normalizeRiderVehicleDesignation(rider.vehicleDesignation, carBodyType); await callSupabaseRpc( "submit_rider_application", { p_vehicle: vehicle, p_operating_area: rider.area, p_credential_number: riderIdentityReferenceForPayload(rider), p_vehicle_registration: rider.registration, p_car_make: rider.carMake, p_car_model: rider.carModel, p_car_body_type: carBodyType, p_vehicle_designation: vehicleDesignation, p_car_year: rider.carYear ? Number(rider.carYear) : null, p_car_color: rider.carColor, p_vehicle_vin: rider.vehicleVin, p_insurance_provider: riderInsuranceProviderForPayload(rider, vehicle), p_insurance_number: riderInsuranceNumberForPayload(rider, vehicle), p_driver_license_expires_on: optionalSupabaseDate(rider.driverLicenseExpiresOn), p_insurance_expires_on: vehicle === "bike" ? null : optionalSupabaseDate(rider.insuranceExpiresOn), p_background_check_consent: true, p_background_check_provider: rider.backgroundCheckProvider || appConfig.backgroundCheckProvider || "not-required-cameroon", p_background_check_consent_version: rider.backgroundCheckConsentVersion || "cameroon-pilot-admin-review", p_document_path: documentPath }, "Submitting the rider application", supabaseProfileSaveTimeoutMs ); lastProfileOnboardingSource = "profile onboarding RPC"; } function authRedirectPathForRole(role) { if (role === "rider") return "/rider"; if (role === "passenger") return "/passenger"; return "/"; } function authRedirectUrlForRole(role) { const path = authRedirectPathForRole(role); try { return new URL(path, window.location.origin).toString(); } catch { return path; } } function authSignupOptionsForProfile(profile) { return { emailRedirectTo: authRedirectUrlForRole(profile?.role), data: { role: profile?.role || "passenger", waka_onboarding: `${profile?.role || "passenger"}_account`, full_name: profile?.name || "" } }; } async function requestSupabaseSignupConfirmation(email, role = null, onStage = null) { if (!supabaseClient?.auth?.resend || !email) return false; try { reportSupabaseStep(onStage, "Requesting the Supabase confirmation email..."); const { error } = await withSupabaseTimeout( supabaseClient.auth.resend({ type: "signup", email, options: { emailRedirectTo: authRedirectUrlForRole(role) } }), "Requesting the Supabase confirmation email", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return true; } catch (error) { logClientWarning("Supabase confirmation email could not be requested.", error); return false; } } function emailConfirmationNeededMessage(role, confirmationRequested) { const accountLabel = role === "rider" ? "rider" : "passenger"; const confirmationText = confirmationRequested ? "Check the inbox and spam folder for the Waka Cameroon confirmation email." : "The confirmation email could not be confirmed. Check Waka Cameroon Auth email settings before testing again."; const riderApplicationText = role === "rider" ? " Waka has received the rider application for admin review; email confirmation is needed before normal sign-in." : ""; return `Waka Cameroon created the ${accountLabel} login, but email confirmation is required before sign-in.${riderApplicationText} ${confirmationText} After confirming the email, return here and sign in with the same email and password. Do not press Save ${accountLabel} again.`; } function explicitRoleSignInRequiredMessage(role) { const accountLabel = role === "rider" ? "rider" : "passenger"; return `Your Waka Cameroon ${accountLabel} email is confirmed. For security, sign in with the same email and password to finish opening the ${accountLabel} account.`; } async function ensureSupabaseAuthUser(profile, onStage, options = {}) { reportSupabaseStep(onStage, "Checking current Supabase session..."); const existingUser = await getSupabaseUser({ timeoutMs: optionalSupabaseRequestTimeoutMs }).catch(async (error) => { if (!isSupabaseTimeoutError(error)) throw error; logClientWarning("Supabase current-session precheck timed out; continuing account setup without the saved browser session.", error); reportSupabaseStep(onStage, "Saved Supabase session did not respond. Continuing with email and password account setup..."); await clearStaleSupabaseSession(); return null; }); if (options.requireExplicitPasswordSignIn && authUserEmail(existingUser) === profile.email) { await clearStaleSupabaseSession(); throw new Error(explicitRoleSignInRequiredMessage(profile.role)); } if (authUserMatchesVerifiedPhone(existingUser, profile)) { if (options.preventExistingAccount) { throw new Error("An account already exists with this phone number. Sign in with the existing account instead of creating a duplicate."); } return attachEmailPasswordToVerifiedPhoneUser(existingUser, profile, onStage); } if (authUserEmail(existingUser) === profile.email) { if (options.preventExistingAccount) { throw new Error("An account already exists with this email address. Sign in with the existing account instead of creating a duplicate."); } return existingUser; } if (existingUser) { reportSupabaseStep(onStage, "Switching Supabase user..."); await clearStaleSupabaseSession(); } if (!profile.email || !profile.password) { throw new Error("Enter an email and password to create the Supabase account."); } const credentials = { email: profile.email, password: profile.password }; reportSupabaseStep(onStage, "Checking for existing Supabase account..."); const signInFirst = await withSupabaseTimeout( supabaseClient.auth.signInWithPassword(credentials), "Checking for an existing Supabase account" ); if (!signInFirst.error && signInFirst.data?.user) { if (options.preventExistingAccount) { await clearStaleSupabaseSession(); throw new Error("An account already exists with this email address. Sign in with the existing account instead of creating a duplicate."); } return signInFirst.data.user; } if (signInFirst.error && !isInvalidLoginCredentialsError(signInFirst.error)) { throw signInFirst.error; } reportSupabaseStep(onStage, "Creating Supabase auth account..."); const signUpResult = await withSupabaseTimeout( supabaseClient.auth.signUp({ ...credentials, options: authSignupOptionsForProfile(profile) }), "Creating the Supabase auth account" ); if (isAlreadyRegisteredError(signUpResult.error) && options.preventExistingAccount) { throw new Error("An account already exists with this email address. Sign in with the existing account instead of creating a duplicate."); } if (signUpResult.error && !isAlreadyRegisteredError(signUpResult.error)) { throw signUpResult.error; } if (signUpResult.data?.user && !signUpResult.data?.session) { const confirmationRequested = await requestSupabaseSignupConfirmation(profile.email, profile.role, onStage); throw new Error(emailConfirmationNeededMessage(profile.role, confirmationRequested)); } if (options.requireExplicitPasswordSignIn && signUpResult.data?.session?.user) { await clearStaleSupabaseSession(); throw new Error(explicitRoleSignInRequiredMessage(profile.role)); } if (signUpResult.data?.session?.user) return signUpResult.data.session.user; reportSupabaseStep(onStage, "Signing in to Supabase..."); const signInResult = await withSupabaseTimeout( supabaseClient.auth.signInWithPassword(credentials), "Signing in to Supabase" ); if (signInResult.error) { if (isInvalidLoginCredentialsError(signInResult.error) && isAlreadyRegisteredError(signUpResult.error)) { const confirmationRequested = await requestSupabaseSignupConfirmation(profile.email, profile.role, onStage); throw new Error(`This email already has a Supabase login, but Supabase did not accept the sign-in yet. Waka will not create a duplicate account. ${confirmationRequested ? "I asked Supabase to resend the confirmation email; check the inbox and spam folder." : "Supabase did not confirm that a confirmation email was sent. Check the Supabase Auth email settings or temporarily disable email confirmation during the pilot."} After confirmation, sign in here with the same email and password.`); } if (isInvalidLoginCredentialsError(signInResult.error)) { if (signUpResult.data?.user && !signUpResult.data?.session) { const confirmationRequested = await requestSupabaseSignupConfirmation(profile.email, profile.role, onStage); throw new Error(emailConfirmationNeededMessage(profile.role, confirmationRequested)); } throw new Error("This email already has a Supabase login. Sign in with the existing password first; Waka will open the correct profile or application form."); } throw new Error(`${signInResult.error.message}. If email confirmation is enabled, confirm the email first or disable email confirmation during the pilot.`); } return signInResult.data.user; } async function refreshSupabaseSignIn(profile, onStage) { if (!profile.email || !profile.password) { throw new Error("Supabase session is stale. Enter the email and password again, then save."); } reportSupabaseStep(onStage, "Refreshing Supabase sign-in..."); await clearStaleSupabaseSession(); const { data, error } = await withSupabaseTimeout( supabaseClient.auth.signInWithPassword({ email: profile.email, password: profile.password }), "Refreshing Supabase sign-in" ); if (error) throw error; if (!data?.user) throw new Error("Supabase sign-in refreshed, but no user was returned."); return data.user; } async function upsertProfilePayload(payload, profile, user, onStage, didRefreshSession = false) { reportSupabaseStep(onStage, didRefreshSession ? "Retrying profile sync..." : "Syncing profile details in background..."); const { error } = await withSupabaseTimeout( supabaseClient.from("profiles").upsert(payload, { onConflict: "id" }), "Saving the profile row", supabaseProfileSaveTimeoutMs ); if (error && isMissingJwtUserError(error) && !didRefreshSession) { reportSupabaseStep(onStage, "Supabase session mismatch found. Refreshing sign-in..."); const refreshedUser = await refreshSupabaseSignIn(profile, onStage); await pause(800); return upsertProfilePayload({ ...payload, id: refreshedUser.id }, profile, refreshedUser, onStage, true); } if (error) throw error; lastProfileOnboardingSource = "direct profile upsert fallback"; return user; } async function syncProfileDetailsToSupabase(profile, user, onStage) { const payload = { id: user.id, role: profile.role, full_name: profile.name, email: profile.email, phone: profile.phone, phone_verified_at: profile.phoneVerifiedAt, national_id_number: profile.nationalId, date_of_birth: profile.dateOfBirth, preferred_language: profile.preferredLanguage, country: profile.country, city: profile.city }; if (profile.profilePhotoPath) { payload.profile_photo_path = profile.profilePhotoPath; } let syncedUser = null; if (!profileOnboardingRpcUnavailable.profile) { try { syncedUser = await upsertProfileWithOnboardingRpc(profile, user, onStage); } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; profileOnboardingRpcUnavailable.profile = true; logClientWarning("Profile onboarding RPC is not installed yet. Falling back to direct profile upsert.", error); } } if (!syncedUser) { reportSupabaseStep(onStage, "Profile onboarding RPC unavailable; using policy-protected profile save..."); syncedUser = await upsertProfilePayload(payload, profile, user, onStage); savePhoneVerificationEvent(syncedUser.id, profile.phone, profile.phoneVerificationProvider ?? "supabase-otp"); } queueProfilePhotoUpload(syncedUser.id, profile.role, profilePhotoInput(profile.role)?.files[0] ?? null); return syncedUser; } function profileRecoveryDraftFromLocalAccount(type, user) { if (!["passenger", "rider"].includes(type)) return null; const account = state[type]; const email = authUserEmail(user); if (!account || (email && account.email && String(account.email).toLowerCase() !== email)) return null; const country = account.country || defaultLaunchCountry(); const city = account.city || defaultLaunchCity(country); const draft = { ...account, role: type, name: account.name || email.split("@")[0] || (type === "rider" ? "Rider" : "Passenger"), email: account.email || email, phone: account.phone || user?.phone || "", phoneVerifiedAt: account.phoneVerifiedAt || user?.phone_confirmed_at || (account.phoneVerified ? new Date().toISOString() : null), nationalId: account.nationalId || account.credential || "", dateOfBirth: account.dateOfBirth || null, preferredLanguage: account.preferredLanguage || state.language, country, city, profilePhotoPath: account.profilePhotoPath ?? null, phoneVerificationProvider: account.phoneVerificationProvider ?? "supabase-auth-recovery" }; if (!draft.name || !draft.email || !draft.phone || !draft.phoneVerifiedAt || !draft.country || !draft.city) return null; return draft; } async function recoverMissingSupabaseProfileFromLocalAccount(type, user, onStage = null) { const draft = profileRecoveryDraftFromLocalAccount(type, user); if (!draft) return null; reportSupabaseStep(onStage, "Supabase sign-in worked. Re-syncing the missing Waka profile from this device..."); const syncedUser = await syncProfileDetailsToSupabase(draft, user, onStage); return loadSupabaseProfileForUser(syncedUser, "*", `Reloading the repaired ${type} profile`); } async function saveProfileToSupabase(profile, onStage, options = {}) { if (!isSupabaseMode()) return null; let user = await ensureSupabaseAuthUser(profile, onStage, options); if (!user) throw new Error("Supabase account could not be created or signed in."); reportSupabaseStep(onStage, options.waitForProfile ? `Creating Waka ${profile.role} profile before reporting success...` : "Account created. Finishing setup in the background..."); const profileSyncPromise = syncProfileDetailsToSupabase(profile, user, onStage) .then((syncedUser) => { reportSupabaseStep(onStage, `${profile.role === "rider" ? "Rider" : "Passenger"} account created and profile synced.`); return syncedUser; }) .catch((error) => { reportSupabaseStep(onStage, `${profile.role === "rider" ? "Rider" : "Passenger"} account created. Profile sync still needs attention: ${error.message}`); logClientWarning("Profile sync was not completed.", error); if (options.waitForProfile) throw error; return user; }); if (options.waitForProfile) { user = await profileSyncPromise; } return { ...user, profilePhotoPath: profile.profilePhotoPath ?? null, profileSyncPromise }; } function queueProfilePhotoUpload(userId, type, file) { if (!isSupabaseMode() || !file) return; uploadProfilePhoto(userId, type, file) .then(async (profilePhotoPath) => { if (!profilePhotoPath) return; if (type === "rider" && state.rider?.id === userId) { state.rider = { ...state.rider, profilePhotoPath }; state.riders = upsertById(state.riders, state.rider); saveState(); renderAll(); } if (type === "passenger" && state.passenger?.id === userId) { state.passenger = { ...state.passenger, profilePhotoPath }; state.passengers = upsertById(state.passengers, state.passenger); saveState(); renderAll(); } if (!profileOnboardingRpcUnavailable.photo) { try { await saveProfilePhotoPathWithRpc(profilePhotoPath); return; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; profileOnboardingRpcUnavailable.photo = true; logClientWarning("Profile photo RPC is not installed yet. Falling back to direct profile update.", error); } } assertClientFallbackAllowed("Profile photo path save", "supabase-profile-onboarding-rpc.sql"); lastProfileOnboardingSource = "direct profile photo update fallback"; const { error } = await withSupabaseTimeout( supabaseClient.from("profiles").update({ profile_photo_path: profilePhotoPath }).eq("id", userId), "Saving the profile photo path" ); if (error) throw error; }) .catch((error) => { logClientWarning("Profile photo upload was skipped.", error); }); } function profilePhotoInput(type) { return type === "rider" ? els.riderPhoto : els.passengerPhoto; } async function uploadProfilePhoto(userId, type, file) { if (!isSupabaseMode() || !file) return null; const safeName = file.name.replace(/[^a-z0-9._-]/gi, "-").toLowerCase(); const path = `${userId}/${type}-${Date.now()}-${safeName}`; const { error } = await withSupabaseTimeout( supabaseClient.storage .from(appConfig.buckets.profilePhotos) .upload(path, file, { upsert: false }), "Uploading the profile photo" ); if (error) throw error; return path; } async function uploadRiderDocument(userId, documentType, file) { if (!isSupabaseMode() || !file) return null; const safeName = file.name.replace(/[^a-z0-9._-]/gi, "-").toLowerCase(); const path = `${userId}/${documentType}-${Date.now()}-${safeName}`; const { error } = await withSupabaseTimeout( supabaseClient.storage .from(appConfig.buckets.riderDocuments) .upload(path, file, { upsert: false }), `Uploading the ${riderDocumentLabels[documentType]}` ); if (error) throw error; return path; } async function uploadRiderDocuments(userId) { const files = selectedRiderDocumentFiles(); const entries = await Promise.all(Object.entries(files).map(async ([documentType, file]) => { return [documentType, await uploadRiderDocument(userId, documentType, file)]; })); return Object.fromEntries(entries); } function riderOnboardingFunctionPayload(rider) { const vehicle = normalizeRideVehicle(rider.vehicle); return { name: rider.name, email: rider.email, password: rider.password, phone: rider.phone, phoneVerifiedAt: rider.phoneVerifiedAt, phoneVerificationProvider: rider.phoneVerificationProvider, nationalId: String(rider.nationalId || "").trim(), dateOfBirth: rider.dateOfBirth, preferredLanguage: rider.preferredLanguage, country: rider.country, city: rider.city, area: rider.area, vehicle, credential: riderIdentityReferenceForPayload(rider), driverLicenseExpiresOn: optionalSupabaseDate(rider.driverLicenseExpiresOn), registration: rider.registration, carMake: rider.carMake, carModel: rider.carModel, carBodyType: rider.carBodyType, vehicleDesignation: rider.vehicleDesignation, navigationPreference: riderNavigationPreference(rider), carYear: rider.carYear, carColor: rider.carColor, vehicleVin: rider.vehicleVin || "", insuranceProvider: riderInsuranceProviderForPayload(rider, vehicle), insuranceNumber: riderInsuranceNumberForPayload(rider, vehicle), insuranceExpiresOn: vehicle === "bike" ? null : optionalSupabaseDate(rider.insuranceExpiresOn), backgroundCheckConsent: true, backgroundCheckProvider: rider.backgroundCheckProvider || appConfig.backgroundCheckProvider || "not-required-cameroon", backgroundCheckConsentVersion: rider.backgroundCheckConsentVersion || "cameroon-pilot-admin-review" }; } const turnstileScriptUrl = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"; let turnstileScriptPromise = null; function turnstileSiteKey() { return String(appConfig.turnstileSiteKey || appConfig.cloudflareTurnstileSiteKey || "").trim(); } function turnstileRequiredForOnboarding() { return configFlagEnabled(appConfig.turnstileRequired); } function loadTurnstileScript() { if (window.turnstile?.render) return Promise.resolve(window.turnstile); if (turnstileScriptPromise) return turnstileScriptPromise; turnstileScriptPromise = new Promise((resolve, reject) => { const existing = document.querySelector(`script[src^="${turnstileScriptUrl.split("?")[0]}"]`); if (existing) { existing.addEventListener("load", () => resolve(window.turnstile), { once: true }); existing.addEventListener("error", () => reject(new Error("Waka security check could not load.")), { once: true }); return; } const script = document.createElement("script"); script.src = turnstileScriptUrl; script.async = true; script.defer = true; script.onload = () => resolve(window.turnstile); script.onerror = () => reject(new Error("Waka security check could not load.")); document.head.append(script); }); return turnstileScriptPromise; } function onboardingTurnstileContainer(role) { return role === "passenger" ? els.passengerTurnstile : els.riderTurnstile; } function onboardingTurnstileLabel(role) { return role === "passenger" ? "passenger account" : "rider account"; } async function renderOnboardingTurnstileChallenge(role) { const container = onboardingTurnstileContainer(role); if (!container) return null; if (!turnstileRequiredForOnboarding()) { container.hidden = true; return null; } const siteKey = turnstileSiteKey(); if (!siteKey) { container.hidden = true; return null; } container.hidden = false; if (container.dataset.turnstileWidgetId) return container.dataset.turnstileWidgetId; const turnstile = await loadTurnstileScript(); if (!turnstile?.render || !container.isConnected) return null; const widgetId = turnstile.render(container, { sitekey: siteKey, action: `${role}-onboarding`, callback: (token) => { container.dataset.turnstileToken = token || ""; }, "expired-callback": () => { delete container.dataset.turnstileToken; }, "error-callback": () => { delete container.dataset.turnstileToken; } }); container.dataset.turnstileWidgetId = String(widgetId); return widgetId; } function renderRiderTurnstileChallenge() { return renderOnboardingTurnstileChallenge("rider"); } function renderPassengerTurnstileChallenge() { return renderOnboardingTurnstileChallenge("passenger"); } function resetOnboardingTurnstileChallenge(role) { const container = onboardingTurnstileContainer(role); if (!container) return; delete container.dataset.turnstileToken; const widgetId = container.dataset.turnstileWidgetId; if (widgetId && window.turnstile?.reset) { try { window.turnstile.reset(widgetId); } catch (error) { logClientWarning("Turnstile reset was skipped.", error); } } } function resetRiderTurnstileChallenge() { resetOnboardingTurnstileChallenge("rider"); } function resetPassengerTurnstileChallenge() { resetOnboardingTurnstileChallenge("passenger"); } function readOnboardingTurnstileToken(role) { const container = onboardingTurnstileContainer(role); if (!container) return ""; const datasetToken = String(container.dataset?.turnstileToken || "").trim(); if (datasetToken) return datasetToken; const widgetId = container.dataset?.turnstileWidgetId; let responseToken = ""; if (widgetId && window.turnstile?.getResponse) { try { responseToken = String(window.turnstile.getResponse(widgetId) || "").trim(); } catch (error) { logClientWarning("Turnstile response token could not be read from the widget.", error); } } if (!responseToken) { responseToken = String(container.querySelector('[name="cf-turnstile-response"]')?.value || "").trim(); } if (responseToken) container.dataset.turnstileToken = responseToken; return responseToken; } async function waitForOnboardingTurnstileToken(role, timeoutMs = 1200) { const startedAt = Date.now(); let token = readOnboardingTurnstileToken(role); while (!token && Date.now() - startedAt < timeoutMs) { await new Promise((resolve) => window.setTimeout(resolve, 100)); token = readOnboardingTurnstileToken(role); } return token; } async function onboardingTurnstileToken(role, onStage = null) { if (!turnstileRequiredForOnboarding()) return ""; if (!turnstileSiteKey()) { throw new Error(`Waka security check is not configured. Add the Cloudflare Turnstile site key before accepting ${role === "passenger" ? "passenger signups" : "rider applications"}.`); } await renderOnboardingTurnstileChallenge(role); const token = await waitForOnboardingTurnstileToken(role); if (!token) { reportSupabaseStep(onStage, "Complete the Waka security check before submitting."); throw new Error(`Complete the Waka security check before submitting the ${onboardingTurnstileLabel(role)}.`); } return token; } function riderTurnstileToken(onStage = null) { return onboardingTurnstileToken("rider", onStage); } function passengerTurnstileToken(onStage = null) { return onboardingTurnstileToken("passenger", onStage); } function appendOnboardingFile(formData, key, file) { if (!file) return; formData.append(key, file, file.name); } function passengerOnboardingFunctionPayload(passenger) { return { name: passenger.name, email: passenger.email, password: passenger.password, phone: passenger.phone, phoneVerifiedAt: passenger.phoneVerifiedAt, phoneVerificationProvider: passenger.phoneVerificationProvider, accountUse: passenger.accountUse, nationalId: passenger.nationalId, dateOfBirth: passenger.dateOfBirth, preferredLanguage: passenger.preferredLanguage, country: passenger.country, city: passenger.city }; } function riderAccountOnboardingFunctionPayload(rider) { return { accountOnly: true, name: rider.name, email: rider.email, password: rider.password, phone: rider.phone, phoneVerifiedAt: rider.phoneVerifiedAt, phoneVerificationProvider: rider.phoneVerificationProvider, preferredLanguage: rider.preferredLanguage, country: rider.country, city: rider.city }; } async function submitPassengerAccountViaOnboardingFunction(passenger, onStage = null) { if (!hasSupabaseRuntime()) throw new Error("Supabase runtime is required for secure passenger onboarding."); reportSupabaseStep(onStage, "Creating passenger account through secure onboarding..."); const turnstileToken = await passengerTurnstileToken(onStage); const onboardingPayload = passengerOnboardingFunctionPayload(passenger); if (turnstileToken) onboardingPayload.turnstileToken = turnstileToken; const formData = new FormData(); formData.append("payload", JSON.stringify(onboardingPayload)); if (turnstileToken) formData.append("turnstileToken", turnstileToken); appendOnboardingFile(formData, "profilePhoto", els.passengerPhoto?.files?.[0] ?? null); const response = await fetch(`${appConfig.supabaseUrl}/functions/v1/${passengerOnboardingSubmitFunctionName()}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${supabaseRestSession?.access_token || appConfig.supabaseAnonKey}` }, body: formData }); const responsePayload = await response.json().catch(() => ({})); if (!response.ok) { resetPassengerTurnstileChallenge(); throw new Error(responsePayload?.error || "Passenger onboarding Edge Function failed."); } if (!responsePayload?.userId) { resetPassengerTurnstileChallenge(); throw new Error("Passenger onboarding completed, but no passenger account id was returned."); } resetPassengerTurnstileChallenge(); return responsePayload; } async function passengerOnboardingFunctionUserResult(passenger, result, onStage = null) { const baseUser = { id: result.userId, email: passenger.email, profilePhotoPath: result.profilePhotoPath ?? passenger.profilePhotoPath ?? null, emailSetupPending: Boolean(result.emailConfirmationRequired) }; if (baseUser.emailSetupPending || !supabaseClient?.auth?.signInWithPassword) return baseUser; try { reportSupabaseStep(onStage, "Signing in to open the passenger workspace..."); const { data, error } = await withSupabaseTimeout( supabaseClient.auth.signInWithPassword({ email: passenger.email, password: passenger.password }), "Signing in after passenger onboarding", supabaseProfileSaveTimeoutMs ); if (error) throw error; if (!data?.user?.id) throw new Error("Supabase sign-in succeeded without returning the passenger account."); return { ...data.user, profilePhotoPath: baseUser.profilePhotoPath, emailSetupPending: false }; } catch (error) { logClientWarning("Passenger account was created, but browser sign-in is still pending.", error); return { ...baseUser, emailSetupPending: true, signInPendingMessage: error?.message || "Confirm the email, then sign in with the same password." }; } } async function submitRiderAccountViaOnboardingFunction(rider, onStage = null) { if (!hasSupabaseRuntime()) throw new Error("Supabase runtime is required for secure rider onboarding."); reportSupabaseStep(onStage, "Creating rider login through secure onboarding..."); const turnstileToken = await riderTurnstileToken(onStage); const onboardingPayload = riderAccountOnboardingFunctionPayload(rider); if (turnstileToken) onboardingPayload.turnstileToken = turnstileToken; const formData = new FormData(); formData.append("payload", JSON.stringify(onboardingPayload)); if (turnstileToken) formData.append("turnstileToken", turnstileToken); appendOnboardingFile(formData, "profilePhoto", els.riderPhoto?.files?.[0] ?? null); const response = await fetch(`${appConfig.supabaseUrl}/functions/v1/${riderOnboardingSubmitFunctionName()}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${supabaseRestSession?.access_token || appConfig.supabaseAnonKey}` }, body: formData }); const responsePayload = await response.json().catch(() => ({})); if (!response.ok) { resetRiderTurnstileChallenge(); throw new Error(responsePayload?.error || "Rider login onboarding Edge Function failed."); } if (!responsePayload?.userId) { resetRiderTurnstileChallenge(); throw new Error("Rider login was created, but no rider account id was returned."); } resetRiderTurnstileChallenge(); return responsePayload; } async function submitRiderApplicationViaOnboardingFunction(rider, onStage = null) { if (!hasSupabaseRuntime()) throw new Error("Supabase runtime is required for secure rider onboarding."); reportSupabaseStep(onStage, "Submitting rider application through secure onboarding..."); const turnstileToken = await riderTurnstileToken(onStage); const onboardingPayload = riderOnboardingFunctionPayload(rider); if (turnstileToken) onboardingPayload.turnstileToken = turnstileToken; const formData = new FormData(); formData.append("payload", JSON.stringify(onboardingPayload)); if (turnstileToken) formData.append("turnstileToken", turnstileToken); appendOnboardingFile(formData, "profilePhoto", els.riderPhoto?.files?.[0] ?? null); Object.entries(selectedRiderDocumentFiles()).forEach(([key, file]) => { appendOnboardingFile(formData, key, file); }); const response = await fetch(`${appConfig.supabaseUrl}/functions/v1/${riderOnboardingSubmitFunctionName()}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${supabaseRestSession?.access_token || appConfig.supabaseAnonKey}` }, body: formData }); const responsePayload = await response.json().catch(() => ({})); if (!response.ok) { resetRiderTurnstileChallenge(); throw new Error(responsePayload?.error || "Rider onboarding Edge Function failed."); } if (!responsePayload?.userId) { resetRiderTurnstileChallenge(); throw new Error("Rider onboarding completed, but no rider account id was returned."); } resetRiderTurnstileChallenge(); return responsePayload; } async function riderOnboardingFunctionUserResult(rider, result, onStage = null) { const baseUser = { id: result.userId, email: rider.email, profilePhotoPath: result.profilePhotoPath ?? rider.profilePhotoPath ?? null, emailSetupPending: Boolean(result.emailConfirmationRequired) }; if (baseUser.emailSetupPending || !supabaseClient?.auth?.signInWithPassword) return baseUser; try { reportSupabaseStep(onStage, "Signing in to open the rider workspace..."); const { data, error } = await withSupabaseTimeout( supabaseClient.auth.signInWithPassword({ email: rider.email, password: rider.password }), "Signing in after rider onboarding", supabaseProfileSaveTimeoutMs ); if (error) throw error; if (!data?.user?.id) throw new Error("Supabase sign-in succeeded without returning the rider account."); return { ...data.user, profilePhotoPath: baseUser.profilePhotoPath, emailSetupPending: false }; } catch (error) { logClientWarning("Rider application was submitted, but browser sign-in is still pending.", error); return { ...baseUser, emailSetupPending: true, signInPendingMessage: error?.message || "Confirm the email, then sign in with the same password." }; } } async function submitRiderComplianceRenewalToSupabase(rider, renewal) { const riderId = rider?.id ?? state.sessions?.rider?.userId; if (!riderId) throw new Error("Rider sign-in is required before uploading renewed documents."); const driverLicenseDocumentPath = renewal.driverLicenseFile ? await uploadRiderDocument(riderId, "driverLicense", renewal.driverLicenseFile) : null; const insuranceDocumentPath = renewal.insuranceFile ? await uploadRiderDocument(riderId, "insurance", renewal.insuranceFile) : null; const documents = { ...riderDocuments(rider), ...(driverLicenseDocumentPath ? { driverLicense: driverLicenseDocumentPath } : {}), ...(insuranceDocumentPath ? { insurance: insuranceDocumentPath } : {}) }; if (!hasSupabaseRuntime()) { return { application: { status: rider?.status === "suspended" ? "pending" : rider?.status, review_note: rider?.status === "suspended" ? "Renewed compliance documents submitted for admin review." : rider?.reviewNote ?? "", driver_license_expires_on: renewal.driverLicenseExpiresOn || rider?.driverLicenseExpiresOn || null, insurance_expires_on: renewal.insuranceExpiresOn || rider?.insuranceExpiresOn || null, document_path: riderDocumentPayload(documents) }, documents }; } if (profileOnboardingRpcUnavailable.riderComplianceRenewal) { throw new Error("Rider compliance renewal backend is not installed yet. Run supabase-rider-compliance-expirations.sql."); } try { const data = await callSupabaseRpc( "submit_rider_compliance_renewal", { p_driver_license_expires_on: renewal.driverLicenseExpiresOn || null, p_driver_license_document_path: driverLicenseDocumentPath, p_insurance_expires_on: renewal.insuranceExpiresOn || null, p_insurance_document_path: insuranceDocumentPath }, "Submitting renewed rider documents", supabaseProfileSaveTimeoutMs + optionalSupabaseRequestTimeoutMs ); const application = Array.isArray(data) ? data[0] : data; return { application, documents }; } catch (error) { if (adminDirectoryRpcMissing(error)) { profileOnboardingRpcUnavailable.riderComplianceRenewal = true; throw new Error("Rider compliance renewal backend is not installed yet. Run supabase-rider-compliance-expirations.sql."); } throw error; } } function riderOnboardingSubmitFunctionName() { return String(appConfig.riderOnboardingSubmitFunctionName || "rider-onboarding-submit").trim() || "rider-onboarding-submit"; } function passengerOnboardingSubmitFunctionName() { return String(appConfig.passengerOnboardingSubmitFunctionName || "passenger-onboarding-submit").trim() || "passenger-onboarding-submit"; } function passwordResetRequestFunctionName() { return String(appConfig.passwordResetRequestFunctionName || "password-reset-request").trim() || "password-reset-request"; } function passwordResetPhoneOtpFunctionName() { return String(appConfig.passwordResetPhoneOtpFunctionName || "password-reset-phone-otp").trim() || "password-reset-phone-otp"; } function passwordResetCompleteFunctionName() { return String(appConfig.passwordResetCompleteFunctionName || "password-reset-complete").trim() || "password-reset-complete"; } function runtimeAllowsTestingRelaxation() { const projectName = String(appConfig.projectName || "").toLowerCase(); const hostname = String(window.location?.hostname || "").toLowerCase(); return /\b(staging|stage|pilot|test|preview|local)\b/.test(projectName) || hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "staging.wakagood.com" || hostname.endsWith(".pages.dev"); } function passwordResetPhoneOtpRelaxedForTesting() { return configFlagEnabled(appConfig.relaxPasswordResetPhoneOtpForTesting) && runtimeAllowsTestingRelaxation(); } function passwordResetPhoneOtpRequired() { return configFlagEnabled(appConfig.passwordResetPhoneOtpRequired ?? true) && !passwordResetPhoneOtpRelaxedForTesting(); } function secureOnboardingFunctionsEnabled() { return configFlagEnabled(appConfig.secureOnboardingFunctionsEnabled); } function storagePathCanBeSigned(path) { return Boolean(path && typeof path === "string" && path.includes("/")); } function storageReviewButton(label, bucket, path) { if (!storagePathCanBeSigned(path)) return ""; return ``; } async function openSignedStorageFile(bucket, path, label) { const signedInUser = state.adminSession || state.sessions.rider || state.sessions.passenger; if (!signedInUser) { if (els.adminStatus) els.adminStatus.textContent = "Sign in before opening stored files."; return; } if (!isSupabaseMode()) { els.adminStatus.textContent = "Secure file viewing is available after Supabase sign-in."; return; } if (!storagePathCanBeSigned(path)) { els.adminStatus.textContent = `${label} is stored as a file name only. New Supabase uploads can be opened securely from here.`; return; } let pendingWindow = null; try { els.adminStatus.textContent = `Creating secure link for ${label}...`; pendingWindow = window.open("", "_blank", "noopener,noreferrer"); if (pendingWindow) { pendingWindow.document.title = `Opening ${label}`; pendingWindow.document.body.textContent = `Opening secure Waka ${label} link...`; } const { data, error } = await withSupabaseTimeout( supabaseClient.storage.from(bucket).createSignedUrl(path, 300), `Creating secure link for ${label}`, optionalSupabaseRequestTimeoutMs ); if (error) throw error; if (pendingWindow) pendingWindow.location.href = data.signedUrl; const opened = pendingWindow || window.open(data.signedUrl, "_blank", "noopener,noreferrer"); els.adminStatus.textContent = opened ? `Opened secure 5-minute link for ${label}.` : `Secure link created for ${label}, but the browser blocked the new tab.`; if (state.adminSession) { void logAdminAudit("admin_open_storage_file", "profiles", state.adminDetail?.id ?? null, { bucket, storage_path: path, file_label: label }); } } catch (error) { try { if (pendingWindow && !pendingWindow.closed) pendingWindow.close(); } catch {} if (els.adminStatus) els.adminStatus.textContent = `Could not open ${label}: ${error.message}`; } } async function saveRiderApplicationToSupabase(rider, userId) { if (!isSupabaseMode()) return; const uploadedDocuments = await uploadRiderDocuments(userId); const vehicle = normalizeRideVehicle(rider.vehicle); const carBodyType = vehicle === "bike" ? "motorbike" : normalizeCarBodyType(rider.carBodyType); const vehicleDesignation = vehicle === "bike" ? "normal" : normalizeRiderVehicleDesignation(rider.vehicleDesignation, carBodyType); const documents = { ...riderDocuments(rider), ...Object.fromEntries(Object.entries(uploadedDocuments).filter(([, value]) => Boolean(value))) }; documents.vehicleDesignation = vehicleDesignation; documents.navigationPreference = riderNavigationPreference(rider); const applicationPayload = { vehicle, operating_area: rider.area, credential_number: riderIdentityReferenceForPayload(rider), vehicle_registration: rider.registration, car_make: rider.carMake, car_model: rider.carModel, car_body_type: carBodyType, vehicle_designation: vehicleDesignation, car_year: rider.carYear ? Number(rider.carYear) : null, car_color: rider.carColor, vehicle_vin: rider.vehicleVin, insurance_provider: riderInsuranceProviderForPayload(rider, vehicle), insurance_number: riderInsuranceNumberForPayload(rider, vehicle), driver_license_expires_on: optionalSupabaseDate(rider.driverLicenseExpiresOn), insurance_expires_on: vehicle === "bike" ? null : optionalSupabaseDate(rider.insuranceExpiresOn), background_check_consent_at: rider.backgroundCheckConsentAt || new Date().toISOString(), background_check_consent_provider: rider.backgroundCheckProvider || appConfig.backgroundCheckProvider || "not-required-cameroon", background_check_consent_version: rider.backgroundCheckConsentVersion || "cameroon-pilot-admin-review", document_path: riderDocumentPayload(documents) }; if (!profileOnboardingRpcUnavailable.riderApplication) { try { await submitRiderApplicationWithRpc(rider, applicationPayload.document_path); return documents; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; profileOnboardingRpcUnavailable.riderApplication = true; logClientWarning("Rider application RPC is not installed yet. Falling back to direct application write.", error); } } assertClientFallbackAllowed("Rider application submission", "supabase-profile-onboarding-rpc.sql"); const { data: existingApplication, error: lookupError } = await withSupabaseTimeout( supabaseClient .from("rider_applications") .select("id,status") .eq("rider_id", userId) .order("created_at", { ascending: false }) .limit(1) .maybeSingle(), "Checking for an existing rider application", supabaseProfileSaveTimeoutMs ); if (lookupError) throw lookupError; if (existingApplication?.id) { if (!["pending", "needs_correction", "declined"].includes(existingApplication.status)) { throw new Error("Approved or suspended rider applications can only be changed by admin support."); } lastProfileOnboardingSource = "direct rider application update fallback"; const { error: updateError } = await withSupabaseTimeout( supabaseClient.from("rider_applications").update({ ...applicationPayload, status: "pending", review_note: null, reviewed_by: null, reviewed_at: null }).eq("id", existingApplication.id), "Updating the existing rider application", supabaseProfileSaveTimeoutMs ); if (updateError) throw updateError; return documents; } lastProfileOnboardingSource = "direct rider application insert fallback"; const { error } = await withSupabaseTimeout( supabaseClient.from("rider_applications").insert({ rider_id: userId, ...applicationPayload }), "Saving the rider application", supabaseProfileSaveTimeoutMs ); if (error) throw error; return documents; } async function callSupabaseRpcResult(functionName, body, label, timeoutMs = optionalSupabaseRequestTimeoutMs) { if (!hasSupabaseRuntime()) return null; if (!supabaseClient) { return withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rpc/${functionName}`, { method: "POST", body }), label, timeoutMs ); } const { data, error } = await withSupabaseTimeout( supabaseClient.rpc(functionName, body), label, timeoutMs ); if (error) { if (shouldRetrySupabaseRpcWithFreshSession(error)) { const accessToken = await currentSupabaseAccessToken(); if (accessToken) { return withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rpc/${functionName}`, { method: "POST", body, accessToken }), label, timeoutMs ); } } throw error; } return data; } async function profileContactAvailability(email, phone, excludeUserId = null, role = null) { if (!hasSupabaseRuntime()) return { emailAvailable: true, phoneAvailable: true }; const lookupRole = strictRoleAuthIsolationEnabled() && normalizedPublicRole(role) ? null : role; const rows = await callSupabaseRpcResult( "profile_contact_available", { p_email: email, p_phone: phone, p_exclude_user_id: excludeUserId, p_role: lookupRole }, "Checking whether this email and phone are available", optionalSupabaseRequestTimeoutMs ); const result = Array.isArray(rows) ? rows[0] : rows; return { emailAvailable: result?.email_available !== false, phoneAvailable: result?.phone_available !== false }; } async function existingProfileRoleSetup(email, password, phone, role, availability = {}, onStage = null) { if (!hasSupabaseRuntime() || !role) return { action: "new" }; const normalizedEmail = String(email || "").trim().toLowerCase(); const duplicateEmail = availability.emailAvailable === false; const duplicatePhone = availability.phoneAvailable === false; if (!duplicateEmail && !duplicatePhone) return { action: "new" }; if (strictRoleAuthIsolationEnabled() && normalizedPublicRole(role)) { return { action: "blocked", reason: duplicateEmail ? "email" : duplicatePhone ? "phone" : "contact" }; } let user = await getSupabaseUser().catch((error) => { logClientWarning("Current Supabase user could not be checked before role setup.", error); return null; }); const signedInOwnsEmail = authUserEmail(user) === normalizedEmail; const signedInOwnsPhone = authUserMatchesVerifiedPhone(user, { phone }); if (!signedInOwnsEmail && !signedInOwnsPhone) { if (!normalizedEmail || !password || !supabaseClient?.auth?.signInWithPassword) { return { action: "blocked", reason: duplicatePhone ? "phone" : "contact" }; } reportSupabaseStep(onStage, "Signing in to the existing Waka login so passenger access can be added..."); const { data, error } = await withSupabaseTimeout( supabaseClient.auth.signInWithPassword({ email: normalizedEmail, password }), "Signing in to the existing Waka login", optionalSupabaseRequestTimeoutMs ); if (error) { if (isInvalidLoginCredentialsError(error)) return { action: "blocked", reason: "credentials" }; throw error; } user = data?.user ?? null; } if (!user) return { action: "blocked", reason: "contact" }; const profile = await loadSupabaseProfileForUser(user, "*", "Loading existing Waka profile for role setup").catch((error) => { logClientWarning("Existing Waka profile could not be loaded before role setup.", error); return null; }); if (duplicatePhone && profile?.phone && !phoneMatches(profile.phone, phone)) { return { action: "blocked", reason: "phone" }; } if (profile && await signedInProfileHasRole(profile, role)) { return { action: "existing_role", user, profile }; } return { action: "can_add_role", user, profile }; } function roleSetupBlockedMessage(role, reason = "contact") { const label = role === "rider" ? "rider" : "passenger"; const splitGuidance = `In this single Auth deployment, passenger and rider accounts need unique contact details until the passenger/rider Auth split is completed. Use a different email and phone for the ${label} account, or finish the two-project Auth setup before reusing this contact for both roles.`; if (reason === "credentials") { return `This email is already attached to a Waka login. ${splitGuidance}`; } if (reason === "phone") { return `This phone number is already attached to another Waka login. ${splitGuidance}`; } return `This email or phone is already attached to a Waka login. ${splitGuidance}`; } function rememberRoleSetupPhoneVerification(type, phone, profile) { if (!profile?.phone_verified_at || !phoneMatches(profile.phone, phone)) return false; state.verification[type] = { phone, phoneDigits: phoneDigits(phone), verifiedPhone: phone, verifiedAt: profile.phone_verified_at, provider: "existing-waka-profile" }; return true; } async function profileAvailabilityExcludeUserId(email, fallbackUserId = null) { if (!hasSupabaseRuntime()) return fallbackUserId; try { const user = await getSupabaseUser(); return authUserEmail(user) === String(email || "").trim().toLowerCase() ? user.id : fallbackUserId; } catch (error) { logClientWarning("Signed-in profile identity could not be checked before contact availability.", error); return fallbackUserId; } } function supabaseBooleanResult(value) { if (Array.isArray(value)) return supabaseBooleanResult(value[0]); if (typeof value === "boolean") return value; if (value && typeof value === "object") { const firstValue = Object.values(value)[0]; return firstValue === true || firstValue === "true"; } return value === true || value === "true"; } async function signedInProfileHasRole(profile, role) { if (!profile?.id || !role) return false; if (profile.role === role) return true; if (!hasSupabaseRuntime()) return false; try { const result = await callSupabaseRpcResult( "profile_has_role", { p_user_id: profile.id, p_role: role }, `Checking whether this account has ${role} access`, optionalSupabaseRequestTimeoutMs ); return supabaseBooleanResult(result); } catch (error) { logClientWarning("Profile role membership check was skipped.", error); return false; } } async function signedInProfileHasRiderApplication(profile) { if (!profile?.id || !hasSupabaseRuntime()) return false; try { if (supabaseClient) { const { data, error } = await withSupabaseTimeout( supabaseClient .from("rider_applications") .select("id,rider_id,status") .eq("rider_id", profile.id) .order("created_at", { ascending: false }) .limit(1), "Checking whether this account has an existing rider application", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return Array.isArray(data) && data.length > 0; } const params = new URLSearchParams(); params.set("rider_id", `eq.${profile.id}`); params.set("select", "id,rider_id,status"); params.set("order", "created_at.desc"); params.set("limit", "1"); const rows = await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rider_applications?${params.toString()}`, { accessToken: supabaseRestSession?.access_token }), "Checking whether this account has an existing rider application", optionalSupabaseRequestTimeoutMs ); return Array.isArray(rows) && rows.length > 0; } catch (error) { logClientWarning("Existing rider application check was skipped.", error); return false; } } function uniqueWorkspaceRoleCandidates(candidates = []) { const roles = []; candidates.forEach((candidate) => { if (!["passenger", "rider"].includes(candidate)) return; if (!availableWorkspaceTab(candidate)) return; if (!roles.includes(candidate)) roles.push(candidate); }); return roles; } function roleRequestedByCurrentShell() { if (typeof publicHomePathIsActive === "function" && publicHomePathIsActive()) return null; if (typeof requestedTabFromLocation === "function") { const requested = requestedTabFromLocation(); if (["passenger", "rider"].includes(requested)) return requested; } if (["passenger", "rider"].includes(state.activeTab)) return state.activeTab; return null; } function keepPublicHomeVisibleForSessionRestore() { if (typeof publicHomePathIsActive !== "function" || !publicHomePathIsActive()) return false; if (typeof requestedTabFromLocation !== "function") return true; return !requestedTabFromLocation(); } function locallyRememberedWorkspaceRole(profile, role) { if (!profile?.id || !["passenger", "rider"].includes(role)) return false; const session = state.sessions?.[role]; const account = state[role]; return session?.userId === profile.id || account?.id === profile.id || account?.supabaseUserId === profile.id; } function workspaceRoleHasRestoreIntent(profile, role) { if (!profile?.id || !["passenger", "rider"].includes(role)) return false; return profile.role === role || locallyRememberedWorkspaceRole(profile, role); } function clearWorkspaceRoleSession(role) { if (!["passenger", "rider"].includes(role)) return; state.sessions[role] = null; state[role] = null; state.accountMode[role] = "signin"; if (role === "passenger") { state.passengerPage = "request"; state.passengerSelectedOfferId = null; } if (role === "rider") { state.riderPage = "overview"; state.riderAvailabilityActivated = false; } } function clearAdminRoleSession() { state.adminSession = null; state.adminDetail = null; state.adminPage = "overview"; if (typeof resetAdminData === "function") resetAdminData(); } function clearOtherRoleSessions(activeRole) { const role = String(activeRole || "").trim().toLowerCase(); if (role === "passenger") { clearWorkspaceRoleSession("rider"); clearAdminRoleSession(); return; } if (role === "rider") { clearWorkspaceRoleSession("passenger"); clearAdminRoleSession(); return; } if (role === "admin") { clearWorkspaceRoleSession("passenger"); clearWorkspaceRoleSession("rider"); } } function workspaceRoleAccountId(role) { const account = state?.[role] ?? null; return String(account?.supabaseUserId ?? account?.id ?? "").trim(); } function workspaceRoleSessionMatchesAccount(role) { if (!["passenger", "rider"].includes(role)) return false; const session = state.sessions?.[role] ?? null; const account = state[role] ?? null; if (!session || !account) return false; const sessionUserId = String(session.userId ?? "").trim(); const accountId = workspaceRoleAccountId(role); if (sessionUserId && accountId && sessionUserId !== accountId) return false; const sessionEmail = String(session.email ?? "").trim().toLowerCase(); const accountEmail = String(account.email ?? "").trim().toLowerCase(); if (sessionEmail && accountEmail && sessionEmail !== accountEmail) return false; const sessionPhone = String(session.phone ?? "").trim(); const accountPhone = String(account.phone ?? "").trim(); if (sessionPhone && accountPhone && typeof phoneMatches === "function" && !phoneMatches(sessionPhone, accountPhone)) return false; return true; } function scrubInvalidWorkspaceSessions() { let changed = false; for (const role of ["passenger", "rider"]) { if (!state.sessions?.[role]) continue; if (!state[role]) continue; if (workspaceRoleSessionMatchesAccount(role)) continue; clearWorkspaceRoleSession(role); changed = true; } return changed; } function activateWorkspaceRoleSession(role, session) { if (!["passenger", "rider"].includes(role)) return; clearOtherRoleSessions(role); state.sessions[role] = { ...(session ?? {}), signedInAt: session?.signedInAt ?? new Date().toISOString() }; } async function signedInProfileCanRestoreWorkspaceRole(profile, role) { if (!profile?.id || !["passenger", "rider"].includes(role)) return false; if (!workspaceRoleHasRestoreIntent(profile, role)) return false; if (signedInProfileHasPrimaryRole(profile, role)) return true; if (strictRoleAuthIsolationEnabled()) return false; if (await signedInProfileHasRole(profile, role)) return true; if (role === "rider" && await signedInProfileHasRiderApplication(profile)) return true; return false; } async function pruneUnauthorizedWorkspaceSessionsForProfile(profile) { if (!profile?.id) return; for (const role of ["passenger", "rider"]) { if (!locallyRememberedWorkspaceRole(profile, role)) continue; if (await signedInProfileCanRestoreWorkspaceRole(profile, role)) continue; clearWorkspaceRoleSession(role); } } async function resolveSupabaseSessionWorkspaceRole(profile) { if (!profile?.id) return null; const storedRiderSession = state.sessions?.rider?.userId === profile.id ? "rider" : null; const storedPassengerSession = state.sessions?.passenger?.userId === profile.id ? "passenger" : null; const candidates = uniqueWorkspaceRoleCandidates([ roleRequestedByCurrentShell(), storedRiderSession, storedPassengerSession, profile.role ]); for (const role of candidates) { if (await signedInProfileCanRestoreWorkspaceRole(profile, role)) { const canCheckRiderApplication = role === "rider" && (!strictRoleAuthIsolationEnabled() || signedInProfileHasPrimaryRole(profile, "rider")); const needsApplication = canCheckRiderApplication ? !(await signedInProfileHasRiderApplication(profile)) : false; return { role, needsApplication }; } } return null; } function profileForWorkspaceRole(profile, roleResolution) { if (!profile || !roleResolution?.role) return profile; const { role, needsApplication = false } = roleResolution; if (profile.role === role && !needsApplication) return profile; return { ...profile, primaryRole: profile.role, role, needsApplication, status: needsApplication ? "profile only" : profile.status }; } async function signedInProfileHasActiveAdminAssignment(profile) { if (!profile?.id || !hasSupabaseRuntime()) return false; try { if (!supabaseClient) { const params = new URLSearchParams(); params.set("admin_id", `eq.${profile.id}`); params.set("revoked_at", "is.null"); params.set("select", "admin_id"); params.set("limit", "1"); const rows = await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/admin_role_assignments?${params.toString()}`), "Checking admin role assignments", optionalSupabaseRequestTimeoutMs ); return Array.isArray(rows) && rows.length > 0; } const { data, error } = await withSupabaseTimeout( supabaseClient .from("admin_role_assignments") .select("admin_id") .eq("admin_id", profile.id) .is("revoked_at", null) .limit(1), "Checking admin role assignments", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return Array.isArray(data) && data.length > 0; } catch (error) { logClientWarning("Admin role assignment check was skipped.", error); return false; } } async function signedInProfileIsAdminIdentity(profile) { if (!profile?.id) return false; if (profile.role === "admin") return true; if (await signedInProfileHasRole(profile, "admin")) return true; return signedInProfileHasActiveAdminAssignment(profile); } function mapSafetyReportFromDatabase(report, profileMap = new Map(), requestMap = new Map()) { const reporter = profileMap.get(report.reporter_id); const reportedUser = profileMap.get(report.reported_user_id); const request = requestMap.get(report.ride_request_id); return { id: report.id, requestId: report.ride_request_id, reporterId: report.reporter_id, reporterName: reporter?.full_name ?? reporter?.email ?? "Reporter", reporterRole: report.reporter_role, reportedUserId: report.reported_user_id, reportedUserName: reportedUser?.full_name ?? reportedUser?.email ?? "Unknown account", category: report.category, severity: report.severity, details: report.details, status: report.status, reviewedBy: report.reviewed_by, reviewedAt: report.reviewed_at, routeSummary: request ? `${request.pickupArea} to ${requestDestinationText(request)}` : `Ride ${report.ride_request_id}`, createdAt: report.created_at }; } function mapTaxDocumentFromDatabase(row, profileMap = new Map()) { const rider = profileMap.get(row.rider_id); return { id: row.id, riderId: row.rider_id, riderName: rider?.full_name ?? "Rider", taxYear: row.tax_year, documentType: row.document_type, provider: row.provider, storagePath: row.storage_path, providerDocumentId: row.provider_document_id ?? "", providerAccountId: row.provider_account_id ?? "", providerDocumentUrl: row.provider_document_url ?? "", deliveryMethod: row.delivery_method ?? (row.storage_path ? "private_storage" : "provider_portal"), filingStatus: row.filing_status ?? "", status: row.status, availableAt: row.available_at ?? null, filedAt: row.filed_at ?? null, issuedAt: row.issued_at ?? null, metadata: row.metadata ?? {}, createdAt: row.created_at, updatedAt: row.updated_at ?? row.created_at }; } function mapTaxIdentityReferenceFromDatabase(row, profileMap = new Map()) { const rider = profileMap.get(row.rider_id); return { id: row.id, riderId: row.rider_id, riderName: rider?.full_name ?? "Rider", provider: row.provider, providerSubjectId: maskProviderReference(row.provider_subject_id), status: row.tax_profile_status, tinLast4: row.tin_last4 ?? "", legalName: row.legal_name ?? "", businessName: row.business_name ?? "", taxClassification: row.tax_classification ?? "", lastVerifiedAt: row.last_verified_at ?? null, createdAt: row.created_at, updatedAt: row.updated_at }; } function mapRiderBackgroundCheckFromDatabase(row, profileMap = new Map()) { const rider = profileMap.get(row.rider_id); return { id: row.id, riderId: row.rider_id, riderName: rider?.full_name ?? "Rider", provider: row.provider, providerReference: row.provider_reference, status: row.status, decision: row.decision, summary: row.summary ?? "", completedAt: row.completed_at ?? null, createdAt: row.created_at }; } function mapRideRatingFromDatabase(row, profileMap = new Map()) { const rated = profileMap.get(row.rated_user_id); const reviewer = profileMap.get(row.reviewer_id); return { id: row.id, requestId: row.ride_request_id, reviewerId: row.reviewer_id, reviewerRole: row.reviewer_role, reviewerName: reviewer?.full_name ?? "Reviewer", ratedUserId: row.rated_user_id, ratedUserName: rated?.full_name ?? "Rated account", score: row.score, safetyScore: row.safety_score ?? row.score, punctualityScore: row.punctuality_score ?? row.score, communicationScore: row.communication_score ?? row.score, vehicleScore: row.vehicle_score ?? row.score, comment: row.comment ?? "", createdAt: row.created_at }; } function mapRiderRatingSummaryFromDatabase(row = {}) { const numberOrNull = (value) => { const numeric = Number(value); return Number.isFinite(numeric) ? numeric : null; }; return { riderId: row.rider_id ?? state.rider?.id ?? null, ratingCount: Number(row.rating_count ?? 0) || 0, overallPercent: numberOrNull(row.overall_percent), safetyPercent: numberOrNull(row.safety_percent), punctualityPercent: numberOrNull(row.punctuality_percent), communicationPercent: numberOrNull(row.communication_percent), vehiclePercent: numberOrNull(row.vehicle_percent), latestRatingAt: row.latest_rating_at ?? null }; } async function loadMyRiderRatingSummaryFromSupabase() { if (!hasSupabaseRuntime() || !state.rider?.id) { state.riderRatingSummary = null; return null; } try { const rows = await callSupabaseRpcResult( "my_rider_rating_summary", {}, "Loading anonymous rider rating summary", optionalSupabaseRequestTimeoutMs ); const summary = mapRiderRatingSummaryFromDatabase(Array.isArray(rows) ? rows[0] : rows); state.riderRatingSummary = summary; return summary; } catch (error) { if (typeof adminDirectoryRpcMissing === "function" && adminDirectoryRpcMissing(error)) { logClientWarning("Anonymous rider rating summary RPC is not installed yet.", error); state.riderRatingSummary = null; return null; } logClientWarning("Anonymous rider rating summary could not be loaded.", error); state.riderRatingSummary = null; return null; } } function riderApplicationErrorMessage(error) { if (/rider_applications_rider_id_fkey/i.test(error.message)) { return "Rider account was created, but the Waka profile row is missing, so the admin application could not be submitted. Use the same email/password and submit again after correcting any duplicate phone or driver's license values."; } if (/duplicate key|unique constraint/i.test(error.message)) { return "This phone number, driver's license, or rider application is already used by another Waka account. Use unique rider details or sign in with the existing account."; } return error.message; } function passengerAccountErrorMessage(error) { const message = String(error?.message || error || ""); if (/profiles_email|email|already exists with this email/i.test(message)) { return roleSetupBlockedMessage("passenger", "credentials"); } if (/profiles_phone|phone|already exists with this phone/i.test(message)) { return roleSetupBlockedMessage("passenger", "phone"); } if (/duplicate key|unique constraint/i.test(message)) { return roleSetupBlockedMessage("passenger", "contact"); } return message; } async function sendVerificationCode(type) { const phoneInput = type === "passenger" ? els.passengerPhone : els.riderPhone; const status = type === "passenger" ? els.passengerStatus : els.riderStatus; const phone = phoneInput.value.trim(); if (phone.length < 8) { setTranslatedStatus(status, "validPhoneRequired"); return; } const cooldownSeconds = phoneOtpCooldownSeconds(type, phone); if (cooldownSeconds > 0) { setTranslatedStatus(status, "phoneOtpCooldown", { seconds: cooldownSeconds }); return; } if (smsVerificationRelaxedForTesting()) { markSmsRelaxedPhoneVerified(type, phone, status); return; } if (usesManualPhoneVerification()) { markManualPhoneVerified(type, phone, status); return; } if (isSupabaseMode()) { startPhoneOtpCooldown(type, phone); setTranslatedStatus(status, "sendingVerificationCode"); const { error } = await supabaseClient.auth.signInWithOtp({ phone }); if (error) { if (error.status !== 429 && !/rate limit|too many/i.test(error.message)) clearPhoneOtpCooldown(type, phone); status.textContent = phoneOtpErrorMessage(error); return; } state.verification[type] = { phone, phoneDigits: phoneDigits(phone), verifiedPhone: null, provider: "supabase-otp" }; saveState(); setTranslatedStatus(status, "verificationCodeSent", { phone }); return; } const code = makeVerificationCode(); state.verification[type] = { phone, phoneDigits: phoneDigits(phone), code, verifiedPhone: null }; saveState(); setTranslatedStatus(status, "demoCode", { code, phone }); } async function verifyPhone(type) { const phoneInput = type === "passenger" ? els.passengerPhone : els.riderPhone; const codeInput = type === "passenger" ? els.passengerVerificationCode : els.riderVerificationCode; const status = type === "passenger" ? els.passengerStatus : els.riderStatus; const verification = state.verification[type]; const phone = phoneInput.value.trim(); if (!verification || !phoneMatches(verification.verifiedPhone ?? verification.phone, phone)) { setTranslatedStatus(status, "freshVerificationCodeRequired"); return false; } if (smsVerificationRelaxedForTesting()) { return markSmsRelaxedPhoneVerified(type, phone, status); } if (usesManualPhoneVerification()) { return markManualPhoneVerified(type, phone, status); } if (isSupabaseMode()) { setTranslatedStatus(status, "verifyingPhoneNumber"); const { data, error } = await supabaseClient.auth.verifyOtp({ phone, token: codeInput.value.trim(), type: "sms" }); if (error) { status.textContent = error.message; return false; } state.verification[type] = { ...verification, phone, phoneDigits: phoneDigits(phone), verifiedPhone: phone, verifiedAt: new Date().toISOString(), userId: data.user?.id ?? null, provider: "supabase-otp" }; state.sessions[type] = { phone, userId: data.user?.id ?? null, signedInAt: new Date().toISOString() }; saveState(); setTranslatedStatus(status, "phoneNumberVerified"); return true; } if (codeInput.value.trim() !== verification.code) { setTranslatedStatus(status, "verificationCodeIncorrect"); return false; } state.verification[type] = { ...verification, phone, phoneDigits: phoneDigits(phone), verifiedPhone: phone, verifiedAt: new Date().toISOString() }; state.sessions[type] = { phone, userId: null, signedInAt: new Date().toISOString() }; saveState(); setTranslatedStatus(status, "phoneNumberVerified"); return true; } function hasVerifiedPhone(type, phone) { const account = type === "passenger" ? state.passenger : state.rider; if (account?.phone && account.phoneVerified && phoneMatches(account.phone, phone)) return true; const verification = state.verification[type]; if (!verification?.verifiedAt) return false; return phoneMatches(verification.verifiedPhone ?? verification.phone, phone); } function phoneVerificationCodeInput(type) { return type === "passenger" ? els.passengerVerificationCode : els.riderVerificationCode; } function phoneVerificationStatusKey(type) { return type === "passenger" ? "passengerPhoneBeforeSave" : "riderPhoneBeforeReview"; } function supabaseUserPhoneVerifiedAt(user) { return user?.phone_confirmed_at ?? user?.confirmed_at ?? user?.last_sign_in_at ?? null; } async function markPhoneVerifiedFromSupabaseSession(type, phone, status) { if (!isSupabaseMode()) return false; let user = null; try { user = await getSupabaseUser(); } catch (error) { logClientWarning("Current Supabase phone session could not be checked.", error); return false; } if (!user?.phone || !phoneMatches(user.phone, phone)) return false; const verifiedAt = supabaseUserPhoneVerifiedAt(user) ?? new Date().toISOString(); state.verification[type] = { ...(state.verification[type] ?? {}), phone, phoneDigits: phoneDigits(phone), verifiedPhone: user.phone, verifiedAt, userId: user.id ?? null, provider: "supabase-otp" }; state.sessions[type] = { ...(state.sessions[type] ?? {}), phone, userId: user.id ?? null, signedInAt: new Date().toISOString() }; saveState(); setTranslatedStatus(status, "phoneNumberVerified"); return true; } async function ensureVerifiedPhoneForAccount(type, phone, status) { if (hasVerifiedPhone(type, phone)) return true; if (smsVerificationRelaxedForTesting()) { return markSmsRelaxedPhoneVerified(type, phone, status); } if (usesManualPhoneVerification()) { return markManualPhoneVerified(type, phone, status); } const verification = state.verification[type]; const codeInput = phoneVerificationCodeInput(type); if (codeInput?.value.trim() && verification && phoneMatches(verification.verifiedPhone ?? verification.phone, phone)) { if (await verifyPhone(type)) return true; return false; } if (await markPhoneVerifiedFromSupabaseSession(type, phone, status)) return true; setTranslatedStatus(status, phoneVerificationStatusKey(type)); return false; } function hasSignedIn(type) { if (type === "passenger" || type === "rider") return workspaceRoleSessionMatchesAccount(type); if (type === "admin") return Boolean(state.adminSession); return Boolean(state.sessions[type]); } function passengerSignInHasRideDeepLink() { if (typeof requestedRideRequestIdFromLocation !== "function") return false; const requestId = requestedRideRequestIdFromLocation(); if (!requestId) return false; if (typeof requestedPassengerWorkspacePageFromLocation !== "function") return false; return requestedPassengerWorkspacePageFromLocation() === "trips"; } function routePassengerToRequestAfterSignIn() { const agencyIntent = typeof passengerBusinessIntentFromLocation === "function" && passengerBusinessIntentFromLocation(); const requestedPage = typeof requestedPassengerWorkspacePageFromLocation === "function" ? requestedPassengerWorkspacePageFromLocation() : null; const requestedPageAllowed = requestedPage && typeof availablePassengerWorkspacePages === "function" && availablePassengerWorkspacePages().includes(requestedPage); const targetPage = requestedPageAllowed ? requestedPage : agencyIntent ? "business" : "request"; if (typeof passengerWorkspacePageSelectedInSession !== "undefined") { passengerWorkspacePageSelectedInSession = false; } state.activeTab = "passenger"; state.showRoleEntry = false; state.passengerPage = targetPage; state.selectedRequestId = null; if (typeof updatePassengerWorkspaceRoute === "function") { updatePassengerWorkspaceRoute(targetPage, { replace: true, preferPathRoute: true }); } if (typeof scheduleWorkspaceBackgroundMapRender === "function") scheduleWorkspaceBackgroundMapRender(); if (typeof forcePassengerApproachRefreshNow === "function") forcePassengerApproachRefreshNow("passenger_sign_in"); return true; } function signInMeta(type) { if (type === "passenger") { return { emailInput: els.passengerSignInEmail, passwordInput: els.passengerSignInPassword, phoneInput: els.passengerSignInPhone, codeInput: els.passengerSignInCode, status: els.passengerSignInStatus, verificationKey: "passengerSignIn" }; } return { emailInput: els.riderSignInEmail, passwordInput: els.riderSignInPassword, phoneInput: els.riderSignInPhone, codeInput: els.riderSignInCode, status: els.riderSignInStatus, verificationKey: "riderSignIn" }; } function passwordResetMeta(type) { if (type === "passenger") { return { form: els.passengerSignInForm, emailInput: els.passengerSignInEmail, status: els.passengerSignInStatus, panel: els.passengerPasswordResetPanel, phoneStep: els.passengerPasswordResetPhoneStep, phoneHint: els.passengerPasswordResetPhoneHint, phoneCodeInput: els.passengerPasswordResetPhoneCode, sendPhoneOtpButton: els.sendPassengerPasswordResetPhoneOtp, verifyPhoneOtpButton: els.verifyPassengerPasswordResetPhoneOtp, phoneStatus: els.passengerPasswordResetPhoneStatus, passwordFields: els.passengerPasswordResetPasswordFields, passwordInput: els.passengerResetPassword, confirmInput: els.passengerResetPasswordConfirm, saveButton: els.savePassengerResetPassword }; } return { form: els.riderSignInForm, emailInput: els.riderSignInEmail, status: els.riderSignInStatus, panel: els.riderPasswordResetPanel, phoneStep: els.riderPasswordResetPhoneStep, phoneHint: els.riderPasswordResetPhoneHint, phoneCodeInput: els.riderPasswordResetPhoneCode, sendPhoneOtpButton: els.sendRiderPasswordResetPhoneOtp, verifyPhoneOtpButton: els.verifyRiderPasswordResetPhoneOtp, phoneStatus: els.riderPasswordResetPhoneStatus, passwordFields: els.riderPasswordResetPasswordFields, passwordInput: els.riderResetPassword, confirmInput: els.riderResetPasswordConfirm, saveButton: els.saveRiderResetPassword }; } function normalizedPasswordResetRole(value) { return value === "passenger" || value === "rider" ? value : ""; } function passwordResetRoleFromLocation() { try { const params = new URLSearchParams(window.location.search); const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, "")); const recoveryRole = normalizedPasswordResetRole(params.get("wakaPasswordReset")); const callbackType = String(params.get("type") || hashParams.get("type") || "").toLowerCase(); const pathname = String(window.location.pathname || "").toLowerCase().replace(/\/+$/, ""); const passwordResetPathRole = pathname.endsWith("/rider/password-reset") ? "rider" : pathname.endsWith("/passenger/password-reset") ? "passenger" : ""; if (callbackType && callbackType !== "recovery") return ""; const hasRecoveryMarker = Boolean(recoveryRole) || Boolean(passwordResetPathRole) || (callbackType === "recovery" && Boolean(passwordResetPathRole)); if (hasRecoveryMarker) { return recoveryRole || passwordResetPathRole || normalizedPasswordResetRole(params.get("tab") || params.get("role") || roleRequestedByCurrentShell() || state.activeTab) || "passenger"; } } catch (_error) {} return normalizedPasswordResetRole(passwordResetRoleCapturedBeforeSupabaseInit); } function passwordResetRedirectUrl(type) { const role = normalizedPasswordResetRole(type) || "passenger"; const url = new URL(`/${role}/password-reset`, window.location.origin); url.searchParams.set("tab", role); url.searchParams.set("wakaPasswordReset", role); url.hash = ""; return url.href; } const passwordResetCooldownStorageKey = "waka-password-reset-cooldowns-v1"; const passwordResetCooldownMs = 60 * 1000; const passwordResetRecoverySessionMaxAgeMs = 15 * 60 * 1000; function passwordResetCooldownKey(type, email) { return `${type}:${String(email || "").trim().toLowerCase()}`; } function passwordResetCooldownMap() { try { const stored = JSON.parse(localStorage.getItem(passwordResetCooldownStorageKey) || "{}"); return stored && typeof stored === "object" ? stored : {}; } catch (_error) { return {}; } } function passwordResetCooldownSeconds(type, email) { const sentAt = Number(passwordResetCooldownMap()[passwordResetCooldownKey(type, email)] || 0); const remainingMs = passwordResetCooldownMs - (Date.now() - sentAt); return remainingMs > 0 ? Math.ceil(remainingMs / 1000) : 0; } function rememberPasswordResetRequest(type, email) { const cooldowns = passwordResetCooldownMap(); const now = Date.now(); cooldowns[passwordResetCooldownKey(type, email)] = now; Object.keys(cooldowns).forEach((key) => { if (now - Number(cooldowns[key] || 0) > passwordResetCooldownMs * 4) delete cooldowns[key]; }); try { localStorage.setItem(passwordResetCooldownStorageKey, JSON.stringify(cooldowns)); } catch (error) { logClientWarning("Password reset cooldown could not be saved.", error); } } function clearPasswordResetLocationFlag() { try { const url = new URL(window.location.href); url.searchParams.delete("wakaPasswordReset"); url.searchParams.delete("code"); url.searchParams.delete("token_hash"); url.searchParams.delete("type"); url.hash = ""; passwordResetRoleCapturedBeforeSupabaseInit = ""; window.history.replaceState(null, "", `${url.pathname}${url.search}${url.hash}`); } catch (_error) {} } function clearPasswordResetCredentialsFromLocation() { try { const role = passwordResetRoleFromLocation() || state.passwordReset?.role; const url = new URL(window.location.href); url.searchParams.delete("code"); url.searchParams.delete("token_hash"); url.searchParams.delete("type"); if (role) { url.pathname = `/${role}/password-reset`; url.searchParams.set("tab", role); url.searchParams.set("wakaPasswordReset", role); } url.hash = ""; window.history.replaceState(null, "", `${url.pathname}${url.search}${url.hash}`); } catch (_error) {} } async function ensureSupabaseAuthClientForPasswordReset(status) { if (supabaseClient?.auth) return true; if (!hasSupabaseConfig()) { status.textContent = "Password reset requires WakaGood hosted account recovery."; return false; } status.textContent = "Connecting to WakaGood account recovery..."; await initSupabaseClient(); if (supabaseClient?.auth) return true; status.textContent = "WakaGood account recovery is not available right now. Please try again."; return false; } function ensureSupabaseConfigForPasswordReset(status) { if (hasSupabaseConfig()) return true; status.textContent = "Password reset requires WakaGood hosted account recovery."; return false; } function passwordResetRoleLabel(role) { return role === "rider" ? "rider" : "passenger"; } function passwordResetRequestAcceptedMessage(role) { return `If that email belongs to a WakaGood ${passwordResetRoleLabel(role)} account, a reset link has been sent. Open the email link to create a new password.`; } function passwordResetRoleMismatchMessage(role) { return `This reset link is not valid for a WakaGood ${passwordResetRoleLabel(role)} account. Use the correct WakaGood portal or contact support.`; } function passwordResetModeActive(type) { const role = normalizedPasswordResetRole(type); return Boolean(role && state.passwordReset?.active === true && state.passwordReset?.role === role); } function setPasswordResetMode(type, active, options = {}) { const role = normalizedPasswordResetRole(type); const resetPhoneOtp = options.resetPhoneOtp === true; const existing = !resetPhoneOtp && state.passwordReset?.role === role ? state.passwordReset : null; state.passwordReset = { role: active && role ? role : "", active: Boolean(active && role), startedAt: active && role ? existing?.startedAt || new Date().toISOString() : null, phoneOtpSentAt: active && role ? existing?.phoneOtpSentAt || null : null, phoneOtpMaskedPhone: active && role ? existing?.phoneOtpMaskedPhone || "" : "", phoneOtpVerified: Boolean(active && role && existing?.phoneOtpVerified === true), phoneOtpVerifiedAt: active && role ? existing?.phoneOtpVerifiedAt || null : null, recoverySessionUserId: active && role ? existing?.recoverySessionUserId || null : null, recoverySessionActivatedAt: active && role ? existing?.recoverySessionActivatedAt || null : null }; } function updatePasswordResetFormMode(type, active = passwordResetModeActive(type)) { const role = normalizedPasswordResetRole(type); if (!role) return; const meta = passwordResetMeta(role); const phoneOtpRequired = passwordResetPhoneOtpRequired(); const phoneOtpVerified = Boolean(state.passwordReset?.role === role && state.passwordReset?.phoneOtpVerified === true); const phoneOtpSatisfied = !phoneOtpRequired || phoneOtpVerified; meta.form?.classList.toggle("auth-recovery-mode", Boolean(active)); const heading = meta.form?.querySelector(".auth-form-heading"); const kicker = heading?.querySelector(".card-kicker"); const title = heading?.querySelector("h2"); const copy = heading?.querySelector("p"); if (active) { if (kicker) kicker.textContent = "Password recovery"; if (title) title.textContent = `Create a new ${passwordResetRoleLabel(role)} password`; if (copy) { copy.textContent = phoneOtpRequired ? "First verify the registered phone on this account, then enter the new password twice." : "Enter your new password twice. You will return to sign in after it is saved."; } if (meta.phoneStep) meta.phoneStep.hidden = !phoneOtpRequired; if (meta.passwordFields) meta.passwordFields.hidden = !phoneOtpSatisfied; if (meta.saveButton) meta.saveButton.disabled = !phoneOtpSatisfied; if (meta.phoneHint && phoneOtpRequired) { meta.phoneHint.textContent = state.passwordReset?.phoneOtpMaskedPhone ? `Enter the code sent to the registered phone ${state.passwordReset.phoneOtpMaskedPhone}.` : "Send a code to the verified phone saved on this account."; } if (meta.phoneStatus && !phoneOtpRequired) { meta.phoneStatus.textContent = passwordResetPhoneOtpRelaxedForTesting() ? "Staging recovery phone verification is relaxed for testing." : ""; } else if (meta.phoneStatus && phoneOtpVerified) { meta.phoneStatus.textContent = "Registered phone verified. Create your new password below."; } } else { if (kicker) kicker.textContent = "Welcome back"; if (title) title.textContent = role === "rider" ? "Rider sign in" : "Passenger sign in"; if (copy) copy.textContent = `Sign in to your WakaGood ${passwordResetRoleLabel(role)} account.`; if (meta.phoneStep) meta.phoneStep.hidden = true; if (meta.passwordFields) meta.passwordFields.hidden = false; if (meta.saveButton) meta.saveButton.disabled = false; } } function clearPasswordResetMode(type = "") { const role = normalizedPasswordResetRole(type || state.passwordReset?.role); setPasswordResetMode(role, false); if (role) updatePasswordResetFormMode(role, false); } function markPasswordResetRecoverySession(session) { const userId = session?.user?.id || session?.data?.user?.id || ""; if (!userId || !state.passwordReset?.active) return; state.passwordReset.recoverySessionUserId = userId; state.passwordReset.recoverySessionActivatedAt = new Date().toISOString(); saveState(); } function passwordResetRecoverySessionRecentlyActivated(userId) { if (!userId || !state.passwordReset?.active) return false; if (state.passwordReset.recoverySessionUserId !== userId) return false; const activatedAt = Date.parse(state.passwordReset.recoverySessionActivatedAt || ""); return Number.isFinite(activatedAt) && Date.now() - activatedAt <= passwordResetRecoverySessionMaxAgeMs; } function clearStalePasswordResetModeForCurrentLocation() { const role = normalizedPasswordResetRole(state.passwordReset?.role); if (!role || state.passwordReset?.active !== true) return false; if (passwordResetRoleFromLocation() === role) return false; clearPasswordResetMode(role); state.accountMode[role] = "signin"; state.activeTab = role; state.showRoleEntry = false; saveState(); return true; } async function abortPasswordResetToSignIn(type, message = "", options = {}) { const role = normalizedPasswordResetRole(type); if (!role) return; const meta = passwordResetMeta(role); if (options.clearAuthSession === true) { await clearStaleSupabaseSession(); } if (meta.passwordInput) meta.passwordInput.value = ""; if (meta.confirmInput) meta.confirmInput.value = ""; if (meta.phoneCodeInput) meta.phoneCodeInput.value = ""; if (meta.panel) meta.panel.hidden = true; clearPasswordResetMode(role); clearPasswordResetLocationFlag(); state.sessions[role] = null; if (role === "passenger") state.passenger = null; if (role === "rider") state.rider = null; state.accountMode[role] = "signin"; state.activeTab = role; state.showRoleEntry = false; saveState(); if (typeof hydrateForms === "function") hydrateForms(); if (typeof switchTab === "function") switchTab(role, { updateUrl: true, preserveEntry: false }); if (typeof renderAll === "function") renderAll(); if (message && meta.status) meta.status.textContent = message; } function preparePasswordResetReturnFromLocation() { const role = passwordResetRoleFromLocation(); if (!role) return ""; setPasswordResetMode(role, true, { resetPhoneOtp: true }); state.sessions[role] = null; if (role === "passenger") state.passenger = null; if (role === "rider") state.rider = null; state.activeTab = role; state.showRoleEntry = false; state.accountMode[role] = "signin"; saveState(); updatePasswordResetFormMode(role, true); const meta = passwordResetMeta(role); if (meta.panel) meta.panel.hidden = false; if (meta.status) meta.status.textContent = "Verifying your WakaGood password reset link..."; return role; } function showPasswordResetPanel(type, message = "", options = {}) { const role = normalizedPasswordResetRole(type); if (!role) return; const meta = passwordResetMeta(role); setPasswordResetMode(role, true, options); state.sessions[role] = null; if (role === "passenger") state.passenger = null; if (role === "rider") state.rider = null; state.activeTab = role; state.showRoleEntry = false; state.accountMode[role] = "signin"; saveState(); if (typeof switchTab === "function") switchTab(role, { updateUrl: false, preserveEntry: false }); updatePasswordResetFormMode(role, true); if (meta.panel) meta.panel.hidden = false; if (message && meta.status) meta.status.textContent = message; if (typeof renderAll === "function") renderAll(); updatePasswordResetFormMode(role, true); if (meta.panel) meta.panel.hidden = false; if (passwordResetPhoneOtpRequired() && !passwordResetPhoneOtpSatisfied(role)) { meta.sendPhoneOtpButton?.focus(); } else { meta.passwordInput?.focus(); } } async function sendRoleScopedPasswordResetRequest(role, email, redirectTo = passwordResetRedirectUrl(role)) { const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${passwordResetRequestFunctionName()}`, { method: "POST", headers: { "Content-Type": "application/json", apikey: appConfig.supabaseAnonKey, Authorization: `Bearer ${appConfig.supabaseAnonKey}` }, body: JSON.stringify({ role, email, redirectTo }) }), "Requesting WakaGood password reset", optionalSupabaseRequestTimeoutMs ); const text = await response.text(); let payload = null; if (text) { try { payload = JSON.parse(text); } catch { payload = { error: text }; } } if (!response.ok) { throw new Error(payload?.error || payload?.message || text || `WakaGood account recovery failed with HTTP ${response.status}.`); } return payload; } function passwordResetPhoneOtpSatisfied(role) { if (!passwordResetPhoneOtpRequired()) return true; return Boolean(state.passwordReset?.role === role && state.passwordReset?.phoneOtpVerified === true); } async function passwordResetRecoveryAccessToken() { const session = await withSupabaseTimeout( supabaseClient.auth.getSession(), "Reading WakaGood recovery session", optionalSupabaseRequestTimeoutMs ); return session?.data?.session?.access_token || ""; } async function callPasswordResetPhoneOtpFunction(role, payload) { const accessToken = await passwordResetRecoveryAccessToken(); if (!accessToken) throw new Error("Open the reset link from your email first."); const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${passwordResetPhoneOtpFunctionName()}`, { method: "POST", headers: { "Content-Type": "application/json", apikey: appConfig.supabaseAnonKey, Authorization: `Bearer ${accessToken}` }, body: JSON.stringify({ role, ...payload }) }), "Completing WakaGood phone recovery", optionalSupabaseRequestTimeoutMs ); const text = await response.text(); let body = null; if (text) { try { body = JSON.parse(text); } catch { body = { error: text }; } } if (!response.ok) { throw new Error(body?.error || body?.message || text || `WakaGood phone recovery failed with HTTP ${response.status}.`); } return body || {}; } async function callPasswordResetCompleteFunction(role, password) { const accessToken = await passwordResetRecoveryAccessToken(); if (!accessToken) throw new Error("Open the reset link from your email first."); const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${passwordResetCompleteFunctionName()}`, { method: "POST", headers: { "Content-Type": "application/json", apikey: appConfig.supabaseAnonKey, Authorization: `Bearer ${accessToken}` }, body: JSON.stringify({ role, password, phoneOtpVerifiedAt: state.passwordReset?.phoneOtpVerifiedAt || null }) }), "Updating WakaGood password", supabaseProfileSaveTimeoutMs ); const text = await response.text(); let body = null; if (text) { try { body = JSON.parse(text); } catch { body = { error: text }; } } if (!response.ok) { throw new Error(body?.error || body?.message || text || `WakaGood password update failed with HTTP ${response.status}.`); } return body || {}; } function passwordResetPhoneOtpErrorMessage(error) { const message = String(error?.message || error || ""); if (/open the reset link/i.test(message)) return "Open the reset link from your email first."; if (/too many|rate|wait/i.test(message)) return "Too many phone-code attempts. Wait before trying again."; if (/incorrect|expired|tried too many/i.test(message)) return "The phone code is incorrect, expired, or has been tried too many times."; if (/sms recovery is not configured|Twilio|could not send/i.test(message)) return "WakaGood phone recovery is not available right now. Please contact support."; return message || "WakaGood phone recovery could not be completed."; } async function sendPasswordResetPhoneOtp(type) { const role = normalizedPasswordResetRole(type); if (!role) return; const meta = passwordResetMeta(role); let cooldownKeyPhone = ""; const clientReady = await ensureSupabaseAuthClientForPasswordReset(meta.status); if (!clientReady) return; if (!passwordResetPhoneOtpRequired()) { state.passwordReset.phoneOtpVerified = true; state.passwordReset.phoneOtpVerifiedAt = new Date().toISOString(); updatePasswordResetFormMode(role, true); if (meta.status) meta.status.textContent = "Phone recovery check is relaxed for this staging test. Create your new password below."; meta.passwordInput?.focus(); return; } try { const sessionReady = await ensurePasswordResetSessionFromUrl(meta); if (!sessionReady) { await abortPasswordResetToSignIn(role, "This password reset link could not be verified. Request a new reset email.", { clearAuthSession: true }); return; } const roleAccess = await passwordResetSessionRoleAccess(role); if (!roleAccess.ok) { await rejectPasswordResetForRoleMismatch(role, meta); return; } const cooldownPhone = roleAccess.profile?.phone || role; cooldownKeyPhone = cooldownPhone; const cooldownSeconds = phoneOtpCooldownSeconds(`passwordReset:${role}`, cooldownPhone); if (cooldownSeconds > 0) { meta.phoneStatus.textContent = `For account security, wait ${cooldownSeconds} seconds before requesting another phone code.`; return; } meta.phoneStatus.textContent = "Sending phone verification code..."; startPhoneOtpCooldown(`passwordReset:${role}`, cooldownPhone); const body = await callPasswordResetPhoneOtpFunction(role, { action: "request" }); state.passwordReset.phoneOtpSentAt = new Date().toISOString(); state.passwordReset.phoneOtpMaskedPhone = String(body.maskedPhone || ""); saveState(); updatePasswordResetFormMode(role, true); meta.phoneStatus.textContent = `Code sent to the registered phone ${state.passwordReset.phoneOtpMaskedPhone || "on file"}. It expires in 10 minutes.`; meta.phoneCodeInput?.focus(); } catch (error) { if (cooldownKeyPhone && !/too many|rate|wait/i.test(String(error?.message || ""))) { clearPhoneOtpCooldown(`passwordReset:${role}`, cooldownKeyPhone); } logClientWarning("Password reset phone OTP request was not completed.", error); meta.phoneStatus.textContent = passwordResetPhoneOtpErrorMessage(error); } } async function verifyPasswordResetPhoneOtp(type) { const role = normalizedPasswordResetRole(type); if (!role) return; const meta = passwordResetMeta(role); const code = meta.phoneCodeInput?.value.trim() || ""; if (!/^\d{6}$/.test(code)) { meta.phoneStatus.textContent = "Enter the 6-digit phone code."; meta.phoneCodeInput?.focus(); return; } const clientReady = await ensureSupabaseAuthClientForPasswordReset(meta.status); if (!clientReady) return; try { const sessionReady = await ensurePasswordResetSessionFromUrl(meta); if (!sessionReady) { await abortPasswordResetToSignIn(role, "This password reset link could not be verified. Request a new reset email.", { clearAuthSession: true }); return; } const roleAccess = await passwordResetSessionRoleAccess(role); if (!roleAccess.ok) { await rejectPasswordResetForRoleMismatch(role, meta); return; } meta.phoneStatus.textContent = "Verifying phone code..."; const body = await callPasswordResetPhoneOtpFunction(role, { action: "verify", code }); state.passwordReset.phoneOtpVerified = true; state.passwordReset.phoneOtpVerifiedAt = String(body.verifiedAt || new Date().toISOString()); if (meta.phoneCodeInput) meta.phoneCodeInput.value = ""; saveState(); updatePasswordResetFormMode(role, true); meta.status.textContent = "Registered phone verified. Create your new password below."; meta.passwordInput?.focus(); } catch (error) { logClientWarning("Password reset phone OTP verification was not completed.", error); meta.phoneStatus.textContent = passwordResetPhoneOtpErrorMessage(error); } } async function signedInProfileCanRecoverRole(profile, role) { if (!profile?.id || !["passenger", "rider"].includes(role)) return false; if (strictRoleAuthIsolationEnabled()) return signedInProfileHasPrimaryRole(profile, role); const primaryRole = String(profile.role || "").toLowerCase(); if (primaryRole === "admin" && !(await signedInProfileHasRole(profile, role))) return false; if (await signedInProfileHasRole(profile, role)) return true; if (role === "rider" && (primaryRole === "passenger" || primaryRole === "rider")) return true; return role === "rider" && await signedInProfileHasRiderApplication(profile); } async function passwordResetSessionRoleAccess(role) { if (!normalizedPasswordResetRole(role)) return { ok: false, reason: "invalid_role" }; const user = await getSupabaseUser(); if (!user?.id) return { ok: false, reason: "missing_session" }; const profile = await loadSupabaseProfileForUser(user, "*", `Checking the WakaGood ${role} reset session`); if (!profile?.id) return { ok: false, reason: "missing_profile", user }; const allowed = await signedInProfileCanRecoverRole(profile, role); return { ok: allowed, reason: allowed ? "" : "role_mismatch", user, profile }; } async function ensurePasswordResetSessionFromUrl(meta) { let hasRecoveryCredential = false; try { const params = new URLSearchParams(window.location.search); const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, "")); const code = params.get("code"); hasRecoveryCredential = Boolean(code); if (code && typeof supabaseClient.auth.exchangeCodeForSession === "function") { meta.status.textContent = "Verifying your WakaGood reset link..."; const { data, error } = await withSupabaseTimeout( supabaseClient.auth.exchangeCodeForSession(code), "Verifying WakaGood password reset link", optionalSupabaseRequestTimeoutMs ); if (error) throw error; if (data?.session) { markPasswordResetRecoverySession(data.session); clearPasswordResetCredentialsFromLocation(); return true; } } const accessToken = hashParams.get("access_token"); const refreshToken = hashParams.get("refresh_token"); hasRecoveryCredential = hasRecoveryCredential || Boolean(accessToken && refreshToken); if (accessToken && refreshToken && typeof supabaseClient.auth.setSession === "function") { meta.status.textContent = "Verifying your WakaGood reset link..."; const { data, error } = await withSupabaseTimeout( supabaseClient.auth.setSession({ access_token: accessToken, refresh_token: refreshToken }), "Activating WakaGood password reset link", optionalSupabaseRequestTimeoutMs ); if (error) throw error; if (data?.session) { markPasswordResetRecoverySession(data.session); clearPasswordResetCredentialsFromLocation(); return true; } } const tokenHash = params.get("token_hash") || hashParams.get("token_hash"); hasRecoveryCredential = hasRecoveryCredential || Boolean(tokenHash); if (tokenHash && typeof supabaseClient.auth.verifyOtp === "function") { meta.status.textContent = "Verifying your WakaGood reset link..."; const { data, error } = await withSupabaseTimeout( supabaseClient.auth.verifyOtp({ token_hash: tokenHash, type: "recovery" }), "Activating WakaGood password reset token", optionalSupabaseRequestTimeoutMs ); if (error) throw error; if (data?.session) { markPasswordResetRecoverySession(data.session); clearPasswordResetCredentialsFromLocation(); return true; } } } catch (error) { try { const fallbackSession = await withSupabaseTimeout( supabaseClient.auth.getSession(), "Checking WakaGood password reset session after link activation", optionalSupabaseRequestTimeoutMs ); const fallbackUserId = fallbackSession?.data?.session?.user?.id || ""; if (passwordResetRecoverySessionRecentlyActivated(fallbackUserId)) { clearPasswordResetCredentialsFromLocation(); return true; } } catch (_fallbackError) {} logClientWarning("Password reset link session could not be activated.", error); meta.status.textContent = "This password reset link could not be verified. Request a new reset email."; return false; } if (hasRecoveryCredential) { meta.status.textContent = "This password reset link could not be verified. Request a new reset email."; return false; } const next = await withSupabaseTimeout( supabaseClient.auth.getSession(), "Checking WakaGood password reset session", optionalSupabaseRequestTimeoutMs ); const userId = next?.data?.session?.user?.id || ""; if (passwordResetRecoverySessionRecentlyActivated(userId)) return true; if (meta?.status) { meta.status.textContent = "Open the reset link from your email first. A normal signed-in session cannot update a password here."; } return false; } async function rejectPasswordResetForRoleMismatch(role, meta) { await clearStaleSupabaseSession(); state.sessions[role] = null; if (role === "passenger") state.passenger = null; if (role === "rider") state.rider = null; clearPasswordResetMode(role); if (meta?.panel) meta.panel.hidden = true; clearPasswordResetLocationFlag(); if (meta?.status) meta.status.textContent = passwordResetRoleMismatchMessage(role); saveState(); if (typeof renderAll === "function") renderAll(); if (meta?.status) meta.status.textContent = passwordResetRoleMismatchMessage(role); } async function requestPasswordReset(type) { const role = normalizedPasswordResetRole(type); if (!role) return; const meta = passwordResetMeta(role); const email = meta.emailInput.value.trim().toLowerCase(); if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { meta.status.textContent = "Enter your email above, then tap Forgot password."; meta.emailInput.focus(); return; } const cooldownSeconds = passwordResetCooldownSeconds(role, email); if (cooldownSeconds > 0) { meta.status.textContent = `For account security, wait ${cooldownSeconds} seconds before requesting another reset email.`; return; } if (!ensureSupabaseConfigForPasswordReset(meta.status)) return; meta.status.textContent = "Sending WakaGood password reset email..."; try { await sendRoleScopedPasswordResetRequest(role, email); rememberPasswordResetRequest(role, email); clearPasswordResetMode(role); if (meta.panel) meta.panel.hidden = true; meta.status.textContent = passwordResetRequestAcceptedMessage(role); } catch (error) { logClientWarning("Password reset email request was not completed.", error); meta.status.textContent = "WakaGood account recovery is being secured right now. Please try again shortly or contact support."; } } async function completePasswordReset(type) { const role = normalizedPasswordResetRole(type); if (!role) return; const meta = passwordResetMeta(role); const clientReady = await ensureSupabaseAuthClientForPasswordReset(meta.status); if (!clientReady) return; try { if (!passwordResetModeActive(role)) { showPasswordResetPanel(role, "Open the reset link from your email first, then enter the new password here."); } const sessionReady = await ensurePasswordResetSessionFromUrl(meta); if (!sessionReady) { await abortPasswordResetToSignIn(role, "This password reset link could not be verified. Request a new reset email.", { clearAuthSession: true }); return; } const roleAccess = await passwordResetSessionRoleAccess(role); if (!roleAccess.ok) { await rejectPasswordResetForRoleMismatch(role, meta); return; } if (!passwordResetPhoneOtpSatisfied(role)) { updatePasswordResetFormMode(role, true); meta.phoneStatus.textContent = "Verify the registered phone before saving a new password."; meta.phoneCodeInput?.focus(); return; } const password = meta.passwordInput.value; const confirm = meta.confirmInput.value; if (password.length < 8) { meta.status.textContent = "Use a new password with at least 8 characters."; meta.passwordInput.focus(); return; } if (password !== confirm) { meta.status.textContent = "The two password entries do not match."; meta.confirmInput.focus(); return; } meta.status.textContent = "Updating your WakaGood password..."; await callPasswordResetCompleteFunction(role, password); meta.passwordInput.value = ""; meta.confirmInput.value = ""; if (meta.phoneCodeInput) meta.phoneCodeInput.value = ""; if (meta.panel) meta.panel.hidden = true; clearPasswordResetLocationFlag(); await clearStaleSupabaseSession(); state.sessions[role] = null; if (role === "passenger") state.passenger = null; if (role === "rider") state.rider = null; clearPasswordResetMode(role); state.accountMode[role] = "signin"; state.activeTab = role; state.showRoleEntry = false; saveState(); hydrateForms(); if (typeof switchTab === "function") switchTab(role, { updateUrl: true, preserveEntry: false }); if (roleAccess.user?.email && meta.emailInput) meta.emailInput.value = roleAccess.user.email; meta.status.textContent = "Password updated. Sign in with your new password."; } catch (error) { meta.status.textContent = error.message || "Password update could not be completed."; } } async function handlePasswordResetReturnFromLocation() { const role = passwordResetRoleFromLocation(); if (!role) return false; const meta = passwordResetMeta(role); showPasswordResetPanel(role, "Verifying your WakaGood password reset link...", { resetPhoneOtp: true }); state.sessions[role] = null; if (role === "passenger") state.passenger = null; if (role === "rider") state.rider = null; saveState(); const clientReady = await ensureSupabaseAuthClientForPasswordReset(meta.status); if (!clientReady) { await abortPasswordResetToSignIn(role, "Open the reset link again when WakaGood account recovery is available.", { clearAuthSession: true }); return true; } try { const sessionReady = await ensurePasswordResetSessionFromUrl(meta); if (!sessionReady) { await abortPasswordResetToSignIn(role, "This password reset link could not be verified. Request a new reset email.", { clearAuthSession: true }); return true; } const roleAccess = await passwordResetSessionRoleAccess(role); if (!roleAccess.ok) { if (roleAccess.reason === "missing_session") { await abortPasswordResetToSignIn(role, "This password reset session expired. Request a new reset email.", { clearAuthSession: true }); return true; } await rejectPasswordResetForRoleMismatch(role, meta); return true; } showPasswordResetPanel( role, passwordResetPhoneOtpRequired() ? "Verify your registered phone before creating a new password." : `Choose a new password for your WakaGood ${passwordResetRoleLabel(role)} account.` ); } catch (error) { logClientWarning("Password reset session role check was not completed.", error); await rejectPasswordResetForRoleMismatch(role, meta); } return true; } function passwordChangeRoleLabel(role) { if (role === "agency") return "agency"; return role === "rider" ? "rider" : "passenger"; } function normalizedPasswordChangeRole(value) { return value === "passenger" || value === "rider" || value === "agency" ? value : ""; } function passwordChangeMeta(type) { if (type === "rider") { return { form: els.riderPasswordChangeForm, currentInput: els.riderCurrentPassword, passwordInput: els.riderNewPassword, confirmInput: els.riderNewPasswordConfirm, button: els.riderChangePassword, status: els.riderPasswordChangeStatus, profile: state.rider }; } if (type === "agency") { return { form: els.agencyPasswordChangeForm, currentInput: els.agencyCurrentPassword, passwordInput: els.agencyNewPassword, confirmInput: els.agencyNewPasswordConfirm, button: els.agencyChangePassword, status: els.agencyPasswordChangeStatus, profile: state.passenger }; } return { form: els.passengerPasswordChangeForm, currentInput: els.passengerCurrentPassword, passwordInput: els.passengerNewPassword, confirmInput: els.passengerNewPasswordConfirm, button: els.passengerChangePassword, status: els.passengerPasswordChangeStatus, profile: state.passenger }; } function currentEmailForPasswordChange(role, session, profile) { return String(session?.user?.email || profile?.email || "").trim().toLowerCase(); } async function changeSignedInPassword(type) { const role = normalizedPasswordChangeRole(type); if (!role) return; const meta = passwordChangeMeta(role); const currentPassword = String(meta.currentInput?.value || ""); const nextPassword = String(meta.passwordInput?.value || ""); const confirmPassword = String(meta.confirmInput?.value || ""); if (!currentPassword) { if (meta.status) meta.status.textContent = "Enter your current password first."; meta.currentInput?.focus(); return; } if (nextPassword.length < 8) { if (meta.status) meta.status.textContent = "Use a new password with at least 8 characters."; meta.passwordInput?.focus(); return; } if (nextPassword !== confirmPassword) { if (meta.status) meta.status.textContent = "The new password entries do not match."; meta.confirmInput?.focus(); return; } if (!hasSupabaseConfig()) { if (meta.status) meta.status.textContent = "Password changes require WakaGood hosted account security."; return; } const label = passwordChangeRoleLabel(role); if (meta.button) meta.button.disabled = true; try { if (!supabaseClient?.auth) { if (meta.status) meta.status.textContent = "Connecting to WakaGood account security..."; await initSupabaseClient(); } if (!supabaseClient?.auth?.signInWithPassword || !supabaseClient?.auth?.updateUser) { throw new Error("WakaGood account security is not available right now."); } const sessionResult = await withSupabaseTimeout( supabaseClient.auth.getSession(), `Checking the signed-in ${label} account`, optionalSupabaseRequestTimeoutMs ); const session = sessionResult?.data?.session || null; const email = currentEmailForPasswordChange(role, session, meta.profile); if (!email) throw new Error("This signed-in account does not have an email address for password changes."); if (meta.status) meta.status.textContent = "Checking the current password..."; const { error: signInError } = await withSupabaseTimeout( supabaseClient.auth.signInWithPassword({ email, password: currentPassword }), `Confirming the current ${label} password`, optionalSupabaseRequestTimeoutMs ); if (signInError) { throw new Error(isInvalidLoginCredentialsError(signInError) ? "The current password is not correct." : signInError.message || "The current password could not be confirmed."); } if (meta.status) meta.status.textContent = "Updating password..."; const { error: updateError } = await withSupabaseTimeout( supabaseClient.auth.updateUser({ password: nextPassword }), `Updating the ${label} password`, supabaseProfileSaveTimeoutMs ); if (updateError) throw updateError; if (meta.currentInput) meta.currentInput.value = ""; if (meta.passwordInput) meta.passwordInput.value = ""; if (meta.confirmInput) meta.confirmInput.value = ""; if (meta.status) meta.status.textContent = "Password changed. Use the new password the next time you sign in."; } catch (error) { logClientWarning("Signed-in password change was not completed.", error); if (meta.status) meta.status.textContent = error.message || "Password could not be changed right now."; } finally { if (meta.button) meta.button.disabled = false; } } async function sendSignInCode(type) { const meta = signInMeta(type); if (!phoneOtpSignInEnabled()) { setTranslatedStatus(meta.status, "passwordSignInOnly"); return; } const phone = meta.phoneInput.value.trim(); if (phone.length < 8) { setTranslatedStatus(meta.status, "validPhoneRequired"); return; } const cooldownSeconds = phoneOtpCooldownSeconds(meta.verificationKey, phone); if (cooldownSeconds > 0) { setTranslatedStatus(meta.status, "phoneOtpCooldown", { seconds: cooldownSeconds }); return; } if (isSupabaseMode() && !usesManualPhoneVerification()) { startPhoneOtpCooldown(meta.verificationKey, phone); setTranslatedStatus(meta.status, "sendingSignInCode"); const { error } = await supabaseClient.auth.signInWithOtp({ phone }); if (error) { if (error.status !== 429 && !/rate limit|too many/i.test(error.message)) clearPhoneOtpCooldown(meta.verificationKey, phone); meta.status.textContent = phoneOtpErrorMessage(error); return; } state.verification[meta.verificationKey] = { phone, provider: "supabase-otp" }; saveState(); setTranslatedStatus(meta.status, "signInCodeSent", { phone }); return; } const code = makeVerificationCode(); state.verification[meta.verificationKey] = { phone, code, provider: "demo" }; saveState(); setTranslatedStatus(meta.status, "demoSignInCode", { code, phone }); } function localAccountForSignIn(type, meta) { const phone = meta.phoneInput.value.trim(); const email = meta.emailInput.value.trim().toLowerCase(); const records = type === "passenger" ? [state.passenger, ...state.passengers] : [state.rider, ...state.riders]; return records .filter(Boolean) .find((record) => (phone && record.phone === phone) || (email && record.email === email)) ?? null; } function applyLocalSignIn(type, meta) { const account = localAccountForSignIn(type, meta); if (!account) { setTranslatedStatus(meta.status, "localSignInAccountMissing", { type }); return false; } activateWorkspaceRoleSession(type, { phone: account.phone, email: account.email, userId: account.supabaseUserId ?? account.id ?? null, signedInAt: new Date().toISOString() }); if (type === "passenger") { state.passenger = account; state.passengers = upsertById(state.passengers, account); } else { state.rider = account; state.riders = upsertById(state.riders, account); } state.accountMode[type] = "signin"; state.activeTab = type; state.showRoleEntry = false; if (type === "passenger") routePassengerToRequestAfterSignIn(); if (type === "rider" && typeof restoreWorkspaceUiState === "function") restoreWorkspaceUiState("rider", { replaceRoute: true }); saveState(); populateLocationFields(); hydrateForms(); switchTab(type); setTranslatedStatus(meta.status, "signedInAs", { identity: account.email ?? account.phone }); if (type === "rider") els.riderSessionSummary.textContent = riderWorkspaceStatusMessage(currentRiderRecord()); return true; } async function signInWithEmailPassword(type, meta) { const email = meta.emailInput.value.trim().toLowerCase(); const password = meta.passwordInput.value; if (!email || !password) return false; if (separateRoleAuthTenantsRequested()) { const configured = roleAuthTenantConfigured(type); meta.status.textContent = configured ? "Separate passenger/rider Auth projects are configured, but the Waka database identity bridge must be enabled before browser sign-in can use them." : `The WakaGood ${type} Auth project is not configured yet. Keep strict single-tenant mode until the separate ${type} tenant is provisioned.`; return true; } setTranslatedStatus(meta.status, "signingInPassword"); try { let user; let profile; if (supabaseClient) { const { data, error } = await withSupabaseTimeout( supabaseClient.auth.signInWithPassword({ email, password }), `Signing in as ${type}` ); if (error) throw error; setTranslatedStatus(meta.status, "loadingWakaProfile"); const { data: profileData, error: profileError } = await withSupabaseTimeout( supabaseClient .from("profiles") .select("*") .eq("id", data.user.id) .maybeSingle(), `Loading the ${type} profile`, supabaseProfileSaveTimeoutMs ); if (profileError) throw profileError; user = data.user; profile = profileData; } else if (hasSupabaseConfig()) { const session = await signInWithSupabasePasswordRest(email, password); setTranslatedStatus(meta.status, "loadingWakaProfile"); user = session.user; profile = await selectProfileRest(user.id, "*", session.access_token); } else { setTranslatedStatus(meta.status, "supabaseConfigNeeded"); return true; } if (!profile) { profile = await recoverMissingSupabaseProfileFromLocalAccount(type, user, (message) => { meta.status.textContent = message; }).catch((error) => { logClientWarning("Missing Waka profile recovery was skipped.", error); return null; }); } if (!profile) { setPendingProfileRecovery(type, user, email); state.accountMode[type] = "create"; state.activeTab = type; state.showRoleEntry = false; saveState(); hydrateForms(); switchTab(type); const createEmailInput = type === "rider" ? els.riderEmail : els.passengerEmail; const createStatus = type === "rider" ? els.riderStatus : els.passengerStatus; if (createEmailInput) createEmailInput.value = email; setTranslatedStatus(createStatus ?? meta.status, "supabaseProfileMissing"); return true; } const profileCanUseRole = await signedInProfileCanUseWorkspaceRole(profile, type); const riderCanCheckApplication = type === "rider" && (!strictRoleAuthIsolationEnabled() || profileCanUseRole); const riderHasApplication = riderCanCheckApplication && await signedInProfileHasRiderApplication(profile); const adminIdentity = type !== "admin" && !profileCanUseRole && !riderHasApplication && await signedInProfileIsAdminIdentity(profile); if (adminIdentity) { setTranslatedStatus(meta.status, "adminPublicPortalBlocked"); clearWorkspaceRoleSession(type); await clearStaleSupabaseSession(); return true; } const riderNeedsApplication = type === "rider" && !riderHasApplication && !adminIdentity; const riderCanUseExistingApplication = type === "rider" && riderHasApplication && !adminIdentity; const riderCanStartApplication = riderNeedsApplication && !riderCanUseExistingApplication && (profileCanUseRole || (!strictRoleAuthIsolationEnabled() && (profile.role === "passenger" || profile.role === "rider"))); if (!profileCanUseRole && !riderCanUseExistingApplication && !riderCanStartApplication) { setTranslatedStatus(meta.status, strictRoleAuthIsolationEnabled() ? "wrongProfileRoleStrict" : "wrongProfileRole", { role: profile.role, type }); clearWorkspaceRoleSession(type); await clearStaleSupabaseSession(); return true; } const signedInProfile = profileForWorkspaceRole(profile, { role: type, needsApplication: riderCanStartApplication }); if (profileAccountIsBlocked(profile)) { meta.status.textContent = profileAccountBlockedMessage(profile); clearWorkspaceRoleSession(type); await clearStaleSupabaseSession(); return true; } applySignedInProfile(type, signedInProfile, user); state.accountMode[type] = "signin"; state.activeTab = type; state.showRoleEntry = false; if (type === "passenger") routePassengerToRequestAfterSignIn(); if (riderCanStartApplication) state.riderPage = "profile"; if (type === "rider" && typeof setRiderWorkspaceLandingAfterSignIn === "function") { setRiderWorkspaceLandingAfterSignIn(riderCanStartApplication, { honorRequestedPage: false, replaceRoute: true }); } saveState(); populateLocationFields(); hydrateForms(); switchTab(type); renderAll(); setTranslatedStatus(meta.status, type === "passenger" ? "signedInPassengerLoaded" : "signedInRiderLoaded", { email }); if (type === "rider") els.riderSessionSummary.textContent = riderWorkspaceStatusMessage(currentRiderRecord()); void refreshWorkspaceAfterEmailSignIn(type, email, meta.status, { riderCanStartApplication }).catch((error) => { logClientWarning(`${type} workspace refresh after sign-in was skipped.`, error); }); } catch (error) { meta.status.textContent = error.message; } return true; } async function refreshWorkspaceAfterEmailSignIn(type, email, status, { riderCanStartApplication = false } = {}) { if (type === "rider") { try { await hydrateProfileFromSupabase(type); } catch (error) { logClientWarning("Rider profile hydration after sign-in was skipped.", error); } if (typeof setRiderWorkspaceLandingAfterSignIn === "function") { setRiderWorkspaceLandingAfterSignIn(riderCanStartApplication); } populateLocationFields(); hydrateForms(); if (typeof activeRole !== "function" || activeRole() === type) { switchTab(type, { updateUrl: false }); } } await refreshPaymentAccountsFromSupabase(type).catch((error) => { logClientWarning("Payment account refresh after sign-in was skipped.", error); }); if (type === "passenger") { await loadPassengerRideRequestsFromSupabase(state.passenger?.id).catch((error) => { logClientWarning("Passenger ride requests could not be reloaded after sign-in.", error); }); } await loadMarketplaceFromSupabase({ includeAccountData: true }).catch((error) => { logClientWarning("Marketplace refresh after sign-in was skipped.", error); }); if (type === "passenger") { if (paymentAccountReady("passenger", state.passenger)) clearPendingPaymentSetup(); routePassengerToRequestAfterSignIn(); saveState(); } if (typeof handlePaymentSetupReturnFromLocation === "function") { await handlePaymentSetupReturnFromLocation().catch((error) => { logClientWarning("Payment setup return after sign-in was skipped.", error); }); } if (typeof handleSubscriptionCheckoutReturnFromLocation === "function") { await handleSubscriptionCheckoutReturnFromLocation().catch((error) => { logClientWarning("Subscription checkout return after sign-in was skipped.", error); }); } renderAll(); setTranslatedStatus(status, type === "passenger" ? "signedInPassengerLoaded" : "signedInRiderLoaded", { email }); if (type === "rider") els.riderSessionSummary.textContent = riderWorkspaceStatusMessage(currentRiderRecord()); } async function verifySignIn(type) { const meta = signInMeta(type); const phone = meta.phoneInput.value.trim(); const verification = state.verification[meta.verificationKey]; if (hasSupabaseConfig() && (!usesManualPhoneVerification() || (meta.emailInput.value.trim() && meta.passwordInput.value))) { if (await signInWithEmailPassword(type, meta)) { return; } } if (!phoneOtpSignInEnabled()) { setTranslatedStatus(meta.status, "passwordSignInOnly"); return; } if (usesManualPhoneVerification()) { applyLocalSignIn(type, meta); return; } if (!verification || verification.phone !== phone) { setTranslatedStatus(meta.status, "signInCodeRequired"); return; } if (isSupabaseMode() && !usesManualPhoneVerification()) { setTranslatedStatus(meta.status, "signingIn"); const { data, error } = await supabaseClient.auth.verifyOtp({ phone, token: meta.codeInput.value.trim(), type: "sms" }); if (error) { meta.status.textContent = error.message; return; } activateWorkspaceRoleSession(type, { phone, userId: data.user?.id ?? null, signedInAt: new Date().toISOString() }); saveState(); setTranslatedStatus(meta.status, "signedInAs", { identity: phone }); hydrateProfileFromSupabase(type); return; } if (meta.codeInput.value.trim() !== verification.code) { setTranslatedStatus(meta.status, "signInCodeIncorrect"); return; } applyLocalSignIn(type, meta); } async function hydrateProfileFromSupabase(type) { if (!hasSupabaseRuntime()) return; if (type === "rider" && riderProfileHydrationInFlight) return riderProfileHydrationInFlight; const run = hydrateProfileFromSupabaseInternal(type); if (type !== "rider") return run; riderProfileHydrationInFlight = run.finally(() => { riderProfileHydrationInFlight = null; }); return riderProfileHydrationInFlight; } function scheduleRiderProfileHydration(reason = "workspace") { if (!hasSupabaseRuntime() || !hasSignedIn("rider")) return; if (typeof activeRole === "function" && activeRole() !== "rider") return; const now = Date.now(); if (riderProfileHydrationInFlight || now - riderProfileHydrationRefreshAt < riderProfileHydrationRefreshMs) return; riderProfileHydrationRefreshAt = now; void hydrateProfileFromSupabase("rider").catch((error) => { logClientWarning(`Rider profile refresh skipped after ${reason}.`, error); }); } async function hydrateProfileFromSupabaseInternal(type) { if (!hasSupabaseRuntime()) return; const user = await getSupabaseUser(); if (!user) return; let data = null; try { data = await loadSupabaseProfileForUser(user, "*", `Loading the ${type} profile`); } catch { return; } if (!data) { data = await recoverMissingSupabaseProfileFromLocalAccount(type, user).catch((error) => { logClientWarning("Missing Waka profile recovery during hydration was skipped.", error); return null; }); } if (!data) return; if (type === "passenger") { state.passenger = { id: data.id, supabaseUserId: data.id, name: data.full_name, email: data.email, phone: data.phone, phoneVerified: Boolean(data.phone_verified_at), phoneVerifiedAt: data.phone_verified_at, nationalId: data.national_id_number, dateOfBirth: data.date_of_birth, preferredLanguage: data.preferred_language, country: data.country, city: data.city, profilePhotoPath: data.profile_photo_path, accountStatus: data.account_status ?? "active", accountStatusReason: data.account_status_reason ?? "", accountStatusChangedAt: data.account_status_changed_at ?? null, accountStatusChangedBy: data.account_status_changed_by ?? null, accountClosedAt: data.account_closed_at ?? null, createdAt: data.created_at }; state.passengers = upsertById(state.passengers, state.passenger); } if (type === "rider") { let application = null; let subscription = null; let taxIdentityReference = null; let taxDocumentRows = []; let backgroundCheckRows = []; if (supabaseClient) { const { data: applicationRows, error: applicationError } = await withSupabaseTimeout( supabaseClient .from("rider_applications") .select("*") .eq("rider_id", user.id) .order("created_at", { ascending: false }) .limit(10), "Loading the rider application", supabaseProfileSaveTimeoutMs ); const { data: subscriptionData } = await withSupabaseTimeout( supabaseClient .from("rider_subscriptions") .select("*") .eq("rider_id", user.id) .maybeSingle(), "Loading the rider subscription", optionalSupabaseRequestTimeoutMs ).catch((error) => { logClientWarning("Rider subscription refresh was skipped.", error); return { data: null }; }); const { data: taxIdentityData } = await withSupabaseTimeout( supabaseClient .from("rider_tax_identity_references") .select("*") .eq("rider_id", user.id) .maybeSingle(), "Loading the rider tax profile", optionalSupabaseRequestTimeoutMs ).catch((error) => { logClientWarning("Rider tax profile refresh was skipped.", error); return { data: null }; }); const { data: taxDocumentData } = await withSupabaseTimeout( supabaseClient .from("rider_tax_documents") .select("*") .eq("rider_id", user.id) .order("tax_year", { ascending: false }), "Loading rider tax documents", optionalSupabaseRequestTimeoutMs ).catch((error) => { logClientWarning("Rider tax document refresh was skipped.", error); return { data: [] }; }); const { data: backgroundCheckData } = await withSupabaseTimeout( supabaseClient .from("rider_background_checks") .select("*") .eq("rider_id", user.id) .order("created_at", { ascending: false }) .limit(10), "Loading rider background checks", optionalSupabaseRequestTimeoutMs ).catch((error) => { logClientWarning("Rider background-check refresh was skipped.", error); return { data: [] }; }); if (applicationError) throw applicationError; application = chooseRiderApplicationForWorkspace(applicationRows ?? []); subscription = subscriptionData; taxIdentityReference = taxIdentityData; taxDocumentRows = taxDocumentData ?? []; backgroundCheckRows = backgroundCheckData ?? []; } else { application = await selectRiderApplicationRest(user.id, supabaseRestSession?.access_token); subscription = await selectRiderSubscriptionRest(user.id, supabaseRestSession?.access_token); const rows = await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rider_tax_identity_references?rider_id=eq.${user.id}&select=*&limit=1`, { accessToken: supabaseRestSession?.access_token }), "Loading the rider tax profile", optionalSupabaseRequestTimeoutMs ).catch(() => []); taxIdentityReference = rows?.[0] ?? null; taxDocumentRows = await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rider_tax_documents?rider_id=eq.${user.id}&select=*&order=tax_year.desc`, { accessToken: supabaseRestSession?.access_token }), "Loading rider tax documents", optionalSupabaseRequestTimeoutMs ).catch(() => []); backgroundCheckRows = await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rider_background_checks?rider_id=eq.${user.id}&select=*&order=created_at.desc&limit=10`, { accessToken: supabaseRestSession?.access_token }), "Loading rider background checks", optionalSupabaseRequestTimeoutMs ).catch(() => []); } if (application?.status === "approved" && !subscription?.trial_ends_at && !subscription?.paid_until) { subscription = { ...(subscription ?? {}), trial_ends_at: riderTrialEndsFromApproval(application, subscription) }; } const needsApplication = !application; const documents = parseRiderDocuments(application?.document_path ?? state.rider?.documentName); const navigationOverride = riderNavigationPreferenceOverrideForRider(data.id); if (navigationOverride) documents.navigationPreference = navigationOverride; const applicationBodyType = normalizeCarBodyType(application?.car_body_type ?? state.rider?.carBodyType); state.rider = { ...(state.rider ?? {}), id: data.id, supabaseUserId: data.id, name: data.full_name, email: data.email, phone: data.phone, phoneVerified: Boolean(data.phone_verified_at), phoneVerifiedAt: data.phone_verified_at, nationalId: data.national_id_number, dateOfBirth: data.date_of_birth, preferredLanguage: data.preferred_language, country: data.country, city: data.city, profilePhotoPath: data.profile_photo_path, accountStatus: data.account_status ?? "active", accountStatusReason: data.account_status_reason ?? "", accountStatusChangedAt: data.account_status_changed_at ?? null, accountStatusChangedBy: data.account_status_changed_by ?? null, accountClosedAt: data.account_closed_at ?? null, area: application?.operating_area ?? state.rider?.area ?? "", vehicle: application?.vehicle ?? state.rider?.vehicle ?? "car", credential: application?.credential_number ?? state.rider?.credential ?? "", registration: application?.vehicle_registration ?? state.rider?.registration ?? "", carMake: application?.car_make ?? state.rider?.carMake ?? "", carModel: application?.car_model ?? state.rider?.carModel ?? "", carBodyType: applicationBodyType, vehicleDesignation: normalizeRiderVehicleDesignation(application?.vehicle_designation ?? documents.vehicleDesignation ?? state.rider?.vehicleDesignation, applicationBodyType), navigationPreference: normalizeRiderNavigationPreference(navigationOverride ?? documents.navigationPreference ?? state.rider?.navigationPreference), carYear: application?.car_year ?? state.rider?.carYear ?? "", carColor: application?.car_color ?? state.rider?.carColor ?? "", vehicleVin: application?.vehicle_vin ?? state.rider?.vehicleVin ?? "", insuranceProvider: application?.insurance_provider ?? state.rider?.insuranceProvider ?? "", insuranceNumber: application?.insurance_number ?? state.rider?.insuranceNumber ?? "", driverLicenseExpiresOn: application?.driver_license_expires_on ?? state.rider?.driverLicenseExpiresOn ?? "", insuranceExpiresOn: application?.insurance_expires_on ?? state.rider?.insuranceExpiresOn ?? "", complianceSuspendedAt: application?.compliance_suspended_at ?? state.rider?.complianceSuspendedAt ?? null, complianceSuspensionReason: application?.compliance_suspension_reason ?? state.rider?.complianceSuspensionReason ?? "", backgroundCheckConsentAt: application?.background_check_consent_at ?? state.rider?.backgroundCheckConsentAt ?? null, backgroundCheckProvider: application?.background_check_consent_provider ?? state.rider?.backgroundCheckProvider ?? "", backgroundCheckConsentVersion: application?.background_check_consent_version ?? state.rider?.backgroundCheckConsentVersion ?? "", backgroundCheckStatus: application?.background_check_status ?? state.rider?.backgroundCheckStatus ?? "not requested", backgroundCheckDecision: application?.background_check_decision ?? state.rider?.backgroundCheckDecision ?? "pending", documentName: navigationOverride ? riderDocumentPayload(documents) : application?.document_path ?? state.rider?.documentName ?? "", documents, needsApplication, driverLicenseDocumentPath: documents.driverLicense, vehicleRegistrationDocumentPath: documents.vehicleRegistration, insuranceDocumentPath: documents.insurance, vehicleInspectionDocumentPath: documents.vehicleInspection, status: application?.status ?? (needsApplication ? "profile only" : state.rider?.status ?? "pending"), reviewNote: application?.review_note ?? state.rider?.reviewNote ?? "", approvedAt: application?.reviewed_at ?? state.rider?.approvedAt ?? null, trialEndsAt: subscription?.trial_ends_at ?? state.rider?.trialEndsAt ?? null, subscriptionPaidUntil: subscription?.paid_until ?? state.rider?.subscriptionPaidUntil ?? null, rating: state.rider?.rating ?? "new", createdAt: application?.created_at ?? state.rider?.createdAt ?? data.created_at }; state.riders = upsertById(state.riders, state.rider); if (taxIdentityReference?.id) { state.taxIdentityReferences = upsertById( state.taxIdentityReferences.filter((item) => item.riderId !== user.id), mapTaxIdentityReferenceFromDatabase(taxIdentityReference) ); } state.taxDocuments = [ ...state.taxDocuments.filter((item) => item.riderId !== user.id), ...taxDocumentRows.map((document) => mapTaxDocumentFromDatabase(document)) ]; state.backgroundChecks = [ ...state.backgroundChecks.filter((item) => item.riderId !== user.id), ...backgroundCheckRows.map((check) => mapRiderBackgroundCheckFromDatabase(check)) ]; } let financeAdjustmentRows = []; if (supabaseClient) { const { data: adjustmentData } = await withSupabaseTimeout( supabaseClient .from("finance_adjustments") .select("*") .eq("subject_id", user.id) .order("created_at", { ascending: false }) .limit(25), "Loading finance adjustment notices", optionalSupabaseRequestTimeoutMs ).catch((error) => { logClientWarning("Finance adjustment refresh was skipped.", error); return { data: [] }; }); financeAdjustmentRows = adjustmentData ?? []; } else { financeAdjustmentRows = await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/finance_adjustments?subject_id=eq.${user.id}&select=*&order=created_at.desc&limit=25`, { accessToken: supabaseRestSession?.access_token }), "Loading finance adjustment notices", optionalSupabaseRequestTimeoutMs ).catch(() => []); } state.financeAdjustments = [ ...state.financeAdjustments.filter((item) => item.subjectId !== user.id), ...financeAdjustmentRows.map((adjustment) => mapFinanceAdjustmentFromDatabase(adjustment)) ]; saveState(); populateLocationFields(); hydrateForms(); switchTab(type); renderAll(); } async function restoreSignedInRoleFromSupabaseSession() { if (!hasSupabaseRuntime()) return false; let user = null; try { user = await getSupabaseUser(); } catch (error) { logClientWarning("Stored Supabase session could not be restored.", error); return false; } if (!user) return false; let profile = null; try { profile = await loadSupabaseProfileForUser(user, "*", "Restoring the signed-in Waka profile"); } catch (error) { logClientWarning("Signed-in Waka profile could not be restored.", error); return false; } if (!profile) { const requestedRole = roleRequestedByCurrentShell() || (["passenger", "rider"].includes(state.activeTab) ? state.activeTab : null); if (requestedRole) { profile = await recoverMissingSupabaseProfileFromLocalAccount(requestedRole, user).catch((error) => { logClientWarning("Missing Waka profile recovery during session restore was skipped.", error); return null; }); } } await pruneUnauthorizedWorkspaceSessionsForProfile(profile); const requestedRole = roleRequestedByCurrentShell(); if (requestedRole && !(await signedInProfileCanRestoreWorkspaceRole(profile, requestedRole))) { clearWorkspaceRoleSession(requestedRole); state.activeTab = requestedRole; state.showRoleEntry = false; saveState(); renderAll(); return false; } const roleResolution = await resolveSupabaseSessionWorkspaceRole(profile); if (!roleResolution) return false; const { role } = roleResolution; if (requestedRole && role !== requestedRole) { clearWorkspaceRoleSession(requestedRole); state.activeTab = requestedRole; state.showRoleEntry = false; saveState(); renderAll(); return false; } if (profileAccountIsBlocked(profile)) { await clearStaleSupabaseSession(); return false; } const keepPublicHomeVisible = role === "passenger" ? false : keepPublicHomeVisibleForSessionRestore(); applySignedInProfile(role, profileForWorkspaceRole(profile, roleResolution), user); state.accountMode[role] = "signin"; if (!keepPublicHomeVisible) { state.activeTab = role; state.showRoleEntry = false; } if (!keepPublicHomeVisible && role === "passenger") routePassengerToRequestAfterSignIn(); saveState(); populateLocationFields(); hydrateForms(); if (typeof applyRouteTab === "function") { applyRouteTab(); } else { renderAll(); } try { await hydrateProfileFromSupabase(role); } catch (error) { logClientWarning("Profile hydration after session restore was skipped.", error); } await refreshPaymentAccountsFromSupabase(role).catch((error) => { logClientWarning("Payment account refresh after session restore was skipped.", error); }); if (role === "passenger") { await loadPassengerRideRequestsFromSupabase(state.passenger?.id).catch((error) => { logClientWarning("Passenger ride requests could not be reloaded after session restore.", error); }); } await loadMarketplaceFromSupabase({ includeAccountData: true }).catch((error) => { logClientWarning("Marketplace refresh after session restore was skipped.", error); }); if (!keepPublicHomeVisible && role === "passenger") { if (paymentAccountReady("passenger", state.passenger)) clearPendingPaymentSetup(); routePassengerToRequestAfterSignIn(); saveState(); } if (typeof applyRouteTab === "function") { applyRouteTab(); return true; } renderAll(); return true; } // Marketplace matching, GPS/proximity, routing links, and live market refresh helpers. let marketRefreshInFlight = false; let marketplaceRealtimeChannel = null; let marketplaceRealtimeSignature = ""; let marketplaceRealtimeRefreshTimer = null; let marketplaceRealtimeReconnectTimer = null; let marketplaceRealtimeRefreshPendingReason = ""; let lastMarketRefreshAt = null; let lastMarketplaceSyncSource = "not refreshed"; let passengerApproachRefreshTimerDelayMs = 0; let lastPassengerApproachImmediateRefreshAt = 0; let riderMarketplaceRefreshTimerDelayMs = 0; const accountNotificationAutoRefreshTimers = { passenger: null, rider: null }; const accountNotificationAutoRefreshTimerDelayMs = { passenger: 0, rider: 0 }; let lastPassengerApproachSource = "not refreshed"; let areaProximityRpcUnavailable = false; let gpsMatchingRpcUnavailable = false; let riderMarketplaceRpcUnavailable = false; let passengerApproachRpcUnavailable = false; function passengerPickupGpsAccuracyLimitMeters() { return passengerPickupGpsMaxAccuracyMeters; } function riderLiveGpsAccuracyLimitMeters() { return riderLiveGpsMaxAccuracyMeters; } let activeRideContactRpcUnavailable = false; let rideRequestRpcUnavailable = false; let lastRidePostSource = "not used"; const addressPickupMatchAccuracyMeters = 100; function fareDistanceEstimateKm(country, city, pickupAreaName, destinationAreaName, pickupGps = pendingPickupGps) { const pickupArea = findArea(country, city, pickupAreaName); const destinationArea = findArea(country, city, destinationAreaName); const areaDistance = estimatedAreaDistanceKm(country, city, pickupArea, destinationArea); if (areaDistance == null) return null; const gpsConfidenceBoost = pickupGps ? 1 : 1.15; return Math.max(1, areaDistance * riderPickupEtaRoadFactor * gpsConfidenceBoost); } function fareDistanceEstimateMiles(country, city, pickupAreaName, destinationAreaName, pickupGps = pendingPickupGps) { const distanceKm = fareDistanceEstimateKm(country, city, pickupAreaName, destinationAreaName, pickupGps); return distanceKm == null ? null : Math.max(0.6, distanceKm * kmToMiles); } function roundToPricingStep(value, step) { const cleanValue = Number(value); const cleanStep = Number(step); if (!Number.isFinite(cleanValue) || cleanValue <= 0) return null; if (!Number.isFinite(cleanStep) || cleanStep <= 0) return cleanValue; return Math.max(cleanStep, Math.round(cleanValue / cleanStep) * cleanStep); } function insurancePricingRuleForMarket(country, city) { const key = `${country || ""}|${city || ""}`; return insurancePricingConfig.regionalRules[key] ?? Object.entries(insurancePricingConfig.regionalRules) .find(([regionKey]) => regionKey.toLowerCase().startsWith(`${String(country || "").toLowerCase()}|`))?.[1] ?? null; } function insurancePricingLoadForFare(distanceMiles, country, city) { if (!configFlagEnabled(appConfig.insurancePricingEnabled ?? insurancePricingConfig.enabled)) { return { amountUsd: 0, activeMiles: 0, perActiveMileUsd: 0, regionLabel: "" }; } const rule = insurancePricingRuleForMarket(country, city); const perActiveMileUsd = Number(rule?.perActiveMileUsd ?? insurancePricingConfig.defaultPerActiveMileUsd); const pickupMileAllowance = Number(rule?.pickupMileAllowance ?? insurancePricingConfig.defaultPickupMileAllowance); const minTripInsuranceUsd = Number(rule?.minTripInsuranceUsd ?? insurancePricingConfig.minTripInsuranceUsd); const tripDistanceMultiplier = Number(rule?.tripDistanceMultiplier ?? insurancePricingConfig.tripDistanceMultiplier); const activeMiles = Math.max(0, Number(distanceMiles || 0) * tripDistanceMultiplier + Math.max(0, pickupMileAllowance)); const amountUsd = Math.max(0, Math.max(minTripInsuranceUsd, activeMiles * perActiveMileUsd)); return { amountUsd: Math.round(amountUsd * 100) / 100, activeMiles: Math.round(activeMiles * 10) / 10, perActiveMileUsd, regionLabel: [country, city].filter(Boolean).join(" / "), commercialLiabilityLimitUsd: rule?.commercialLiabilityLimitUsd ?? null, requiresTelematicsSdk: Boolean(rule?.requiresTelematicsSdk) }; } function fareGuidanceFromDistance(distanceMiles, minutes, stops = [], meta = {}) { const stopCount = normalizeRideStops(stops).length; const passengerCount = normalizeRidePassengerCount(meta.passengerCount ?? 1); const luggageCount = normalizeRideLuggageCount(meta.luggageCount ?? 0); const extraPassengerLoad = Math.max(0, passengerCount - 1) * (fareGuidanceConfig.perExtraPassengerUsd ?? 0); const luggageLoad = luggageCount * (fareGuidanceConfig.perLuggageUsd ?? 0); const rawDistanceMiles = Math.max(0.6, Number(distanceMiles) || 0); const cleanDistanceMiles = Math.max( 0.6, roundToPricingStep(rawDistanceMiles, fareGuidanceConfig.distanceStepMiles) ?? rawDistanceMiles ); const rawMinutes = Math.max(1, Math.ceil(Number(minutes) || cleanDistanceMiles * 4)); const cleanMinutes = Math.max( 1, Math.round(roundToPricingStep(rawMinutes, fareGuidanceConfig.minuteStep) ?? rawMinutes) ); const insuranceLoad = insurancePricingLoadForFare(cleanDistanceMiles, meta.country, meta.city); const midpoint = Math.max( fareGuidanceConfig.minFareUsd, (fareGuidanceConfig.baseFareUsd + cleanDistanceMiles * fareGuidanceConfig.perMileUsd + cleanMinutes * fareGuidanceConfig.perMinuteUsd + stopCount * fareGuidanceConfig.perStopUsd + extraPassengerLoad + luggageLoad + insuranceLoad.amountUsd) * fareGuidanceConfig.fuelIndex ); const pricingStep = fareGuidancePricingStep(meta.country); const roundedMidpoint = roundFareGuidanceAmount(midpoint, meta.country); const roundedMinimum = roundFareGuidanceAmount(fareGuidanceConfig.minFareUsd, meta.country); const min = Math.max(roundedMinimum, roundFareGuidanceAmount(midpoint * fareGuidanceConfig.minMultiplier, meta.country)); const max = Math.max(min + pricingStep, roundFareGuidanceAmount(midpoint * fareGuidanceConfig.maxMultiplier, meta.country)); return { distanceKm: cleanDistanceMiles / kmToMiles, distanceMiles: cleanDistanceMiles, minutes: cleanMinutes, stopCount, passengerCount, luggageCount, extraPassengerFare: extraPassengerLoad, luggageFare: luggageLoad, midpoint: roundedMidpoint, min, max, source: meta.source ?? "zone", provider: meta.provider ?? "zone", cached: Boolean(meta.cached), routeKey: meta.routeKey ?? null, routePolyline: meta.routePolyline ?? null, insuranceCostUsd: insuranceLoad.amountUsd, insuranceActiveMiles: insuranceLoad.activeMiles, insurancePerActiveMileUsd: insuranceLoad.perActiveMileUsd, insuranceRegion: insuranceLoad.regionLabel, insuranceCommercialLiabilityLimitUsd: insuranceLoad.commercialLiabilityLimitUsd, insuranceRequiresTelematicsSdk: insuranceLoad.requiresTelematicsSdk, destinationFingerprint: meta.destinationFingerprint ?? null, estimatedAt: meta.estimatedAt ?? new Date().toISOString() }; } function fareGuidancePricingStep(country = defaultLaunchCountry()) { return moneyCurrencyForCountry(country) === "XAF" ? Math.max(1, Number(fareGuidanceConfig.pricingStepXaf ?? 100) || 100) : Math.max(1, Number(fareGuidanceConfig.pricingStepUsd ?? 1) || 1); } function roundFareGuidanceAmount(amount, country = defaultLaunchCountry()) { const step = fareGuidancePricingStep(country); const value = Number(amount); if (!Number.isFinite(value)) return step; return Math.max(step, Math.round(value / step) * step); } const routeGuidanceAbsoluteMaxDistanceMiles = 600; const routeGuidanceAbsoluteMaxMinutes = 18 * 60; function routeGuidanceHasUsableNumbers(guidance) { const distance = Number(guidance?.distanceMiles); const minutes = Number(guidance?.minutes); return Number.isFinite(distance) && distance > 0 && Number.isFinite(minutes) && minutes > 0; } function fareGuidanceForRide(country, city, pickupAreaName, destinationAreaName, pickupGps = pendingPickupGps, stops = [], meta = {}) { const distanceMiles = fareDistanceEstimateMiles(country, city, pickupAreaName, destinationAreaName, pickupGps); if (distanceMiles == null) return null; const stopCount = normalizeRideStops(stops).length; const adjustedDistanceMiles = distanceMiles * (1 + stopCount * fareGuidanceConfig.stopDistanceMultiplier); const adjustedDistanceKm = adjustedDistanceMiles / kmToMiles; const minutes = (pickupEtaMinutes(adjustedDistanceKm, { vehicle: "car" }) ?? Math.ceil(adjustedDistanceMiles * 4)) + stopCount * fareGuidanceConfig.perStopMinutes; return fareGuidanceFromDistance(adjustedDistanceMiles, minutes, stops, { source: "zone", provider: "zone", country, city, ...meta }); } function fareGuidanceMessage(guidance) { if (passengerManualFareOnly()) return manualPassengerFareMessage(); if (!guidance) { return matchingText( "fareGuidanceNeedPickupDestination", "Enter pickup and destination addresses, then enter the FCFA fare you want to offer." ); } const stops = guidance.stopCount ? `, ${guidance.stopCount} stop${guidance.stopCount === 1 ? "" : "s"}` : ""; const passengerDetail = guidance.passengerCount > 1 ? `, ${guidance.passengerCount} passengers` : ""; const luggageDetail = guidance.luggageCount > 0 ? `, ${guidance.luggageCount} luggage piece${guidance.luggageCount === 1 ? "" : "s"}` : ""; const country = selectedPassengerCountry(); const routeSummary = `${formatRouteDistanceForRequest(guidance.distanceMiles, { country })} and about ${guidance.minutes} minutes${stops}${passengerDetail}${luggageDetail}`; if (els.fareOffer) els.fareOffer.placeholder = "Enter fare"; return matchingText( "fareGuidanceTripContext", `Trip context: ${routeSummary}. Enter your FCFA fare offer; riders can accept, decline, or counter when negotiation is enabled.`, { routeSummary } ); } function routeGuidancePendingMessage() { if (passengerManualFareOnly()) return manualPassengerFareMessage(); return routeEstimatesEnabled() ? matchingText( "fareGuidancePendingRoute", "Enter a destination and either use current location or enter a pickup address to estimate the fare before publishing." ) : fareGuidanceMessage(null); } function routeGuidanceUnavailableMessage() { return routeEstimateFallbackAllowedForTesting() ? "Route distance is unavailable right now. For staging, Waka will publish with the fare you enter." : "Route distance is unavailable right now. Please try again in a moment."; } function routeGuidancePickupDescriptionKey(origin, pickupDescription) { return origin?.source === "browser-gps" ? "current-location" : String(pickupDescription ?? "").trim(); } function routeGuidanceGpsKey(point) { const gps = normalizeGpsPoint(point); return gps ? `${gps.latitude.toFixed(3)},${gps.longitude.toFixed(3)}` : ""; } function routeGuidanceInputKey(country, city, pickupAreaName, destinationAreaName, pickupDescription, destination, stops = [], pickupGpsForRoute = pendingPickupGps, destinationPlaceOverride = null, fareFactors = null) { const legacyCall = arguments.length <= 7; const origin = routeOriginForEstimate(country, city, pickupAreaName, pickupDescription, pickupGpsForRoute); const pickupPlace = origin.source === "google-places" ? { placeId: origin.placeId, formattedAddress: origin.formattedAddress, displayName: origin.address } : pickupPlaceForRoute(pickupDescription); const overridePlace = normalizedPlaceSelection(destinationPlaceOverride); const destinationPlace = legacyCall ? destinationPlaceForRoute(destination) : destinationPlaceMatchesInput(overridePlace, destination) ? overridePlace : destinationPlaceForRoute(destination); const pickupGps = origin.source === "browser-gps" && validGpsCoordinate(Number(origin.latitude), Number(origin.longitude)) ? { latitude: Number(origin.latitude), longitude: Number(origin.longitude) } : null; const gpsKey = routeGuidanceGpsKey(pickupGps); return JSON.stringify({ country, city, pickupAreaName, destinationAreaName, pickupDescription: routeGuidancePickupDescriptionKey(origin, pickupDescription), destination: String(destination ?? "").trim(), pickupPlaceId: pickupPlace?.placeId ?? null, destinationPlaceId: destinationPlace?.placeId ?? null, gpsKey, stops: normalizeRideStops(stops), fareFactors: fareFactors ? { passengerCount: normalizeRidePassengerCount(fareFactors.passengerCount), luggageCount: normalizeRideLuggageCount(fareFactors.luggageCount) } : null }); } function routeGuidanceAttemptKeyForInputs(country, city, pickupAreaName, destinationAreaName, pickupDescription, destination, stops = [], pickupGpsForRoute = pendingPickupGps) { const destinationPlace = destinationPlaceForRoute(destination); const origin = routeOriginForEstimate(country, city, pickupAreaName, pickupDescription, pickupGpsForRoute); const pickupGps = routeGuidanceGpsKey(origin); const destinationPoint = validGpsCoordinate(Number(destinationPlace?.latitude), Number(destinationPlace?.longitude)) ? `${Number(destinationPlace.latitude).toFixed(3)},${Number(destinationPlace.longitude).toFixed(3)}` : ""; return JSON.stringify({ country, city, pickupAreaName, destinationAreaName, originSource: origin?.source ?? "", pickupAddress: routeGuidancePickupDescriptionKey(origin, origin?.address || pickupDescription).toLowerCase(), pickupGps, destinationPlaceId: destinationPlace?.placeId ?? null, destinationAddress: String(destinationPlace?.formattedAddress || destination || "").trim().toLowerCase(), destinationPoint, stops: normalizeRideStops(stops) }); } function cachedConfirmedFareGuidanceForKey(key) { const latestGuidance = lastRouteFareGuidance && key && key === lastRouteFareGuidanceKey && routeGuidanceConfirmedForPublish(lastRouteFareGuidance) ? lastRouteFareGuidance : null; if (latestGuidance) return latestGuidance; return stablePassengerFareGuidance && key && key === stablePassengerFareGuidanceKey && routeGuidanceConfirmedForPublish(stablePassengerFareGuidance) ? stablePassengerFareGuidance : null; } async function waitForConfirmedFareGuidance(key, attempts = 16) { for (let attempt = 0; attempt < attempts; attempt += 1) { const cachedGuidance = cachedConfirmedFareGuidanceForKey(key); if (cachedGuidance || fareGuidanceInFlightKey !== key) return cachedGuidance; await pause(250); } return cachedConfirmedFareGuidanceForKey(key); } function fareGuidancePreviewInputs() { const enteredPickupDescription = els.pickupDescription?.value.trim() ?? ""; const selectedPickupGps = selectedCurrentPickupGps ?? pendingPickupGps; const pickupDescription = els.pickupUseCurrentLocation?.checked ? (currentPickupLocationLabel(selectedPickupGps) || enteredPickupDescription) : enteredPickupDescription; const destination = els.destination?.value.trim() ?? ""; const country = selectedPassengerCountry(); const city = typeof selectedRidePickupCity === "function" ? selectedRidePickupCity() : state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(country); return { country, city, pickupAreaName: els.pickupArea?.value ?? "", destinationAreaName: els.destinationArea?.value ?? "", pickupDescription, destination, stops: rideStopsFormValue() }; } function routeEstimateErrorMessage(error) { const message = String(error?.message || "").trim(); if (!message) return routeGuidanceUnavailableMessage(); if (/edge function returned a non-2xx status code/i.test(message)) return routeGuidanceUnavailableMessage(); return message; } function clearFareGuidancePreview() { window.clearTimeout(fareGuidanceTimer); fareGuidanceInFlightKey = ""; lastRouteFareGuidance = null; lastRouteFareGuidanceKey = ""; lastRouteEstimateAttemptKey = ""; stablePassengerFareGuidance = null; stablePassengerFareGuidanceKey = ""; } function rememberStablePassengerFareGuidance(key, guidance) { if (!key || !guidance) return; stablePassengerFareGuidance = guidance; stablePassengerFareGuidanceKey = key; } function immediateCoordinateFareGuidance(country, city, pickupAreaName, destinationAreaName, pickupDescription, destination, stops = [], destinationPlaceOverride = null, pickupGps = pendingPickupGps, fareFactors = {}) { const origin = routeOriginForEstimate(country, city, pickupAreaName, pickupDescription, pickupGps); if (routeOriginNeedsConfirmedAddressForPricing(origin)) return null; const overridePlace = normalizedPlaceSelection(destinationPlaceOverride); const destinationPlace = destinationPlaceMatchesInput(overridePlace, destination) ? overridePlace : destinationPlaceForRoute(destination); const originPoint = validGpsCoordinate(Number(origin?.latitude), Number(origin?.longitude)) ? { latitude: Number(origin.latitude), longitude: Number(origin.longitude) } : null; const destinationPoint = validGpsCoordinate(Number(destinationPlace?.latitude), Number(destinationPlace?.longitude)) ? { latitude: Number(destinationPlace.latitude), longitude: Number(destinationPlace.longitude) } : null; if (!originPoint || !destinationPoint) return null; const stopPoints = normalizeRideStops(stops).map(stopRoutePoint); const points = [originPoint, ...stopPoints.filter(Boolean), destinationPoint]; const straightKm = points.slice(1).reduce((total, point, index) => ( total + gpsDistanceKmBetween(points[index], point) ), 0); if (!Number.isFinite(straightKm) || straightKm <= 0) return null; const missingStopPointCount = Math.max(0, normalizeRideStops(stops).length - stopPoints.filter(Boolean).length); const distanceMiles = Math.max(0.6, straightKm * riderPickupEtaRoadFactor * kmToMiles); const minutes = Math.max(3, Math.ceil(distanceMiles * 2.1 + missingStopPointCount * fareGuidanceConfig.perStopMinutes)); const guidance = fareGuidanceFromDistance(distanceMiles, minutes, stops, { source: "place-preview", provider: "local-place-preview", country, city, ...fareFactors }); const areaGuidance = fareGuidanceForRide(country, city, pickupAreaName, destinationAreaName, pickupGps, stops, fareFactors); if (routeGuidanceLooksImplausible(guidance, areaGuidance)) { lastRouteEstimateError = new Error("Route estimate looked unrealistic for this service area. Re-select the pickup and destination addresses, then try again."); logClientWarning("Ignoring implausible local route preview.", { preview: guidance, area: areaGuidance }); return null; } return guidance; } function routeGuidanceLooksImplausible(guidance, localGuidance, options = {}) { if (!guidance) return false; if (!routeGuidanceHasUsableNumbers(guidance)) return true; const distance = Number(guidance.distanceMiles); const minutes = Number(guidance.minutes); const absoluteMaxDistance = Number(options.maxDistanceMiles ?? routeGuidanceAbsoluteMaxDistanceMiles); const absoluteMaxMinutes = Number(options.maxMinutes ?? routeGuidanceAbsoluteMaxMinutes); if (Number.isFinite(absoluteMaxDistance) && absoluteMaxDistance > 0 && distance > absoluteMaxDistance) return true; if (Number.isFinite(absoluteMaxMinutes) && absoluteMaxMinutes > 0 && minutes > absoluteMaxMinutes) return true; if (!localGuidance || !routeGuidanceHasUsableNumbers(localGuidance)) return false; const localDistance = Number(localGuidance.distanceMiles); const localMinutes = Number(localGuidance.minutes); const distanceLimit = Number(options.distanceRatio ?? 2.6); const minuteLimit = Number(options.minuteRatio ?? 3.2); const absoluteDistanceLimit = Number(options.absoluteDistanceMiles ?? 12); const absoluteMinuteLimit = Number(options.absoluteMinutes ?? 60); return (distance > localDistance * distanceLimit && distance - localDistance > absoluteDistanceLimit) || (minutes > localMinutes * minuteLimit && minutes - localMinutes > absoluteMinuteLimit); } function safeRouteGuidance(guidance, localGuidance, context = "route") { if (!routeGuidanceLooksImplausible(guidance, localGuidance)) return guidance; lastRouteEstimateError = new Error(`The route service returned an unusually large ${context} estimate, so Waka kept the local address-based estimate.`); logClientWarning("Ignoring implausible route estimate.", { context, route: guidance, local: localGuidance }); return localGuidance ?? null; } function scheduleFareGuidancePreview() { if (!els.fareGuidance) return null; if (passengerManualFareOnly()) { clearFareGuidancePreview(); els.fareGuidance.textContent = manualPassengerFareMessage(); if (els.fareOffer) els.fareOffer.placeholder = "Enter fare"; if (typeof updatePassengerFareModeControls === "function") updatePassengerFareModeControls(null); return null; } const inputs = fareGuidancePreviewInputs(); const pickupGpsForPreview = passengerPickupGpsForFormChoice(); const origin = routeOriginForEstimate( inputs.country, inputs.city, inputs.pickupAreaName, inputs.pickupDescription, pickupGpsForPreview ); const destinationPlace = destinationPlaceForRoute(inputs.destination); if (routePricingRequiresConfirmedPlaces(inputs.country)) { const missingPickup = routeOriginNeedsConfirmedAddressForPricing(origin); const missingDestination = !destinationPlace; if (!inputs.destination || missingPickup || missingDestination) { clearFareGuidancePreview(); const message = !inputs.destination ? routeGuidancePendingMessage() : missingPickup && missingDestination ? confirmedCameroonLocationsRequiredMessage() : missingPickup ? currentLocationNeedsAddressMessage() : confirmedCameroonDestinationRequiredMessage(); els.fareGuidance.textContent = message; if (els.fareOffer) els.fareOffer.placeholder = ""; if (typeof updatePassengerFareModeControls === "function") updatePassengerFareModeControls(null); return null; } } if (!inputs.destination || !routeOriginIsSpecific(origin)) { clearFareGuidancePreview(); els.fareGuidance.textContent = routeGuidancePendingMessage(); if (typeof updatePassengerFareModeControls === "function") updatePassengerFareModeControls(null); return null; } const fareFactors = currentRideTripDetails(); const key = routeGuidanceInputKey( inputs.country, inputs.city, inputs.pickupAreaName, inputs.destinationAreaName, inputs.pickupDescription, inputs.destination, inputs.stops, pickupGpsForPreview, destinationPlace, fareFactors ); const cachedGuidance = cachedConfirmedFareGuidanceForKey(key); if (cachedGuidance) { els.fareGuidance.textContent = fareGuidanceMessage(cachedGuidance); if (typeof updatePassengerFareModeControls === "function") updatePassengerFareModeControls(cachedGuidance); return cachedGuidance; } const immediateGuidance = immediateCoordinateFareGuidance( inputs.country, inputs.city, inputs.pickupAreaName, inputs.destinationAreaName, inputs.pickupDescription, inputs.destination, inputs.stops, null, pickupGpsForPreview, fareFactors ); els.fareGuidance.textContent = "Checking accurate driving distance before publishing..."; if (key === fareGuidanceInFlightKey) return null; const attemptKey = routeGuidanceAttemptKeyForInputs( inputs.country, inputs.city, inputs.pickupAreaName, inputs.destinationAreaName, inputs.pickupDescription, inputs.destination, inputs.stops, pickupGpsForPreview ); lastRouteEstimateAttemptKey = attemptKey; const requestId = ++fareGuidanceRequestId; window.clearTimeout(fareGuidanceTimer); fareGuidanceInFlightKey = key; fareGuidanceTimer = window.setTimeout(async () => { try { const guidance = await accurateFareGuidanceForRide( inputs.country, inputs.city, inputs.pickupAreaName, inputs.destinationAreaName, inputs.destination, pickupGpsForPreview, inputs.stops, inputs.pickupDescription, destinationPlace, fareFactors ); if (requestId !== fareGuidanceRequestId) return; fareGuidanceInFlightKey = ""; const finalGuidance = safeRouteGuidance(guidance, immediateGuidance, "fare") ?? immediateGuidance; lastRouteFareGuidance = finalGuidance; lastRouteFareGuidanceKey = key; rememberStablePassengerFareGuidance(key, finalGuidance); if (els.fareGuidance) { els.fareGuidance.textContent = finalGuidance ? fareGuidanceMessage(finalGuidance) : routeEstimateErrorMessage(lastRouteEstimateError); } if (typeof updatePassengerFareModeControls === "function") updatePassengerFareModeControls(finalGuidance); } catch (error) { if (requestId !== fareGuidanceRequestId) return; fareGuidanceInFlightKey = ""; lastRouteFareGuidance = immediateGuidance; lastRouteFareGuidanceKey = immediateGuidance ? key : ""; rememberStablePassengerFareGuidance(lastRouteFareGuidanceKey, immediateGuidance); if (els.fareGuidance) { els.fareGuidance.textContent = immediateGuidance ? `${fareGuidanceMessage(immediateGuidance)} ${matchingText("routeServiceFallbackEstimate", "The route service is unavailable right now, so this quick estimate is shown.")}` : routeEstimateErrorMessage(error); } if (typeof updatePassengerFareModeControls === "function") updatePassengerFareModeControls(immediateGuidance); } }, 250); return null; } function updateFareGuidance() { if (!els.fareGuidance) return null; if (passengerManualFareOnly()) { clearFareGuidancePreview(); els.fareGuidance.textContent = manualPassengerFareMessage(); if (els.fareOffer) els.fareOffer.placeholder = "Enter fare"; if (typeof updatePassengerFareModeControls === "function") updatePassengerFareModeControls(null); return null; } if (routeEstimatesEnabled()) { return scheduleFareGuidancePreview(); } if (!els.destination?.value.trim()) { els.fareGuidance.textContent = fareGuidanceMessage(null); if (typeof updatePassengerFareModeControls === "function") updatePassengerFareModeControls(null); return null; } const country = selectedPassengerCountry(); const city = typeof selectedRidePickupCity === "function" ? selectedRidePickupCity() : state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(country); const guidance = fareGuidanceForRide(country, city, els.pickupArea?.value, els.destinationArea?.value, pendingPickupGps, rideStopsFormValue(), currentRideTripDetails()); els.fareGuidance.textContent = fareGuidanceMessage(guidance); if (els.fareOffer && guidance) { els.fareOffer.placeholder = normalizeCarTypePreference(els.vehiclePreference?.value) === "suv" ? String(Number(guidance.max) + 1) : String(guidance.min); } if (typeof updatePassengerFareModeControls === "function") updatePassengerFareModeControls(guidance); return guidance; } function routeEstimatesEnabled() { return ["google-routes", "cameroon-local", "osrm", "valhalla", "pgrouting"].includes(String(appConfig.routeEstimatesProvider || "zone").toLowerCase()); } function passengerManualFareOnly() { return String(appConfig.routeEstimatesProvider || "").trim().toLowerCase() === "manual-fare"; } function matchingText(key, fallback = "", values = {}) { const translated = typeof translatedMessage === "function" ? translatedMessage(key, values) : typeof translatedValue === "function" ? translatedValue(key) : ""; return translated || fallback; } function manualPassengerFareMessage() { return matchingText( "rideFareGuidanceManual", "Enter the fare you want to offer in FCFA. Suggestions are optional; typed pickup and destination text can still be published. Waka sends vehicle, passenger count, luggage, and stops to riders for agreement." ); } function accurateRouteEstimateRequired() { return routeEstimatesEnabled() && (configFlagEnabled(appConfig.requireRouteEstimateBeforePublish) || strictProductionModeEnabled()); } function routeEstimateFallbackAllowedForTesting() { return configFlagEnabled(appConfig.relaxRouteEstimateForTesting) && /\b(staging|pilot|test|preview)\b/i.test(String(appConfig.projectName || "")); } function routePricingRequiresConfirmedPlaces(country = selectedPassengerCountry()) { if (passengerManualFareOnly()) return false; return String(country || "").trim().toLowerCase() === "cameroon" && String(appConfig.placesAutocompleteProvider || "").trim().toLowerCase() === "cameroon-local"; } function routeOriginNeedsConfirmedAddressForPricing(origin) { if (!routePricingRequiresConfirmedPlaces(origin?.country)) return false; const source = String(origin?.source || "").trim().toLowerCase(); if (source === "browser-gps") return false; return !origin?.placeId && !validGpsCoordinate(Number(origin?.latitude), Number(origin?.longitude)); } function routeDestinationNeedsConfirmedAddressForPricing(destination) { if (!routePricingRequiresConfirmedPlaces(destination?.country)) return false; return !destination?.placeId && !validGpsCoordinate(Number(destination?.latitude), Number(destination?.longitude)); } function currentLocationNeedsAddressMessage() { return matchingText( "currentLocationNeedsAddressForEstimate", "Choose the pickup from Cameroon suggestions, or check Current and capture GPS, before Waka estimates fare." ); } function confirmedCameroonDestinationRequiredMessage() { return matchingText( "confirmedDestinationRequiredForEstimate", "Choose the destination from Cameroon suggestions before Waka estimates fare." ); } function confirmedCameroonLocationsRequiredMessage() { return matchingText( "confirmedLocationsRequiredForEstimate", "Choose pickup and destination from Cameroon suggestions before Waka estimates fare. For pickup, Current GPS is also accepted." ); } function normalizedRouteEstimateSourceForDatabase(source) { const normalized = String(source || "").trim().toLowerCase(); if (normalized === "manual" || normalized === "manual-fare" || (!normalized && passengerManualFareOnly())) return "manual"; if (normalized === "google-routes") return "google-routes"; if (["cameroon-local", "osrm", "valhalla", "pgrouting"].includes(normalized)) return "cameroon-local"; return "zone"; } function normalizedRouteEstimateProviderForDatabase(source, provider) { const normalizedSource = normalizedRouteEstimateSourceForDatabase(source); if (normalizedSource === "manual") return "manual-fare"; if (normalizedSource === "google-routes") return String(provider || "google-routes").trim() || "google-routes"; if (normalizedSource === "cameroon-local") return String(provider || source || "cameroon-local").trim() || "cameroon-local"; return "zone"; } function routeGuidanceConfirmedForPublish(guidance) { if (!routeEstimatesEnabled() || !accurateRouteEstimateRequired()) return true; if (routeEstimateFallbackAllowedForTesting()) return !guidance || !routeGuidanceLooksImplausible(guidance, null); return ["google-routes", "cameroon-local"].includes(normalizedRouteEstimateSourceForDatabase(guidance?.source)); } function routeEstimateFunctionName() { return String(appConfig.routeEstimateFunctionName || "route-estimate").trim() || "route-estimate"; } function typedRouteAddress(text, city, country) { return compactLocationQuery([text, city, country]); } function destinationRouteAddress(destination, destinationAreaName, city, country) { const destinationText = String(destination ?? "").trim(); return destinationText.length >= 6 ? typedRouteAddress(destinationText, city, country) : compactLocationQuery([destinationText, destinationAreaName, city, country]); } function placesAutocompleteEnabled() { return ["google-places", "cameroon-local", "local-gazetteer"].includes(String(appConfig.placesAutocompleteProvider || "none").toLowerCase()); } function placesAutocompleteFunctionName() { return String(appConfig.placesAutocompleteFunctionName || "place-autocomplete").trim() || "place-autocomplete"; } function autoPickupGpsEnabled() { return configFlagEnabled(appConfig.autoPickupGpsEnabled); } function autoRiderGpsEnabled() { return configFlagEnabled(appConfig.autoRiderGpsEnabled); } function passengerPickupGpsForFormChoice(fallbackGps = pendingPickupGps) { if (!els.pickupUseCurrentLocation?.checked) return null; return normalizeGpsPoint(selectedCurrentPickupGps) ?? normalizeGpsPoint(fallbackGps); } function normalizedPlaceSelection(place) { if (!place || typeof place !== "object") return null; const placeId = String(place.placeId ?? "").trim(); const displayName = String(place.displayName ?? "").trim(); const formattedAddress = String(place.formattedAddress ?? "").trim(); const latitude = Number(place.latitude); const longitude = Number(place.longitude); const source = String(place.source ?? "").trim(); return { placeId: placeId || null, displayName: displayName || formattedAddress || null, formattedAddress: formattedAddress || null, latitude: Number.isFinite(latitude) ? latitude : null, longitude: Number.isFinite(longitude) ? longitude : null, accuracyMeters: place.accuracyMeters ?? null, capturedAt: place.capturedAt ?? null, source: source || null, selectedAt: place.selectedAt ?? new Date().toISOString() }; } function placeMatchesInput(place, inputValue) { if (!place) return false; const input = String(inputValue ?? "").trim().toLowerCase(); if (!input) return false; return [place.displayName, place.formattedAddress].some((value) => String(value ?? "").trim().toLowerCase() === input); } function destinationPlaceMatchesInput(place, destination) { return placeMatchesInput(place, destination); } function pickupPlaceMatchesInput(place, pickup) { return placeMatchesInput(place, pickup); } function pickupPlaceForRoute(pickup = els.pickupDescription?.value) { const place = normalizedPlaceSelection(selectedPickupPlace); return pickupPlaceMatchesInput(place, pickup) ? place : null; } function currentPickupLocationLabel(point = pendingPickupGps) { const gps = normalizeGpsPoint(point); return gps ? "Current location" : ""; } function pickupUsesCurrentLocationText(value) { return /^current location\b/i.test(String(value ?? "").trim()); } function pickupUsesGpsFallbackText(value) { return /^verified gps pickup\b/i.test(String(value ?? "").trim()); } function destinationPlaceForRoute(destination = els.destination?.value) { const place = normalizedPlaceSelection(selectedDestinationPlace); return destinationPlaceMatchesInput(place, destination) ? place : null; } function stopPlaceKey(stop) { return String(stop ?? "").replace(/\s+/g, " ").trim().toLowerCase(); } function rememberSelectedStopPlace(place) { const normalized = normalizedPlaceSelection(place); if (!normalized) return; [normalized.formattedAddress, normalized.displayName].forEach((label) => { const key = stopPlaceKey(label); if (key) selectedStopPlaces.set(key, normalized); }); while (selectedStopPlaces.size > placeDetailsCacheLimit) { selectedStopPlaces.delete(selectedStopPlaces.keys().next().value); } } function stopPlaceForRoute(stop) { return normalizedPlaceSelection(selectedStopPlaces.get(stopPlaceKey(stop))); } function stopRoutePoint(stop) { const place = stopPlaceForRoute(stop); if (validGpsCoordinate(Number(place?.latitude), Number(place?.longitude))) { return { latitude: Number(place.latitude), longitude: Number(place.longitude) }; } return null; } function stopRouteAddress(stop, city, country) { const place = stopPlaceForRoute(stop); return { address: place?.formattedAddress || place?.displayName || typedRouteAddress(stop, city, country), placeId: place?.placeId ?? null, formattedAddress: place?.formattedAddress ?? null, latitude: place?.latitude ?? null, longitude: place?.longitude ?? null }; } function routeOriginForEstimate(country, city, pickupAreaName, pickupDescription, pickupGps = pendingPickupGps) { const pickupText = String(pickupDescription ?? "").trim(); const currentPickupSelected = typeof passengerWantsCurrentPickup === "function" ? passengerWantsCurrentPickup() : Boolean(els.pickupUseCurrentLocation?.checked); const rawSelectedPlace = pickupPlaceForRoute(pickupText); const selectedPlace = rawSelectedPlace?.source === "browser-gps" && !currentPickupSelected ? null : rawSelectedPlace; const selectedPlaceIsBrowserGps = selectedPlace?.source === "browser-gps"; const selectedPlaceGps = selectedPlaceIsBrowserGps && validGpsCoordinate(Number(selectedPlace?.latitude), Number(selectedPlace?.longitude)) ? normalizeGpsPoint({ latitude: selectedPlace.latitude, longitude: selectedPlace.longitude, accuracyMeters: selectedPlace.accuracyMeters ?? null, capturedAt: selectedPlace.capturedAt ?? null }) : null; const selectedGps = currentPickupSelected ? normalizeGpsPoint(selectedCurrentPickupGps) ?? selectedPlaceGps : selectedPlaceGps; const selectedPlaceHasRoutePoint = Boolean(selectedPlace?.placeId) || validGpsCoordinate(Number(selectedPlace?.latitude), Number(selectedPlace?.longitude)); if (selectedPlaceIsBrowserGps && selectedGps) { return { latitude: selectedGps.latitude, longitude: selectedGps.longitude, accuracyMeters: selectedGps.accuracyMeters ?? null, capturedAt: selectedGps.capturedAt ?? null, address: pickupText || selectedPlace.formattedAddress || selectedPlace.displayName || compactLocationQuery([pickupAreaName, city, country]), placeId: null, formattedAddress: selectedPlace.formattedAddress ?? pickupText ?? null, area: pickupAreaName, city, country, source: "browser-gps" }; } const useSelectedGps = selectedGps && (!pickupText || pickupUsesCurrentLocationText(pickupText) || pickupUsesGpsFallbackText(pickupText) || (selectedPlace && !selectedPlaceHasRoutePoint)); const gps = normalizeGpsPoint(useSelectedGps ? selectedGps : pickupGps); if (gps && selectedPlace && !selectedPlaceHasRoutePoint && selectedGps) { return { latitude: gps.latitude, longitude: gps.longitude, accuracyMeters: gps.accuracyMeters ?? null, capturedAt: gps.capturedAt ?? null, address: pickupText || selectedPlace.formattedAddress || selectedPlace.displayName || compactLocationQuery([pickupAreaName, city, country]), placeId: null, formattedAddress: selectedPlace.formattedAddress ?? pickupText ?? null, area: pickupAreaName, city, country, source: "browser-gps" }; } if (selectedPlace) { return { address: selectedPlace.formattedAddress || selectedPlace.displayName || compactLocationQuery([pickupText, pickupAreaName, city, country]), placeId: selectedPlace.placeId ?? null, formattedAddress: selectedPlace.formattedAddress ?? null, latitude: selectedPlace.latitude ?? null, longitude: selectedPlace.longitude ?? null, accuracyMeters: selectedPlace.accuracyMeters ?? null, capturedAt: selectedPlace.capturedAt ?? null, area: pickupAreaName, city, country, source: "google-places" }; } if (gps && (pickupUsesCurrentLocationText(pickupText) || pickupUsesGpsFallbackText(pickupText))) { return { latitude: gps.latitude, longitude: gps.longitude, accuracyMeters: gps.accuracyMeters ?? null, capturedAt: gps.capturedAt ?? null, address: pickupText || compactLocationQuery([pickupAreaName, city, country]), placeId: null, formattedAddress: null, area: pickupAreaName, city, country, source: "browser-gps" }; } const typedAddress = pickupText.length >= 6 ? typedRouteAddress(pickupText, city, country) : compactLocationQuery([pickupText, pickupAreaName, city, country]); if (pickupText.length >= 6) { return { address: typedAddress, placeId: null, formattedAddress: null, latitude: null, longitude: null, area: pickupAreaName, city, country, source: "typed-address" }; } if (gps) { return { latitude: gps.latitude, longitude: gps.longitude, accuracyMeters: gps.accuracyMeters ?? null, capturedAt: gps.capturedAt ?? null, address: typedAddress, placeId: null, formattedAddress: null, area: pickupAreaName, city, country, source: "browser-gps" }; } return { address: typedAddress, placeId: null, formattedAddress: null, latitude: null, longitude: null, area: pickupAreaName, city, country, source: "typed-address" }; } function routeOriginIsSpecific(origin) { return Boolean(origin?.placeId) || validGpsCoordinate(Number(origin?.latitude), Number(origin?.longitude)) || String(origin?.address ?? "").trim().length >= 6; } function requestPickupGpsFromRouteOrigin(origin, fallbackGps = pendingPickupGps) { if (validGpsCoordinate(Number(origin?.latitude), Number(origin?.longitude))) { const fallback = normalizeGpsPoint(fallbackGps); const originSource = String(origin?.source ?? "").trim().toLowerCase(); const addressAccuracyMeters = originSource === "google-places" ? addressPickupMatchAccuracyMeters : null; return normalizeGpsPoint({ latitude: Number(origin.latitude), longitude: Number(origin.longitude), accuracyMeters: origin.accuracyMeters ?? fallback?.accuracyMeters ?? addressAccuracyMeters, capturedAt: origin.capturedAt ?? fallback?.capturedAt ?? new Date().toISOString() }); } if (origin?.source !== "browser-gps") return null; return normalizeGpsPoint(selectedCurrentPickupGps) ?? normalizeGpsPoint(fallbackGps); } function routeEstimateRequestBody(country, city, pickupGps, destinationAreaName, destination, stops = [], pickupDescription = els.pickupDescription?.value, pickupAreaName = els.pickupArea?.value, destinationPlaceOverride = null) { const origin = routeOriginForEstimate(country, city, pickupAreaName, pickupDescription, pickupGps); const overridePlace = normalizedPlaceSelection(destinationPlaceOverride); const selectedPlace = destinationPlaceMatchesInput(overridePlace, destination) ? overridePlace : destinationPlaceForRoute(destination); return { origin, destination: { address: selectedPlace?.formattedAddress || selectedPlace?.displayName || destinationRouteAddress(destination, destinationAreaName, city, country), placeId: selectedPlace?.placeId ?? null, formattedAddress: selectedPlace?.formattedAddress ?? null, latitude: selectedPlace?.latitude ?? null, longitude: selectedPlace?.longitude ?? null, area: destinationAreaName, city, country }, stops: normalizeRideStops(stops).map((stop) => stopRouteAddress(stop, city, country).address), travelMode: "DRIVE" }; } async function currentSupabaseAccessToken() { if (supabaseClient?.auth?.getSession) { const { data } = await supabaseClient.auth.getSession(); return data?.session?.access_token ?? supabaseRestSession?.access_token ?? null; } return supabaseRestSession?.access_token ?? null; } async function fetchRouteEstimateFromEdge(body) { if (!hasSupabaseRuntime()) throw new Error("Supabase runtime is required for accurate route estimates."); await assertPlatformFeatureEnabled("route_estimates_enabled", "Route estimates"); const functionName = routeEstimateFunctionName(); const token = await currentSupabaseAccessToken(); if (!token) throw new Error("Passenger sign-in is required for accurate route estimates."); const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${functionName}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify(body) }), "Fetching accurate route estimate", optionalSupabaseRequestTimeoutMs ); const payload = await response.json().catch(() => null); if (!response.ok) throw new Error(payload?.error || "Route estimate Edge Function failed."); return payload; } function fareGuidanceFromRouteEstimate(routeEstimate, stops = [], meta = {}) { const distanceMeters = Number(routeEstimate?.distanceMeters); const durationSeconds = Number(routeEstimate?.durationSeconds); if (!Number.isFinite(distanceMeters) || distanceMeters <= 0) return null; if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) return null; return fareGuidanceFromDistance( Math.max(0.6, distanceMeters * metersToMiles), Math.max(1, Math.ceil(durationSeconds / 60)), stops, { source: routeEstimate.source ?? (routeEstimate.provider === "google-routes" ? "google-routes" : "cameroon-local"), provider: routeEstimate.provider ?? routeEstimate.source ?? "cameroon-local", cached: Boolean(routeEstimate.cached), routeKey: routeEstimate.routeKey ?? null, routePolyline: routeEstimate.routePolyline ?? null, country: meta.country, city: meta.city, passengerCount: meta.passengerCount, luggageCount: meta.luggageCount, destinationFingerprint: routeEstimate.destinationFingerprint ?? null, estimatedAt: routeEstimate.estimatedAt ?? new Date().toISOString() } ); } async function accurateFareGuidanceForRide(country, city, pickupAreaName, destinationAreaName, destination, pickupGps = pendingPickupGps, stops = [], pickupDescription = els.pickupDescription?.value, destinationPlaceOverride = null, fareFactors = {}) { const fallback = routeEstimatesEnabled() ? null : fareGuidanceForRide(country, city, pickupAreaName, destinationAreaName, pickupGps, stops, fareFactors); if (!routeEstimatesEnabled()) return fallback; const localGuidance = immediateCoordinateFareGuidance(country, city, pickupAreaName, destinationAreaName, pickupDescription, destination, stops, destinationPlaceOverride, pickupGps, fareFactors); const body = routeEstimateRequestBody(country, city, pickupGps, destinationAreaName, destination, stops, pickupDescription, pickupAreaName, destinationPlaceOverride); if (!routeOriginIsSpecific(body.origin)) { if (accurateRouteEstimateRequired()) throw new Error("Pickup address or pickup GPS is required before accurate route pricing."); return null; } if (routeOriginNeedsConfirmedAddressForPricing(body.origin)) { throw new Error(currentLocationNeedsAddressMessage()); } if (routeDestinationNeedsConfirmedAddressForPricing(body.destination)) { throw new Error(confirmedCameroonDestinationRequiredMessage()); } if (!body.destination.address) { if (accurateRouteEstimateRequired()) throw new Error("Destination address is required before accurate route pricing."); return null; } try { lastRouteEstimateError = null; const estimate = await fetchRouteEstimateFromEdge(body); const guidance = fareGuidanceFromRouteEstimate(estimate, stops, { country, city, ...fareFactors }); if (!guidance) throw new Error("The route service did not return a usable driving distance."); const safeGuidance = safeRouteGuidance(guidance, localGuidance, "fare"); if (safeGuidance === guidance) lastRouteEstimateError = null; return safeGuidance; } catch (error) { logClientWarning("Accurate route estimate failed.", error); lastRouteEstimateError = error; if (routeEstimateFallbackAllowedForTesting() && localGuidance) return localGuidance; if (accurateRouteEstimateRequired() && !routeEstimateFallbackAllowedForTesting()) { throw new Error("Driving distance could not be confirmed. Please try again in a moment."); } return null; } } function validGpsCoordinate(latitude, longitude) { return Number.isFinite(latitude) && Number.isFinite(longitude) && latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180; } function normalizeGpsPoint(value) { if (!value) return null; const latitude = Number(value.latitude ?? value.lat); const longitude = Number(value.longitude ?? value.lng ?? value.lon); if (!validGpsCoordinate(latitude, longitude)) return null; const accuracyMeters = Number(value.accuracyMeters ?? value.accuracy); return { latitude, longitude, accuracyMeters: Number.isFinite(accuracyMeters) ? Math.round(accuracyMeters) : null, capturedAt: value.capturedAt ?? new Date().toISOString() }; } function gpsPointFromPosition(position) { return normalizeGpsPoint({ latitude: position.coords.latitude, longitude: position.coords.longitude, accuracyMeters: position.coords.accuracy, capturedAt: new Date(position.timestamp || Date.now()).toISOString() }); } function parseDatabasePointObject(value) { if (!value || typeof value !== "object") return null; if (Array.isArray(value) && value.length >= 2) { return { longitude: Number(value[0]), latitude: Number(value[1]) }; } if (Array.isArray(value.coordinates) && value.coordinates.length >= 2) { return { longitude: Number(value.coordinates[0]), latitude: Number(value.coordinates[1]) }; } return { latitude: Number(value.latitude ?? value.lat ?? value.y), longitude: Number(value.longitude ?? value.lng ?? value.lon ?? value.x) }; } function parseDatabasePointText(value) { const text = String(value ?? "").trim(); if (!text) return null; try { return parseDatabasePointObject(JSON.parse(text)); } catch { // Continue with WKT/EWKB parsing below. } const pointMatch = text.match(/^(?:SRID=\d+;)?POINT\s*\(\s*(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)\s*\)$/i); if (pointMatch) { return { longitude: Number(pointMatch[1]), latitude: Number(pointMatch[2]) }; } if (!/^[0-9a-f]+$/i.test(text) || text.length % 2 !== 0) return null; const bytes = Uint8Array.from(text.match(/.{2}/g).map((byte) => parseInt(byte, 16))); if (bytes.length < 21) return null; const view = new DataView(bytes.buffer); const littleEndian = view.getUint8(0) === 1; let geometryType = view.getUint32(1, littleEndian); let offset = 5; if (geometryType & 0x20000000) { offset += 4; geometryType &= ~0x20000000; } if ((geometryType & 0xff) !== 1 || bytes.length < offset + 16) return null; return { longitude: view.getFloat64(offset, littleEndian), latitude: view.getFloat64(offset + 8, littleEndian) }; } function gpsPointFromDatabaseLocation(value, accuracyMeters = null, capturedAt = null) { const point = typeof value === "string" ? parseDatabasePointText(value) : parseDatabasePointObject(value); if (!validGpsCoordinate(Number(point?.latitude), Number(point?.longitude))) return null; return normalizeGpsPoint({ latitude: point.latitude, longitude: point.longitude, accuracyMeters, capturedAt }); } function gpsPointToDatabase(value) { const point = normalizeGpsPoint(value); return point ? `SRID=4326;POINT(${point.longitude} ${point.latitude})` : null; } function gpsStatusLabel(value, emptyText = "GPS not shared") { const point = normalizeGpsPoint(value); if (!point) return emptyText; return "Location active"; } function passengerPickupGpsReadyLabel(value) { return normalizeGpsPoint(value) ? "Exact pickup location is ready." : "Exact pickup location is off."; } function gpsDistanceMetersBetween(a, b) { const first = normalizeGpsPoint(a); const second = normalizeGpsPoint(b); if (!first || !second) return null; return gpsDistanceKmBetween(first, second) * 1000; } function gpsAgeMinutes(point) { const capturedAt = point?.capturedAt; if (!capturedAt) return null; const capturedTime = new Date(capturedAt).getTime(); if (!Number.isFinite(capturedTime)) return null; return Math.max(0, Math.floor((Date.now() - capturedTime) / 60000)); } function pickupGpsQualityIssue(point) { const pickupGps = normalizeGpsPoint(point); if (!pickupGps) return null; const ageMinutes = gpsAgeMinutes(pickupGps); if (ageMinutes == null) { return matchingText( "pickupGpsCaptureTimeUnavailable", "Exact pickup capture time is unavailable. Waka can still publish with a clear typed landmark." ); } if (ageMinutes > passengerPickupGpsFreshMinutes) { return matchingText( "pickupGpsOld", `Exact pickup location is ${ageMinutes} minutes old. Waka can still publish with a clear typed landmark.`, { minutes: ageMinutes } ); } if (pickupGps.accuracyMeters == null) { return matchingText( "pickupGpsAccuracyUnavailable", "Exact pickup accuracy is unavailable. Waka can still publish with the pickup address or landmark." ); } if (pickupGps.accuracyMeters > passengerPickupGpsAccuracyLimitMeters()) { return matchingText( "pickupGpsNeedsClearerSignal", "Exact pickup location needs a clearer signal. Waka can still publish with a clear typed landmark." ); } return null; } function riderLiveGpsQualityIssue(point) { const liveGps = normalizeGpsPoint(point); if (!liveGps) return null; const ageMinutes = gpsAgeMinutes(liveGps); if (ageMinutes == null) { return "Live rider GPS capture time is unavailable. Capture it again before sharing live GPS for matching."; } if (ageMinutes > riderLiveGpsFreshMinutes) { return `Live rider GPS is ${ageMinutes} minutes old. Capture it again so nearby passengers are matched to your current position.`; } if (liveGps.accuracyMeters == null) { return "Rider location quality is still loading. Keep Waka open for a moment, then activate again."; } if (liveGps.accuracyMeters > riderLiveGpsAccuracyLimitMeters()) { return `Rider location is not clear enough yet. Waka needs ${riderLiveGpsAccuracyLimitMeters()} meters or better. Turn on precise/high-accuracy location, keep Waka open, move into a clearer spot, then activate again.`; } return null; } function formatGpsAgeLabel(point) { const ageMinutes = gpsAgeMinutes(point); if (ageMinutes == null) return "capture time unknown"; if (ageMinutes === 0) return "captured just now"; return `captured ${ageMinutes} min ago`; } function pickupGpsQualityChip(request) { if (activeRole() !== "rider") return null; const pickupGps = requestPickupGps(request); if (!request?.pickupLocationShared && !pickupGps) return null; return "Pickup location verified"; } function requestPickupGps(request) { return normalizeGpsPoint(request?.pickupGps ?? { latitude: request?.pickupLatitude, longitude: request?.pickupLongitude, accuracyMeters: request?.pickupGpsAccuracyMeters, capturedAt: request?.pickupGpsCapturedAt }); } function fareHistoryTrail(source, currentFare, currentCreatedAt = null) { const rows = Array.isArray(source?.fareHistory ?? source?.fare_history) ? (source.fareHistory ?? source.fare_history) : []; const trail = rows .map((entry) => ({ fare: Number(entry?.fare ?? entry?.amount ?? entry?.fare_xaf ?? entry), createdAt: entry?.createdAt ?? entry?.created_at ?? currentCreatedAt ?? source?.createdAt ?? source?.created_at ?? null })) .filter((entry) => Number.isFinite(entry.fare) && entry.fare > 0) .sort((a, b) => new Date(a.createdAt ?? 0).getTime() - new Date(b.createdAt ?? 0).getTime()) .reduce((deduped, entry) => { const previous = deduped[deduped.length - 1]; if (!previous || Number(previous.fare) !== Number(entry.fare)) deduped.push(entry); return deduped; }, []); const resolvedCurrentFare = Number(currentFare); if (Number.isFinite(resolvedCurrentFare) && resolvedCurrentFare > 0 && (!trail.length || Number(trail[trail.length - 1].fare) !== resolvedCurrentFare)) { trail.push({ fare: resolvedCurrentFare, createdAt: currentCreatedAt ?? source?.updatedAt ?? source?.createdAt ?? null }); } return trail; } function fareChangeFromTrail(trail) { if (!Array.isArray(trail) || trail.length < 2) return null; const initialFare = Number(trail[0]?.fare); const currentFare = Number(trail[trail.length - 1]?.fare); const delta = currentFare - initialFare; if (!Number.isFinite(delta) || delta === 0) return null; return { direction: delta > 0 ? "up" : "down", amount: Math.abs(delta), previousFare: initialFare, currentFare, changedAt: trail[trail.length - 1]?.createdAt ?? new Date().toISOString() }; } function fareChangeChipFromTrail(trail, country, prefix = "") { const change = fareChangeFromTrail(trail); if (!change) return null; const direction = change.direction === "up" ? "\u2191" : "\u2193"; return `${prefix}${direction} ${formatMoney(change.amount, country)} from ${formatMoney(change.previousFare, country)}`; } function requestMarketplaceFareChange(nextRequest, previousRequest) { if (!nextRequest || nextRequest.status !== "open") return null; const historyChange = fareChangeFromTrail(fareHistoryTrail(nextRequest, nextRequest.fareOffer, nextRequest.updatedAt ?? nextRequest.createdAt)); if (historyChange?.direction === "up") return historyChange; const currentFare = Number(nextRequest.fareOffer); const previousFare = Number(previousRequest?.fareOffer); if (Number.isFinite(currentFare) && Number.isFinite(previousFare) && currentFare > previousFare) { return { direction: "up", amount: currentFare - previousFare, previousFare, currentFare, changedAt: new Date().toISOString() }; } const existingChange = normalizeMarketplaceFareChange(previousRequest?.marketplaceFareChange); if (existingChange && Number.isFinite(currentFare) && Number(existingChange.currentFare) === currentFare) { return existingChange; } return null; } function preserveRideRequestPickup(nextRequest, previousRequest) { if (!nextRequest) return nextRequest; const previousGps = requestPickupGps(previousRequest); const nextGps = requestPickupGps(nextRequest); const pickupGps = nextGps ?? previousGps; const pickupDescription = String(nextRequest.pickupDescription ?? "").trim() ? nextRequest.pickupDescription : previousRequest?.pickupDescription ?? nextRequest.pickupDescription; const pickupArea = String(nextRequest.pickupArea ?? "").trim() ? nextRequest.pickupArea : previousRequest?.pickupArea ?? nextRequest.pickupArea; return { ...nextRequest, pickupArea, pickupDescription, pickupLocationShared: Boolean(nextRequest.pickupLocationShared || previousRequest?.pickupLocationShared || pickupGps), pickupGps: pickupGps ?? nextRequest.pickupGps ?? previousRequest?.pickupGps ?? null, pickupLatitude: pickupGps?.latitude ?? nextRequest.pickupLatitude ?? previousRequest?.pickupLatitude ?? null, pickupLongitude: pickupGps?.longitude ?? nextRequest.pickupLongitude ?? previousRequest?.pickupLongitude ?? null, pickupGpsAccuracyMeters: pickupGps?.accuracyMeters ?? nextRequest.pickupGpsAccuracyMeters ?? previousRequest?.pickupGpsAccuracyMeters ?? null, pickupGpsCapturedAt: pickupGps?.capturedAt ?? nextRequest.pickupGpsCapturedAt ?? previousRequest?.pickupGpsCapturedAt ?? null, marketplaceFareChange: requestMarketplaceFareChange(nextRequest, previousRequest) }; } function riderMarketplaceFareChangeChip(request) { if (activeRole() !== "rider" || request?.status !== "open") return null; const change = normalizeMarketplaceFareChange(request?.marketplaceFareChange); const currentFare = Number(request?.fareOffer); if (!change || change.direction !== "up" || !Number.isFinite(currentFare) || Number(change.currentFare) !== currentFare) return null; return `\u2191 ${formatMoney(change.amount, request.country)} from ${formatMoney(change.previousFare, request.country)}`; } function clearRequestMarketplaceFareChange(requestId) { if (!requestId) return; let changed = false; state.requests = state.requests.map((request) => { if (request.id !== requestId || !request.marketplaceFareChange) return request; changed = true; return { ...request, marketplaceFareChange: null }; }); if (changed) clearStateLookupIndexes(); } function pickupGpsIsUsableForMatching(request) { const pickupGps = requestPickupGps(request); if (!pickupGps) return false; if (pickupGps.accuracyMeters == null || pickupGps.accuracyMeters > passengerPickupGpsAccuracyLimitMeters()) return false; const capturedTime = pickupGps.capturedAt ? new Date(pickupGps.capturedAt).getTime() : null; const createdTime = request?.createdAt ? new Date(request.createdAt).getTime() : null; if (!Number.isFinite(capturedTime)) return false; if (Number.isFinite(capturedTime) && Number.isFinite(createdTime)) { const maxAgeMs = passengerPickupGpsFreshMinutes * 60000; if (capturedTime < createdTime - maxAgeMs) return false; if (capturedTime > createdTime + 5 * 60000) return false; } else { const currentAgeMs = Date.now() - capturedTime; if (currentAgeMs > passengerPickupGpsFreshMinutes * 60000) return false; if (currentAgeMs < -5 * 60000) return false; } return true; } function requestPickupGpsForMatching(request) { if (!pickupGpsIsUsableForMatching(request)) return null; return requestPickupGps(request); } function riderCurrentGps(rider) { return normalizeGpsPoint(rider?.currentGps ?? { latitude: rider?.currentLatitude, longitude: rider?.currentLongitude, accuracyMeters: rider?.currentGpsAccuracyMeters, capturedAt: rider?.currentGpsCapturedAt }); } function clearRiderLiveGpsFields(rider) { if (!rider) return rider; return { ...rider, currentGps: null, currentLatitude: null, currentLongitude: null, currentGpsAccuracyMeters: null, currentGpsCapturedAt: null }; } function saveCurrentRiderRecord(rider) { if (!rider) return; state.rider = state.rider?.id === rider.id ? rider : state.rider; state.riders = upsertById(state.riders, rider); saveState(); } function riderLiveGpsAgeMinutes(rider) { const capturedAt = rider?.currentGps?.capturedAt ?? rider?.currentGpsCapturedAt; if (!capturedAt) return null; const capturedTime = new Date(capturedAt).getTime(); if (!Number.isFinite(capturedTime)) return null; return Math.max(0, Math.floor((Date.now() - capturedTime) / 60000)); } function riderLiveGpsIsFresh(rider = currentRiderRecord()) { const ageMinutes = riderLiveGpsAgeMinutes(rider); return ageMinutes != null && ageMinutes <= riderLiveGpsFreshMinutes; } function riderLiveGpsIsUsable(rider = currentRiderRecord()) { const currentGps = riderCurrentGps(rider); return Boolean(currentGps && riderLiveGpsIsFresh(rider) && !riderLiveGpsQualityIssue(currentGps)); } function riderCurrentFreshGps(rider = currentRiderRecord()) { if (!riderLiveGpsIsUsable(rider)) return null; return riderCurrentGps(rider); } function riderLiveGpsStatusSummary(rider = currentRiderRecord()) { if (!riderCurrentGps(rider)) return `Live GPS required before receiving requests.`; const ageMinutes = riderLiveGpsAgeMinutes(rider); if (ageMinutes == null) return `Live GPS needs a fresh capture before receiving requests.`; const qualityIssue = riderLiveGpsQualityIssue(riderCurrentGps(rider)); if (qualityIssue) return qualityIssue; if (ageMinutes <= riderLiveGpsFreshMinutes) { const remaining = Math.max(1, riderLiveGpsFreshMinutes - ageMinutes); return `Live GPS active for about ${remaining} min.`; } return `Live GPS expired ${ageMinutes} min ago; automatic refresh is needed for GPS matching.`; } function riderLiveGpsNeedsClearing(rider = currentRiderRecord()) { return Boolean(riderCurrentGps(rider) && !riderLiveGpsIsUsable(rider)); } function gpsDistanceKmBetween(first, second) { const a = normalizeGpsPoint(first); const b = normalizeGpsPoint(second); if (!a || !b) return null; const toRadians = (value) => (value * Math.PI) / 180; const lat1 = toRadians(a.latitude); const lat2 = toRadians(b.latitude); const deltaLat = toRadians(b.latitude - a.latitude); const deltaLng = toRadians(b.longitude - a.longitude); const haversine = Math.sin(deltaLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(deltaLng / 2) ** 2; return 6371 * 2 * Math.atan2(Math.sqrt(haversine), Math.sqrt(1 - haversine)); } function gpsDistanceKmForRequest(request, rider = currentRiderRecord()) { if (request?.matchSource === "area_fallback") return null; const rpcDistance = request?.gpsDistanceMeters; if (rpcDistance !== null && rpcDistance !== undefined && Number.isFinite(Number(rpcDistance))) { return Number(rpcDistance) / 1000; } return gpsDistanceKmBetween(requestPickupGpsForMatching(request), riderCurrentFreshGps(rider)); } function riderProximityToRequest(request, rider = currentRiderRecord()) { if (!request || !rider || request.country !== rider.country || request.city !== rider.city) return null; const pickup = findArea(request.country, request.city, request.pickupArea); const riderArea = findArea(rider.country, rider.city, rider.area); const distanceKm = estimatedAreaDistanceKm(request.country, request.city, pickup, riderArea); if (distanceKm == null) return null; return { distanceKm, pickupArea: pickup?.name ?? request.pickupArea, riderArea: riderArea?.name ?? rider.area, limit: riderServiceRadius(rider, request), label: distanceKm < 1 ? "Closest pickup area" : distanceKm <= 3 ? "Near pickup area" : "Within service range" }; } function riderWithinRequestProximity(request, rider = currentRiderRecord()) { const proximity = riderProximityToRequest(request, rider); return Boolean(proximity && proximity.distanceKm <= proximity.limit && riderWithinPickupEta(proximity.distanceKm, rider, request)); } function riderWithinGpsProximity(request, rider = currentRiderRecord()) { if (!request || !rider) return false; const distanceKm = gpsDistanceKmForRequest(request, rider); return distanceKm != null && distanceKm <= riderServiceRadius(rider, request) && riderWithinPickupEta(distanceKm, rider, request); } function riderPickupMaxEtaMinutesForRequest(request) { return isScheduledRequest(request) ? scheduledRiderPickupMaxEtaMinutes : riderPickupMaxEtaMinutes; } function riderWithinPickupEta(distanceKm, rider = currentRiderRecord(), request = null) { const eta = pickupEtaMinutes(distanceKm, rider); return eta != null && eta <= riderPickupMaxEtaMinutesForRequest(request); } function pickupProximityModel(request, rider = currentRiderRecord()) { if (!request || !rider) return null; const gpsDistanceKm = gpsDistanceKmForRequest(request, rider); if (gpsDistanceKm != null) { return { source: request.matchSource === "postgis" ? "GPS/PostGIS" : "GPS", distanceKm: gpsDistanceKm, etaMinutes: pickupEtaMinutes(gpsDistanceKm, rider), label: "GPS pickup" }; } const proximity = riderProximityToRequest(request, rider); if (!proximity) return null; return { source: "Area estimate", distanceKm: proximity.distanceKm, etaMinutes: pickupEtaMinutes(proximity.distanceKm, rider), label: proximity.label }; } function pickupProximitySortValue(request, rider = currentRiderRecord()) { return pickupProximityModel(request, rider)?.etaMinutes ?? Number.POSITIVE_INFINITY; } function pickupAreasWithinRiderRadius(rider = currentRiderRecord()) { if (!rider) return []; const riderArea = findArea(rider.country, rider.city, rider.area); const limit = riderServiceRadius(rider); const nearbyAreas = areas(rider.country, rider.city) .filter((area) => { const distanceKm = estimatedAreaDistanceKm(rider.country, rider.city, area, riderArea); return distanceKm != null && distanceKm <= limit; }) .map((area) => area.name); return nearbyAreas.length ? nearbyAreas : [rider.area].filter(Boolean); } function riderActiveImmediateRide(rider = currentRiderRecord()) { if (!rider) return null; return state.requests.find((request) => selectedRiderIdForRequest(request) === rider.id && !isScheduledRequest(request) && ["matched", "arrived", "in_progress"].includes(request.status)) ?? null; } function riderInProgressImmediateRide(rider = currentRiderRecord()) { if (!rider) return null; return state.requests.find((request) => selectedRiderIdForRequest(request) === rider.id && !isScheduledRequest(request) && request.status === "in_progress") ?? null; } function requestDestinationGps(request) { return normalizeGpsPoint({ latitude: request?.destinationLatitude, longitude: request?.destinationLongitude }); } function riderDropoffEtaMinutes(request, rider = currentRiderRecord()) { const riderGps = riderCurrentFreshGps(rider); const destinationGps = requestDestinationGps(request); if (!riderGps || !destinationGps) return null; const distanceMeters = gpsDistanceMetersBetween(riderGps, destinationGps); if (distanceMeters == null || !Number.isFinite(Number(distanceMeters))) return null; return pickupEtaMinutes(Number(distanceMeters) / 1000, rider); } function riderInProgressRideNearDropoff(rider = currentRiderRecord()) { const ride = riderInProgressImmediateRide(rider); if (!ride) return false; const eta = riderDropoffEtaMinutes(ride, rider); return eta != null && eta <= riderDropoffRequestLeadMinutes; } function riderBlockingImmediateRide(rider = currentRiderRecord()) { const ride = riderActiveImmediateRide(rider); if (!ride) return null; if (ride.status === "in_progress" && riderInProgressRideNearDropoff(rider)) return null; return ride; } function riderCanReviewAnotherImmediateRequest(request, rider = currentRiderRecord()) { const blockingRide = riderBlockingImmediateRide(rider); return !blockingRide || blockingRide.id === request?.id; } function riderShouldHoldNextRideNavigation(request, rider = currentRiderRecord()) { const inProgressRide = riderInProgressImmediateRide(rider); return Boolean(inProgressRide && request && inProgressRide.id !== request.id && !isScheduledRequest(request)); } function riderPickupNavigationShouldWaitForDropoff(request, rider = currentRiderRecord()) { return Boolean(request && request.status === "matched" && riderShouldHoldNextRideNavigation(request, rider)); } function queuedRiderPickupAfterDropoff(completedRequestId = "", rider = currentRiderRecord()) { if (!rider?.id) return null; return state.requests .filter((request) => request.id !== completedRequestId && selectedRiderIdForRequest(request) === rider.id && request.status === "matched" && !isScheduledRequest(request)) .sort((a, b) => new Date(a.matchedAt ?? a.createdAt ?? 0).getTime() - new Date(b.matchedAt ?? b.createdAt ?? 0).getTime())[0] ?? null; } function selectedRequest() { const request = stateLookupIndexes().requestMap.get(state.selectedRequestId) ?? null; if (activeRole() === "passenger" && request && !passengerRideRequestVisibleInActiveBoard(request)) return null; return request; } function selectedPassengerCountry() { const country = state.passenger?.country ?? els.passengerCountry?.value ?? els.passengerActiveCountry?.value ?? defaultLaunchCountry(); return enabledLaunchCountries().includes(country) ? country : defaultLaunchCountry(); } function selectedPassengerCity() { const country = selectedPassengerCountry(); const city = state.passenger?.city ?? els.passengerCity?.value ?? els.passengerActiveCity?.value ?? defaultLaunchCity(country); return cityNames(country).includes(city) ? city : defaultLaunchCity(country); } function selectedRiderCountry() { const country = state.rider?.country ?? els.riderActiveCountry?.value ?? els.riderCountry?.value ?? defaultLaunchCountry(); return enabledLaunchCountries().includes(country) ? country : defaultLaunchCountry(); } function selectedRiderCity() { const country = selectedRiderCountry(); const city = state.rider?.city ?? els.riderActiveCity?.value ?? els.riderCity?.value ?? defaultLaunchCity(country); return cityNames(country).includes(city) ? city : defaultLaunchCity(country); } function activeRole() { return availableWorkspaceTab(state.activeTab) ?? defaultRuntimeTab(); } function currentRiderRecord() { if (!state.rider) return null; const indexed = stateLookupIndexes().riderMap.get(state.rider.id); return indexed ? { ...indexed, ...state.rider } : state.rider; } function requestBelongsToPassenger(request) { return Boolean(request && state.passenger && request.passengerId === state.passenger.id); } function passengerPendingRide(requests = state.requests) { if (!state.passenger) return null; return requests.find((request) => requestBelongsToPassenger(request) && ["open", "matched", "arrived", "in_progress"].includes(request.status)) ?? null; } function passengerActiveRideRequestStatuses() { return ["open", "matched", "arrived", "in_progress"]; } function passengerRideRequestVisibleInActiveBoard(request) { return Boolean(requestBelongsToPassenger(request) && passengerActiveRideRequestStatuses().includes(request.status)); } function focusPassengerPendingRideAfterMarketplaceSync() { const pendingRide = passengerPendingRide(); if (!pendingRide) return false; state.selectedRequestId = pendingRide.id; state.passengerPage = "trips"; state.activeTab = "passenger"; state.showRoleEntry = false; saveState(); return true; } function passengerPendingRideMessage(request = passengerPendingRide()) { if (!request) return ""; const status = rideStatusLabel(request).toLowerCase(); return `You already have a ${status} ride request. Complete or cancel it before requesting another ride.`; } function offerBelongsToRider(offer) { return riderIdentityMatches(offer?.riderId); } function selectedRiderIdForRequest(request) { if (!request) return null; const indexes = stateLookupIndexes(); const requestOffers = indexes.offersByRequestId.get(request.id) ?? []; const explicitSelectedOffer = indexes.offerMap.get(request.selectedOfferId) ?? null; const storedRiderOffer = requestOffers.find((offer) => request.selectedRiderId && offer.riderId === request.selectedRiderId) ?? null; const acceptedOffer = requestOffers.find((offer) => offer.type === "accepted") ?? null; const onlyCompletedOffer = request.status === "completed" && requestOffers.length === 1 ? requestOffers[0] : null; const completedRideSettlement = request.status === "completed" ? (state.rideSettlements ?? []).find((settlement) => settlement.requestId === request.id && settlement.riderId) : null; if (request.status === "completed") { return completedRideSettlement?.riderId ?? explicitSelectedOffer?.riderId ?? storedRiderOffer?.riderId ?? acceptedOffer?.riderId ?? onlyCompletedOffer?.riderId ?? request.selectedRiderId ?? null; } return request.selectedRiderId ?? explicitSelectedOffer?.riderId ?? storedRiderOffer?.riderId ?? acceptedOffer?.riderId ?? null; } function requestHasSelectedOffer(request) { return Boolean(request?.selectedOfferId || request?.selected_offer_id); } function riderIdentityIds(rider = currentRiderRecord()) { return uniqueMarketplaceIds([ rider?.id, rider?.supabaseUserId, state.sessions?.rider?.userId ]).map((id) => String(id)); } function riderIdentityMatches(id, rider = currentRiderRecord()) { const value = String(id ?? ""); return Boolean(value && riderIdentityIds(rider).includes(value)); } function selectedRiderNameForRequest(request) { if (!request) return null; const selectedRiderId = selectedRiderIdForRequest(request); const rider = stateLookupIndexes().riderMap.get(selectedRiderId); const completedRideSettlement = request.status === "completed" ? (state.rideSettlements ?? []).find((settlement) => settlement.requestId === request.id && settlement.riderId === selectedRiderId) : null; return request.selectedRiderName ?? rider?.name ?? completedRideSettlement?.riderName ?? null; } function firstNameOnly(name, fallback = "Matched contact") { const normalized = String(name ?? "").trim().replace(/\s+/g, " "); return normalized ? normalized.split(" ")[0] : fallback; } function passengerFirstNameForRequest(request) { return firstNameOnly(request?.passengerName, "Passenger"); } function selectedRiderFirstNameForRequest(request) { return firstNameOnly(selectedRiderNameForRequest(request), "Rider"); } function requestHasRiderMatch(request) { if (!request || !state.rider) return false; return riderIdentityMatches(selectedRiderIdForRequest(request)); } function requestIsActiveForCurrentRider(request, rider = currentRiderRecord()) { return Boolean(request && rider && !riderPrePickupCancellationClearedForCurrentRider(request, rider) && riderIdentityMatches(selectedRiderIdForRequest(request), rider) && ["matched", "arrived", "in_progress"].includes(request.status)); } function requestPickupCanShowPreciseText(request) { if (!request) return false; if (activeRole() === "passenger") return requestBelongsToPassenger(request); if (activeRole() === "rider") return requestIsActiveForCurrentRider(request); return activeRole() === "admin"; } function requestPickupAreaText(request, fallback = "Pickup") { const town = requestPickupTownText(request, ""); if (town) return town; const area = String(request?.pickupArea ?? "").trim(); if (area) return area; if (requestPickupGps(request) || request?.pickupLocationShared) return "Verified pickup location"; return String(request?.city ?? "").trim() || fallback; } function requestPickupDisplayText(request, fallback = "Pickup") { if (activeRole() === "rider" && !requestPickupCanShowPreciseText(request)) { return requestPickupAreaText(request, fallback); } const description = String(request?.pickupFormattedAddress ?? request?.pickupDescription ?? "").trim(); if (description && !pickupUsesGpsFallbackText(description)) return description; if (requestPickupGps(request) || request?.pickupLocationShared) return "Verified pickup location"; return String(request?.pickupArea ?? "").trim() || fallback; } function stripPostalFromAddressRegion(value) { return String(value ?? "") .replace(/\b\d{4,}(?:-\d{4})?\b.*$/, "") .replace(/\s+[A-Z]\d[A-Z]\s*\d[A-Z]\d.*$/i, "") .trim(); } function addressTownLabel(address) { const parts = String(address ?? "") .split(",") .map((part) => part.trim()) .filter(Boolean); if (!parts.length) return ""; const last = parts[parts.length - 1]?.toLowerCase(); const withoutCountry = ["usa", "us", "united states", "cameroon"].includes(last) ? parts.slice(0, -1) : parts; if (withoutCountry.length >= 3 && /\d/.test(withoutCountry[0])) { const region = stripPostalFromAddressRegion(withoutCountry[2]); return [withoutCountry[1], region].filter(Boolean).join(", "); } if (withoutCountry.length >= 2) { const region = stripPostalFromAddressRegion(withoutCountry[1]); return [withoutCountry[0], region].filter(Boolean).join(", "); } return withoutCountry[0] ?? ""; } function nearestKnownGpsTownLabel(request, maxDistanceKm = 10) { const gps = requestPickupGps(request); const townCenters = launchGpsTownCenters?.[request?.country]?.[request?.city] ?? []; if (!gps || !townCenters.length) return ""; const nearest = townCenters .map((town) => ({ name: town.name, distanceKm: gpsDistanceKmBetween(gps, { latitude: town.latitude, longitude: town.longitude }) })) .filter((town) => town.name && Number.isFinite(Number(town.distanceKm))) .sort((a, b) => a.distanceKm - b.distanceKm)[0]; return nearest && nearest.distanceKm <= maxDistanceKm ? nearest.name : ""; } function matchingLaunchAreaName(country, city, label) { const town = String(label ?? "").split(",")[0]?.trim().toLowerCase(); if (!town) return ""; return areas(country, city).find((area) => area.name.toLowerCase() === town)?.name ?? ""; } function nearestKnownGpsAreaName(country, city, gps, maxDistanceKm = 10) { const townCenters = launchGpsTownCenters?.[country]?.[city] ?? []; const point = normalizeGpsPoint(gps); if (!point || !townCenters.length) return ""; const nearest = townCenters .map((town) => ({ name: matchingLaunchAreaName(country, city, town.name), distanceKm: gpsDistanceKmBetween(point, { latitude: town.latitude, longitude: town.longitude }) })) .filter((town) => town.name && Number.isFinite(Number(town.distanceKm))) .sort((a, b) => a.distanceKm - b.distanceKm)[0]; return nearest && nearest.distanceKm <= maxDistanceKm ? nearest.name : ""; } function inferredLaunchLocationFromGps(country, gps, maxDistanceKm = 35) { const point = normalizeGpsPoint(gps); const enabledCountry = enabledLaunchCountries().includes(country) ? country : defaultLaunchCountry(); if (!point || !enabledCountry) return null; const countryCenters = launchGpsTownCenters?.[enabledCountry] ?? {}; const candidates = []; for (const city of cityNames(enabledCountry)) { for (const center of countryCenters[city] ?? []) { const centerPoint = normalizeGpsPoint({ latitude: center.latitude, longitude: center.longitude }); if (!center?.name || !centerPoint) continue; const distanceKm = gpsDistanceKmBetween(point, centerPoint); if (distanceKm == null || !Number.isFinite(Number(distanceKm))) continue; candidates.push({ city, area: matchingLaunchAreaName(enabledCountry, city, center.name) || center.name, distanceKm }); } } const nearest = candidates.sort((a, b) => a.distanceKm - b.distanceKm)[0]; if (!nearest) return null; const cityLimitKm = Math.max(8, Math.min(45, citySpanKm(enabledCountry, nearest.city) * 2)); const allowedKm = Math.max(maxDistanceKm, cityLimitKm); if (nearest.distanceKm > allowedKm) return null; return { country: enabledCountry, city: nearest.city, area: nearest.area, distanceKm: nearest.distanceKm }; } function pickupAreaForPublish(country, city, selectedArea, pickupDescription, pickupGps) { const addressTown = pickupUsesCurrentLocationText(pickupDescription) || pickupUsesGpsFallbackText(pickupDescription) ? "" : addressTownLabel(pickupDescription); return matchingLaunchAreaName(country, city, addressTown) || nearestKnownGpsAreaName(country, city, pickupGps) || selectedArea; } function destinationAreaForPublish(country, city, selectedArea, destination, destinationPlace) { const destinationTown = addressTownLabel(destinationPlace?.formattedAddress || destination); return matchingLaunchAreaName(country, city, destinationTown) || selectedArea; } function requestPickupTownText(request, fallback = "Pickup") { const address = String(request?.pickupFormattedAddress ?? request?.pickupDescription ?? "").trim(); const addressTown = pickupUsesCurrentLocationText(address) || pickupUsesGpsFallbackText(address) ? "" : addressTownLabel(address); if (addressTown) return addressTown; const gpsTown = nearestKnownGpsTownLabel(request); if (gpsTown) return gpsTown; if (requestPickupGps(request) || request?.pickupLocationShared) return "Verified GPS pickup"; return String(request?.pickupArea ?? "").trim() || String(request?.city ?? "").trim() || fallback; } function requestDestinationTownText(request, fallback = "Destination") { const displayRequest = typeof riderVisibleRouteRequest === "function" ? riderVisibleRouteRequest(request) : request; const addressTown = addressTownLabel(displayRequest?.destinationFormattedAddress || displayRequest?.destination); return addressTown || String(displayRequest?.destinationArea ?? "").trim() || String(displayRequest?.city ?? "").trim() || fallback; } function requestDestinationPreviewText(request) { const displayRequest = typeof riderVisibleRouteRequest === "function" ? riderVisibleRouteRequest(request) : request; const destination = requestDestinationTownText(displayRequest); const stopCount = normalizeRideStops(displayRequest?.rideStops).length; return stopCount ? `${stopCount} stop${stopCount === 1 ? "" : "s"} then ${destination}` : destination; } function requestDestinationDisplayText(request) { const displayRequest = typeof riderVisibleRouteRequest === "function" ? riderVisibleRouteRequest(request) : request; if (activeRole() === "rider" && !requestIsActiveForCurrentRider(request)) { return requestDestinationPreviewText(displayRequest); } return requestDestinationText(displayRequest); } function riderCanShowOfferControls(rider = currentRiderRecord(), request = selectedRequest()) { if (activeRole() !== "rider") return false; if (!riderCanSeeRequests(rider)) return false; if (!request) return false; return request.status === "open" && roleCanSeeRequest(request) && !requestIsActiveForCurrentRider(request); } function riderCanLeaveSelectedRequest(rider = currentRiderRecord(), request = selectedRequest()) { if (activeRole() !== "rider" || !rider?.id || !request?.id) return false; if (["completed", "cancelled"].includes(request.status)) return false; if (requestIsActiveForCurrentRider(request, rider)) return false; if (request.status === "open") return true; return state.offers.some((offer) => offer.requestId === request.id && offer.riderId === rider.id); } function activeMarketLocation() { const request = selectedRequest(); if (request && activeRole() !== "admin" && roleCanSeeRequest(request)) return { country: request.country, city: request.city }; if (activeRole() === "rider") return { country: selectedRiderCountry(), city: selectedRiderCity() }; return { country: selectedPassengerCountry(), city: selectedPassengerCity() }; } function requestMatchesVehicleFilter(request) { if (activeRole() === "rider") return true; return state.filter === "all" || normalizeRideVehicle(request.vehicle) === normalizeRideVehicle(state.filter); } function riderMarketplaceDestinationFilter() { return normalizeRiderMarketplaceDestinationFilter(state.riderMarketplaceDestinationFilter); } function riderMarketplaceDestinationFilterIsActive(filter = riderMarketplaceDestinationFilter()) { return Boolean(filter.enabled && filter.consent && (filter.country || filter.city || filter.area || filter.query)); } function riderMarketplaceDestinationFilterSummary(filter = riderMarketplaceDestinationFilter()) { if (!riderMarketplaceDestinationFilterIsActive(filter)) return "Showing all nearby rides."; const parts = [ filter.area, filter.city, filter.country, filter.query ? `contains "${filter.query}"` : "" ].filter(Boolean); const nearby = filter.area ? `, including nearby towns within ${riderDestinationFilterNeighborRadiusMiles} miles` : ""; return `Showing rides headed toward ${parts.join(", ")}${nearby}.`; } function requestDestinationFilterSearchText(request) { return [ request?.country, request?.city, request?.destinationArea, request?.destination, request?.destinationFormattedAddress, requestDestinationText(request), ...normalizeRideStops(request?.rideStops) ].map((value) => String(value ?? "").trim().toLowerCase()).filter(Boolean).join(" "); } function launchGpsTownCenterForName(country, city, name) { const town = String(name ?? "").split(",")[0]?.trim().toLowerCase(); if (!town) return null; const center = (launchGpsTownCenters?.[country]?.[city] ?? []) .find((item) => String(item?.name ?? "").trim().toLowerCase() === town); return center ? normalizeGpsPoint({ latitude: center.latitude, longitude: center.longitude }) : null; } function requestDestinationFilterPoint(request) { const directGps = requestDestinationGps(request); if (directGps) return directGps; const country = request?.country; const city = request?.city; const candidates = [ request?.destinationArea, addressTownLabel(request?.destinationFormattedAddress || request?.destination), requestDestinationTownText(request) ]; for (const candidate of candidates) { const areaName = matchingLaunchAreaName(country, city, candidate) || candidate; const point = launchGpsTownCenterForName(country, city, areaName); if (point) return point; } return null; } function riderDestinationFilterPreferenceDistanceKm(request, filter = riderMarketplaceDestinationFilter(), searchText = null) { if (!riderMarketplaceDestinationFilterIsActive(filter) || !filter.area) return null; const haystack = searchText ?? requestDestinationFilterSearchText(request); const areaNeedle = filter.area.toLowerCase(); if (request?.destinationArea === filter.area || haystack.includes(areaNeedle)) return 0; const selectedAreaPoint = launchGpsTownCenterForName(filter.country || request?.country, filter.city || request?.city, filter.area); const requestDestinationPoint = requestDestinationFilterPoint(request); const distanceKm = gpsDistanceKmBetween(selectedAreaPoint, requestDestinationPoint); return Number.isFinite(Number(distanceKm)) ? Number(distanceKm) : null; } function riderDestinationFilterSortValue(request) { const distanceKm = riderDestinationFilterPreferenceDistanceKm(request); return distanceKm == null ? Number.POSITIVE_INFINITY : distanceKm; } function requestMatchesRiderMarketplaceDestinationFilter(request) { if (activeRole() !== "rider") return true; const filter = riderMarketplaceDestinationFilter(); if (!riderMarketplaceDestinationFilterIsActive(filter)) return true; if (requestIsActiveForCurrentRider(request)) return true; const searchText = requestDestinationFilterSearchText(request); if (filter.country && request?.country !== filter.country && !searchText.includes(filter.country.toLowerCase())) return false; if (filter.city) { const cityNeedle = filter.city.toLowerCase(); const cityMatches = request?.city === filter.city || searchText.includes(cityNeedle) || String(request?.destinationFormattedAddress ?? request?.destination ?? "").toLowerCase().includes(cityNeedle) || addressTownLabel(request?.destinationFormattedAddress || request?.destination).toLowerCase().includes(cityNeedle); if (!cityMatches) return false; } if (filter.area) { const preferenceDistanceKm = riderDestinationFilterPreferenceDistanceKm(request, filter, searchText); if (preferenceDistanceKm == null || preferenceDistanceKm > riderDestinationFilterNeighborRadiusMiles / kmToMiles) return false; } if (filter.query && !searchText.includes(filter.query.toLowerCase())) return false; return true; } function requestMatchesRiderVehicle(request, rider = currentRiderRecord()) { if (!request || !rider) return false; const requestVehicle = normalizeRideVehicle(request.vehicle); const riderVehicle = normalizeRideVehicle(rider.vehicle); if (requestVehicle !== riderVehicle) return false; if (requestVehicle === "bike") return true; return riderCanServeCarTypePreference(rider, request.carTypePreference); } function isScheduledRequest(request) { return Boolean(request?.scheduledAt); } function scheduleChip(request) { return isScheduledRequest(request) ? uiText("scheduledAtChip", "Scheduled: {time}", { time: formatDateTime(request.scheduledAt) }) : uiText("immediateRide", "Immediate ride"); } function requestReopenedAfterRiderCancellation(request, previousRequest = null) { const wasMatchedToRider = previousRequest?.selectedRiderId && ["matched", "arrived"].includes(previousRequest.status); const looksLikeServerReopen = request?.status === "open" && request.releasedAt && !request.selectedOfferId && !request.cancelledAt && !request.cancelledBy; return Boolean(request && request.status === "open" && request.releasedAt && !request.selectedOfferId && (looksLikeServerReopen || (request.cancelledBy && request.cancelledBy !== request.passengerId) || wasMatchedToRider)); } function requestCancelledByMatchedRider(request) { if (requestCancelledByPassenger(request)) return false; const riderId = selectedRiderIdForRequest(request) ?? request?.cancellationFeeRiderId; return Boolean(request?.cancelledBy && riderId && request.cancelledBy === riderId); } function requestCancelledByCurrentRider(request) { if (!request) return false; if (requestCancelledByPassenger(request) && !riderInitiatedRideCancellationRequestIds.has(request.id)) return false; if (riderInitiatedRideCancellationRequestIds.has(request.id)) return true; const riderIds = [ state.rider?.id, state.rider?.supabaseUserId, selectedRiderIdForRequest(request), request?.cancellationFeeRiderId ].filter(Boolean).map(String); return Boolean(request.cancelledBy && riderIds.includes(String(request.cancelledBy))); } function requestCancelledByPassenger(request) { return Boolean(request?.cancelledBy && request?.passengerId && request.cancelledBy === request.passengerId); } function rideCancelledNoticeBodyForRider(request) { if (requestCancelledByPassenger(request)) { return uiText("passengerCancelledRideRemoved", "Passenger has canceled this ride request. It has been removed from your active marketplace."); } if (requestCancelledByCurrentRider(request)) { return uiText("youCancelledRideRemoved", "You cancelled this ride. It has been removed from your active trip list."); } if (requestCancelledByMatchedRider(request)) { return uiText("youCancelledRideRemoved", "You cancelled this ride. It has been removed from your active trip list."); } return uiText("rideCancelledRemoved", "This ride was cancelled. It has been removed from your active marketplace."); } function rideStatusLabel(request) { if (requestReopenedAfterRiderCancellation(request)) return uiText("reopened", "Reopened"); return { open: uiText("open", "Open"), matched: uiText("matched", "Matched"), arrived: uiText("riderArrived", "Rider arrived"), in_progress: uiText("rideInProgress", "Ride in progress"), completed: uiText("completed", "Completed"), cancelled: uiText("cancelled", "Cancelled") }[request?.status] ?? request?.status ?? uiText("unknown", "Unknown"); } function reopenedRequestChip(request) { return requestReopenedAfterRiderCancellation(request) ? uiText("riderCancelledReposted", "Rider cancelled; reposted to nearby riders") : null; } function proximityChip(request, rider = currentRiderRecord()) { if (activeRole() !== "rider") return null; if (selectedRiderIdForRequest(request) === rider?.id) return uiText("matchedToYou", "Matched to you"); const model = pickupProximityModel(request, rider); if (!model) return null; return uiText("pickupEtaChip", "Pickup ETA: {eta}", { eta: formatPickupEta(model.etaMinutes) }); } function offerDistanceChip(offer, request) { if (activeRole() !== "passenger") return null; const rider = stateLookupIndexes().riderMap.get(offer.riderId); if (Number.isFinite(Number(offer?.pickupDistanceMeters))) { const distanceKm = Number(offer.pickupDistanceMeters) / 1000; return uiText("riderPickupEtaChip", "Rider pickup ETA: {eta}", { eta: formatPickupEta(pickupEtaMinutes(distanceKm, rider)) }); } const model = pickupProximityModel(request, rider); if (!model) return null; return uiText("riderPickupEtaChip", "Rider pickup ETA: {eta}", { eta: formatPickupEta(model.etaMinutes) }); } function formatPickupDistanceKm(distanceKm, request) { if (distanceKm == null || !Number.isFinite(Number(distanceKm))) return "distance not estimated"; return request?.country === "United States" ? formatDistanceMiles(Number(distanceKm) * kmToMiles) : formatDistanceKm(Number(distanceKm)); } function formatRouteDistanceForRequest(distanceMiles, request) { if (distanceMiles == null || !Number.isFinite(Number(distanceMiles))) return "distance not estimated"; if (request?.country === "United States") return formatDistanceMiles(distanceMiles).replace(/ away$/, ""); return formatDistanceKm(Number(distanceMiles) / kmToMiles).replace(/ away$/, ""); } function destinationDriveChip(request) { const displayRequest = typeof riderVisibleRouteRequest === "function" ? riderVisibleRouteRequest(request) : request; return Number.isFinite(Number(displayRequest?.estimatedDistanceMiles)) ? uiText("destinationDriveChip", "Destination drive: {distance}", { distance: formatRouteDistanceForRequest(Number(displayRequest.estimatedDistanceMiles), displayRequest) }) : null; } function pickupDistanceSourceLabel(source) { return { postgis: "GPS/PostGIS", gps: "GPS", area_estimate: "area estimate" }[source] ?? source ?? "area estimate"; } function approachDistanceSourceIsLive(source) { return ["postgis", "gps", "gps/postgis"].includes(String(source ?? "").trim().toLowerCase()); } function riderApproachGps(request) { return normalizeGpsPoint(request?.riderApproachGps ?? { latitude: request?.riderApproachLatitude, longitude: request?.riderApproachLongitude, accuracyMeters: request?.riderApproachAccuracyMeters, capturedAt: request?.riderApproachCapturedAt }); } function selectedOfferForRequest(request) { if (!request) return null; const indexes = stateLookupIndexes(); return indexes.offerMap.get(request.selectedOfferId) ?? (indexes.offersByRequestId.get(request.id) ?? []).find((offer) => offer.riderId === selectedRiderIdForRequest(request)) ?? null; } function riderApproachModel(request) { const selectedRiderId = selectedRiderIdForRequest(request); if (!selectedRiderId) return null; const rider = stateLookupIndexes().riderMap.get(selectedRiderId); if (request.riderApproachIsLive && approachDistanceSourceIsLive(request.riderApproachSource) && Number.isFinite(Number(request.riderApproachDistanceMeters))) { const distanceKm = Number(request.riderApproachDistanceMeters) / 1000; return { source: pickupDistanceSourceLabel(request.riderApproachSource), distanceKm, etaMinutes: pickupEtaMinutes(distanceKm, rider), isLive: Boolean(request.riderApproachIsLive), riderGps: riderApproachGps(request), pickupGps: requestPickupGps(request), destinationGps: requestDestinationGps(request), capturedAt: request.riderApproachCapturedAt ?? null, accuracyMeters: request.riderApproachAccuracyMeters ?? null }; } const selectedOffer = selectedOfferForRequest(request); if (approachDistanceSourceIsLive(selectedOffer?.distanceSource) && Number.isFinite(Number(selectedOffer?.pickupDistanceMeters))) { const distanceKm = Number(selectedOffer.pickupDistanceMeters) / 1000; return { source: pickupDistanceSourceLabel(selectedOffer.distanceSource), distanceKm, etaMinutes: pickupEtaMinutes(distanceKm, rider), isLive: selectedOffer.distanceSource === "postgis" || selectedOffer.distanceSource === "gps", riderGps: riderApproachGps(request), pickupGps: requestPickupGps(request), destinationGps: requestDestinationGps(request), capturedAt: null, accuracyMeters: null }; } const model = pickupProximityModel(request, rider); if (!model) return null; if (!approachDistanceSourceIsLive(model.source)) return null; return { source: model.source, distanceKm: model.distanceKm, etaMinutes: model.etaMinutes, isLive: model.source === "GPS/PostGIS" || model.source === "GPS", riderGps: riderApproachGps(request), pickupGps: requestPickupGps(request), destinationGps: requestDestinationGps(request), capturedAt: null, accuracyMeters: null }; } function riderApproachChip(request) { if (activeRole() !== "passenger" || !selectedRiderIdForRequest(request)) return null; const model = riderApproachModel(request); if (!model) return "Rider approach: waiting for live update"; return `Rider approach: ${formatPickupEta(model.etaMinutes)}`; } function passengerHasTrackableRide() { return activeRole() === "passenger" && hasSignedIn("passenger") && state.requests.some((request) => requestBelongsToPassenger(request) && selectedRiderIdForRequest(request) && ["matched", "arrived", "in_progress"].includes(request.status)); } function passengerShouldAutoRefreshMarketplace() { return activeRole() === "passenger" && hasSignedIn("passenger") && Boolean(passengerPendingRide()); } function passengerMarketplaceRefreshDelayMs() { const request = passengerPendingRide(); if (!passengerShouldAutoRefreshMarketplace() || document.hidden || !request) return 0; if (request.status === "open") return marketplaceNegotiationRefreshIntervalMs; if (["matched", "arrived", "in_progress"].includes(request.status)) return passengerApproachRefreshIntervalMs; return riderMarketplaceRefreshIntervalMs; } function stopPassengerApproachAutoRefresh() { if (passengerApproachRefreshTimer == null) return; window.clearTimeout(passengerApproachRefreshTimer); passengerApproachRefreshTimer = null; passengerApproachRefreshTimerDelayMs = 0; } function ensurePassengerApproachAutoRefresh() { const delayMs = passengerMarketplaceRefreshDelayMs(); if (!delayMs) { stopPassengerApproachAutoRefresh(); return; } if (passengerApproachRefreshTimer != null && passengerApproachRefreshTimerDelayMs === delayMs) return; stopPassengerApproachAutoRefresh(); passengerApproachRefreshTimerDelayMs = delayMs; passengerApproachRefreshTimer = window.setTimeout(() => { passengerApproachRefreshTimer = null; passengerApproachRefreshTimerDelayMs = 0; if (!passengerShouldAutoRefreshMarketplace() || document.hidden) return; if (marketRefreshInFlight) { ensurePassengerApproachAutoRefresh(); return; } void refreshMarketplace({ silent: true, reason: "passenger_adaptive_poll" }) .finally(() => ensurePassengerApproachAutoRefresh()); }, delayMs); } function forcePassengerApproachRefreshNow(reason = "passenger_instant_approach") { if (activeRole() !== "passenger" || !hasSignedIn("passenger") || document.hidden) return false; if (!passengerPendingRide() && !passengerHasTrackableRide()) return false; const now = Date.now(); if (lastPassengerApproachImmediateRefreshAt && now - lastPassengerApproachImmediateRefreshAt < 5000) return false; lastPassengerApproachImmediateRefreshAt = now; stopPassengerApproachAutoRefresh(); if (marketRefreshInFlight) { marketplaceRealtimeRefreshPendingReason = reason; return true; } if (!canRefreshMarketplace()) return false; void refreshMarketplace({ silent: true, reason }); return true; } function riderShouldAutoRefreshMarketplace() { return activeRole() === "rider" && hasSignedIn("rider"); } function riderHasOpenOfferNegotiation(rider = currentRiderRecord()) { if (!rider) return false; const request = selectedRequest(); if (request?.status === "open" && roleCanSeeRequest(request)) return true; const requestMap = stateLookupIndexes().requestMap; return state.offers.some((offer) => riderIdentityMatches(offer.riderId, rider) && requestMap.get(offer.requestId)?.status === "open"); } function riderHasMatchedOrActiveRide(rider = currentRiderRecord()) { return Boolean(rider && state.requests.some((request) => requestIsActiveForCurrentRider(request, rider))); } function riderAvailabilityIsActivated() { return state.riderAvailabilityActivated === true; } function riderAvailabilityRequiredText() { return "Activate rider availability before reviewing ride requests or sending offers."; } function riderMarketplaceRefreshDelayMs() { const rider = currentRiderRecord(); if (!riderShouldAutoRefreshMarketplace() || document.hidden) return 0; if (!rider) return 0; if (riderHasOpenOfferNegotiation(rider)) return marketplaceNegotiationRefreshIntervalMs; if (riderHasMatchedOrActiveRide(rider)) return passengerApproachRefreshIntervalMs; if (!riderAvailabilityIsActivated()) return 0; if (!riderCanSeeRequests(rider)) return riderMarketplaceRefreshIntervalMs; return riderMarketplaceRefreshIntervalMs; } function stopRiderMarketplaceAutoRefresh() { if (riderMarketplaceRefreshTimer == null) return; window.clearTimeout(riderMarketplaceRefreshTimer); riderMarketplaceRefreshTimer = null; riderMarketplaceRefreshTimerDelayMs = 0; } function ensureRiderMarketplaceAutoRefresh() { const delayMs = riderMarketplaceRefreshDelayMs(); if (!delayMs) { stopRiderMarketplaceAutoRefresh(); return; } if (riderMarketplaceRefreshTimer != null && riderMarketplaceRefreshTimerDelayMs === delayMs) return; stopRiderMarketplaceAutoRefresh(); riderMarketplaceRefreshTimerDelayMs = delayMs; riderMarketplaceRefreshTimer = window.setTimeout(() => { riderMarketplaceRefreshTimer = null; riderMarketplaceRefreshTimerDelayMs = 0; if (!riderShouldAutoRefreshMarketplace() || document.hidden) return; if (marketRefreshInFlight) { ensureRiderMarketplaceAutoRefresh(); return; } void refreshMarketplace({ silent: true, reason: "rider_adaptive_poll" }) .finally(() => ensureRiderMarketplaceAutoRefresh()); }, delayMs); } function accountNoticeRefreshDelayMs(type) { if (document.hidden || activeRole() !== type || !hasSignedIn(type)) return 0; if (type === "passenger") { return passengerPendingRide() ? accountNotificationActiveRefreshIntervalMs : accountNotificationIdleRefreshIntervalMs; } if (type === "rider") { const rider = currentRiderRecord(); if (!rider) return 0; return riderAvailabilityIsActivated() || riderHasOpenOfferNegotiation(rider) || riderHasMatchedOrActiveRide(rider) ? accountNotificationActiveRefreshIntervalMs : 0; } return 0; } function stopAccountNoticeAutoRefresh(type) { const timer = accountNotificationAutoRefreshTimers[type]; if (timer == null) return; window.clearTimeout(timer); accountNotificationAutoRefreshTimers[type] = null; accountNotificationAutoRefreshTimerDelayMs[type] = 0; } function ensureAccountNoticeAutoRefresh(type) { const delayMs = accountNoticeRefreshDelayMs(type); if (!delayMs) { stopAccountNoticeAutoRefresh(type); return; } if (accountNotificationAutoRefreshTimers[type] != null && accountNotificationAutoRefreshTimerDelayMs[type] === delayMs) { return; } stopAccountNoticeAutoRefresh(type); accountNotificationAutoRefreshTimerDelayMs[type] = delayMs; accountNotificationAutoRefreshTimers[type] = window.setTimeout(() => { accountNotificationAutoRefreshTimers[type] = null; accountNotificationAutoRefreshTimerDelayMs[type] = 0; if (!accountNoticeRefreshDelayMs(type)) return; void refreshAccountNotificationsFromSupabase(type, { deliverPhone: true, refreshRide: true }).finally(() => ensureAccountNoticeAutoRefresh(type)); }, delayMs); } function ensureAccountNoticeAutoRefreshes() { ensureAccountNoticeAutoRefresh("passenger"); ensureAccountNoticeAutoRefresh("rider"); } function stopAccountNoticeAutoRefreshes() { stopAccountNoticeAutoRefresh("passenger"); stopAccountNoticeAutoRefresh("rider"); } function marketplaceVisibleResumeIsStale() { if (!lastMarketRefreshAt) return true; return Date.now() - lastMarketRefreshAt.getTime() >= marketplaceVisibleResumeRefreshStaleMs; } function resumeMarketplaceAutoRefreshAfterVisibilityChange() { if (document.hidden) { stopPassengerApproachAutoRefresh(); stopRiderMarketplaceAutoRefresh(); stopAccountNoticeAutoRefreshes(); return; } ensurePassengerApproachAutoRefresh(); ensureRiderMarketplaceAutoRefresh(); ensureAccountNoticeAutoRefreshes(); ensureMarketplaceRealtimeSubscription(); if (marketplaceRealtimeRefreshPendingReason && canRefreshMarketplace() && !marketRefreshInFlight) { const pendingReason = marketplaceRealtimeRefreshPendingReason; marketplaceRealtimeRefreshPendingReason = ""; scheduleMarketplaceRealtimeRefresh(pendingReason); } if (!canRefreshMarketplace() || marketRefreshInFlight || !marketplaceVisibleResumeIsStale()) return; void refreshMarketplace({ silent: true, reason: "visible_resume" }); } function marketplaceRealtimeUserSignature() { if (!hasSupabaseRuntime() || (!hasSignedIn("passenger") && !hasSignedIn("rider"))) return ""; return [ activeRole(), state.passenger?.id ?? "", state.rider?.id ?? "" ].join(":"); } function scheduleMarketplaceRealtimeRefresh(reason = "realtime") { if (document.hidden || marketRefreshInFlight) { marketplaceRealtimeRefreshPendingReason = reason; return; } if (marketplaceRealtimeRefreshTimer != null) { marketplaceRealtimeRefreshPendingReason = reason; return; } marketplaceRealtimeRefreshTimer = window.setTimeout(() => { marketplaceRealtimeRefreshTimer = null; const pendingReason = marketplaceRealtimeRefreshPendingReason || reason; marketplaceRealtimeRefreshPendingReason = ""; if (document.hidden || marketRefreshInFlight) { marketplaceRealtimeRefreshPendingReason = pendingReason; return; } if (!canRefreshMarketplace()) { marketplaceRealtimeRefreshPendingReason = pendingReason; return; } void refreshMarketplace({ silent: true, reason: pendingReason }); }, marketplaceRealtimeRefreshDebounceMs); } function forceMarketplaceRefreshSoon(reason = "forced") { if (document.hidden) { marketplaceRealtimeRefreshPendingReason = reason; return; } if (marketRefreshInFlight) { marketplaceRealtimeRefreshPendingReason = reason; return; } if (!canRefreshMarketplace()) return; void refreshMarketplace({ silent: true, reason }); } function clearMarketplaceRealtimeReconnectTimer() { if (marketplaceRealtimeReconnectTimer == null) return; window.clearTimeout(marketplaceRealtimeReconnectTimer); marketplaceRealtimeReconnectTimer = null; } function removeMarketplaceRealtimeChannel(channel = marketplaceRealtimeChannel) { if (!channel || !supabaseClient?.removeChannel) return; try { supabaseClient.removeChannel(channel); } catch (error) { logClientWarning("Marketplace realtime channel could not be removed.", error); } } function scheduleMarketplaceRealtimeReconnect(status = "reconnect", expectedSignature = marketplaceRealtimeSignature) { if (marketplaceRealtimeReconnectTimer != null) return; const reconnectSignature = expectedSignature; marketplaceRealtimeReconnectTimer = window.setTimeout(() => { marketplaceRealtimeReconnectTimer = null; if (!reconnectSignature || reconnectSignature !== marketplaceRealtimeSignature || reconnectSignature !== marketplaceRealtimeUserSignature()) { stopMarketplaceRealtimeSubscription(); ensureMarketplaceRealtimeSubscription(); return; } const channel = marketplaceRealtimeChannel; marketplaceRealtimeChannel = null; marketplaceRealtimeSignature = ""; removeMarketplaceRealtimeChannel(channel); ensureMarketplaceRealtimeSubscription(); scheduleMarketplaceRealtimeRefresh(`realtime_${status.toLowerCase()}`); }, marketplaceRealtimeReconnectDelayMs); } function stopMarketplaceRealtimeSubscription() { if (marketplaceRealtimeRefreshTimer != null) { window.clearTimeout(marketplaceRealtimeRefreshTimer); marketplaceRealtimeRefreshTimer = null; } clearMarketplaceRealtimeReconnectTimer(); marketplaceRealtimeRefreshPendingReason = ""; removeMarketplaceRealtimeChannel(); marketplaceRealtimeChannel = null; marketplaceRealtimeSignature = ""; } function ensureMarketplaceRealtimeSubscription() { const signature = marketplaceRealtimeUserSignature(); if (!signature || !supabaseClient?.channel || !platformFeatureEnabled("marketplace_realtime_enabled")) { stopMarketplaceRealtimeSubscription(); return; } void loadPlatformFeatureFlagsFromSupabase().then(() => { if (!platformFeatureEnabled("marketplace_realtime_enabled")) stopMarketplaceRealtimeSubscription(); }); if (marketplaceRealtimeChannel && marketplaceRealtimeSignature === signature) return; stopMarketplaceRealtimeSubscription(); marketplaceRealtimeSignature = signature; const channelSignature = signature; marketplaceRealtimeChannel = supabaseClient .channel(`waka-marketplace-${signature}-${Date.now()}`) .on("postgres_changes", { event: "*", schema: "public", table: "marketplace_refresh_events" }, () => scheduleMarketplaceRealtimeRefresh("marketplace_refresh_events")) .on("postgres_changes", { event: "*", schema: "public", table: "ride_offers" }, () => scheduleMarketplaceRealtimeRefresh("ride_offers")) .on("postgres_changes", { event: "*", schema: "public", table: "ride_chats" }, () => scheduleMarketplaceRealtimeRefresh("ride_chats")) .on("postgres_changes", { event: "*", schema: "public", table: "ride_route_changes" }, () => scheduleMarketplaceRealtimeRefresh("ride_route_changes")) .on("postgres_changes", { event: "*", schema: "public", table: "admin_notifications" }, () => scheduleMarketplaceRealtimeRefresh("admin_notifications")) .subscribe((status) => { if (channelSignature !== marketplaceRealtimeSignature) return; if (status === "SUBSCRIBED") { clearMarketplaceRealtimeReconnectTimer(); scheduleMarketplaceRealtimeRefresh("realtime_subscribed"); return; } if (["CHANNEL_ERROR", "TIMED_OUT", "CLOSED"].includes(status)) { logClientWarning(`Marketplace realtime subscription status: ${status}`); scheduleMarketplaceRealtimeReconnect(status, channelSignature); } }); } function mapsCoordinate(point) { const gps = normalizeGpsPoint(point); return gps ? `${gps.latitude},${gps.longitude}` : null; } function compactLocationQuery(parts) { return parts .map((part) => String(part ?? "").trim()) .filter(Boolean) .join(", "); } function pickupMapsDestination(request) { return mapsCoordinate(requestPickupGps(request)) ?? compactLocationQuery([request?.pickupDescription, request?.pickupArea, request?.city, request?.country]); } function pickupAddressLooksPreciseForNavigation(value) { const text = String(value ?? "").trim(); if (!text || pickupUsesCurrentLocationText(text) || pickupUsesGpsFallbackText(text)) return false; if (/^-?\d{1,3}(?:\.\d+)?,\s*-?\d{1,3}(?:\.\d+)?$/.test(text)) return true; const streetToken = /\b(?:road|rd|street|st|avenue|ave|drive|dr|lane|ln|boulevard|blvd|court|ct|circle|cir|highway|hwy|way|place|pl|terrace|ter|pike|parkway|pkwy|route|rte)\b/i.test(text); return /\d/.test(text) && (streetToken || text.includes(",")); } function requestHasPrecisePickupNavigation(request) { if (!request) return false; if (requestPickupGps(request)) return true; return pickupAddressLooksPreciseForNavigation(request.pickupFormattedAddress) || pickupAddressLooksPreciseForNavigation(request.pickupDescription); } function riderPickupNavigationClarificationMessage(request) { return matchingText( "riderPickupNavigationClarifyPopup", "Pickup GPS is not clear enough for reliable navigation to {pickup}. Use Contact/Open chat to clarify the exact pickup landmark with the passenger, then continue using the typed pickup address shown on this ride.", { pickup: requestPickupDisplayText(request, "the pickup point") } ); } function showRiderPickupNavigationClarification(request) { const message = riderPickupNavigationClarificationMessage(request); if (activeRole() === "rider" && els.offerRequestContext) { els.offerRequestContext.textContent = matchingText( "riderPickupNavigationClarifyStatus", "Ride accepted. Pickup GPS is not clear enough for navigation. Use Contact/Open chat to clarify the pickup landmark with the passenger.", { pickup: requestPickupDisplayText(request, "the pickup point") } ); } if (typeof showWakaGoodAlert === "function") { void showWakaGoodAlert(message); return true; } if (typeof alert === "function") alert(message); return true; } function destinationMapsQuery(request) { return mapsCoordinate({ latitude: request?.destinationLatitude, longitude: request?.destinationLongitude }) ?? compactLocationQuery([ request?.destinationFormattedAddress || request?.destination, request?.destinationArea, request?.city, request?.country ]); } function rideStopIndex(request) { const stops = normalizeRideStops(request?.rideStops); return Math.min(stops.length, Math.max(0, Number(request?.currentStopIndex ?? 0) || 0)); } function nextRideLeg(request) { const stops = normalizeRideStops(request?.rideStops); const index = rideStopIndex(request); if (!["arrived", "in_progress"].includes(request?.status)) { return { type: "pickup", label: "Pickup", destination: pickupMapsDestination(request), remainingStops: stops.length }; } if (index < stops.length) { return { type: "stop", index, label: `Stop ${index + 1}`, destination: stops[index], remainingStops: stops.length - index }; } return { type: "destination", label: "Destination", destination: destinationMapsQuery(request), remainingStops: 0 }; } function googleMapsSearchUrl(query) { if (!query) return ""; return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(query)}`; } const wakaAndroidRuntimeStorageKey = "waka-android-runtime-context-v1"; function wakaAndroidPackageFromUrl() { return new URLSearchParams(window.location.search).get("androidPackage") || ""; } function rememberWakaAndroidPackage(value = wakaAndroidPackageFromUrl()) { const normalized = String(value || "").trim(); if (!normalized) return ""; try { sessionStorage.setItem(wakaAndroidRuntimeStorageKey, normalized); localStorage.setItem(wakaAndroidRuntimeStorageKey, normalized); } catch { // Android WebView storage can be unavailable during early startup; URL detection still covers this page load. } return normalized; } function rememberedWakaAndroidPackage() { try { return sessionStorage.getItem(wakaAndroidRuntimeStorageKey) || localStorage.getItem(wakaAndroidRuntimeStorageKey) || ""; } catch { return ""; } } function currentWakaAndroidPackage() { return rememberWakaAndroidPackage(wakaAndroidPackageFromUrl()) || rememberedWakaAndroidPackage(); } function isAndroidRuntime() { const params = new URLSearchParams(window.location.search); return params.has("androidAppVersion") || Boolean(currentWakaAndroidPackage()) || /Android/i.test(navigator.userAgent || ""); } function isWakaAndroidAppRuntime() { const params = new URLSearchParams(window.location.search); return Boolean(currentWakaAndroidPackage() || params.has("androidAppVersion") || window.WakaAndroid || wakaAndroidBridge()); } function wakaAndroidNavigationBridgeAvailable() { const bridge = wakaAndroidBridge(); return Boolean(bridge && typeof bridge.openNavigation === "function"); } function wakaAndroidNavigationUrl(provider, destination, origin = null, waypoints = []) { if (!destination || !wakaAndroidNavigationBridgeAvailable()) return ""; const params = new URLSearchParams({ provider: normalizeRiderNavigationPreference(provider), destination }); if (origin) params.set("origin", origin); const cleanWaypoints = normalizeRideStops(waypoints); if (cleanWaypoints.length) params.set("waypoints", cleanWaypoints.join("|")); return `waka-nav://navigate?${params.toString()}`; } function androidPackageIntentUrl(fallbackUrl, packageName) { if (!fallbackUrl || !packageName) return fallbackUrl || ""; try { const parsed = new URL(fallbackUrl); const target = `${parsed.host}${parsed.pathname}${parsed.search}${parsed.hash}`; const scheme = parsed.protocol.replace(":", ""); return `intent://${target}#Intent;scheme=${scheme};package=${packageName};S.browser_fallback_url=${encodeURIComponent(fallbackUrl)};end`; } catch { return fallbackUrl; } } function googleMapsWebDirectionsUrl(destination, origin = null, waypoints = []) { if (!destination) return ""; const params = new URLSearchParams({ api: "1", destination, travelmode: "driving", dir_action: "navigate" }); if (origin) params.set("origin", origin); const cleanWaypoints = normalizeRideStops(waypoints); if (cleanWaypoints.length) params.set("waypoints", cleanWaypoints.join("|")); return `https://www.google.com/maps/dir/?${params.toString()}`; } function googleMapsAndroidIntentUrl(destination, origin = null, waypoints = []) { const fallbackUrl = googleMapsWebDirectionsUrl(destination, origin, waypoints); return androidPackageIntentUrl(fallbackUrl, "com.google.android.apps.maps"); } function googleMapsTurnByTurnUrl(destination) { if (!destination) return ""; if (wakaAndroidNavigationBridgeAvailable()) return wakaAndroidNavigationUrl("google_maps", destination); if (isAndroidRuntime()) return googleMapsAndroidIntentUrl(destination); return googleMapsWebDirectionsUrl(destination); } function googleMapsDirectionsUrl(destination, origin = null, waypoints = []) { if (!destination) return ""; if (wakaAndroidNavigationBridgeAvailable()) return wakaAndroidNavigationUrl("google_maps", destination, origin, waypoints); if (isAndroidRuntime()) return googleMapsAndroidIntentUrl(destination, origin, waypoints); return googleMapsWebDirectionsUrl(destination, origin, waypoints); } function riderPickupNavigationUrl(request, rider = currentRiderRecord()) { const destination = pickupMapsDestination(request); if (riderNavigationPreference(rider) === "waze") return wazeNavigationUrl(destination); return googleMapsDirectionsUrl(destination); } function wazeNavigationUrl(destination) { if (wakaAndroidNavigationBridgeAvailable()) return wakaAndroidNavigationUrl("waze", destination); return wazeExternalNavigationUrl(destination); } function wazeExternalNavigationUrl(destination) { if (!destination) return ""; const normalized = String(destination).replace(/\s+/g, ""); const coordinatePattern = /^-?\d+(?:\.\d+)?,-?\d+(?:\.\d+)?$/; const params = new URLSearchParams({ navigate: "yes" }); if (coordinatePattern.test(normalized)) { params.set("ll", normalized); } else { params.set("q", destination); } const query = params.toString(); const fallbackUrl = `https://waze.com/ul?${query}`; if (isAndroidRuntime()) return androidPackageIntentUrl(fallbackUrl, "com.waze"); return fallbackUrl; } function wakaAndroidBridge() { return window.WakaAndroid || null; } function wakaAndroidNavigationUrlFromExternalUrl(url) { if (!wakaAndroidNavigationBridgeAvailable()) return ""; if (!url || /^waka-nav:/i.test(url)) return url || ""; try { if (/^google\.navigation:/i.test(url)) { const rawQuery = String(url).replace(/^google\.navigation:/i, ""); const params = new URLSearchParams(rawQuery); return wakaAndroidNavigationUrl("google_maps", params.get("q") || ""); } const parsed = new URL(url); if (/waze\.com$/i.test(parsed.hostname) && parsed.pathname.startsWith("/ul")) { return wakaAndroidNavigationUrl("waze", parsed.searchParams.get("q") || parsed.searchParams.get("ll") || ""); } if (/google\.com$/i.test(parsed.hostname) && parsed.pathname.startsWith("/maps/")) { return wakaAndroidNavigationUrl( "google_maps", parsed.searchParams.get("destination") || parsed.searchParams.get("query") || "" ); } } catch { return ""; } return ""; } function externalNavigationUrlFromWakaAndroidUrl(url) { if (!url || !/^waka-nav:/i.test(url)) return ""; try { const parsed = new URL(url); const provider = normalizeRiderNavigationPreference(parsed.searchParams.get("provider")); const destination = parsed.searchParams.get("destination") || ""; const origin = parsed.searchParams.get("origin") || null; const waypoints = normalizeRideStops((parsed.searchParams.get("waypoints") || "").split("|")); if (provider === "waze") return wazeExternalNavigationUrl(destination); return isAndroidRuntime() ? googleMapsAndroidIntentUrl(destination, origin, waypoints) : googleMapsWebDirectionsUrl(destination, origin, waypoints); } catch { return ""; } } function openWakaAndroidNavigationUrl(url) { if (!url) return false; try { const parsed = new URL(url); if (parsed.protocol !== "waka-nav:") return false; const bridge = wakaAndroidBridge(); if (!bridge || typeof bridge.openNavigation !== "function") return false; bridge.openNavigation( parsed.searchParams.get("provider") || "google_maps", parsed.searchParams.get("destination") || "", parsed.searchParams.get("origin") || "", parsed.searchParams.get("waypoints") || "" ); return true; } catch (error) { logClientWarning("Android navigation bridge failed.", error); return false; } } function riderPickupWazeUrl(request) { return wazeNavigationUrl(pickupMapsDestination(request)); } function pickupMapUrl(request) { return googleMapsSearchUrl(pickupMapsDestination(request)); } function destinationMapUrl(request, origin = null) { return googleMapsDirectionsUrl(destinationMapsQuery(request), origin, request?.rideStops); } function nextRideLegNavigationUrl(request, rider = currentRiderRecord()) { const leg = nextRideLeg(request); if (!leg.destination) return ""; if (riderNavigationPreference(rider) === "waze") return wazeNavigationUrl(leg.destination); return googleMapsDirectionsUrl(leg.destination); } function riderDestinationNavigationUrl(request, rider = currentRiderRecord()) { const destination = destinationMapsQuery(request); if (riderNavigationPreference(rider) === "waze") return wazeNavigationUrl(destination); return destinationMapUrl(request, pickupMapsDestination(request)); } function navigationUrlCanOpenSameWindow(url) { return /^waka-nav:/i.test(url) || /^intent:/i.test(url) || /^google\.navigation:/i.test(url) || /^https:\/\/(?:www\.)?google\.com\/maps\/dir\//i.test(url) || /^waze:/i.test(url) || /^https:\/\/waze\.com\/ul/i.test(url); } function openNavigationUrl(url, { auto = false } = {}) { if (!url) return false; try { const wakaUrl = wakaAndroidNavigationUrlFromExternalUrl(url); if (wakaUrl && openWakaAndroidNavigationUrl(wakaUrl)) return true; const externalWakaUrl = externalNavigationUrlFromWakaAndroidUrl(url); if (externalWakaUrl) { if (auto && navigationUrlCanOpenSameWindow(externalWakaUrl)) { window.location.assign(externalWakaUrl); return true; } if (!auto) { const opened = window.open(externalWakaUrl, "_blank", "noopener,noreferrer"); return Boolean(opened); } return false; } if (/^waka-nav:/i.test(url)) { if (isWakaAndroidAppRuntime()) { window.location.assign(url); return true; } return false; } if (auto && navigationUrlCanOpenSameWindow(url)) { window.location.assign(url); return true; } if (auto) return false; const opened = window.open(url, "_blank", "noopener,noreferrer"); if (opened) return true; if (!auto || navigationUrlCanOpenSameWindow(url)) { window.location.assign(url); return true; } } catch (error) { logClientWarning("Navigation app could not open.", error); } return false; } function offerPickupDistanceSnapshot(request, rider) { const model = pickupProximityModel(request, rider); if (!model) return {}; const source = model.source === "GPS/PostGIS" ? "postgis" : model.source === "GPS" ? "gps" : "area_estimate"; return { pickupDistanceMeters: Math.round(model.distanceKm * 1000), distanceSource: source }; } function confirmationChip(request) { if (!isScheduledRequest(request)) return null; const status = request.riderConfirmationStatus; if (status === "requested") return "Rider confirmation requested"; if (status === "confirmed") return `Rider confirmed ${formatDateTime(request.riderConfirmedAt)}`; if (status === "declined") return "Rider cannot keep plan"; if (status === "released") return "Rider released"; return request.status === "matched" ? "Confirmation not requested" : "Awaiting rider selection"; } function riderBaseReadyForRequests(rider = currentRiderRecord()) { return Boolean(rider && hasSignedIn("rider") && rider.status === "approved" && isSubscriptionActive(rider) && riderComplianceReady(rider) && paymentAccountReady("rider", rider)); } function riderCanSeeRequests(rider = currentRiderRecord()) { return Boolean(riderAvailabilityIsActivated() && riderBaseReadyForRequests(rider) && riderCurrentFreshGps(rider)); } function roleCanSeeRequest(request) { if (!request) return false; if (activeRole() === "passenger") { return requestBelongsToPassenger(request); } if (activeRole() === "rider") { const rider = currentRiderRecord(); const ownMatchedRequest = requestHasRiderMatch(request); const ownActiveRequest = ownMatchedRequest && ["matched", "arrived", "in_progress"].includes(request.status); if (ownActiveRequest && riderPrePickupCancellationClearedForCurrentRider(request, rider)) return false; if (ownActiveRequest) return true; if (!riderCanSeeRequests(rider) || request.status !== "open") return false; if (riderRequestDismissedByCurrentRider(request, rider)) return false; const vehicleMatches = requestMatchesRiderVehicle(request, rider); const gpsDistanceKm = gpsDistanceKmForRequest(request, rider); const isNearEnough = gpsDistanceKm != null ? riderWithinGpsProximity(request, rider) : riderWithinRequestProximity(request, rider); const destinationAllowed = requestDestinationMatchesDailyRegions(request, rider); const notBusyElsewhere = riderCanReviewAnotherImmediateRequest(request, rider); return vehicleMatches && isNearEnough && destinationAllowed && notBusyElsewhere; } return false; } function visibleRequestsForRole() { const { country, city } = activeMarketLocation(); return state.requests .filter((item) => ( activeRole() === "rider" && requestIsActiveForCurrentRider(item) ) || (item.country === country && item.city === city)) .filter((item) => activeRole() !== "passenger" || passengerRideRequestVisibleInActiveBoard(item)) .filter(requestMatchesVehicleFilter) .filter(roleCanSeeRequest) .filter(requestMatchesRiderMarketplaceDestinationFilter) .sort((a, b) => { if (activeRole() === "rider") { const rider = currentRiderRecord(); const aIsOwnActive = requestHasRiderMatch(a) && ["matched", "arrived", "in_progress"].includes(a.status) ? 0 : 1; const bIsOwnActive = requestHasRiderMatch(b) && ["matched", "arrived", "in_progress"].includes(b.status) ? 0 : 1; if (aIsOwnActive !== bIsOwnActive) return aIsOwnActive - bIsOwnActive; const aDestinationPreference = riderDestinationFilterSortValue(a); const bDestinationPreference = riderDestinationFilterSortValue(b); if (aDestinationPreference !== bDestinationPreference) return aDestinationPreference - bDestinationPreference; const aPickupEta = pickupProximitySortValue(a, rider); const bPickupEta = pickupProximitySortValue(b, rider); if (aPickupEta !== bPickupEta) return aPickupEta - bPickupEta; const fareDifference = Number(b.fareOffer ?? 0) - Number(a.fareOffer ?? 0); if (fareDifference !== 0) return fareDifference; } return new Date(b.createdAt) - new Date(a.createdAt); }); } function riderAlertKey(request, rider = currentRiderRecord()) { return [rider?.id, request?.id, request?.status, Number(request?.fareOffer ?? 0)].filter(Boolean).join(":"); } function riderVisibleAlertedKeys() { return new Set(Array.isArray(state.riderNearbyRequestAlertedKeys) ? state.riderNearbyRequestAlertedKeys : []); } function riderDismissedRequestKey(request, rider = currentRiderRecord()) { if (!request?.id || !rider?.id) return ""; return [rider.id, request.id, String(Number(request.fareOffer ?? 0))].join(":"); } function riderDismissedRequestKeys() { return new Set(Array.isArray(state.riderDismissedRequestKeys) ? state.riderDismissedRequestKeys : []); } function riderLocalStateIdentity(rider = currentRiderRecord()) { return rider?.id ?? rider?.supabaseUserId ?? state.sessions?.rider?.userId ?? ""; } function riderPrePickupCancellationClearKey(request, rider = currentRiderRecord()) { const riderId = riderLocalStateIdentity(rider); if (!request?.id || !riderId) return ""; return `${riderId}:${request.id}`; } function riderClearedPrePickupCancellationKeys() { return new Set(Array.isArray(state.riderClearedPrePickupCancellationKeys) ? state.riderClearedPrePickupCancellationKeys : []); } function riderPrePickupCancellationClearedForCurrentRider(request, rider = currentRiderRecord()) { const key = riderPrePickupCancellationClearKey(request, rider); return Boolean(key && riderClearedPrePickupCancellationKeys().has(key)); } function rememberRiderPrePickupCancellationClear(request, rider = currentRiderRecord()) { const key = riderPrePickupCancellationClearKey(request, rider); if (!key) return; state.riderClearedPrePickupCancellationKeys = [...riderClearedPrePickupCancellationKeys(), key].slice(-100); } function riderRequestDismissedByCurrentRider(request, rider = currentRiderRecord()) { const key = riderDismissedRequestKey(request, rider); return Boolean(key && riderDismissedRequestKeys().has(key)); } function riderRequestReopenedForCurrentRider(request, rider = currentRiderRecord()) { if (!request?.id || !rider?.id || request.status !== "open") return false; const currentFare = Number(request.fareOffer ?? 0); const prefix = `${rider.id}:${request.id}:`; return [...riderDismissedRequestKeys()].some((key) => { if (!key.startsWith(prefix)) return false; const dismissedFare = Number(key.slice(prefix.length)); return Number.isFinite(dismissedFare) && dismissedFare < currentFare; }); } function riderReopenedFareChip(request, rider = currentRiderRecord()) { return riderRequestReopenedForCurrentRider(request, rider) ? `Reopened at ${formatMoney(request.fareOffer, request.country)}` : null; } function rememberRiderDismissedRequest(request, rider = currentRiderRecord()) { const key = riderDismissedRequestKey(request, rider); if (!key) return; state.riderDismissedRequestKeys = [...riderDismissedRequestKeys(), key].slice(-300); } function riderHasAlreadyHandledMarketplaceRequest(request, rider = currentRiderRecord()) { if (!request?.id || !rider?.id) return false; if (riderRequestDismissedByCurrentRider(request, rider)) return true; if (riderRequestReopenedForCurrentRider(request, rider)) return true; return state.offers.some((offer) => offer.requestId === request.id && offer.riderId === rider.id); } function rememberRiderNearbyAlert(request, rider = currentRiderRecord()) { const key = riderAlertKey(request, rider); if (!key) return; state.riderNearbyRequestAlertedKeys = [...riderVisibleAlertedKeys(), key].slice(-200); saveState(); } function unreadRiderNearbyRequests(requests = visibleRequestsForRole()) { if (activeRole() !== "rider") return []; const rider = currentRiderRecord(); if (!riderCanSeeRequests(rider)) return []; const alerted = riderVisibleAlertedKeys(); return requests .filter((request) => request?.status === "open") .filter((request) => !riderHasAlreadyHandledMarketplaceRequest(request, rider)) .filter((request) => !alerted.has(riderAlertKey(request, rider))); } function nearbyRideAlertSummary(request) { const fare = formatMoney(request?.fareOffer, request?.country); const distance = proximityChip(request) ?? "pickup distance pending"; const stops = normalizeRideStops(request?.rideStops); const stopText = stops.length ? `, ${stops.length} stop${stops.length === 1 ? "" : "s"}` : ""; return `New ride nearby: ${fare}, ${distance}${stopText}.`; } const wakaNotificationVibrationPattern = [120, 60, 120, 60, 180]; function playNearbyRideCue() { try { if (navigator.vibrate) navigator.vibrate(wakaNotificationVibrationPattern); } catch (error) { logClientWarning("Nearby ride vibration cue was not available.", error); } } function workspaceNotificationUrl(role, page, requestId = "") { const workspaceRole = role === "rider" ? "rider" : "passenger"; const workspacePage = page || (workspaceRole === "rider" ? "requests" : "trips"); const url = new URL(`/${workspaceRole}`, window.location.origin); url.searchParams.set(workspaceRole === "rider" ? "riderPage" : "passengerPage", workspacePage); if (requestId) url.searchParams.set("requestId", requestId); return url.toString(); } const notificationPreferenceOptions = [ { key: "all", label: "Allow notifications" }, { key: "ride", label: "Ride updates" }, { key: "chat", label: "Messages" }, { key: "fare", label: "Fare and offers" }, { key: "admin", label: "Waka notices" } ]; function notificationPreferenceType(notice = {}) { const text = `${notice.eventType || ""} ${notice.id || ""} ${notice.title || ""} ${notice.body || ""}`.toLowerCase(); if (/chat|message|ride_chat_message/.test(text)) return "chat"; if (/fare|offer|counter|boost|increased|rider_counter_offer|passenger_fare_increased/.test(text)) return "fare"; if (!notice.requestId || /admin|broadcast|notice|support|approval|correction|checkr|eligibility/.test(text)) return "admin"; return "ride"; } function notificationPreferenceEnabled(role, key) { const type = role === "rider" ? "rider" : "passenger"; const normalizedKey = notificationPreferenceOptions.some((option) => option.key === key) ? key : "ride"; const preferences = state.notificationPreferences?.[type] ?? normalizeNotificationPreferenceSet(); if (normalizedKey === "all") return preferences.all !== false; if (preferences.all === false) return false; return preferences[normalizedKey] !== false; } function setNotificationPreference(role, key, enabled) { const type = role === "rider" ? "rider" : "passenger"; if (!notificationPreferenceOptions.some((option) => option.key === key)) return; state.notificationPreferences ||= normalizeNotificationPreferences(); state.notificationPreferences[type] ||= normalizeNotificationPreferenceSet(); state.notificationPreferences[type][key] = enabled !== false; saveState(); renderAll(); } function noticeDeliveryAllowedByPreference(role, notice) { return notificationPreferenceEnabled(role, notificationPreferenceType(notice)); } const queuedPhoneDeliveryProcessKeys = new Set(); const phoneDeliveryProcessableRideEvents = new Set([ "passenger_fare_increased", "rider_counter_offer", "passenger_rejected_offer", "rider_offer_withdrawn", "ride_matched", "ride_reopened", "ride_cancelled", "ride_chat_message", "ride_arrived", "ride_started", "ride_stop_arrived", "ride_completed", "route_change_requested", "route_change_accepted", "route_change_declined", "scheduled_ride_reminder", "scheduled_ride_confirmation_needed" ]); function processQueuedPhoneDeliveryForNotice(role, notice) { if (!notice?.requestId || !hasSupabaseRuntime() || typeof processRideRequestPushDelivery !== "function") return false; if (!noticeDeliveryAllowedByPreference(role, notice)) return false; const eventType = noticePopupSemanticEvent(notice); if (!phoneDeliveryProcessableRideEvents.has(eventType)) return false; const key = [role, notice.requestId, eventType, notice.id || notice.createdAt || ""].join(":"); if (queuedPhoneDeliveryProcessKeys.has(key)) return false; queuedPhoneDeliveryProcessKeys.add(key); Promise.resolve(processRideRequestPushDelivery(notice.requestId, { eventTypes: [eventType] })) .catch((error) => logClientWarning("Queued phone notification delivery could not be processed.", error)) .finally(() => { window.setTimeout(() => queuedPhoneDeliveryProcessKeys.delete(key), 15000); }); return true; } async function showRideTransactionPhoneNotification({ role = "rider", page = "", request = null, requestId = "", title = "Waka update", body = "", tag = "", cue = true } = {}) { const resolvedRequestId = requestId || request?.id || ""; const workspaceRole = role === "passenger" ? "passenger" : "rider"; if (!notificationPreferenceEnabled(workspaceRole, notificationPreferenceType({ id: tag, title, body, requestId: resolvedRequestId, eventType: tag }))) return; if (cue) playNearbyRideCue(); if (typeof Notification === "undefined" || Notification.permission !== "granted") return; const notificationUrl = workspaceNotificationUrl(workspaceRole, page, resolvedRequestId); const displayTitle = wakaGoodDialogBrand; const displayBody = title && title !== displayTitle ? `${title}${body ? `\n${body}` : ""}` : body; const options = { body: displayBody, icon: "./icons/icon-192.png", badge: "./icons/icon-192.png", tag: tag || `waka-${workspaceRole}-ride-${resolvedRequestId || Date.now()}`, renotify: true, silent: false, vibrate: wakaNotificationVibrationPattern, data: { url: notificationUrl, requestId: resolvedRequestId, role: workspaceRole } }; try { const registration = "serviceWorker" in navigator ? await navigator.serviceWorker.ready : null; if (registration?.showNotification) { await registration.showNotification(displayTitle, options); return; } const notice = new Notification(displayTitle, options); notice.onclick = () => { window.focus(); window.location.assign(notificationUrl); }; } catch (error) { logClientWarning("Ride transaction phone notification was not available.", error); } } async function showRiderNearbyPhoneNotification(request) { if (!request) return; const title = "New nearby ride request"; const body = nearbyRideAlertSummary(request); await showRideTransactionPhoneNotification({ role: "rider", page: "requests", request, title, body, tag: `waka-ride-request-${request.id}` }); } function dismissRiderNearbyAlert(request) { rememberRiderNearbyAlert(request); const panel = document.querySelector(".nearby-ride-alert"); if (panel) panel.remove(); riderNearbyAlertActiveId = null; } function focusRiderRequestView(request, { refresh = true, replace = true } = {}) { if (!request) return; document.querySelector(".nearby-ride-alert")?.remove(); clearRiderDecisionQueueForRequest(request.id); clearRequestMarketplaceFareChange(request.id); state.activeTab = "rider"; state.showRoleEntry = false; state.riderPage = "requests"; state.selectedRequestId = request.id; riderNearbyAlertActiveId = null; if (typeof updateRiderWorkspaceRoute === "function") updateRiderWorkspaceRoute("requests", { replace, requestId: request.id }); saveState(); renderAll(); window.setTimeout(() => { document.querySelector("#riderRequestDetailPanel, #marketPanel") ?.scrollIntoView({ behavior: "smooth", block: "start" }); }, 0); if (refresh) void refreshMarketplace({ silent: true }); } function openRiderMarketplaceView({ refresh = true, replace = true } = {}) { state.activeTab = "rider"; state.showRoleEntry = false; state.riderPage = "requests"; state.selectedRequestId = null; riderNearbyAlertActiveId = null; if (typeof updateRiderWorkspaceRoute === "function") updateRiderWorkspaceRoute("requests", { replace, requestId: "" }); saveState(); renderAll(); window.setTimeout(() => { document.querySelector("#requestsBoard, #marketPanel") ?.scrollIntoView({ behavior: "smooth", block: "start" }); }, 0); if (refresh) void refreshMarketplace({ silent: true }); } function focusPassengerRequestView(request, { refresh = true, replace = true } = {}) { if (!request) return; state.activeTab = "passenger"; state.showRoleEntry = false; state.passengerPage = "trips"; state.selectedRequestId = request.id; if (typeof updatePassengerWorkspaceRoute === "function") updatePassengerWorkspaceRoute("trips", { replace, requestId: request.id }); saveState(); renderAll(); window.setTimeout(() => { document.querySelector("#requestList .market-card.selected, #marketPanel") ?.scrollIntoView({ behavior: "smooth", block: "start" }); }, 0); if (refresh) void refreshMarketplace({ silent: true }); } function focusRideNoticeView(type, request, { refresh = false, replace = true } = {}) { if (!request) return false; if (type === "passenger") { if (!requestBelongsToPassenger(request)) return false; focusPassengerRequestView(request, { refresh, replace }); return true; } if (type === "rider") { if (!state.rider?.id) return false; focusRiderRequestView(request, { refresh, replace }); return true; } return false; } function riderWorkloadMode() { return ["normal", "focus"].includes(state.riderWorkloadMode) ? state.riderWorkloadMode : "normal"; } function setRiderWorkloadMode(mode) { state.riderWorkloadMode = mode === "focus" ? "focus" : "normal"; saveState(); renderAll(); } function riderFocusModeActive() { return riderWorkloadMode() === "focus"; } function riderDecisionQueueItems() { const riderId = state.rider?.id; const requestMap = stateLookupIndexes().requestMap; return (state.riderDecisionQueue ?? []) .filter((item) => item?.requestId && (!item.riderId || item.riderId === riderId)) .filter((item) => requestMap.has(item.requestId)) .sort((a, b) => new Date(b.createdAt ?? 0) - new Date(a.createdAt ?? 0)) .slice(0, 8); } function riderDecisionQueueKey(request, eventType = "ride_update") { return [ "rider-decision", state.rider?.id, request?.id, eventType, Number(request?.fareOffer ?? 0) ].filter(Boolean).join(":"); } function queueRiderDecisionUpdate(title, body, requestId, eventType = "ride_update") { if (!state.rider?.id || !requestId) return null; const request = stateLookupIndexes().requestMap.get(requestId) ?? null; const item = { id: riderDecisionQueueKey(request ?? { id: requestId }, eventType), riderId: state.rider.id, requestId, title, body, eventType, fareOffer: request ? Number(request.fareOffer ?? 0) : null, createdAt: new Date().toISOString() }; state.riderDecisionQueue = upsertById(state.riderDecisionQueue ?? [], item).slice(0, 25); saveState(); return item; } function clearRiderDecisionQueueForRequest(requestId) { if (!requestId || !Array.isArray(state.riderDecisionQueue)) return; state.riderDecisionQueue = state.riderDecisionQueue.filter((item) => item.requestId !== requestId); } function dismissRiderDecisionQueueItem(itemId) { if (!itemId) return; state.riderDecisionQueue = (state.riderDecisionQueue ?? []).filter((item) => item.id !== itemId); saveState(); renderAll(); } function viewRiderDecisionQueueItem(itemId) { const item = (state.riderDecisionQueue ?? []).find((entry) => entry.id === itemId); const request = item?.requestId ? stateLookupIndexes().requestMap.get(item.requestId) : null; if (!item || !request) { dismissRiderDecisionQueueItem(itemId); return; } clearRiderDecisionQueueForRequest(item.requestId); focusRiderRequestView(request, { refresh: true, replace: true }); } function riderHasActiveRequestDecisionInProgress(targetRequestId = "") { if (activeRole() !== "rider" || typeof riderWorkspacePage !== "function") return false; if (riderWorkspacePage() !== "requests") return false; const current = selectedRequest(); if (!current?.id || current.id === targetRequestId) return false; return Boolean(current.status === "open" && riderCanShowOfferControls(currentRiderRecord(), current)); } function riderShouldQueueRideNotice(targetRequestId = "") { if (activeRole() !== "rider" || typeof riderWorkspacePage !== "function") return false; if (riderWorkspacePage() !== "requests") return false; if (riderHasActiveRequestDecisionInProgress(targetRequestId)) return true; return riderFocusModeActive(); } function shouldAutoFocusRideNotice(type, request) { if (!request) return false; if (type !== "rider") return true; return !riderShouldQueueRideNotice(request.id); } const riderRouteChangeDecisionRetryKeys = new Set(); function queueRiderRouteChangeDecisionRetry(requestOrId, notice) { const requestId = typeof requestOrId === "string" ? requestOrId : requestOrId?.id ?? notice?.requestId ?? ""; if (!requestId) return false; const key = [requestId, notice?.id || notice?.createdAt || notice?.actionUrl || "route-change"].filter(Boolean).join(":"); if (riderRouteChangeDecisionRetryKeys.has(key)) return true; riderRouteChangeDecisionRetryKeys.add(key); window.setTimeout(async () => { try { if (canRefreshMarketplace() && !marketRefreshInFlight) { await refreshMarketplace({ silent: true, reason: "route_change_notice_retry" }); } else { scheduleMarketplaceRealtimeRefresh("route_change_notice_retry"); } const refreshedRequest = stateLookupIndexes().requestMap.get(requestId) ?? (typeof requestOrId === "string" ? null : requestOrId); if (showRiderRouteChangeDecisionForRequest(refreshedRequest)) { rememberNoticePopup(notice); } } finally { window.setTimeout(() => riderRouteChangeDecisionRetryKeys.delete(key), 5000); } }, 250); return true; } function showRiderRouteChangeDecisionForRequest(request, change = pendingRouteChangeForRequest(request), { render = true } = {}) { if (!request?.id || !change?.id || !state.rider?.id) return false; if (!riderIdentityMatches(selectedRiderIdForRequest(request)) || change.status !== "pending") return false; state.activeTab = "rider"; state.showRoleEntry = false; state.riderPage = "requests"; state.selectedRequestId = request.id; document.querySelector(".nearby-ride-alert")?.remove(); clearRiderDecisionQueueForRequest(request.id); clearRequestMarketplaceFareChange(request.id); if (typeof updateRiderWorkspaceRoute === "function") { updateRiderWorkspaceRoute("requests", { replace: true, requestId: request.id }); } saveState(); if (render && typeof renderAll === "function") renderAll(); window.setTimeout(() => { if (pendingRouteChangeForRequest(stateLookupIndexes().requestMap.get(request.id) ?? request)?.id === change.id) { renderRiderRouteChangeDecisionModal(); } }, 0); return true; } function reconcileRiderPendingRouteChangeDecision({ render = false } = {}) { if (activeRole() !== "rider" || !state.rider?.id) return false; const activeRequests = state.requests .filter((request) => requestIsActiveForCurrentRider(request) && pendingRouteChangeForRequest(request)) .sort((left, right) => (left.id === state.selectedRequestId ? -1 : 0) - (right.id === state.selectedRequestId ? -1 : 0)); const request = activeRequests[0] ?? null; if (!request) return false; return showRiderRouteChangeDecisionForRequest(request, pendingRouteChangeForRequest(request), { render }); } function viewRiderNearbyAlert(request) { if (!request) return; rememberRiderNearbyAlert(request); focusRiderRequestView(request, { refresh: true, replace: true }); } function showRiderNearbyRequestAlert(request) { if (!request || riderNearbyAlertActiveId === request.id) return; document.querySelector(".nearby-ride-alert")?.remove(); riderNearbyAlertActiveId = request.id; rememberRiderNearbyAlert(request); if (!notificationPreferenceEnabled("rider", "ride")) { riderNearbyAlertActiveId = null; return; } const panel = document.createElement("section"); panel.className = "nearby-ride-alert"; panel.setAttribute("role", "dialog"); panel.setAttribute("aria-live", "assertive"); const title = document.createElement("strong"); title.textContent = "New nearby ride"; const detail = document.createElement("p"); detail.textContent = nearbyRideAlertSummary(request); const meta = document.createElement("small"); meta.textContent = `${requestPickupTownText(request)} to ${requestDestinationDisplayText(request)}. ${riderDestinationScopeLabel()}.`; const actions = document.createElement("div"); actions.className = "review-actions"; const view = document.createElement("button"); view.type = "button"; view.className = "secondary-action"; view.textContent = "View request"; view.addEventListener("click", () => viewRiderNearbyAlert(request)); const dismiss = document.createElement("button"); dismiss.type = "button"; dismiss.className = "ghost-action"; dismiss.textContent = "Dismiss"; dismiss.addEventListener("click", () => dismissRiderNearbyAlert(request)); actions.append(view, dismiss); panel.append(title, detail, meta, actions); document.body.append(panel); playNearbyRideCue(); if (!hasSupabaseRuntime()) void showRiderNearbyPhoneNotification(request); } function notifyRiderAboutNearbyRequests(requests = visibleRequestsForRole()) { const [request] = unreadRiderNearbyRequests(requests); if (!request) return; if (riderShouldQueueRideNotice(request.id)) { queueRiderDecisionUpdate("New nearby ride", nearbyRideAlertSummary(request), request.id, "nearby_request"); rememberRiderNearbyAlert(request); return; } if (!riderHasActiveRequestDecisionInProgress(request.id)) { openRiderMarketplaceView({ refresh: false, replace: true }); } showRiderNearbyRequestAlert(request); } function visibleOffersForRole(request) { if (!request || !roleCanSeeRequest(request)) return []; const offers = offersForRequest(request.id); if (activeRole() === "rider") { return offers.filter(offerBelongsToRider); } if (activeRole() === "passenger" && request.status !== "open") return []; const rejected = new Set(state.rejectedOfferIds ?? []); return sortOffersForPassenger(offers.filter((offer) => !rejected.has(offer.id)), request); } function canChatOnRequest(request) { if (!request || !rideLifecycleChatStatuses.includes(request.status)) return false; if (activeRole() === "passenger") return requestBelongsToPassenger(request); if (activeRole() === "rider") return riderIdentityMatches(selectedRiderIdForRequest(request)); return false; } function pruneInactiveRideChatsForViewer() { if (activeRole() !== "passenger" && activeRole() !== "rider") return false; if (!Array.isArray(state.chats) || !state.chats.length) return false; const requestMap = stateLookupIndexes().requestMap; const clearedRequestIds = new Set(); const nextChats = state.chats.filter((message) => { const request = requestMap.get(message.requestId); if (!request || canChatOnRequest(request)) return true; clearedRequestIds.add(message.requestId); return false; }); if (nextChats.length === state.chats.length) return false; state.chats = nextChats; if (typeof clearChatVoiceSignedUrlCacheForRequest === "function") { clearedRequestIds.forEach((requestId) => clearChatVoiceSignedUrlCacheForRequest(requestId)); } return true; } const riderOfferExpiryMs = 10 * 60 * 1000; function offerAgeMs(offer) { const createdAt = new Date(offer?.createdAt ?? 0).getTime(); if (!createdAt || Number.isNaN(createdAt)) return 0; return Math.max(0, Date.now() - createdAt); } function offerIsExpired(offer, request = selectedRequest()) { if (!offer?.id || !request || request.status !== "open") return false; if (request.selectedOfferId === offer.id) return false; return offerAgeMs(offer) > riderOfferExpiryMs; } function offerExpiryChip(offer, request = selectedRequest()) { if (!offer?.createdAt || !request || request.status !== "open") return null; const ageMs = offerAgeMs(offer); if (offerIsExpired(offer, request)) return "Expired offer"; const remainingMinutes = Math.max(1, Math.ceil((riderOfferExpiryMs - ageMs) / 60000)); return `Expires in ${remainingMinutes} min`; } function offerStatusChip(offer, request = selectedRequest()) { if (offerIsExpired(offer, request)) return "Expired"; if (offer?.type === "accepted") return "Accepted passenger fare"; const delta = Number(offer?.fare ?? 0) - Number(request?.fareOffer ?? 0); if (Number.isFinite(delta) && delta > 0) return "Counter-offer"; if (Number.isFinite(delta) && delta < 0) return "Lower fare offer"; return "Matches current fare"; } function offerFareDifference(offer, request) { return Math.abs(Number(offer?.fare ?? 0) - Number(request?.fareOffer ?? 0)); } function offerFareChangeTrail(offer) { return fareHistoryTrail(offer, offer?.fare, offer?.createdAt); } function sortOffersForPassenger(offers, request) { return [...offers].sort((a, b) => { const fareDifference = Number(a.fare ?? 0) - Number(b.fare ?? 0); if (fareDifference !== 0) return fareDifference; return new Date(a.createdAt ?? 0) - new Date(b.createdAt ?? 0); }); } function offerFareDeltaChip(offer, request) { if (activeRole() !== "passenger" || !request) return null; return fareChangeChipFromTrail(offerFareChangeTrail(offer), request.country); } function passengerOfferFareChangeChip(request) { if (activeRole() !== "passenger" || !request || request.status !== "open" || !requestBelongsToPassenger(request)) return null; const changedOffers = offersForRequest(request.id) .map((offer) => { const trail = offerFareChangeTrail(offer); return { offer, trail, change: fareChangeFromTrail(trail) }; }) .filter((item) => item.change) .sort((a, b) => new Date(b.change.changedAt ?? b.offer.createdAt ?? 0).getTime() - new Date(a.change.changedAt ?? a.offer.createdAt ?? 0).getTime()); const latest = changedOffers[0]; if (!latest) return null; return fareChangeChipFromTrail(latest.trail, request.country, "Rider fare "); } function canRefreshMarketplace() { return Boolean(hasSupabaseRuntime() && activeRole() !== "admin" && (hasSignedIn("passenger") || hasSignedIn("rider"))); } function areaProximityRpcMissing(error) { return /waka_area_distance_km|waka_city_span_km/i.test(error.message ?? String(error)); } function adminDirectoryRpcMissing(error) { return /schema cache|Could not find the function|function .* does not exist|404/i.test(error.message ?? String(error)); } function riderMarketplaceRpcBody(rider) { return { p_pickup_areas: pickupAreasWithinRiderRadius(rider), p_limit: riderMarketplacePageSize, p_offset: 0 }; } async function fetchRiderMarketplaceRpcRows(rider) { const body = riderMarketplaceRpcBody(rider); if (!supabaseClient) { return withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/rider_marketplace_requests", { method: "POST", body }), "Loading rider marketplace requests", optionalSupabaseRequestTimeoutMs ); } const { data, error } = await withSupabaseTimeout( supabaseClient.rpc("rider_marketplace_requests", body), "Loading rider marketplace requests", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return data ?? []; } async function fetchRiderMarketplaceGpsRpcRows(rider) { const body = riderMarketplaceRpcBody(rider); if (!supabaseClient) { return withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/rider_marketplace_requests_gps", { method: "POST", body }), "Loading GPS rider marketplace requests", optionalSupabaseRequestTimeoutMs ); } const { data, error } = await withSupabaseTimeout( supabaseClient.rpc("rider_marketplace_requests_gps", body), "Loading GPS rider marketplace requests", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return data ?? []; } async function fetchPassengerApproachRows() { if (!supabaseClient) { return withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/passenger_active_ride_approach", { method: "POST", body: {} }), "Loading passenger ride approach", optionalSupabaseRequestTimeoutMs ); } const { data, error } = await withSupabaseTimeout( supabaseClient.rpc("passenger_active_ride_approach"), "Loading passenger ride approach", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return data ?? []; } async function loadPassengerApproachFromSupabase() { if (passengerApproachRpcUnavailable || !hasSignedIn("passenger")) return false; try { const rows = await fetchPassengerApproachRows(); lastPassengerApproachSource = "passenger_active_ride_approach RPC"; const approachMap = new Map(rows.map((row) => { const mapped = mapPassengerApproachFromDatabase(row); return [mapped.requestId, mapped]; })); if (!approachMap.size) return false; state.requests = state.requests.map((request) => { const approach = approachMap.get(request.id); return approach ? { ...request, ...approach } : request; }); return true; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; passengerApproachRpcUnavailable = true; logClientWarning("Passenger active ride approach RPC is not installed yet. Falling back to offer distance snapshots.", error); return false; } } async function fetchActiveRideContactRows() { if (!supabaseClient) { return withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/active_ride_contacts", { method: "POST", body: {} }), "Loading active ride contacts", optionalSupabaseRequestTimeoutMs ); } const { data, error } = await withSupabaseTimeout( supabaseClient.rpc("active_ride_contacts"), "Loading active ride contacts", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return data ?? []; } async function loadActiveRideContactsFromSupabase() { if (activeRideContactRpcUnavailable || (!hasSignedIn("passenger") && !hasSignedIn("rider"))) return false; try { const rows = await fetchActiveRideContactRows(); const contactMap = new Map(rows.map((row) => { const mapped = mapActiveRideContactFromDatabase(row); return [mapped.requestId, mapped]; })); if (!contactMap.size) return false; state.requests = state.requests.map((request) => { const contact = contactMap.get(request.id); return contact ? { ...request, ...contact } : request; }); return true; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; activeRideContactRpcUnavailable = true; logClientWarning("Active ride contact RPC is not installed yet. Text chat still works after rider selection.", error); return false; } } function emptyMarketplaceTableResult() { return { data: [], warning: null, count: 0, limit: 0, offset: 0 }; } function uniqueMarketplaceIds(values = []) { return [...new Set(values.filter(Boolean))]; } function currentMarketplaceUserIds() { return uniqueMarketplaceIds([ state.passenger?.id, state.passenger?.supabaseUserId, state.sessions?.passenger?.userId, state.rider?.id, state.rider?.supabaseUserId, state.sessions?.rider?.userId ]); } const accountNotificationRefreshIntervalMs = 1000; const accountNotificationRefreshState = { passenger: { at: 0, promise: null, accountId: "", hydrated: false, startedAt: Date.now() }, rider: { at: 0, promise: null, accountId: "", hydrated: false, startedAt: Date.now() } }; let pushSubscriptionRpcUnavailable = false; function runtimeTableLoadOptions(table, limits = marketplaceSyncLoadLimits, page = 0) { const limit = limits[table] ?? null; return { count: true, limit, offset: limit ? Math.max(0, page) * limit : 0 }; } function runtimeTableCountFromContentRange(contentRange, fallbackCount) { const match = String(contentRange ?? "").match(/\/(\d+|\*)$/); if (!match || match[1] === "*") return fallbackCount; return Number(match[1]); } function applyRuntimeRestFilters(params, filters = []) { filters.forEach((filter) => { if (!filter?.column || filter.value === undefined || filter.value === null || filter.value === "") return; if (filter.operator === "in") { params.set(filter.column, `in.(${filter.value.join(",")})`); return; } params.set(filter.column, `${filter.operator ?? "eq"}.${filter.value}`); }); } function applyRuntimeSupabaseFilters(query, filters = []) { return filters.reduce((nextQuery, filter) => { if (!filter?.column || filter.value === undefined || filter.value === null || filter.value === "") return nextQuery; if (filter.operator === "ilike") return nextQuery.ilike(filter.column, filter.value); if (filter.operator === "in") return nextQuery.in(filter.column, filter.value); return nextQuery.eq(filter.column, filter.value); }, query); } async function selectRuntimeTable(table, select = "*", orderColumn = null, options = {}) { const limit = Number.isFinite(options.limit) ? options.limit : null; const offset = Number.isFinite(options.offset) ? Math.max(0, options.offset) : 0; const shouldCount = Boolean(options.count); const filters = options.filters ?? []; if (!supabaseClient && supabaseRestSession?.access_token) { const params = new URLSearchParams(); params.set("select", select); if (orderColumn) params.set("order", `${orderColumn}.desc`); if (limit) params.set("limit", `${limit}`); if (offset) params.set("offset", `${offset}`); applyRuntimeRestFilters(params, filters); const response = await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/${table}?${params.toString()}`, { headers: shouldCount ? { Prefer: "count=exact" } : {}, returnResponse: true }), `Loading ${table} records`, optionalSupabaseRequestTimeoutMs ); const data = response.data ?? []; return { data, warning: null, count: shouldCount ? runtimeTableCountFromContentRange(response.headers.get("content-range"), data.length) : data.length, limit, offset }; } let query = shouldCount ? supabaseClient.from(table).select(select, { count: "exact" }) : supabaseClient.from(table).select(select); query = applyRuntimeSupabaseFilters(query, filters); if (orderColumn) query = query.order(orderColumn, { ascending: false }); if (limit) { query = query.range(offset, offset + limit - 1); } else if (offset) { query = query.range(offset, offset + 999); } const { data, error, count } = await withSupabaseTimeout( query, `Loading ${table} records`, optionalSupabaseRequestTimeoutMs ); if (error) throw error; return { data: data ?? [], warning: null, count: shouldCount ? count ?? data?.length ?? 0 : data?.length ?? 0, limit, offset }; } function marketplaceTableOptions(table, filters = []) { return { ...runtimeTableLoadOptions(table, marketplaceSyncLoadLimits), filters: filters.filter(Boolean) }; } function marketplaceEqFilter(column, value) { return value ? { column, value } : null; } function marketplaceInFilter(column, values = []) { const scopedValues = uniqueMarketplaceIds(values); return scopedValues.length ? { column, operator: "in", value: scopedValues } : null; } async function selectScopedMarketplaceTable(table, orderColumn, filters = []) { const scopedFilters = filters.filter(Boolean); if (!scopedFilters.length) return emptyMarketplaceTableResult(); return selectRuntimeTable(table, "*", orderColumn, marketplaceTableOptions(table, scopedFilters)); } function pushNotificationsSupported() { return Boolean("Notification" in window && "serviceWorker" in navigator && "PushManager" in window); } function pushNotificationPublicKey() { return String(appConfig.pushNotificationPublicKey || window.WAKA_PUSH_PUBLIC_KEY || "").trim(); } function pushStatusElement(type) { return type === "rider" ? els.riderPushStatus : els.passengerPushStatus; } function pushButtonElement(type) { return type === "rider" ? els.riderEnablePush : els.passengerEnablePush; } function base64UrlToUint8Array(value) { const padding = "=".repeat((4 - (value.length % 4)) % 4); const base64 = `${value}${padding}`.replace(/-/g, "+").replace(/_/g, "/"); const raw = window.atob(base64); return Uint8Array.from([...raw].map((character) => character.charCodeAt(0))); } function pushSubscriptionRecordFromBrowser(type, subscription) { const json = subscription.toJSON(); const account = type === "rider" ? state.rider : state.passenger; return { id: makeId("push"), userId: account?.id, role: type, endpoint: json.endpoint, p256dh: json.keys?.p256dh, auth: json.keys?.auth, userAgent: navigator.userAgent, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; } async function savePushSubscriptionToSupabase(record) { if (!record?.endpoint || !record?.p256dh || !record?.auth) throw new Error("Push subscription is missing browser keys."); if (!hasSupabaseRuntime()) return record; if (!pushSubscriptionRpcUnavailable) { try { await callSupabaseRpc( "register_push_subscription", { p_endpoint: record.endpoint, p_p256dh: record.p256dh, p_auth: record.auth, p_user_agent: record.userAgent }, "Saving phone notification permission", supabaseProfileSaveTimeoutMs ); return record; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; pushSubscriptionRpcUnavailable = true; logClientWarning("Push subscription RPC is not installed yet. Falling back to direct table upsert.", error); } } assertClientFallbackAllowed("Push notification subscription", "supabase-notification-delivery.sql"); const payload = { user_id: record.userId, role: record.role, endpoint: record.endpoint, p256dh: record.p256dh, auth: record.auth, user_agent: record.userAgent, updated_at: record.updatedAt }; if (!supabaseClient) { await withSupabaseTimeout( supabaseRestRequest("/rest/v1/push_subscriptions?on_conflict=endpoint", { method: "POST", body: payload, headers: { Prefer: "resolution=merge-duplicates,return=minimal" } }), "Saving phone notification permission", supabaseProfileSaveTimeoutMs ); return record; } const { error } = await withSupabaseTimeout( supabaseClient .from("push_subscriptions") .upsert(payload, { onConflict: "endpoint" }), "Saving phone notification permission", supabaseProfileSaveTimeoutMs ); if (error) throw error; return record; } function updatePushNotificationControls(type) { const button = pushButtonElement(type); const status = pushStatusElement(type); if (!button || !status) return; const signedIn = hasSignedIn(type); button.hidden = !signedIn; status.hidden = !signedIn; if (!signedIn) return; if (!pushNotificationsSupported()) { button.disabled = true; status.textContent = "This browser does not support phone push notifications."; return; } const hasPushKey = Boolean(pushNotificationPublicKey()); const muted = !notificationPreferenceEnabled(type, "all"); button.disabled = Notification.permission === "denied" || muted; button.textContent = muted ? "Notifications muted" : Notification.permission === "granted" ? "Phone notifications enabled" : "Enable phone notifications"; status.textContent = muted ? "Notifications are muted for this account on this device. Turn on Allow notifications below to receive ride, fare, message, or Waka notices." : Notification.permission === "granted" ? hasPushKey ? "Phone push notifications are allowed on this device." : "Ride request popups are allowed while Waka is open on this device. Add pushNotificationPublicKey for closed-app push." : Notification.permission === "denied" ? "Phone notifications are blocked in this browser. Change site settings to allow them." : hasPushKey ? "Enable phone notifications to receive ride request popups and admin broadcasts on this device." : "Enable phone notifications for ride request popups while Waka is open. Closed-app push needs pushNotificationPublicKey."; } async function enableAccountPushNotifications(type) { const status = pushStatusElement(type); const button = pushButtonElement(type); if (!hasSignedIn(type)) { if (status) status.textContent = "Sign in before enabling phone notifications."; return; } if (!pushNotificationsSupported()) { if (status) status.textContent = "This browser does not support phone push notifications."; return; } try { if (button) button.disabled = true; if (status) status.textContent = "Requesting phone notification permission..."; const permission = await Notification.requestPermission(); if (permission !== "granted") { if (status) status.textContent = "Phone notification permission was not granted."; return; } const publicKey = pushNotificationPublicKey(); if (!publicKey) { if (status) status.textContent = "Ride request popups are enabled while Waka is open. Add pushNotificationPublicKey for closed-app push."; return; } const registration = await navigator.serviceWorker.ready; const existing = await registration.pushManager.getSubscription(); const subscription = existing ?? await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: base64UrlToUint8Array(publicKey) }); const saved = await savePushSubscriptionToSupabase(pushSubscriptionRecordFromBrowser(type, subscription)); state.pushSubscriptions = upsertById(state.pushSubscriptions, saved); saveState(); if (status) status.textContent = "Phone push notifications are enabled for Waka notices on this device."; } catch (error) { if (status) status.textContent = `Could not enable phone notifications: ${error.message}`; } finally { updatePushNotificationControls(type); } } function noticePopupAlreadyShown(notice) { const shown = state.notificationPopupIds ?? []; return noticePopupDeliveryKeys(notice).some((key) => shown.includes(key)); } function noticePopupSemanticEvent(notice) { const text = `${notice?.eventType || ""} ${notice?.id || ""} ${notice?.title || ""} ${notice?.body || ""}`.toLowerCase(); if (/nearby_ride_request|nearby_request|new nearby ride|new scheduled ride/.test(text)) return "nearby_ride_request"; if (/passenger_fare_increased|passenger.*(increased|updated).*fare|\bfare-/.test(text)) return "passenger_fare_increased"; if (/rider_counter_offer|new rider (counter-)?offer|rider.*(counter|updated).*fare|\boffer-/.test(text)) return "rider_counter_offer"; if (/passenger_rejected_offer|passenger left negotiation|declined this offer|rejected-offer/.test(text)) return "passenger_rejected_offer"; if (/rider_offer_withdrawn|rider left negotiation|withdrew their offer|offer-withdrawn/.test(text)) return "rider_offer_withdrawn"; if (/ride_matched|ride matched|\bmatched-/.test(text)) return "ride_matched"; if (/ride_reopened|rider_cancelled_before_pickup|rider_canceled_before_pickup|cancelled_before_pickup|canceled_before_pickup|rider cancelled|rider canceled|open again|reopened/.test(text)) return "ride_reopened"; if (/ride_cancelled|ride canceled|ride cancelled|\bcancelled-|\bcanceled-/.test(text)) return "ride_cancelled"; if (/route_change_requested|route-change-requested|route change request|requested a destination change|requested an added stop/.test(text)) return "route_change_requested"; if (/route_change_accepted|route-change-accepted|accepted route change|route change accepted|rider accepted route change/.test(text)) return "route_change_accepted"; if (/route_change_declined|route-change-declined|declined route change|route change declined|rider declined route change/.test(text)) return "route_change_declined"; if (/ride_arrived|rider arrived|\barrived-/.test(text)) return "ride_arrived"; if (/ride_started|ride started|\bstarted-/.test(text)) return "ride_started"; if (/ride_stop_arrived|added stop|\bstop-/.test(text)) return "ride_stop_arrived"; if (/ride_completed|ride completed|\bcompleted-/.test(text)) return "ride_completed"; if (/ride_chat_message|new ride message|\bchat-/.test(text)) return "ride_chat_message"; return String(notice?.eventType || notice?.title || "").trim().toLowerCase().replace(/\s+/g, "_"); } function noticePopupDeliveryKey(notice) { return noticePopupDeliveryKeys(notice)[0] ?? ""; } function noticePopupFareAmountToken(notice) { const text = `${notice?.title || ""} ${notice?.body || ""}`; const dollarMatches = [...text.matchAll(/\$\s*(\d+(?:\.\d{1,2})?)/g)]; if (dollarMatches.length) return dollarMatches[dollarMatches.length - 1][1].replace(/[^\d.]/g, ""); const xafMatches = [...text.matchAll(/(\d[\d,]*(?:\.\d{1,2})?)\s*(?:XAF|FCFA)\b/gi)]; if (xafMatches.length) return xafMatches[xafMatches.length - 1][1].replace(/[^\d.]/g, ""); const fareMatch = text.match(/\b(?:fare|offer|offered|asks|asked|counter(?:ed)?|updated)\D{0,32}(\d+(?:\.\d{1,2})?)/i); return fareMatch?.[1]?.replace(/[^\d.]/g, "") || ""; } function noticePopupDeliveryKeys(notice) { if (!notice?.recipientRole || !notice?.requestId) return []; const event = noticePopupSemanticEvent(notice); if (!event) return []; const identityKey = notice.id ? [notice.id] : []; if (event === "ride_chat_message") { return [["notice-delivered", notice.recipientRole, notice.requestId, event, notice.id || notice.createdAt || Date.now()].filter(Boolean).join(":"), ...identityKey].filter(Boolean); } if (/^route_change_/.test(event)) { return [["notice-delivered", notice.recipientRole, notice.requestId, event, notice.id || notice.createdAt || notice.actionUrl || Date.now()].filter(Boolean).join(":"), ...identityKey].filter(Boolean); } const text = `${notice?.title || ""} ${notice?.body || ""}`; const amount = /(fare|offer)/.test(event) ? noticePopupFareAmountToken(notice) : ""; const actor = /(fare|offer)/.test(event) ? noticePopupActorToken(notice) : ""; const semanticKey = ["notice-delivered", notice.recipientRole, notice.requestId, event, actor, amount].filter(Boolean).join(":"); const keys = [ semanticKey, ...identityKey ]; if (/(fare|offer)/.test(event)) { keys.push(["notice-delivered", notice.recipientRole, notice.requestId, event, amount].filter(Boolean).join(":")); } return [...new Set(keys.filter(Boolean))]; } function upsertNotificationByDeliveryKey(items, notice) { const keys = new Set(noticePopupDeliveryKeys(notice)); if (!keys.size) return upsertById(items, notice); return [ notice, ...items.filter((item) => item.id !== notice.id && !noticePopupDeliveryKeys(item).some((key) => keys.has(key))) ]; } function uniqueNotificationsByDeliveryKey(notifications = []) { const seen = new Set(); return notifications.filter((notice) => { const keys = noticePopupDeliveryKeys(notice); const primaryKey = keys[0] || notice?.id || ""; if (!primaryKey) return true; if (seen.has(primaryKey) || keys.some((key) => seen.has(key))) return false; keys.forEach((key) => seen.add(key)); return true; }); } function noticePopupActorToken(notice) { if (notice?.createdBy) return `actor-${String(notice.createdBy).toLowerCase()}`; const text = `${notice?.title || ""} ${notice?.body || ""}`; const explicitActor = text.match(/\b(rider|passenger)\s+([A-Za-z0-9][A-Za-z0-9'.-]*)/i); if (explicitActor) return `${explicitActor[1].toLowerCase()}-${explicitActor[2].toLowerCase()}`; if (/passenger/i.test(text)) return "passenger"; if (/rider/i.test(text)) return "rider"; return ""; } function rideNoticePopupAllowed(type, notice, request) { if (!notice?.requestId) return true; const marker = `${notice.eventType || ""} ${notice.id || ""} ${notice.title || ""}`.toLowerCase(); const rideEndedMarker = /(cancel|cancelled|reject|rejected|withdrawn|left|no[-_\s]?longer[-_\s]?available|rider_offer_withdrawn|passenger_rejected_offer)/.test(marker); if (!request) return rideEndedMarker; if (request.status === "cancelled") return /(cancel|cancelled|ride_cancelled)/.test(marker); if (request.status === "completed" && !/(completed|ride_completed)/.test(marker)) return false; if (/(chat|ride_chat_message|matched|ride_matched)/.test(marker)) { if (!["matched", "arrived", "in_progress"].includes(request.status)) return false; if (type === "passenger") return requestBelongsToPassenger(request); if (type === "rider") return riderIdentityMatches(selectedRiderIdForRequest(request)); } if (/(nearby|offer|counter|fare|passenger_fare_increased|rider_counter_offer)/.test(marker)) { return request.status === "open"; } return true; } function addRideAccountNotice(type, title, body, requestId, eventKey = "", { autoFocus = true, createdBy = null, createdByRole = null } = {}) { const account = type === "rider" ? state.rider : state.passenger; if (!account?.id) return null; const notice = { id: eventKey ? `notice-${type}-${eventKey}` : makeId("notice"), recipientId: account.id, recipientRole: type, title, body, requestId, actionUrl: workspaceNotificationUrl(type, type === "rider" ? "requests" : "trips", requestId), eventType: eventKey ? eventKey.split("-")[0] : "", createdBy, createdByRole, createdAt: new Date().toISOString(), readAt: null }; if (noticeCreatedByCurrentAccount(type, notice)) return null; const alreadyDelivered = noticePopupAlreadyShown(notice); state.notifications = upsertNotificationByDeliveryKey(state.notifications, notice); const noticeRequest = requestId ? state.requests.find((request) => request.id === requestId) : null; const canDeliverNotice = rideNoticePopupAllowed(type, notice, noticeRequest); const autoFocusAllowed = Boolean(autoFocus && noticeRequest && shouldAutoFocusRideNotice(type, noticeRequest)); let queuedForRider = false; if (type === "rider" && autoFocus && noticeRequest && !autoFocusAllowed) { queueRiderDecisionUpdate(title, body, requestId, notice.eventType || "ride_update"); queuedForRider = true; } if (autoFocusAllowed) { focusRideNoticeView(type, noticeRequest, { refresh: false, replace: true }); } saveState(); const preferenceAllowed = noticeDeliveryAllowedByPreference(type, notice); let deliveredInline = false; if (queuedForRider) { rememberNoticePopup(notice); if (preferenceAllowed && !alreadyDelivered) { void showRideTransactionPhoneNotification({ role: type, page: type === "rider" ? "requests" : "trips", requestId, title, body, tag: `waka-${type}-${eventKey || requestId || Date.now()}` }); } return notice; } if (canDeliverNotice && preferenceAllowed && !alreadyDelivered && typeof showAccountNoticePopup === "function") { showAccountNoticePopup(type, notice, { autoFocusRide: false }); deliveredInline = noticePopupAlreadyShown(notice); } if (!canDeliverNotice || !preferenceAllowed || alreadyDelivered) return notice; rememberNoticePopup(notice); void showRideTransactionPhoneNotification({ role: type, page: type === "rider" ? "requests" : "trips", requestId, title, body, tag: `waka-${type}-${eventKey || requestId || Date.now()}` }); return notice; } function addRiderRideNotice(title, body, requestId, eventKey = "", options = {}) { return addRideAccountNotice("rider", title, body, requestId, eventKey, options); } function normalizeAccountNoticeForDelivery(type, notice, request = null) { if (type === "rider" && request && requestCancelledByCurrentRider(request) && /passenger cancelled/i.test(`${notice?.title || ""} ${notice?.body || ""}`)) { return { ...notice, title: "Ride cancelled", body: rideCancelledNoticeBodyForRider(request) }; } return notice; } function rememberNoticePopup(notice) { state.notificationPopupIds = [...new Set([ ...(state.notificationPopupIds ?? []), ...noticePopupDeliveryKeys(notice) ].filter(Boolean))].slice(-140); saveState(); } function visibleNoticePopupAlreadyShown(notice) { const keys = new Set(noticePopupDeliveryKeys(notice)); if (!keys.size) return false; return [...document.querySelectorAll(".notice-popup[data-notice-delivery-key]")] .some((node) => keys.has(node.dataset.noticeDeliveryKey)); } async function refreshRiderStateAfterAdminNotice(notice) { if (!hasSupabaseRuntime() || !hasSignedIn("rider")) return; const noticeText = `${notice?.title || ""} ${notice?.body || ""}`; if (!/(approved|approval|correction|checkr|background|eligibility|suspended|declined)/i.test(noticeText)) return; try { await hydrateProfileFromSupabase("rider"); await refreshPaymentAccountsFromSupabase("rider"); await loadMarketplaceFromSupabase({ includeAccountData: true }); renderAll(); } catch (error) { logClientWarning("Rider state could not be refreshed after admin notice.", error); } } function showAccountNoticePopup(type, notice, { autoFocusRide = false } = {}) { if (!notice?.id || noticePopupAlreadyShown(notice)) return; if (document.hidden) return; if (!noticeDeliveryAllowedByPreference(type, notice)) { rememberNoticePopup(notice); return; } if (type === "rider") void refreshRiderStateAfterAdminNotice(notice); const noticeText = `${notice.title} ${notice.body}`; const riderCorrectionNotice = type === "rider" && /correction/i.test(noticeText); const riderCheckrNotice = type === "rider" && /(checkr|background|eligibility)/i.test(noticeText); const noticeRequest = notice.requestId ? state.requests.find((request) => request.id === notice.requestId) : null; notice = normalizeAccountNoticeForDelivery(type, notice, noticeRequest); if (noticePopupAlreadyShown(notice) || visibleNoticePopupAlreadyShown(notice)) { rememberNoticePopup(notice); return; } if (!riderCorrectionNotice && !riderCheckrNotice && !rideNoticePopupAllowed(type, notice, noticeRequest)) return; rememberNoticePopup(notice); playNearbyRideCue(); const rideNotice = Boolean(noticeRequest && (type === "passenger" || type === "rider")); if (autoFocusRide && rideNotice) focusRideNoticeView(type, noticeRequest, { refresh: false, replace: true }); const node = document.createElement("aside"); node.className = `notice-popup${riderCorrectionNotice || riderCheckrNotice ? " notice-popup-actionable" : ""}`; node.dataset.noticeDeliveryKey = noticePopupDeliveryKey(notice); node.setAttribute("role", "status"); node.innerHTML = `
${escapeHtml(type === "rider" ? "Rider notice" : "Passenger notice")} ${escapeHtml(notice.title)}

${escapeHtml(notice.body)}

`; node.querySelector(".notice-popup-open")?.addEventListener("click", () => { if (type === "passenger" && rideNotice) focusPassengerRequestView(noticeRequest, { refresh: true, replace: true }); else if (type === "rider" && rideNotice) focusRiderRequestView(noticeRequest, { refresh: true, replace: true }); else if (type === "passenger") setPassengerWorkspacePage("notices"); if (type === "rider" && riderCorrectionNotice && typeof openRiderCorrectionForm === "function") openRiderCorrectionForm(); else if (type === "rider" && riderCheckrNotice && typeof setRiderWorkspacePage === "function") setRiderWorkspacePage("checks"); else if (type === "rider" && !rideNotice && typeof setRiderWorkspacePage === "function") setRiderWorkspacePage("notices"); else if (type === "rider" && !rideNotice) els.riderNoticePanel?.scrollIntoView({ behavior: "smooth", block: "start" }); node.remove(); }); node.querySelector(".notice-popup-close")?.addEventListener("click", () => node.remove()); document.body.append(node); if (!riderCorrectionNotice && !riderCheckrNotice) { window.setTimeout(() => node.remove(), 12000); } } function accountIdentityIdsForNoticeRole(type) { const account = type === "rider" ? state.rider : state.passenger; const session = type === "rider" ? state.sessions?.rider : state.sessions?.passenger; return uniqueMarketplaceIds([ account?.id, account?.supabaseUserId, session?.userId ]).map((id) => String(id)); } function noticeCreatedByCurrentAccount(type, notice) { const accountIds = accountIdentityIdsForNoticeRole(type); if (!accountIds.length || !notice?.createdBy) return false; const createdBy = String(notice.createdBy); const createdByRole = String(notice.createdByRole || "").toLowerCase(); if (createdByRole) { return createdByRole === type && accountIds.includes(createdBy); } return accountIds.includes(createdBy); } function deliverAccountNotificationNotice(type, notice, { autoFocusRide = false, deliverPhone = false, phoneTagPrefix = "account" } = {}) { if (!notice?.id || !hasSignedIn(type) || noticeCreatedByCurrentAccount(type, notice)) { return false; } const request = notice.requestId ? state.requests.find((item) => item.id === notice.requestId) : null; notice = normalizeAccountNoticeForDelivery(type, notice, request); if (deliverPhone) processQueuedPhoneDeliveryForNotice(type, notice); if (noticePopupAlreadyShown(notice)) return false; const semanticEvent = noticePopupSemanticEvent(notice); if (type === "passenger" && request && semanticEvent === "ride_reopened") { state.selectedRequestId = request.id; state.passengerPage = "trips"; if (typeof updatePassengerWorkspaceRoute === "function") updatePassengerWorkspaceRoute("trips", { replace: true, requestId: request.id }); showMandatoryPassengerRideReopenedNotice(request, notice.id || notice.createdAt || "", notice.createdBy || ""); if (typeof renderAll === "function") renderAll(); return true; } if (type === "rider" && semanticEvent === "route_change_requested") { if (!noticeDeliveryAllowedByPreference(type, notice)) { rememberNoticePopup(notice); return false; } const shown = request ? showRiderRouteChangeDecisionForRequest(request) : false; if (shown) { rememberNoticePopup(notice); return true; } queueRiderRouteChangeDecisionRetry(request ?? notice.requestId, notice); return true; } if (type === "rider" && request?.status === "open" && semanticEvent === "nearby_ride_request" && !document.hidden) { rememberNoticePopup(notice); return false; } if (!rideNoticePopupAllowed(type, notice, request)) return false; if (!noticeDeliveryAllowedByPreference(type, notice)) { rememberNoticePopup(notice); return false; } const autoFocusAllowed = Boolean(autoFocusRide && request && shouldAutoFocusRideNotice(type, request)); let queuedForRider = false; if (type === "rider" && autoFocusRide && request && !autoFocusAllowed) { queueRiderDecisionUpdate(notice.title, notice.body, notice.requestId, notice.eventType || "ride_update"); queuedForRider = true; } if (queuedForRider) { rememberNoticePopup(notice); if (deliverPhone) { void showRideTransactionPhoneNotification({ role: type, page: type === "rider" ? "requests" : "trips", request, requestId: notice.requestId, title: notice.title, body: notice.body, tag: `waka-${type}-${phoneTagPrefix}-${notice.id}` }); } return true; } showAccountNoticePopup(type, notice, { autoFocusRide: autoFocusAllowed }); if (deliverPhone) { rememberNoticePopup(notice); void showRideTransactionPhoneNotification({ role: type, page: type === "rider" ? "requests" : "trips", request, requestId: notice.requestId, title: notice.title, body: notice.body, tag: `waka-${type}-${phoneTagPrefix}-${notice.id}` }); return true; } if (noticePopupAlreadyShown(notice)) return true; return false; } async function refreshRideStateForAccountNotifications(notifications, reason = "account_notice_poll") { if (!notifications.some((notification) => notification.requestId) || !canRefreshMarketplace()) return; if (marketRefreshInFlight) { scheduleMarketplaceRealtimeRefresh(reason); return; } await refreshMarketplace({ silent: true, reason }); } async function refreshAccountNotificationsFromSupabase(type, options = {}) { if (!hasSupabaseRuntime() || !hasSignedIn(type)) return []; const account = type === "passenger" ? state.passenger : state.rider; const accountIds = accountIdentityIdsForNoticeRole(type); if (!accountIds.length) return []; const existingIds = new Set(currentAccountNotifications(type).map((notification) => notification.id)); const tracker = accountNotificationRefreshState[type] ?? accountNotificationRefreshState.passenger; if (tracker.accountId !== account.id) { tracker.accountId = account.id; tracker.hydrated = false; tracker.at = 0; tracker.startedAt = Date.now(); } const initialHydration = !tracker.hydrated; const now = Date.now(); if (!options.force && tracker.promise) return tracker.promise; if (!options.force && now - tracker.at < accountNotificationRefreshIntervalMs) { return currentAccountNotifications(type); } tracker.at = now; tracker.promise = selectScopedMarketplaceTable("admin_notifications", "created_at", [ marketplaceInFilter("recipient_id", accountIds), marketplaceEqFilter("recipient_role", type) ]) .then(async (result) => { const notifications = (result.data ?? []).map(mapAdminNotificationFromDatabase); state.notifications = state.notifications.filter((notification) => !(accountIds.includes(String(notification.recipientId)) && notification.recipientRole === type)); notifications.forEach((notification) => { state.notifications = upsertNotificationByDeliveryKey(state.notifications, notification); }); saveState(); tracker.hydrated = true; const newNotifications = uniqueNotificationsByDeliveryKey(notifications .filter((notification) => { if (options.force || existingIds.has(notification.id)) return false; if (!initialHydration) return true; const createdAt = new Date(notification.createdAt ?? 0).getTime(); return Number.isFinite(createdAt) && createdAt >= (tracker.startedAt ?? now) - 1000; }) .filter((notification) => !noticeCreatedByCurrentAccount(type, notification))) .slice(0, 5); if (options.refreshRide) { await refreshRideStateForAccountNotifications(newNotifications); } if (newNotifications.some((notification) => notification.requestId)) { forceMarketplaceRefreshSoon(`${type}_account_notice`); } newNotifications.forEach((notification) => { deliverAccountNotificationNotice(type, notification, { autoFocusRide: true, deliverPhone: Boolean(options.deliverPhone), phoneTagPrefix: "notice-poll" }); }); return currentAccountNotifications(type); }) .catch((error) => { logClientWarning(`${type} admin notices could not be refreshed.`, error); return currentAccountNotifications(type); }) .finally(() => { tracker.promise = null; }); return tracker.promise; } function mergeMarketplaceTableResults(results = []) { const rowsById = new Map(); const warnings = []; results.forEach((result) => { if (result?.warning) warnings.push(result.warning); (result?.data ?? []).forEach((row) => { rowsById.set(row.id ?? JSON.stringify(row), row); }); }); const rows = [...rowsById.values()]; return { data: rows, warning: warnings[0] ?? null, count: rows.length, limit: null, offset: 0 }; } async function selectAnyUserScopedMarketplaceTable(table, orderColumn, columns = []) { const userIds = currentMarketplaceUserIds(); if (!userIds.length || !columns.length) return emptyMarketplaceTableResult(); const results = await Promise.all(columns.map((column) => ( selectScopedMarketplaceTable(table, orderColumn, [marketplaceInFilter(column, userIds)]) ))); return mergeMarketplaceTableResults(results); } async function selectRiderCompletedMileageSegmentsForRequests(riderIds = [], requestIds = []) { const scopedRiderIds = uniqueMarketplaceIds(riderIds); const scopedRequestIds = uniqueMarketplaceIds(requestIds); if (!scopedRiderIds.length || !scopedRequestIds.length) return emptyMarketplaceTableResult(); try { return await selectScopedMarketplaceTable("insurance_telemetry_segments", "started_at", [ marketplaceInFilter("rider_id", scopedRiderIds), marketplaceInFilter("ride_request_id", scopedRequestIds), marketplaceEqFilter("status", "closed") ]); } catch (error) { logClientWarning("Completed rider mileage could not be loaded; marketplace refresh will continue.", error); return emptyMarketplaceTableResult(); } } async function selectRideRouteChangesForMarketplace() { try { return await selectAnyUserScopedMarketplaceTable("ride_route_changes", "created_at", ["passenger_id", "rider_id"]); } catch (error) { logClientWarning("Ride route changes could not be loaded; chat fallback will continue.", error); return emptyMarketplaceTableResult(); } } async function selectRideRouteChangesForRequests(requestIds = []) { const scopedRequestIds = uniqueMarketplaceIds(requestIds); if (!scopedRequestIds.length) return emptyMarketplaceTableResult(); try { return await selectScopedMarketplaceTable("ride_route_changes", "created_at", [marketplaceInFilter("ride_request_id", scopedRequestIds)]); } catch (error) { logClientWarning("Ride route changes could not be loaded by ride request; account scoped fallback will continue.", error); return emptyMarketplaceTableResult(); } } async function loadPassengerRideRequestsFromSupabase(passengerId = state.passenger?.id) { if (!hasSupabaseRuntime() || !passengerId) return []; const requestsResult = await selectScopedMarketplaceTable("ride_requests", "created_at", [ marketplaceEqFilter("passenger_id", passengerId) ]); const requestRows = requestsResult.data ?? []; const requestIds = uniqueMarketplaceIds(requestRows.map((request) => request.id)); const [offersResult, chatsResult, routeChangesResult, routeChangesByRequestResult] = await Promise.all([ selectScopedMarketplaceTable("ride_offers", "created_at", [marketplaceInFilter("ride_request_id", requestIds)]), selectScopedMarketplaceTable("ride_chats", "created_at", [marketplaceInFilter("ride_request_id", requestIds)]), selectRideRouteChangesForMarketplace(), selectRideRouteChangesForRequests(requestIds) ]); const offers = (offersResult.data ?? []).map(mapOfferFromDatabase); const offerMap = new Map(offers.map((offer) => [offer.id, offer])); const previousRequestMap = stateLookupIndexes().requestMap; const requests = requestRows.map((request) => { const mapped = mapRideRequestFromDatabase(request, new Map(), offerMap); return preserveRideRequestPickup(mapped, previousRequestMap.get(mapped.id)); }); const requestIdSet = new Set(requestIds); state.requests = [ ...state.requests.filter((request) => request.passengerId !== passengerId), ...requests ]; state.offers = [ ...state.offers.filter((offer) => !requestIdSet.has(offer.requestId)), ...offers ]; state.chats = [ ...state.chats.filter((message) => !requestIdSet.has(message.requestId)), ...(chatsResult.data ?? []).map(mapChatFromDatabase) ]; mergeMarketplaceTableResults([routeChangesResult, routeChangesByRequestResult]).data.map(mapRideRouteChangeFromDatabase).forEach((change) => { state.routeChangeRequests = upsertById(state.routeChangeRequests ?? [], change); }); if (typeof mergeRouteChangeEventsFromChats === "function") { mergeRouteChangeEventsFromChats(state.chats); } saveState(); return requests; } async function selectMarketplaceRequests() { const rider = activeRole() === "rider" ? currentRiderRecord() : null; if (riderCanSeeRequests(rider) && !gpsMatchingRpcUnavailable) { try { const rows = await fetchRiderMarketplaceGpsRpcRows(rider); lastMarketplaceSyncSource = "rider_marketplace_requests_gps RPC"; return { data: riderShowsAllNearbyPickups() ? rows : rows.filter((request) => requestDestinationMatchesDailyRegions(mapRideRequestFromDatabase(request), rider)), warning: null, count: rows[0]?.total_count ?? rows.length, limit: riderMarketplacePageSize, offset: 0, source: "rider_gps_rpc" }; } catch (error) { if (areaProximityRpcMissing(error)) areaProximityRpcUnavailable = true; if (!adminDirectoryRpcMissing(error)) throw error; gpsMatchingRpcUnavailable = true; logClientWarning( areaProximityRpcMissing(error) ? "Server-side area proximity helper is not installed yet. Falling back to local distance estimates." : "GPS/PostGIS rider marketplace RPC is not installed yet. Falling back to the GPS-gated area-distance marketplace RPC.", error ); } } if (riderCanSeeRequests(rider) && !riderMarketplaceRpcUnavailable) { try { const rows = await fetchRiderMarketplaceRpcRows(rider); lastMarketplaceSyncSource = "rider_marketplace_requests RPC"; return { data: riderShowsAllNearbyPickups() ? rows : rows.filter((request) => requestDestinationMatchesDailyRegions(mapRideRequestFromDatabase(request), rider)), warning: null, count: rows[0]?.total_count ?? rows.length, limit: riderMarketplacePageSize, offset: 0, source: "rider_rpc" }; } catch (error) { if (areaProximityRpcMissing(error)) areaProximityRpcUnavailable = true; if (!adminDirectoryRpcMissing(error)) throw error; riderMarketplaceRpcUnavailable = true; logClientWarning( areaProximityRpcMissing(error) ? "Server-side area proximity helper is not installed yet. Falling back to capped table reads." : "Rider marketplace RPC is not installed yet. Falling back to capped table reads.", error ); } } lastMarketplaceSyncSource = "capped table reads"; if (riderCanSeeRequests(rider)) { assertClientFallbackAllowed("Rider marketplace table read", "supabase-rider-marketplace-rpc.sql"); return selectScopedMarketplaceTable("ride_requests", "created_at", [ marketplaceEqFilter("status", "open"), marketplaceEqFilter("country", rider.country), marketplaceEqFilter("city", rider.city), marketplaceEqFilter("vehicle_preference", normalizeRideVehicle(rider.vehicle)) ]); } if (state.passenger?.id) { return selectScopedMarketplaceTable("ride_requests", "created_at", [ marketplaceEqFilter("passenger_id", state.passenger.id) ]); } return emptyMarketplaceTableResult(); } function rememberRiderDayPreferences(preferences) { preferences.forEach((preference) => { state.riderDayPreferences = upsertById( state.riderDayPreferences.filter((item) => !(item.riderId === preference.riderId && item.serviceDate === preference.serviceDate)), preference ); if (state.rider?.id === preference.riderId && preference.serviceDate === localDateKey()) { state.rider = { ...state.rider, dailyRegions: preference }; state.riderDestinationScope = preference.showAllNearbyPickups ? "all" : "preferred"; state.riders = upsertById(state.riders, state.rider); } }); } function pruneStaleRiderMarketplaceRequests(latestRequestIds = new Set(), rider = currentRiderRecord()) { if (activeRole() !== "rider" || !rider) return new Set(); const latestIds = latestRequestIds instanceof Set ? latestRequestIds : new Set(latestRequestIds); const removedIds = new Set(); state.requests = state.requests.filter((request) => { if (!request?.id || latestIds.has(request.id)) return true; if (requestBelongsToPassenger(request)) return true; if (requestIsActiveForCurrentRider(request, rider)) return true; if (request.country !== rider.country || request.city !== rider.city) return true; if (normalizeRideVehicle(request.vehicle) !== normalizeRideVehicle(rider.vehicle)) return true; removedIds.add(request.id); return false; }); if (removedIds.size) { state.offers = state.offers.filter((offer) => !removedIds.has(offer.requestId)); state.chats = state.chats.filter((message) => !removedIds.has(message.requestId)); if (removedIds.has(state.selectedRequestId)) state.selectedRequestId = null; } return removedIds; } function marketplaceStateSnapshot() { return { requests: new Map(state.requests.map((request) => [request.id, { status: request.status, fareOffer: Number(request.fareOffer ?? 0), selectedOfferId: request.selectedOfferId ?? null, selectedRiderId: selectedRiderIdForRequest(request), matchedAt: request.matchedAt ?? null, country: request.country, city: request.city, vehicle: request.vehicle, passengerId: request.passengerId ?? null, createdAt: request.createdAt ?? null }])), offerIds: new Set(state.offers.map((offer) => offer.id)), offers: new Map(state.offers.map((offer) => [offer.id, { requestId: offer.requestId, riderId: offer.riderId, fare: Number(offer.fare ?? 0), type: offer.type ?? "", createdAt: offer.createdAt ?? null }])), riderOfferRequestIds: new Set(state.offers .filter((offer) => riderIdentityMatches(offer.riderId)) .map((offer) => offer.requestId)), chatIds: new Set(state.chats.map((message) => message.id)), routeChanges: new Map((state.routeChangeRequests ?? []).map((change) => [change.id, { requestId: change.requestId, status: change.status, type: change.type, requestedAt: change.requestedAt ?? null, decidedAt: change.decidedAt ?? null }])), notificationIds: new Set(state.notifications.map((notification) => notification.id)) }; } function accountIdForNoticeRole(type) { return type === "rider" ? state.rider?.id : state.passenger?.id; } function incomingChatMessageForRole(message, type, request) { if (!message || message.sender === "system" || !request) return false; if (!["matched", "arrived", "in_progress"].includes(request.status)) return false; const accountId = accountIdForNoticeRole(type); if (!accountId) return false; if (message.sender === type) return false; if (message.senderId === accountId && !["passenger", "rider"].includes(message.sender)) return false; if (type === "passenger") return requestBelongsToPassenger(request); if (type === "rider") return selectedRiderIdForRequest(request) === accountId; return false; } function chatNoticeBodyForRole(type, message) { const sender = type === "rider" ? "Passenger" : "Rider"; const preview = String(message?.text ?? "").replace(/\s+/g, " ").trim(); const clipped = preview.length > 90 ? `${preview.slice(0, 87)}...` : preview; return clipped ? `${sender}: ${clipped}` : `${sender} sent a ride chat message.`; } function loadedNoticeIsFreshForPopup(notice) { const createdAt = new Date(notice?.createdAt ?? 0).getTime(); if (!Number.isFinite(createdAt) || createdAt <= 0) return false; return Date.now() - createdAt <= marketplaceLoadedNoticePopupMaxAgeMs; } function deliverLoadedAccountNotifications(type, previous) { if (!previous?.notificationIds || !hasSignedIn(type)) return; uniqueNotificationsByDeliveryKey(currentAccountNotifications(type) .filter((notice) => !previous.notificationIds.has(notice.id)) .filter(loadedNoticeIsFreshForPopup)) .slice(0, 5) .reverse() .forEach((notice) => { deliverAccountNotificationNotice(type, notice, { autoFocusRide: true, deliverPhone: true, phoneTagPrefix: "loaded" }); }); } function riderActiveRideNavigationUrl(request) { if (!request || !requestIsActiveForCurrentRider(request)) return ""; if (typeof riderPickupNavigationShouldWaitForDropoff === "function" && riderPickupNavigationShouldWaitForDropoff(request)) return ""; if (request.status === "matched") return riderPickupNavigationUrl(request); if (request.status === "in_progress") return nextRideLegNavigationUrl(request); return ""; } function riderActiveRideNavigationKey(request, prefix = "rider-active-nav") { const leg = nextRideLeg(request); const timestamp = request.status === "matched" ? request.matchedAt || request.selectedOfferId || "match" : request.status === "in_progress" ? request.startedAt || request.lastStopArrivedAt || request.updatedAt || "in-progress" : request.updatedAt || "ride"; return `${prefix}-${request.id}-${request.status}-${leg.type}-${leg.index ?? rideStopIndex(request)}-${timestamp}`; } function openRiderActiveRideNavigation(request, eventKey = "") { if (!request || !requestIsActiveForCurrentRider(request)) return false; const url = riderActiveRideNavigationUrl(request); if (!url) return false; const key = eventKey || riderActiveRideNavigationKey(request); const opened = openNavigationUrl(url, { auto: true }); if (opened) { state.notificationPopupIds = [...new Set([...(state.notificationPopupIds ?? []), key])].slice(-100); saveState(); } return opened; } function openRiderPickupNavigation(request, eventKey = "") { if (!request || request.status !== "matched") return false; return openRiderActiveRideNavigation(request, eventKey || riderActiveRideNavigationKey(request, "rider-pickup-nav")); } async function refreshMatchedRequestForPickupNavigation(requestId) { if (!requestId || !canRefreshMarketplace()) return state.requests.find((request) => request.id === requestId) ?? null; const startedAt = Date.now(); while (marketRefreshInFlight && Date.now() - startedAt < 3000) { await new Promise((resolve) => window.setTimeout(resolve, 120)); } if (!marketRefreshInFlight && typeof loadMarketplaceFromSupabase === "function") { await loadMarketplaceFromSupabase({ includeAccountData: true }); if (typeof renderAll === "function") renderAll(); } return state.requests.find((request) => request.id === requestId) ?? null; } async function openRiderPickupNavigationWhenPrecise(requestOrId, eventKey = "") { const requestId = typeof requestOrId === "string" ? requestOrId : requestOrId?.id; if (!requestId) return false; let request = typeof requestOrId === "string" ? state.requests.find((item) => item.id === requestId) : requestOrId; let navKey = eventKey || (request ? riderActiveRideNavigationKey(request, "rider-pickup-nav") : `rider-pickup-nav-${requestId}`); if (!shouldOpenRiderPickupNavigation(request, navKey)) return false; if (!requestHasPrecisePickupNavigation(request)) { request = await refreshMatchedRequestForPickupNavigation(requestId) ?? request; navKey = eventKey || riderActiveRideNavigationKey(request, "rider-pickup-nav"); } if (!requestHasPrecisePickupNavigation(request)) { logClientWarning("Skipped automatic rider pickup navigation until exact pickup GPS or street address is available.", { requestId }); showRiderPickupNavigationClarification(request); return false; } if (!shouldOpenRiderPickupNavigation(request, navKey)) return false; return openRiderPickupNavigation(request, navKey); } async function openRiderPickupNavigationAfterFareAccepted(requestOrId, eventKey = "") { const requestId = typeof requestOrId === "string" ? requestOrId : requestOrId?.id; if (!requestId) return false; let request = typeof requestOrId === "string" ? state.requests.find((item) => item.id === requestId) : requestOrId; let navKey = eventKey || (request ? riderActiveRideNavigationKey(request, "rider-pickup-nav") : `rider-pickup-nav-${requestId}`); if (!shouldOpenRiderPickupNavigation(request, navKey)) return false; if (!requestHasPrecisePickupNavigation(request)) { request = await refreshMatchedRequestForPickupNavigation(requestId) ?? request; navKey = eventKey || riderActiveRideNavigationKey(request, "rider-pickup-nav"); if (!shouldOpenRiderPickupNavigation(request, navKey)) return false; } if (!requestHasPrecisePickupNavigation(request)) { logClientWarning("Skipped rider pickup navigation after fare acceptance because pickup GPS or street address is not precise enough.", { requestId, pickup: pickupMapsDestination(request) }); showRiderPickupNavigationClarification(request); return false; } return openRiderPickupNavigation(request, navKey); } function shouldOpenRiderActiveRideNavigation(request, eventKey) { return Boolean(request && eventKey && requestIsActiveForCurrentRider(request) && ["matched", "in_progress"].includes(request.status) && !(typeof riderPickupNavigationShouldWaitForDropoff === "function" && riderPickupNavigationShouldWaitForDropoff(request)) && !(state.notificationPopupIds ?? []).includes(eventKey)); } function shouldOpenRiderPickupNavigation(request, eventKey) { return Boolean(request && request.status === "matched" && shouldOpenRiderActiveRideNavigation(request, eventKey)); } function ensureRiderActiveRideNavigation() { if (activeRole() !== "rider" || !state.rider?.id) return false; const request = activeRideForRole(selectedRequest()); if (!request || !["matched", "in_progress"].includes(request.status)) return false; const key = riderActiveRideNavigationKey(request); if (!shouldOpenRiderActiveRideNavigation(request, key)) return false; return openRiderActiveRideNavigation(request, key); } function openQueuedRiderPickupAfterDropoff(completedRequestId = "") { if (activeRole() !== "rider" || !state.rider?.id) return false; const request = queuedRiderPickupAfterDropoff(completedRequestId); if (!request) return false; state.selectedRequestId = request.id; state.riderPage = "requests"; if (typeof updateRiderWorkspaceRoute === "function") { updateRiderWorkspaceRoute("requests", { replace: true, requestId: request.id }); } saveState(); const navKey = riderActiveRideNavigationKey(request, "rider-pickup-nav-after-dropoff"); if (typeof openRiderPickupNavigationWhenPrecise === "function") { void openRiderPickupNavigationWhenPrecise(request, navKey); return true; } if (requestHasPrecisePickupNavigation(request) && shouldOpenRiderPickupNavigation(request, navKey)) { return openRiderPickupNavigation(request, navKey); } return false; } function showMandatoryPassengerRideReopenedNotice(request, eventKey = "", actorId = "") { if (!request?.id || activeRole() !== "passenger" || !state.passenger?.id) return; const title = "Rider cancelled before pickup"; const body = "The rider cancelled before pickup. Your ride request is still open in the marketplace for another rider to accept."; const canonicalNotice = { id: `notice-passenger-ride-reopened-${request.id}-${eventKey || request.releasedAt || "now"}`, recipientRole: "passenger", title, body, requestId: request.id, eventType: "ride_reopened", createdBy: actorId || null, createdByRole: actorId ? "rider" : null }; const key = `mandatory-passenger-ride-reopened-${request.id}-${actorId || "rider"}-${eventKey || request.releasedAt || "now"}`; if ((state.notificationPopupIds ?? []).includes(key) || noticePopupAlreadyShown(canonicalNotice) || visibleNoticePopupAlreadyShown(canonicalNotice)) return; rememberNoticePopup(canonicalNotice); state.notificationPopupIds = [...new Set([...(state.notificationPopupIds ?? []), key])].slice(-140); saveState(); if (document.hidden) { void showRideTransactionPhoneNotification({ role: "passenger", page: "trips", request, requestId: request.id, title, body, tag: `waka-passenger-${key}` }); return; } playNearbyRideCue(); const node = document.createElement("aside"); node.className = "notice-popup notice-popup-actionable"; node.dataset.noticeDeliveryKey = noticePopupDeliveryKey(canonicalNotice); node.setAttribute("role", "status"); node.innerHTML = `
Passenger notice ${escapeHtml(title)}

${escapeHtml(body)}

`; node.querySelector(".notice-popup-open")?.addEventListener("click", () => { focusPassengerRequestView(request, { refresh: true, replace: true }); node.remove(); }); node.querySelector(".notice-popup-close")?.addEventListener("click", () => node.remove()); document.body.append(node); window.setTimeout(() => node.remove(), 16000); } function notifyMarketplaceChanges(previous) { if (!previous) return; if (activeRole() === "passenger" && state.passenger?.id) { const passengerRequests = state.requests.filter((request) => requestBelongsToPassenger(request)); const passengerRequestIds = new Set(passengerRequests.map((request) => request.id)); passengerRequests.forEach((request) => { const before = previous.requests.get(request.id); const matchedNow = selectedRiderIdForRequest(request) && ["matched", "arrived", "in_progress"].includes(request.status); const wasMatched = before?.selectedRiderId && ["matched", "arrived", "in_progress"].includes(before.status); if (matchedNow && !wasMatched) { state.selectedRequestId = request.id; state.passengerPage = "trips"; if (typeof updatePassengerWorkspaceRoute === "function") updatePassengerWorkspaceRoute("trips", { replace: true, requestId: request.id }); if (typeof forcePassengerApproachRefreshNow === "function") forcePassengerApproachRefreshNow("passenger_match_detected"); if (passengerInitiatedRideMatchRequestIds.has(request.id)) { focusPassengerRequestView(request, { refresh: false, replace: true }); return; } addRideAccountNotice( "passenger", `Rider ${selectedRiderFirstNameForRequest(request)} accepted your fare`, `Rider ${selectedRiderFirstNameForRequest(request)} accepted your fare. Track pickup from your ride card.`, request.id, `matched-${request.id}-${request.matchedAt || request.selectedOfferId || "now"}`, { createdBy: selectedRiderIdForRequest(request) || null, createdByRole: selectedRiderIdForRequest(request) ? "rider" : null } ); } if (before && before.status !== request.status) { if (request.status === "open" && requestReopenedAfterRiderCancellation(request, before)) { state.selectedRequestId = request.id; state.passengerPage = "trips"; if (typeof updatePassengerWorkspaceRoute === "function") updatePassengerWorkspaceRoute("trips", { replace: true, requestId: request.id }); const popupCountBeforeNotice = document.querySelectorAll(".notice-popup").length; addRideAccountNotice( "passenger", "Rider cancelled before pickup", "The rider cancelled before pickup. Your ride request is still open in the marketplace for another rider to accept.", request.id, `ride_reopened-${request.id}-${request.cancelledAt || request.releasedAt || "now"}`, { createdBy: before.selectedRiderId || null, createdByRole: before.selectedRiderId ? "rider" : null } ); if (document.querySelectorAll(".notice-popup").length <= popupCountBeforeNotice) { showMandatoryPassengerRideReopenedNotice(request, `${request.cancelledAt || request.releasedAt || "now"}`, before.selectedRiderId || ""); } } else if (request.status === "arrived") { addRideAccountNotice( "passenger", "Rider arrived at pickup", "Your rider has arrived at the pickup point. Meet the rider when you are ready.", request.id, `arrived-${request.id}-${request.arrivedAt || "now"}`, { createdBy: selectedRiderIdForRequest(request) || null, createdByRole: selectedRiderIdForRequest(request) ? "rider" : null } ); } else if (request.status === "in_progress") { addRideAccountNotice( "passenger", "Ride started", "Rider confirmed pickup. The ride is now in progress.", request.id, `started-${request.id}-${request.startedAt || "now"}`, { createdBy: selectedRiderIdForRequest(request) || null, createdByRole: selectedRiderIdForRequest(request) ? "rider" : null } ); } else if (request.status === "completed") { addRideAccountNotice( "passenger", "Ride completed", "Rider marked drop-off complete. Fare settlement is being processed.", request.id, `completed-${request.id}-${request.completedAt || "now"}`, { createdBy: selectedRiderIdForRequest(request) || null, createdByRole: selectedRiderIdForRequest(request) ? "rider" : null } ); } } }); state.offers.forEach((offer) => { const beforeOffer = previous.offers?.get(offer.id); const offerChanged = Boolean(beforeOffer && ( Number(beforeOffer.fare ?? 0) !== Number(offer.fare ?? 0) || beforeOffer.type !== (offer.type ?? "") || beforeOffer.createdAt !== (offer.createdAt ?? null) )); if (!passengerRequestIds.has(offer.requestId) || (beforeOffer && !offerChanged)) return; const request = state.requests.find((item) => item.id === offer.requestId); if (!request || request.status !== "open") return; const rider = stateLookupIndexes().riderMap.get(offer.riderId); const formattedFare = formatMoney(offer.fare, request.country); const riderFirstName = firstNameOnly(rider?.name, "Rider"); const title = `Rider ${riderFirstName} updated fare offer to ${formattedFare}`; addRideAccountNotice( "passenger", title, `Rider ${riderFirstName} updated fare offer to ${formattedFare}. Review the offer to accept or counter.`, request.id, `offer-${offer.id}-${offer.updatedAt || offer.createdAt || "updated"}-${offer.fare}`, { createdBy: offer.riderId || null, createdByRole: offer.riderId ? "rider" : null } ); }); state.chats.forEach((message) => { if (previous.chatIds.has(message.id)) return; const request = state.requests.find((item) => item.id === message.requestId); if (!incomingChatMessageForRole(message, "passenger", request)) return; addRideAccountNotice( "passenger", "New ride message", chatNoticeBodyForRole("passenger", message), request.id, `chat-${message.id}` ); }); (state.routeChangeRequests ?? []).forEach((change) => { const before = previous.routeChanges?.get(change.id); if (before?.status === change.status || !["accepted", "declined"].includes(change.status)) return; const request = state.requests.find((item) => item.id === change.requestId); if (!request || !requestBelongsToPassenger(request)) return; const label = routeChangeTypeLabel(change.type).toLowerCase(); const title = change.status === "accepted" ? "Rider accepted route change" : "Rider declined route change"; const body = change.status === "accepted" ? `The rider accepted the ${label}. The route and fare are updated.` : `The rider declined the ${label}. The agreed route and fare stay unchanged.`; addRideAccountNotice( "passenger", title, body, request.id, `route-change-${change.status}-${change.id}-${change.decidedAt || "now"}`, { createdBy: selectedRiderIdForRequest(request) || null, createdByRole: selectedRiderIdForRequest(request) ? "rider" : null } ); }); deliverLoadedAccountNotifications("passenger", previous); } if (activeRole() === "rider" && state.rider?.id) { const currentRequestIds = new Set(state.requests.map((request) => request.id)); state.requests.forEach((request) => { const before = previous.requests.get(request.id); const wasRelevantToRider = Boolean(before && ( riderIdentityMatches(before.selectedRiderId) || previous.riderOfferRequestIds?.has(request.id) || state.selectedRequestId === request.id )); if (wasRelevantToRider && before.status !== request.status && request.status === "open" && requestReopenedAfterRiderCancellation(request, before)) { if (state.selectedRequestId === request.id) state.selectedRequestId = null; return; } if (wasRelevantToRider && before.status !== request.status && request.status === "cancelled") { if (state.selectedRequestId === request.id) state.selectedRequestId = null; addRiderRideNotice( "Ride cancelled", rideCancelledNoticeBodyForRider(request), request.id, `cancelled-${request.id}-${request.cancelledAt || "now"}`, { createdBy: request.passengerId || null, createdByRole: request.passengerId ? "passenger" : null } ); return; } const matchedToRider = requestIsActiveForCurrentRider(request); const wasMatchedToRider = riderIdentityMatches(before?.selectedRiderId) && ["matched", "arrived", "in_progress"].includes(before.status); if (matchedToRider && !wasMatchedToRider) { const currentRide = riderInProgressImmediateRide(currentRiderRecord()); const queuedUntilDropoff = riderPickupNavigationShouldWaitForDropoff(request); if (queuedUntilDropoff && currentRide?.id && currentRide.id !== request.id) { state.selectedRequestId = currentRide.id; state.riderPage = "requests"; if (typeof updateRiderWorkspaceRoute === "function") updateRiderWorkspaceRoute("requests", { replace: true, requestId: currentRide.id }); } else if (!riderHasActiveRequestDecisionInProgress(request.id)) { state.selectedRequestId = request.id; state.riderPage = "requests"; if (typeof updateRiderWorkspaceRoute === "function") updateRiderWorkspaceRoute("requests", { replace: true, requestId: request.id }); } addRiderRideNotice( "Passenger accepted your offer", queuedUntilDropoff ? `Passenger accepted your offer. This next pickup is queued until the current ride is dropped off.` : `Passenger accepted your offer. Head to ${requestPickupDisplayText(request)} using your saved navigation preference.`, request.id, `matched-${request.id}-${request.matchedAt || request.selectedOfferId || "now"}`, { createdBy: request.passengerId || null, createdByRole: request.passengerId ? "passenger" : null } ); if (!queuedUntilDropoff) { const navKey = riderActiveRideNavigationKey(request, "rider-pickup-nav"); if (typeof openRiderPickupNavigationAfterFareAccepted === "function") { void openRiderPickupNavigationAfterFareAccepted(request, navKey); } else if (typeof openRiderPickupNavigationWhenPrecise === "function") { void openRiderPickupNavigationWhenPrecise(request, navKey); } else if (requestHasPrecisePickupNavigation(request) && shouldOpenRiderPickupNavigation(request, navKey)) { openRiderPickupNavigation(request, navKey); } } } else if (matchedToRider && before && before.status !== request.status && request.status === "in_progress") { const navKey = riderActiveRideNavigationKey(request, "rider-next-leg-nav"); if (shouldOpenRiderActiveRideNavigation(request, navKey)) openRiderActiveRideNavigation(request, navKey); } else if (request.status === "open" && before && Number(request.fareOffer ?? 0) > Number(before.fareOffer ?? 0)) { const ownOffer = state.offers.find((offer) => offer.requestId === request.id && riderIdentityMatches(offer.riderId)); if (ownOffer) { const formattedFare = formatMoney(request.fareOffer, request.country); const passengerFirstName = passengerFirstNameForRequest(request); addRiderRideNotice( `Passenger ${passengerFirstName} updated fare offer to ${formattedFare}`, `Passenger ${passengerFirstName} updated fare offer to ${formattedFare}. Review the request to accept or counter.`, request.id, `fare-${request.id}-${request.fareOffer}`, { createdBy: request.passengerId || null, createdByRole: request.passengerId ? "passenger" : null } ); } } }); (state.routeChangeRequests ?? []).forEach((change) => { const before = previous.routeChanges?.get(change.id); if (before?.status === change.status || change.status !== "pending") return; const request = state.requests.find((item) => item.id === change.requestId); if (!request || !riderIdentityMatches(selectedRiderIdForRequest(request)) || !["matched", "arrived", "in_progress"].includes(request.status)) return; showRiderRouteChangeDecisionForRequest(request, change, { render: false }); const label = routeChangeTypeLabel(change.type).toLowerCase(); const addOn = formatMoney(change.additionalFare, request.country); const total = formatMoney(change.totalFare, request.country); addRiderRideNotice( change.type === "add_stop" ? "Passenger requested an added stop" : "Passenger requested a destination change", `Review the ${label} now. Added fare: ${addOn}. New total if accepted: ${total}.`, request.id, `route-change-requested-${change.id}-${change.requestedAt || "now"}`, { autoFocus: false, createdBy: request.passengerId || null, createdByRole: request.passengerId ? "passenger" : null } ); }); previous.requests.forEach((before, requestId) => { if (currentRequestIds.has(requestId)) return; const sameMarketplace = before?.country === state.rider.country && before?.city === state.rider.city; const wasSelected = state.selectedRequestId === requestId; const wasRelevantToRider = riderIdentityMatches(before?.selectedRiderId) || previous.riderOfferRequestIds?.has(requestId) || (wasSelected && sameMarketplace); if (!wasRelevantToRider) return; if (state.selectedRequestId === requestId) state.selectedRequestId = null; state.offers = state.offers.filter((offer) => offer.requestId !== requestId); state.chats = state.chats.filter((message) => message.requestId !== requestId); addRiderRideNotice( "Ride request unavailable", "That passenger request is no longer available. It has been removed from your marketplace.", requestId, `no-longer-available-${requestId}`, { createdBy: before.passengerId || null, createdByRole: before.passengerId ? "passenger" : null } ); }); state.chats.forEach((message) => { if (previous.chatIds.has(message.id)) return; const request = state.requests.find((item) => item.id === message.requestId); if (!incomingChatMessageForRole(message, "rider", request)) return; addRiderRideNotice( "New ride message", chatNoticeBodyForRole("rider", message), request.id, `chat-${message.id}` ); }); deliverLoadedAccountNotifications("rider", previous); saveState(); } } async function loadMarketplaceFromSupabase({ includeAccountData = false } = {}) { if (!hasSupabaseRuntime() || (!hasSignedIn("passenger") && !hasSignedIn("rider"))) return; const userIds = currentMarketplaceUserIds(); const passengerIds = uniqueMarketplaceIds([state.passenger?.id, state.passenger?.supabaseUserId, state.sessions?.passenger?.userId]); const riderIds = riderIdentityIds(); const shouldLoadAccountData = Boolean(includeAccountData); const preloadedRiderDayPreferencesResult = riderIds.length ? await selectScopedMarketplaceTable("rider_day_preferences", "updated_at", [marketplaceInFilter("rider_id", riderIds)]) : { data: [] }; rememberRiderDayPreferences((preloadedRiderDayPreferencesResult.data ?? []).map((preference) => mapRiderDayPreferenceFromDatabase(preference))); const [requestsResult, notificationsResult, financeAdjustmentsResult, paymentAccountsResult, businessAccountsResult, riderDayPreferencesResult, rideRatingsResult, rideSettlementsResult, rideTipsResult, routeChangesResult, riderOwnOffersResult] = await Promise.all([ selectMarketplaceRequests(), selectScopedMarketplaceTable("admin_notifications", "created_at", [marketplaceInFilter("recipient_id", userIds)]), shouldLoadAccountData ? selectScopedMarketplaceTable("finance_adjustments", "created_at", [marketplaceInFilter("subject_id", userIds)]) : emptyMarketplaceTableResult(), shouldLoadAccountData ? selectScopedMarketplaceTable("payment_accounts", "updated_at", [marketplaceInFilter("user_id", userIds)]) : emptyMarketplaceTableResult(), shouldLoadAccountData ? selectScopedMarketplaceTable("business_accounts", "updated_at", [marketplaceInFilter("owner_id", passengerIds)]) : emptyMarketplaceTableResult(), Promise.resolve(preloadedRiderDayPreferencesResult), shouldLoadAccountData ? selectAnyUserScopedMarketplaceTable("ride_ratings", "created_at", ["reviewer_id"]) : emptyMarketplaceTableResult(), shouldLoadAccountData ? selectAnyUserScopedMarketplaceTable("ride_payment_settlements", "created_at", ["passenger_id", "rider_id"]) : emptyMarketplaceTableResult(), shouldLoadAccountData ? selectAnyUserScopedMarketplaceTable("ride_tips", "created_at", ["passenger_id", "rider_id"]) : emptyMarketplaceTableResult(), selectRideRouteChangesForMarketplace(), riderIds.length ? selectScopedMarketplaceTable("ride_offers", "created_at", [marketplaceInFilter("rider_id", riderIds)]) : emptyMarketplaceTableResult() ]); const riderOwnOfferRows = riderOwnOffersResult.data ?? []; const riderOwnOfferIds = uniqueMarketplaceIds(riderOwnOfferRows.map((offer) => offer.id)); const riderMatchedRequestsResult = riderOwnOfferIds.length ? await selectScopedMarketplaceTable("ride_requests", "created_at", [marketplaceInFilter("selected_offer_id", riderOwnOfferIds)]) : emptyMarketplaceTableResult(); const combinedRequestsResult = mergeMarketplaceTableResults([requestsResult, riderMatchedRequestsResult]); const requestIds = uniqueMarketplaceIds((combinedRequestsResult.data ?? []).map((request) => request.id)); const [offersResult, chatsResult, riderCompletedMileageSegmentsResult, routeChangesByRequestResult] = await Promise.all([ selectScopedMarketplaceTable("ride_offers", "created_at", [marketplaceInFilter("ride_request_id", requestIds)]), selectScopedMarketplaceTable("ride_chats", "created_at", [marketplaceInFilter("ride_request_id", requestIds)]), shouldLoadAccountData && riderIds.length ? selectRiderCompletedMileageSegmentsForRequests(riderIds, requestIds) : emptyMarketplaceTableResult(), selectRideRouteChangesForRequests(requestIds) ]); const offers = mergeMarketplaceTableResults([offersResult, riderOwnOffersResult]).data.map(mapOfferFromDatabase); const offerMap = new Map(offers.map((offer) => [offer.id, offer])); const previousRequestMap = stateLookupIndexes().requestMap; const requests = (combinedRequestsResult.data ?? []).map((request) => { const mapped = mapRideRequestFromDatabase(request, new Map(), offerMap); return preserveRideRequestPickup(mapped, previousRequestMap.get(mapped.id)); }); const chats = (chatsResult.data ?? []).map(mapChatFromDatabase); const notifications = (notificationsResult.data ?? []).map(mapAdminNotificationFromDatabase); const financeAdjustments = (financeAdjustmentsResult.data ?? []).map((adjustment) => mapFinanceAdjustmentFromDatabase(adjustment)); const paymentAccounts = (paymentAccountsResult.data ?? []).map((account) => mapPaymentAccountFromDatabase(account)); const businessAccounts = (businessAccountsResult.data ?? []).map((account) => mapBusinessAccountFromDatabase(account)); const businessAccountIds = uniqueMarketplaceIds(businessAccounts.map((account) => account.id)); const businessSubscriptionsResult = shouldLoadAccountData && businessAccountIds.length ? await selectScopedMarketplaceTable("business_subscriptions", "updated_at", [ marketplaceInFilter("business_account_id", businessAccountIds) ]) : emptyMarketplaceTableResult(); const businessSubscriptions = (businessSubscriptionsResult.data ?? []).map(mapBusinessSubscriptionFromDatabase); const riderDayPreferences = (riderDayPreferencesResult.data ?? []).map((preference) => mapRiderDayPreferenceFromDatabase(preference)); const rideRatings = (rideRatingsResult.data ?? []).map((rating) => mapRideRatingFromDatabase(rating)); const rideSettlements = (rideSettlementsResult.data ?? []).map((settlement) => mapRideSettlementFromDatabase(settlement)); const rideTips = (rideTipsResult.data ?? []).map((tip) => mapRideTipFromDatabase(tip)); const routeChanges = mergeMarketplaceTableResults([routeChangesResult, routeChangesByRequestResult]).data.map(mapRideRouteChangeFromDatabase); const riderCompletedMileageSegments = (riderCompletedMileageSegmentsResult.data ?? []).map(mapRiderCompletedMileageSegmentFromDatabase); pruneStaleRiderMarketplaceRequests(new Set(requests.map((request) => request.id))); requests.forEach((request) => { state.requests = upsertById(state.requests, preserveRideRequestPickup(request, stateLookupIndexes().requestMap.get(request.id))); }); const refreshedRequestIdSet = new Set(requestIds); if (shouldLoadAccountData && riderIds.length) { state.riderCompletedMileageSegments = (state.riderCompletedMileageSegments ?? []) .filter((segment) => !(riderIds.includes(segment.riderId) && refreshedRequestIdSet.has(segment.requestId))); } state.offers = state.offers.filter((offer) => !refreshedRequestIdSet.has(offer.requestId)); state.chats = state.chats.filter((message) => !refreshedRequestIdSet.has(message.requestId)); offers.forEach((offer) => { state.offers = upsertById(state.offers, offer); }); chats.forEach((message) => { state.chats = upsertById(state.chats, message); }); routeChanges.forEach((change) => { state.routeChangeRequests = upsertById(state.routeChangeRequests ?? [], change); }); if (typeof mergeRouteChangeEventsFromChats === "function") { mergeRouteChangeEventsFromChats(state.chats); } notifications.forEach((notification) => { state.notifications = upsertNotificationByDeliveryKey(state.notifications, notification); }); financeAdjustments.forEach((adjustment) => { state.financeAdjustments = upsertById(state.financeAdjustments, adjustment); }); paymentAccounts.forEach((account) => { state.paymentAccounts = upsertById( state.paymentAccounts.filter((item) => !(item.role === account.role && item.userId === account.userId)), account ); }); businessAccounts.forEach((account) => { state.businessAccounts = upsertById(state.businessAccounts, account); }); businessSubscriptions.forEach((subscription) => { state.businessSubscriptions = upsertById(state.businessSubscriptions, subscription); }); rememberRiderDayPreferences(riderDayPreferences); rideRatings.forEach((rating) => { state.rideRatings = upsertById(state.rideRatings, rating); }); rideSettlements.forEach((settlement) => { state.rideSettlements = upsertById(state.rideSettlements, settlement); }); rideTips.forEach((tip) => { state.rideTips = upsertById(state.rideTips, tip); }); riderCompletedMileageSegments.forEach((segment) => { state.riderCompletedMileageSegments = upsertById(state.riderCompletedMileageSegments ?? [], segment); }); if (shouldLoadAccountData && riderIds.length && typeof loadMyRiderRatingSummaryFromSupabase === "function") { await loadMyRiderRatingSummaryFromSupabase(); } await loadPassengerApproachFromSupabase(); await loadActiveRideContactsFromSupabase(); saveState(); } async function refreshMarketplace({ silent = false, reason = "manual" } = {}) { if (!canRefreshMarketplace() || marketRefreshInFlight) return; const previous = marketplaceStateSnapshot(); marketRefreshInFlight = true; els.refreshMarket.disabled = true; if (!silent) els.selectedSummary.textContent = "Refreshing shared marketplace from Supabase..."; try { await expireRiderLiveGpsIfNeeded(); await loadMarketplaceFromSupabase({ includeAccountData: !silent }); notifyMarketplaceChanges(previous); reconcileRiderPendingRouteChangeDecision({ render: false }); lastMarketRefreshAt = new Date(); if (!silent) { els.selectedSummary.textContent = `Marketplace refreshed ${lastMarketRefreshAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.`; } renderAll(); ensureAccountNoticeAutoRefreshes(); notifyRiderAboutNearbyRequests(); } catch (error) { if (!silent) els.selectedSummary.textContent = `Market refresh failed: ${error.message}`; } finally { marketRefreshInFlight = false; els.refreshMarket.disabled = false; ensurePassengerApproachAutoRefresh(); ensureRiderMarketplaceAutoRefresh(); ensureAccountNoticeAutoRefreshes(); ensureMarketplaceRealtimeSubscription(); if (marketplaceRealtimeRefreshPendingReason && !document.hidden && canRefreshMarketplace()) { const pendingReason = marketplaceRealtimeRefreshPendingReason; marketplaceRealtimeRefreshPendingReason = ""; scheduleMarketplaceRealtimeRefresh(pendingReason); } } } function clearSelectedRequestOutsideLocation(country, city) { const request = selectedRequest(); if (request && (request.country !== country || request.city !== city)) { state.selectedRequestId = null; } } // Lazy draggable background map for the passenger request and rider initialize workspaces. const workspaceMapTileSize = 256; const workspaceMapDefaultZoom = 12; const workspaceMapExactZoom = 15; const workspaceMapFallbackCenter = { latitude: 39.0458, longitude: -76.6413 }; const workspaceMapMaxRenderedTiles = 80; const workspaceMapDeviceGpsMinimumRenderMs = 15 * 1000; const workspaceMapTileUsageStorageKey = "waka-mapbox-tile-usage-v1"; const workspaceMapTileUsageDefaultSoftLimit = 150000; const workspaceMapTileUsageDefaultHardLimit = 190000; let workspaceMapInitialized = false; let workspaceMapAnimationFrame = 0; let workspaceMapResizeTimer = 0; let workspaceMapDragState = null; let workspaceMapDeviceGps = null; let workspaceMapDeviceGpsWatchId = null; let workspaceMapDeviceGpsContextKey = ""; let workspaceMapLastDeviceGpsRenderAt = 0; let workspaceMapTileUsageAlertedKey = ""; const workspaceMapLoadedTileKeys = new Set(); const workspaceMapRoadSegmentCache = new Map(); const workspaceMapRoadSegmentInFlight = new Set(); let workspaceMapRoadSegmentRpcUnavailable = false; let workspaceMapState = { center: null, zoom: workspaceMapDefaultZoom, contextKey: "", modelKey: "", userPanned: false }; function workspaceMapElements() { return { root: document.querySelector("#workspaceBackgroundMap"), viewport: document.querySelector("#workspaceBackgroundMapViewport"), tiles: document.querySelector("#workspaceMapTiles"), routeLayer: document.querySelector("#workspaceMapRouteLayer"), markers: document.querySelector("#workspaceMapMarkers") }; } function workspaceMapToken() { return String(appConfig?.mapboxAccessToken || "").trim(); } function workspaceMapFlagEnabled(value, fallback = false) { if (typeof configFlagEnabled === "function") return configFlagEnabled(value ?? fallback); if (value == null) return Boolean(fallback); return value === true || value === "true" || value === 1 || value === "1"; } function workspaceMapTileMapsEnabled() { return workspaceMapFlagEnabled(appConfig?.mapboxTileMapsEnabled, true); } function workspaceMapTileUsageLimit(name, fallback) { const value = Number(appConfig?.[name] ?? fallback); return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : fallback; } function workspaceMapTileUsageMonthKey(now = new Date()) { return `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`; } function workspaceMapTileUsageRead() { const month = workspaceMapTileUsageMonthKey(); try { const stored = JSON.parse(localStorage.getItem(workspaceMapTileUsageStorageKey) || "{}"); if (stored?.month === month) { return { month, count: Math.max(0, Number(stored.count) || 0) }; } } catch {} return { month, count: 0 }; } function workspaceMapTileUsageWrite(record) { try { localStorage.setItem(workspaceMapTileUsageStorageKey, JSON.stringify({ month: record.month, count: Math.max(0, Number(record.count) || 0) })); } catch (error) { if (typeof logClientWarning === "function") logClientWarning("Mapbox tile usage estimate could not be saved.", error); } } function workspaceMapTileUsageSoftLimit() { return workspaceMapTileUsageLimit("mapboxTileRequestMonthlySoftLimit", workspaceMapTileUsageDefaultSoftLimit); } function workspaceMapTileUsageHardLimit() { return workspaceMapTileUsageLimit("mapboxTileRequestMonthlyHardLimit", workspaceMapTileUsageDefaultHardLimit); } function workspaceMapTileUsageHardLimitReached() { const hardLimit = workspaceMapTileUsageHardLimit(); return hardLimit > 0 && workspaceMapTileUsageRead().count >= hardLimit; } function workspaceMapTileUsageCanRequest(count = 1) { const hardLimit = workspaceMapTileUsageHardLimit(); if (hardLimit <= 0) return true; return workspaceMapTileUsageRead().count + count <= hardLimit; } function workspaceMapReportTileUsageLimit(record, limitType) { const alertKey = `${record.month}:${limitType}`; if (workspaceMapTileUsageAlertedKey === alertKey) return; workspaceMapTileUsageAlertedKey = alertKey; const softLimit = workspaceMapTileUsageSoftLimit(); const hardLimit = workspaceMapTileUsageHardLimit(); const detail = { month: record.month, estimatedTileRequests: record.count, softLimit, hardLimit, limitType }; if (typeof logClientWarning === "function") { logClientWarning(`Mapbox tile request estimate reached the configured ${limitType} limit.`, detail); } try { document.dispatchEvent(new CustomEvent("waka:mapbox-tile-usage-alert", { detail })); } catch {} } function workspaceMapRecordTileRequest(count = 1) { const record = workspaceMapTileUsageRead(); record.count += Math.max(0, Number(count) || 0); workspaceMapTileUsageWrite(record); const softLimit = workspaceMapTileUsageSoftLimit(); const hardLimit = workspaceMapTileUsageHardLimit(); if (hardLimit > 0 && record.count >= hardLimit) { workspaceMapReportTileUsageLimit(record, "hard"); } else if (softLimit > 0 && record.count >= softLimit) { workspaceMapReportTileUsageLimit(record, "soft"); } return record; } function workspaceMapTileMapsAvailable() { return workspaceMapTileMapsEnabled() && workspaceMapToken().length > 0 && !workspaceMapTileUsageHardLimitReached(); } function workspaceMapStylePath() { const style = String(appConfig?.mapboxStyleId || "mapbox/streets-v12").trim(); const parts = style.split("/").filter(Boolean); const owner = parts.length >= 2 ? parts[0] : "mapbox"; const styleId = parts.length >= 2 ? parts.slice(1).join("/") : "streets-v12"; return `${encodeURIComponent(owner)}/${encodeURIComponent(styleId)}`; } function workspaceMapPassengerHasMatchedRide() { if (typeof requestBelongsToPassenger !== "function") return false; const activeStatuses = new Set(["matched", "arrived", "in_progress", "completed"]); return (state.requests ?? []).some((request) => requestBelongsToPassenger(request) && activeStatuses.has(request?.status)); } function workspaceMapContext() { if (!workspaceMapTileMapsEnabled()) return null; const role = typeof activeRole === "function" ? activeRole() : state.activeTab; if (role === "passenger" && typeof passengerWorkspacePage === "function" && passengerWorkspacePage() === "request" && state.sessions?.passenger && state.passenger && els.rideRequestForm && !els.rideRequestForm.hidden) { return { role: "passenger", page: "request" }; } if (role === "rider" && typeof riderWorkspacePage === "function" && riderWorkspacePage() === "initialize" && state.sessions?.rider && state.rider) { return { role: "rider", page: "initialize" }; } return null; } function workspaceMapPoint(point, label, type) { const gps = normalizeGpsPoint(point); if (!gps) return null; return { latitude: gps.latitude, longitude: gps.longitude, label, type }; } function workspaceMapDecodeGooglePolyline(value) { const encoded = String(value || ""); const points = []; let index = 0; let latitude = 0; let longitude = 0; const decodeValue = () => { let result = 0; let shift = 0; let byte = 0; do { if (index >= encoded.length) return null; byte = encoded.charCodeAt(index) - 63; index += 1; result |= (byte & 0x1f) << shift; shift += 5; } while (byte >= 0x20); return (result & 1) ? ~(result >> 1) : (result >> 1); }; while (index < encoded.length) { const latitudeDelta = decodeValue(); const longitudeDelta = decodeValue(); if (latitudeDelta == null || longitudeDelta == null) break; latitude += latitudeDelta; longitude += longitudeDelta; const point = workspaceMapPoint({ latitude: latitude / 1e5, longitude: longitude / 1e5 }, "", "route"); if (point) points.push(point); } return points; } function workspaceMapRoutePathPointsFromPolyline(value) { return workspaceMapDecodeGooglePolyline(value); } function workspaceMapDevicePoint(label = "Y", type = "device") { return workspaceMapPoint(workspaceMapDeviceGps, label, type); } function workspaceMapSyncDeviceGpsWatch(context) { const contextKey = context ? `${context.role}:${context.page}` : ""; const shouldWatch = Boolean(context && navigator.geolocation); if (!shouldWatch) { if (workspaceMapDeviceGpsWatchId != null && navigator.geolocation?.clearWatch) { navigator.geolocation.clearWatch(workspaceMapDeviceGpsWatchId); } workspaceMapDeviceGpsWatchId = null; workspaceMapDeviceGpsContextKey = ""; workspaceMapLastDeviceGpsRenderAt = 0; return; } if (workspaceMapDeviceGpsWatchId != null && workspaceMapDeviceGpsContextKey === contextKey) return; if (workspaceMapDeviceGpsWatchId != null && navigator.geolocation?.clearWatch) { navigator.geolocation.clearWatch(workspaceMapDeviceGpsWatchId); } workspaceMapDeviceGpsContextKey = contextKey; workspaceMapLastDeviceGpsRenderAt = 0; const applyDeviceGpsPoint = (position, options = {}) => { const point = typeof gpsPointFromPosition === "function" ? gpsPointFromPosition(position) : null; if (!point) return; workspaceMapDeviceGps = point; const now = Date.now(); if (!options.forceRender && workspaceMapLastDeviceGpsRenderAt && now - workspaceMapLastDeviceGpsRenderAt < workspaceMapDeviceGpsMinimumRenderMs) { return; } workspaceMapLastDeviceGpsRenderAt = now; workspaceMapState.userPanned = false; scheduleWorkspaceBackgroundMapRender(); }; try { if (navigator.geolocation?.getCurrentPosition) { navigator.geolocation.getCurrentPosition( (position) => applyDeviceGpsPoint(position, { forceRender: true }), () => {}, { enableHighAccuracy: true, timeout: 8000, maximumAge: 0 } ); } workspaceMapDeviceGpsWatchId = navigator.geolocation.watchPosition( (position) => { applyDeviceGpsPoint(position); }, () => {}, { enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 } ); } catch { workspaceMapDeviceGpsWatchId = null; workspaceMapDeviceGpsContextKey = ""; } } function workspaceMapTownCenter(country, city, name) { if (typeof launchGpsTownCenterForName === "function") { return launchGpsTownCenterForName(country, city, name); } return null; } function workspaceMapFirstTownCenter(country, city) { const first = typeof launchGpsTownCenters === "object" ? launchGpsTownCenters?.[country]?.[city]?.[0] : null; return workspaceMapPoint(first || workspaceMapFallbackCenter, "", "center"); } function workspaceMapPassengerModel() { const country = typeof selectedPassengerCountry === "function" ? selectedPassengerCountry() : state.passenger?.country; const city = typeof selectedRidePickupCity === "function" ? selectedRidePickupCity() : state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(country); const pickupText = els.pickupDescription?.value ?? ""; const pickupGps = typeof passengerPickupGpsForFormChoice === "function" ? passengerPickupGpsForFormChoice() : null; const devicePoint = workspaceMapDevicePoint("Y", "device"); const pickupOrigin = typeof routeOriginForEstimate === "function" ? routeOriginForEstimate(country, city, els.pickupArea?.value, pickupText, pickupGps) : null; const pickup = workspaceMapPoint(pickupOrigin, "P", "pickup") || workspaceMapPoint(pickupGps, "P", "pickup") || workspaceMapPoint(workspaceMapTownCenter(country, city, els.pickupArea?.value), "P", "pickup"); const destinationText = String(els.destination?.value ?? "").trim(); const destinationPlace = typeof destinationPlaceForRoute === "function" ? destinationPlaceForRoute(destinationText) : null; const destination = destinationText ? workspaceMapPoint(destinationPlace, "D", "destination") || workspaceMapPoint(workspaceMapTownCenter(country, city, els.destinationArea?.value), "D", "destination") : null; const stopPoints = typeof normalizeRideStops === "function" && typeof stopRoutePoint === "function" && typeof rideStopsFormValue === "function" ? normalizeRideStops(rideStopsFormValue()).map((stop) => workspaceMapPoint(stopRoutePoint(stop), "", "stop")).filter(Boolean) : []; const markers = [devicePoint, pickup, ...stopPoints, destination].filter(Boolean); const routePoints = [pickup, ...stopPoints, destination].filter(Boolean); const fallback = workspaceMapFirstTownCenter(country, city); const exactCenter = devicePoint || pickup; const guidanceKey = typeof routeGuidanceInputKey === "function" ? routeGuidanceInputKey( country, city, els.pickupArea?.value, els.destinationArea?.value, pickupText, destinationText, typeof rideStopsFormValue === "function" ? rideStopsFormValue() : [], pickupGps, destinationPlace ) : ""; const guidance = guidanceKey && typeof cachedConfirmedFareGuidanceForKey === "function" ? cachedConfirmedFareGuidanceForKey(guidanceKey) : null; const routePathPoints = workspaceMapRoutePathPointsFromPolyline(guidance?.routePolyline); return { country, city, contextKey: "passenger:request", modelKey: [devicePoint, ...routePoints, ...routePathPoints].filter(Boolean).map((point) => `${point.type}:${point.latitude.toFixed(5)},${point.longitude.toFixed(5)}`).join("|") || `${country}:${city}:${els.pickupArea?.value || ""}:${els.destinationArea?.value || ""}`, center: exactCenter || workspaceMapCenterForPoints(routePathPoints.length ? routePathPoints : routePoints.length ? routePoints : markers.length ? markers : [fallback]), markers: markers.length ? markers : [fallback], routePoints, routePathPoints, zoom: exactCenter ? workspaceMapExactZoom : 13 }; } function rideRequestRoutePreviewModel() { const country = typeof selectedPassengerCountry === "function" ? selectedPassengerCountry() : state.passenger?.country; const city = typeof selectedRidePickupCity === "function" ? selectedRidePickupCity() : state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(country); const pickupText = els.pickupDescription?.value ?? ""; const pickupGps = typeof passengerPickupGpsForFormChoice === "function" ? passengerPickupGpsForFormChoice() : null; const pickupOrigin = typeof routeOriginForEstimate === "function" ? routeOriginForEstimate(country, city, els.pickupArea?.value, pickupText, pickupGps) : null; const pickup = workspaceMapPoint(pickupOrigin, "P", "pickup") || workspaceMapPoint(pickupGps, "P", "pickup") || workspaceMapPoint(workspaceMapTownCenter(country, city, els.pickupArea?.value), "P", "pickup"); const destinationText = String(els.destination?.value ?? "").trim(); const destinationPlace = destinationText && typeof destinationPlaceForRoute === "function" ? destinationPlaceForRoute(destinationText) : null; const destination = destinationText ? workspaceMapPoint(destinationPlace, "D", "destination") || workspaceMapPoint(workspaceMapTownCenter(country, city, els.destinationArea?.value), "D", "destination") : null; const stopPoints = typeof normalizeRideStops === "function" && typeof stopRoutePoint === "function" && typeof rideStopsFormValue === "function" ? normalizeRideStops(rideStopsFormValue()).map((stop) => workspaceMapPoint(stopRoutePoint(stop), "", "stop")).filter(Boolean) : []; const fallback = workspaceMapFirstTownCenter(country, city); const markers = [pickup, ...stopPoints, destination].filter(Boolean); const routePoints = [pickup, ...stopPoints, destination].filter(Boolean); const guidanceKey = typeof routeGuidanceInputKey === "function" ? routeGuidanceInputKey( country, city, els.pickupArea?.value, els.destinationArea?.value, pickupText, destinationText, typeof rideStopsFormValue === "function" ? rideStopsFormValue() : [], pickupGps, destinationPlace ) : ""; const guidance = guidanceKey && typeof cachedConfirmedFareGuidanceForKey === "function" ? cachedConfirmedFareGuidanceForKey(guidanceKey) : null; const routePathPoints = workspaceMapRoutePathPointsFromPolyline(guidance?.routePolyline); const points = routePathPoints.length ? routePathPoints : routePoints.length ? routePoints : markers.length ? markers : [fallback]; return { country, city, center: workspaceMapCenterForPoints(points), markers: markers.length ? markers : [fallback], routePoints, routePathPoints, zoom: routePoints.length >= 2 ? 13 : 12 }; } function workspaceMapRiderModel() { const rider = typeof currentRiderRecord === "function" ? currentRiderRecord() : state.rider; const country = rider?.country ?? (typeof selectedRiderCountry === "function" ? selectedRiderCountry() : state.rider?.country); const city = rider?.city ?? (typeof selectedRiderCity === "function" ? selectedRiderCity() : state.rider?.city); const devicePoint = workspaceMapPoint( (typeof riderCurrentGps === "function" ? riderCurrentGps(rider) : null) || workspaceMapDeviceGps, "R", "rider" ); const riderPoint = devicePoint || workspaceMapPoint(workspaceMapTownCenter(country, city, rider?.area ?? els.riderActiveArea?.value), "R", "rider") || workspaceMapFirstTownCenter(country, city); const requestMarkers = (state.requests || []) .filter((request) => request?.status === "open" && request?.country === country && request?.city === city) .map((request) => workspaceMapPoint( typeof requestPickupGps === "function" ? requestPickupGps(request) : null, "F", "request" )) .filter(Boolean) .slice(0, 10); const markers = [riderPoint, ...requestMarkers].filter(Boolean); return { country, city, contextKey: "rider:initialize", modelKey: markers.map((point) => `${point.type}:${point.latitude.toFixed(5)},${point.longitude.toFixed(5)}`).join("|") || `${country}:${city}:${rider?.area || ""}`, center: devicePoint || riderPoint, markers, routePoints: [], zoom: devicePoint ? workspaceMapExactZoom : Number(appConfig?.riderInitializeMapZoom) || 13 }; } function workspaceMapModel(context = workspaceMapContext()) { if (!context) return null; return context.role === "rider" ? workspaceMapRiderModel() : workspaceMapPassengerModel(); } function workspaceMapCenterForPoints(points) { const valid = points.filter(Boolean); if (!valid.length) return workspaceMapFallbackCenter; const sum = valid.reduce((total, point) => ({ latitude: total.latitude + point.latitude, longitude: total.longitude + point.longitude }), { latitude: 0, longitude: 0 }); return { latitude: sum.latitude / valid.length, longitude: sum.longitude / valid.length }; } function workspaceMapClampLatitude(latitude) { return Math.max(-85.05112878, Math.min(85.05112878, Number(latitude) || 0)); } function workspaceMapWorldPoint(point, zoom) { const latitude = workspaceMapClampLatitude(point?.latitude); const longitude = Number(point?.longitude) || 0; const sin = Math.sin((latitude * Math.PI) / 180); const scale = workspaceMapTileSize * (2 ** zoom); return { x: ((longitude + 180) / 360) * scale, y: (0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI)) * scale }; } function workspaceMapLatLngFromWorld(world, zoom) { const scale = workspaceMapTileSize * (2 ** zoom); const longitude = (world.x / scale) * 360 - 180; const n = Math.PI - (2 * Math.PI * world.y) / scale; const latitude = (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))); return { latitude: workspaceMapClampLatitude(latitude), longitude: ((longitude + 540) % 360) - 180 }; } function workspaceMapZoomForPoints(points, viewport, fallbackZoom) { const valid = points.filter(Boolean); if (valid.length < 2 || !viewport?.clientWidth || !viewport?.clientHeight) return fallbackZoom; for (let zoom = 15; zoom >= 8; zoom -= 1) { const pixels = valid.map((point) => workspaceMapWorldPoint(point, zoom)); const xs = pixels.map((point) => point.x); const ys = pixels.map((point) => point.y); const spanX = Math.max(...xs) - Math.min(...xs); const spanY = Math.max(...ys) - Math.min(...ys); if (spanX <= viewport.clientWidth * 0.68 && spanY <= viewport.clientHeight * 0.55) return zoom; } return 8; } function workspaceMapTileUrl(zoom, x, y) { const token = workspaceMapToken(); if (!token) return ""; return `https://api.mapbox.com/styles/v1/${workspaceMapStylePath()}/tiles/${workspaceMapTileSize}/${zoom}/${x}/${y}@2x?access_token=${encodeURIComponent(token)}`; } function workspaceMapRenderTiles(elements, centerWorld, zoom, options = {}) { const { root, viewport, tiles } = elements; const token = options.useMapboxTiles === false || !workspaceMapTileMapsAvailable() ? "" : workspaceMapToken(); root.classList.toggle("workspace-map-fallback", !token); if (!token) { if (tiles.childElementCount) tiles.replaceChildren(); return; } const width = viewport.clientWidth || window.innerWidth; const height = viewport.clientHeight || window.innerHeight; const leftWorld = centerWorld.x - width / 2; const topWorld = centerWorld.y - height / 2; const minX = Math.floor(leftWorld / workspaceMapTileSize) - 1; const maxX = Math.floor((leftWorld + width) / workspaceMapTileSize) + 1; const minY = Math.floor(topWorld / workspaceMapTileSize) - 1; const maxY = Math.floor((topWorld + height) / workspaceMapTileSize) + 1; const maxTile = 2 ** zoom; const existingTiles = new Map(Array.from(tiles.children) .filter((node) => node instanceof HTMLImageElement && node.dataset.tileKey) .map((node) => [node.dataset.tileKey, node])); const fragment = document.createDocumentFragment(); let rendered = 0; for (let tileY = minY; tileY <= maxY; tileY += 1) { if (tileY < 0 || tileY >= maxTile) continue; for (let tileX = minX; tileX <= maxX; tileX += 1) { if (rendered >= workspaceMapMaxRenderedTiles) break; const wrappedX = ((tileX % maxTile) + maxTile) % maxTile; const tileKey = `${zoom}:${wrappedX}:${tileY}`; let image = existingTiles.get(tileKey); if (!image) { if (!workspaceMapTileUsageCanRequest(1)) { workspaceMapReportTileUsageLimit(workspaceMapTileUsageRead(), "hard"); continue; } image = document.createElement("img"); image.className = "workspace-map-tile"; image.dataset.tileKey = tileKey; image.alt = ""; image.decoding = "async"; image.loading = "lazy"; image.draggable = false; image.src = workspaceMapTileUrl(zoom, wrappedX, tileY); workspaceMapRecordTileRequest(1); if (workspaceMapLoadedTileKeys.has(tileKey)) image.classList.add("loaded"); image.addEventListener("load", () => { workspaceMapLoadedTileKeys.add(tileKey); image.classList.add("loaded"); }, { once: true }); } image.style.left = `${Math.round(tileX * workspaceMapTileSize - leftWorld)}px`; image.style.top = `${Math.round(tileY * workspaceMapTileSize - topWorld)}px`; fragment.append(image); rendered += 1; } } tiles.replaceChildren(fragment); } function workspaceMapScreenPoint(point, centerWorld, zoom, viewport) { const world = workspaceMapWorldPoint(point, zoom); return { x: world.x - centerWorld.x + viewport.clientWidth / 2, y: world.y - centerWorld.y + viewport.clientHeight / 2 }; } function workspaceMapRoadOverlayEnabled(model, options = {}) { const country = String(model?.country || "").trim().toLowerCase(); if (country !== "cameroon") return false; if (workspaceMapRoadSegmentRpcUnavailable) return false; if (!workspaceMapFlagEnabled(appConfig?.cameroonRoadOverlayEnabled, true)) return false; if (options.useMapboxTiles !== false && workspaceMapTileMapsAvailable()) return false; return Boolean(typeof supabaseRestRequest === "function" || (typeof supabaseClient !== "undefined" && supabaseClient?.rpc)); } function workspaceMapMetersPerPixel(latitude, zoom) { const lat = workspaceMapClampLatitude(latitude); return 156543.03392 * Math.cos((lat * Math.PI) / 180) / (2 ** zoom); } function workspaceMapRoadRadiusMeters(center, zoom, viewport) { const width = viewport?.clientWidth || 320; const height = viewport?.clientHeight || 180; const diagonalPixels = Math.sqrt((width * width) + (height * height)); const radius = diagonalPixels * workspaceMapMetersPerPixel(center?.latitude, zoom) * 0.72; return Math.round(Math.max(650, Math.min(8000, radius))); } function workspaceMapRoadKey(model, center, radiusMeters) { const city = String(model?.city || "").trim().toLowerCase(); const lat = Number(center?.latitude); const lng = Number(center?.longitude); if (!city || !Number.isFinite(lat) || !Number.isFinite(lng)) return ""; return [city, lat.toFixed(3), lng.toFixed(3), Math.round(radiusMeters / 500) * 500].join(":"); } function workspaceMapRoadPointFromCoordinate(coordinate) { if (!Array.isArray(coordinate) || coordinate.length < 2) return null; return workspaceMapPoint({ latitude: Number(coordinate[1]), longitude: Number(coordinate[0]) }, "", "road"); } function workspaceMapRoadSegmentsFromRows(rows) { return (Array.isArray(rows) ? rows : []) .map((row) => { const coordinates = Array.isArray(row?.coordinates) ? row.coordinates : []; const points = coordinates.map(workspaceMapRoadPointFromCoordinate).filter(Boolean); return { name: row?.road_name || "", roadClass: row?.road_class || "road", points }; }) .filter((segment) => segment.points.length >= 2); } function workspaceMapRoadClassName(value) { const normalized = String(value || "road").replace(/[^a-z0-9_-]/gi, "").toLowerCase() || "road"; if (["motorway", "trunk", "primary"].includes(normalized)) return "major"; if (["secondary", "tertiary"].includes(normalized)) return "medium"; return "minor"; } async function workspaceMapFetchRoadSegments(key, model, center, radiusMeters) { if (!key || workspaceMapRoadSegmentInFlight.has(key) || workspaceMapRoadSegmentRpcUnavailable) return; workspaceMapRoadSegmentInFlight.add(key); try { const body = { p_city: model?.city || null, p_center_lat: Number(center.latitude), p_center_lng: Number(center.longitude), p_radius_meters: Math.round(radiusMeters), p_limit: 90 }; let rows = []; if (typeof supabaseClient !== "undefined" && supabaseClient?.rpc) { const request = supabaseClient.rpc("cameroon_roads_near_view", body); const result = typeof withSupabaseTimeout === "function" ? await withSupabaseTimeout(request, "Loading Cameroon map roads", optionalSupabaseRequestTimeoutMs) : await request; if (result?.error) throw result.error; rows = result?.data || []; } else if (typeof supabaseRestRequest === "function") { rows = await supabaseRestRequest("/rest/v1/rpc/cameroon_roads_near_view", { method: "POST", body: JSON.stringify(body) }, "Loading Cameroon map roads", optionalSupabaseRequestTimeoutMs); } workspaceMapRoadSegmentCache.set(key, { rows: workspaceMapRoadSegmentsFromRows(rows), loadedAt: Date.now() }); if (typeof renderAll === "function") renderAll(); else if (typeof scheduleWorkspaceBackgroundMapRender === "function") scheduleWorkspaceBackgroundMapRender(); } catch (error) { if (/cameroon_roads_near_view|schema cache|function/i.test(String(error?.message || error))) { workspaceMapRoadSegmentRpcUnavailable = true; } if (typeof logClientWarning === "function") logClientWarning("Cameroon road overlay could not be loaded.", error); } finally { workspaceMapRoadSegmentInFlight.delete(key); } } function workspaceMapRoadSegmentsForView(model, center, zoom, viewport, options = {}) { if (!workspaceMapRoadOverlayEnabled(model, options)) return []; const radiusMeters = workspaceMapRoadRadiusMeters(center, zoom, viewport); const key = workspaceMapRoadKey(model, center, radiusMeters); if (!key) return []; const cached = workspaceMapRoadSegmentCache.get(key); if (cached) return cached.rows; void workspaceMapFetchRoadSegments(key, model, center, radiusMeters); return []; } function workspaceMapRenderRoadSegments(routeLayer, segments, centerWorld, zoom, viewport) { for (const segment of segments || []) { const points = segment.points .map((point) => workspaceMapScreenPoint(point, centerWorld, zoom, viewport)) .filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y)) .map((point) => point.x.toFixed(1) + "," + point.y.toFixed(1)) .join(" "); if (!points) continue; const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline"); polyline.setAttribute("class", "workspace-map-road-line " + workspaceMapRoadClassName(segment.roadClass)); polyline.setAttribute("points", points); routeLayer.append(polyline); } } function workspaceMapRenderOverlay(elements, model, centerWorld, zoom, options = {}) { const { viewport, routeLayer, markers } = elements; const width = viewport.clientWidth || window.innerWidth; const height = viewport.clientHeight || window.innerHeight; routeLayer.setAttribute("viewBox", `0 0 ${Math.max(1, width)} ${Math.max(1, height)}`); routeLayer.replaceChildren(); markers.replaceChildren(); const roadCenter = model.center || workspaceMapCenterForPoints(model.markers || []); const roadSegments = workspaceMapRoadSegmentsForView(model, roadCenter, zoom, viewport, options); workspaceMapRenderRoadSegments(routeLayer, roadSegments, centerWorld, zoom, viewport); const routeLinePoints = workspaceMapRouteDisplayPoints(model); if (routeLinePoints.length >= 2) { const points = routeLinePoints .map((point) => workspaceMapScreenPoint(point, centerWorld, zoom, viewport)) .map((point) => `${point.x.toFixed(1)},${point.y.toFixed(1)}`) .join(" "); const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline"); polyline.setAttribute("class", "workspace-map-route-line"); polyline.setAttribute("points", points); routeLayer.append(polyline); } for (const marker of model.markers || []) { const point = workspaceMapScreenPoint(marker, centerWorld, zoom, viewport); const node = document.createElement("div"); node.className = `workspace-map-marker ${marker.type || ""}`.trim(); node.style.left = `${point.x}px`; node.style.top = `${point.y}px`; node.textContent = marker.label || ""; markers.append(node); } } function workspaceMapRouteDisplayPoints(model) { return (model?.routePathPoints?.length ? model.routePathPoints : model?.routePoints || []).filter(Boolean); } function renderInlineWorkspaceMap(container, model, options = {}) { if (!container || !model) return; let tiles = container.querySelector(".workspace-map-tiles"); let routeLayer = container.querySelector(".workspace-map-route-layer"); let markers = container.querySelector(".workspace-map-markers"); if (!tiles) { tiles = document.createElement("div"); tiles.className = "workspace-map-tiles"; container.append(tiles); } if (!routeLayer) { routeLayer = document.createElementNS("http://www.w3.org/2000/svg", "svg"); routeLayer.setAttribute("class", "workspace-map-route-layer"); routeLayer.setAttribute("preserveAspectRatio", "none"); routeLayer.setAttribute("aria-hidden", "true"); container.append(routeLayer); } if (!markers) { markers = document.createElement("div"); markers.className = "workspace-map-markers"; container.append(markers); } const routeLinePoints = workspaceMapRouteDisplayPoints(model); const points = (routeLinePoints.length ? routeLinePoints : model.markers || []).filter(Boolean); const viewport = container; const zoom = options.zoom ?? workspaceMapZoomForPoints(points, viewport, model.zoom || workspaceMapExactZoom); const center = model.center || workspaceMapCenterForPoints(points); const centerWorld = workspaceMapWorldPoint(center, zoom); workspaceMapRenderTiles( { root: container, viewport, tiles, routeLayer, markers }, centerWorld, zoom, { useMapboxTiles: options.useMapboxTiles } ); workspaceMapRenderOverlay({ viewport, routeLayer, markers }, model, centerWorld, zoom, options); } function rideRequestRoutePreviewReadyForTiles(model) { const hasPickup = Boolean( String(els.pickupDescription?.value ?? "").trim() || els.pickupUseCurrentLocation?.checked || (typeof passengerPickupGpsForFormChoice === "function" && passengerPickupGpsForFormChoice()) ); const hasDestination = Boolean(String(els.destination?.value ?? "").trim()); return Boolean(hasPickup && hasDestination && model?.routePoints?.length >= 2); } function rideRequestRoutePreviewUseMapboxTiles(model) { return Boolean( workspaceMapFlagEnabled(appConfig?.passengerRequestTileMapEnabled, false) && rideRequestRoutePreviewReadyForTiles(model) && workspaceMapTileMapsAvailable() ); } function renderRideRequestRoutePreview() { const container = els.rideRequestRoutePreview; if (!container) return; container.hidden = false; const visible = Boolean( els.rideRequestForm && !els.rideRequestForm.hidden && typeof activeRole === "function" && activeRole() === "passenger" && typeof passengerWorkspacePage === "function" && passengerWorkspacePage() === "request" ); if (!visible) return; const model = rideRequestRoutePreviewModel(); window.requestAnimationFrame(() => renderInlineWorkspaceMap(container, model, { useMapboxTiles: rideRequestRoutePreviewUseMapboxTiles(model) })); } function renderWorkspaceBackgroundMap() { const elements = workspaceMapElements(); if (!elements.root || !elements.viewport || !elements.tiles || !elements.routeLayer || !elements.markers) return; initializeWorkspaceBackgroundMap(); const context = workspaceMapContext(); workspaceMapSyncDeviceGpsWatch(context); const model = workspaceMapModel(context); if (!context || !model) { elements.root.hidden = true; document.body.classList.remove("workspace-map-active"); workspaceMapState.contextKey = ""; workspaceMapState.userPanned = false; return; } elements.root.hidden = false; document.body.classList.add("workspace-map-active"); const contextChanged = model.contextKey !== workspaceMapState.contextKey; const modelChanged = model.modelKey !== workspaceMapState.modelKey; const viewport = elements.viewport; const zoom = workspaceMapZoomForPoints(workspaceMapRouteDisplayPoints(model), viewport, model.zoom || workspaceMapDefaultZoom); if (contextChanged || modelChanged || !workspaceMapState.center || !workspaceMapState.userPanned) { workspaceMapState.center = model.center; workspaceMapState.zoom = zoom; workspaceMapState.contextKey = model.contextKey; workspaceMapState.modelKey = model.modelKey; workspaceMapState.userPanned = false; } const centerWorld = workspaceMapWorldPoint(workspaceMapState.center, workspaceMapState.zoom); const useMapboxTiles = context.role === "passenger" && context.page === "request" ? rideRequestRoutePreviewUseMapboxTiles(model) : context.role !== "passenger"; workspaceMapRenderTiles(elements, centerWorld, workspaceMapState.zoom, { useMapboxTiles }); workspaceMapRenderOverlay(elements, model, centerWorld, workspaceMapState.zoom, { useMapboxTiles }); } function scheduleWorkspaceBackgroundMapRender() { if (workspaceMapAnimationFrame) return; workspaceMapAnimationFrame = window.requestAnimationFrame(() => { workspaceMapAnimationFrame = 0; renderWorkspaceBackgroundMap(); }); } function workspaceMapPointerDown(event) { const elements = workspaceMapElements(); if (!elements.viewport || !workspaceMapState.center) return; if (event.pointerType === "mouse" && event.button !== 0) return; event.preventDefault(); elements.viewport.classList.add("dragging"); elements.viewport.setPointerCapture?.(event.pointerId); workspaceMapDragState = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, startWorld: workspaceMapWorldPoint(workspaceMapState.center, workspaceMapState.zoom) }; } function workspaceMapPointerMove(event) { if (!workspaceMapDragState || workspaceMapDragState.pointerId !== event.pointerId) return; event.preventDefault(); const dx = event.clientX - workspaceMapDragState.startX; const dy = event.clientY - workspaceMapDragState.startY; workspaceMapState.center = workspaceMapLatLngFromWorld({ x: workspaceMapDragState.startWorld.x - dx, y: workspaceMapDragState.startWorld.y - dy }, workspaceMapState.zoom); workspaceMapState.userPanned = true; scheduleWorkspaceBackgroundMapRender(); } function workspaceMapPointerEnd(event) { if (!workspaceMapDragState || workspaceMapDragState.pointerId !== event.pointerId) return; const elements = workspaceMapElements(); elements.viewport?.classList.remove("dragging"); elements.viewport?.releasePointerCapture?.(event.pointerId); workspaceMapDragState = null; } function workspaceMapWheel(event) { const context = workspaceMapContext(); if (!context) return; event.preventDefault(); const delta = event.deltaY > 0 ? -1 : 1; workspaceMapState.zoom = Math.max(8, Math.min(16, (workspaceMapState.zoom || workspaceMapDefaultZoom) + delta)); workspaceMapState.userPanned = true; scheduleWorkspaceBackgroundMapRender(); } function initializeWorkspaceBackgroundMap() { if (workspaceMapInitialized) return; const elements = workspaceMapElements(); if (!elements.viewport) return; workspaceMapInitialized = true; elements.viewport.addEventListener("pointerdown", workspaceMapPointerDown); elements.viewport.addEventListener("pointermove", workspaceMapPointerMove); elements.viewport.addEventListener("pointerup", workspaceMapPointerEnd); elements.viewport.addEventListener("pointercancel", workspaceMapPointerEnd); elements.viewport.addEventListener("wheel", workspaceMapWheel, { passive: false }); const schedulePassengerMapRenders = () => { scheduleWorkspaceBackgroundMapRender(); if (typeof renderRideRequestRoutePreview === "function") renderRideRequestRoutePreview(); }; ["passengerCountry", "passengerCity", "pickupCity", "pickupArea", "destinationArea", "pickupDescription", "destination", "rideStops", "pickupUseCurrentLocation", "rideTiming", "vehiclePreference"].forEach((id) => { document.querySelector(`#${id}`)?.addEventListener("input", schedulePassengerMapRenders); document.querySelector(`#${id}`)?.addEventListener("change", () => { workspaceMapState.userPanned = false; schedulePassengerMapRenders(); }); }); window.addEventListener("resize", () => { window.clearTimeout(workspaceMapResizeTimer); workspaceMapResizeTimer = window.setTimeout(() => { workspaceMapState.userPanned = false; renderWorkspaceBackgroundMap(); }, 120); }); } // Payment preferences, subscriptions, business billing, settlements, tips, ratings, and tax access helpers. const subscriptionFee = riderMonthlySubscriptionFee; const trialDays = 30; const pendingPaymentSetupStorageKey = "waka-pending-payment-setup-v1"; const pendingPaymentSetupMaxAgeMs = 24 * 60 * 60 * 1000; let pendingPaymentSetupPollTimer = null; const riderSubscriptionPlans = { wallet_topup: { label: "Rider wallet top-up", amount: riderWalletTopupMinimum, days: 0, description: `Wallet bundles start at ${formatMoney(riderWalletTopupMinimum)}. After the ${trialDays}-day free period, the first ${riderDailyFreeRideAllowance} completed rides each day are free; ride ${riderDailyFreeRideAllowance + 1}+ deducts ${Math.round(riderWalletCommissionRate * 100)}% of the fare from the wallet. Low-balance notices start below ${formatMoney(riderWalletLowBalanceThreshold)}` }, monthly_access: { label: "Monthly Waka Rider Access", amount: riderMonthlySubscriptionFee, days: riderMonthlyAccessDays, description: `${formatMoney(riderMonthlySubscriptionFee)} monthly access for ${riderMonthlyAccessDays} days with no per-ride wallet deduction while active` } }; let subscriptionPaymentRpcUnavailable = { submit: false, verify: false, decline: false }; let lastSubscriptionPaymentSource = "not used"; let paymentAccountRpcUnavailable = false; let lastPaymentAccountSource = "not used"; let businessAccountRpcUnavailable = false; let lastBusinessAccountSource = "not used"; let riderDayRegionsRpcUnavailable = false; let lastRiderDayRegionsSource = "not used"; function agreedFareBaseForRequest(request) { return Number(request?.agreedFare ?? offersForRequest(request?.id).find((offer) => offer.id === request?.selectedOfferId)?.fare ?? request?.fareOffer ?? 0); } function acceptedRouteChangeFareForRequest(request) { return Math.max(0, Number(request?.acceptedRouteChangeFare ?? request?.accepted_route_change_fare ?? 0) || 0); } function agreedFareForRequest(request) { return agreedFareBaseForRequest(request) + acceptedRouteChangeFareForRequest(request); } function passengerCancellationFeeEstimate(request, atTime = Date.now()) { if (!request || !["matched", "arrived"].includes(request.status) || !selectedRiderIdForRequest(request)) { return { amount: 0, currency: moneyCurrencyForCountry(request?.country), elapsedMinutes: 0, status: "not_applicable" }; } const matchedAt = request.matchedAt ? new Date(request.matchedAt).getTime() : null; const elapsedMinutes = matchedAt && Number.isFinite(matchedAt) ? Math.max(0, Math.ceil((atTime - matchedAt) / 60000)) : 0; if (request.status === "matched" && elapsedMinutes < passengerCancellationFeeConfig.graceMinutes) { return { amount: 0, currency: moneyCurrencyForCountry(request.country), elapsedMinutes, status: "grace_period" }; } const fare = Math.max(0, agreedFareForRequest(request)); const base = request.status === "arrived" ? passengerCancellationFeeConfig.arrivedBaseUsd : passengerCancellationFeeConfig.matchedBaseUsd; const cap = Math.max(base, Math.ceil(fare * passengerCancellationFeeConfig.capFareRatio)); const amount = Math.min(cap, base + elapsedMinutes * passengerCancellationFeeConfig.perMinuteUsd); return { amount, currency: moneyCurrencyForCountry(request.country), elapsedMinutes, status: amount > 0 ? "pending_charge" : "not_applicable" }; } function inProgressCancellationCompensationEstimate(request, atTime = Date.now()) { if (!request || request.status !== "in_progress" || !selectedRiderIdForRequest(request)) { return { amount: 0, currency: moneyCurrencyForCountry(request?.country), elapsedMinutes: 0, status: "not_applicable", fareRatio: 0 }; } const startedAt = request.startedAt ? new Date(request.startedAt).getTime() : null; const elapsedMinutes = startedAt && Number.isFinite(startedAt) ? Math.max(1, Math.ceil((atTime - startedAt) / 60000)) : 1; const fare = Math.max(0, agreedFareForRequest(request)); if (fare <= 0) { return { amount: 0, currency: moneyCurrencyForCountry(request.country), elapsedMinutes, status: "not_applicable", fareRatio: 0 }; } const estimatedMinutes = Math.max( 1, Number(request.estimatedTravelMinutes || inProgressCancellationCompensationConfig.fallbackEstimatedMinutes) || inProgressCancellationCompensationConfig.fallbackEstimatedMinutes ); const elapsedRatio = elapsedMinutes / estimatedMinutes; const fareRatio = Math.min( inProgressCancellationCompensationConfig.maximumFareRatio, Math.max(inProgressCancellationCompensationConfig.minimumFareRatio, elapsedRatio) ); const minimumAmount = Math.max(minimumFareOffer(request.country), Math.ceil(fare * inProgressCancellationCompensationConfig.minimumFareRatio)); const maximumAmount = Math.max(minimumAmount, Math.ceil(fare * inProgressCancellationCompensationConfig.maximumFareRatio)); const amount = Math.min(maximumAmount, Math.max(minimumAmount, Math.ceil(fare * fareRatio))); return { amount, currency: moneyCurrencyForCountry(request.country), elapsedMinutes, status: amount > 0 ? "pending_charge" : "not_applicable", fareRatio }; } function riderCancellationNoPassengerChargeEstimate(request) { return { amount: 0, currency: moneyCurrencyForCountry(request?.country), elapsedMinutes: 0, status: "waived", fareRatio: 0 }; } function rideCancellationCompensationEstimate(request, atTime = Date.now(), actorRole = typeof activeRole === "function" ? activeRole() : "") { if (actorRole === "rider") return riderCancellationNoPassengerChargeEstimate(request); if (request?.status === "in_progress") return inProgressCancellationCompensationEstimate(request, atTime); return passengerCancellationFeeEstimate(request, atTime); } function cancellationFeeText(request) { const amount = Number(request?.cancellationFeeAmount ?? 0); if (amount > 0) { return `Passenger cancellation fee: ${formatMoney(amount, request.country)} (${request.cancellationFeeStatus ?? "pending"}).`; } if (typeof activeRole === "function" && activeRole() === "rider" && ["matched", "arrived", "in_progress"].includes(request?.status)) { return ""; } if (request?.status === "in_progress") { const estimate = inProgressCancellationCompensationEstimate(request); if (estimate.amount > 0) return `If this ride is cancelled now, Waka will charge a partial fare of ${formatMoney(estimate.amount, request.country)} for about ${estimate.elapsedMinutes} minute${estimate.elapsedMinutes === 1 ? "" : "s"} after pickup.`; } const estimate = passengerCancellationFeeEstimate(request); if (estimate.amount > 0) return `Passenger cancellation now may charge ${formatMoney(estimate.amount, request.country)} for rider time.`; if (estimate.status === "grace_period") return "Passenger cancellation is still inside the short no-fee grace window."; return ""; } function dollarsToCents(amount) { return Math.max(0, Math.round(Number(amount || 0) * 100)); } function centsToDollars(cents) { return Number(cents || 0) / 100; } function formatMoneyCents(cents, country = defaultLaunchCountry()) { if (moneyCurrencyForCountry(country) !== "USD") return formatMoney(Math.round(centsToDollars(cents)), country); return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(centsToDollars(cents)); } function stripeProcessingFeeCents(amountCents) { return Math.max(0, Math.ceil(Number(amountCents || 0) * stripeProcessingFeeRate + stripeProcessingFixedUsd * 100)); } function riderTrialHasEnded(rider) { if (!rider?.trialEndsAt) return true; return new Date(rider.trialEndsAt).getTime() < Date.now(); } function selectedRiderForRequest(request) { const riderId = selectedRiderIdForRequest(request); if (!riderId) return null; return state.riders.find((rider) => rider.id === riderId) ?? null; } function riderFacilitationFeeCents(fareCents, rider) { return riderTrialHasEnded(rider) ? Math.ceil(Number(fareCents || 0) * riderFacilitationFeeRate) : 0; } function businessRideServiceFeeCents(request, fareCents) { if (!request?.businessAccountId) return 0; const account = businessAccountForRequest(request); if (businessAccountWaivesRideServiceFee(account)) return 0; return Math.ceil(Number(fareCents || 0) * businessRideServiceFeeRate); } function rideFinancialBreakdown(request, tipAmount = 0) { const rider = selectedRiderForRequest(request); const fareCents = dollarsToCents(agreedFareForRequest(request)); const tipCents = dollarsToCents(tipAmount); const grossCents = fareCents + tipCents; const stripeFeeCents = directRidePaymentMode() ? 0 : stripeProcessingFeeCents(grossCents); const facilitationFeeCents = riderFacilitationFeeCents(fareCents, rider); const businessServiceFeeCents = businessRideServiceFeeCents(request, fareCents); const riderPayoutCents = Math.max(0, grossCents - stripeFeeCents - facilitationFeeCents); return { fareCents, tipCents, grossCents, stripeFeeCents, facilitationFeeCents, businessServiceFeeCents, riderPayoutCents, facilitationFeeWaived: facilitationFeeCents === 0, rider }; } function rideFinancialSummary(request) { if (!request || request.status !== "completed") return ""; const breakdown = rideFinancialBreakdown(request, totalTipAmountForRequest(request.id)); const businessFeeText = breakdown.businessServiceFeeCents ? ` Business account service fee charged separately to the business: ${formatMoneyCents(breakdown.businessServiceFeeCents, request.country)}.` : ""; if (directRidePaymentMode()) { return `Direct payment ride: passenger pays the rider by cash or mobile money. Waka commission is tracked through the rider wallet after any free period.${businessFeeText}`; } return `Rider payout estimate: ${formatMoneyCents(breakdown.riderPayoutCents, request.country)} after provider fee ${formatMoneyCents(breakdown.stripeFeeCents, request.country)}. Waka ride fee: none; rider keeps the rest of the fare.${businessFeeText}`; } function paymentFromDatabase(value) { return { cash: "cash", mtn_money: "mtn", orange_money: "orange", agree_before_ride: "decide", online_card: "online_card", online_wallet: "online_wallet" }[value] ?? "decide"; } function paymentToDatabase(value) { return { cash: "cash", mtn: "mtn_money", orange: "orange_money", decide: "agree_before_ride", online_card: "online_card", online_wallet: "online_wallet" }[value] ?? "agree_before_ride"; } function paymentLabel(value) { return { cash: "Cash", mtn: "MTN Mobile Money", orange: "Orange Money", decide: "Agree before ride", online_card: "Card or online payment", online_wallet: "Online wallet or bank transfer" }[value] ?? "Agree before ride"; } function requiresOnlineRidePayment(country) { return Boolean(country && !africanRidePaymentCountries.has(country)); } function ridePaymentOptionsForCountry(country) { if (requiresOnlineRidePayment(country)) { return [ { value: "online_card", label: "Card or online payment" }, { value: "online_wallet", label: "Online wallet or bank transfer" } ]; } return [ { value: "cash", label: "Cash in hand" }, { value: "mtn", label: "MTN Mobile Money" }, { value: "orange", label: "Orange Money" }, { value: "decide", label: "Agree with rider before ride" } ]; } function validPaymentPreferenceForCountry(value, country) { const options = ridePaymentOptionsForCountry(country); return options.some((option) => option.value === value) ? value : options[0]?.value ?? "decide"; } function enabledLaunchCountries() { const configured = Array.isArray(appConfig.enabledLaunchCountries) ? appConfig.enabledLaunchCountries : []; const validConfigured = configured .map((country) => String(country ?? "").trim()) .filter((country) => country && countryCities[country]); if (validConfigured.length) return [...new Set(validConfigured)]; const firstLaunchCountry = String(appConfig.firstLaunchCountry ?? "").trim(); if (firstLaunchCountry && countryCities[firstLaunchCountry]) return [firstLaunchCountry]; return Object.keys(countryCities); } function onlineRidePaymentMarketCount() { return enabledLaunchCountries().filter((country) => requiresOnlineRidePayment(country)).length; } function ridePaymentProviderSupportsOnline(provider = appConfig.paymentProvider) { return productionOnlineRidePaymentProviderPattern.test(String(provider ?? "")); } function directRidePaymentMode() { return !ridePaymentProviderSupportsOnline() && enabledLaunchCountries().every((country) => !requiresOnlineRidePayment(country)); } function currentAccountNotifications(type) { const account = type === "passenger" ? state.passenger : state.rider; if (!account?.id) return []; const accountIds = typeof accountIdentityIdsForNoticeRole === "function" ? accountIdentityIdsForNoticeRole(type) : [account.id].filter(Boolean).map((id) => String(id)); const adminNotices = state.notifications .filter((notification) => accountIds.includes(String(notification.recipientId)) && notification.recipientRole === type) .map((notification) => ({ ...notification, noticeKind: "admin_notice" })); const financeNotices = financeAdjustmentsForAccount(type, account.id).map((adjustment) => ({ id: `finance-${adjustment.id}`, title: financeAdjustmentUserTitle(adjustment), body: financeAdjustmentUserBody(adjustment), recipientId: account.id, recipientRole: type, deliveryChannels: ["in_app"], createdAt: adjustment.processedAt || adjustment.updatedAt || adjustment.createdAt, noticeKind: "finance_adjustment" })); return [...adminNotices, ...financeNotices] .sort((a, b) => new Date(b.createdAt ?? 0) - new Date(a.createdAt ?? 0)); } function financeAdjustmentRecords() { return state.financeAdjustments ?? []; } function financeAdjustmentsForAccount(role, accountId) { if (!accountId) return []; return financeAdjustmentRecords() .filter((adjustment) => { const roleMatches = adjustment.subjectRole === role || (role === "passenger" && adjustment.subjectRole === "business"); return adjustment.subjectId === accountId && roleMatches && adjustment.visibleToUser !== false; }) .sort((a, b) => new Date(b.processedAt ?? b.updatedAt ?? b.createdAt ?? 0) - new Date(a.processedAt ?? a.updatedAt ?? a.createdAt ?? 0)); } function financeAdjustmentUserTitle(adjustment) { const labels = { passenger_refund: "Ride refund", partial_passenger_refund: "Partial ride refund", business_service_fee_refund: "Business ride fee refund", rider_subscription_refund: "Rider access refund", business_subscription_refund: "Business subscription refund", passenger_credit: "Passenger credit", passenger_debit: "Passenger account adjustment", rider_bonus: "Rider bonus", rider_debit: "Rider account adjustment", manual_correction: "Account correction" }; return labels[adjustment.adjustmentType] || "Finance update"; } function financeAdjustmentUserBody(adjustment) { const amount = formatMoneyCents(adjustment.amountCents || 0); const status = String(adjustment.status || "recorded").replace(/_/g, " "); const reason = adjustment.reason ? ` Reason: ${adjustment.reason}` : ""; return `${amount} ${financeAdjustmentUserTitle(adjustment).toLowerCase()} is ${status}.${reason}`; } function riderPaymentRequests(riderId = state.rider?.id) { if (!riderId) return []; return state.paymentRequests.filter((request) => request.riderId === riderId).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); } function pendingPaymentRequestForRider(riderId = state.rider?.id) { return riderPaymentRequests(riderId).find((request) => request.status === "pending") ?? null; } function paymentAccountRecords() { return state.paymentAccounts; } function paymentAccountFor(role, userId) { if (!userId) return null; return paymentAccountRecords() .filter((account) => account.role === role && account.userId === userId) .sort((a, b) => new Date(b.updatedAt ?? b.createdAt ?? 0) - new Date(a.updatedAt ?? a.createdAt ?? 0))[0] ?? null; } async function refreshPaymentAccountsFromSupabase(role) { if (!hasSupabaseRuntime()) return false; const roles = role ? [role] : ["passenger", "rider"]; let refreshed = false; for (const accountRole of roles) { const account = accountRole === "passenger" ? state.passenger : currentRiderRecord(); const userId = account?.id; if (!userId || !hasSignedIn(accountRole)) continue; const rows = supabaseClient ? await withSupabaseTimeout( supabaseClient .from("payment_accounts") .select("*") .eq("user_id", userId) .eq("role", accountRole) .order("updated_at", { ascending: false }), `Loading ${accountRole} payment account`, optionalSupabaseRequestTimeoutMs ).then(({ data, error }) => { if (error) throw error; return data ?? []; }) : await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/payment_accounts?select=*&user_id=eq.${encodeURIComponent(userId)}&role=eq.${encodeURIComponent(accountRole)}&order=updated_at.desc`, { accessToken: supabaseRestSession?.access_token }), `Loading ${accountRole} payment account`, optionalSupabaseRequestTimeoutMs ); rows.map((row) => mapPaymentAccountFromDatabase(row, new Map([[userId, { full_name: account?.name, email: account?.email }]]))) .forEach((paymentAccount) => { state.paymentAccounts = upsertById( state.paymentAccounts.filter((item) => !(item.role === paymentAccount.role && item.userId === paymentAccount.userId)), paymentAccount ); refreshed = true; }); } if (refreshed) saveState(); return refreshed; } function paymentAccountReady(role, account) { const userId = account?.id; if (!userId) return false; if (directRidePaymentMode()) return true; if (paymentSetupRelaxedForTesting()) return true; return Boolean(paymentAccountFor(role, userId)?.status === "linked"); } function stagingPaymentAccountForTesting(role, account) { const existing = paymentAccountFor(role, account?.id); const isRider = role === "rider"; const now = new Date().toISOString(); return { id: existing?.id ?? makeId("payacct"), userId: account.id, userName: account.name, role, provider: isRider ? "stripe-connect-test" : "stripe-test", accountType: isRider ? "test_payout_account" : "test_card", accountHolder: account.name || (isRider ? "Waka rider" : "Waka passenger"), accountLast4: isRider ? "0000" : "4242", institutionName: isRider ? "Stripe Connect staging payout" : "Stripe test mode", reference: `staging-${isRider ? "rider-payout" : "test"}-${account.id}`, status: "linked", createdAt: existing?.createdAt ?? now, updatedAt: now }; } async function ensureStagingPaymentAccountForTesting(role, account, { localFallback = true } = {}) { if (!paymentSetupRelaxedForTesting() || !account?.id || !hasSignedIn(role)) return paymentAccountFor(role, account?.id); const existing = paymentAccountFor(role, account.id); if (existing?.status === "linked") return existing; const stagingAccount = stagingPaymentAccountForTesting(role, account); let savedAccount = stagingAccount; try { savedAccount = await savePaymentAccountToSupabase(stagingAccount); } catch (error) { if (!localFallback) throw error; logClientWarning(`Staging ${role} payment account could not be saved to Supabase; keeping it local for pilot testing.`, error); } state.paymentAccounts = upsertById( state.paymentAccounts.filter((item) => !(item.role === role && item.userId === account.id)), savedAccount ); saveState(); return savedAccount; } function paymentAccountSummary(role, account) { const paymentAccount = paymentAccountFor(role, account?.id); if (!account) return "Sign in before payment setup."; if (directRidePaymentMode()) { return role === "rider" ? "Cameroon direct-payment mode is active. Riders receive fares directly from passengers; Waka wallet top-ups and optional monthly access are paid separately through MTN/Orange." : "Cameroon direct-payment mode is active. Passenger pays the matched rider by cash, MTN Mobile Money, or Orange Money."; } if (paymentSetupRelaxedForTesting()) { return role === "rider" ? "Staging payout setup is relaxed for testing. Stripe Connect is still required before production." : "Staging payment setup is relaxed for testing. A real payment method is still required before production."; } if (role === "rider" && !paymentAccount) return "Stripe payout account is not connected yet."; if (!paymentAccount) return "Stripe payment setup required. Waka does not collect card or bank credentials."; const ending = paymentAccount.accountLast4 && paymentAccount.accountLast4 !== "0000" ? ` ending ${paymentAccount.accountLast4}` : ""; if (role === "rider") { return paymentAccount.status === "linked" ? `Stripe payout account${ending} is linked.` : "Stripe payout setup is not complete yet. Finish Stripe Connect onboarding before receiving ride requests."; } return `${paymentAccount.provider} ${paymentAccount.accountType}${ending} is ${paymentAccount.status}.`; } function paymentSetupRelaxedForTesting() { return configFlagEnabled(appConfig.relaxPaymentSetupForTesting); } function paymentSetupConfirmFunctionName() { return String(appConfig.paymentSetupConfirmFunctionName || "payment-method-setup-confirm").trim() || "payment-method-setup-confirm"; } function paymentSetupReturnParams() { const params = new URLSearchParams(window.location.search); const payment = String(params.get("payment") || "").toLowerCase(); if (!payment) return null; const path = window.location.pathname.toLowerCase(); const role = path.includes("rider") ? "rider" : "passenger"; return { payment, sessionId: String(params.get("session_id") || "").trim(), role }; } function normalizePaymentSetupRole(role) { return role === "rider" ? "rider" : "passenger"; } function readPendingPaymentSetup() { try { const pending = JSON.parse(localStorage.getItem(pendingPaymentSetupStorageKey)); if (!pending || typeof pending !== "object") return null; const sessionId = String(pending.sessionId || "").trim(); const role = normalizePaymentSetupRole(pending.role); const createdAtMs = new Date(pending.createdAt || 0).getTime(); if (!sessionId || !Number.isFinite(createdAtMs) || Date.now() - createdAtMs > pendingPaymentSetupMaxAgeMs) { localStorage.removeItem(pendingPaymentSetupStorageKey); return null; } return { payment: "success", sessionId, role, pending: true }; } catch { try { localStorage.removeItem(pendingPaymentSetupStorageKey); } catch { // Storage can be unavailable; the URL return can still finish setup. } return null; } } function rememberPendingPaymentSetup(role, sessionId) { const normalizedSessionId = String(sessionId || "").trim(); if (!normalizedSessionId) return; try { localStorage.setItem(pendingPaymentSetupStorageKey, JSON.stringify({ role: normalizePaymentSetupRole(role), sessionId: normalizedSessionId, createdAt: new Date().toISOString() })); } catch { // If storage is blocked, Waka can still confirm while the return URL is present. } } function clearPendingPaymentSetup(sessionId = "") { try { if (!sessionId) { localStorage.removeItem(pendingPaymentSetupStorageKey); stopPendingPaymentSetupPolling(); return; } const pending = JSON.parse(localStorage.getItem(pendingPaymentSetupStorageKey)); if (!pending || String(pending.sessionId || "") === sessionId) { localStorage.removeItem(pendingPaymentSetupStorageKey); stopPendingPaymentSetupPolling(); } } catch { // Nothing else to clear. } } function stopPendingPaymentSetupPolling() { if (!pendingPaymentSetupPollTimer) return; window.clearInterval(pendingPaymentSetupPollTimer); pendingPaymentSetupPollTimer = null; } function paymentSetupStillInProgressError(error) { return /\bnot complete yet\b|\bdid not save a payment method\b|\bsetup intent\b/i.test(String(error?.message || error || "")); } function schedulePendingPaymentSetupConfirmation({ immediate = false } = {}) { if (!readPendingPaymentSetup()) { stopPendingPaymentSetupPolling(); return; } if (immediate) { window.setTimeout(() => { void handlePaymentSetupReturnFromLocation({ fromPendingCheck: true }); }, 0); } if (pendingPaymentSetupPollTimer) return; let attempts = 0; pendingPaymentSetupPollTimer = window.setInterval(() => { attempts += 1; if (!readPendingPaymentSetup() || attempts > 60) { stopPendingPaymentSetupPolling(); return; } void handlePaymentSetupReturnFromLocation({ fromPendingCheck: true }); }, 5000); } function paymentStatusElement(role) { return role === "rider" ? els.riderPaymentStatus : els.passengerPaymentStatus; } function clearPaymentSetupReturnParams() { const params = new URLSearchParams(window.location.search); params.delete("payment"); params.delete("session_id"); const nextQuery = params.toString(); const nextUrl = `${window.location.pathname}${nextQuery ? `?${nextQuery}` : ""}${window.location.hash || ""}`; window.history.replaceState({}, "", nextUrl); } function subscriptionCheckoutReturnParams() { const params = new URLSearchParams(window.location.search); const subscription = String(params.get("subscription") || "").trim().toLowerCase(); if (!subscription) return null; if (!["success", "cancelled", "business_success", "business_cancelled"].includes(subscription)) return null; return { kind: subscription.startsWith("business_") ? "business" : "rider", status: subscription.includes("cancelled") ? "cancelled" : "success" }; } function clearSubscriptionCheckoutReturnParams() { const params = new URLSearchParams(window.location.search); params.delete("subscription"); const nextQuery = params.toString(); const nextUrl = `${window.location.pathname}${nextQuery ? `?${nextQuery}` : ""}${window.location.hash || ""}`; window.history.replaceState({}, "", nextUrl); } function subscriptionCheckoutReturnMessage({ kind, status }) { if (kind === "business") { return status === "success" ? "Business Partner checkout completed. Waka is refreshing the monthly billing state; billing starts after any active free business month." : "Business Partner checkout was cancelled. Verified businesses can still use the free month and Starter can continue with the 10% completed-ride fee model after that."; } return status === "success" ? "Rider payment checkout completed. Waka is refreshing provider confirmation; wallet or monthly access will update as soon as the webhook confirms payment." : "Rider payment checkout was cancelled. Rider marketplace access stays unchanged; retry wallet top-up or monthly access when ready."; } function applySubscriptionCheckoutReturnRoute({ kind }) { if (kind === "business") { state.activeTab = "passenger"; state.passengerPage = "business"; if (!hasSignedIn("passenger")) state.accountMode.passenger = "signin"; if (typeof updatePassengerWorkspaceRoute === "function") { updatePassengerWorkspaceRoute("business", { replace: true }); } return; } state.activeTab = "rider"; state.riderPage = "checks"; if (!hasSignedIn("rider")) state.accountMode.rider = "signin"; if (typeof updateRiderWorkspaceRoute === "function") { updateRiderWorkspaceRoute("checks", { replace: true }); } } async function handleSubscriptionCheckoutReturnFromLocation() { const checkoutReturn = subscriptionCheckoutReturnParams(); if (!checkoutReturn) return false; applySubscriptionCheckoutReturnRoute(checkoutReturn); clearSubscriptionCheckoutReturnParams(); saveState(); await loadMarketplaceFromSupabase({ includeAccountData: true }).catch((error) => { logClientWarning("Marketplace refresh after subscription checkout return was skipped.", error); }); renderAll(); const statusElement = checkoutReturn.kind === "business" ? els.businessAccountStatus : els.subscriptionPaymentStatus; if (statusElement) statusElement.textContent = subscriptionCheckoutReturnMessage(checkoutReturn); return true; } async function ensureSignedInForPaymentReturn(role) { if (hasSignedIn(role)) return true; const user = await getSupabaseUser().catch(() => null); if (!user) return false; const profile = supabaseClient ? await supabaseClient .from("profiles") .select("*") .eq("id", user.id) .maybeSingle() .then(({ data, error }) => { if (error) throw error; return data; }) : await selectProfileRest(user.id, "*", supabaseRestSession?.access_token); if (!profile || profile.role !== role) return false; applySignedInProfile(role, profile, user); state.activeTab = role; if (role === "passenger") state.passengerPage = "payment"; saveState(); return true; } async function confirmPaymentMethodSetup(sessionId, role) { const token = await currentSupabaseAccessToken(); if (!token) throw new Error("Sign in to finish linking the Stripe payment method."); const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${paymentSetupConfirmFunctionName()}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ sessionId, role }) }), "Confirming Stripe payment setup", supabaseProfileSaveTimeoutMs ); const payload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(payload?.error || "Stripe payment setup could not be confirmed."); return payload.paymentAccount; } async function handlePaymentSetupReturnFromLocation({ fromPendingCheck = false } = {}) { const locationReturn = paymentSetupReturnParams(); if (locationReturn?.payment === "success" && locationReturn.sessionId) { rememberPendingPaymentSetup(locationReturn.role, locationReturn.sessionId); } const paymentReturn = locationReturn ?? readPendingPaymentSetup(); if (!paymentReturn) return false; const { payment, sessionId, role } = paymentReturn; const status = paymentStatusElement(role); const signedInAccount = role === "passenger" ? state.passenger : currentRiderRecord(); if (!locationReturn && payment === "success" && paymentAccountReady(role, signedInAccount)) { clearPendingPaymentSetup(sessionId); if (status) status.textContent = paymentAccountSummary(role, signedInAccount); if (role === "passenger") state.passengerPage = "request"; saveState(); renderAll(); return true; } state.activeTab = role; if (role === "passenger") state.passengerPage = "payment"; if (payment === "cancelled") { if (status) status.textContent = "Stripe card setup was cancelled. No passenger payment method was linked."; clearPendingPaymentSetup(sessionId); clearPaymentSetupReturnParams(); saveState(); return true; } if (payment !== "success") return false; if (!await ensureSignedInForPaymentReturn(role)) { state.accountMode[role] = "signin"; if (status) status.textContent = "Sign in to finish linking the Stripe payment method."; if (locationReturn) clearPaymentSetupReturnParams(); saveState(); if (!fromPendingCheck) renderAll(); return true; } if (!sessionId) { if (status) status.textContent = "Stripe returned without a setup session id. Open Stripe setup again so Waka can link the saved card immediately."; saveState(); return true; } try { if (status) status.textContent = "Confirming saved Stripe payment method..."; const row = await confirmPaymentMethodSetup(sessionId, role); const account = role === "passenger" ? state.passenger : currentRiderRecord(); const savedAccount = mapPaymentAccountFromDatabase(row, new Map([[account?.id, { full_name: account?.name }]])); state.paymentAccounts = upsertById( state.paymentAccounts.filter((item) => !(item.role === role && item.userId === account?.id)), savedAccount ); await refreshPaymentAccountsFromSupabase(role); await loadMarketplaceFromSupabase({ includeAccountData: true }).catch((marketplaceError) => { logClientWarning("Marketplace refresh after Stripe setup was skipped.", marketplaceError); }); clearPendingPaymentSetup(sessionId); clearPaymentSetupReturnParams(); if (role === "passenger") state.passengerPage = "request"; saveState(); renderAll(); const refreshedStatus = paymentStatusElement(role); if (refreshedStatus) refreshedStatus.textContent = paymentAccountSummary(role, account); if (role === "passenger" && els.passengerRideGate) { els.passengerRideGate.textContent = "Passenger payment method is ready. You can request rides."; } return true; } catch (error) { await refreshPaymentAccountsFromSupabase(role).catch(() => false); const account = role === "passenger" ? state.passenger : currentRiderRecord(); if (paymentAccountReady(role, account)) { clearPendingPaymentSetup(sessionId); if (role === "passenger") state.passengerPage = "request"; saveState(); renderAll(); const refreshedStatus = paymentStatusElement(role); if (refreshedStatus) refreshedStatus.textContent = paymentAccountSummary(role, account); return true; } if (paymentSetupStillInProgressError(error)) { if (status) status.textContent = "Finish Stripe card setup in the secure tab, then return here. Waka will link the saved card automatically."; schedulePendingPaymentSetupConfirmation(); saveState(); return true; } if (status) status.textContent = `Stripe payment setup could not be linked yet: ${error.message}`; saveState(); return true; } } function selectedSubscriptionPlanKey() { const value = els.subscriptionPlan?.value || "wallet_topup"; return riderSubscriptionPlans[value] ? value : "wallet_topup"; } function selectedSubscriptionRenewalMode() { return els.subscriptionRenewalMode?.value === "automatic" ? "automatic" : "manual"; } function selectedSubscriptionPaymentProvider() { const value = els.subscriptionPaymentProvider?.value || "mtn_momo"; return ["mtn_momo", "orange_money"].includes(value) ? value : "mtn_momo"; } function selectedSubscriptionPayerPhone() { return String(els.subscriptionPayerPhone?.value || "").trim(); } function selectedSubscriptionTopupAmount() { const planKey = selectedSubscriptionPlanKey(); if (planKey !== "wallet_topup") return riderSubscriptionPlans.monthly_access.amount; const amount = Math.round(Number(els.subscriptionTopupAmount?.value || 0)); return Number.isFinite(amount) ? amount : 0; } function subscriptionProviderLabel(value) { if (value === "orange_money") return "Orange Money"; return "MTN Mobile Money"; } function selectedSubscriptionPlan() { return riderSubscriptionPlans[selectedSubscriptionPlanKey()] ?? riderSubscriptionPlans.wallet_topup; } function riderPlanSummary() { if (directRidePaymentMode()) { return `After the ${trialDays}-day free period that starts at admin approval, the first ${riderDailyFreeRideAllowance} completed rides each day remain free. Ride ${riderDailyFreeRideAllowance + 1}+ deducts ${Math.round(riderWalletCommissionRate * 100)}% of the fare from the rider wallet. Wallet bundles start at ${formatMoney(riderWalletTopupMinimum)}, low-balance notices start below ${formatMoney(riderWalletLowBalanceThreshold)}, and optional ${formatMoney(riderMonthlySubscriptionFee)} monthly access gives uninterrupted use with no per-ride wallet deduction while active.`; } const plans = Object.values(riderSubscriptionPlans) .map((plan) => plan.description) .join("; "); return `After the ${trialDays}-day free trial: ${plans}. Riders can renew manually before expiry or choose automatic provider renewal. Waka takes no money from each ride fare; rider payout is fare minus provider processing only.`; } const businessPlanLabels = { [businessStarterPlanCode]: `Starter - ${businessFreeTrialDays}-day free month, then ${Math.round(businessRideServiceFeeRate * 100)}% per completed ride`, [businessPartnerPlanCode]: `Partner - ${businessFreeTrialDays}-day free month, then ${formatMoney(businessPartnerMonthlySubscriptionFee)}/month with no ${Math.round(businessRideServiceFeeRate * 100)}% ride fee` }; function normalizeBusinessPlanCode(value) { return value === businessPartnerPlanCode ? businessPartnerPlanCode : businessStarterPlanCode; } function businessPlanLabel(value) { return businessPlanLabels[normalizeBusinessPlanCode(value)]; } function normalizeBusinessAccountStatus(value) { const status = String(value || "pending_review").toLowerCase(); if (status === "pending") return "pending_review"; return ["pending_review", "active", "past_due", "suspended", "cancelled", "rejected"].includes(status) ? status : "pending_review"; } function businessVerificationLabel(status) { return { pending_review: "Pending Waka verification", verified: "Verified by Waka", rejected: "Rejected", suspended: "Suspended" }[status] ?? status; } function businessAccountRecords() { return state.businessAccounts; } function businessSubscriptionRecords() { return state.businessSubscriptions; } function passengerBusinessAccounts(passenger = state.passenger) { if (!passenger?.id) return []; return businessAccountRecords() .filter((account) => account.ownerId === passenger.id) .sort((a, b) => new Date(b.createdAt ?? 0) - new Date(a.createdAt ?? 0)); } function businessSubscriptionFor(accountId) { if (!accountId) return null; return businessSubscriptionRecords() .filter((subscription) => subscription.businessAccountId === accountId) .sort((a, b) => new Date(b.updatedAt ?? b.createdAt ?? 0) - new Date(a.updatedAt ?? a.createdAt ?? 0))[0] ?? null; } function businessSubscriptionIsActive(subscription, now = new Date()) { if (!subscription || subscription.status !== "active") return false; const paidUntil = new Date(subscription.paidUntil || ""); return Number.isFinite(paidUntil.getTime()) && paidUntil.getTime() > now.getTime(); } function businessFreeTrialEndsAt(account) { const endsAt = new Date(account?.freeTrialEndsAt || ""); return Number.isFinite(endsAt.getTime()) ? endsAt : null; } function businessFreeTrialIsActive(account, now = new Date()) { const endsAt = businessFreeTrialEndsAt(account); return Boolean(endsAt && endsAt.getTime() > now.getTime()); } function businessFreeTrialText(account) { const endsAt = businessFreeTrialEndsAt(account); if (!endsAt) return `${businessFreeTrialDays}-day free month starts after Waka verification.`; const daysLeft = Math.max(0, Math.ceil((endsAt.getTime() - Date.now()) / (24 * 60 * 60 * 1000))); return daysLeft > 0 ? `Free business month active until ${formatDate(endsAt)} (${pluralDays(daysLeft)} left).` : `Free business month ended on ${formatDate(endsAt)}.`; } function businessAccountForRequest(request) { if (!request?.businessAccountId) return null; return businessAccountRecords().find((account) => account.id === request.businessAccountId) ?? null; } function businessAccountWaivesRideServiceFee(account) { if (businessFreeTrialIsActive(account)) return true; return normalizeBusinessPlanCode(account?.planCode) === businessPartnerPlanCode; } function businessAccountCanRequest(account) { if (!account) return false; if (normalizeBusinessAccountStatus(account.status) !== "active") return false; if ((account.verificationStatus || "pending_review") !== "verified") return false; if (businessFreeTrialIsActive(account)) return true; if (normalizeBusinessPlanCode(account.planCode) === businessPartnerPlanCode) { return businessSubscriptionIsActive(businessSubscriptionFor(account.id)); } return true; } function businessAccountSummary(account) { const serviceFee = `${Math.round(businessRideServiceFeeRate * 100)}% business ride service fee`; if (!account) return `Business accounts require Waka verification before ride billing. Waka gives verified businesses one free month; after that Starter adds a ${serviceFee}, or Partner is ${formatMoney(businessPartnerMonthlySubscriptionFee)}/month with no per-ride Waka business fee.`; const status = normalizeBusinessAccountStatus(account.status); const verification = account.verificationStatus || "pending_review"; const planCode = normalizeBusinessPlanCode(account.planCode); const subscription = businessSubscriptionFor(account.id); if (status === "rejected" || verification === "rejected") return `${account.businessName} was not approved for business ride billing.`; if (status === "suspended") return `${account.businessName} is suspended and cannot publish business rides.`; if (status !== "active" || verification !== "verified") return `${account.businessName} is waiting for Waka verification before the ${businessFreeTrialDays}-day free business month starts.`; const trialText = businessFreeTrialText(account); if (planCode === businessPartnerPlanCode) { if (businessFreeTrialIsActive(account)) return `${account.businessName} is verified on Business Partner. ${trialText} The monthly Partner charge starts after the free month and waives the ${Math.round(businessRideServiceFeeRate * 100)}% Waka ride fee.`; if (businessSubscriptionIsActive(subscription)) return `${account.businessName} is verified on Business Partner. The ${formatMoney(businessPartnerMonthlySubscriptionFee)}/month plan waives the ${Math.round(businessRideServiceFeeRate * 100)}% Waka ride fee.`; return `${account.businessName} is verified for Business Partner. ${trialText} Start the ${formatMoney(businessPartnerMonthlySubscriptionFee)}/month checkout to keep business rides active after the free month.`; } return `${account.businessName} is verified on Business Starter. ${trialText} After the free month, completed business rides add a ${serviceFee} paid to Waka, separate from the rider fare.`; } function localDateKey(date = new Date()) { const local = new Date(date.getTime() - date.getTimezoneOffset() * 60000); return local.toISOString().slice(0, 10); } function riderDayPreferenceRecords() { return state.riderDayPreferences; } function riderDayPreferenceFor(rider = currentRiderRecord(), dateKey = localDateKey()) { if (!rider?.id) return null; const localPreferenceDate = rider.dailyRegions?.serviceDate ?? rider.dailyRegions?.date; const localPreference = localPreferenceDate === dateKey ? rider.dailyRegions : null; return riderDayPreferenceRecords() .find((item) => item.riderId === rider.id && item.serviceDate === dateKey) ?? localPreference; } function riderDailyDestinationRegions(rider = currentRiderRecord()) { const preference = riderDayPreferenceFor(rider); return Array.isArray(preference?.regions) ? preference.regions.filter(Boolean) : []; } function riderDailyRegionsReady(rider = currentRiderRecord()) { return riderDailyDestinationRegions(rider).length > 0; } function riderShowsAllNearbyPickups(rider = currentRiderRecord()) { if (rider) return true; const preference = riderDayPreferenceFor(rider); return preference?.showAllNearbyPickups === true || state.riderDestinationScope === "all"; } function riderDestinationScopeLabel() { return "all nearby pickups"; } function riderDailyRegionUpdatesUsed(rider = currentRiderRecord()) { return Number(riderDayPreferenceFor(rider)?.updatesUsed ?? 0); } function riderDailyRegionUpdatesRemaining(rider = currentRiderRecord()) { return Math.max(0, 2 - riderDailyRegionUpdatesUsed(rider)); } function taxDocumentRecords() { return state.taxDocuments; } function taxIdentityReferenceRecords() { return state.taxIdentityReferences; } function taxIdentityForRider(riderId = state.rider?.id) { if (!riderId) return null; return taxIdentityReferenceRecords() .filter((record) => record.riderId === riderId) .sort((a, b) => new Date(b.updatedAt ?? b.createdAt ?? 0) - new Date(a.updatedAt ?? a.createdAt ?? 0))[0] ?? null; } function taxIdentityStatusText(reference) { if (!reference) return "Not started"; const status = String(reference.status || "pending").replace(/_/g, " "); const last4 = reference.tinLast4 ? ` Last four: ${reference.tinLast4}.` : " Full tax identifier is not stored in Waka."; return `${reference.provider || appConfig.taxOnboardingProvider || "Provider"}: ${status}.${last4}`; } function taxDocumentsForRider(riderId = state.rider?.id) { if (!riderId) return []; return taxDocumentRecords() .filter((document) => document.riderId === riderId) .sort((a, b) => { const yearDelta = Number(b.taxYear) - Number(a.taxYear); if (yearDelta) return yearDelta; return new Date(b.availableAt ?? b.issuedAt ?? b.updatedAt ?? b.createdAt ?? 0) - new Date(a.availableAt ?? a.issuedAt ?? a.updatedAt ?? a.createdAt ?? 0); }); } function rideRatingRecords() { return state.rideRatings; } const riderRatingCategoryDefinitions = [ { key: "overall", label: "Overall", field: "score", percentField: "overallPercent" }, { key: "safety", label: "Safety", field: "safetyScore", percentField: "safetyPercent" }, { key: "punctuality", label: "Pickup timing", field: "punctualityScore", percentField: "punctualityPercent" }, { key: "communication", label: "Communication", field: "communicationScore", percentField: "communicationPercent" }, { key: "vehicle", label: "Vehicle", field: "vehicleScore", percentField: "vehiclePercent" } ]; function rideSettlementRecords() { return state.rideSettlements; } function rideTipRecords() { return state.rideTips; } function totalTipAmountForRequest(requestId) { return rideTipRecords() .filter((tip) => tip.requestId === requestId && !["failed", "refunded"].includes(tip.status)) .reduce((total, tip) => total + Number(tip.amount || 0), 0); } function passengerTipForRequest(requestId, passengerId = state.passenger?.id) { if (!requestId || !passengerId) return null; return rideTipRecords().find((tip) => tip.requestId === requestId && tip.passengerId === passengerId) ?? null; } function ratingsForRider(riderId) { if (!riderId) return []; return rideRatingRecords().filter((rating) => rating.ratedUserId === riderId); } function riderRatingAggregateForRider(riderId) { if (!riderId || state.adminSession?.source === "supabase") return null; const summary = state.riderRatingSummary; if (!summary || summary.riderId !== riderId || !Number(summary.ratingCount)) return null; return summary; } function ratingPercentFromAverage(average) { return Number.isFinite(average) ? Math.round((average / 5) * 100) : null; } function averageScoreForRatings(ratings, field) { const values = ratings .map((rating) => Number(rating[field] ?? rating.score)) .filter((value) => Number.isFinite(value) && value >= 1 && value <= 5); if (!values.length) return null; return values.reduce((total, value) => total + value, 0) / values.length; } function riderRatingCategorySummaries(riderId) { const aggregate = riderRatingAggregateForRider(riderId); if (aggregate) { return riderRatingCategoryDefinitions.map((definition) => ({ key: definition.key, label: definition.label, percent: aggregate[definition.percentField], count: aggregate.ratingCount })); } const ratings = ratingsForRider(riderId); return riderRatingCategoryDefinitions.map((definition) => ({ key: definition.key, label: definition.label, percent: ratingPercentFromAverage(averageScoreForRatings(ratings, definition.field)), count: ratings.length })); } function averageRatingForRider(riderId) { const aggregate = riderRatingAggregateForRider(riderId); if (aggregate) { return { average: Number(aggregate.overallPercent) / 20, count: aggregate.ratingCount, percent: aggregate.overallPercent }; } const ratings = ratingsForRider(riderId); if (!ratings.length) return null; const average = ratings.reduce((total, rating) => total + Number(rating.score || 0), 0) / ratings.length; return { average, count: ratings.length, percent: ratingPercentFromAverage(average) }; } function ratingSummaryForRider(riderId) { const rating = averageRatingForRider(riderId); return rating ? `${Math.round(rating.percent ?? rating.average * 20)}% from ${rating.count} rating${rating.count === 1 ? "" : "s"}` : "new"; } function requestDestinationText(request) { const area = request?.destinationArea; const detail = request?.destinationFormattedAddress || request?.destination; const destinationText = detail || area || "Destination"; const stops = normalizeRideStops(request?.rideStops); return stops.length ? `${stops.join(" -> ")} -> ${destinationText}` : destinationText; } function estimatedTravelMinutesForRequest(request) { const stored = Number(request?.estimatedTravelMinutes); if (Number.isFinite(stored) && stored > 0) return stored; const guidance = fareGuidanceForRide( request?.country, request?.city, request?.pickupArea, request?.destinationArea, requestPickupGps(request), request?.rideStops ); return guidance?.minutes ?? 30; } function destinationUpdateWindowMinutes(request) { return Math.max(5, Math.ceil(estimatedTravelMinutesForRequest(request) * destinationUpdateTravelFraction)); } function canUpdateRideDestination(request) { if (!request || activeRole() !== "passenger" || !requestBelongsToPassenger(request)) return false; return ["open", "matched", "arrived", "in_progress"].includes(request.status); } function requestDestinationMatchesDailyRegions(request, rider = currentRiderRecord()) { if (!request || !rider) return false; if (requestHasRiderMatch(request)) return true; if (riderShowsAllNearbyPickups(rider)) return true; const regions = riderDailyDestinationRegions(rider).map((region) => region.toLowerCase()); if (!regions.length) return false; const destinationArea = String(request.destinationArea ?? "").toLowerCase(); const destinationText = String(request.destination ?? "").toLowerCase(); return regions.some((region) => destinationArea === region || destinationText.includes(region)); } function isSubscriptionActive(rider) { if (!rider || rider.status !== "approved") return false; if (directRidePaymentMode()) return true; const now = Date.now(); const trialActive = rider.trialEndsAt && new Date(rider.trialEndsAt).getTime() >= now; const paidActive = rider.subscriptionPaidUntil && new Date(rider.subscriptionPaidUntil).getTime() >= now; return Boolean(trialActive || paidActive); } function daysUntil(value) { if (!value) return 0; return Math.max(0, Math.ceil((new Date(value).getTime() - Date.now()) / 86400000)); } function riderAccessEnd(rider) { if (!rider) return null; const trialTime = rider.trialEndsAt ? new Date(rider.trialEndsAt).getTime() : 0; const paidTime = rider.subscriptionPaidUntil ? new Date(rider.subscriptionPaidUntil).getTime() : 0; if (directRidePaymentMode() && trialTime < Date.now() && paidTime < Date.now()) return null; if (paidTime >= trialTime && rider.subscriptionPaidUntil) return rider.subscriptionPaidUntil; return rider.trialEndsAt ?? rider.subscriptionPaidUntil ?? null; } function riderAccessLabel(rider) { const now = Date.now(); if (rider?.subscriptionPaidUntil && new Date(rider.subscriptionPaidUntil).getTime() >= now) return directRidePaymentMode() ? "monthly access" : "paid access"; if (rider?.trialEndsAt && new Date(rider.trialEndsAt).getTime() >= now) return "free trial"; if (directRidePaymentMode() && rider?.status === "approved") return "wallet/free rides"; return "rider access"; } function pluralDays(days) { return `${days} day${days === 1 ? "" : "s"}`; } function mapPaymentRequestFromDatabase(request, riderMap = new Map()) { const rider = riderMap.get(request.rider_id); return { id: request.id, riderId: request.rider_id, riderName: rider?.name ?? rider?.full_name ?? "Rider", planType: request.plan_type ?? "monthly", amount: request.amount_xaf, provider: request.provider, paymentPhone: request.payment_phone, reference: request.provider_reference, status: request.status, reviewNote: request.review_note ?? "", reviewedBy: request.reviewed_by, reviewedAt: request.reviewed_at, createdAt: request.created_at }; } function mapPaymentAccountFromDatabase(account, profileMap = new Map()) { const profile = profileMap.get(account.user_id); return { id: account.id, userId: account.user_id, userName: profile?.full_name ?? profile?.email ?? "Account holder", role: account.role, provider: account.provider, accountType: account.account_type, accountHolder: account.account_holder, accountLast4: account.account_last4, institutionName: account.institution_name, reference: account.provider_reference, providerCustomerReference: account.provider_customer_reference ?? "", status: account.status, createdAt: account.created_at, updatedAt: account.updated_at }; } function mapBusinessAccountFromDatabase(row, profileMap = new Map()) { const owner = profileMap.get(row.owner_id); return { id: row.id, ownerId: row.owner_id, ownerName: owner?.full_name ?? owner?.email ?? "Business owner", businessName: row.business_name, billingEmail: row.billing_email, businessCategory: row.business_category ?? "other", businessAddress: row.business_address ?? "", contactName: row.contact_name ?? "", contactPhone: row.contact_phone ?? "", planCode: normalizeBusinessPlanCode(row.plan_code), verificationStatus: row.verification_status ?? "pending_review", reviewedBy: row.reviewed_by ?? null, reviewedAt: row.reviewed_at ?? null, freeTrialStartedAt: row.free_trial_started_at ?? null, freeTrialEndsAt: row.free_trial_ends_at ?? null, reviewNote: row.review_note ?? "", status: normalizeBusinessAccountStatus(row.status), createdAt: row.created_at, updatedAt: row.updated_at }; } function mapBusinessSubscriptionFromDatabase(row) { return { id: row.id, businessAccountId: row.business_account_id, planCode: normalizeBusinessPlanCode(row.plan_code), amount: centsToDollars(row.amount_cents), provider: row.provider, reference: row.provider_reference ?? "", paidUntil: row.paid_until ?? null, status: row.status, refundedAmount: centsToDollars(row.refunded_amount_cents), refundStatus: row.refund_status ?? "not_refunded", refundReference: row.refund_reference ?? "", refundReason: row.refund_reason ?? "", refundedAt: row.refunded_at ?? null, createdAt: row.created_at, updatedAt: row.updated_at }; } function mapRideSettlementFromDatabase(row, profileMap = new Map()) { const passenger = profileMap.get(row.passenger_id); const rider = profileMap.get(row.rider_id); return { id: row.id, requestId: row.ride_request_id, passengerId: row.passenger_id, passengerName: passenger?.full_name ?? "Passenger", riderId: row.rider_id, riderName: rider?.full_name ?? "Rider", fareAmount: centsToDollars(row.fare_amount_cents), stripeFeeAmount: centsToDollars(row.stripe_fee_cents), facilitationFeeAmount: centsToDollars(row.facilitation_fee_cents), businessServiceFeeAmount: centsToDollars(row.business_service_fee_cents), riderPayoutAmount: centsToDollars(row.rider_payout_cents), status: row.status, providerReference: row.provider_reference ?? "", providerChargeReference: row.provider_charge_reference ?? "", providerTransferReference: row.provider_transfer_reference ?? "", providerFeeAmount: centsToDollars(row.provider_fee_cents), businessServiceFeeProviderReference: row.business_service_fee_provider_reference ?? "", businessServiceFeeStatus: row.business_service_fee_status ?? (row.business_service_fee_cents ? "pending_charge" : "not_applicable"), businessServiceFeeFailureReason: row.business_service_fee_failure_reason ?? "", passengerRefundedAmount: centsToDollars(row.passenger_refunded_cents), businessServiceFeeRefundedAmount: centsToDollars(row.business_service_fee_refunded_cents), lastRefundReference: row.last_refund_reference ?? "", refundReason: row.refund_reason ?? "", processedAt: row.processed_at, failureReason: row.failure_reason ?? "", createdAt: row.created_at, updatedAt: row.updated_at }; } function mapRideTipFromDatabase(row, profileMap = new Map()) { const passenger = profileMap.get(row.passenger_id); const rider = profileMap.get(row.rider_id); return { id: row.id, requestId: row.ride_request_id, passengerId: row.passenger_id, passengerName: passenger?.full_name ?? "Passenger", riderId: row.rider_id, riderName: rider?.full_name ?? "Rider", amount: centsToDollars(row.amount_cents), stripeFeeAmount: centsToDollars(row.stripe_fee_cents), riderPayoutAmount: centsToDollars(row.rider_payout_cents), status: row.status, providerReference: row.provider_reference ?? "", createdAt: row.created_at, updatedAt: row.updated_at }; } function mapFinanceAdjustmentFromDatabase(row, profileMap = new Map()) { const subject = profileMap.get(row.subject_id); return { id: row.id, subjectRole: row.subject_role, subjectId: row.subject_id, subjectName: subject?.full_name ?? subject?.email ?? "Account", rideRequestId: row.ride_request_id ?? "", settlementId: row.settlement_id ?? "", subscriptionPaymentId: row.subscription_payment_id ?? "", businessSubscriptionId: row.business_subscription_id ?? "", adjustmentType: row.adjustment_type, amountCents: row.amount_cents, amount: centsToDollars(row.amount_cents), currency: row.currency ?? "USD", reason: row.reason ?? "", status: row.status, provider: row.provider ?? "", providerReference: row.provider_reference ?? "", visibleToUser: row.visible_to_user !== false, adminId: row.admin_id ?? "", metadata: row.metadata ?? {}, processedAt: row.processed_at ?? null, createdAt: row.created_at, updatedAt: row.updated_at }; } function mapRiderDayPreferenceFromDatabase(preference, riderMap = new Map()) { const rider = riderMap.get(preference.rider_id); return { id: preference.id, riderId: preference.rider_id, riderName: rider?.name ?? rider?.full_name ?? "Rider", serviceDate: preference.service_date, country: preference.country, city: preference.city, originArea: preference.origin_area, regions: Array.isArray(preference.destination_regions) ? preference.destination_regions : [], showAllNearbyPickups: preference.show_all_nearby_pickups === true, updatesUsed: preference.updates_used, createdAt: preference.created_at, updatedAt: preference.updated_at }; } function subscriptionPaymentRpcBody(paymentRequest) { return { p_plan_type: "monthly_access", p_provider: paymentRequest.provider, p_payment_phone: paymentRequest.paymentPhone, p_provider_reference: paymentRequest.reference, p_amount_xaf: paymentRequest.amount }; } async function savePaymentRequestToSupabase(paymentRequest) { if (!hasSupabaseRuntime()) return paymentRequest; const payload = { rider_id: paymentRequest.riderId, plan_type: "monthly_access", amount_xaf: paymentRequest.amount, provider: paymentRequest.provider, payment_phone: paymentRequest.paymentPhone, provider_reference: paymentRequest.reference, status: "pending" }; const riderMap = new Map([[paymentRequest.riderId, { name: paymentRequest.riderName }]]); if (!subscriptionPaymentRpcUnavailable.submit) { try { const body = subscriptionPaymentRpcBody(paymentRequest); const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("rider_submit_subscription_payment_request", body), "Submitting the subscription payment reference", supabaseProfileSaveTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/rider_submit_subscription_payment_request", { method: "POST", body }), "Submitting the subscription payment reference", supabaseProfileSaveTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastSubscriptionPaymentSource = "subscription payment RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) return mapPaymentRequestFromDatabase(row, riderMap); } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; subscriptionPaymentRpcUnavailable.submit = true; logClientWarning("Subscription payment submit RPC is not installed yet. Falling back to direct table insert.", error); } } assertClientFallbackAllowed("Subscription payment reference submission", "supabase-subscription-payment-requests.sql"); lastSubscriptionPaymentSource = "direct payment request insert fallback"; if (!supabaseClient) { const data = await withSupabaseTimeout( supabaseRestRequest("/rest/v1/subscription_payment_requests", { method: "POST", body: payload, headers: { Prefer: "return=representation" } }), "Submitting the subscription payment reference", supabaseProfileSaveTimeoutMs ); return mapPaymentRequestFromDatabase(Array.isArray(data) ? data[0] : data, riderMap); } const { data, error } = await withSupabaseTimeout( supabaseClient .from("subscription_payment_requests") .insert(payload) .select("*") .single(), "Submitting the subscription payment reference", supabaseProfileSaveTimeoutMs ); if (error) throw error; return mapPaymentRequestFromDatabase(data, riderMap); } async function savePaymentAccountToSupabase(account) { if (!hasSupabaseRuntime()) return account; const body = { p_role: account.role, p_provider: account.provider, p_account_type: account.accountType, p_account_holder: account.accountHolder, p_account_last4: account.accountLast4, p_institution_name: account.institutionName, p_provider_reference: account.reference }; if (!paymentAccountRpcUnavailable) { try { const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("save_payment_account_setup", body), "Saving the payment account", optionalSupabaseRequestTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/save_payment_account_setup", { method: "POST", body }), "Saving the payment account", optionalSupabaseRequestTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastPaymentAccountSource = "payment account RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; return row?.id ? mapPaymentAccountFromDatabase(row, new Map([[account.userId, { full_name: account.userName }]])) : account; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; paymentAccountRpcUnavailable = true; logClientWarning("Payment account RPC is not installed yet. Falling back to direct payment account upsert.", error); } } assertClientFallbackAllowed("Payment account setup", "supabase-payment-accounts.sql"); lastPaymentAccountSource = "direct payment account upsert fallback"; const payload = { user_id: account.userId, role: account.role, provider: account.provider, account_type: account.accountType, account_holder: account.accountHolder, account_last4: account.accountLast4, institution_name: account.institutionName, provider_reference: account.reference, status: "linked", updated_at: new Date().toISOString() }; if (supabaseClient) { const { data, error } = await withSupabaseTimeout( supabaseClient .from("payment_accounts") .upsert(payload, { onConflict: "user_id,role" }) .select("*") .single(), "Saving the payment account", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return mapPaymentAccountFromDatabase(data, new Map([[account.userId, { full_name: account.userName }]])); } const data = await withSupabaseTimeout( supabaseRestRequest("/rest/v1/payment_accounts?on_conflict=user_id,role", { method: "POST", body: payload, headers: { Prefer: "resolution=merge-duplicates,return=representation" } }), "Saving the payment account", optionalSupabaseRequestTimeoutMs ); return mapPaymentAccountFromDatabase(Array.isArray(data) ? data[0] : data, new Map([[account.userId, { full_name: account.userName }]])); } async function saveBusinessAccountToSupabase(account) { if (!hasSupabaseRuntime()) return account; const body = { p_business_name: account.businessName, p_billing_email: account.billingEmail, p_business_category: account.businessCategory, p_business_address: account.businessAddress, p_contact_name: account.contactName, p_contact_phone: account.contactPhone, p_plan_code: normalizeBusinessPlanCode(account.planCode), p_referral_code: account.referralCode ?? "" }; if (businessAccountRpcUnavailable) { throw new Error("Business verification RPC is not installed yet. Run supabase-business-starter-service-fee.sql before creating production business accounts."); } if (!businessAccountRpcUnavailable) { try { const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("create_business_account", body), "Creating the business account", supabaseProfileSaveTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/create_business_account", { method: "POST", body, headers: { Prefer: "return=representation" } }), "Creating the business account", supabaseProfileSaveTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastBusinessAccountSource = "business account RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; return row?.id ? mapBusinessAccountFromDatabase(row, new Map([[account.ownerId, { full_name: account.ownerName }]])) : account; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; businessAccountRpcUnavailable = true; logClientWarning("Business account verification RPC is not installed yet.", error); throw new Error("Business verification RPC is not installed yet. Run supabase-business-starter-service-fee.sql before creating production business accounts."); } } throw new Error("Business verification RPC is not installed yet. Run supabase-business-starter-service-fee.sql before creating production business accounts."); } async function saveRiderDayPreferenceToSupabase(preference) { if (!hasSupabaseRuntime()) return preference; const body = { p_country: preference.country, p_city: preference.city, p_origin_area: preference.originArea, p_destination_regions: preference.regions }; if (!riderDayRegionsRpcUnavailable) { try { const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("rider_save_day_regions", body), "Saving today's rider regions", optionalSupabaseRequestTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/rider_save_day_regions", { method: "POST", body }), "Saving today's rider regions", optionalSupabaseRequestTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastRiderDayRegionsSource = "rider day regions RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; return row?.id ? mapRiderDayPreferenceFromDatabase(row, new Map([[preference.riderId, { name: preference.riderName }]])) : preference; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; riderDayRegionsRpcUnavailable = true; logClientWarning("Rider day regions RPC is not installed yet. Falling back to direct day-region upsert.", error); } } assertClientFallbackAllowed("Rider day-region setup", "supabase-rider-day-regions.sql"); lastRiderDayRegionsSource = "direct rider day-region upsert fallback"; const payload = { rider_id: preference.riderId, service_date: preference.serviceDate, country: preference.country, city: preference.city, origin_area: preference.originArea, destination_regions: preference.regions, show_all_nearby_pickups: preference.showAllNearbyPickups === true, updates_used: preference.updatesUsed, updated_at: new Date().toISOString() }; if (supabaseClient) { const { data, error } = await withSupabaseTimeout( supabaseClient .from("rider_day_preferences") .upsert(payload, { onConflict: "rider_id,service_date" }) .select("*") .single(), "Saving today's rider regions", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return mapRiderDayPreferenceFromDatabase(data, new Map([[preference.riderId, { name: preference.riderName }]])); } const data = await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rider_day_preferences?on_conflict=rider_id,service_date", { method: "POST", body: payload, headers: { Prefer: "resolution=merge-duplicates,return=representation" } }), "Saving today's rider regions", optionalSupabaseRequestTimeoutMs ); return mapRiderDayPreferenceFromDatabase(Array.isArray(data) ? data[0] : data, new Map([[preference.riderId, { name: preference.riderName }]])); } function paymentFormValues(type) { const prefix = type === "passenger" ? "passenger" : "rider"; const account = type === "passenger" ? state.passenger : currentRiderRecord(); const existingAccount = paymentAccountFor(type, account?.id); const provider = els[`${prefix}PaymentProvider`]?.value || (type === "rider" ? "stripe_connect" : "stripe"); const reference = els[`${prefix}PaymentReference`]?.value.trim() || existingAccount?.reference || `${type}-stripe-${account?.id ?? makeId("account")}`; return { id: existingAccount?.id ?? makeId("payacct"), userId: account?.id, userName: account?.name, role: type, provider, accountType: "bank_account", accountHolder: els[`${prefix}AccountHolder`].value.trim(), accountLast4: els[`${prefix}AccountLast4`].value.trim(), institutionName: els[`${prefix}BankName`].value.trim(), reference, status: "linked", createdAt: existingAccount?.createdAt ?? new Date().toISOString(), updatedAt: new Date().toISOString() }; } async function savePaymentSetup(type, event) { event.preventDefault(); const account = type === "passenger" ? state.passenger : currentRiderRecord(); const status = type === "passenger" ? els.passengerPaymentStatus : els.riderPaymentStatus; if (!account || !hasSignedIn(type)) { status.textContent = "Sign in before saving a payment account."; return; } const paymentAccount = paymentFormValues(type); if (!paymentAccount.institutionName || !paymentAccount.accountHolder || !/^\d{4}$/.test(paymentAccount.accountLast4) || paymentAccount.reference.length < 4) { status.textContent = type === "rider" ? "Enter account holder, bank or payout account name, and last 4 digits." : "Enter account holder, bank or processor, last 4 digits, and reference."; return; } try { status.textContent = "Saving payment account..."; const savedAccount = await savePaymentAccountToSupabase(paymentAccount); state.paymentAccounts = upsertById( state.paymentAccounts.filter((item) => !(item.role === type && item.userId === account.id)), savedAccount ); saveState(); renderAll(); status.textContent = paymentAccountSummary(type, account); } catch (error) { status.textContent = `Payment account was not saved: ${error.message}`; } } async function startPaymentMethodSetup(type) { if (!hasSupabaseRuntime()) { throw new Error("Stripe payment setup requires the Supabase staging or production runtime."); } const body = { role: type }; const token = await currentSupabaseAccessToken(); if (!token) throw new Error("Sign in before opening Stripe setup."); const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/payment-method-setup-start`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify(body) }), "Starting Stripe payment setup", supabaseProfileSaveTimeoutMs ); const responsePayload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(responsePayload?.error || "Stripe payment setup Edge Function failed."); if (!responsePayload?.url) throw new Error("Stripe did not return a hosted setup URL."); return responsePayload; } async function startPassengerPaymentSetup(event) { event.preventDefault(); if (!state.passenger || !hasSignedIn("passenger")) { els.passengerPaymentStatus.textContent = "Sign in as a passenger before opening payment setup."; return; } if (directRidePaymentMode()) { els.passengerPaymentStatus.textContent = paymentAccountSummary("passenger", state.passenger); if (els.passengerRideGate) { els.passengerRideGate.textContent = "Ready to publish with cash, MTN Mobile Money, or Orange Money."; } return; } let setupWindow = null; try { setupWindow = window.open("", "wakaStripeCardSetup"); if (setupWindow) { setupWindow.document.title = "Opening Stripe"; setupWindow.document.body.innerHTML = '

Opening secure Stripe setup

Keep Waka open in the original tab. This window will continue to Stripe.

'; } } catch { setupWindow = null; } try { setButtonBusy(els.startPassengerPaymentSetup, true); els.passengerPaymentStatus.textContent = "Opening secure Stripe card setup..."; const checkout = await startPaymentMethodSetup("passenger"); rememberPendingPaymentSetup("passenger", checkout.providerReference); schedulePendingPaymentSetupConfirmation(); if (setupWindow && !setupWindow.closed) { els.passengerPaymentStatus.textContent = "Stripe card setup opened in a separate tab. Finish there, then return here; Waka will link the saved card automatically."; setupWindow.location.href = checkout.url; setupWindow.focus(); } else { els.passengerPaymentStatus.textContent = "Popup was blocked. Redirecting this tab to secure Stripe card setup..."; window.location.assign(checkout.url); } } catch (error) { if (setupWindow && !setupWindow.closed) setupWindow.close(); if (paymentSetupRelaxedForTesting()) { try { els.passengerPaymentStatus.textContent = `Stripe setup could not open: ${error.message}. Staging is linking a test payment method...`; const account = state.passenger; const stagingAccount = { id: paymentAccountFor("passenger", account.id)?.id ?? makeId("payacct"), userId: account.id, userName: account.name, role: "passenger", provider: "stripe-test", accountType: "test_card", accountHolder: account.name, accountLast4: "4242", institutionName: "Stripe test mode", reference: `staging-test-${account.id}`, status: "linked", createdAt: paymentAccountFor("passenger", account.id)?.createdAt ?? new Date().toISOString(), updatedAt: new Date().toISOString() }; let savedAccount = stagingAccount; let localOnly = false; try { savedAccount = await savePaymentAccountToSupabase(stagingAccount); } catch (saveError) { localOnly = true; logClientWarning("Staging test payment method could not be saved to Supabase; keeping it local for pilot testing.", saveError); } state.paymentAccounts = upsertById( state.paymentAccounts.filter((item) => !(item.role === "passenger" && item.userId === account.id)), savedAccount ); state.passengerPage = "payment"; saveState(); renderAll(); if (els.passengerPaymentStatus) { els.passengerPaymentStatus.textContent = localOnly ? `Staging test payment method is linked on this device because Stripe setup returned: ${error.message}` : `Staging test payment method is linked because Stripe setup returned: ${error.message}`; } if (els.passengerRideGate) { els.passengerRideGate.textContent = "Passenger payment method is ready for staging."; } return; } catch (fallbackError) { els.passengerPaymentStatus.textContent = `Stripe setup failed, and the staging test setup could not be linked: ${fallbackError.message}`; return; } } els.passengerPaymentStatus.textContent = `Could not open Stripe setup: ${error.message}`; } finally { setButtonBusy(els.startPassengerPaymentSetup, false); } } async function startBusinessSubscriptionCheckout(accountId) { const account = passengerBusinessAccounts().find((item) => item.id === accountId); if (!account) { els.businessAccountStatus.textContent = "Business account was not found."; return; } try { els.businessAccountStatus.textContent = `Opening Business Partner checkout for ${account.businessName}...`; const checkout = await startSubscriptionCheckout("business_subscription", account.id); els.businessAccountStatus.textContent = "Business Partner checkout opened. Stripe will honor any active free business month before the monthly Partner charge starts; Partner waives the 10% business ride fee."; window.location.assign(checkout.url); } catch (error) { els.businessAccountStatus.textContent = `Could not open business upgrade checkout: ${error.message}`; } } async function startSubscriptionCheckout(kind, entityId) { if (!hasSupabaseRuntime()) { throw new Error("Provider-hosted checkout requires the Supabase production runtime."); } const planKey = kind === "rider_subscription" ? selectedSubscriptionPlanKey() : undefined; const renewalMode = kind === "rider_subscription" ? selectedSubscriptionRenewalMode() : undefined; const paymentProvider = kind === "rider_subscription" && directRidePaymentMode() ? selectedSubscriptionPaymentProvider() : undefined; const payerPhone = kind === "rider_subscription" && directRidePaymentMode() ? selectedSubscriptionPayerPhone() : undefined; const amountXaf = kind === "rider_subscription" && directRidePaymentMode() ? selectedSubscriptionTopupAmount() : undefined; const body = kind === "business_subscription" ? { kind, businessAccountId: entityId } : { kind, riderId: entityId, plan: planKey, renewalMode, provider: paymentProvider, payerPhone, amountXaf }; let responsePayload = null; if (supabaseClient?.functions?.invoke) { const { data, error } = await withSupabaseTimeout( supabaseClient.functions.invoke("subscription-checkout-start", { body }), "Starting provider-hosted checkout", supabaseProfileSaveTimeoutMs ); if (error) throw error; responsePayload = data; } else { const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/subscription-checkout-start`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${supabaseRestSession?.access_token || appConfig.supabaseAnonKey}`, "content-type": "application/json" }, body: JSON.stringify(body) }), "Starting provider-hosted checkout", supabaseProfileSaveTimeoutMs ); responsePayload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(responsePayload?.error || "Subscription checkout Edge Function failed."); } if (!responsePayload?.url && !responsePayload?.mobileMoney) throw new Error("The payment provider did not return a subscription checkout URL."); lastSubscriptionPaymentSource = "subscription checkout Edge Function"; return responsePayload; } async function paySubscription() { const rider = currentRiderRecord(); if (!rider || rider.status !== "approved") return; try { setTranslatedStatus(els.subscriptionPaymentStatus, "submittingPaymentSupabase"); const plan = selectedSubscriptionPlan(); const renewal = directRidePaymentMode() ? "manual" : selectedSubscriptionRenewalMode(); const provider = selectedSubscriptionPaymentProvider(); const payerPhone = selectedSubscriptionPayerPhone(); if (directRidePaymentMode() && !payerPhone) { throw new Error(`Enter the phone number that will approve the ${subscriptionProviderLabel(provider)} rider payment.`); } if (directRidePaymentMode() && selectedSubscriptionPlanKey() === "wallet_topup" && selectedSubscriptionTopupAmount() < riderWalletTopupMinimum) { throw new Error(`Wallet top-up must be at least ${formatMoney(riderWalletTopupMinimum)}.`); } const checkout = await startSubscriptionCheckout("rider_subscription", rider.id); saveState(); if (els.subscriptionPaymentStatus) { if (checkout.mobileMoney) { const amount = checkout.amountXaf ? `${Number(checkout.amountXaf).toLocaleString("en-US")} FCFA` : formatMoney(plan.amount); const instructions = Array.isArray(checkout.paymentInstructions) ? checkout.paymentInstructions.join(" ") : ""; const label = checkout.paymentKind === "rider_wallet_topup" ? "wallet top-up" : "monthly access"; els.subscriptionPaymentStatus.textContent = `${subscriptionProviderLabel(checkout.provider || provider)} rider ${label} payment started for ${amount}. ${checkout.message || ""} ${instructions}`.replace(/\s+/g, " ").trim(); } else { els.subscriptionPaymentStatus.textContent = `${plan.label} checkout opened for ${renewal === "automatic" ? "automatic renewal" : "manual upfront renewal"}.`; } } else { setTranslatedStatus(els.subscriptionPaymentStatus, "paymentReferenceSubmitted"); } if (checkout.url) window.location.assign(checkout.url); } catch (error) { setTranslatedStatus(els.subscriptionPaymentStatus, "paymentReferenceFailed", { message: error.message }); } } // Ride request, offer negotiation, lifecycle, chat, safety report, rating, and tip flows. let rideLifecycleRpcUnavailable = false; let lastRideLifecycleSource = "not used"; const rideLifecycleActionInFlight = new Set(); let marketplaceActionRpcUnavailable = { fare: false, offer: false, rejection: false, selection: false, chat: false }; const contactProfilePhotoUrlCache = new Map(); const lifecycleGpsMaxAccuracyMeters = 150; const pickupArrivalMaxDistanceMeters = 200; const stopArrivalMaxDistanceMeters = 300; const destinationCompletionMaxDistanceMeters = 300; function rideFlowText(key, fallback = "", values = {}) { const translated = typeof translatedMessage === "function" ? translatedMessage(key, values) : typeof translatedValue === "function" ? translatedValue(key) : ""; return translated || fallback; } function lifecycleGpsAccuracyLimitMeters() { return lifecycleGpsMaxAccuracyMeters; } function lifecycleDistanceLimitMeters(actionName) { if (actionName === "arrive") return pickupArrivalMaxDistanceMeters; if (actionName === "stop") return stopArrivalMaxDistanceMeters; return destinationCompletionMaxDistanceMeters; } const rideLifecycleSupabaseTimeoutMs = Math.max(optionalSupabaseRequestTimeoutMs, supabaseProfileSaveTimeoutMs); let lastMarketplaceActionSource = "not used"; let safetyReportRpcUnavailable = { submit: false, review: false }; let supportTicketRpcUnavailable = { submit: false, review: false }; let lastSafetyReportSource = "not used"; let lastSupportTicketSource = "not used"; function requestPayloadForSupabase(request) { const pickupLocation = gpsPointToDatabase(requestPickupGps(request)); const pickupGps = requestPickupGps(request); const payload = { passenger_id: request.passengerId, business_account_id: request.businessAccountId || null, country: request.country, city: request.city, pickup_area: request.pickupArea, pickup_description: request.pickupDescription, destination_area: request.destinationArea, destination: request.destination, destination_place_id: request.destinationPlaceId ?? null, destination_formatted_address: request.destinationFormattedAddress ?? null, destination_lat: request.destinationLatitude ?? null, destination_lng: request.destinationLongitude ?? null, vehicle_preference: normalizeRideVehicle(request.vehicle), car_type_preference: normalizeRideVehicle(request.vehicle) === "bike" ? "bike" : normalizeCarTypePreference(request.carTypePreference), ride_stops: normalizeRideStops(request.rideStops), ride_stop_points: rideStopPointsForRoute(request.rideStops, request.rideStopPoints).filter(Boolean), passenger_count: normalizeRidePassengerCount(request.passengerCount), luggage_count: normalizeRideLuggageCount(request.luggageCount), luggage_note: normalizeRideLuggageNote(request.luggageNote), estimated_distance_miles: request.estimatedDistanceMiles, estimated_travel_minutes: request.estimatedTravelMinutes, route_estimate_source: normalizedRouteEstimateSourceForDatabase(request.routeEstimateSource), route_estimate_provider: normalizedRouteEstimateProviderForDatabase(request.routeEstimateSource, request.routeEstimateProvider), route_estimate_cached: Boolean(request.routeEstimateCached), route_estimate_key: request.routeEstimateKey ?? null, route_estimate_destination_fingerprint: request.routeEstimateDestinationFingerprint ?? null, route_estimate_polyline: request.routeEstimatePolyline ?? null, route_estimate_created_at: request.routeEstimateCreatedAt ?? null, fare_offer_xaf: request.fareOffer, fare_mode: normalizePassengerFareMode(request.fareMode), payment_preference: paymentToDatabase(request.paymentPreference), scheduled_at: request.scheduledAt, rider_confirmation_status: request.riderConfirmationStatus, rider_confirmation_requested_at: request.riderConfirmationRequestedAt, rider_confirmed_at: request.riderConfirmedAt, released_at: request.releasedAt, status: request.status }; if (pickupLocation) payload.pickup_location = pickupLocation; if (pickupLocation && pickupGps?.accuracyMeters != null) payload.pickup_gps_accuracy_meters = pickupGps.accuracyMeters; if (pickupLocation && pickupGps?.capturedAt) payload.pickup_gps_captured_at = pickupGps.capturedAt; return payload; } function rideRequestRpcBody(request) { const pickupGps = requestPickupGps(request); return { p_country: request.country, p_city: request.city, p_business_account_id: request.businessAccountId || null, p_pickup_area: request.pickupArea, p_pickup_description: request.pickupDescription, p_destination_area: request.destinationArea, p_destination: request.destination, p_destination_place_id: request.destinationPlaceId ?? null, p_destination_formatted_address: request.destinationFormattedAddress ?? null, p_destination_lat: request.destinationLatitude ?? null, p_destination_lng: request.destinationLongitude ?? null, p_vehicle_preference: normalizeRideVehicle(request.vehicle), p_car_type_preference: normalizeRideVehicle(request.vehicle) === "bike" ? "bike" : normalizeCarTypePreference(request.carTypePreference), p_ride_stops: normalizeRideStops(request.rideStops), p_ride_stop_points: rideStopPointsForRoute(request.rideStops, request.rideStopPoints).filter(Boolean), p_passenger_count: normalizeRidePassengerCount(request.passengerCount), p_luggage_count: normalizeRideLuggageCount(request.luggageCount), p_luggage_note: normalizeRideLuggageNote(request.luggageNote), p_estimated_distance_miles: request.estimatedDistanceMiles, p_estimated_travel_minutes: request.estimatedTravelMinutes, p_route_estimate_source: normalizedRouteEstimateSourceForDatabase(request.routeEstimateSource), p_route_estimate_provider: normalizedRouteEstimateProviderForDatabase(request.routeEstimateSource, request.routeEstimateProvider), p_route_estimate_cached: Boolean(request.routeEstimateCached), p_route_estimate_key: request.routeEstimateKey ?? null, p_route_estimate_destination_fingerprint: request.routeEstimateDestinationFingerprint ?? null, p_route_estimate_polyline: request.routeEstimatePolyline ?? null, p_route_estimate_created_at: request.routeEstimateCreatedAt ?? null, p_fare_offer_xaf: request.fareOffer, p_fare_mode: normalizePassengerFareMode(request.fareMode), p_payment_preference: paymentToDatabase(request.paymentPreference), p_scheduled_at: request.scheduledAt, p_pickup_lat: pickupGps?.latitude ?? null, p_pickup_lng: pickupGps?.longitude ?? null, p_pickup_accuracy_meters: pickupGps?.accuracyMeters ?? null, p_pickup_captured_at: pickupGps?.capturedAt ?? null }; } async function saveRideRequestToSupabase(request) { if (!hasSupabaseRuntime()) return request; if (!rideRequestRpcUnavailable) { try { const body = rideRequestRpcBody(request); const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("passenger_create_ride_request", body), "Publishing the ride request", supabaseProfileSaveTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/passenger_create_ride_request", { method: "POST", body }), "Publishing the ride request", supabaseProfileSaveTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastRidePostSource = "ride request RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) { const savedRequest = mapRideRequestFromDatabase(row); await processRideRequestPushDelivery(savedRequest.id, { eventTypes: ["nearby_ride_request"] }); return savedRequest; } } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; rideRequestRpcUnavailable = true; logClientWarning("Ride request RPC is not installed yet. Falling back to direct table insert.", error); } } assertClientFallbackAllowed("Ride request publishing", "supabase-ride-request-rpc.sql"); lastRidePostSource = "direct ride request insert fallback"; if (!supabaseClient) { const data = await withSupabaseTimeout( supabaseRestRequest("/rest/v1/ride_requests", { method: "POST", body: requestPayloadForSupabase(request), headers: { Prefer: "return=representation" } }), "Publishing the ride request", supabaseProfileSaveTimeoutMs ); return mapRideRequestFromDatabase(Array.isArray(data) ? data[0] : data); } const { data, error } = await withSupabaseTimeout( supabaseClient .from("ride_requests") .insert(requestPayloadForSupabase(request)) .select("*") .single(), "Publishing the ride request", supabaseProfileSaveTimeoutMs ); if (error) throw error; return mapRideRequestFromDatabase(data); } function rideNotificationDeliveryFunctionName() { return String(appConfig.notificationDeliveryFunctionName || "notification-delivery").trim() || "notification-delivery"; } async function processRideRequestPushDelivery(requestId, { eventTypes = [] } = {}) { if (!requestId || !hasSupabaseRuntime()) return null; const functionName = rideNotificationDeliveryFunctionName(); const scopedEventTypes = Array.isArray(eventTypes) ? [...new Set(eventTypes.map((type) => String(type || "").trim()).filter(Boolean))] : []; const body = { requestId, limit: 100 }; if (scopedEventTypes.length) body.eventTypes = scopedEventTypes; try { if (supabaseClient?.functions?.invoke) { const { data, error } = await withSupabaseTimeout( supabaseClient.functions.invoke(functionName, { body }), "Processing ride request phone push", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return data; } const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${functionName}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${supabaseRestSession?.access_token || appConfig.supabaseAnonKey}`, "content-type": "application/json" }, body: JSON.stringify(body) }), "Processing ride request phone push", optionalSupabaseRequestTimeoutMs ); const payload = await response.json().catch(() => null); if (!response.ok) throw new Error(payload?.error || "Notification delivery Edge Function failed."); return payload; } catch (error) { logClientWarning("Ride request phone push could not be processed immediately.", error); return { error: error.message }; } } async function updateRideRequestFareInSupabase(requestId, fareOffer) { const request = stateLookupIndexes().requestMap.get(requestId); if (request && Number(fareOffer) !== Number(request.fareOffer ?? 0) && typeof passengerCanSendFareProposal === "function" && !passengerCanSendFareProposal(request)) { throw new Error(fareProposalLimitMessage("passenger", request)); } if (!hasSupabaseRuntime()) return; if (!marketplaceActionRpcUnavailable.fare) { try { const body = { p_request_id: requestId, p_fare_offer_xaf: fareOffer }; const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("passenger_update_open_request_fare", body), "Updating the passenger fare", supabaseProfileSaveTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/passenger_update_open_request_fare", { method: "POST", body }), "Updating the passenger fare", supabaseProfileSaveTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastMarketplaceActionSource = "marketplace action RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) { await processRideRequestPushDelivery(row.id, { eventTypes: ["passenger_fare_increased"] }); return mapRideRequestFromDatabase(row); } throw new Error("Passenger fare RPC did not return an updated request."); } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; marketplaceActionRpcUnavailable.fare = true; logClientWarning("Passenger fare RPC is not installed yet. Fare updates need server-side request checks.", error); throw new Error("Run supabase-marketplace-actions-rpc.sql before fare updates in Supabase mode."); } } if (marketplaceActionRpcUnavailable.fare) { throw new Error("Run supabase-marketplace-actions-rpc.sql before fare updates in Supabase mode."); } } async function saveOfferToSupabase(offer) { if (!hasSupabaseRuntime()) return offer; if (!marketplaceActionRpcUnavailable.offer) { try { const body = { p_ride_request_id: offer.requestId, p_fare_xaf: offer.fare, p_type: offer.type, p_public_note: offer.note || null }; const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("rider_save_offer", body), "Saving the rider offer", supabaseProfileSaveTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/rider_save_offer", { method: "POST", body }), "Saving the rider offer", supabaseProfileSaveTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastMarketplaceActionSource = "marketplace action RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) return mapOfferFromDatabase(row); throw new Error("Rider offer RPC did not return a saved offer."); } catch (error) { if (areaProximityRpcMissing(error)) areaProximityRpcUnavailable = true; if (!adminDirectoryRpcMissing(error)) throw error; marketplaceActionRpcUnavailable.offer = true; logClientWarning( areaProximityRpcMissing(error) ? "Server-side area proximity helper is not installed yet. Rider offers need server-side proximity checks." : "Rider offer RPC is not installed yet. Offer submission needs server-side proximity checks.", error ); throw new Error(areaProximityRpcMissing(error) ? "Run supabase-area-proximity.sql and supabase-marketplace-actions-rpc.sql before rider offers in Supabase mode." : "Run supabase-marketplace-actions-rpc.sql before rider offers in Supabase mode."); } } if (marketplaceActionRpcUnavailable.offer) { throw new Error("Run supabase-marketplace-actions-rpc.sql before rider offers in Supabase mode."); } } async function withdrawRiderOfferFromSupabase(requestId) { if (!hasSupabaseRuntime()) return null; const body = { p_ride_request_id: requestId }; const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("rider_withdraw_offer", body), "Leaving the rider negotiation", supabaseProfileSaveTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/rider_withdraw_offer", { method: "POST", body }), "Leaving the rider negotiation", supabaseProfileSaveTimeoutMs ); if (supabaseClient && data.error) throw data.error; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) { await processRideRequestPushDelivery(row.id, { eventTypes: ["rider_offer_withdrawn"] }); return mapRideRequestFromDatabase(row); } return null; } async function rejectRiderOfferInSupabase(offerId) { if (!hasSupabaseRuntime()) return null; if (!marketplaceActionRpcUnavailable.rejection) { try { const body = { p_offer_id: offerId }; const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("passenger_reject_rider_offer", body), "Rejecting the rider offer", supabaseProfileSaveTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/passenger_reject_rider_offer", { method: "POST", body }), "Rejecting the rider offer", supabaseProfileSaveTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastMarketplaceActionSource = "marketplace action RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) { await processRideRequestPushDelivery(row.id, { eventTypes: ["passenger_rejected_offer"] }); return mapRideRequestFromDatabase(row); } return null; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; marketplaceActionRpcUnavailable.rejection = true; logClientWarning("Passenger rider-offer rejection RPC is not installed yet. Rejection needs server-side rider suppression.", error); throw new Error("Run supabase-marketplace-actions-rpc.sql before rejecting rider offers in Supabase mode."); } } throw new Error("Run supabase-marketplace-actions-rpc.sql before rejecting rider offers in Supabase mode."); } async function chooseOfferInSupabase(request, offer) { if (!hasSupabaseRuntime()) return; if (!marketplaceActionRpcUnavailable.selection) { try { const body = { p_offer_id: offer.id }; const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("passenger_select_rider_offer", body), "Choosing the rider offer", supabaseProfileSaveTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/passenger_select_rider_offer", { method: "POST", body }), "Choosing the rider offer", supabaseProfileSaveTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastMarketplaceActionSource = "marketplace action RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) return mapRideRequestFromDatabase(row, new Map(), new Map([[offer.id, offer]])); return null; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; marketplaceActionRpcUnavailable.selection = true; logClientWarning("Passenger rider-selection RPC is not installed yet. Rider selection needs server-side proximity checks.", error); throw new Error("Run supabase-marketplace-actions-rpc.sql before choosing riders in Supabase mode."); } } if (marketplaceActionRpcUnavailable.selection) { throw new Error("Run supabase-marketplace-actions-rpc.sql before choosing riders in Supabase mode."); } } function currentActorIdForChat() { if (activeRole() === "passenger") return state.sessions.passenger?.userId ?? state.passenger?.id; if (activeRole() === "rider") return state.sessions.rider?.userId ?? state.rider?.id; return state.adminSession?.userId ?? null; } const chatVoiceMaxDurationMs = 60 * 1000; const chatVoiceMaxBytes = 5 * 1024 * 1024; const chatVoiceSignedUrlCache = new Map(); let chatVoiceRecorder = null; let chatVoiceRecordingStream = null; let chatVoiceRecordingStartedAt = 0; let chatVoiceRecordingTimer = null; let chatVoiceRecordingChunks = []; function chatVoiceBucketName() { return String(appConfig.buckets?.rideVoiceNotes || "ride-voice-notes").trim() || "ride-voice-notes"; } function clearChatVoiceSignedUrlCacheForRequest(requestId) { if (!requestId) return; const needle = `/${requestId}/`; for (const cacheKey of chatVoiceSignedUrlCache.keys()) { if (cacheKey.includes(needle)) chatVoiceSignedUrlCache.delete(cacheKey); } } function isChatVoiceMessage(message) { return message?.mediaType === "voice_note" && Boolean(message.mediaPath); } function chatVoiceDurationLabel(seconds) { const total = Math.max(0, Math.round(Number(seconds) || 0)); const minutes = Math.floor(total / 60); const remainder = String(total % 60).padStart(2, "0"); return `${minutes}:${remainder}`; } function preferredChatVoiceMimeType() { if (typeof MediaRecorder === "undefined" || typeof MediaRecorder.isTypeSupported !== "function") return ""; return [ "audio/webm;codecs=opus", "audio/webm", "audio/ogg;codecs=opus", "audio/mp4" ].find((type) => MediaRecorder.isTypeSupported(type)) || ""; } function chatVoiceFileExtension(mimeType = "") { const type = String(mimeType).toLowerCase(); if (type.includes("ogg")) return "ogg"; if (type.includes("mp4") || type.includes("m4a")) return "m4a"; if (type.includes("wav")) return "wav"; return "webm"; } function safeStorageSegment(value, fallback = "voice") { return String(value || fallback) .replace(/[^a-z0-9._-]/gi, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, "") .slice(0, 96) || fallback; } function setChatVoiceStatus(message) { if (els.chatVoiceStatus) els.chatVoiceStatus.textContent = message || ""; } function chatVoiceRecordingSupported() { return Boolean(navigator.mediaDevices?.getUserMedia && typeof MediaRecorder !== "undefined"); } function selectedChatRequest() { const workspaceRequest = typeof selectedWorkspaceRequest === "function" ? selectedWorkspaceRequest() : null; if (workspaceRequest && canChatOnRequest(workspaceRequest)) return workspaceRequest; const activeRide = typeof activeRideForRole === "function" ? activeRideForRole(selectedRequest()) : null; if (activeRide && canChatOnRequest(activeRide)) return activeRide; const request = selectedRequest(); return request && canChatOnRequest(request) ? request : workspaceRequest ?? activeRide ?? request; } function chatVoiceIsRecording() { return Boolean(chatVoiceRecorder); } function updateChatVoiceButtonState({ disabled = false, recording = false } = {}) { if (!els.chatVoiceButton) return; els.chatVoiceButton.disabled = disabled; els.chatVoiceButton.classList.toggle("recording", recording); els.chatVoiceButton.textContent = recording ? "Stop" : "Voice"; els.chatVoiceButton.setAttribute("aria-label", recording ? "Stop and send voice message" : "Record voice message"); } function stopChatVoiceRecording() { if (!chatVoiceRecorder) return; if (chatVoiceRecorder.state !== "inactive") chatVoiceRecorder.stop(); } function cleanupChatVoiceRecording() { if (chatVoiceRecordingTimer) window.clearTimeout(chatVoiceRecordingTimer); chatVoiceRecordingTimer = null; chatVoiceRecordingStream?.getTracks?.().forEach((track) => track.stop()); chatVoiceRecordingStream = null; chatVoiceRecorder = null; chatVoiceRecordingStartedAt = 0; chatVoiceRecordingChunks = []; updateChatVoiceButtonState({ disabled: false, recording: false }); } async function uploadChatVoiceNote(requestId, messageId, blob) { if (!isSupabaseMode() || !supabaseClient) { throw new Error("Voice messages require Supabase storage."); } const senderId = currentActorIdForChat(); if (!senderId) throw new Error("Sign in before sending voice messages."); const extension = chatVoiceFileExtension(blob.type); const path = [ safeStorageSegment(senderId, "sender"), safeStorageSegment(requestId, "request"), `${safeStorageSegment(messageId, "voice")}-${Date.now()}.${extension}` ].join("/"); const { error } = await withSupabaseTimeout( supabaseClient.storage .from(chatVoiceBucketName()) .upload(path, blob, { contentType: blob.type || "audio/webm", cacheControl: "3600", upsert: false }), "Uploading the voice message", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return { mediaBucket: chatVoiceBucketName(), mediaPath: path }; } async function ensureChatVoiceAudioUrl(message, audioElement) { if (!audioElement || !isChatVoiceMessage(message)) return; if (message.mediaUrl) { audioElement.src = message.mediaUrl; return; } if (!isSupabaseMode() || !supabaseClient) return; const cacheKey = `${message.mediaBucket || chatVoiceBucketName()}:${message.mediaPath}`; const cached = chatVoiceSignedUrlCache.get(cacheKey); if (cached) { audioElement.src = cached; return; } try { const { data, error } = await withSupabaseTimeout( supabaseClient.storage .from(message.mediaBucket || chatVoiceBucketName()) .createSignedUrl(message.mediaPath, 600), "Opening the voice message", optionalSupabaseRequestTimeoutMs ); if (error || !data?.signedUrl) throw error || new Error("Signed voice message URL was not returned."); chatVoiceSignedUrlCache.set(cacheKey, data.signedUrl); audioElement.src = data.signedUrl; } catch (error) { audioElement.replaceWith(Object.assign(document.createElement("small"), { textContent: `Voice message could not open: ${error.message}` })); } } function chatMessageShouldNotifyCounterparty(message) { return Boolean(message?.requestId && message.sender !== "system" && (String(message.text ?? "").trim() || isChatVoiceMessage(message))); } async function saveChatMessageToSupabase(message, { throwOnError = false } = {}) { if (!hasSupabaseRuntime()) return; const senderId = message.senderId || currentActorIdForChat(); if (!senderId) { const error = new Error("Sign in before sending chat messages."); if (throwOnError) throw error; return null; } const body = message.sender === "system" ? `[System] ${message.systemPayload ?? message.text}` : message.text; const hasMedia = isChatVoiceMessage(message); try { if (hasMedia) { const rpcBody = { p_ride_request_id: message.requestId, p_body: body, p_sender_role: message.sender === "rider" ? "rider" : message.sender === "passenger" ? "passenger" : null, p_media_type: message.mediaType, p_media_bucket: message.mediaBucket || chatVoiceBucketName(), p_media_path: message.mediaPath, p_media_mime_type: message.mediaMimeType || "", p_media_duration_seconds: Math.round(Number(message.mediaDurationSeconds) || 0), p_media_size_bytes: Math.round(Number(message.mediaSizeBytes) || 0) }; const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("save_ride_chat_message_with_media", rpcBody), "Saving the voice message", optionalSupabaseRequestTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/save_ride_chat_message_with_media", { method: "POST", body: rpcBody }), "Saving the voice message", optionalSupabaseRequestTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastMarketplaceActionSource = "marketplace action media RPC"; if (chatMessageShouldNotifyCounterparty(message)) { void processRideRequestPushDelivery(message.requestId, { eventTypes: ["ride_chat_message"] }); } const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; return row?.id ? mapChatFromDatabase(row) : null; } if (!marketplaceActionRpcUnavailable.chat) { try { const rpcBody = { p_ride_request_id: message.requestId, p_body: body, p_sender_role: message.sender === "rider" ? "rider" : message.sender === "passenger" ? "passenger" : null }; const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("save_ride_chat_message", rpcBody), "Saving the chat message", optionalSupabaseRequestTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/save_ride_chat_message", { method: "POST", body: rpcBody }), "Saving the chat message", optionalSupabaseRequestTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastMarketplaceActionSource = "marketplace action RPC"; if (chatMessageShouldNotifyCounterparty(message)) { void processRideRequestPushDelivery(message.requestId, { eventTypes: ["ride_chat_message"] }); } const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; return row?.id ? mapChatFromDatabase(row) : null; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; marketplaceActionRpcUnavailable.chat = true; logClientWarning("Ride chat RPC is not installed yet. Falling back to direct chat insert.", error); } } const payload = { ride_request_id: message.requestId, sender_id: senderId, sender_role: message.sender === "rider" ? "rider" : message.sender === "passenger" ? "passenger" : null, body, media_type: message.mediaType ?? null, media_bucket: message.mediaBucket ?? null, media_path: message.mediaPath ?? null, media_mime_type: message.mediaMimeType ?? null, media_duration_seconds: message.mediaDurationSeconds ?? null, media_size_bytes: message.mediaSizeBytes ?? null }; assertClientFallbackAllowed("Ride chat message save", "supabase-marketplace-actions-rpc.sql"); lastMarketplaceActionSource = "direct table write fallback"; if (!supabaseClient) { await withSupabaseTimeout( supabaseRestRequest("/rest/v1/ride_chats", { method: "POST", body: payload, headers: { Prefer: "return=minimal" } }), "Saving the chat message", optionalSupabaseRequestTimeoutMs ); return null; } const { error } = await withSupabaseTimeout( supabaseClient.from("ride_chats").insert(payload), "Saving the chat message", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return null; } catch (error) { logClientWarning("Chat message was not synced to Supabase.", error); if (throwOnError) throw error; return null; } } function contactRelayFunctionName() { return String(appConfig.contactRelayFunctionName || "ride-contact-relay").trim() || "ride-contact-relay"; } function ridePaymentSettlementFunctionName() { return String(appConfig.ridePaymentSettlementFunctionName || "ride-payment-settlement").trim() || "ride-payment-settlement"; } async function processRidePaymentSettlement(requestId) { if (!hasSupabaseRuntime()) return null; const token = await currentSupabaseAccessToken(); if (!token) throw new Error("Sign in before processing the ride payment."); const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${ridePaymentSettlementFunctionName()}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ requestId }) }), "Processing completed ride payment", supabaseProfileSaveTimeoutMs ); const payload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(payload?.error || "Stripe ride payment settlement failed."); return payload; } async function relayRideChatMessageToPhone(message) { if (!hasSupabaseRuntime() || message.sender === "system") return; const token = await currentSupabaseAccessToken(); if (!token) return; try { const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${contactRelayFunctionName()}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ channel: "sms", rideRequestId: message.requestId, message: message.text }) }), "Sending Waka SMS relay", optionalSupabaseRequestTimeoutMs ); const payload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(payload?.error || "Waka SMS relay failed."); if (els.chatStatus) els.chatStatus.textContent = "Open - SMS relay sent"; } catch (error) { if (els.chatStatus) { els.chatStatus.textContent = /not configured|missing|provider/i.test(String(error.message)) ? "Open - in-app sent; SMS relay not configured" : "Open - in-app sent; SMS relay failed"; } logClientWarning("Waka SMS relay was not sent.", error); } } async function saveSafetyReportToSupabase(report) { if (!hasSupabaseRuntime()) return report; const payload = { ride_request_id: report.requestId, reporter_id: report.reporterId, reporter_role: report.reporterRole, reported_user_id: report.reportedUserId, category: report.category, severity: report.severity, details: report.details, status: "open" }; const preserveDisplayFields = (savedReport) => ({ ...savedReport, reporterName: report.reporterName ?? savedReport.reporterName, reportedUserName: report.reportedUserName ?? savedReport.reportedUserName, routeSummary: report.routeSummary ?? savedReport.routeSummary }); if (!safetyReportRpcUnavailable.submit) { try { const rpcBody = { p_ride_request_id: report.requestId, p_reported_user_id: report.reportedUserId, p_category: report.category, p_severity: report.severity, p_details: report.details }; const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("submit_safety_report", rpcBody), "Submitting the safety report", optionalSupabaseRequestTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/submit_safety_report", { method: "POST", body: rpcBody }), "Submitting the safety report", optionalSupabaseRequestTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastSafetyReportSource = "safety report RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) return preserveDisplayFields(mapSafetyReportFromDatabase(row)); } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; safetyReportRpcUnavailable.submit = true; logClientWarning("Safety report submit RPC is not installed yet. Falling back to direct table insert.", error); } } assertClientFallbackAllowed("Safety report submission", "supabase-safety-reports.sql"); lastSafetyReportSource = "direct safety report insert fallback"; if (!supabaseClient) { const data = await withSupabaseTimeout( supabaseRestRequest("/rest/v1/safety_reports", { method: "POST", body: payload, headers: { Prefer: "return=representation" } }), "Submitting the safety report", optionalSupabaseRequestTimeoutMs ); return preserveDisplayFields(mapSafetyReportFromDatabase(Array.isArray(data) ? data[0] : data)); } const { data, error } = await withSupabaseTimeout( supabaseClient .from("safety_reports") .insert(payload) .select("*") .single(), "Submitting the safety report", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return preserveDisplayFields(mapSafetyReportFromDatabase(data)); } async function saveSupportTicketToSupabase(ticket) { if (!hasSupabaseRuntime()) return ticket; const payload = { account_id: ticket.accountId, account_role: ticket.accountRole, category: ticket.category, subject: ticket.subject, message: ticket.message, priority: ticket.priority, status: "open" }; const preserveDisplayFields = (savedTicket) => ({ ...savedTicket, accountName: ticket.accountName ?? savedTicket.accountName }); if (!supportTicketRpcUnavailable.submit) { try { const rpcBody = { p_category: ticket.category, p_subject: ticket.subject, p_message: ticket.message, p_priority: ticket.priority }; const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("submit_support_ticket", rpcBody), "Submitting the support request", optionalSupabaseRequestTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/submit_support_ticket", { method: "POST", body: rpcBody }), "Submitting the support request", optionalSupabaseRequestTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastSupportTicketSource = "support ticket RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) return preserveDisplayFields(mapSupportTicketFromDatabase(row)); } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; supportTicketRpcUnavailable.submit = true; logClientWarning("Support ticket RPC is not installed yet. Falling back to direct table insert.", error); } } assertClientFallbackAllowed("Support ticket submission", "supabase-support-tickets.sql"); lastSupportTicketSource = "direct support ticket insert fallback"; if (!supabaseClient) { const data = await withSupabaseTimeout( supabaseRestRequest("/rest/v1/support_tickets", { method: "POST", body: payload, headers: { Prefer: "return=representation" } }), "Submitting the support request", optionalSupabaseRequestTimeoutMs ); return preserveDisplayFields(mapSupportTicketFromDatabase(Array.isArray(data) ? data[0] : data)); } const { data, error } = await withSupabaseTimeout( supabaseClient .from("support_tickets") .insert(payload) .select("*") .single(), "Submitting the support request", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return preserveDisplayFields(mapSupportTicketFromDatabase(data)); } async function saveRideRatingToSupabase(rating) { if (!hasSupabaseRuntime()) return rating; await ensureRideRatingReviewerSession(rating); const legacyRpcBody = { p_ride_request_id: rating.requestId, p_rated_user_id: rating.ratedUserId, p_score: rating.score, p_comment: rating.comment }; const rpcBody = { ...legacyRpcBody, p_safety_score: rating.safetyScore ?? rating.score, p_punctuality_score: rating.punctualityScore ?? rating.score, p_communication_score: rating.communicationScore ?? rating.score, p_vehicle_score: rating.vehicleScore ?? rating.score }; const submitRatingRpc = async (functionName, body) => { const result = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc(functionName, body), "Submitting the ride rating", optionalSupabaseRequestTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rpc/${functionName}`, { method: "POST", body }), "Submitting the ride rating", optionalSupabaseRequestTimeoutMs ); if (supabaseClient && result.error) throw result.error; return result; }; try { let data; try { data = await submitRatingRpc("submit_ride_rating_v2", rpcBody); } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; data = await submitRatingRpc("submit_ride_rating", legacyRpcBody); } if (supabaseClient && data.error) throw data.error; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) return mapRideRatingFromDatabase(row); throw new Error("Ride rating RPC did not return a saved rating."); } catch (error) { if (adminDirectoryRpcMissing(error)) { logClientWarning("Ride rating RPC is not installed yet. Ratings need server-side completed-ride and counterparty checks.", error); throw new Error("Run supabase-ride-ratings.sql before ride ratings in Supabase mode."); } throw error; } } function rideRatingRoleLabel(role = activeRole()) { return role === "rider" ? "rider" : "passenger"; } async function ensureRideRatingReviewerSession(rating) { if (!hasSupabaseRuntime()) return; const reviewerId = String(rating?.reviewerId ?? "").trim(); const roleLabel = rideRatingRoleLabel(rating?.reviewerRole); if (!reviewerId) { throw new Error(`Sign in again as the ${roleLabel} before submitting this rating.`); } if (typeof getSupabaseUser !== "function") return; const user = await getSupabaseUser(); if (!user?.id) { throw new Error(`Sign in again as the ${roleLabel} before submitting this rating.`); } if (String(user.id) !== reviewerId) { throw new Error(`This browser is currently signed in as another Waka account. Sign in again as the ${roleLabel} for this completed ride, then submit the rating.`); } } async function saveRideTipToSupabase(tip) { if (!hasSupabaseRuntime()) return tip; const rpcBody = { p_ride_request_id: tip.requestId, p_tip_amount_cents: dollarsToCents(tip.amount) }; try { const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("passenger_tip_rider", rpcBody), "Submitting the rider tip", optionalSupabaseRequestTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/passenger_tip_rider", { method: "POST", body: rpcBody }), "Submitting the rider tip", optionalSupabaseRequestTimeoutMs ); if (supabaseClient && data.error) throw data.error; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) return mapRideTipFromDatabase(row); throw new Error("Passenger tip RPC did not return a saved tip."); } catch (error) { if (adminDirectoryRpcMissing(error)) { logClientWarning("Ride tip RPC is not installed yet. Tips need server-side completion and payout checks.", error); throw new Error("Run supabase-ride-lifecycle.sql before passenger tips in Supabase mode."); } throw error; } } async function updateRideDestinationInSupabase(request, nextDestination, guidance, nextStops = request.rideStops, destinationPlace = null, nextStopPoints = rideStopPointsForRoute(nextStops, request.rideStopPoints)) { if (!hasSupabaseRuntime()) return null; const destinationChanged = ![request.destination, request.destinationFormattedAddress] .some((value) => String(value ?? "").trim().toLowerCase() === String(nextDestination ?? "").trim().toLowerCase()); const existingPlace = destinationChanged ? null : normalizedPlaceSelection({ placeId: request.destinationPlaceId, displayName: request.destination, formattedAddress: request.destinationFormattedAddress, latitude: request.destinationLatitude, longitude: request.destinationLongitude }); const selectedPlace = normalizedPlaceSelection(destinationPlace) ?? existingPlace; const nextDestinationArea = destinationAreaForPublish( request.country, request.city, request.destinationArea, guidance?.destinationFormattedAddress ?? selectedPlace?.formattedAddress ?? nextDestination, selectedPlace ); const body = { p_request_id: request.id, p_destination_area: nextDestinationArea, p_destination: nextDestination, p_destination_place_id: guidance?.destinationPlaceId ?? selectedPlace?.placeId ?? null, p_destination_formatted_address: guidance?.destinationFormattedAddress ?? selectedPlace?.formattedAddress ?? null, p_destination_lat: guidance?.destinationLatitude ?? selectedPlace?.latitude ?? null, p_destination_lng: guidance?.destinationLongitude ?? selectedPlace?.longitude ?? null, p_ride_stops: normalizeRideStops(nextStops), p_ride_stop_points: normalizeRideStopPoints(nextStopPoints, nextStops), p_estimated_distance_miles: guidance?.distanceMiles ?? request.estimatedDistanceMiles ?? null, p_estimated_travel_minutes: guidance?.minutes ?? request.estimatedTravelMinutes ?? null, p_route_estimate_source: normalizedRouteEstimateSourceForDatabase(guidance?.source ?? request.routeEstimateSource), p_route_estimate_provider: normalizedRouteEstimateProviderForDatabase(guidance?.source ?? request.routeEstimateSource, guidance?.provider ?? request.routeEstimateProvider), p_route_estimate_cached: Boolean(guidance?.cached ?? request.routeEstimateCached), p_route_estimate_key: guidance?.routeKey ?? request.routeEstimateKey ?? null, p_route_estimate_destination_fingerprint: guidance?.destinationFingerprint ?? request.routeEstimateDestinationFingerprint ?? null, p_route_estimate_polyline: guidance?.routePolyline ?? request.routeEstimatePolyline ?? null, p_route_estimate_created_at: guidance?.estimatedAt ?? request.routeEstimateCreatedAt ?? null }; const row = await callSupabaseRpc( "passenger_update_ride_destination", body, "Updating the ride destination", optionalSupabaseRequestTimeoutMs ); if (Array.isArray(row)) return row[0] ?? null; return row ?? null; } const routeChangeEventPrefix = "WAKA_ROUTE_CHANGE_EVENT "; function routeChangeTypeLabel(type) { return type === "add_stop" ? "added stop" : "final destination change"; } function routeChangeVerb(type) { return type === "add_stop" ? "add a stop" : "change the destination"; } function routeChangeSystemText(event) { const type = routeChangeTypeLabel(event.type); const additionalFare = formatMoney(event.additionalFare ?? 0, event.country); const totalFare = formatMoney(event.totalFare ?? 0, event.country); const destination = event.destination ? ` Destination: ${event.destination}.` : ""; const stopCount = normalizeRideStops(event.rideStops).length; const stopText = stopCount ? ` Stops on route: ${stopCount}.` : ""; if (event.action === "accepted") { return `Rider acknowledged the ${type}. The route is updated. Added fare: ${additionalFare}. New ride total: ${totalFare}.${destination}${stopText}`; } if (event.action === "declined") { return `Rider declined the ${type}. The agreed route and fare stay unchanged.`; } return `Passenger requested a ${type}. Rider must acknowledge to update the route, or decline to keep the current route. Added fare if accepted: ${additionalFare}. New ride total: ${totalFare}.${destination}${stopText}`; } function parseRouteChangeEventText(text) { const clean = String(text ?? "").replace(/^\[System\]\s*/i, "").trim(); if (!clean.startsWith(routeChangeEventPrefix)) return null; try { const event = JSON.parse(clean.slice(routeChangeEventPrefix.length)); if (!event || event.kind !== "route_change" || !event.id || !event.requestId) return null; return { event, message: routeChangeSystemText(event) }; } catch { return null; } } function routeChangeEventPayload(change, action, request = state.requests.find((item) => item.id === change.requestId)) { return { kind: "route_change", action, id: change.id, requestId: change.requestId, type: change.type, country: request?.country ?? change.country ?? defaultLaunchCountry(), destinationArea: change.destinationArea ?? routeChangeDestinationArea(request, change), destination: change.destination, destinationPlaceId: change.destinationPlaceId ?? null, destinationFormattedAddress: change.destinationFormattedAddress ?? null, destinationLatitude: change.destinationLatitude ?? null, destinationLongitude: change.destinationLongitude ?? null, rideStops: normalizeRideStops(change.rideStops), rideStopPoints: normalizeRideStopPoints(change.rideStopPoints, change.rideStops), routeEstimate: change.routeEstimate ?? null, routeDelta: change.routeDelta ?? null, additionalFare: Math.max(0, Number(change.additionalFare ?? 0) || 0), totalFare: Math.max(0, Number(change.totalFare ?? 0) || 0), acceptedRouteChangeFare: Math.max(0, Number(change.acceptedRouteChangeFare ?? request?.acceptedRouteChangeFare ?? 0) || 0), requestedAt: change.requestedAt ?? new Date().toISOString(), decidedAt: action === "proposed" ? null : new Date().toISOString(), passengerId: request?.passengerId ?? change.passengerId ?? null, riderId: selectedRiderIdForRequest(request) ?? change.riderId ?? null }; } function pushRouteChangeSystemEvent(change, action, request = state.requests.find((item) => item.id === change.requestId)) { const event = routeChangeEventPayload(change, action, request); const message = { id: makeId("chat"), requestId: event.requestId, sender: "system", text: routeChangeSystemText(event), systemPayload: `${routeChangeEventPrefix}${JSON.stringify(event)}`, routeChangeEvent: event, createdAt: new Date().toISOString() }; state.chats.push(message); void saveChatMessageToSupabase(message); return message; } function routeChangeRequestsForRequest(requestId) { return (state.routeChangeRequests ?? []) .filter((change) => change.requestId === requestId) .sort((a, b) => new Date(b.requestedAt ?? b.createdAt ?? 0) - new Date(a.requestedAt ?? a.createdAt ?? 0)); } function pendingRouteChangeForRequest(request) { if (!request?.id) return null; return routeChangeRequestsForRequest(request.id).find((change) => change.status === "pending") ?? null; } function requestWithPendingRouteChange(request, change = pendingRouteChangeForRequest(request)) { if (!request || !change || change.status !== "pending") return request; return { ...request, ...routeChangePatch(change, request), pendingRouteChangeId: change.id, pendingRouteChange: change }; } function riderVisibleRouteRequest(request) { return activeRole() === "rider" && requestIsActiveForCurrentRider(request) ? requestWithPendingRouteChange(request) : request; } function routeChangeNeedsRiderApproval(request) { return Boolean(request && ["matched", "arrived", "in_progress"].includes(request.status) && (selectedRiderIdForRequest(request) || requestHasSelectedOffer(request))); } function routeChangeEstimatedTotal(request, additionalFare) { const base = routeChangeNeedsRiderApproval(request) ? agreedFareForRequest(request) : Number(request?.fareOffer ?? 0); return Math.max(0, Number(base || 0) + Number(additionalFare || 0)); } function routeChangeBaseFare(request) { return routeChangeNeedsRiderApproval(request) ? agreedFareForRequest(request) : Number(request?.fareOffer ?? 0) || 0; } function routeChangeIsAfterPickup(request) { return request?.status === "in_progress"; } function routeChangeElapsedRideMinutes(request) { if (!routeChangeIsAfterPickup(request) || !request?.startedAt) return 0; const startedAt = new Date(request.startedAt).getTime(); if (!Number.isFinite(startedAt)) return 0; return Math.max(0, Math.ceil((Date.now() - startedAt) / 60000)); } function destinationChangeBillableMinutes(guidance, stops = []) { const distanceMiles = positiveNumberOrNull(guidance?.distanceMiles); const stopCount = normalizeRideStops(stops).length; const rawMinutes = positiveNumberOrNull(guidance?.minutes); return distanceMiles == null ? rawMinutes : Math.min( rawMinutes ?? Math.ceil(distanceMiles * 2.1), Math.max(1, Math.ceil(distanceMiles * routeChangeFareConfig.billableMinutesPerAddedMile + stopCount * fareGuidanceConfig.perStopMinutes)) ); } function destinationChangeReplacementFare(guidance, country, stops = []) { if (!guidance) return null; const distanceMiles = positiveNumberOrNull(guidance.distanceMiles); const billableMinutes = destinationChangeBillableMinutes(guidance, stops); const fareGuidance = distanceMiles == null ? null : fareGuidanceFromDistance(distanceMiles, billableMinutes, stops, { source: guidance.source, provider: guidance.provider, cached: guidance.cached, routeKey: guidance.routeKey, routePolyline: guidance.routePolyline, country, city: guidance.city, destinationFingerprint: guidance.destinationFingerprint, estimatedAt: guidance.estimatedAt }); const fare = Number(fareGuidance?.midpoint ?? guidance.midpoint ?? guidance.min ?? guidance.max); if (!Number.isFinite(fare) || fare <= 0) return null; return Math.max(minimumFareOffer(country), Math.ceil(fare)); } function routeChangeAdditionalFare(request, guidance, nextStops, type, baselineGuidance = null) { if (type === "change_destination" && !routeChangeIsAfterPickup(request)) { const replacementFare = destinationChangeReplacementFare(guidance, request?.country, nextStops); if (replacementFare != null) { return Math.ceil(replacementFare - routeChangeBaseFare(request)); } } const delta = routeChangeDeltaSummary(request, guidance, nextStops, baselineGuidance); const elapsedRideMinutes = type === "change_destination" ? routeChangeElapsedRideMinutes(request) : 0; const directCost = ( delta.addedMiles * fareGuidanceConfig.perMileUsd + delta.billableAddedMinutes * fareGuidanceConfig.perMinuteUsd * routeChangeFareConfig.trafficTimeChargeMultiplier + delta.addedStops * fareGuidanceConfig.perStopUsd + elapsedRideMinutes * fareGuidanceConfig.perMinuteUsd * routeChangeFareConfig.trafficTimeChargeMultiplier ) * fareGuidanceConfig.fuelIndex; const baseFare = Math.max(0, agreedFareForRequest(request)); const proportionalCost = type !== "add_stop" && delta.baselineDistanceMiles != null && delta.nextDistanceMiles != null && delta.nextDistanceMiles > delta.baselineDistanceMiles ? baseFare * ((delta.nextDistanceMiles - delta.baselineDistanceMiles) / delta.baselineDistanceMiles) : 0; const minimum = type === "add_stop" ? routeChangeFareConfig.minStopFareUsd : routeChangeFareConfig.minAdditionalFareUsd; const surcharge = routeChangeIsAfterPickup(request) ? 1 + routeChangeFareConfig.afterPickupSurchargeRate : 1; return Math.max(1, Math.ceil(Math.max(directCost, proportionalCost, minimum) * surcharge)); } function routeChangeTotalFare(request, additionalFare, guidance, nextStops, type) { if (type === "change_destination" && !routeChangeIsAfterPickup(request)) { const replacementFare = destinationChangeReplacementFare(guidance, request?.country, nextStops); if (replacementFare != null) return replacementFare; } return routeChangeEstimatedTotal(request, additionalFare); } function routeChangeGuidanceIsConfirmed(guidance) { if (!guidance) return false; return ["google-routes", "cameroon-local"].includes(normalizedRouteEstimateSourceForDatabase(guidance.source)); } function fallbackGuidanceIsUsableForRouteChange(guidance, request) { return Boolean(guidance) && positiveNumberOrNull(request?.estimatedDistanceMiles) != null && positiveNumberOrNull(guidance?.distanceMiles) != null && positiveNumberOrNull(guidance?.minutes) != null; } function routeChangeFallbackGuidance(request, nextStops) { const storedDistance = positiveNumberOrNull(request?.estimatedDistanceMiles); const storedMinutes = positiveNumberOrNull(request?.estimatedTravelMinutes); if (storedDistance == null) return null; const currentStops = normalizeRideStops(request?.rideStops); const updatedStops = normalizeRideStops(nextStops); const addedStops = Math.max(0, updatedStops.length - currentStops.length); const adjustedDistanceMiles = storedDistance * (1 + addedStops * fareGuidanceConfig.stopDistanceMultiplier); const adjustedMinutes = (storedMinutes ?? Math.ceil(storedDistance * 2.1)) + addedStops * fareGuidanceConfig.perStopMinutes; return fareGuidanceFromDistance(adjustedDistanceMiles, adjustedMinutes, updatedStops, { source: "zone", provider: "stored-route-fallback", country: request?.country, city: request?.city }); } function routeChangeDestinationFallbackGuidance(request, destination, nextStops, destinationPlace = null) { const pickupGps = normalizeGpsPoint(requestPickupGps(request)); const selectedDestination = normalizedPlaceSelection(destinationPlace); const destinationPoint = validGpsCoordinate(Number(selectedDestination?.latitude), Number(selectedDestination?.longitude)) ? { latitude: Number(selectedDestination.latitude), longitude: Number(selectedDestination.longitude) } : null; if (pickupGps && destinationPoint) { const stopPoints = normalizeRideStops(nextStops).map(stopRoutePoint); const points = [pickupGps, ...stopPoints.filter(Boolean), destinationPoint]; const straightKm = points.slice(1).reduce((total, point, index) => ( total + gpsDistanceKmBetween(points[index], point) ), 0); if (Number.isFinite(straightKm) && straightKm > 0) { const missingStopPointCount = Math.max(0, normalizeRideStops(nextStops).length - stopPoints.filter(Boolean).length); const distanceMiles = Math.max(0.6, straightKm * riderPickupEtaRoadFactor * kmToMiles); const minutes = Math.max(3, Math.ceil(distanceMiles * 2.1 + missingStopPointCount * fareGuidanceConfig.perStopMinutes)); return fareGuidanceFromDistance(distanceMiles, minutes, nextStops, { source: "place-preview", provider: "local-route-change-preview", country: request?.country, city: request?.city }); } } const storedFallback = routeChangeFallbackGuidance(request, nextStops); return storedFallback ? { ...storedFallback, provider: "stored-route-destination-fallback" } : null; } function requireConfirmedRouteChangeGuidance(guidance, label, fallback = null, request = null) { if (!routeEstimatesEnabled()) return guidance; const safeGuidance = safeRouteGuidance(guidance, fallback, label.toLowerCase()); if (routeChangeGuidanceIsConfirmed(safeGuidance)) return safeGuidance; if (fallbackGuidanceIsUsableForRouteChange(fallback, request)) return fallback; throw new Error(`${label} could not be confirmed by Waka route estimates. Choose the pickup, stop, and destination from address suggestions, then try again.`); } function positiveNumberOrNull(value) { const number = Number(value); return Number.isFinite(number) && number > 0 ? number : null; } function routeChangeBaselineGuidance(request, guidance = null) { return { distanceMiles: positiveNumberOrNull(guidance?.distanceMiles) ?? positiveNumberOrNull(request?.estimatedDistanceMiles), minutes: positiveNumberOrNull(guidance?.minutes) ?? positiveNumberOrNull(request?.estimatedTravelMinutes), source: guidance?.source ?? request?.routeEstimateSource ?? null, provider: guidance?.provider ?? request?.routeEstimateProvider ?? null, cached: Boolean(guidance?.cached ?? request?.routeEstimateCached), routeKey: guidance?.routeKey ?? request?.routeEstimateKey ?? null, routePolyline: guidance?.routePolyline ?? request?.routeEstimatePolyline ?? null, destinationFingerprint: guidance?.destinationFingerprint ?? request?.routeEstimateDestinationFingerprint ?? null, estimatedAt: guidance?.estimatedAt ?? request?.routeEstimateCreatedAt ?? null }; } function routeChangeDeltaSummary(request, guidance, nextStops, baselineGuidance = null) { const baseline = routeChangeBaselineGuidance(request, baselineGuidance); const nextDistance = Number(guidance?.distanceMiles); const nextMinutes = Number(guidance?.minutes); const addedMiles = baseline.distanceMiles != null && Number.isFinite(nextDistance) ? Math.max(0, nextDistance - baseline.distanceMiles) : 0; const addedMinutes = baseline.minutes != null && Number.isFinite(nextMinutes) ? Math.max(0, nextMinutes - baseline.minutes) : 0; const previousStops = normalizeRideStops(request?.rideStops).length; const addedStops = Math.max(0, normalizeRideStops(nextStops).length - previousStops); const billableAddedMinutes = Math.min( addedMinutes, Math.max(0, Math.ceil( addedMiles * routeChangeFareConfig.billableMinutesPerAddedMile + addedStops * fareGuidanceConfig.perStopMinutes )) ); return { baselineDistanceMiles: baseline.distanceMiles, baselineMinutes: baseline.minutes, nextDistanceMiles: Number.isFinite(nextDistance) && nextDistance > 0 ? nextDistance : null, nextMinutes: Number.isFinite(nextMinutes) && nextMinutes > 0 ? nextMinutes : null, addedMiles, addedMinutes, billableAddedMinutes, addedStops, nextProvider: guidance?.provider ?? null, baselineSource: baseline.source, baselineProvider: baseline.provider }; } function routeChangeDistanceLine(request, delta) { const pieces = []; if (delta.baselineDistanceMiles != null) { pieces.push(`Current route: ${formatRouteDistanceForRequest(delta.baselineDistanceMiles, request)}`); } if (delta.nextDistanceMiles != null) { pieces.push(`Updated route: ${formatRouteDistanceForRequest(delta.nextDistanceMiles, request)}`); } pieces.push(`Added drive: ${formatRouteDistanceForRequest(delta.addedMiles, request)}`); if (delta.addedMinutes > 0) pieces.push(`Traffic-time change: about ${Math.ceil(delta.addedMinutes)} minutes`); return pieces.join("\n"); } function routeChangeConfirmationMessage(request, type, delta, additionalFare, totalFare, actionLabel, guidance = null, nextStops = []) { const baseFare = routeChangeBaseFare(request); const lines = []; if (type === "change_destination" && !routeChangeIsAfterPickup(request)) { if (delta.nextDistanceMiles != null) { lines.push(`New route from pickup: ${formatRouteDistanceForRequest(delta.nextDistanceMiles, request)}`); } const billableMinutes = destinationChangeBillableMinutes(guidance ?? { distanceMiles: delta.nextDistanceMiles, minutes: delta.nextMinutes }, nextStops); if (billableMinutes != null) { lines.push(`Fare pricing time: about ${Math.ceil(billableMinutes)} minutes`); } lines.push(`Current fare: ${formatMoney(baseFare, request.country)}`); lines.push(`New fare: ${formatMoney(totalFare, request.country)}`); if (additionalFare > 0) { lines.push(`Fare increases by ${formatMoney(additionalFare, request.country)}`); } else if (additionalFare < 0) { lines.push(`Fare decreases by ${formatMoney(Math.abs(additionalFare), request.country)}`); } else { lines.push("Fare stays the same."); } lines.push(""); lines.push("Passenger has not been picked up yet, so Waka reprices the trip from pickup to the new destination."); } else { lines.push(routeChangeDistanceLine(request, delta)); const elapsedMinutes = routeChangeElapsedRideMinutes(request); if (type === "change_destination" && elapsedMinutes > 0) { lines.push(`Elapsed ride time considered: about ${elapsedMinutes} minutes`); } lines.push(`Added fare: ${formatMoney(additionalFare, request.country)}`); lines.push(`New ride total: ${formatMoney(totalFare, request.country)}`); if (routeChangeNeedsExtraReview(delta)) { lines.push(""); lines.push("This looks like a large route change. Check the stop or destination address before applying."); } } const heading = actionLabel === "Apply" ? "Apply route update" : `${actionLabel} ${routeChangeVerb(type)}`; return `${heading}?\n\n${lines.join("\n")}`; } function routeChangeNeedsExtraReview(delta) { return delta.addedMiles >= routeChangeFareConfig.detourReviewMiles || delta.addedMinutes >= routeChangeFareConfig.detourReviewMinutes; } function routeChangeStopNeedsReselection(type, delta) { return type === "add_stop" && routeChangeNeedsExtraReview(delta) && delta.nextProvider !== "stored-route-fallback"; } function routeChangePatch(change, request = null) { const routeEstimate = change.routeEstimate ?? {}; return { destinationArea: routeChangeDestinationArea(request, change), destination: change.destination ?? request?.destination, destinationPlaceId: change.destinationPlaceId ?? request?.destinationPlaceId ?? null, destinationFormattedAddress: change.destinationFormattedAddress ?? request?.destinationFormattedAddress ?? null, destinationLatitude: change.destinationLatitude ?? request?.destinationLatitude ?? null, destinationLongitude: change.destinationLongitude ?? request?.destinationLongitude ?? null, rideStops: normalizeRideStops(change.rideStops ?? request?.rideStops), rideStopPoints: normalizeRideStopPoints(change.rideStopPoints ?? request?.rideStopPoints, change.rideStops ?? request?.rideStops), estimatedDistanceMiles: routeEstimate.distanceMiles ?? request?.estimatedDistanceMiles ?? null, estimatedTravelMinutes: routeEstimate.minutes ?? request?.estimatedTravelMinutes ?? null, routeEstimateSource: routeEstimate.source ?? request?.routeEstimateSource ?? null, routeEstimateProvider: routeEstimate.provider ?? request?.routeEstimateProvider ?? null, routeEstimateCached: Boolean(routeEstimate.cached ?? request?.routeEstimateCached), routeEstimateKey: routeEstimate.routeKey ?? request?.routeEstimateKey ?? null, routeEstimatePolyline: routeEstimate.routePolyline ?? request?.routeEstimatePolyline ?? null, routeEstimateDestinationFingerprint: routeEstimate.destinationFingerprint ?? request?.routeEstimateDestinationFingerprint ?? null, routeEstimateCreatedAt: routeEstimate.estimatedAt ?? request?.routeEstimateCreatedAt ?? null }; } function routeChangeDestinationPlace(change = {}) { return normalizedPlaceSelection({ placeId: change.destinationPlaceId, displayName: change.destination, formattedAddress: change.destinationFormattedAddress || change.destination, latitude: change.destinationLatitude, longitude: change.destinationLongitude }); } function routeChangeDestinationArea(request, change = {}) { if (!request) return change?.destinationArea ?? null; return change?.destinationArea ?? destinationAreaForPublish( request.country, request.city, request.destinationArea, change.destinationFormattedAddress || change.destination || request.destination, routeChangeDestinationPlace(change) ); } function applyAcceptedRouteChange(change, savedRequest = null) { updateRequestById(change.requestId, (request) => { const acceptedFare = Math.max( Number(request.acceptedRouteChangeFare ?? 0) || 0, Number(change.acceptedRouteChangeFare ?? 0) || 0, Number(savedRequest?.accepted_route_change_fare ?? savedRequest?.acceptedRouteChangeFare ?? 0) || 0 ); return { ...request, ...routeChangePatch(change, request), ...(savedRequest?.id === change.requestId ? mapRideRequestFromDatabase(savedRequest, new Map(), stateLookupIndexes().offerMap) : {}), acceptedRouteChangeFare: savedRequest?.accepted_route_change_fare ?? acceptedFare }; }); } async function routeChangeGuidanceForRoute(request, destination, stops, pickupGps, pickupDescription, destinationPlace = null) { const guidanceKey = routeGuidanceInputKey( request.country, request.city, request.pickupArea, request.destinationArea, pickupDescription, destination, stops, pickupGps, destinationPlace ); let guidance = cachedConfirmedFareGuidanceForKey(guidanceKey); if (guidance) return guidance; if (fareGuidanceInFlightKey === guidanceKey) { guidance = await waitForConfirmedFareGuidance(guidanceKey); if (guidance) return guidance; } guidance = await accurateFareGuidanceForRide( request.country, request.city, request.pickupArea, request.destinationArea, destination, pickupGps, stops, pickupDescription, destinationPlace ); if (routeGuidanceConfirmedForPublish(guidance)) { lastRouteFareGuidance = guidance; lastRouteFareGuidanceKey = guidanceKey; } return guidance; } function upsertRouteChangeRequest(change) { state.routeChangeRequests = upsertById(state.routeChangeRequests ?? [], change); } function mergeRouteChangeEventsFromChats(messages = state.chats) { const events = messages .map((message) => message.routeChangeEvent ?? parseRouteChangeEventText(message.body ?? message.text)?.event) .filter((event) => event?.kind === "route_change" && event.id && event.requestId) .sort((a, b) => new Date(a.decidedAt ?? a.requestedAt ?? 0) - new Date(b.decidedAt ?? b.requestedAt ?? 0)); events.forEach((event) => { const existing = (state.routeChangeRequests ?? []).find((change) => change.id === event.id); const request = state.requests.find((item) => item.id === event.requestId); const baseChange = { ...(existing ?? {}), id: event.id, requestId: event.requestId, type: event.type, country: event.country, destinationArea: event.destinationArea ?? existing?.destinationArea ?? request?.destinationArea ?? null, destination: event.destination, destinationPlaceId: event.destinationPlaceId ?? null, destinationFormattedAddress: event.destinationFormattedAddress ?? null, destinationLatitude: event.destinationLatitude ?? null, destinationLongitude: event.destinationLongitude ?? null, rideStops: normalizeRideStops(event.rideStops), rideStopPoints: normalizeRideStopPoints(event.rideStopPoints, event.rideStops), routeEstimate: event.routeEstimate ?? null, routeDelta: event.routeDelta ?? null, additionalFare: Number(event.additionalFare ?? 0) || 0, totalFare: Number(event.totalFare ?? 0) || 0, acceptedRouteChangeFare: Number(event.acceptedRouteChangeFare ?? 0) || 0, requestedAt: event.requestedAt ?? existing?.requestedAt ?? new Date().toISOString(), riderId: event.riderId ?? existing?.riderId ?? selectedRiderIdForRequest(request), passengerId: event.passengerId ?? existing?.passengerId ?? request?.passengerId ?? null }; if (event.action === "declined") { upsertRouteChangeRequest({ ...baseChange, status: "declined", decidedAt: event.decidedAt ?? new Date().toISOString() }); return; } if (event.action === "accepted") { const wasAccepted = existing?.status === "accepted"; upsertRouteChangeRequest({ ...baseChange, status: "accepted", decidedAt: event.decidedAt ?? new Date().toISOString() }); if (!wasAccepted) applyAcceptedRouteChange(baseChange); return; } upsertRouteChangeRequest({ ...baseChange, status: existing?.status ?? "pending" }); }); } async function acceptRideRouteChangeInSupabase(request, change) { if (!hasSupabaseRuntime()) return null; const routeEstimate = change.routeEstimate ?? {}; const body = { p_request_id: request.id, p_change_id: change.id, p_destination_area: routeChangeDestinationArea(request, change), p_destination: change.destination, p_destination_place_id: change.destinationPlaceId ?? null, p_destination_formatted_address: change.destinationFormattedAddress ?? null, p_destination_lat: change.destinationLatitude ?? null, p_destination_lng: change.destinationLongitude ?? null, p_ride_stops: normalizeRideStops(change.rideStops), p_ride_stop_points: normalizeRideStopPoints(change.rideStopPoints, change.rideStops), p_estimated_distance_miles: routeEstimate.distanceMiles ?? request.estimatedDistanceMiles ?? null, p_estimated_travel_minutes: routeEstimate.minutes ?? request.estimatedTravelMinutes ?? null, p_route_estimate_source: normalizedRouteEstimateSourceForDatabase(routeEstimate.source ?? request.routeEstimateSource), p_route_estimate_provider: normalizedRouteEstimateProviderForDatabase(routeEstimate.source ?? request.routeEstimateSource, routeEstimate.provider ?? request.routeEstimateProvider), p_route_estimate_cached: Boolean(routeEstimate.cached ?? request.routeEstimateCached), p_route_estimate_key: routeEstimate.routeKey ?? request.routeEstimateKey ?? null, p_route_estimate_polyline: routeEstimate.routePolyline ?? request.routeEstimatePolyline ?? null, p_route_estimate_destination_fingerprint: routeEstimate.destinationFingerprint ?? request.routeEstimateDestinationFingerprint ?? null, p_route_estimate_created_at: routeEstimate.estimatedAt ?? request.routeEstimateCreatedAt ?? null, p_additional_fare_xaf: Math.max(0, Math.ceil(Number(change.additionalFare ?? 0) || 0)) }; const row = await callSupabaseRpcResult( "rider_accept_ride_route_change", body, "Accepting the route change", optionalSupabaseRequestTimeoutMs ); const saved = Array.isArray(row) ? row[0] ?? null : row ?? null; if (saved?.id) { await processRideRequestPushDelivery(request.id, { eventTypes: ["route_change_accepted"] }); } return saved; } async function requestRideRouteChangeInSupabase(request, change) { if (!hasSupabaseRuntime()) return null; const body = { p_request_id: request.id, p_change_id: change.id, p_change_type: change.type, p_destination: change.destination ?? request.destination, p_destination_place_id: change.destinationPlaceId ?? null, p_destination_formatted_address: change.destinationFormattedAddress ?? null, p_destination_lat: change.destinationLatitude ?? null, p_destination_lng: change.destinationLongitude ?? null, p_ride_stops: normalizeRideStops(change.rideStops), p_ride_stop_points: normalizeRideStopPoints(change.rideStopPoints, change.rideStops), p_route_estimate: change.routeEstimate ?? null, p_additional_fare_xaf: Math.max(0, Math.ceil(Number(change.additionalFare ?? 0) || 0)), p_total_fare_xaf: Math.max(0, Math.ceil(Number(change.totalFare ?? 0) || 0)), p_change_payload: routeChangeEventPayload(change, "proposed", request) }; let row = null; try { row = await callSupabaseRpcResult( "passenger_request_ride_route_change", body, "Sending the route change request", optionalSupabaseRequestTimeoutMs ); } catch (error) { const fallbackChange = await requestRideRouteChangeViaDestinationUpdateFallback(request, change, error); if (fallbackChange?.id) return fallbackChange; throw error; } const saved = Array.isArray(row) ? row[0] ?? null : row ?? null; if (saved?.id) { await processRideRequestPushDelivery(request.id, { eventTypes: ["route_change_requested"] }); } return saved; } async function requestRideRouteChangeViaDestinationUpdateFallback(request, change, originalError) { if (!hasSupabaseRuntime() || !request?.id || !change?.id) return null; logClientWarning("Route-change RPC failed; trying destination-update fallback so the rider still receives an approval request.", originalError); try { await updateRideDestinationInSupabase( request, change.destination ?? request.destination, change.routeEstimate ?? null, normalizeRideStops(change.rideStops), routeChangeDestinationPlace(change), normalizeRideStopPoints(change.rideStopPoints, change.rideStops) ); const result = typeof selectRideRouteChangesForRequests === "function" ? await selectRideRouteChangesForRequests([request.id]) : { data: [] }; const pendingChanges = (result.data ?? []) .map(mapRideRouteChangeFromDatabase) .filter((item) => item.requestId === request.id && item.status === "pending") .sort((a, b) => new Date(b.requestedAt ?? b.createdAt ?? 0) - new Date(a.requestedAt ?? a.createdAt ?? 0)); const fallbackChange = pendingChanges[0] ?? null; if (fallbackChange?.id) { await processRideRequestPushDelivery(request.id, { eventTypes: ["route_change_requested"] }); return fallbackChange; } } catch (fallbackError) { logClientWarning("Destination-update route-change fallback failed.", fallbackError); } return null; } async function declineRideRouteChangeInSupabase(request, change) { if (!hasSupabaseRuntime()) return null; const row = await callSupabaseRpcResult( "rider_decline_ride_route_change", { p_request_id: request.id, p_change_id: change.id }, "Declining the route change", optionalSupabaseRequestTimeoutMs ); const saved = Array.isArray(row) ? row[0] ?? null : row ?? null; if (saved?.id) { await processRideRequestPushDelivery(request.id, { eventTypes: ["route_change_declined"] }); } return saved; } async function acceptRouteChangeRequest(changeId) { const change = (state.routeChangeRequests ?? []).find((item) => item.id === changeId); const request = state.requests.find((item) => item.id === change?.requestId); if (!change || !request || !riderIdentityMatches(selectedRiderIdForRequest(request)) || change.status !== "pending") return; let savedRequest = null; try { savedRequest = await acceptRideRouteChangeInSupabase(request, change); } catch (error) { translatedAlert("acceptRouteChangeFailed", { message: error.message }); return; } if (hasSupabaseRuntime() && !savedRequest) { translatedAlert("acceptRouteChangeSaveFailed"); return; } const acceptedRouteChangeFare = Math.max( Number(savedRequest?.accepted_route_change_fare ?? 0) || 0, (Number(request.acceptedRouteChangeFare ?? 0) || 0) + (Number(change.additionalFare ?? 0) || 0) ); const acceptedChange = { ...change, status: "accepted", decidedAt: new Date().toISOString(), acceptedRouteChangeFare }; upsertRouteChangeRequest(acceptedChange); applyAcceptedRouteChange(acceptedChange, savedRequest); const updatedRequest = state.requests.find((item) => item.id === request.id) ?? request; pushRouteChangeSystemEvent(acceptedChange, "accepted", updatedRequest); saveState(); renderAll(); if (updatedRequest.status === "in_progress" && typeof openRiderActiveRideNavigation === "function") { const navKey = riderActiveRideNavigationKey(updatedRequest, "rider-route-change-nav"); if (shouldOpenRiderActiveRideNavigation(updatedRequest, navKey)) openRiderActiveRideNavigation(updatedRequest, navKey); } void refreshMarketplace({ silent: true }); } async function declineRouteChangeRequest(changeId) { const change = (state.routeChangeRequests ?? []).find((item) => item.id === changeId); const request = state.requests.find((item) => item.id === change?.requestId); if (!change || !request || !riderIdentityMatches(selectedRiderIdForRequest(request)) || change.status !== "pending") return; let savedChange = null; try { savedChange = await declineRideRouteChangeInSupabase(request, change); } catch (error) { translatedAlert("declineRouteChangeFailed", { message: error.message }); return; } if (hasSupabaseRuntime() && !savedChange) { translatedAlert("declineRouteChangeSaveFailed"); return; } const declinedChange = { ...change, ...(savedChange ? mapRideRouteChangeFromDatabase(savedChange) : {}), status: "declined", decidedAt: new Date().toISOString() }; upsertRouteChangeRequest(declinedChange); pushRouteChangeSystemEvent(declinedChange, "declined", request); saveState(); renderAll(); void refreshMarketplace({ silent: true }); } async function submitDestinationUpdate(event, requestId) { event.preventDefault(); const form = event.currentTarget; const status = form.querySelector(".destination-update-status"); const input = form.querySelector(".destination-update-input"); const stopsInput = form.querySelector(".stops-update-input"); const routeChangeType = event.submitter?.dataset.routeChangeType === "stop" ? "add_stop" : "change_destination"; const request = state.requests.find((item) => item.id === requestId); if (!canUpdateRideDestination(request)) { status.textContent = "Route changes are closed for this ride."; return; } const currentStops = normalizeRideStops(request.rideStops); const currentStopPoints = normalizeRideStopPoints(request.rideStopPoints, currentStops); let nextDestination = request.destinationFormattedAddress || request.destination; let nextStops = currentStops; let nextStopPoints = currentStopPoints; const currentDestinationPlace = normalizedPlaceSelection({ placeId: request.destinationPlaceId, displayName: request.destination, formattedAddress: request.destinationFormattedAddress, latitude: request.destinationLatitude, longitude: request.destinationLongitude }); let destinationPlace = currentDestinationPlace; if (routeChangeType === "change_destination") { nextDestination = input.value.trim(); if (nextDestination.length < 3) { status.textContent = "Enter the new destination address before requesting the change."; return; } const destinationChanged = ![request.destination, request.destinationFormattedAddress] .some((value) => String(value ?? "").trim().toLowerCase() === nextDestination.toLowerCase()); if (!destinationChanged) { status.textContent = "Enter a destination that is different from the current one."; return; } destinationPlace = destinationPlaceMatchesInput(form.__destinationUpdatePlace, nextDestination) ? normalizedPlaceSelection(form.__destinationUpdatePlace) : null; } else { const addedStops = normalizeRideStops(stopsInput?.value ?? ""); if (!addedStops.length) { status.textContent = "Enter at least one stop address before requesting an added stop."; return; } nextStops = normalizeRideStops([...currentStops, ...addedStops]); if (nextStops.length === currentStops.length) { status.textContent = "That stop is already on this route."; return; } nextStopPoints = rideStopPointsForRoute(nextStops, currentStopPoints); } const fallbackGuidance = routeChangeType === "add_stop" ? routeChangeFallbackGuidance(request, nextStops) : routeChangeDestinationFallbackGuidance(request, nextDestination, nextStops, destinationPlace); let guidance = fallbackGuidance; try { let baselineGuidance = null; if (routeEstimatesEnabled()) { status.textContent = "Checking updated driving distance..."; const pickupGps = requestPickupGps(request); const accurateGuidance = await routeChangeGuidanceForRoute( request, nextDestination, nextStops, pickupGps, request.pickupDescription, destinationPlace ).catch((error) => { logClientWarning("Could not confirm updated route before pricing route change; using fallback guidance when available.", error); return null; }); guidance = requireConfirmedRouteChangeGuidance(accurateGuidance, "Updated route", fallbackGuidance, request); status.textContent = "Comparing route change against current route..."; const accurateBaselineGuidance = await routeChangeGuidanceForRoute( request, request.destinationFormattedAddress || request.destination, currentStops, pickupGps, request.pickupDescription, currentDestinationPlace ).catch((error) => { logClientWarning("Could not refresh current route baseline before pricing route change.", error); return null; }); baselineGuidance = routeChangeBaselineGuidance(request, accurateBaselineGuidance); } const delta = routeChangeDeltaSummary(request, guidance, nextStops, baselineGuidance); const additionalFare = routeChangeAdditionalFare(request, guidance, nextStops, routeChangeType, baselineGuidance); const totalFare = routeChangeTotalFare(request, additionalFare, guidance, nextStops, routeChangeType); const change = { id: makeId("routeChange"), requestId: request.id, type: routeChangeType, country: request.country, destinationArea: routeChangeDestinationArea(request, { destination: nextDestination, destinationPlaceId: guidance?.destinationPlaceId ?? destinationPlace?.placeId ?? null, destinationFormattedAddress: guidance?.destinationFormattedAddress ?? destinationPlace?.formattedAddress ?? null, destinationLatitude: guidance?.destinationLatitude ?? destinationPlace?.latitude ?? null, destinationLongitude: guidance?.destinationLongitude ?? destinationPlace?.longitude ?? null }), destination: nextDestination, destinationPlaceId: guidance?.destinationPlaceId ?? destinationPlace?.placeId ?? null, destinationFormattedAddress: guidance?.destinationFormattedAddress ?? destinationPlace?.formattedAddress ?? null, destinationLatitude: guidance?.destinationLatitude ?? destinationPlace?.latitude ?? null, destinationLongitude: guidance?.destinationLongitude ?? destinationPlace?.longitude ?? null, rideStops: nextStops, rideStopPoints: nextStopPoints, routeEstimate: guidance ? { distanceMiles: guidance.distanceMiles, minutes: guidance.minutes, source: guidance.source, provider: guidance.provider, cached: guidance.cached, routeKey: guidance.routeKey, routePolyline: guidance.routePolyline, destinationFingerprint: guidance.destinationFingerprint, estimatedAt: guidance.estimatedAt } : null, routeDelta: delta, additionalFare, totalFare, requestedAt: new Date().toISOString(), status: routeChangeNeedsRiderApproval(request) ? "pending" : "accepted", passengerId: request.passengerId, riderId: selectedRiderIdForRequest(request) }; if (routeChangeNeedsRiderApproval(request)) { const verbalNote = request.status === "in_progress" ? "\nPickup has already happened. Ask the rider verbally before sending this request; the rider still must accept it in the app." : ""; const ok = await showWakaGoodConfirm(`${routeChangeConfirmationMessage(request, routeChangeType, delta, additionalFare, change.totalFare, "Request to", guidance, nextStops)}${verbalNote}`); if (!ok) { status.textContent = "Route change was not sent."; return; } status.textContent = "Sending route change to rider..."; let savedChange = null; try { savedChange = await requestRideRouteChangeInSupabase(request, change); } catch (error) { status.textContent = `Route change failed: ${error.message}`; return; } if (hasSupabaseRuntime() && !savedChange) { status.textContent = "Route change failed: Waka could not save this request. Refresh and try again."; return; } if (savedChange) { Object.assign(change, mapRideRouteChangeFromDatabase(savedChange)); } upsertRouteChangeRequest(change); pushRouteChangeSystemEvent(change, "proposed", request); if (typeof addPassengerRideNotice === "function" && requestBelongsToPassenger(request)) { addPassengerRideNotice( routeChangeType === "add_stop" ? "Stop request sent" : "Destination change sent", `Waiting for rider approval. Added fare: ${formatMoney(additionalFare, request.country)}. New total if accepted: ${formatMoney(change.totalFare, request.country)}.`, request.id ); } if (typeof clearPassengerDestinationUpdateDraft === "function") clearPassengerDestinationUpdateDraft(request.id); status.textContent = "Sent to rider for approval."; saveState(); renderAll(); void refreshMarketplace({ silent: true }); return; } const nextFareOffer = Math.max(0, Number(change.totalFare ?? routeChangeEstimatedTotal(request, additionalFare))); if (request.status === "open" && nextFareOffer !== Number(request.fareOffer ?? 0) && !passengerCanSendFareProposal(request)) { status.textContent = fareProposalLimitMessage("passenger", request); return; } const ok = await showWakaGoodConfirm(routeChangeConfirmationMessage(request, routeChangeType, delta, additionalFare, change.totalFare, "Apply", guidance, nextStops)); if (!ok) { status.textContent = "Route change was not applied."; return; } status.textContent = "Updating route and fare..."; const saved = await updateRideDestinationInSupabase(request, nextDestination, guidance, nextStops, destinationPlace, nextStopPoints); let savedFare = null; if (request.status === "open" && nextFareOffer !== Number(request.fareOffer ?? 0)) { savedFare = await updateRideRequestFareInSupabase(request.id, nextFareOffer).catch((fareError) => { logClientWarning("Route change fare update did not sync to Supabase.", fareError); return null; }); } updateRequestById(request.id, (item) => ({ ...item, ...routeChangePatch(change, item), destination: saved?.destination ?? change.destination, destinationArea: saved?.destination_area ?? change.destinationArea ?? item.destinationArea, destinationPlaceId: saved?.destination_place_id ?? change.destinationPlaceId, destinationFormattedAddress: saved?.destination_formatted_address ?? change.destinationFormattedAddress, destinationLatitude: saved?.destination_lat ?? change.destinationLatitude, destinationLongitude: saved?.destination_lng ?? change.destinationLongitude, rideStops: normalizeRideStops(saved?.ride_stops ?? nextStops), rideStopPoints: normalizeRideStopPoints(saved?.ride_stop_points ?? nextStopPoints, saved?.ride_stops ?? nextStops), fareOffer: savedFare?.fareOffer ?? savedFare?.fare_offer_xaf ?? nextFareOffer, fareHistory: savedFare?.fareHistory ?? savedFare?.fare_history ?? (nextFareOffer !== Number(item.fareOffer ?? 0) ? fareProposalHistoryWithNextFare(item, item.fareOffer, nextFareOffer) : item.fareHistory) })); upsertRouteChangeRequest(change); const fareChangeText = additionalFare > 0 ? `Fare offer increased by ${formatMoney(additionalFare, request.country)} to ${formatMoney(nextFareOffer, request.country)}.` : additionalFare < 0 ? `Fare offer decreased by ${formatMoney(Math.abs(additionalFare), request.country)} to ${formatMoney(nextFareOffer, request.country)}.` : `Fare offer stays at ${formatMoney(nextFareOffer, request.country)}.`; pushSystemChat(request.id, `Passenger updated the route to ${requestDestinationText({ ...request, destination: nextDestination, rideStops: nextStops })}. ${fareChangeText}`); if (typeof addPassengerRideNotice === "function" && requestBelongsToPassenger(request)) { addPassengerRideNotice( routeChangeType === "add_stop" ? "Stop added" : "Destination updated", `Route updated. ${fareChangeText}`, request.id ); } if (typeof clearPassengerDestinationUpdateDraft === "function") clearPassengerDestinationUpdateDraft(request.id); saveState(); renderAll(); } catch (error) { status.textContent = `Route change failed: ${/edge function|non-2xx|failed|timeout|network|unavailable/i.test(String(error.message)) ? "route distance is temporarily unavailable. Try again in a moment." : error.message}`; } } function canCancelBeforeStart(request) { if (!request || !preStartCancellationStatuses.includes(request.status)) return false; if (activeRole() === "passenger") return requestBelongsToPassenger(request); return activeRole() === "rider" && riderIdentityMatches(selectedRiderIdForRequest(request)); } function canCancelInProgress(request) { return Boolean(request && request.status === "in_progress" && ((activeRole() === "passenger" && requestBelongsToPassenger(request)) || (activeRole() === "rider" && riderIdentityMatches(selectedRiderIdForRequest(request))))); } function canSeeRideLifecycleActions(request) { if (!request) return false; if (activeRole() === "passenger") return requestBelongsToPassenger(request); return activeRole() === "rider" && riderIdentityMatches(selectedRiderIdForRequest(request)); } function activeRideForRole(preferredRequest = selectedRequest()) { if (activeRole() === "rider" && state.rider) { const inProgressRide = riderInProgressImmediateRide(state.rider); if (inProgressRide) return inProgressRide; } if (canSeeRideLifecycleActions(preferredRequest)) return preferredRequest; if (activeRole() === "passenger" && state.passenger) { return state.requests.find((request) => requestBelongsToPassenger(request) && ["open", "matched", "arrived", "in_progress"].includes(request.status)) ?? null; } if (activeRole() === "rider" && state.rider) { return state.requests.find((request) => riderIdentityMatches(selectedRiderIdForRequest(request)) && ["matched", "arrived", "in_progress"].includes(request.status)) ?? null; } return null; } function rideStopProgressText(request) { const stops = normalizeRideStops(request?.rideStops); if (!stops.length) return rideFlowText("rideNoAddedStops", "No added stops."); const index = rideStopIndex(request); if (index >= stops.length) { return rideFlowText( "rideAllStopsMarked", `All ${stops.length} added stop${stops.length === 1 ? "" : "s"} marked. Continue to destination.`, { count: stops.length } ); } return rideFlowText( "rideNextStop", `Next stop ${index + 1} of ${stops.length}: ${stops[index]}.`, { current: index + 1, total: stops.length, stop: stops[index] } ); } function rideLifecycleActionSummary(request) { if (!request) return ""; if (request.status === "open") { return requestReopenedAfterRiderCancellation(request) ? rideFlowText("rideRequestReopenedAfterRiderCancel", "Rider cancelled. This request is open again.") : rideFlowText("rideCancelBeforeChoosingRider", "Cancel any time before choosing a rider."); } if (request.status === "matched") return activeRole() === "rider" ? rideFlowText("rideStepRiderMarkArrival", "Mark arrival when you are with the passenger. GPS will be recorded when available.") : rideFlowText("rideStepPassengerRiderOnWay", "Rider on the way. {fee}", { fee: cancellationFeeText(request) }).trim(); if (request.status === "arrived") return activeRole() === "rider" ? rideFlowText("rideStepRiderConfirmPickup", "Confirm pickup to start destination navigation.") : rideFlowText("rideStepPassengerRiderArrived", "Rider arrived. Meet the rider when ready. {fee}", { fee: cancellationFeeText(request) }).trim(); if (request.status === "in_progress") return activeRole() === "passenger" ? rideFlowText("rideStepPassengerRiderCompletesDropoff", "{progress} Rider completes at drop-off.", { progress: rideStopProgressText(request) }) : rideFlowText("rideStepRiderCompleteDestination", "{progress} Complete at destination. {fee}", { progress: rideStopProgressText(request), fee: cancellationFeeText(request) }).trim(); if (request.status === "completed") return rideFlowText("rideAlreadyComplete", "Ride is already marked complete. {summary}", { summary: rideFinancialSummary(request) }).trim(); if (request.status === "cancelled") return rideFlowText("rideCancelled", "Ride has been cancelled. {fee}", { fee: cancellationFeeText(request) }).trim(); return rideFlowText("rideActionsProgress", "Ride actions update as the trip progresses."); } function canTipRequest(request) { return Boolean(request && activeRole() === "passenger" && requestBelongsToPassenger(request) && request.status === "completed" && selectedRiderIdForRequest(request) && !passengerTipForRequest(request.id)); } function reportableRideForRole(preferredRequest = selectedRequest()) { if (canReportOnRequest(preferredRequest)) return preferredRequest; const actionRequest = activeRideForRole(preferredRequest); return canReportOnRequest(actionRequest) ? actionRequest : null; } function canReportOnRequest(request) { if (!request || !rideReportStatuses.includes(request.status)) return false; if (activeRole() === "passenger") return requestBelongsToPassenger(request); if (activeRole() === "rider") return riderIdentityMatches(selectedRiderIdForRequest(request)); return false; } function reportTargetForRequest(request) { if (!request) return { id: null, name: "Unknown account" }; if (activeRole() === "passenger") { return { id: selectedRiderIdForRequest(request), name: selectedRiderFirstNameForRequest(request) }; } return { id: request.passengerId, name: passengerFirstNameForRequest(request) }; } function activeRideContactForRequest(request) { if (!request || !canChatOnRequest(request)) return null; const fallbackName = activeRole() === "rider" ? passengerFirstNameForRequest(request) : selectedRiderFirstNameForRequest(request); return { name: firstNameOnly(request.contactName, fallbackName), profilePhotoPath: request.contactProfilePhotoPath ?? "", relayPhone: request.contactRelayPhone ?? "", relayStatus: request.contactRelayStatus ?? "relay_not_configured" }; } function contactRelayStatusText(contact) { if (contact?.relayStatus && contact.relayStatus !== "relay_not_configured") { return "WakaGood message tracking is active. Voice calls are disabled for this Cameroon ride."; } return "Chat is active. Calls are disabled so WakaGood admin can review ride communication if support is needed."; } function appendContactActions(container, request) { const contact = activeRideContactForRequest(request); if (!contact) return; const compactPassengerContact = activeRole() === "passenger"; const panel = document.createElement("div"); panel.className = "contact-actions"; panel.classList.toggle("compact-contact-actions", compactPassengerContact); const avatar = document.createElement("span"); avatar.className = "profile-avatar contact-profile-avatar"; avatar.textContent = contact.name.slice(0, 1).toUpperCase() || "W"; panel.append(avatar); ensureContactProfilePhotoUrl(contact, avatar); if (!compactPassengerContact) { const title = document.createElement("strong"); title.textContent = `Message ${contact.name} in WakaGood`; panel.append(title); const detail = document.createElement("small"); detail.textContent = contactRelayStatusText(contact); panel.append(detail); } const chatButton = document.createElement("button"); chatButton.type = "button"; chatButton.className = compactPassengerContact ? "secondary-action icon-only-action" : "secondary-action"; chatButton.setAttribute("aria-label", `Open WakaGood chat with ${contact.name}`); chatButton.textContent = compactPassengerContact ? "Chat" : "Open chat"; chatButton.addEventListener("click", () => { els.chatInput?.focus(); els.chatPanel?.scrollIntoView({ behavior: "smooth", block: "nearest" }); }); panel.append(chatButton); container.append(panel); } async function ensureContactProfilePhotoUrl(contact, avatar) { if (!contact?.profilePhotoPath || !avatar || !isSupabaseMode() || !supabaseClient) return; const cached = contactProfilePhotoUrlCache.get(contact.profilePhotoPath); if (cached) { avatar.innerHTML = ``; return; } try { const { data, error } = await supabaseClient.storage .from(appConfig.buckets.profilePhotos) .createSignedUrl(contact.profilePhotoPath, 600); if (error || !data?.signedUrl) throw error || new Error("Signed profile photo URL was not returned."); contactProfilePhotoUrlCache.set(contact.profilePhotoPath, data.signedUrl); avatar.innerHTML = ``; } catch (error) { logClientWarning("Matched contact profile photo could not be opened.", error); } } function ratingTargetForRequest(request) { if (!request) return null; if (activeRole() === "passenger") { const riderId = selectedRiderIdForRequest(request); return riderId ? { id: riderId, name: selectedRiderFirstNameForRequest(request) } : null; } if (activeRole() === "rider" && selectedRiderIdForRequest(request) === state.rider?.id) { return { id: request.passengerId, name: passengerFirstNameForRequest(request) }; } return null; } async function resolveRideRatingContext(request) { let resolvedRequest = request; let target = ratingTargetForRequest(resolvedRequest); if (hasSupabaseRuntime() && resolvedRequest?.status === "completed" && roleCanSeeRequest(resolvedRequest) && typeof loadMarketplaceFromSupabase === "function") { try { await loadMarketplaceFromSupabase({ includeAccountData: true }); resolvedRequest = stateLookupIndexes().requestMap.get(resolvedRequest.id) ?? resolvedRequest; target = ratingTargetForRequest(resolvedRequest); } catch (error) { logClientWarning("Could not refresh completed ride details before rating.", error); } } return { request: resolvedRequest, target }; } function existingRatingForRequest(request) { const reviewerId = activeRole() === "rider" ? state.rider?.id : state.passenger?.id; if (!request?.id || !reviewerId) return null; return rideRatingRecords().find((rating) => rating.requestId === request.id && rating.reviewerId === reviewerId) ?? null; } function canRateRequest(request) { return Boolean(request && request.status === "completed" && roleCanSeeRequest(request) && ratingTargetForRequest(request) && !existingRatingForRequest(request)); } function currentEligibleOfferContext() { const request = selectedRequest(); const rider = currentRiderRecord(); if (!request) { translatedAlert("selectRideRequestFirst"); return null; } if (!rider) { translatedAlert("createRiderFirst"); return null; } if (!hasSignedIn("rider")) { translatedAlert("riderSignInRequired"); return null; } if (rider.status !== "approved") { translatedAlert("riderApprovalRequired"); return null; } if (!isSubscriptionActive(rider)) { translatedAlert("riderAccessRequired"); return null; } if (!paymentAccountReady("rider", rider)) { translatedAlert("riderPaymentRequired"); return null; } if (typeof riderAvailabilityIsActivated === "function" && !riderAvailabilityIsActivated()) { if (els.offerRequestContext) els.offerRequestContext.textContent = riderAvailabilityRequiredText(); if (typeof setRiderWorkspacePage === "function") setRiderWorkspacePage("initialize"); return null; } if (!riderCurrentFreshGps(rider)) { translatedAlert("riderLiveGpsRequired"); return null; } if (!roleCanSeeRequest(request)) { const message = riderRequestEligibilityMessage(request, rider); if (els.offerRequestContext) els.offerRequestContext.textContent = message; void showWakaGoodAlert(message); return null; } const blockingRide = riderBlockingImmediateRide(rider); if (blockingRide && blockingRide.id !== request.id && !isScheduledRequest(request)) { translatedAlert("immediateOfferLocked"); return null; } if (request.status !== "open") { if (requestIsActiveForCurrentRider(request, rider)) { if (els.offerRequestContext) { const heldForCurrentRide = riderShouldHoldNextRideNavigation(request, rider); const nextStop = heldForCurrentRide ? "Queued after current ride" : request.status === "in_progress" ? `Destination: ${requestDestinationDisplayText(request)}` : `Pickup: ${requestPickupDisplayText(request)}`; els.offerRequestContext.textContent = `This ride is already matched to you. ${nextStop}.`; } const currentRide = riderInProgressImmediateRide(rider); focusRiderRequestView(currentRide && currentRide.id !== request.id ? currentRide : request, { refresh: false, replace: true }); return null; } translatedAlert("requestClosed"); return null; } return { request, rider }; } function closeOpenBargainingAfterRideMatch({ matchedRequestId, selectedOfferId, selectedRiderId }) { if (!matchedRequestId || !selectedOfferId || !selectedRiderId) return; const openRequestIds = new Set(state.requests .filter((request) => request.status === "open") .map((request) => request.id)); state.offers = state.offers.filter((offer) => { if (offer.id === selectedOfferId) return true; if (offer.requestId === matchedRequestId) return false; if (offer.riderId === selectedRiderId && openRequestIds.has(offer.requestId)) return false; return true; }); } const marketplaceActionInFlightKeys = new Set(); function showMarketplaceActionBusyMessage(message) { if (activeRole() === "rider" && els.offerRequestContext) { els.offerRequestContext.textContent = message; return; } if (els.selectedSummary) els.selectedSummary.textContent = message; } async function runMarketplaceActionOnce(actionKey, busyMessage, action) { if (!actionKey) return action(); if (marketplaceActionInFlightKeys.has(actionKey)) { showMarketplaceActionBusyMessage(busyMessage); return null; } marketplaceActionInFlightKeys.add(actionKey); try { return await action(); } finally { marketplaceActionInFlightKeys.delete(actionKey); } } function negotiatedFareAcceptanceSummary({ request, fare, offeredByLabel, acceptingRole }) { const amount = formatMoney(fare, request?.country); const pickup = requestPickupDisplayText(request); const destination = requestDestinationDisplayText(request); const schedule = isScheduledRequest(request) ? formatDateTime(request.scheduledAt) : "Immediate ride"; const roleCopy = acceptingRole === "rider" ? "This will match you to this passenger." : "This will match you with this rider."; const otherRiderCopy = "After matching, this request will no longer be available to other riders."; return [ "Confirm negotiated fare", "", `Fare to accept: ${amount}`, `Offer from: ${offeredByLabel || "Waka user"}`, `Pickup: ${pickup}`, `Destination: ${destination}`, `Timing: ${schedule}`, "", roleCopy, otherRiderCopy, "", "OK: accept this fare and match the ride.", "Cancel: continue negotiations." ].join("\n"); } async function confirmNegotiatedFareAcceptance(details) { if (!requestIsNegotiableFare(details?.request)) return true; return await showWakaGoodConfirm(negotiatedFareAcceptanceSummary(details)); } function normalizedOfferFareHistoryEntries(offer) { const rows = Array.isArray(offer?.fareHistory) ? offer.fareHistory : []; return rows .map((entry) => ({ fare: Number(entry?.fare ?? entry?.amount ?? entry?.fare_xaf ?? entry), createdAt: entry?.createdAt ?? entry?.created_at ?? offer?.createdAt ?? null })) .filter((entry) => Number.isFinite(entry.fare) && entry.fare > 0) .sort((a, b) => new Date(a.createdAt ?? 0).getTime() - new Date(b.createdAt ?? 0).getTime()); } function offerWithUpdatedFareHistory(savedOffer, previousOffer, fare) { const history = normalizedOfferFareHistoryEntries(previousOffer); const previousFare = Number(previousOffer?.fare); if (Number.isFinite(previousFare) && previousFare > 0) { history.push({ fare: previousFare, createdAt: previousOffer?.createdAt ?? null }); } const nextFare = Number(fare ?? savedOffer?.fare); if (Number.isFinite(nextFare) && nextFare > 0) { history.push({ fare: nextFare, createdAt: savedOffer?.createdAt ?? new Date().toISOString() }); } const deduped = history.reduce((trail, entry) => { const previous = trail[trail.length - 1]; if (!previous || Number(previous.fare) !== Number(entry.fare)) trail.push(entry); return trail; }, []); return { ...savedOffer, fareHistory: deduped }; } function fareProposalTrail(source, currentFare, currentCreatedAt = null) { const trail = typeof fareHistoryTrail === "function" ? fareHistoryTrail(source, currentFare, currentCreatedAt) : []; if (trail.length) return trail; const fare = Number(currentFare); return Number.isFinite(fare) && fare > 0 ? [{ fare, createdAt: currentCreatedAt ?? source?.createdAt ?? null }] : []; } function fareProposalAttemptCount(source, currentFare, currentCreatedAt = null) { return fareProposalTrail(source, currentFare, currentCreatedAt).length; } function passengerFareProposalAttemptCount(request) { return fareProposalAttemptCount(request, request?.fareOffer, request?.createdAt); } function riderFareProposalAttemptCount(request, rider = currentRiderRecord()) { const offer = state.offers.find((item) => item.requestId === request?.id && riderIdentityMatches(item.riderId, rider)); return offer ? fareProposalAttemptCount(offer, offer.fare, offer.createdAt) : 0; } function fareProposalAttemptsRemaining(count) { return Math.max(0, fareProposalAttemptLimit - Number(count || 0)); } function passengerCanSendFareProposal(request) { return passengerFareProposalAttemptCount(request) < fareProposalAttemptLimit; } function riderCanSendFareProposal(request, rider = currentRiderRecord()) { return riderFareProposalAttemptCount(request, rider) < fareProposalAttemptLimit; } function fareProposalLimitMessage(role, request) { const actor = role === "rider" ? "Rider" : "Passenger"; const fare = role === "rider" ? state.offers.find((item) => item.requestId === request?.id && riderIdentityMatches(item.riderId))?.fare : request?.fareOffer; const fareText = Number.isFinite(Number(fare)) && Number(fare) > 0 ? ` Current fare on the table: ${formatMoney(fare, request?.country)}.` : ""; return `${actor} fare proposals are limited to ${fareProposalAttemptLimit} attempts for this request.${fareText} Accept or decline instead of sending another fare.`; } function fareProposalHistoryWithNextFare(source, currentFare, nextFare) { const history = fareProposalTrail(source, currentFare, source?.updatedAt ?? source?.createdAt ?? null); const proposedFare = Number(nextFare); if (Number.isFinite(proposedFare) && proposedFare > 0) { const previous = history[history.length - 1]; if (!previous || Number(previous.fare) !== proposedFare) { history.push({ fare: proposedFare, createdAt: new Date().toISOString() }); } } return history; } function riderRequestEligibilityMessage(request, rider = currentRiderRecord()) { if (!request) return translatedMessage("selectRideRequestFirst"); if (!rider) return translatedMessage("createRiderFirst"); if (!requestMatchesRiderVehicle(request, rider)) { return "This request needs a different approved vehicle type. Check whether the passenger requested bike, normal car, or XL/Special."; } if (typeof requestDestinationMatchesDailyRegions === "function" && !requestDestinationMatchesDailyRegions(request, rider)) { return "This request is outside the destination areas selected for your current work session."; } if (typeof riderCanReviewAnotherImmediateRequest === "function" && !riderCanReviewAnotherImmediateRequest(request, rider)) { return translatedMessage("immediateOfferLocked"); } const gpsDistanceKm = gpsDistanceKmForRequest(request, rider); if (gpsDistanceKm != null && !riderWithinGpsProximity(request, rider)) { return "This request is outside your current live GPS service radius."; } if (gpsDistanceKm == null && !riderWithinRequestProximity(request, rider)) { return "This request is outside your current operating area. Update your live area or GPS, then refresh the marketplace."; } return translatedMessage("selectNearbyRequest"); } function riderOfferSaveErrorMessage(error) { const message = String(error?.message || error || ""); if (/does not match the rider country, city, or vehicle/i.test(message)) { return "The server rejected this offer because the saved rider/request matching rules are still using strict city matching. Refresh the rider page and try again after the latest update is applied."; } return message; } async function saveRiderOffer({ request, rider, fare, type }) { const note = ""; if (note.length > riderOfferNoteMaxLength) { els.offerRequestContext.textContent = `Keep rider notes to ${riderOfferNoteMaxLength} characters or less.`; return; } if (type === "counter" && !riderCanSendFareProposal(request, rider)) { if (els.offerRequestContext) els.offerRequestContext.textContent = fareProposalLimitMessage("rider", request); return; } const xlFloor = xlSpecialFareFloorForRequest(request); if (xlFloor != null && Number(fare) <= xlFloor) { els.offerRequestContext.textContent = `XL/Special offers must be above ${formatMoney(xlFloor, request.country)} for this route.`; return; } const existing = state.offers.find((offer) => offer.requestId === request.id && offer.riderId === rider.id); const minimumNextFare = Number(request.fareOffer ?? 0); if (type === "counter" && Number(fare) <= minimumNextFare) { els.offerRequestContext.textContent = `Enter any whole-number counter-offer higher than ${formatMoney(minimumNextFare, request.country)}.`; return; } const offer = { id: existing?.id ?? makeId("offer"), requestId: request.id, riderId: rider.id, fare, type, note, ...offerPickupDistanceSnapshot(request, rider), createdAt: new Date().toISOString() }; try { if (paymentSetupRelaxedForTesting()) { await ensureStagingPaymentAccountForTesting("rider", rider, { localFallback: false }); } const savedOffer = await saveOfferToSupabase(offer); const savedOfferWithHistory = offerWithUpdatedFareHistory(savedOffer, existing, fare); state.offers = state.offers.filter((item) => item.id !== offer.id && item.id !== savedOfferWithHistory.id); state.offers.unshift(savedOfferWithHistory); if (type === "accepted") { state.requests = state.requests.map((item) => { if (item.id !== request.id) return item; return preserveRideRequestPickup({ ...item, status: "matched", selectedOfferId: savedOfferWithHistory.id, agreedFare: savedOfferWithHistory.fare, selectedRiderId: savedOfferWithHistory.riderId, selectedRiderName: firstNameOnly(rider?.name, "Rider"), riderConfirmationStatus: isScheduledRequest(item) ? "not_requested" : null, riderConfirmationRequestedAt: null, riderConfirmedAt: null, releasedAt: null, matchedAt: new Date().toISOString() }, item); }); const currentRide = riderInProgressImmediateRide(rider); state.selectedRequestId = currentRide && currentRide.id !== request.id ? currentRide.id : request.id; pushSystemChat( request.id, currentRide && currentRide.id !== request.id ? `Rider accepted the passenger fare of ${formatMoney(savedOfferWithHistory.fare)}. This next pickup is queued until the current ride is completed.` : `Rider accepted the passenger fare of ${formatMoney(savedOfferWithHistory.fare)}. Ride matched automatically.` ); closeOpenBargainingAfterRideMatch({ matchedRequestId: request.id, selectedOfferId: savedOfferWithHistory.id, selectedRiderId: savedOfferWithHistory.riderId }); } els.offerForm.reset(); const shouldReturnToMarketplace = type === "counter" && activeRole() === "rider" && typeof returnRiderToMarketplace === "function"; saveState(); if (shouldReturnToMarketplace) returnRiderToMarketplace({ replace: true, refresh: true }); else renderAll(); if (type === "accepted") { if (els.offerRequestContext) els.offerRequestContext.textContent = "Ride accepted. Opening pickup navigation when the matched ride is ready..."; const matchedRequest = preserveRideRequestPickup(state.requests.find((item) => item.id === request.id) ?? request, request); if (typeof riderPickupNavigationShouldWaitForDropoff === "function" && riderPickupNavigationShouldWaitForDropoff(matchedRequest, rider)) { if (els.offerRequestContext) { els.offerRequestContext.textContent = "Next pickup queued. Finish the current drop-off before Waka opens navigation to the new pickup."; } } else { const navKey = `rider-pickup-nav-${matchedRequest.id}-${matchedRequest.matchedAt || savedOfferWithHistory.id}`; if (typeof openRiderPickupNavigationAfterFareAccepted === "function") { void openRiderPickupNavigationAfterFareAccepted(matchedRequest, navKey).then((opened) => { if (!opened && els.offerRequestContext) { els.offerRequestContext.textContent = "Ride accepted. Use Navigate on the ride card if your browser blocked automatic navigation."; } }); } else if (typeof openRiderPickupNavigationWhenPrecise === "function") { void openRiderPickupNavigationWhenPrecise(matchedRequest, navKey).then((opened) => { if (!opened && els.offerRequestContext && requestHasPrecisePickupNavigation(matchedRequest)) { els.offerRequestContext.textContent = "Ride accepted. Use Navigate on the ride card if your browser blocked automatic navigation."; } }); } else if (typeof shouldOpenRiderPickupNavigation === "function" && requestHasPrecisePickupNavigation(matchedRequest) && shouldOpenRiderPickupNavigation(matchedRequest, navKey)) { openRiderPickupNavigation(matchedRequest, navKey); } } } void processRideRequestPushDelivery(request.id, { eventTypes: [type === "accepted" ? "ride_matched" : "rider_counter_offer"] }); if (typeof forceMarketplaceRefreshSoon === "function") { forceMarketplaceRefreshSoon(type === "accepted" ? "rider_accept_after_offer" : "rider_counter_after_offer"); } else if (!shouldReturnToMarketplace) { void refreshMarketplace({ silent: true }); } if (type === "accepted" && activeRole() === "rider") { window.setTimeout(() => { document.querySelector("#rideActionPanel, .rider-ride-tool-shell, #requestsBoard, #marketPanel") ?.scrollIntoView({ behavior: "smooth", block: "start" }); }, 120); } } catch (error) { translatedAlert("offerSendFailed", { message: riderOfferSaveErrorMessage(error) }); } } async function acceptPassengerFare() { const requestId = selectedRequest()?.id ?? state.selectedRequestId ?? "unknown-request"; const riderId = currentRiderRecord()?.id ?? state.rider?.id ?? "unknown-rider"; await runMarketplaceActionOnce(`rider-accept:${requestId}:${riderId}`, "Acceptance is already being confirmed. Please wait a moment.", async () => { if (hasSupabaseRuntime()) await refreshMarketplace({ silent: true, reason: "rider_accept_precheck" }); const context = currentEligibleOfferContext(); if (!context) return; if (!await confirmNegotiatedFareAcceptance({ request: context.request, fare: context.request.fareOffer, offeredByLabel: passengerFirstNameForRequest(context.request), acceptingRole: "rider" })) { if (els.offerRequestContext) els.offerRequestContext.textContent = "Acceptance cancelled. Continue negotiating or send a counter-offer."; return; } await saveRiderOffer({ ...context, fare: context.request.fareOffer, type: "accepted" }); }); } function nextRiderRequestAfterLeaving(requestId) { return visibleRequestsForRole().find((request) => request.id !== requestId) ?? null; } function locallyDismissRiderMarketplaceRequest(request, rider = currentRiderRecord()) { if (!request?.id || activeRole() !== "rider") return null; if (typeof rememberRiderDismissedRequest === "function") rememberRiderDismissedRequest(request, rider); state.offers = state.offers.filter((offer) => !(offer.requestId === request.id && offer.riderId === rider?.id)); const nextRequest = nextRiderRequestAfterLeaving(request.id); if (state.selectedRequestId === request.id) state.selectedRequestId = null; if (typeof updateRiderWorkspaceRoute === "function") { updateRiderWorkspaceRoute("requests", { replace: true, requestId: "" }); } saveState(); renderAll(); return nextRequest; } async function dropRiderNegotiation() { const request = selectedRequest(); const rider = currentRiderRecord(); if (!request || activeRole() !== "rider") { translatedAlert("selectRideRequestFirst"); return; } const ownOffer = state.offers.find((offer) => offer.requestId === request.id && offer.riderId === rider?.id); const nextRequest = locallyDismissRiderMarketplaceRequest(request, rider); els.offerForm?.reset(); if (els.offerRequestContext) { els.offerRequestContext.textContent = nextRequest ? "Request declined. Review the marketplace when ready." : ownOffer ? "Request declined and your open rider offer is being withdrawn. Select another request when one appears." : "Request declined. Select another request when one appears."; } if (request.status === "open") { void withdrawRiderOfferFromSupabase(request.id) .then((updatedRequest) => { if (updatedRequest?.id) state.requests = upsertById(state.requests, updatedRequest); void refreshMarketplace({ silent: true, reason: "rider_left_negotiation" }); }) .catch((error) => { const staleRequest = /no longer open|no longer available|not found|cancelled/i.test(error.message || ""); if (staleRequest) state.requests = state.requests.filter((item) => item.id !== request.id); else logClientWarning("Rider decline could not be synced immediately; local marketplace was updated.", error); void refreshMarketplace({ silent: true, reason: "rider_left_negotiation_sync_failed" }); }); } else { void refreshMarketplace({ silent: true, reason: "rider_left_negotiation" }); } } async function ignoreRiderMarketplaceRequest(requestId) { const request = stateLookupIndexes().requestMap.get(requestId) ?? null; if (!request || activeRole() !== "rider") return; const rider = currentRiderRecord(); locallyDismissRiderMarketplaceRequest(request, rider); if (request.status === "open") { void withdrawRiderOfferFromSupabase(request.id) .then((updatedRequest) => { if (updatedRequest?.id) state.requests = upsertById(state.requests, updatedRequest); void refreshMarketplace({ silent: true, reason: "rider_declined_request" }); }) .catch((error) => { const staleRequest = /no longer open|no longer available|not found|cancelled/i.test(error.message || ""); if (staleRequest) state.requests = state.requests.filter((item) => item.id !== request.id); else logClientWarning("Rider declined a request locally, but backend sync was delayed.", error); void refreshMarketplace({ silent: true, reason: "rider_declined_request_sync_failed" }); }); } else { void refreshMarketplace({ silent: true, reason: "rider_declined_request" }); } } async function sendOffer(event) { event.preventDefault(); if (hasSupabaseRuntime()) await refreshMarketplace({ silent: true, reason: "rider_offer_precheck" }); const context = currentEligibleOfferContext(); if (!context) return; if (requestIsNonNegotiableFare(context.request)) { els.offerRequestContext.textContent = "This passenger chose a non-negotiable fare. Accept the fare or decline the request."; return; } const typedFare = String(els.counterFare?.value ?? "").trim(); const typedNoteFare = String(els.counterNote?.value ?? "").trim(); const fareSource = typedFare || (/^\$?\d+(\.\d{1,2})?$/.test(typedNoteFare) ? typedNoteFare : ""); const requestedFare = Number(String(fareSource).replace(/[^\d.]/g, "")); if (!requestedFare) { els.offerRequestContext.textContent = "Enter your counter-offer fare, or use Accept passenger fare."; if (els.counterFare && !els.counterFare.disabled) els.counterFare.focus(); return; } if (!Number.isInteger(requestedFare)) { els.offerRequestContext.textContent = "Use whole-number counter-offers. Enter a higher whole amount or cancel if the fare does not work."; return; } if (requestedFare === context.request.fareOffer) { els.offerRequestContext.textContent = "That matches the passenger fare. Use Accept passenger fare, or enter a different counter-offer."; return; } const minimumNextFare = Number(context.request.fareOffer ?? 0); if (requestedFare <= minimumNextFare) { els.offerRequestContext.textContent = `Enter any whole-number counter-offer higher than ${formatMoney(minimumNextFare, context.request.country)}.`; return; } const xlFloor = xlSpecialFareFloorForRequest(context.request); if (xlFloor != null && requestedFare <= xlFloor) { els.offerRequestContext.textContent = `XL/Special counter-offers must be above ${formatMoney(xlFloor, context.request.country)}.`; return; } if (!riderCanSendFareProposal(context.request, context.rider)) { els.offerRequestContext.textContent = fareProposalLimitMessage("rider", context.request); return; } await runMarketplaceActionOnce(`rider-counter:${context.request.id}:${context.rider.id}`, "Counter-offer is already being sent. Please wait a moment.", async () => { await saveRiderOffer({ ...context, fare: requestedFare, type: "counter" }); }); } async function chooseOffer(offerId) { const offer = state.offers.find((item) => item.id === offerId); if (!offer) return; const requestToMatch = state.requests.find((request) => request.id === offer.requestId); if (activeRole() !== "passenger" || !requestBelongsToPassenger(requestToMatch)) { translatedAlert("passengerOwnRequestRequired"); return; } if (offerIsExpired(offer, requestToMatch)) { translatedAlert("riderOfferExpired"); renderAll(); return; } const rider = state.riders.find((item) => item.id === offer.riderId); let passengerConfirmedAcceptance = false; try { await runMarketplaceActionOnce(`passenger-select:${offer.requestId}`, "Fare acceptance is already being confirmed. Please wait a moment.", async () => { if (hasSupabaseRuntime()) await refreshMarketplace({ silent: true, reason: "passenger_accept_offer_precheck" }); const freshOffer = state.offers.find((item) => item.id === offer.id) ?? offer; const freshRequest = state.requests.find((request) => request.id === freshOffer.requestId) ?? requestToMatch; const freshRider = state.riders.find((item) => item.id === freshOffer.riderId) ?? rider; if (freshRequest?.status !== "open") { translatedAlert("requestClosed"); renderAll(); return; } if (offerIsExpired(freshOffer, freshRequest)) { translatedAlert("riderOfferExpired"); renderAll(); return; } if (!await confirmNegotiatedFareAcceptance({ request: freshRequest, fare: freshOffer.fare, offeredByLabel: firstNameOnly(freshRider?.name, "Rider"), acceptingRole: "passenger" })) { if (els.selectedSummary) els.selectedSummary.textContent = "Acceptance cancelled. Continue negotiating or review other rider offers."; return; } passengerConfirmedAcceptance = true; passengerInitiatedRideMatchRequestIds.add(freshOffer.requestId); let savedRequest = null; try { savedRequest = await chooseOfferInSupabase(freshRequest, freshOffer); } catch (error) { translatedAlert("chooseRiderFailed", { message: error.message }); return; } state.requests = state.requests.map((request) => { if (request.id !== freshOffer.requestId) return request; const serverState = savedRequest?.id === request.id ? savedRequest : {}; return preserveRideRequestPickup({ ...request, ...serverState, status: "matched", selectedOfferId: freshOffer.id, agreedFare: freshOffer.fare, selectedRiderId: freshOffer.riderId, selectedRiderName: firstNameOnly(freshRider?.name, "Rider"), riderConfirmationStatus: isScheduledRequest(request) ? "not_requested" : null, riderConfirmationRequestedAt: null, riderConfirmedAt: null, releasedAt: null, matchedAt: new Date().toISOString() }, request); }); closeOpenBargainingAfterRideMatch({ matchedRequestId: freshOffer.requestId, selectedOfferId: freshOffer.id, selectedRiderId: freshOffer.riderId }); state.selectedRequestId = freshOffer.requestId; state.passengerPage = "trips"; if (typeof updatePassengerWorkspaceRoute === "function") { updatePassengerWorkspaceRoute("trips", { replace: true, requestId: freshOffer.requestId, preferPathRoute: true }); } const systemMessage = { id: makeId("chat"), requestId: freshOffer.requestId, sender: "system", text: isScheduledRequest(freshRequest) ? `Scheduled ride matched at ${formatMoney(freshOffer.fare)} for ${formatDateTime(freshRequest.scheduledAt)}. Passenger can request confirmation before travel.` : `Ride matched at ${formatMoney(freshOffer.fare)}. Confirm pickup and ${paymentLabel(freshRequest.paymentPreference).toLowerCase()} before the ride starts.`, createdAt: new Date().toISOString() }; state.chats.push(systemMessage); void saveChatMessageToSupabase(systemMessage); saveState(); renderAll(); void processRideRequestPushDelivery(freshOffer.requestId, { eventTypes: ["ride_matched"] }); void recordInsuranceTelemetryTransitionInSupabase( { ...freshRequest, id: freshOffer.requestId, selectedOfferId: freshOffer.id, selectedRiderId: freshOffer.riderId }, "match" ); if (typeof forcePassengerApproachRefreshNow === "function") forcePassengerApproachRefreshNow("passenger_choose_offer"); if (typeof forceMarketplaceRefreshSoon === "function") forceMarketplaceRefreshSoon("passenger_choose_offer"); else void refreshMarketplace({ silent: true }); }); } finally { if (passengerConfirmedAcceptance) { window.setTimeout(() => passengerInitiatedRideMatchRequestIds.delete(offer.requestId), 90000); } } } function pushSystemChat(requestId, text) { const message = { id: makeId("chat"), requestId, sender: "system", text, createdAt: new Date().toISOString() }; state.chats.push(message); void saveChatMessageToSupabase(message); } function updateRequestById(requestId, updater) { state.requests = state.requests.map((request) => request.id === requestId ? updater(request) : request); } async function changeRideStateInSupabase(requestId, actionName, reason = "", gpsPoint = null) { if (!hasSupabaseRuntime()) return; const point = normalizeGpsPoint(gpsPoint); const gpsBody = { request_id: requestId, action_name: actionName, reason, p_lat: point?.latitude ?? null, p_lng: point?.longitude ?? null, p_accuracy_meters: point?.accuracyMeters ?? null, p_captured_at: point?.capturedAt ?? null, p_actor_role: activeRole() === "rider" ? "rider" : activeRole() === "passenger" ? "passenger" : null }; const legacyBody = { request_id: requestId, action_name: actionName, reason }; try { const data = await callSupabaseRpcResult( "change_ride_state", gpsBody, "Updating the ride status", rideLifecycleSupabaseTimeoutMs ); lastRideLifecycleSource = "ride lifecycle RPC"; const row = Array.isArray(data) ? data[0] ?? null : data; return row?.id ? mapRideRequestFromDatabase(row, new Map(), stateLookupIndexes().offerMap) : null; } catch (error) { const message = error.message ?? String(error); const missingFunction = /schema cache|could not find the function|function .*change_ride_state.*does not exist|pgrst202|404/i.test(message); if (missingFunction && point) { try { const data = await callSupabaseRpcResult( "change_ride_state", legacyBody, "Updating the ride status", rideLifecycleSupabaseTimeoutMs ); lastRideLifecycleSource = "ride lifecycle RPC"; const row = Array.isArray(data) ? data[0] ?? null : data; return row?.id ? mapRideRequestFromDatabase(row, new Map(), stateLookupIndexes().offerMap) : null; } catch (legacyError) { error = legacyError; } } if (missingFunction) rideLifecycleRpcUnavailable = true; throw new Error(missingFunction ? "Ride lifecycle is not installed in Supabase yet. Run supabase-ride-lifecycle.sql, then retry." : error.message); } } function lifecyclePointForRequest(request, actionName) { if (actionName === "arrive") return requestPickupGpsForMatching(request); if (actionName === "stop") return rideStopPointAt(request, rideStopIndex(request)); if (actionName === "complete" && request?.destinationLatitude != null && request?.destinationLongitude != null) { return { latitude: request.destinationLatitude, longitude: request.destinationLongitude, accuracyMeters: null, capturedAt: request.routeEstimateCreatedAt ?? request.createdAt }; } return null; } function lifecycleCanUseAddressOnlyPickupArrival(request) { const pickupText = String(request?.pickupDescription ?? request?.pickupFormattedAddress ?? "").trim(); return Boolean(pickupText && !pickupUsesCurrentLocationText(pickupText) && !pickupUsesGpsFallbackText(pickupText)); } function lifecycleCanUseGpsEvidenceOnlyCompletion(request) { const destinationText = String(request?.destinationFormattedAddress ?? request?.destination ?? request?.destinationArea ?? "").trim(); return Boolean(destinationText); } async function validatedLifecycleGpsPoint(request, actionName) { if (!["arrive", "stop", "complete"].includes(actionName)) return null; if (typeof getCurrentGpsPoint !== "function") { if (typeof logClientWarning === "function") { logClientWarning("Ride stage continued without phone GPS support.", { actionName, requestId: request?.id }); } return null; } const target = lifecyclePointForRequest(request, actionName); if (actionName === "stop" && !target) return null; const addressOnlyPickupArrival = actionName === "arrive" && !target && lifecycleCanUseAddressOnlyPickupArrival(request); const gpsEvidenceOnlyCompletion = actionName === "complete" && !target && lifecycleCanUseGpsEvidenceOnlyCompletion(request); if (!target && !addressOnlyPickupArrival && !gpsEvidenceOnlyCompletion) { if (typeof logClientWarning === "function") { logClientWarning("Ride stage continued with address-only ride details.", { actionName, requestId: request?.id }); } return null; } const accuracyLimit = lifecycleGpsAccuracyLimitMeters(actionName); let current = null; try { const gpsCapture = typeof getBestCurrentGpsPoint === "function" ? getBestCurrentGpsPoint({ desiredAccuracyMeters: accuracyLimit, samples: 4, totalTimeoutMs: 22000, sampleTimeoutMs: 8000 }) : getCurrentGpsPoint({ maximumAge: 0, timeout: 15000, enableHighAccuracy: true }); current = normalizeGpsPoint(await gpsCapture); } catch (error) { if (typeof logClientWarning === "function") { logClientWarning("Ride stage continued after GPS capture failed.", error); } return null; } if (!current) { if (typeof logClientWarning === "function") { logClientWarning("Ride stage continued after GPS returned no usable point.", { actionName, requestId: request?.id }); } return null; } if (current.accuracyMeters == null || current.accuracyMeters > accuracyLimit) { if (typeof logClientWarning === "function") { logClientWarning("Ride stage continued with low-accuracy GPS evidence.", { actionName, requestId: request?.id, accuracyMeters: current.accuracyMeters ?? null, accuracyLimit }); } return current; } if (addressOnlyPickupArrival || gpsEvidenceOnlyCompletion) return current; const distanceMeters = (gpsDistanceKmBetween(current, target) ?? Number.POSITIVE_INFINITY) * 1000; const allowedMeters = lifecycleDistanceLimitMeters(actionName); if (distanceMeters > allowedMeters) { if (typeof logClientWarning === "function") { logClientWarning("Ride stage continued even though GPS evidence was outside the mapped point.", { actionName, requestId: request?.id, distanceMeters: Math.round(distanceMeters), allowedMeters }); } return current; } return current; } function rideLifecycleMessage(request, actionName, actorRole = activeRole()) { const cancellationFee = actionName === "cancel" ? rideCancellationCompensationEstimate(request, Date.now(), actorRole) : null; if (actionName === "cancel" && request?.status === "in_progress") { if (actorRole === "rider") { return "Rider cancelled the ride after pickup. Passenger will not be charged because the rider cancelled."; } const actor = "Passenger cancelled"; return `${actor} the ride after pickup.${cancellationFee?.amount > 0 ? ` Partial rider compensation pending: ${formatMoney(cancellationFee.amount, request.country)} for about ${cancellationFee.elapsedMinutes} minute${cancellationFee.elapsedMinutes === 1 ? "" : "s"} after pickup.` : ""}`; } return { arrive: `${selectedRiderFirstNameForRequest(request)} marked arrival at the pickup point.`, start: "Rider confirmed the passenger pickup.", stop: `${selectedRiderFirstNameForRequest(request)} marked arrival at ${nextRideLeg(request).label.toLowerCase()}.`, complete: "Ride completed.", cancel: actorRole === "rider" ? "Rider cancelled before the ride started. The passenger request was reopened for other nearby riders." : `Passenger cancelled the ride before it started.${cancellationFee?.amount > 0 ? ` Rider compensation fee pending: ${formatMoney(cancellationFee.amount, request.country)}.` : ""}` }[actionName] ?? "Ride status updated."; } function applyRideLifecycleState(request, actionName, reason = "", actorRole = activeRole(), actorId = currentActorIdForChat()) { if (actionName === "cancel") { if (request.status === "in_progress" && ["passenger", "rider"].includes(actorRole)) { const cancellationFee = actorRole === "rider" ? riderCancellationNoPassengerChargeEstimate(request) : inProgressCancellationCompensationEstimate(request); return { ...request, status: "cancelled", cancelledBy: actorId, cancelledAt: new Date().toISOString(), cancelReason: reason, cancellationFeeAmount: cancellationFee.amount, cancellationFeeCurrency: cancellationFee.currency, cancellationFeeStatus: cancellationFee.status, cancellationFeeRiderId: actorRole === "rider" ? null : selectedRiderIdForRequest(request), cancellationFeeElapsedMinutes: actorRole === "rider" ? null : cancellationFee.elapsedMinutes }; } if (actorRole === "rider") { const cancelingRiderIds = [state.rider?.id, state.rider?.supabaseUserId, actorId] .filter(Boolean) .map(String); state.offers = state.offers.filter((offer) => !( offer.requestId === request.id && cancelingRiderIds.includes(String(offer.riderId)) )); return { ...request, status: "open", selectedOfferId: null, agreedFare: null, selectedRiderId: null, selectedRiderName: null, currentStopIndex: 0, lastStopArrivedAt: null, matchedAt: null, arrivedAt: null, startedAt: null, completedAt: null, riderConfirmationStatus: isScheduledRequest(request) ? "released" : null, riderConfirmationRequestedAt: null, riderConfirmedAt: null, releasedAt: new Date().toISOString(), cancelledBy: null, cancelledAt: null, cancelReason: reason }; } const cancellationFee = passengerCancellationFeeEstimate(request); return { ...request, status: "cancelled", cancelledBy: actorId, cancelledAt: new Date().toISOString(), cancelReason: reason, cancellationFeeAmount: cancellationFee.amount, cancellationFeeCurrency: cancellationFee.currency, cancellationFeeStatus: cancellationFee.status, cancellationFeeRiderId: selectedRiderIdForRequest(request), cancellationFeeElapsedMinutes: cancellationFee.elapsedMinutes }; } if (actionName === "stop") { const stops = normalizeRideStops(request?.rideStops); return { ...request, currentStopIndex: Math.min(stops.length, rideStopIndex(request) + 1), lastStopArrivedAt: new Date().toISOString() }; } const nextStatus = { arrive: "arrived", start: "in_progress", complete: "completed" }[actionName]; if (!nextStatus) return request; const nowIso = new Date().toISOString(); return { ...request, status: nextStatus, arrivedAt: actionName === "arrive" ? nowIso : request.arrivedAt, startedAt: actionName === "start" ? nowIso : request.startedAt, currentStopIndex: actionName === "start" ? 0 : request.currentStopIndex, lastStopArrivedAt: actionName === "start" ? null : request.lastStopArrivedAt, completedAt: actionName === "complete" ? nowIso : request.completedAt }; } function rideLifecycleTargetAlreadyReached(request, actionName, previousRequest = null) { if (!request) return false; if (actionName === "arrive") return ["arrived", "in_progress", "completed"].includes(request.status); if (actionName === "start") return ["in_progress", "completed"].includes(request.status); if (actionName === "stop") return request.status === "in_progress" && rideStopIndex(request) > rideStopIndex(previousRequest); if (actionName === "complete") return request.status === "completed"; return false; } function reconcileAppliedRiderPrePickupCancellation(requestId, previousRequest = null) { if (activeRole() !== "rider" || !previousRequest || !["matched", "arrived"].includes(previousRequest.status)) { return false; } const refreshed = state.requests.find((item) => item.id === requestId); const riderId = state.rider?.id; const noLongerMatchedToThisRider = !refreshed || selectedRiderIdForRequest(refreshed) !== riderId || !["matched", "arrived", "in_progress"].includes(refreshed.status); const reopenedForMarketplace = refreshed?.status === "open" && !selectedRiderIdForRequest(refreshed); if (!noLongerMatchedToThisRider && !reopenedForMarketplace) return false; return clearRiderPrePickupCancellationView(requestId, previousRequest); } function clearRiderPrePickupCancellationView(requestId, previousRequest = null, { refresh = false } = {}) { if (activeRole() !== "rider" || !requestId) return false; const request = previousRequest ?? state.requests.find((item) => item.id === requestId); if (request && !["matched", "arrived"].includes(request.status)) return false; if (request && typeof rememberRiderDismissedRequest === "function") { rememberRiderDismissedRequest(request, currentRiderRecord()); } if (request && typeof rememberRiderPrePickupCancellationClear === "function") { rememberRiderPrePickupCancellationClear(request, currentRiderRecord()); } state.riderPage = "requests"; state.selectedRequestId = null; state.offers = state.offers.filter((offer) => offer.requestId !== requestId); state.requests = state.requests.filter((item) => item.id !== requestId); clearStateLookupIndexes(); if (typeof resetRideDockPanels === "function") resetRideDockPanels(); if (typeof updateRiderWorkspaceRoute === "function") { updateRiderWorkspaceRoute("requests", { replace: true, requestId: "" }); } saveState(); if (typeof returnRiderToMarketplace === "function") { returnRiderToMarketplace({ replace: true, refresh }); } else { renderAll(); if (refresh) void refreshMarketplace({ silent: true, reason: "rider_cancel_force_clear" }); } return true; } async function rideLifecycleAlreadyAppliedAfterRefresh(requestId, actionName, previousRequest = null) { if (!hasSupabaseRuntime() || !["arrive", "start", "stop", "complete", "cancel"].includes(actionName)) return false; await refreshMarketplace({ silent: true, reason: "ride_lifecycle_idempotent_recheck" }); if (actionName === "cancel" && reconcileAppliedRiderPrePickupCancellation(requestId, previousRequest)) return true; const refreshed = state.requests.find((item) => item.id === requestId); if (!rideLifecycleTargetAlreadyReached(refreshed, actionName, previousRequest)) return false; renderAll(); return true; } function clearRiderSelectedTerminalRide(requestId, actionName) { if (activeRole() !== "rider" || !["cancel", "complete"].includes(actionName)) return; const request = state.requests.find((item) => item.id === requestId); if (!request || !["completed", "cancelled"].includes(request.status)) return; if (state.selectedRequestId !== requestId) return; state.selectedRequestId = null; if (typeof updateRiderWorkspaceRoute === "function") { updateRiderWorkspaceRoute("requests", { replace: true, requestId: "" }); } } function rideLifecycleNotificationEventTypes(actionName) { return { arrive: ["ride_arrived"], start: ["ride_started"], stop: ["ride_stop_arrived"], complete: ["ride_completed"], cancel: ["ride_cancelled"] }[actionName] ?? []; } function createLocalRideSettlement(request, fareAmount = agreedFareForRequest(request)) { if (!request || request.status === "completed" || rideSettlementRecords().some((settlement) => settlement.requestId === request.id)) return; const breakdown = rideFinancialBreakdown({ ...request, agreedFare: fareAmount, acceptedRouteChangeFare: 0 }); state.rideSettlements = upsertById(state.rideSettlements, { id: makeId("settlement"), requestId: request.id, passengerId: request.passengerId, passengerName: request.passengerName, riderId: selectedRiderIdForRequest(request), riderName: selectedRiderNameForRequest(request) ?? "Rider", fareAmount: Number(fareAmount || 0), stripeFeeAmount: centsToDollars(breakdown.stripeFeeCents), facilitationFeeAmount: centsToDollars(breakdown.facilitationFeeCents), businessServiceFeeAmount: centsToDollars(breakdown.businessServiceFeeCents), riderPayoutAmount: centsToDollars(breakdown.riderPayoutCents), status: "pending_provider_payout", providerReference: "", createdAt: new Date().toISOString() }); } async function changeRideLifecycle(requestId, actionName, reason = "") { const request = state.requests.find((item) => item.id === requestId); if (!request) return false; const actorRole = activeRole(); const actorId = currentActorIdForChat(); if ( actorRole === "rider" && ["start", "stop", "complete"].includes(actionName) && riderIdentityMatches(selectedRiderIdForRequest(request)) ) { const pendingChange = pendingRouteChangeForRequest(request); if (pendingChange) { showRiderRouteChangeDecisionForRequest(request, pendingChange); translatedAlert("reviewRouteChangeBeforeProceeding"); return false; } } if (actionName === "cancel" && actorRole === "rider") { riderInitiatedRideCancellationRequestIds.add(requestId); } const riderPrePickupCancellationReopensRequest = actionName === "cancel" && actorRole === "rider" && ["matched", "arrived"].includes(request.status); const lifecycleKey = `${requestId}:${actionName}`; if (rideLifecycleActionInFlight.has(lifecycleKey)) return false; const cancellationSettlementEstimate = actionName === "cancel" && ((actorRole === "passenger" && ["matched", "arrived"].includes(request.status)) || (actorRole === "passenger" && request.status === "in_progress")) ? rideCancellationCompensationEstimate(request, Date.now(), actorRole) : null; try { rideLifecycleActionInFlight.add(lifecycleKey); const lifecycleGps = await validatedLifecycleGpsPoint(request, actionName); const savedRequest = await changeRideStateInSupabase(requestId, actionName, reason, lifecycleGps); updateRequestById(requestId, (item) => { const localState = applyRideLifecycleState(item, actionName, reason, actorRole, actorId); return savedRequest?.id === requestId ? { ...localState, ...savedRequest } : localState; }); void recordInsuranceTelemetryTransitionInSupabase( savedRequest?.id === requestId ? savedRequest : request, actionName, lifecycleGps ); } catch (error) { if (/(Ride state change not allowed|Updating the ride status is taking too long)/i.test(error.message) && await rideLifecycleAlreadyAppliedAfterRefresh(requestId, actionName, request)) { return true; } if (/(Ride state change not allowed|Updating the ride status is taking too long)/i.test(error.message) && actionName === "cancel" && activeRole() === "rider" && ["matched", "arrived"].includes(request.status) && clearRiderPrePickupCancellationView(requestId, request)) { return true; } void showWakaGoodAlert(error.message); return false; } finally { rideLifecycleActionInFlight.delete(lifecycleKey); } let settlementMessage = ""; if (actionName === "complete") { createLocalRideSettlement(request); if (!ridePaymentProviderSupportsOnline()) { settlementMessage = "Direct cash or mobile-money payment is handled between passenger and rider. Waka commission is handled through the rider wallet after the free period."; } else { try { const settlement = await processRidePaymentSettlement(requestId); if (settlement?.ok) { settlementMessage = settlement.alreadySettled ? "Stripe ride payment was already settled." : settlement.chargeHeld ? `Passenger card was charged. Rider payout ${formatMoney(centsToDollars(settlement.riderPayoutCents || 0), request.country)} is held until the rider finishes Stripe payout setup or admin releases it.` : `Stripe ride payment processed. Rider payout: ${formatMoney(centsToDollars(settlement.riderPayoutCents || 0), request.country)} after estimated Stripe fee ${formatMoney(centsToDollars(settlement.stripeFeeCents || 0), request.country)}.`; } } catch (error) { settlementMessage = `Stripe ride payment needs admin review: ${error.message}`; void showWakaGoodAlert(settlementMessage); logClientWarning("Completed ride payment settlement failed.", error); } } } if (actionName === "cancel" && cancellationSettlementEstimate?.amount > 0) { createLocalRideSettlement(request, cancellationSettlementEstimate.amount); if (!ridePaymentProviderSupportsOnline()) { settlementMessage = "Cancellation compensation is recorded for admin review; no automatic card charge runs in the Cameroon direct-payment MVP."; } else { try { const settlement = await processRidePaymentSettlement(requestId); if (settlement?.ok) { settlementMessage = settlement.alreadySettled ? "Cancellation compensation payment was already settled." : settlement.chargeHeld ? `Passenger card was charged for cancellation compensation. Rider payout ${formatMoney(centsToDollars(settlement.riderPayoutCents || 0), request.country)} is held until Stripe payout setup or admin release.` : `Cancellation compensation payment processed. Rider payout: ${formatMoney(centsToDollars(settlement.riderPayoutCents || 0), request.country)} after estimated Stripe fee ${formatMoney(centsToDollars(settlement.stripeFeeCents || 0), request.country)}.`; } } catch (error) { settlementMessage = `Cancellation compensation payment needs admin review: ${error.message}`; void showWakaGoodAlert(settlementMessage); logClientWarning("Ride cancellation compensation payment settlement failed.", error); } } } pushSystemChat(requestId, rideLifecycleMessage(request, actionName, actorRole)); if (settlementMessage) pushSystemChat(requestId, settlementMessage); clearRiderSelectedTerminalRide(requestId, actionName); if (actionName === "complete" && typeof openQueuedRiderPickupAfterDropoff === "function") { openQueuedRiderPickupAfterDropoff(requestId); } saveState(); if (riderPrePickupCancellationReopensRequest) { clearRiderPrePickupCancellationView(requestId, request); } else { renderAll(); void refreshMarketplace({ silent: true }); } const lifecycleEventTypes = riderPrePickupCancellationReopensRequest ? ["ride_reopened"] : rideLifecycleNotificationEventTypes(actionName); if (lifecycleEventTypes.length) { await processRideRequestPushDelivery(requestId, { eventTypes: lifecycleEventTypes }); } return true; } async function cancelRideBeforeStart(requestId) { let request = state.requests.find((item) => item.id === requestId); const originalRequest = request; if (activeRole() === "rider" && hasSupabaseRuntime()) { try { await refreshMarketplace({ silent: true, reason: "rider_cancel_precheck" }); request = state.requests.find((item) => item.id === requestId); if (!canCancelBeforeStart(request)) { clearRiderPrePickupCancellationView(requestId, originalRequest ?? request ?? { id: requestId, status: "matched" }); return; } } catch (error) { logClientWarning("Marketplace refresh before rider cancellation was skipped.", error); } } if (!canCancelBeforeStart(request)) return; if (activeRole() === "passenger") { const estimate = passengerCancellationFeeEstimate(request); if (estimate.amount > 0) { const ok = await showWakaGoodConfirm(`Cancelling now may charge ${formatMoney(estimate.amount, request.country)} to compensate the rider for ${estimate.elapsedMinutes} minute${estimate.elapsedMinutes === 1 ? "" : "s"} since match. Continue?`); if (!ok) return; } } const reason = await showWakaGoodPrompt("Optional cancellation reason", ""); if (reason === null) return; await changeRideLifecycle(requestId, "cancel", reason.trim()); } async function cancelRideInProgress(requestId) { const request = state.requests.find((item) => item.id === requestId); if (!canCancelInProgress(request)) return; const actorRole = activeRole(); const estimate = actorRole === "rider" ? riderCancellationNoPassengerChargeEstimate(request) : inProgressCancellationCompensationEstimate(request); const ok = actorRole === "rider" ? await showWakaGoodConfirm("End this ride now? Passenger will not be charged because you are cancelling from the rider side.") : await showWakaGoodConfirm(`Cancel this ride now? Waka will charge the passenger a partial fare of ${formatMoney(estimate.amount, request.country)} based on about ${estimate.elapsedMinutes} minute${estimate.elapsedMinutes === 1 ? "" : "s"} after pickup, then route that payout through Stripe.`); if (!ok) return; const reason = await showWakaGoodPrompt("Optional cancellation reason", ""); if (reason === null) return; await changeRideLifecycle(requestId, "cancel", reason.trim()); } async function requestScheduledRideConfirmation(requestId) { const request = state.requests.find((item) => item.id === requestId); if (!request || !requestBelongsToPassenger(request) || !isScheduledRequest(request) || request.status !== "matched") return; if (hasSupabaseRuntime()) { try { await callSupabaseRpc( "request_scheduled_ride_confirmation", { request_id: requestId }, "Requesting scheduled ride confirmation", optionalSupabaseRequestTimeoutMs ); } catch (error) { translatedAlert("requestConfirmationFailed", { message: error.message }); return; } } updateRequestById(requestId, (item) => ({ ...item, riderConfirmationStatus: "requested", riderConfirmationRequestedAt: new Date().toISOString() })); pushSystemChat(requestId, `Passenger requested rider confirmation for the scheduled ride on ${formatDateTime(request.scheduledAt)}.`); saveState(); renderAll(); } async function confirmScheduledRide(requestId) { const request = state.requests.find((item) => item.id === requestId); if (!request || selectedRiderIdForRequest(request) !== state.rider?.id || request.riderConfirmationStatus !== "requested") return; if (hasSupabaseRuntime()) { try { await callSupabaseRpc( "rider_confirm_scheduled_ride", { request_id: requestId }, "Confirming the scheduled ride", optionalSupabaseRequestTimeoutMs ); } catch (error) { translatedAlert("confirmScheduledFailed", { message: error.message }); return; } } updateRequestById(requestId, (item) => ({ ...item, riderConfirmationStatus: "confirmed", riderConfirmedAt: new Date().toISOString() })); pushSystemChat(requestId, `Rider confirmed the scheduled ride for ${formatDateTime(request.scheduledAt)}.`); saveState(); renderAll(); } async function releaseScheduledRide(requestId, message) { const request = state.requests.find((item) => item.id === requestId); if (!request || !isScheduledRequest(request) || request.status !== "matched") return; const allowed = (activeRole() === "passenger" && requestBelongsToPassenger(request)) || (activeRole() === "rider" && selectedRiderIdForRequest(request) === state.rider?.id); if (!allowed) return; if (hasSupabaseRuntime()) { try { await callSupabaseRpc( "release_scheduled_ride_match", { request_id: requestId }, "Reopening the scheduled ride", optionalSupabaseRequestTimeoutMs ); } catch (error) { translatedAlert("reopenScheduledFailed", { message: error.message }); return; } } updateRequestById(requestId, (item) => ({ ...item, status: "open", selectedOfferId: null, agreedFare: null, selectedRiderId: null, selectedRiderName: null, riderConfirmationStatus: "released", riderConfirmationRequestedAt: null, riderConfirmedAt: null, releasedAt: new Date().toISOString() })); pushSystemChat(requestId, message); saveState(); renderAll(); } async function sendChatVoiceBlob(request, blob, durationSeconds) { const sender = activeRole(); const localUrl = URL.createObjectURL(blob); const message = { id: makeId("chat"), requestId: request.id, senderId: currentActorIdForChat(), sender, text: `Voice message (${chatVoiceDurationLabel(durationSeconds)})`, mediaType: "voice_note", mediaBucket: chatVoiceBucketName(), mediaPath: "", mediaMimeType: blob.type || "audio/webm", mediaDurationSeconds: durationSeconds, mediaSizeBytes: blob.size, mediaUrl: localUrl, deliveryStatus: "sending", createdAt: new Date().toISOString() }; state.chats.push(message); saveState(); renderChat(); try { const media = await uploadChatVoiceNote(request.id, message.id, blob); const uploadedMessage = { ...message, ...media, mediaUrl: localUrl }; state.chats = state.chats.map((item) => item.id === message.id ? uploadedMessage : item); saveState(); renderChat(); const savedMessage = await saveChatMessageToSupabase(uploadedMessage, { throwOnError: true }); const deliveredMessage = savedMessage?.id ? { ...savedMessage, mediaUrl: localUrl, deliveryStatus: "sent" } : { ...uploadedMessage, deliveryStatus: "sent" }; state.chats = upsertById(state.chats.filter((item) => item.id !== message.id), deliveredMessage); saveState(); renderChat(); if (els.chatStatus) els.chatStatus.textContent = "Open - voice message sent"; setChatVoiceStatus("Voice message sent."); if (typeof forceMarketplaceRefreshSoon === "function") forceMarketplaceRefreshSoon("ride_voice_chat_sent"); } catch (error) { state.chats = state.chats.map((item) => item.id === message.id ? { ...item, deliveryStatus: "failed", deliveryError: error.message } : item); saveState(); renderChat(); if (els.chatStatus) els.chatStatus.textContent = `Open - voice message failed: ${error.message}`; setChatVoiceStatus(`Voice message failed: ${error.message}`); } } async function finishChatVoiceRecording(request, startedAt, chunks, mimeType) { const blob = new Blob(chunks, { type: mimeType || "audio/webm" }); const durationSeconds = Math.max(1, Math.round((Date.now() - startedAt) / 1000)); if (!blob.size) { setChatVoiceStatus("No voice was recorded."); return; } if (blob.size > chatVoiceMaxBytes) { setChatVoiceStatus("Voice message is too large. Keep it under one minute."); return; } setChatVoiceStatus("Uploading voice message..."); await sendChatVoiceBlob(request, blob, durationSeconds); } async function toggleChatVoiceRecording(event) { event?.preventDefault?.(); if (chatVoiceRecorder) { stopChatVoiceRecording(); return; } const request = selectedChatRequest(); if (!request || !canChatOnRequest(request)) { setChatVoiceStatus("Voice messages open only after passenger chooses a rider."); return; } if (!chatVoiceRecordingSupported()) { setChatVoiceStatus("This browser cannot record voice messages."); return; } if (!isSupabaseMode()) { setChatVoiceStatus("Voice messages need Supabase storage."); return; } try { setChatVoiceStatus("Requesting microphone permission..."); const mimeType = preferredChatVoiceMimeType(); const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true } }); const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : undefined); chatVoiceRecordingStream = stream; chatVoiceRecorder = recorder; chatVoiceRecordingChunks = []; chatVoiceRecordingStartedAt = Date.now(); recorder.addEventListener("dataavailable", (recordingEvent) => { if (recordingEvent.data?.size) chatVoiceRecordingChunks.push(recordingEvent.data); }); recorder.addEventListener("stop", () => { const chunks = chatVoiceRecordingChunks.slice(); const startedAt = chatVoiceRecordingStartedAt; const recordedMimeType = recorder.mimeType || mimeType || "audio/webm"; cleanupChatVoiceRecording(); void finishChatVoiceRecording(request, startedAt, chunks, recordedMimeType); }, { once: true }); recorder.start(); updateChatVoiceButtonState({ recording: true }); setChatVoiceStatus("Recording... tap Stop to send."); chatVoiceRecordingTimer = window.setTimeout(() => { if (chatVoiceRecorder?.state === "recording") stopChatVoiceRecording(); }, chatVoiceMaxDurationMs); } catch (error) { cleanupChatVoiceRecording(); setChatVoiceStatus(/permission|denied|notallowed/i.test(String(error?.message || error)) ? "Microphone permission is required for voice messages." : `Could not record voice message: ${error.message}`); } } async function sendChat(event) { event.preventDefault(); const request = selectedChatRequest(); const text = els.chatInput.value.trim(); if (!request || !canChatOnRequest(request) || !text) return; const sender = activeRole(); const message = { id: makeId("chat"), requestId: request.id, senderId: currentActorIdForChat(), sender, text, deliveryStatus: "sending", createdAt: new Date().toISOString() }; state.chats.push(message); els.chatInput.value = ""; saveState(); renderChat(); try { const savedMessage = await saveChatMessageToSupabase(message, { throwOnError: true }); const deliveredMessage = savedMessage?.id ? { ...savedMessage, deliveryStatus: "sent" } : { ...message, deliveryStatus: "sent" }; state.chats = upsertById(state.chats.filter((item) => item.id !== message.id), deliveredMessage); saveState(); renderChat(); void relayRideChatMessageToPhone(message); if (els.chatStatus) els.chatStatus.textContent = "Open - message sent"; if (typeof forceMarketplaceRefreshSoon === "function") forceMarketplaceRefreshSoon("ride_chat_sent"); } catch (error) { state.chats = state.chats.map((item) => item.id === message.id ? { ...item, deliveryStatus: "failed", deliveryError: error.message } : item); saveState(); renderChat(); if (els.chatStatus) els.chatStatus.textContent = `Open - message failed: ${error.message}`; } } async function submitSafetyReport(event) { event.preventDefault(); const request = reportableRideForRole(); if (!canReportOnRequest(request)) { setTranslatedStatus(els.safetyReportStatus, "safetyReportUnavailable"); return; } const details = els.safetyReportDetails.value.trim(); if (details.length < 10) { setTranslatedStatus(els.safetyReportStatus, "safetyReportNeedsDetail"); return; } const reporterId = currentActorIdForChat(); if (!reporterId) { setTranslatedStatus(els.safetyReportStatus, "safetyReportSignInRequired"); return; } const target = reportTargetForRequest(request); const report = { id: makeId("report"), requestId: request.id, reporterId, reporterName: activeRole() === "passenger" ? state.passenger?.name : state.rider?.name, reporterRole: activeRole(), reportedUserId: target.id, reportedUserName: target.name, category: els.safetyReportCategory.value, severity: els.safetyReportSeverity.value, details, status: "open", routeSummary: `${request.pickupArea} to ${requestDestinationText(request)}`, createdAt: new Date().toISOString() }; try { setTranslatedStatus(els.safetyReportStatus, hasSupabaseRuntime() ? "submittingSafetySupabase" : "savingSafetyReport"); const savedReport = await saveSafetyReportToSupabase(report); state.safetyReports = upsertById(state.safetyReports, savedReport); els.safetyReportDetails.value = ""; saveState(); renderAll(); setTranslatedStatus(els.safetyReportStatus, "safetyReportSubmitted"); } catch (error) { setTranslatedStatus(els.safetyReportStatus, "safetyReportFailed", { message: error.message }); } } function supportTicketConfirmationMessage(ticket) { const reference = String(ticket?.id ?? "") .replace(/[^a-z0-9]/gi, "") .slice(0, 8) .toUpperCase(); return `Delivered to Waka admin inbox${reference ? ` (Ref ${reference})` : ""}.`; } async function submitAccountSupportTicket(event) { event.preventDefault(); const form = event.currentTarget; const type = form?.dataset.supportType === "rider" ? "rider" : "passenger"; const account = type === "rider" ? state.rider : state.passenger; const session = state.sessions[type]; const categoryInput = els[`${type}SupportCategory`]; const subjectInput = els[`${type}SupportSubject`]; const messageInput = els[`${type}SupportMessage`]; const status = els[`${type}SupportStatus`]; if (!account?.id || !session) { if (status) { status.classList.remove("success-status"); status.classList.add("error-status"); status.textContent = "Sign in before contacting Waka support."; } return; } const message = messageInput?.value.trim() ?? ""; const subject = subjectInput?.value.trim() || `${type === "rider" ? "Rider" : "Passenger"} support request`; if (message.length < 10) { if (status) { status.classList.remove("success-status"); status.classList.add("error-status"); status.textContent = "Add enough detail for Waka admin to understand the request."; } return; } const ticket = { id: makeId("support"), accountId: account.id, accountRole: type, accountName: account.name || account.email || account.phone || type, category: categoryInput?.value || "other", subject, message, priority: categoryInput?.value === "safety" ? "high" : "medium", status: "open", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; try { if (status) { status.classList.remove("success-status", "error-status"); status.textContent = hasSupabaseRuntime() ? "Sending support request to Waka admin..." : "Saving support request..."; } const savedTicket = await saveSupportTicketToSupabase(ticket); state.supportTickets = upsertById(state.supportTickets, savedTicket); if (subjectInput) subjectInput.value = ""; if (messageInput) messageInput.value = ""; saveState(); if (status) { status.classList.remove("error-status"); status.classList.add("success-status"); status.textContent = supportTicketConfirmationMessage(savedTicket); } } catch (error) { if (status) { status.classList.remove("success-status"); status.classList.add("error-status"); status.textContent = `Could not send support request: ${error.message}`; } } } async function submitRideRating(event) { event.preventDefault(); const initialRequest = typeof selectedWorkspaceRequest === "function" ? selectedWorkspaceRequest() : selectedRequest(); if (els.rideRatingStatus && hasSupabaseRuntime()) { els.rideRatingStatus.textContent = "Checking completed ride details before submitting..."; } const { request, target } = await resolveRideRatingContext(initialRequest); if (!(request && request.status === "completed" && roleCanSeeRequest(request) && target && !existingRatingForRequest(request))) { els.rideRatingStatus.textContent = "Ratings open after selecting a completed ride that has not already been rated."; return; } const reviewerId = currentActorIdForChat(); const categoryScore = (element) => Number(element?.value || els.rideRatingScore.value); const rating = { id: makeId("rating"), requestId: request.id, reviewerId, reviewerRole: activeRole(), reviewerName: activeRole() === "passenger" ? state.passenger?.name : state.rider?.name, ratedUserId: target.id, ratedUserName: target.name, score: Number(els.rideRatingScore.value), safetyScore: categoryScore(els.rideRatingSafety), punctualityScore: categoryScore(els.rideRatingPunctuality), communicationScore: categoryScore(els.rideRatingCommunication), vehicleScore: categoryScore(els.rideRatingVehicle), comment: els.rideRatingComment.value.trim(), createdAt: new Date().toISOString() }; try { els.rideRatingStatus.textContent = hasSupabaseRuntime() ? "Submitting rating to Supabase..." : "Saving rating..."; const savedRating = await saveRideRatingToSupabase(rating); state.rideRatings = upsertById(state.rideRatings, savedRating); if (state.rider?.id === savedRating.ratedUserId && typeof loadMyRiderRatingSummaryFromSupabase === "function") { await loadMyRiderRatingSummaryFromSupabase(); } if (state.rider?.id === savedRating.ratedUserId) state.rider.rating = ratingSummaryForRider(state.rider.id); state.riders = state.riders.map((rider) => ( rider.id === savedRating.ratedUserId ? { ...rider, rating: ratingSummaryForRider(rider.id) } : rider )); els.rideRatingComment.value = ""; saveState(); renderAll(); els.rideRatingStatus.textContent = "Rating submitted. Thank you for helping accountability."; } catch (error) { els.rideRatingStatus.textContent = `Could not submit rating: ${error.message}`; } } async function submitRideTip(event, requestId) { event.preventDefault(); const form = event.currentTarget; const status = form.querySelector(".ride-tip-status"); const input = form.querySelector(".ride-tip-input"); const request = state.requests.find((item) => item.id === requestId); if (!canTipRequest(request)) { status.textContent = "Tips are available once, after a completed passenger ride."; return; } const amount = Number(String(input.value).replace(/[^\d.]/g, "")); if (!Number.isFinite(amount) || amount <= 0) { status.textContent = "Enter a tip amount greater than zero."; return; } const tipCents = dollarsToCents(amount); const stripeFeeCents = stripeProcessingFeeCents(tipCents); const tip = { id: makeId("tip"), requestId: request.id, passengerId: request.passengerId, passengerName: request.passengerName, riderId: selectedRiderIdForRequest(request), riderName: selectedRiderNameForRequest(request) ?? "Rider", amount: centsToDollars(tipCents), stripeFeeAmount: centsToDollars(stripeFeeCents), riderPayoutAmount: centsToDollars(Math.max(0, tipCents - stripeFeeCents)), status: "pending_provider_payout", providerReference: "", createdAt: new Date().toISOString() }; try { status.textContent = hasSupabaseRuntime() ? "Submitting tip through Supabase..." : "Saving tip..."; const savedTip = await saveRideTipToSupabase(tip); state.rideTips = upsertById(state.rideTips, savedTip); input.value = ""; saveState(); renderAll(); } catch (error) { status.textContent = `Could not submit tip: ${error.message}`; } } // Passenger-facing workspace, ride request, offer review, map, chat, and account UI. let passengerNearbyRiderCountsTimer = null; let passengerNearbyRiderCountsRequestId = 0; let passengerNearbyRiderCountsAutoTimer = null; let passengerNearbyRiderCountsLastRefreshAt = 0; let passengerWorkspacePageSelectedInSession = false; let passengerFareBoostLastFocus = null; let passengerOfferCounterLastFocus = null; let passengerFareBoostOpenRequestId = null; let passengerRideDockRequestId = null; let passengerRideDockOpenPanel = null; let passengerRideOptionLastPointerAt = 0; let passengerFareModeLastPointerAt = 0; let passengerAddStopLastPointerAt = 0; let riderRideDockRequestId = null; let riderRideDockOpenPanel = null; const recentAddressConfirmationPromises = new Set(); const rideRequestOptionPanelNames = ["stops", "timing", "vehicle", "fare"]; const intercityOperatorWorkspaceTabs = ["profile", "departures", "bookings", "reports", "payments", "advertising", "messages"]; let intercityOperatorWorkspaceTab = "profile"; let intercityOperatorAgencyFilterValue = "all"; let intercityOperatorStatusFilterValue = "all"; let intercityOperatorSearchValue = ""; function passengerUiText(key, fallback = "", values = {}) { const translated = typeof translatedMessage === "function" ? translatedMessage(key, values) : typeof translatedValue === "function" ? translatedValue(key) : ""; return translated || fallback; } function setPassengerUiStatus(node, key, fallback = "", values = {}) { if (!node) return; if (typeof setTranslatedStatus === "function") { setTranslatedStatus(node, key, values); } else { node.textContent = passengerUiText(key, fallback, values); } } function updateRidePaymentOptions(country = selectedPassengerCountry()) { if (!els.paymentPreference) return; const options = ridePaymentOptionsForCountry(country); const selectedValue = validPaymentPreferenceForCountry(els.paymentPreference.value, country); populateSelectOptions(els.paymentPreference, options, selectedValue); } function updateScheduledRideControls() { if (!els.rideTiming || !els.scheduledAt) return; const scheduled = els.rideTiming.value === "scheduled"; const field = els.scheduledAtField ?? els.scheduledAt.closest("label"); if (field) field.hidden = !scheduled; els.scheduledAt.disabled = !scheduled; if (!scheduled) els.scheduledAt.value = ""; } function openScheduledDateTimePicker() { if (!els.rideTiming || !els.scheduledAt || els.rideTiming.value !== "scheduled" || els.scheduledAt.disabled) return; window.setTimeout(() => { els.scheduledAt.focus(); try { els.scheduledAt.showPicker?.(); } catch { // Some mobile browsers block programmatic picker opening; focus still reveals the input. } }, 0); } function rideRequestOptionTarget(name) { if (name === "stops") return { button: els.addRideStop, panel: els.rideStopsPanel, focus: els.rideStops }; if (name === "timing") return { button: els.toggleRideTiming, panel: els.rideTimingPanel, focus: els.rideTiming }; if (name === "vehicle") return { button: els.toggleVehiclePreference, panel: els.vehiclePreferencePanel, focus: els.vehiclePreference }; if (name === "fare") return { button: els.toggleFareDetails, panel: els.fareDetailsPanel, focus: els.fareOffer }; return { button: null, panel: null, focus: null }; } function negotiatedFareInlineActive() { return Boolean(passengerFareMode() === "negotiable" && els.destination?.value.trim()); } function setRideRequestOptionPanelState(name, expanded, { focus = false } = {}) { const target = rideRequestOptionTarget(name); const isExpanded = Boolean(expanded); if (target.panel) target.panel.hidden = !isExpanded; if (target.button) { target.button.classList.toggle("active", isExpanded); target.button.setAttribute("aria-expanded", isExpanded ? "true" : "false"); } if (focus && isExpanded && target.focus instanceof HTMLElement) { window.setTimeout(() => target.focus.focus(), 0); } } function closeRideRequestOptionPanel(name) { if (name === "fare" && negotiatedFareInlineActive()) return; if (typeof passengerManualFareOnly === "function" && passengerManualFareOnly() && (name === "fare" || name === "vehicle")) return; const target = rideRequestOptionTarget(name); if (target.panel) target.panel.hidden = true; if (target.button) { target.button.classList.remove("active"); target.button.setAttribute("aria-expanded", "false"); } } function closeOtherRideRequestOptionPanels(activeName) { rideRequestOptionPanelNames .filter((name) => name !== activeName) .forEach(closeRideRequestOptionPanel); } function setRideRequestOptionPanel(name, expanded, { focus = false } = {}) { const isExpanded = Boolean(expanded); if (isExpanded) closeOtherRideRequestOptionPanels(name); setRideRequestOptionPanelState(name, isExpanded, { focus }); } function syncNegotiatedFareInlinePanel({ focus = false } = {}) { const inline = negotiatedFareInlineActive(); els.fareDetailsPanel?.classList.toggle("negotiated-fare-inline", inline); if (inline) { setRideRequestOptionPanelState("fare", true, { focus }); } else if (passengerFareMode() === "negotiable") { setRideRequestOptionPanelState("fare", false); } } function toggleRideRequestOptionPanel(name, { focus = true } = {}) { const target = rideRequestOptionTarget(name); const nextExpanded = !target.panel || target.panel.hidden; setRideRequestOptionPanel(name, nextExpanded, { focus }); } function initializeRideRequestOptionPanels() { const manualFareOnly = typeof passengerManualFareOnly === "function" && passengerManualFareOnly(); setRideRequestOptionPanel("stops", Boolean(els.rideStops && !els.rideStops.disabled && normalizeRideStops(els.rideStops.value).length)); setRideRequestOptionPanel("timing", els.rideTiming?.value === "scheduled"); setRideRequestOptionPanel("vehicle", manualFareOnly || normalizeCarTypePreference(els.vehiclePreference?.value) !== "sedan"); setRideRequestOptionPanel("fare", manualFareOnly); } function handleRideRequestOptionToggle(name, event) { const now = Date.now(); if (event?.type === "click" && now - passengerRideOptionLastPointerAt < 700) { event.preventDefault?.(); return; } if (event?.type && event.type !== "click" && event.type !== "keydown") { if (now - passengerRideOptionLastPointerAt < 250) { event.preventDefault?.(); return; } passengerRideOptionLastPointerAt = now; } event?.preventDefault?.(); toggleRideRequestOptionPanel(name); } function handleRideRequestOptionKeyToggle(name, event) { if (!["Enter", " "].includes(event?.key)) return; handleRideRequestOptionToggle(name, event); } function activateRideStopTool(event) { const now = Date.now(); if (event?.type === "click" && now - passengerAddStopLastPointerAt < 700) { event.preventDefault?.(); return; } if (event?.type && event.type !== "click" && event.type !== "keydown") { if (now - passengerAddStopLastPointerAt < 250) { event.preventDefault?.(); return; } passengerAddStopLastPointerAt = now; } handleAddRideStop(event); } function activateRideStopToolFromKey(event) { if (!["Enter", " "].includes(event?.key)) return; activateRideStopTool(event); } function handleRideTimingChange() { setRideRequestOptionPanel("timing", true); updateScheduledRideControls(); openScheduledDateTimePicker(); } function setPassengerRiderAvailabilityMessage(message) { if (!els.passengerRiderAvailability) return; els.passengerRiderAvailability.textContent = message; } function renderPassengerRiderAvailabilityCounts(row) { if (!els.passengerRiderAvailability) return; const normal = Math.max(0, Number(row?.normal_count ?? row?.normalCount ?? 0) || 0); const special = Math.max(0, Number(row?.special_count ?? row?.specialCount ?? 0) || 0); const total = Math.max(0, Number(row?.total_count ?? row?.totalCount ?? normal + special) || 0); els.passengerRiderAvailability.innerHTML = ` ${escapeHtml(String(normal))} Normal available ${escapeHtml(String(special))} XL/Special available ${escapeHtml(String(total))} total near pickup `; } function passengerAvailabilityPickupPoint() { if (!state.passenger) return null; const pickupCity = selectedRidePickupCity(); const pickupGps = passengerPickupGpsForFormChoice(); const origin = routeOriginForEstimate( state.passenger.country, pickupCity, els.pickupArea?.value, els.pickupDescription?.value, pickupGps ); return requestPickupGpsFromRouteOrigin(origin, pickupGps); } async function refreshPassengerNearbyRiderCounts() { if (!els.passengerRiderAvailability) return null; if (!hasSupabaseRuntime() || !state.passenger || !hasSignedIn("passenger")) { setPassengerRiderAvailabilityMessage("Sign in and set pickup to see nearby rider availability."); return null; } const pickupPoint = passengerAvailabilityPickupPoint(); if (!pickupPoint) { setPassengerRiderAvailabilityMessage("Enter a pickup address or check exact current location to see nearby rider availability."); return null; } const requestId = ++passengerNearbyRiderCountsRequestId; passengerNearbyRiderCountsLastRefreshAt = Date.now(); setPassengerRiderAvailabilityMessage("Checking available riders near pickup..."); try { const rows = await callSupabaseRpcResult( "passenger_nearby_rider_counts", { p_country: state.passenger.country, p_city: selectedRidePickupCity(), p_pickup_lat: pickupPoint.latitude, p_pickup_lng: pickupPoint.longitude }, "Checking nearby rider availability", optionalSupabaseRequestTimeoutMs ); if (requestId !== passengerNearbyRiderCountsRequestId) return null; const row = Array.isArray(rows) ? rows[0] : rows; renderPassengerRiderAvailabilityCounts(row); return row; } catch (error) { if (requestId === passengerNearbyRiderCountsRequestId) { setPassengerRiderAvailabilityMessage("Nearby rider counts are temporarily unavailable."); } logClientWarning("Nearby rider availability count could not be loaded.", error); return null; } } function schedulePassengerNearbyRiderCountsRefresh(delayMs = 350) { window.clearTimeout(passengerNearbyRiderCountsTimer); passengerNearbyRiderCountsTimer = window.setTimeout(() => { void refreshPassengerNearbyRiderCounts() .finally(() => ensurePassengerNearbyRiderCountsAutoRefresh()); }, delayMs); } function passengerNearbyRiderCountsShouldAutoRefresh() { return Boolean( activeRole() === "passenger" && !document.hidden && hasSignedIn("passenger") && passengerWorkspacePage() === "request" && passengerAvailabilityPickupPoint() ); } function stopPassengerNearbyRiderCountsAutoRefresh() { if (passengerNearbyRiderCountsAutoTimer == null) return; window.clearTimeout(passengerNearbyRiderCountsAutoTimer); passengerNearbyRiderCountsAutoTimer = null; } function ensurePassengerNearbyRiderCountsAutoRefresh() { if (!passengerNearbyRiderCountsShouldAutoRefresh()) { stopPassengerNearbyRiderCountsAutoRefresh(); return; } if (passengerNearbyRiderCountsAutoTimer != null) return; const elapsedMs = passengerNearbyRiderCountsLastRefreshAt ? Date.now() - passengerNearbyRiderCountsLastRefreshAt : passengerNearbyRiderCountsRefreshIntervalMs; const delayMs = Math.max( 500, passengerNearbyRiderCountsRefreshIntervalMs - Math.max(0, elapsedMs) ); passengerNearbyRiderCountsAutoTimer = window.setTimeout(() => { passengerNearbyRiderCountsAutoTimer = null; if (!passengerNearbyRiderCountsShouldAutoRefresh()) return; void refreshPassengerNearbyRiderCounts() .finally(() => ensurePassengerNearbyRiderCountsAutoRefresh()); }, delayMs); } function renderNotificationPreferenceControls(list, type) { if (!list || typeof notificationPreferenceOptions === "undefined") return; const panel = document.createElement("article"); panel.className = "notice-item notification-preferences"; const allEnabled = notificationPreferenceEnabled(type, "all"); const typeOptions = notificationPreferenceOptions.filter((option) => option.key !== "all"); const choices = typeOptions.map((option) => { const checked = notificationPreferenceEnabled(type, option.key) ? " checked" : ""; const disabled = allEnabled ? "" : " disabled"; return ` `; }).join(""); panel.innerHTML = ` Notification preferences

Mute all notifications or choose which updates can interrupt this device. Ride records still stay in this notices list.

${choices}
`; panel.querySelectorAll("[data-notification-preference]").forEach((input) => { input.addEventListener("change", () => { setNotificationPreference(type, input.dataset.notificationPreference, input.checked); }); }); list.append(panel); } function renderAccountNotices(type) { const panel = type === "passenger" ? els.passengerNoticePanel : els.riderNoticePanel; const list = type === "passenger" ? els.passengerNoticeList : els.riderNoticeList; const signedIn = type === "passenger" ? Boolean(hasSignedIn("passenger") && state.passenger) : Boolean(hasSignedIn("rider") && state.rider); if (!signedIn) panel.hidden = true; if (signedIn && type === "rider") panel.hidden = riderWorkspacePage() !== "notices"; if (typeof updatePushNotificationControls === "function") updatePushNotificationControls(type); list.innerHTML = ""; if (!signedIn) return; renderNotificationPreferenceControls(list, type); const notices = currentAccountNotifications(type); if (typeof refreshAccountNotificationsFromSupabase === "function") { const activeNoticePage = type === "passenger" ? passengerWorkspacePage() === "notices" : riderWorkspacePage() === "notices"; const shouldRefresh = true; if (shouldRefresh) { void refreshAccountNotificationsFromSupabase(type, { force: activeNoticePage }).then(() => { const latest = currentAccountNotifications(type); if (latest.length !== notices.length) renderAccountNotices(type); }); } } if (!notices.length) { list.append(emptyState("No notices for this account.")); return; } notices.slice(0, 5).forEach((notice) => { const item = document.createElement("article"); item.className = "notice-item"; item.innerHTML = ` ${escapeHtml(notice.title)}

${escapeHtml(notice.body)}

${formatDateTime(notice.createdAt)} - ${escapeHtml(notificationDeliveryLabel(notice.deliveryChannels))} `; list.append(item); }); } function renderBusinessAccountPanel() { if (!els.businessAccountForm) return; if (agencyWorkspaceActive()) { els.businessAccountForm.hidden = true; if (els.businessReferralPanel) els.businessReferralPanel.hidden = true; return; } const passengerSignedIn = Boolean(hasSignedIn("passenger") && state.passenger); if (!passengerSignedIn) { els.businessAccountForm.hidden = true; return; } const accounts = passengerBusinessAccounts(); els.businessAccountStatus.textContent = accounts.length ? businessAccountSummary(accounts[0]) : `Business accounts require Waka verification before ride billing. Verified businesses get ${businessFreeTrialDays} days free; after that Starter adds ${Math.round(businessRideServiceFeeRate * 100)}% per completed ride or Partner is ${formatMoney(businessPartnerMonthlySubscriptionFee)}/month with no ${Math.round(businessRideServiceFeeRate * 100)}% ride fee.`; els.businessAccountList.innerHTML = ""; renderReferralPanel("business"); if (!accounts.length) { els.businessAccountList.append(emptyState("No business account is linked to this passenger yet.")); return; } accounts.forEach((account) => { const item = document.createElement("article"); item.className = "notice-item"; const subscription = businessSubscriptionFor(account.id); const canStartPartnerCheckout = account.status === "active" && account.verificationStatus === "verified" && !businessSubscriptionIsActive(subscription); item.innerHTML = ` ${escapeHtml(account.businessName)}

${escapeHtml(businessAccountSummary(account))}

${escapeHtml(account.billingEmail)} - ${escapeHtml(businessPlanLabel(account.planCode))} - ${businessAccountCanRequest(account) ? "Business rides active" : "Admin review or billing required"} ${escapeHtml(businessFreeTrialText(account))} ${account.businessAddress ? `${escapeHtml(account.businessAddress)}` : ""} ${canStartPartnerCheckout ? `
` : ""} `; item.querySelector(".business-subscription-start")?.addEventListener("click", () => startBusinessSubscriptionCheckout(account.id)); els.businessAccountList.append(item); }); } let intercityCatalogLoadPromise = null; let publicIntercityCatalogConfigRetryTimer = null; let publicIntercityCatalogLoading = false; let intercityOwnerLoadPromise = null; let agencyWorkspaceSessionPromise = null; let agencyWorkspaceSessionCheckedAt = 0; let intercityOperatorAutoRefreshTimer = null; let intercityTravelerLoadPromise = null; let selectedPublicIntercityDepartureId = ""; let selectedPublicIntercitySeatNumbers = []; let publicIntercityCatalogLoadedAt = 0; let lastPublicIntercityAgencyIds = new Set(); let lastPublicIntercityDepartureIds = new Set(); let intercityOwnerDataLoadedAt = 0; let intercityTravelerDataLoadedAt = 0; function publicIntercityCatalogIsLoading() { return Boolean(publicIntercityCatalogLoading || (intercityCatalogLoadPromise && !publicIntercityCatalogLoadedAt)); } const agencyWorkspaceSessionStorageKey = "waka-agency-workspace-owner"; const configuredCameroonIntercityCities = typeof countryCities !== "undefined" && Array.isArray(countryCities?.Cameroon) ? countryCities.Cameroon : []; const cameroonIntercityCities = [...new Set(configuredCameroonIntercityCities.concat([ "Abong-Mbang", "Akonolinga", "Bafang", "Bafia", "Bafoussam", "Bali", "Bamenda", "Bandjoun", "Bangangte", "Banyo", "Batouri", "Belo", "Bertoua", "Bogo", "Buea", "Deido", "Dibombari", "Dimako", "Dizangue", "Djoum", "Douala", "Dschang", "Ebolowa", "Edea", "Eseka", "Evodoula", "Foumban", "Foumbot", "Fundong", "Garoua", "Guider", "Idenau", "Kaele", "Kousseri", "Kribi", "Kumba", "Kumbo", "Limbe", "Loum", "Mamfe", "Manjo", "Maroua", "Mbalmayo", "Mbandjock", "Mbanga", "Mbouda", "Meiganga", "Melong", "Mokolo", "Mora", "Mutengene", "Nanga-Eboko", "Ngaoundere", "Nguti", "Nkambe", "Nkongsamba", "Ntui", "Obala", "Penja", "Sangmelima", "Tiko", "Tibati", "Wum", "Yagoua", "Yaounde", "Yokadouma" ]))].filter(Boolean).sort((a, b) => a.localeCompare(b)); const cameroonFallbackIntercityAgencies = [ { id: "directory-finexs-voyages", ownerProfileId: null, businessAccountId: null, companyName: "FINEXS Voyages", slug: "finexs-voyages-directory", description: "Directory listing for Waka inter-city discovery. Live departures appear here after the operator or Waka admin publishes schedule, fare, seat, and boarding details.", supportEmail: "", supportPhone: "", terminalAddress: "Yaounde terminal details load when live departures are published.", hqCity: "Yaounde", operatedCities: ["Yaounde", "Douala", "Mbalmayo"], mtnMomoName: "", mtnMomoNumber: "", orangeMoneyName: "", orangeMoneyNumber: "", boardingPolicy: "Arrive early and confirm the exact boarding point after the live departure is published.", monthlyFeeXaf: 25000, subscriptionStatus: "trial", subscriptionPaidUntil: null, trialStartedAt: "2026-01-01T00:00:00.000Z", trialEndsAt: "2100-01-01T00:00:00.000Z", status: "active", allowsGuestBooking: true, advertisingEnabled: true, active: true, createdAt: null, updatedAt: null }, { id: "directory-buca-voyages", ownerProfileId: null, businessAccountId: null, companyName: "Buca Voyages", slug: "buca-voyages-directory", description: "Directory listing for Waka inter-city discovery. Live departures appear here after the operator or Waka admin publishes schedule, fare, seat, and boarding details.", supportEmail: "", supportPhone: "", terminalAddress: "Mvan, Yaounde. Exact boarding details load when live departures are published.", hqCity: "Yaounde", operatedCities: ["Yaounde", "Mbalmayo", "Sangmelima"], mtnMomoName: "", mtnMomoNumber: "", orangeMoneyName: "", orangeMoneyNumber: "", boardingPolicy: "Arrive early and confirm the exact boarding point after the live departure is published.", monthlyFeeXaf: 25000, subscriptionStatus: "trial", subscriptionPaidUntil: null, trialStartedAt: "2026-01-01T00:00:00.000Z", trialEndsAt: "2100-01-01T00:00:00.000Z", status: "active", allowsGuestBooking: true, advertisingEnabled: true, active: true, createdAt: null, updatedAt: null }, { id: "directory-waka-starter-bamenda-connect", ownerProfileId: null, businessAccountId: null, companyName: "Waka Starter Bamenda Connect", slug: "waka-starter-bamenda-connect", description: "Starter operator record kept visible in the public planner until more live agencies onboard.", supportEmail: "starter-bamenda@wakagood.com", supportPhone: "+237670000101", terminalAddress: "Up Station terminal, Bamenda", hqCity: "Bamenda", operatedCities: ["Bamenda", "Douala", "Yaounde", "Bafoussam"], mtnMomoName: "Waka Starter Bamenda Connect", mtnMomoNumber: "+237670000101", orangeMoneyName: "Waka Starter Bamenda Connect", orangeMoneyNumber: "+237690000101", boardingPolicy: "Arrive at least one hour before departure with your booking reference.", monthlyFeeXaf: 25000, subscriptionStatus: "trial", subscriptionPaidUntil: null, trialStartedAt: "2026-01-01T00:00:00.000Z", trialEndsAt: "2100-01-01T00:00:00.000Z", status: "active", allowsGuestBooking: true, advertisingEnabled: true, active: true, createdAt: null, updatedAt: null }, { id: "directory-waka-starter-capital-link", ownerProfileId: null, businessAccountId: null, companyName: "Waka Starter Capital Link", slug: "waka-starter-capital-link", description: "Starter operator record kept visible in the public planner until more live agencies onboard.", supportEmail: "starter-yaounde@wakagood.com", supportPhone: "+237670000202", terminalAddress: "Mvan departure point, Yaounde", hqCity: "Yaounde", operatedCities: ["Yaounde", "Douala", "Bamenda", "Bafoussam"], mtnMomoName: "Waka Starter Capital Link", mtnMomoNumber: "+237670000202", orangeMoneyName: "Waka Starter Capital Link", orangeMoneyNumber: "+237690000202", boardingPolicy: "Arrive at least one hour before departure with your booking reference.", monthlyFeeXaf: 25000, subscriptionStatus: "trial", subscriptionPaidUntil: null, trialStartedAt: "2026-01-01T00:00:00.000Z", trialEndsAt: "2100-01-01T00:00:00.000Z", status: "active", allowsGuestBooking: true, advertisingEnabled: true, active: true, createdAt: null, updatedAt: null }, { id: "directory-waka-starter-coastline", ownerProfileId: null, businessAccountId: null, companyName: "Waka Starter Coastline", slug: "waka-starter-coastline", description: "Starter operator record kept visible in the public planner until more live agencies onboard.", supportEmail: "starter-coast@wakagood.com", supportPhone: "+237670000303", terminalAddress: "Mile 17 terminal, Buea", hqCity: "Buea", operatedCities: ["Buea", "Limbe", "Douala", "Kumba"], mtnMomoName: "Waka Starter Coastline", mtnMomoNumber: "+237670000303", orangeMoneyName: "Waka Starter Coastline", orangeMoneyNumber: "+237690000303", boardingPolicy: "Arrive at least one hour before departure with your booking reference.", monthlyFeeXaf: 25000, subscriptionStatus: "trial", subscriptionPaidUntil: null, trialStartedAt: "2026-01-01T00:00:00.000Z", trialEndsAt: "2100-01-01T00:00:00.000Z", status: "active", allowsGuestBooking: true, advertisingEnabled: true, active: true, createdAt: null, updatedAt: null }, { id: "directory-waka-starter-west-link", ownerProfileId: null, businessAccountId: null, companyName: "Waka Starter West Link", slug: "waka-starter-westlink", description: "Starter operator record kept visible in the public planner until more live agencies onboard.", supportEmail: "starter-west@wakagood.com", supportPhone: "+237670000404", terminalAddress: "Total Tamdja terminal, Bafoussam", hqCity: "Bafoussam", operatedCities: ["Bafoussam", "Douala", "Yaounde", "Bamenda", "Mbouda"], mtnMomoName: "Waka Starter West Link", mtnMomoNumber: "+237670000404", orangeMoneyName: "Waka Starter West Link", orangeMoneyNumber: "+237690000404", boardingPolicy: "Arrive at least one hour before departure with your booking reference.", monthlyFeeXaf: 25000, subscriptionStatus: "trial", subscriptionPaidUntil: null, trialStartedAt: "2026-01-01T00:00:00.000Z", trialEndsAt: "2100-01-01T00:00:00.000Z", status: "active", allowsGuestBooking: true, advertisingEnabled: true, active: true, createdAt: null, updatedAt: null } ]; function intercityAgencyDefaultTrialEndsAt() { return "2100-01-01T00:00:00.000Z"; } function normalizeIntercityBoolean(value, fallback = false) { if (value === undefined || value === null || value === "") return fallback; if (typeof value === "boolean") return value; if (typeof value === "number") return value !== 0; const normalized = String(value).trim().toLowerCase(); if (["true", "1", "yes", "y", "on"].includes(normalized)) return true; if (["false", "0", "no", "n", "off"].includes(normalized)) return false; return fallback; } function normalizeIntercityAgencyRecord(record = {}) { const source = record && typeof record === "object" ? record : {}; const operatedCities = Array.isArray(source.operatedCities) ? source.operatedCities : (Array.isArray(source.operated_cities) ? source.operated_cities : []); const ownerProfileId = source.ownerProfileId ?? source.owner_profile_id ?? null; const directoryKey = String(source.id ?? source.slug ?? source.companyName ?? source.company_name ?? ""); const inferredDirectoryListing = directoryKey.startsWith("directory-") || directoryKey.endsWith("-directory"); const publicDirectoryListed = normalizeIntercityBoolean( source.publicDirectoryListed ?? source.public_directory_listed ?? source.directoryListed ?? source.directory_listed, inferredDirectoryListing ); return { id: source.id || source.slug || source.companyName || source.company_name || crypto.randomUUID(), ownerProfileId, businessAccountId: source.businessAccountId ?? source.business_account_id ?? null, companyName: String(source.companyName ?? source.company_name ?? "").trim(), slug: String(source.slug ?? "").trim(), description: String(source.description ?? "").trim(), logoPath: String(source.logoPath ?? source.logo_path ?? "").trim(), logoUrl: String(source.logoUrl ?? source.logo_url ?? "").trim(), logoAltText: String(source.logoAltText ?? source.logo_alt_text ?? "").trim(), supportEmail: String(source.supportEmail ?? source.support_email ?? "").trim(), supportPhone: String(source.supportPhone ?? source.support_phone ?? "").trim(), terminalAddress: String(source.terminalAddress ?? source.terminal_address ?? "").trim(), hqCity: String(source.hqCity ?? source.hq_city ?? "").trim(), operatedCities: [...new Set(operatedCities.map((value) => String(value || "").trim()).filter(Boolean))], mtnMomoName: String(source.mtnMomoName ?? source.mtn_momo_name ?? "").trim(), mtnMomoNumber: String(source.mtnMomoNumber ?? source.mtn_momo_number ?? "").trim(), orangeMoneyName: String(source.orangeMoneyName ?? source.orange_money_name ?? "").trim(), orangeMoneyNumber: String(source.orangeMoneyNumber ?? source.orange_money_number ?? "").trim(), boardingPolicy: String(source.boardingPolicy ?? source.boarding_policy ?? "").trim(), monthlyFeeXaf: Number(source.monthlyFeeXaf ?? source.monthly_fee_xaf ?? 25000) || 25000, subscriptionStatus: String(source.subscriptionStatus ?? source.subscription_status ?? "trial").trim() || "trial", subscriptionPaidUntil: source.subscriptionPaidUntil ?? source.subscription_paid_until ?? null, trialStartedAt: source.trialStartedAt ?? source.trial_started_at ?? null, trialEndsAt: source.trialEndsAt ?? source.trial_ends_at ?? intercityAgencyDefaultTrialEndsAt(), status: String(source.status ?? "active").trim() || "active", allowsGuestBooking: source.allowsGuestBooking ?? source.allows_guest_booking ?? true, advertisingEnabled: source.advertisingEnabled ?? source.advertising_enabled ?? true, publicDirectoryListed, claimRequired: normalizeIntercityBoolean(source.claimRequired ?? source.claim_required, publicDirectoryListed && !ownerProfileId), active: source.active !== false, createdAt: source.createdAt ?? source.created_at ?? null, updatedAt: source.updatedAt ?? source.updated_at ?? null }; } function intercityAgencyLogoBucketName() { return String(appConfig.buckets?.agencyLogos || "agency-logos").trim() || "agency-logos"; } function intercityAgencyPromotionBucketName() { return String(appConfig.buckets?.agencyPromotions || "agency-promotions").trim() || "agency-promotions"; } function intercityStoragePublicUrl(bucket, path) { const safePath = String(path || "").trim(); if (!safePath) return ""; try { const publicUrl = supabaseClient?.storage?.from(bucket)?.getPublicUrl(safePath)?.data?.publicUrl; if (publicUrl) return publicUrl; } catch { // Fall through to deterministic public object URL construction. } const baseUrl = String(appConfig.supabaseUrl || "").replace(/\/+$/, ""); if (!baseUrl) return ""; return `${baseUrl}/storage/v1/object/public/${encodeURIComponent(bucket)}/${safePath.split("/").map(encodeURIComponent).join("/")}`; } function intercityAgencyLogoUrl(agency) { const explicitUrl = String(agency?.logoUrl ?? agency?.logo_url ?? "").trim(); if (explicitUrl) return explicitUrl; const logoPath = String(agency?.logoPath ?? agency?.logo_path ?? "").trim(); return logoPath ? intercityStoragePublicUrl(intercityAgencyLogoBucketName(), logoPath) : ""; } function intercityAgencyLogoAlt(agency) { return String(agency?.logoAltText ?? agency?.logo_alt_text ?? "").trim() || `${String(agency?.companyName ?? agency?.company_name ?? "Transport agency").trim() || "Transport agency"} logo`; } function intercityAgencyInitials(agency) { const name = String(agency?.companyName ?? agency?.company_name ?? "Agency").trim(); return name .split(/\s+/) .map((part) => part[0] || "") .join("") .slice(0, 3) .toUpperCase() || "AG"; } function intercityAgencyLogoMarkup(agency, className = "agency-logo-mark") { const logoUrl = intercityAgencyLogoUrl(agency); const alt = intercityAgencyLogoAlt(agency); if (logoUrl) { return `${escapeHtml(alt)}`; } return ``; } function safeIntercityLogoFileName(name) { return String(name || "agency-logo") .replace(/[^a-z0-9._-]/gi, "-") .replace(/-+/g, "-") .toLowerCase() .slice(0, 120) || "agency-logo"; } function safeIntercityPromotionFileName(name) { return String(name || "agency-promotion") .replace(/[^a-z0-9._-]/gi, "-") .replace(/-+/g, "-") .toLowerCase() .slice(0, 120) || "agency-promotion"; } function validateIntercityPromotionImageFile(file) { if (!file) return; const allowedTypes = new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]); if (!allowedTypes.has(String(file.type || "").toLowerCase())) { throw new Error("Upload a JPG, PNG, WebP, or GIF promotion image."); } if (file.size > 5 * 1024 * 1024) { throw new Error("Promotion image must be 5 MB or smaller."); } } function validateIntercityAgencyLogoFile(file) { if (!file) return; const allowedTypes = new Set(["image/jpeg", "image/png", "image/webp", "image/svg+xml"]); if (!allowedTypes.has(String(file.type || "").toLowerCase())) { throw new Error("Upload a PNG, JPG, WebP, or SVG company logo."); } if (file.size > 5 * 1024 * 1024) { throw new Error("Company logo must be 5 MB or smaller."); } } async function uploadIntercityAgencyLogo(agencyId, file) { if (!file) return ""; if (!hasSupabaseRuntime()) throw new Error("Agency logo upload requires Supabase storage."); validateIntercityAgencyLogoFile(file); const path = `${agencyId}/logo-${Date.now()}-${safeIntercityLogoFileName(file.name)}`; const { error } = await supabaseClient.storage .from(intercityAgencyLogoBucketName()) .upload(path, file, { upsert: false, contentType: file.type || "application/octet-stream" }); if (error) throw error; return path; } async function uploadIntercityAgencyPromotionImage(agencyId, file) { if (!file) return ""; if (!hasSupabaseRuntime()) throw new Error("Promotion image upload requires Supabase storage."); validateIntercityPromotionImageFile(file); const path = `${agencyId}/promotion-${Date.now()}-${safeIntercityPromotionFileName(file.name)}`; const { error } = await supabaseClient.storage .from(intercityAgencyPromotionBucketName()) .upload(path, file, { upsert: false, contentType: file.type || "application/octet-stream" }); if (error) throw error; return path; } function intercityPromotionImageUrl(promotion) { const explicitUrl = String(promotion?.imageUrl ?? promotion?.image_url ?? "").trim(); if (explicitUrl) return explicitUrl; const imagePath = String(promotion?.imagePath ?? promotion?.image_path ?? "").trim(); return imagePath ? intercityStoragePublicUrl(intercityAgencyPromotionBucketName(), imagePath) : ""; } function intercityPromotionImageAlt(promotion, agency = null) { return String(promotion?.imageAltText ?? promotion?.image_alt_text ?? "").trim() || String(promotion?.publicTitle ?? promotion?.public_title ?? promotion?.campaignName ?? promotion?.campaign_name ?? "").trim() || `${String(agency?.companyName ?? agency?.company_name ?? "Transport agency").trim() || "Transport agency"} promotion`; } function safePublicPromotionUrl(value) { const raw = String(value || "").trim(); if (!raw) return ""; try { const parsed = new URL(raw, window.location.origin); return ["http:", "https:"].includes(parsed.protocol) ? parsed.toString() : ""; } catch { return ""; } } function updateIntercityAgencyLogoPreview(agency = null) { if (!els.intercityAgencyLogoPreview) return; const file = els.intercityAgencyLogo?.files?.[0] ?? null; const previewUrl = file ? URL.createObjectURL(file) : intercityAgencyLogoUrl(agency); if (previewUrl) { els.intercityAgencyLogoPreview.innerHTML = ``; } else { els.intercityAgencyLogoPreview.textContent = agency ? intercityAgencyInitials(agency) : "Logo"; } if (els.intercityAgencyLogoStatus) { if (file) { els.intercityAgencyLogoStatus.textContent = `Selected logo: ${file.name}. It will be visible on the agency platform, booking receipts, messages, and reports after saving.`; } else if (agency?.logoPath || agency?.logoUrl) { els.intercityAgencyLogoStatus.textContent = "Company logo saved and visible on public cards, agency dashboards, booking receipts, messages, and reports."; } else { els.intercityAgencyLogoStatus.textContent = "Optional. The logo appears on public search cards, agency dashboards, booking receipts, messages, and trip reports."; } } } async function saveIntercityAgencyLogoMetadata(agency, file, logoAltText) { if (!agency?.id || !file) return agency; const logoPath = await uploadIntercityAgencyLogo(agency.id, file); const logoUrl = intercityStoragePublicUrl(intercityAgencyLogoBucketName(), logoPath); const rows = await supabaseRestRequest(`/rest/v1/cameroon_intercity_agencies?id=eq.${encodeURIComponent(agency.id)}`, { method: "PATCH", body: { logo_path: logoPath, logo_url: logoUrl, logo_alt_text: logoAltText || `${agency.companyName || "Transport agency"} logo`, updated_at: new Date().toISOString() }, headers: { Prefer: "return=representation" } }); return mapIntercityAgencyFromDatabase(Array.isArray(rows) ? rows[0] : rows); } function intercityBookingFunctionName() { return String(appConfig.intercityBookingFunctionName || "intercity-booking-submit").trim() || "intercity-booking-submit"; } function intercityOperatorActionFunctionName() { return String(appConfig.intercityOperatorActionFunctionName || "intercity-operator-action").trim() || "intercity-operator-action"; } function agencyPlatformPaymentStartFunctionName() { return String(appConfig.agencyPlatformPaymentStartFunctionName || "agency-platform-payment-start").trim() || "agency-platform-payment-start"; } function slugifyIntercityAgency(value) { return String(value ?? "") .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 80); } function mapIntercityAgencyFromDatabase(row = {}) { return normalizeIntercityAgencyRecord(row); } function mapIntercityDepartureFromDatabase(row = {}) { return { id: row.id, agencyId: row.agency_id, routeId: row.route_id ?? null, busLabel: row.bus_label ?? "", originCity: row.origin_city ?? "", destinationCity: row.destination_city ?? "", boardingLocation: row.boarding_location ?? "", dropoffLocation: row.dropoff_location ?? "", departureAt: row.departure_at ?? null, checkInDeadlineMinutes: Number(row.check_in_deadline_minutes ?? 60) || 60, estimatedDurationMinutes: Number(row.estimated_duration_minutes ?? 0) || 0, seatCapacity: Number(row.seat_capacity ?? 0) || 0, reservedSeats: Number(row.reserved_seats ?? 0) || 0, blockedSeatNumbers: Array.isArray(row.blocked_seat_numbers) ? row.blocked_seat_numbers.map((value) => Number(value) || 0).filter((value) => value > 0) : [], occupiedSeatNumbers: Array.isArray(row.occupied_seat_numbers) ? row.occupied_seat_numbers.map((value) => Number(value) || 0).filter((value) => value > 0) : [], fareXaf: Number(row.fare_xaf ?? 0) || 0, acceptsMtnMomo: row.accepts_mtn_momo !== false, acceptsOrangeMoney: row.accepts_orange_money !== false, acceptsPayLater: row.accepts_pay_later !== false, travelStatus: row.travel_status ?? "scheduled", statusNote: row.status_note ?? "", notes: row.notes ?? "", active: row.active !== false, createdAt: row.created_at ?? null, updatedAt: row.updated_at ?? null }; } function mapIntercityBookingFromDatabase(row = {}) { return { id: row.id, departureId: row.departure_id, agencyId: row.agency_id, passengerProfileId: row.passenger_profile_id ?? null, bookingReference: row.booking_reference ?? "", travelerName: row.traveler_name ?? "", travelerDateOfBirth: row.traveler_date_of_birth ?? "", travelerIdentityNumber: row.traveler_identity_number ?? "", travelerEmail: row.traveler_email ?? "", travelerPhone: row.traveler_phone ?? "", seatCount: Number(row.seat_count ?? 0) || 0, seatNumbers: Array.isArray(row.seat_numbers) ? row.seat_numbers.map((value) => Number(value) || 0).filter((value) => value > 0) : [], amountXaf: Number(row.amount_xaf ?? 0) || 0, paymentMethod: row.payment_method ?? "pay_later", payerPhone: row.payer_phone ?? "", paymentStatus: row.payment_status ?? "pending", bookingStatus: row.booking_status ?? "reserved", operatorNote: row.operator_note ?? "", cancelledByRole: row.cancelled_by_role ?? "", paymentConfirmedAt: row.payment_confirmed_at ?? null, mobileMoneyPaymentIntentId: row.mobile_money_payment_intent_id ?? null, paymentProvider: row.payment_provider ?? "", paymentProviderReference: row.payment_provider_reference ?? "", paymentProviderStatus: row.payment_provider_status ?? "", paymentProviderMessage: row.payment_provider_message ?? "", paymentCheckoutUrl: row.payment_checkout_url ?? "", paymentCallbackReceivedAt: row.payment_callback_received_at ?? null, agencySettlementStatus: row.agency_settlement_status ?? "not_started", passengerManifest: Array.isArray(row.passenger_manifest) ? row.passenger_manifest : [], departureSnapshot: row.departure_snapshot && typeof row.departure_snapshot === "object" ? row.departure_snapshot : {}, notes: row.notes ?? "", receiptEmailSentAt: row.receipt_email_sent_at ?? null, receiptDeliveryStatus: row.receipt_delivery_status ?? "pending", createdAt: row.created_at ?? null, updatedAt: row.updated_at ?? null }; } function mapIntercityAdvertisingRequestFromDatabase(row = {}) { return { id: row.id, agencyId: row.agency_id ?? "", campaignName: row.campaign_name ?? "", publicTitle: row.public_title ?? "", publicBody: row.public_body ?? "", ctaLabel: row.cta_label ?? "", ctaUrl: row.cta_url ?? "", imagePath: row.image_path ?? "", imageAltText: row.image_alt_text ?? "", publishStartAt: row.publish_start_at ?? null, publishEndAt: row.publish_end_at ?? null, publicActive: row.public_active !== false, contentUpdatedAt: row.content_updated_at ?? null, placement: row.placement ?? "website_banner", durationDays: Number(row.duration_days ?? 7) || 7, amountXaf: Number(row.amount_xaf ?? 0) || 0, provider: row.provider ?? "mtn_momo", payerName: row.payer_name ?? "", payerPhone: row.payer_phone ?? "", providerReference: row.provider_reference ?? "", note: row.note ?? "", status: row.status ?? "pending_review", reviewedBy: row.reviewed_by ?? null, reviewedAt: row.reviewed_at ?? null, createdAt: row.created_at ?? null, updatedAt: row.updated_at ?? null }; } function mapIntercityBookingMessageFromDatabase(row = {}) { return { id: row.id, bookingId: row.booking_id ?? "", departureId: row.departure_id ?? "", agencyId: row.agency_id ?? "", passengerProfileId: row.passenger_profile_id ?? null, senderProfileId: row.sender_profile_id ?? null, senderRole: row.sender_role ?? "operator", messageBody: row.message_body ?? "", deliveryChannels: Array.isArray(row.delivery_channels) ? row.delivery_channels.filter(Boolean) : [], emailDeliveryStatus: row.email_delivery_status ?? "pending", visibleToPassenger: row.visible_to_passenger !== false, createdAt: row.created_at ?? null, updatedAt: row.updated_at ?? null, messageKind: "booking" }; } function mapIntercityAgencyMessageFromDatabase(row = {}) { return { id: row.id, agencyId: row.agency_id ?? "", senderProfileId: row.sender_profile_id ?? null, senderRole: row.sender_role ?? "admin", subject: row.subject ?? "", messageBody: row.message_body ?? "", deliveryChannels: Array.isArray(row.delivery_channels) ? row.delivery_channels.filter(Boolean) : [], emailDeliveryStatus: row.email_delivery_status ?? "pending", visibleToAgency: row.visible_to_agency !== false, createdAt: row.created_at ?? null, updatedAt: row.updated_at ?? null, messageKind: "agency" }; } function mapIntercityAgencyPaymentFromDatabase(row = {}) { return { id: row.id, agencyId: row.agency_id, amountXaf: Number(row.amount_xaf ?? 25000) || 25000, billingPeriodStart: row.billing_period_start ?? "", billingPeriodEnd: row.billing_period_end ?? "", provider: row.provider ?? "mtn_momo", payerName: row.payer_name ?? "", payerPhone: row.payer_phone ?? "", providerReference: row.provider_reference ?? "", mobileMoneyPaymentIntentId: row.mobile_money_payment_intent_id ?? null, paymentProviderStatus: row.payment_provider_status ?? "", paymentProviderMessage: row.payment_provider_message ?? "", paymentCheckoutUrl: row.payment_checkout_url ?? "", paymentCallbackReceivedAt: row.payment_callback_received_at ?? null, paymentConfirmedAt: row.payment_confirmed_at ?? null, status: row.status ?? "pending_review", reviewNote: row.review_note ?? "", reviewedBy: row.reviewed_by ?? null, reviewedAt: row.reviewed_at ?? null, createdAt: row.created_at ?? null, updatedAt: row.updated_at ?? null }; } function intercityAgencyRecords() { return (Array.isArray(state.intercityAgencies) ? state.intercityAgencies : []) .map((agency) => normalizeIntercityAgencyRecord(agency)) .filter((agency) => agency.companyName || agency.id); } function mergeIntercityAgencyDirectory(agencies = []) { let merged = cameroonFallbackIntercityAgencies.map((agency) => normalizeIntercityAgencyRecord(agency)); (Array.isArray(agencies) ? agencies : []).forEach((agency) => { merged = upsertById(merged, normalizeIntercityAgencyRecord(agency)); }); return merged.sort((a, b) => String(a.companyName || "").localeCompare(String(b.companyName || ""))); } function intercityDepartureRecords() { return Array.isArray(state.intercityDepartures) ? state.intercityDepartures : []; } function intercityBookingRecords() { return Array.isArray(state.intercityBookings) ? state.intercityBookings : []; } function intercityAgencyPaymentRecords() { return Array.isArray(state.intercityAgencyPayments) ? state.intercityAgencyPayments : []; } function intercityAdvertisingRequestRecords() { return Array.isArray(state.intercityAdvertisingRequests) ? state.intercityAdvertisingRequests : []; } function intercityBookingMessageRecords() { return Array.isArray(state.intercityBookingMessages) ? state.intercityBookingMessages : []; } function intercityAgencyMessageRecords() { return Array.isArray(state.intercityAgencyMessages) ? state.intercityAgencyMessages : []; } function selectedIntercityAgencyCities() { const selected = [...(els.intercityAgencyCitiesSelect?.selectedOptions ?? [])] .map((option) => option.value.trim()) .filter(Boolean); const custom = String(els.intercityAgencyCitiesCustom?.value ?? "") .split(",") .map((value) => value.trim()) .filter(Boolean); return [...new Set(selected.concat(custom))].sort((a, b) => a.localeCompare(b)); } function setSelectedIntercityAgencyCities(cities = []) { const normalized = [...new Set((Array.isArray(cities) ? cities : []).map((value) => String(value || "").trim()).filter(Boolean))]; if (els.intercityAgencyCitiesSelect) { [...els.intercityAgencyCitiesSelect.options].forEach((option) => { option.selected = normalized.includes(option.value); }); } if (els.intercityAgencyCitiesCustom) { els.intercityAgencyCitiesCustom.value = normalized.filter((city) => !cameroonIntercityCities.includes(city)).join(", "); } } function populateCameroonIntercityCityOptions() { const cities = [...new Set(cameroonIntercityCities.concat(intercityPublicCities()))] .filter(Boolean) .sort((a, b) => a.localeCompare(b)); if (els.cameroonIntercityCityOptions) { els.cameroonIntercityCityOptions.innerHTML = ""; cities.forEach((city) => { const option = document.createElement("option"); option.value = city; els.cameroonIntercityCityOptions.append(option); }); } if (els.intercityAgencyCitiesSelect) { const selectedCities = selectedIntercityAgencyCities(); els.intercityAgencyCitiesSelect.innerHTML = ""; cities.forEach((city) => { const option = document.createElement("option"); option.value = city; option.textContent = city; option.selected = selectedCities.includes(city); els.intercityAgencyCitiesSelect.append(option); }); } } function intercityAgencyCanOperate(agency) { const normalized = normalizeIntercityAgencyRecord(agency); return Boolean(normalized.companyName && normalized.active !== false && normalized.status === "active"); } function intercityAgencyIsSearchable(agency) { const normalized = normalizeIntercityAgencyRecord(agency); if (!normalized.companyName || normalized.active === false || normalized.status === "suspended") return false; return intercityAgencyCanOperate(normalized) || normalized.publicDirectoryListed === true; } function intercityAgencyServiceStatus(agency) { if (!agency) return translatedValue("notAvailable") || "Not available"; if (agency.active === false || agency.status === "suspended") return translatedValue("suspended") || "Suspended"; if (agency.publicDirectoryListed && !intercityAgencyCanOperate(agency)) return "Public directory listing. Departures pending."; if (agency.status === "pending_review") return translatedValue("pendingReview") || "Pending review"; if (agency.subscriptionStatus === "trial") { const ends = agency.trialEndsAt ? formatDateTime(agency.trialEndsAt) : ""; return ends ? `Trial until ${ends}` : "Trial active"; } if (agency.subscriptionPaidUntil) return `Paid until ${formatDateTime(agency.subscriptionPaidUntil)}`; if (agency.subscriptionStatus === "past_due") return "Payment due"; return agency.subscriptionStatus || "Active"; } function availableSeatsForIntercityDeparture(departure) { if (!departure) return 0; const blockedCount = Array.isArray(departure.blockedSeatNumbers) ? departure.blockedSeatNumbers.length : 0; return Math.max(0, (Number(departure.seatCapacity ?? 0) || 0) - (Number(departure.reservedSeats ?? 0) || 0) - blockedCount); } function intercityDepartureBookings(departureId) { return intercityBookingRecords().filter((booking) => booking.departureId === departureId); } function normalizeIntercityDuplicateText(value) { return String(value || "").replace(/\s+/g, " ").trim().toLowerCase(); } function intercityDepartureDuplicateMatchesPayload(departure, payload) { if (!departure || !payload) return false; const departureTime = new Date(String(departure.departureAt || "")).getTime(); const payloadTime = new Date(String(payload.departure_at || "")).getTime(); if (!Number.isFinite(departureTime) || !Number.isFinite(payloadTime)) return false; return departure.agencyId === payload.agency_id && Math.abs(departureTime - payloadTime) < 1000 && normalizeIntercityDuplicateText(departure.busLabel) === normalizeIntercityDuplicateText(payload.bus_label) && normalizeIntercityDuplicateText(departure.originCity) === normalizeIntercityDuplicateText(payload.origin_city) && normalizeIntercityDuplicateText(departure.destinationCity) === normalizeIntercityDuplicateText(payload.destination_city) && normalizeIntercityDuplicateText(departure.boardingLocation) === normalizeIntercityDuplicateText(payload.boarding_location) && departure.active !== false && !["cancelled", "completed"].includes(String(departure.travelStatus || "").toLowerCase()); } async function findExistingIntercityDepartureDuplicate(payload) { const localDuplicate = intercityDepartureRecords().find((departure) => intercityDepartureDuplicateMatchesPayload(departure, payload)); if (localDuplicate) return localDuplicate; if (!hasSupabaseRuntime()) return null; const agencyId = encodeURIComponent(payload.agency_id || ""); const departureAt = encodeURIComponent(payload.departure_at || ""); if (!agencyId || !departureAt) return null; const rows = await supabaseRestRequest(`/rest/v1/cameroon_intercity_departures?agency_id=eq.${agencyId}&departure_at=eq.${departureAt}&select=*&order=created_at.desc&limit=10`) .catch((error) => { logClientWarning("Could not check for duplicate inter-city departure before save.", error); return []; }); return (Array.isArray(rows) ? rows : []) .map(mapIntercityDepartureFromDatabase) .find((departure) => intercityDepartureDuplicateMatchesPayload(departure, payload)) || null; } async function verifySavedIntercityDepartureFromSupabase(departureId) { if (!departureId || !hasSupabaseRuntime()) return null; const rows = await supabaseRestRequest(`/rest/v1/cameroon_intercity_departures?id=eq.${encodeURIComponent(departureId)}&select=*&limit=1`); return Array.isArray(rows) && rows[0] ? mapIntercityDepartureFromDatabase(rows[0]) : null; } function intercityActiveSeatNumbers(departureId) { const used = new Set(); intercityDepartureBookings(departureId) .filter((booking) => !["cancelled", "expired"].includes(booking.bookingStatus)) .forEach((booking) => { (booking.seatNumbers || []).forEach((seat) => { if (seat > 0) used.add(seat); }); }); return used; } function intercityDisplayOccupiedSeatNumbers(departure) { const blocked = intercityBlockedSeatSet(departure); const occupied = new Set(Array.isArray(departure?.occupiedSeatNumbers) ? departure.occupiedSeatNumbers.filter((value) => value > 0) : []); intercityActiveSeatNumbers(departure?.id).forEach((seat) => occupied.add(seat)); const targetCount = Math.max(0, Number(departure?.reservedSeats ?? 0) || 0); if (occupied.size >= targetCount) return occupied; for (let seat = 1; seat <= Number(departure?.seatCapacity ?? 0); seat += 1) { if (blocked.has(seat) || occupied.has(seat)) continue; occupied.add(seat); if (occupied.size >= targetCount) break; } return occupied; } function intercityPaidSeatNumbers(departureId) { const paid = new Set(); intercityDepartureBookings(departureId) .filter((booking) => booking.paymentStatus === "paid") .forEach((booking) => { (booking.seatNumbers || []).forEach((seat) => { if (seat > 0) paid.add(seat); }); }); return paid; } function intercityBlockedSeatSet(departure) { return new Set(Array.isArray(departure?.blockedSeatNumbers) ? departure.blockedSeatNumbers.filter((value) => value > 0) : []); } function intercityAvailableSeatNumbers(departure) { if (!departure?.seatCapacity) return []; const blocked = intercityBlockedSeatSet(departure); const occupied = intercityDisplayOccupiedSeatNumbers(departure); const seats = []; for (let seat = 1; seat <= Number(departure.seatCapacity || 0); seat += 1) { if (blocked.has(seat) || occupied.has(seat)) continue; seats.push(seat); } return seats; } function intercityBusClassLabel(seatCapacity) { const capacity = Number(seatCapacity ?? 0) || 0; if (capacity >= 60) return "Coach"; if (capacity >= 30) return "Mid-size bus"; if (capacity >= 15) return "Mini bus"; if (capacity >= 1) return "Shuttle"; return "Bus"; } function intercityCapacityLabel(departure) { const capacity = Number(departure?.seatCapacity ?? 0) || 0; if (!capacity) return "Capacity not set"; return `${capacity}-seat ${intercityBusClassLabel(capacity).toLowerCase()}`; } function intercityBlockedSeatLabel(departure) { const blockedCount = Array.isArray(departure?.blockedSeatNumbers) ? departure.blockedSeatNumbers.length : 0; if (!blockedCount) return "No held seats"; return `${blockedCount} seat${blockedCount === 1 ? "" : "s"} held offline / unavailable`; } function parseIntercitySeatNumberList(value, max = 200) { const limit = Math.max(0, Number(max) || 0); const seen = new Set(); String(value || "").split(/[\s,;]+/).forEach((token) => { const rangeMatch = token.match(/^(\d+)-(\d+)$/); if (rangeMatch) { const start = Number(rangeMatch[1]) || 0; const end = Number(rangeMatch[2]) || 0; for (let seat = Math.min(start, end); seat <= Math.max(start, end); seat += 1) { if (seat >= 1 && seat <= limit) seen.add(seat); } return; } const seat = Number(token) || 0; if (seat >= 1 && seat <= limit) seen.add(Math.floor(seat)); }); return [...seen].sort((a, b) => a - b); } async function manageIntercityDepartureSeatAvailability(departure, save) { if (!departure?.id || typeof save !== "function") return; const currentSeats = (departure.blockedSeatNumbers || []).join(", "); const seatResponse = await showWakaGoodPrompt( "Enter seat numbers to mark unavailable or already paid in person, separated by commas. Leave blank to release all held seats.", currentSeats ); if (seatResponse === null) return; const blockedSeatNumbers = parseIntercitySeatNumberList(seatResponse, departure.seatCapacity || 200); const noteResponse = await showWakaGoodPrompt( "Optional availability note shown with this departure, for example: Seats 1-4 paid at terminal.", departure.statusNote || "" ); if (noteResponse === null) return; await save({ blockedSeatNumbers, statusNote: String(noteResponse || "").trim() }); } function renderIntercitySeatMap(container, departure, { highlightSeats = [], selectable = false, selectedSeats = [], onSeatToggle = null } = {}) { if (!container) return; container.innerHTML = ""; if (!departure || !departure.seatCapacity) { container.hidden = true; return; } container.hidden = false; const occupied = intercityDisplayOccupiedSeatNumbers(departure); const paid = intercityPaidSeatNumbers(departure.id); const blocked = intercityBlockedSeatSet(departure); const highlighted = new Set((Array.isArray(highlightSeats) ? highlightSeats : []).filter((value) => value > 0)); const selected = new Set((Array.isArray(selectedSeats) ? selectedSeats : []).filter((value) => value > 0)); for (let seat = 1; seat <= departure.seatCapacity; seat += 1) { const unavailable = blocked.has(seat) || occupied.has(seat) || paid.has(seat); const chip = document.createElement(selectable && !unavailable ? "button" : "span"); chip.className = "intercity-seat-chip"; if (chip.tagName === "BUTTON") { chip.type = "button"; chip.setAttribute("aria-pressed", selected.has(seat) ? "true" : "false"); chip.setAttribute("aria-label", `Seat ${seat}`); chip.addEventListener("click", () => { if (typeof onSeatToggle === "function") onSeatToggle(seat); }); } else if (selectable) { chip.setAttribute("aria-label", `Seat ${seat} unavailable`); } if (selectable && !unavailable) chip.classList.add("selectable"); if (selected.has(seat)) chip.classList.add("selected"); if (blocked.has(seat)) chip.classList.add("blocked"); else if (highlighted.has(seat)) chip.classList.add("highlight"); else if (paid.has(seat)) chip.classList.add("paid"); else if (occupied.has(seat)) chip.classList.add("reserved"); chip.textContent = String(seat); container.append(chip); } } function intercityMessagesForBooking(bookingId) { return intercityBookingMessageRecords() .filter((message) => message.bookingId === bookingId && message.visibleToPassenger !== false) .sort((a, b) => new Date(a.createdAt ?? 0) - new Date(b.createdAt ?? 0)); } function intercityXafLabel(amountXaf) { return `${(Number(amountXaf || 0) || 0).toLocaleString("en-US")} FCFA`; } function intercityPassengerManifestEntries(booking) { const manifest = Array.isArray(booking?.passengerManifest) ? booking.passengerManifest : []; if (manifest.length) { return manifest.map((entry, index) => ({ seatNumber: Number(entry.seatNumber ?? entry.seat_number ?? booking.seatNumbers?.[index] ?? 0) || 0, travelerName: String(entry.travelerName ?? entry.name ?? "").trim(), dateOfBirth: String(entry.dateOfBirth ?? entry.date_of_birth ?? "").trim(), identityNumber: String(entry.identityNumber ?? entry.identity_number ?? entry.nationalIdNumber ?? "").trim() })).filter((entry) => entry.travelerName || entry.dateOfBirth || entry.identityNumber || entry.seatNumber); } return [{ seatNumber: booking?.seatNumbers?.[0] || 0, travelerName: booking?.travelerName || "", dateOfBirth: booking?.travelerDateOfBirth || "", identityNumber: booking?.travelerIdentityNumber || "" }]; } function intercityPassengerManifestSummary(booking) { return intercityPassengerManifestEntries(booking).map((entry) => { const parts = [ entry.seatNumber ? `Seat ${entry.seatNumber}` : "", entry.travelerName || "Name required", entry.dateOfBirth ? `DOB ${entry.dateOfBirth}` : "DOB required", entry.identityNumber ? `ID ${entry.identityNumber}` : "physical ID inspection required" ].filter(Boolean); return parts.join(" | "); }).join("; "); } function activeIntercityReportBookings(bookings) { return bookings.filter((booking) => !["cancelled", "expired"].includes(String(booking.bookingStatus || ""))); } function buildIntercityOperatorTripReports(departures, bookings) { const activeBookings = activeIntercityReportBookings(bookings); return departures.map((departure) => { const departureBookings = activeBookings .filter((booking) => booking.departureId === departure.id) .sort((a, b) => { const aSeat = a.seatNumbers.length ? Math.min(...a.seatNumbers) : Number.MAX_SAFE_INTEGER; const bSeat = b.seatNumbers.length ? Math.min(...b.seatNumbers) : Number.MAX_SAFE_INTEGER; if (aSeat !== bSeat) return aSeat - bSeat; return new Date(a.createdAt || 0) - new Date(b.createdAt || 0); }); const paidBookings = departureBookings.filter((booking) => booking.paymentStatus === "paid"); const pendingBookings = departureBookings.filter((booking) => booking.paymentStatus !== "paid"); const sumAmount = (items) => items.reduce((total, booking) => total + (Number(booking.amountXaf || 0) || 0), 0); const bookedSeats = departureBookings.reduce((total, booking) => ( total + (booking.seatNumbers.length || Number(booking.seatCount || 0) || 0) ), 0); return { id: departure.id, agencyId: departure.agencyId, routeLabel: `${departure.originCity || ""} -> ${departure.destinationCity || ""}`, busLabel: departure.busLabel || intercityBusClassLabel(departure.seatCapacity), departureAt: departure.departureAt, travelStatus: departure.travelStatus || "scheduled", active: departure.active !== false, seatCapacity: Number(departure.seatCapacity || 0) || 0, bookedSeats, availableSeats: availableSeatsForIntercityDeparture(departure), bookingCount: departureBookings.length, paidBookings: paidBookings.length, pendingBookings: pendingBookings.length, totalAmountXaf: sumAmount(departureBookings), paidAmountXaf: sumAmount(paidBookings), pendingAmountXaf: sumAmount(pendingBookings), bookings: departureBookings }; }).sort((a, b) => new Date(a.departureAt || 0) - new Date(b.departureAt || 0)); } function intercityOperatorTabLabel(tab) { const labels = { profile: translatedValue("operatorTabProfile") || "Profile", departures: translatedValue("operatorTabDepartures") || "Departures", bookings: translatedValue("operatorTabBookings") || "Bookings", reports: translatedValue("operatorTabReports") || "Reports", payments: translatedValue("operatorTabPayments") || "Payments", advertising: translatedValue("operatorTabAdvertising") || "Advertising", messages: translatedValue("operatorTabMessages") || "Messages" }; return labels[tab] || "Profile"; } function intercityOperatorStatusOptions(tab) { const allItems = translatedValue("operatorFilterAllItems") || "All items"; const all = [{ value: "all", label: allItems }]; if (tab === "profile") { return all.concat([ { value: "active", label: translatedValue("operatorFilterActive") || "Active" }, { value: "trial", label: translatedValue("operatorFilterTrial") || "Trial" }, { value: "payment_due", label: translatedValue("operatorFilterPaymentDue") || "Payment due" }, { value: "pending_review", label: translatedValue("operatorFilterPendingReview") || "Pending review" }, { value: "suspended", label: translatedValue("operatorFilterSuspended") || "Suspended" } ]); } if (tab === "departures") { return all.concat([ { value: "active", label: translatedValue("operatorFilterActive") || "Active" }, { value: "paused", label: translatedValue("operatorFilterPaused") || "Paused" }, { value: "scheduled", label: translatedValue("operatorFilterScheduled") || "Scheduled" }, { value: "boarding", label: translatedValue("operatorFilterBoarding") || "Boarding" }, { value: "departed", label: translatedValue("operatorFilterDeparted") || "Departed" }, { value: "arrived", label: translatedValue("operatorFilterArrived") || "Arrived" }, { value: "completed", label: translatedValue("operatorFilterCompleted") || "Completed" }, { value: "delayed", label: translatedValue("operatorFilterDelayed") || "Delayed" }, { value: "cancelled", label: translatedValue("operatorFilterCancelled") || "Cancelled" } ]); } if (tab === "bookings") { return all.concat([ { value: "pending_payment", label: translatedValue("operatorFilterPendingPayment") || "Pending payment" }, { value: "paid", label: translatedValue("operatorFilterPaid") || "Paid" }, { value: "reserved", label: translatedValue("operatorFilterReserved") || "Reserved" }, { value: "confirmed", label: translatedValue("operatorFilterConfirmed") || "Confirmed" }, { value: "checked_in", label: translatedValue("operatorFilterCheckedIn") || "Checked in" }, { value: "completed", label: translatedValue("operatorFilterCompleted") || "Completed" }, { value: "cancelled", label: translatedValue("operatorFilterCancelled") || "Cancelled" } ]); } if (tab === "messages") { return all.concat([ { value: "passenger", label: translatedValue("operatorFilterFromPassenger") || "From passenger" }, { value: "operator", label: translatedValue("operatorFilterFromOperator") || "From operator" }, { value: "admin", label: translatedValue("operatorFilterFromAdmin") || "From Waka admin" }, { value: "recent", label: translatedValue("operatorFilterRecent") || "Last 24h" } ]); } if (tab === "reports") { return all.concat([ { value: "upcoming", label: translatedValue("operatorFilterUpcoming") || "Upcoming" }, { value: "today", label: translatedValue("operatorFilterToday") || "Today" }, { value: "pending_payment", label: translatedValue("operatorFilterPendingPayment") || "Pending payment" }, { value: "paid", label: translatedValue("operatorFilterPaid") || "Paid" }, { value: "scheduled", label: translatedValue("operatorFilterScheduled") || "Scheduled" }, { value: "boarding", label: translatedValue("operatorFilterBoarding") || "Boarding" }, { value: "departed", label: translatedValue("operatorFilterDeparted") || "Departed" }, { value: "completed", label: translatedValue("operatorFilterCompleted") || "Completed" } ]); } return all.concat([ { value: "pending_review", label: translatedValue("operatorFilterPendingReview") || "Pending review" }, { value: "approved", label: translatedValue("operatorFilterApproved") || "Approved" }, { value: "rejected", label: translatedValue("operatorFilterRejected") || "Rejected" } ]); } function intercityOperatorAgencyMatches(agencyId) { return intercityOperatorAgencyFilterValue === "all" || agencyId === intercityOperatorAgencyFilterValue; } function intercityOperatorSearchMatches(...values) { const query = String(intercityOperatorSearchValue || "").trim().toLowerCase(); if (!query) return true; return values.some((value) => String(value || "").toLowerCase().includes(query)); } function intercityOperatorStatusMatches(tab, item) { const filter = intercityOperatorStatusFilterValue; if (filter === "all") return true; if (tab === "profile") { if (filter === "active") return item.active !== false && item.status === "active"; if (filter === "trial") return item.subscriptionStatus === "trial"; if (filter === "payment_due") return item.subscriptionStatus === "past_due"; if (filter === "pending_review") return item.status === "pending_review"; if (filter === "suspended") return item.active === false || item.status === "suspended"; return true; } if (tab === "departures") { if (filter === "active") return item.active !== false; if (filter === "paused") return item.active === false; return item.travelStatus === filter; } if (tab === "bookings") { if (filter === "pending_payment") return item.paymentStatus !== "paid"; if (filter === "paid") return item.paymentStatus === "paid"; return item.bookingStatus === filter; } if (tab === "messages") { if (filter === "recent") return Date.now() - new Date(item.createdAt || 0).getTime() <= 24 * 60 * 60 * 1000; return item.senderRole === filter; } if (tab === "reports") { const departureTime = new Date(item.departureAt || 0).getTime(); if (filter === "upcoming") return departureTime >= Date.now(); if (filter === "today") return String(item.departureAt || "").slice(0, 10) === new Date().toISOString().slice(0, 10); if (filter === "pending_payment") return item.pendingAmountXaf > 0 || item.pendingBookings > 0; if (filter === "paid") return item.bookingCount > 0 && item.pendingBookings < 1; return item.travelStatus === filter; } return item.status === filter; } function setIntercityOperatorWorkspaceTab(tab) { if (!intercityOperatorWorkspaceTabs.includes(tab)) return; intercityOperatorWorkspaceTab = tab; intercityOperatorStatusFilterValue = "all"; if (els.intercityOperatorStatusFilter) els.intercityOperatorStatusFilter.value = "all"; renderIntercityOperatorPanels(); scrollIntercityOperatorPanelIntoView(); } function setIntercityOperatorAgencyFilter(value) { intercityOperatorAgencyFilterValue = String(value || "all"); renderIntercityOperatorPanels(); } function setIntercityOperatorStatusFilter(value) { intercityOperatorStatusFilterValue = String(value || "all"); renderIntercityOperatorPanels(); } function setIntercityOperatorSearch(value) { intercityOperatorSearchValue = String(value || ""); renderIntercityOperatorPanels(); } function wireIntercityOperatorConsoleControls() { els.intercityOperatorTabs?.querySelectorAll("[data-operator-tab]").forEach((button) => { button.onclick = () => setIntercityOperatorWorkspaceTab(button.dataset.operatorTab); }); if (els.intercityOperatorAgencyFilter) { els.intercityOperatorAgencyFilter.onchange = () => setIntercityOperatorAgencyFilter(els.intercityOperatorAgencyFilter.value); } if (els.intercityOperatorStatusFilter) { els.intercityOperatorStatusFilter.onchange = () => setIntercityOperatorStatusFilter(els.intercityOperatorStatusFilter.value); } if (els.intercityOperatorSearch) { els.intercityOperatorSearch.oninput = () => setIntercityOperatorSearch(els.intercityOperatorSearch.value); } } function scrollIntercityOperatorPanelIntoView() { const panel = [...document.querySelectorAll("[data-operator-panel]")] .find((section) => section.dataset.operatorPanel === intercityOperatorWorkspaceTab); if (!panel || panel.hidden) return; window.setTimeout(() => panel.scrollIntoView({ block: "start", behavior: "smooth" }), 0); } function focusIntercityOperatorBooking(booking) { if (!booking) return; intercityOperatorWorkspaceTab = "bookings"; intercityOperatorStatusFilterValue = "all"; intercityOperatorAgencyFilterValue = booking.agencyId || intercityOperatorAgencyFilterValue; intercityOperatorSearchValue = booking.bookingReference || booking.travelerName || ""; if (els.intercityOperatorSearch) els.intercityOperatorSearch.value = intercityOperatorSearchValue; renderIntercityOperatorPanels(); } function syncIntercityOperatorPanelVisibility() { const showBusinessPanels = Boolean(hasSignedIn("passenger") && passengerWorkspacePage() === "business"); document.querySelectorAll("[data-operator-panel]").forEach((section) => { section.hidden = !showBusinessPanels || section.dataset.operatorPanel !== intercityOperatorWorkspaceTab; }); } function renderIntercityOperatorConsole({ agencies, payments, departures, bookings, reports, advertisingRequests, messages, filteredCounts, nextAction }) { if (!els.intercityOperatorTabs) return; wireIntercityOperatorConsoleControls(); const agencyWorkspace = agencyWorkspaceActive(); const agencyFilterLabel = translatedValue("operatorFilterAllAgencies") || "All agencies"; const searchPlaceholder = translatedValue("operatorSearchPlaceholder") || "Search route, traveler, city, or reference"; const counts = { profile: agencies.length, departures: departures.length, bookings: bookings.length, reports: reports.length, payments: payments.length, advertising: advertisingRequests.length, messages: messages.length }; if (els.intercityOperatorSearch) els.intercityOperatorSearch.placeholder = searchPlaceholder; if (els.intercityOperatorAgencyFilter) { if (agencyWorkspace && agencies.length === 1 && intercityOperatorAgencyFilterValue === "all") { intercityOperatorAgencyFilterValue = agencies[0].id; } const agencyFilterField = els.intercityOperatorAgencyFilter.closest("label"); if (agencyFilterField) agencyFilterField.hidden = agencyWorkspace && agencies.length <= 1; const activeValue = agencies.some((agency) => agency.id === intercityOperatorAgencyFilterValue) ? intercityOperatorAgencyFilterValue : "all"; if (activeValue !== intercityOperatorAgencyFilterValue) intercityOperatorAgencyFilterValue = activeValue; populateSelectOptions( els.intercityOperatorAgencyFilter, [{ value: "all", label: agencyFilterLabel }].concat( agencies.map((agency) => ({ value: agency.id, label: agency.companyName })) ), intercityOperatorAgencyFilterValue ); } if (els.intercityOperatorStatusFilter) { const options = intercityOperatorStatusOptions(intercityOperatorWorkspaceTab); if (!options.some((option) => option.value === intercityOperatorStatusFilterValue)) { intercityOperatorStatusFilterValue = "all"; } populateSelectOptions(els.intercityOperatorStatusFilter, options, intercityOperatorStatusFilterValue); } els.intercityOperatorTabs.querySelectorAll("[data-operator-tab]").forEach((button) => { const tab = button.dataset.operatorTab; const active = tab === intercityOperatorWorkspaceTab; button.classList.toggle("active", active); button.setAttribute("aria-pressed", active ? "true" : "false"); button.innerHTML = `${escapeHtml(intercityOperatorTabLabel(tab))} ${escapeHtml(String(counts[tab] ?? 0))}`; }); if (els.intercityOperatorTabSummary) { const focusedAgency = intercityOperatorAgencyFilterValue === "all" ? agencyFilterLabel : agencyNameForIntercity(intercityOperatorAgencyFilterValue); const filterLabel = intercityOperatorStatusOptions(intercityOperatorWorkspaceTab) .find((option) => option.value === intercityOperatorStatusFilterValue)?.label || (translatedValue("operatorFilterAllItems") || "All items"); const visibleCount = filteredCounts[intercityOperatorWorkspaceTab] ?? 0; const totalCount = counts[intercityOperatorWorkspaceTab] ?? 0; els.intercityOperatorTabSummary.innerHTML = ` ${escapeHtml(intercityOperatorTabLabel(intercityOperatorWorkspaceTab))}

${escapeHtml(translatedValue("operatorVisibleCountLabel") || "Visible now")}: ${escapeHtml(`${visibleCount} of ${totalCount}`)}

${escapeHtml(translatedValue("operatorAgencyFilter") || "Agency")}: ${escapeHtml(focusedAgency)}

${escapeHtml(translatedValue("operatorStatusFilter") || "Filter")}: ${escapeHtml(filterLabel)}

${escapeHtml(translatedValue("operatorNextActionLabel") || "Next action")}: ${escapeHtml(nextAction)}

`; } const preferredAgencyId = intercityOperatorAgencyFilterValue !== "all" ? intercityOperatorAgencyFilterValue : agencies.length === 1 ? agencies[0].id : ""; [els.intercityAgencyPaymentSelect, els.intercityDepartureAgency, els.intercityAdvertisingAgency].forEach((select) => { if (!select || !preferredAgencyId) return; if (![...select.options].some((option) => option.value === preferredAgencyId)) return; if (!select.value || intercityOperatorAgencyFilterValue !== "all") select.value = preferredAgencyId; }); syncIntercityOperatorPanelVisibility(); } function ownedIntercityAgencies() { const ownerId = intercityAgencyOwnerProfileId(); if (!ownerId) return []; const agencies = intercityAgencyRecords() .filter((agency) => agency.ownerProfileId === ownerId) .sort((a, b) => new Date(b.updatedAt ?? b.createdAt ?? 0) - new Date(a.updatedAt ?? a.createdAt ?? 0)); const lockedAgencyId = agencyWorkspaceLockedAgencyId(); if (!lockedAgencyId) return agencies; const lockedAgencies = agencies.filter((agency) => agency.id === lockedAgencyId); return agencyWorkspaceRouteActive() ? lockedAgencies : (lockedAgencies.length ? lockedAgencies : agencies); } function intercityAgencyOwnerProfileId() { return String(state.passenger?.id ?? state.sessions?.passenger?.userId ?? "").trim(); } function agencyWorkspaceRouteActive() { const segments = routePathSegments(); return segments[0] === "agency" && segments[1] === "workspace"; } function agencyWorkspaceLockedAgencyId() { if (!agencyWorkspaceRouteActive()) return ""; const hint = readAgencyWorkspaceSessionHint(); const hintedAgencyId = String(hint?.agencyId || "").trim(); if (hintedAgencyId) return hintedAgencyId; if (intercityOperatorAgencyFilterValue !== "all") return intercityOperatorAgencyFilterValue; const ownerId = intercityAgencyOwnerProfileId(); if (!ownerId) return ""; const agencies = intercityAgencyRecords() .filter((agency) => agency.ownerProfileId === ownerId) .sort((a, b) => new Date(b.updatedAt ?? b.createdAt ?? 0) - new Date(a.updatedAt ?? a.createdAt ?? 0)); return agencies.find((agency) => intercityAgencyCanOperate(agency))?.id || agencies[0]?.id || ""; } function readAgencyWorkspaceSessionHint() { try { const raw = window.sessionStorage?.getItem(agencyWorkspaceSessionStorageKey); if (!raw) return null; const parsed = JSON.parse(raw); return parsed && typeof parsed === "object" ? parsed : null; } catch (_error) { return null; } } function rememberAgencyWorkspaceSessionHint(agency, user = {}) { if (!agencyWorkspaceRouteActive() || !agency?.id) return; try { window.sessionStorage?.setItem(agencyWorkspaceSessionStorageKey, JSON.stringify({ userId: String(user?.id || agency.ownerProfileId || state.passenger?.id || "").trim(), email: String(user?.email || agency.supportEmail || state.passenger?.email || "").trim(), phone: String(user?.phone || agency.supportPhone || state.passenger?.phone || "").trim(), agencyId: agency.id, companyName: agency.companyName || "Agency operator", supportEmail: agency.supportEmail || user?.email || "", supportPhone: agency.supportPhone || user?.phone || "", hqCity: agency.hqCity || "", signedInAt: new Date().toISOString() })); } catch (_error) { // Session storage improves agency locking, but the workspace can still rely on Supabase state. } } function applyAgencyWorkspaceSessionHint() { if (!agencyWorkspaceRouteActive() || state.passenger?.id) return false; const hint = readAgencyWorkspaceSessionHint(); const userId = String(hint?.userId || "").trim(); if (!userId) return false; const companyName = String(hint.companyName || hint.email || "Agency operator").trim(); state.sessions.passenger = { phone: String(hint.phone || hint.supportPhone || "").trim(), email: String(hint.email || hint.supportEmail || "").trim(), userId, signedInAt: hint.signedInAt || new Date().toISOString() }; state.passenger = { id: userId, supabaseUserId: userId, name: companyName, email: String(hint.supportEmail || hint.email || "").trim(), phone: String(hint.supportPhone || hint.phone || "").trim(), accountUse: "business", country: "Cameroon", city: String(hint.hqCity || "").trim(), accountStatus: "active" }; state.passengerPage = "business"; if (hint.agencyId) { intercityOperatorAgencyFilterValue = hint.agencyId; state.intercityAgencies = upsertById(intercityAgencyRecords(), normalizeIntercityAgencyRecord({ id: hint.agencyId, ownerProfileId: userId, companyName, supportEmail: hint.supportEmail || hint.email || "", supportPhone: hint.supportPhone || hint.phone || "", hqCity: hint.hqCity || "", status: "active", subscriptionStatus: "trial", publicDirectoryListed: true, active: true, updatedAt: hint.signedInAt || new Date().toISOString() })); } return true; } function agencyWorkspacePassengerFromAgency(agency, user = {}) { const userId = String(user?.id || agency?.ownerProfileId || "").trim(); if (!userId) return null; return { id: userId, supabaseUserId: userId, name: agency?.companyName || user?.email || "Agency operator", email: agency?.supportEmail || user?.email || "", phone: agency?.supportPhone || user?.phone || "", accountUse: "business", country: "Cameroon", city: agency?.hqCity || "", accountStatus: agency?.active === false ? "suspended" : "active" }; } async function currentSupabaseAgencyWorkspaceUser() { if (typeof getSupabaseUser === "function") { return getSupabaseUser({ sessionTimeoutMs: 6000, timeoutMs: 9000 }).catch((error) => { logClientWarning("Agency workspace Supabase session could not be read.", error); return null; }); } const session = await supabaseClient?.auth?.getSession?.().catch(() => null); return session?.data?.session?.user ?? null; } async function ensureAgencyWorkspaceSession() { if (!agencyWorkspaceRouteActive() || !hasSupabaseRuntime()) return null; if (hasSignedIn("passenger") && state.passenger?.id) return state.passenger; if (agencyWorkspaceSessionPromise) return agencyWorkspaceSessionPromise; if (agencyWorkspaceSessionCheckedAt && Date.now() - agencyWorkspaceSessionCheckedAt < 5000) return null; agencyWorkspaceSessionPromise = (async () => { const user = await currentSupabaseAgencyWorkspaceUser(); if (!user?.id) return null; const rows = await supabaseRestRequest(`/rest/v1/cameroon_intercity_agencies?owner_profile_id=eq.${encodeURIComponent(user.id)}&select=*&order=updated_at.desc&limit=5`); const agencies = (rows ?? []).map(mapIntercityAgencyFromDatabase); const approvedAgency = agencies.find((agency) => agency.active !== false && agency.status === "active") ?? agencies[0] ?? null; if (!approvedAgency) return null; state.sessions.passenger = { phone: approvedAgency.supportPhone || user.phone || "", email: approvedAgency.supportEmail || user.email || "", userId: user.id, signedInAt: new Date().toISOString() }; state.passenger = agencyWorkspacePassengerFromAgency(approvedAgency, user); state.passengerPage = "business"; intercityOperatorAgencyFilterValue = approvedAgency.id || intercityOperatorAgencyFilterValue; state.intercityAgencies = intercityAgencyRecords() .filter((agency) => agency.ownerProfileId !== user.id) .concat(agencies); rememberAgencyWorkspaceSessionHint(approvedAgency, user); saveState(); return state.passenger; })().finally(() => { agencyWorkspaceSessionPromise = null; agencyWorkspaceSessionCheckedAt = Date.now(); }); return agencyWorkspaceSessionPromise; } function agencyNameForIntercity(agencyId) { return intercityAgencyRecords().find((agency) => agency.id === agencyId)?.companyName || "Agency"; } function publicAgencySlugFromLocation() { const segments = window.location.pathname .toLowerCase() .replace(/\/+$/, "") .split("/") .filter(Boolean); if (segments[0] !== "agency" || !segments[1]) return ""; const reserved = new Set(["create", "workspace", "index.html", "agency.html"]); if (reserved.has(segments[1]) || segments[1].includes(".")) return ""; try { return decodeURIComponent(segments[1]).trim(); } catch { return ""; } } function publicAgencyPageRequested() { return Boolean(publicAgencySlugFromLocation()); } function agencyPublicPageUrl(agency) { const slug = String(agency?.slug || "").trim(); return slug ? `/agency/${encodeURIComponent(slug)}` : "/travel"; } function publicIntercityRequestedAgencyIdFromLocation() { try { return String(new URLSearchParams(window.location.search).get("agency") || "").trim(); } catch { return ""; } } function publicIntercityTravelUrlForAgency(agencyId = "") { const id = String(agencyId || "").trim(); return id ? `/travel?agency=${encodeURIComponent(id)}` : "/travel"; } function setPublicPageMode(mode) { const page = mode === "agencies" ? "agencies" : "travel"; if (document.documentElement) document.documentElement.dataset.publicPage = page; if (document.body) document.body.dataset.publicPage = page; } function openPublicIntercityPlannerForAgency(agencyId = "") { const id = String(agencyId || "").trim(); const route = publicIntercityTravelUrlForAgency(id); if (window.location.pathname !== "/travel" || window.location.search !== (id ? `?agency=${encodeURIComponent(id)}` : "")) { window.history.pushState({ publicPage: "travel", agencyId: id }, "", route); } setPublicPageMode("travel"); selectedPublicIntercityDepartureId = ""; selectedPublicIntercitySeatNumbers = []; if (els.publicIntercityAgency && id) els.publicIntercityAgency.value = id; renderEntryExperience(); renderPublicIntercityPlanner(); els.publicIntercityResultsGrid?.scrollIntoView({ behavior: "smooth", block: "start" }); } function intercityAgencyHasPublicPage(agency) { return Boolean(agency?.publicDirectoryListed === true && intercityAgencyIsSearchable(agency)); } function publicAgencyPageAgency() { const slug = publicAgencySlugFromLocation(); if (!slug) return null; return intercityAgencyRecords().find((agency) => ( String(agency.slug || "").toLowerCase() === slug && intercityAgencyHasPublicPage(agency) )) ?? null; } function publicAgencyPageAgencyId() { return publicAgencyPageAgency()?.id || ""; } function intercityDepartureDateKey(departure) { const raw = String(departure?.departureAt ?? ""); if (!raw) return ""; const date = new Date(raw); return Number.isFinite(date.getTime()) ? date.toISOString().slice(0, 10) : ""; } function intercityDepartureMatchesFilters(departure) { if (!departure?.active) return false; if (publicIntercityCatalogLoadedAt && !lastPublicIntercityDepartureIds.has(departure.id)) return false; const agency = intercityAgencyRecords().find((item) => item.id === departure.agencyId); if (!agency || !intercityAgencyCanOperate(agency)) return false; const agencyPageRequested = publicAgencyPageRequested(); const agencyPageId = publicAgencyPageAgencyId(); if (agencyPageRequested && !agencyPageId) return false; const agencyId = agencyPageId || els.publicIntercityAgency?.value || ""; const origin = els.publicIntercityOrigin?.value || ""; const destination = els.publicIntercityDestination?.value || ""; const date = els.publicIntercityDate?.value || ""; if (agencyId && departure.agencyId !== agencyId) return false; if (origin && departure.originCity !== origin) return false; if (destination && departure.destinationCity !== destination) return false; if (date && intercityDepartureDateKey(departure) !== date) return false; const departureTime = new Date(departure.departureAt || ""); if (!Number.isFinite(departureTime.getTime()) || departureTime.getTime() <= Date.now()) return false; return true; } function publicIntercityDepartures() { return intercityDepartureRecords() .filter(intercityDepartureMatchesFilters) .sort((a, b) => new Date(a.departureAt ?? 0) - new Date(b.departureAt ?? 0)); } function allVisiblePublicIntercityDepartures() { const agencyPageId = publicAgencyPageAgencyId(); if (publicAgencyPageRequested() && !agencyPageId) return []; return intercityDepartureRecords() .filter((departure) => { if (!departure?.active) return false; if (publicIntercityCatalogLoadedAt && !lastPublicIntercityDepartureIds.has(departure.id)) return false; if (agencyPageId && departure.agencyId !== agencyPageId) return false; const agency = intercityAgencyRecords().find((item) => item.id === departure.agencyId); if (!agency || !intercityAgencyCanOperate(agency)) return false; const departureTime = new Date(departure.departureAt || ""); return Number.isFinite(departureTime.getTime()) && departureTime.getTime() > Date.now(); }) .sort((a, b) => new Date(a.departureAt ?? 0) - new Date(b.departureAt ?? 0)); } function publicIntercitySearchContext() { const agencyId = publicAgencyPageAgencyId() || els.publicIntercityAgency?.value || ""; const agency = intercityAgencyRecords().find((item) => item.id === agencyId); return { agency: agency?.companyName || (translatedValue("anyTransportAgency") || "Any transport agency"), origin: els.publicIntercityOrigin?.value || (translatedValue("anyDepartureCity") || "Any departure city"), destination: els.publicIntercityDestination?.value || (translatedValue("anyDestinationCity") || "Any destination city"), date: els.publicIntercityDate?.value || "" }; } function renderPublicIntercitySearchContextSummary() { const context = publicIntercitySearchContext(); const hasSpecificFilter = Boolean( (els.publicIntercityAgency?.value || "") || (els.publicIntercityOrigin?.value || "") || (els.publicIntercityDestination?.value || "") || (els.publicIntercityDate?.value || "") ); if (!hasSpecificFilter) { return `

${escapeHtml("Search above by agency, travel date, departure city, and destination city. The selected departure below carries those details into booking.")}

`; } return `
${escapeHtml("Your search")}
${escapeHtml(translatedValue("agency") || "Agency")}${escapeHtml(context.agency)}
${escapeHtml(translatedValue("travelDate") || "Travel date")}${escapeHtml(context.date || "Any date")}
${escapeHtml(translatedValue("origin") || "Departure city")}${escapeHtml(context.origin)}
${escapeHtml(translatedValue("destination") || "Destination city")}${escapeHtml(context.destination)}
`; } function intercityPublicCities() { const cities = new Set(cameroonIntercityCities); intercityAgencyRecords().filter((agency) => intercityAgencyIsSearchable(agency)).forEach((agency) => { if (agency.hqCity) cities.add(agency.hqCity); (Array.isArray(agency.operatedCities) ? agency.operatedCities : []).forEach((city) => cities.add(city)); }); intercityDepartureRecords().forEach((departure) => { if (departure.originCity) cities.add(departure.originCity); if (departure.destinationCity) cities.add(departure.destinationCity); }); return [...cities].filter(Boolean).sort((a, b) => a.localeCompare(b)); } function publicIntercityAgencies() { const agencies = intercityAgencyRecords() .filter((agency) => intercityAgencyIsSearchable(agency)) .filter((agency) => !hasSupabaseConfig() || !String(agency.id || "").startsWith("directory-")) .filter((agency) => !publicIntercityCatalogLoadedAt || lastPublicIntercityAgencyIds.has(agency.id)) .sort((a, b) => String(a.companyName || "").localeCompare(String(b.companyName || ""))); if (agencies.length) return agencies; if (publicIntercityCatalogLoadedAt || hasSupabaseConfig()) return []; return cameroonFallbackIntercityAgencies .map((agency) => normalizeIntercityAgencyRecord(agency)) .filter((agency) => intercityAgencyIsSearchable(agency)) .sort((a, b) => String(a.companyName || "").localeCompare(String(b.companyName || ""))); } function intercityPromotionIsPublic(promotion) { if (!promotion || promotion.status !== "approved" || promotion.publicActive === false) return false; const agency = intercityAgencyRecords().find((item) => item.id === promotion.agencyId); if (!agency || !intercityAgencyIsSearchable(agency)) return false; const now = Date.now(); const start = promotion.publishStartAt ? new Date(promotion.publishStartAt).getTime() : 0; const end = promotion.publishEndAt ? new Date(promotion.publishEndAt).getTime() : Number.POSITIVE_INFINITY; if (Number.isFinite(start) && start > now) return false; if (Number.isFinite(end) && end < now) return false; return Boolean(String(promotion.publicTitle || promotion.campaignName || "").trim()); } function publicIntercityPromotions() { const placementPriority = new Map([ ["homepage_spotlight", 0], ["website_banner", 1], ["featured_route", 2], ["push_notice", 3] ]); const agencyPageId = publicAgencyPageAgencyId(); return intercityAdvertisingRequestRecords() .filter(intercityPromotionIsPublic) .filter((promotion) => !agencyPageId || promotion.agencyId === agencyPageId) .sort((a, b) => { const placementA = placementPriority.get(a.placement) ?? 9; const placementB = placementPriority.get(b.placement) ?? 9; if (placementA !== placementB) return placementA - placementB; return new Date(b.publishStartAt ?? b.createdAt ?? 0) - new Date(a.publishStartAt ?? a.createdAt ?? 0); }); } function renderPublicAgencyPage() { if (!els.publicAgencyPagePanel || !els.publicAgencyPageContent) return; if (!publicAgencyPageRequested()) { els.publicAgencyPagePanel.hidden = true; els.publicAgencyPageContent.innerHTML = ""; return; } els.publicAgencyPagePanel.hidden = false; const agency = publicAgencyPageAgency(); if (!agency) { els.publicAgencyPageContent.innerHTML = `
${escapeHtml(translatedValue("agency") || "Agency")}

${escapeHtml("Agency page not available")}

${escapeHtml(publicIntercityCatalogLoadedAt ? "This agency page is not public right now. Waka admin may still be reviewing it, or the agency may have removed the listing." : "Loading the agency profile, approved public content, and live departures...")}

${escapeHtml(translatedValue("searchIntercityDepartures") || "Search inter-city departures")}
`; return; } const agencyDepartures = allVisiblePublicIntercityDepartures() .filter((departure) => departure.agencyId === agency.id) .sort((a, b) => new Date(a.departureAt || 0) - new Date(b.departureAt || 0)); const cities = agency.operatedCities.length ? agency.operatedCities.join(", ") : agency.hqCity || "Cameroon"; const routePairs = [...new Set(agencyDepartures.map((departure) => `${departure.originCity} -> ${departure.destinationCity}`))].slice(0, 6); const departuresMarkup = agencyDepartures.length ? agencyDepartures.map((departure) => { const availableSeats = availableSeatsForIntercityDeparture(departure); const soldOut = availableSeats < 1; return `
${escapeHtml(formatDateTime(departure.departureAt || ""))} ${escapeHtml(`${departure.originCity || ""} -> ${departure.destinationCity || ""}`)}

${escapeHtml([departure.busLabel, departure.boardingLocation].filter(Boolean).join(" | ") || departure.originCity || "")}

${escapeHtml(translatedValue("farePerSeat") || "Fare per seat (FCFA)")}${departure.fareXaf > 0 ? escapeHtml(`${departure.fareXaf.toLocaleString("en-US")} FCFA`) : "Call operator"}
${escapeHtml(translatedValue("seatsLeftLabel") || "Seats left")}${escapeHtml(`${availableSeats} of ${departure.seatCapacity}`)}
${escapeHtml(translatedValue("boardingLocationLabel") || "Boarding point")}${escapeHtml(departure.boardingLocation || departure.originCity || "")}
${escapeHtml(translatedValue("dropoffLocationLabel") || "Drop-off point")}${escapeHtml(departure.dropoffLocation || departure.destinationCity || "")}
`; }).join("") : `
${escapeHtml("No published departure yet.")}${escapeHtml("When this agency publishes an active departure, it will appear here for booking.")}
`; els.publicAgencyPageContent.innerHTML = `
${intercityAgencyLogoMarkup(agency)}
${escapeHtml(agency.hqCity || "Cameroon")}

${escapeHtml(agency.companyName || "Transport agency")}

${escapeHtml("Coverage")}${escapeHtml(cities)}
${escapeHtml(translatedValue("agencyTerminalLabel") || "Agency terminal")}${escapeHtml(agency.terminalAddress || "Terminal details pending")}
${escapeHtml(translatedValue("supportPhone") || "Support phone")}${escapeHtml(agency.supportPhone || "Agency phone pending")}
${escapeHtml("Published departures")}${escapeHtml(String(agencyDepartures.length))}
${agency.boardingPolicy ? `${escapeHtml(agency.boardingPolicy)}` : ""}
${escapeHtml("Compare all agencies")}
${routePairs.length ? `
${escapeHtml("Active routes")}
${routePairs.map((route) => `${escapeHtml(route)}`).join("")}
` : ""}
${escapeHtml("Published departures")}
${departuresMarkup}
`; els.publicAgencyPageContent.querySelectorAll(".agency-page-book-departure").forEach((button) => { button.addEventListener("click", () => { selectedPublicIntercityDepartureId = button.dataset.departureId || ""; selectedPublicIntercitySeatNumbers = []; if (els.publicIntercityAgency) els.publicIntercityAgency.value = agency.id; renderPublicIntercityPlanner(); els.publicIntercityBookingPanel?.scrollIntoView({ behavior: "smooth", block: "start" }); }); }); } function renderPublicIntercityPromotions() { if (!els.publicIntercityPromotionList || !els.publicIntercityPromotionsPanel) return; els.publicIntercityPromotionList.innerHTML = ""; els.publicIntercityPromotionsPanel.hidden = true; } function renderPublicIntercityDiscoverySummary() { if (els.publicIntercityCitiesDirectory) { els.publicIntercityCitiesDirectory.innerHTML = ""; intercityPublicCities().slice(0, 18).forEach((city) => { const chip = document.createElement("span"); chip.className = "chip"; chip.textContent = city; els.publicIntercityCitiesDirectory.append(chip); }); } if (els.publicIntercityAgenciesDirectory) { els.publicIntercityAgenciesDirectory.innerHTML = ""; publicIntercityAgencies().forEach((agency) => { const chip = document.createElement(intercityAgencyHasPublicPage(agency) ? "a" : "span"); chip.className = "chip"; chip.textContent = agency.hqCity ? `${agency.companyName} (${agency.hqCity})` : agency.companyName; if (intercityAgencyHasPublicPage(agency)) chip.href = agencyPublicPageUrl(agency); els.publicIntercityAgenciesDirectory.append(chip); }); } } function renderPublicAgencyDirectoryPanel() { if (!els.publicAgencyDirectoryList) return; const agencyIdsWithPublicDepartures = new Set(allVisiblePublicIntercityDepartures().map((departure) => departure.agencyId)); const agencies = publicIntercityAgencies() .filter((agency) => intercityAgencyHasPublicPage(agency) || agencyIdsWithPublicDepartures.has(agency.id)) .sort((a, b) => String(a.companyName || "").localeCompare(String(b.companyName || ""))); els.publicAgencyDirectoryList.innerHTML = ""; if (!agencies.length) { const item = document.createElement("article"); item.className = "public-agency-directory-empty"; item.innerHTML = ` ${escapeHtml(publicIntercityCatalogLoadedAt ? (translatedValue("noPublicAgencyPages") || "No public agency page is listed yet.") : (translatedValue("loadingApprovedAgencyPages") || "Loading approved agency pages."))} ${escapeHtml(publicIntercityCatalogLoadedAt ? (translatedValue("whenAdminPublishesAgencyPage") || "When Waka admin publishes an agency page, it will appear here for travelers.") : (translatedValue("wakaAdminControlsPublicAgencyPages") || "Waka admin controls which agency pages appear publicly."))} ${escapeHtml(translatedValue("searchAllIntercityDepartures") || "Search all inter-city departures")} `; els.publicAgencyDirectoryList.append(item); return; } agencies.slice(0, 12).forEach((agency) => { const item = document.createElement("article"); item.className = "public-agency-directory-card"; const agencyDepartures = allVisiblePublicIntercityDepartures().filter((departure) => departure.agencyId === agency.id); const routes = [...new Set(agencyDepartures.map((departure) => `${departure.originCity} -> ${departure.destinationCity}`))].slice(0, 3); const nextDeparture = agencyDepartures .filter((departure) => Number.isFinite(new Date(departure.departureAt || "").getTime())) .sort((a, b) => new Date(a.departureAt || 0) - new Date(b.departureAt || 0))[0] || null; const cities = agency.operatedCities.length ? agency.operatedCities.slice(0, 4).join(", ") : agency.hqCity || "Cameroon"; item.innerHTML = `
${intercityAgencyLogoMarkup(agency)}
${escapeHtml(agency.hqCity || "Cameroon")} ${escapeHtml(agency.companyName || "Transport agency")}
${escapeHtml(cities)} ${escapeHtml(`${agencyDepartures.length} departure${agencyDepartures.length === 1 ? "" : "s"}`)} ${nextDeparture ? `${escapeHtml(`${translatedValue("agencyDirectoryNextDeparture") || "Next"}: ${nextDeparture.originCity} -> ${nextDeparture.destinationCity}`)}` : ""} ${routes.map((route) => `${escapeHtml(route)}`).join("")}
${escapeHtml(translatedValue("openAgencyPage") || "Open agency page")}
`; item.querySelector(".public-agency-book-departures")?.addEventListener("click", () => { openPublicIntercityPlannerForAgency(agency.id); }); els.publicAgencyDirectoryList.append(item); }); } function setIntercityCityOptions(select, selectedValue = "") { if (!select) return; const currentValue = selectedValue || select.value || ""; const placeholder = select.id === "publicIntercityOrigin" ? (translatedValue("anyDepartureCity") || "Any departure city") : (translatedValue("anyDestinationCity") || "Any destination city"); const placeholderKey = select.id === "publicIntercityOrigin" ? "anyDepartureCity" : "anyDestinationCity"; select.innerHTML = ""; const blank = document.createElement("option"); blank.value = ""; blank.dataset.i18n = placeholderKey; blank.textContent = placeholder; select.append(blank); intercityPublicCities().forEach((city) => { const option = document.createElement("option"); option.value = city; option.textContent = city; option.selected = city === currentValue; select.append(option); }); if (currentValue && [...select.options].some((option) => option.value === currentValue)) { select.value = currentValue; } } function setPublicIntercityAgencyOptions(select, selectedValue = "") { if (!select) return; const currentValue = selectedValue || select.value || ""; const agencies = publicIntercityAgencies(); const loading = publicIntercityCatalogIsLoading() && !agencies.length; select.innerHTML = ""; const blank = document.createElement("option"); blank.value = ""; blank.textContent = loading ? translatedValue("loadingTransportAgencies") || "Loading transport agencies..." : translatedValue("anyTransportAgency") || "Any transport agency"; select.append(blank); agencies.forEach((agency) => { const option = document.createElement("option"); option.value = agency.id; option.textContent = agency.hqCity ? `${agency.companyName} (${agency.hqCity})` : agency.companyName; option.selected = agency.id === currentValue; select.append(option); }); if (loading) { const loadingOption = document.createElement("option"); loadingOption.value = ""; loadingOption.disabled = true; loadingOption.textContent = translatedValue("loadingAgencyDepartures") || "Loading verified agency departures."; select.append(loadingOption); } if (currentValue && [...select.options].some((option) => option.value === currentValue)) { select.value = currentValue; } } function setOwnedIntercityAgencyOptions(select, selectedValue = "") { if (!select) return; const ownedAgencies = ownedIntercityAgencies(); const lockedAgencyId = agencyWorkspaceLockedAgencyId(); const defaultAgencyId = (lockedAgencyId && ownedAgencies.some((agency) => agency.id === lockedAgencyId) ? lockedAgencyId : "") || ownedAgencies.find((agency) => intercityAgencyCanOperate(agency))?.id || ownedAgencies[0]?.id || ""; const currentValue = selectedValue || (agencyWorkspaceRouteActive() ? defaultAgencyId : select.value) || defaultAgencyId || ""; const lockedForAgencyWorkspace = agencyWorkspaceRouteActive(); const field = select.closest("label"); if (field) field.hidden = lockedForAgencyWorkspace; select.disabled = lockedForAgencyWorkspace; select.innerHTML = ""; const blank = document.createElement("option"); blank.value = ""; blank.textContent = "Choose agency"; select.append(blank); ownedAgencies.forEach((agency) => { const option = document.createElement("option"); option.value = agency.id; option.textContent = agency.companyName; option.selected = agency.id === currentValue; select.append(option); }); if (currentValue && [...select.options].some((option) => option.value === currentValue)) { select.value = currentValue; } } function setIntercityDepartureSaveStatus(message, tone = "", { announce = false, focus = false } = {}) { const node = els.intercityDepartureStatus; if (node) { node.textContent = message; node.classList.toggle("success-status", tone === "success"); node.classList.toggle("error-status", tone === "error"); node.setAttribute("role", "status"); node.setAttribute("aria-live", tone === "error" ? "assertive" : "polite"); if (focus) { window.setTimeout(() => node.scrollIntoView({ behavior: "smooth", block: "center" }), 0); } } if (!announce) return; if (typeof showWakaGoodAlert === "function") { void showWakaGoodAlert(message); } else if (typeof window !== "undefined" && typeof window.alert === "function") { window.alert(message); } } let intercityDepartureSaveFallbackWired = false; let intercityDepartureSaveInFlight = false; function setIntercityDepartureSaveBusy(isBusy) { if (!els.saveIntercityDeparture) return; els.saveIntercityDeparture.disabled = Boolean(isBusy); els.saveIntercityDeparture.textContent = isBusy ? "Saving departure..." : "Save departure"; } function ensureIntercityDepartureSaveFallbackWired() { if (intercityDepartureSaveFallbackWired || typeof document === "undefined") return; intercityDepartureSaveFallbackWired = true; document.addEventListener("click", (event) => { const button = event.target?.closest?.("#saveIntercityDeparture"); if (!button) return; event.preventDefault(); void submitIntercityDeparture(event); }); document.addEventListener("submit", (event) => { if (event.target?.id !== "intercityDepartureForm") return; event.preventDefault(); void submitIntercityDeparture(event); }); } function intercityAgencyApprovalPending(agency) { return Boolean(agency && agency.status === "pending_review"); } function intercityAgencyApprovalBlockedReason(agency) { if (!agency) return "Choose an agency first."; if (intercityAgencyApprovalPending(agency)) { return "This agency is waiting for Waka admin approval. Departure publishing unlocks only after approval."; } if (agency.status === "suspended" || agency.active === false) { return "This agency is suspended. Contact Waka admin before publishing departures."; } if (!intercityAgencyCanOperate(agency)) { return `This agency cannot publish departures right now: ${intercityAgencyServiceStatus(agency)}.`; } return ""; } function adoptLoaderPublicIntercityCatalog({ render = true } = {}) { const catalog = window.WAKA_PUBLIC_INTERCITY_CATALOG; if (!catalog || window.WAKA_ADOPTED_PUBLIC_INTERCITY_CATALOG === catalog) return false; const agencyRows = Array.isArray(catalog.agencies) ? catalog.agencies : []; const departureRows = Array.isArray(catalog.departures) ? catalog.departures : []; if (!agencyRows.length && !departureRows.length) return false; window.WAKA_ADOPTED_PUBLIC_INTERCITY_CATALOG = catalog; lastPublicIntercityAgencyIds = new Set(agencyRows.map((row) => row.id).filter(Boolean)); lastPublicIntercityDepartureIds = new Set(departureRows.map((row) => row.id).filter(Boolean)); const existingAgencies = intercityAgencyRecords(); const agencies = agencyRows.map(mapIntercityAgencyFromDatabase).map((agency) => { const existing = existingAgencies.find((item) => item.id === agency.id); if (!existing) return agency; return { ...agency, ownerProfileId: existing.ownerProfileId ?? agency.ownerProfileId, businessAccountId: existing.businessAccountId ?? agency.businessAccountId }; }); const departures = departureRows.map(mapIntercityDepartureFromDatabase); const preservedAgencies = intercityAgencyRecords().filter((agency) => !agencies.some((item) => item.id === agency.id)); state.intercityAgencies = mergeIntercityAgencyDirectory(preservedAgencies.concat(agencies)); state.intercityDepartures = intercityDepartureRecords().filter((departure) => !departures.some((item) => item.id === departure.id)); departures.forEach((departure) => { state.intercityDepartures = upsertById(state.intercityDepartures, departure); }); publicIntercityCatalogLoadedAt = Date.now(); publicIntercityCatalogLoading = false; populateCameroonIntercityCityOptions(); setPublicIntercityAgencyOptions(els.publicIntercityAgency); setIntercityCityOptions(els.publicIntercityOrigin); setIntercityCityOptions(els.publicIntercityDestination); if (render) renderPublicIntercityCatalogSurfaces(); return true; } async function loadPublicIntercityCatalog({ force = false } = {}) { if (!els.publicIntercityDepartureList && !els.publicAgencyDirectoryList && !els.publicAgencyPageContent && !els.publicIntercityAgency) return; if (!force && adoptLoaderPublicIntercityCatalog()) return; if (!hasSupabaseConfig()) { publicIntercityCatalogLoading = true; if (els.publicIntercitySearchStatus) { els.publicIntercitySearchStatus.textContent = "Connecting to the live WakaGood inter-city catalog..."; } if (publicIntercityCatalogConfigRetryTimer) window.clearTimeout(publicIntercityCatalogConfigRetryTimer); publicIntercityCatalogConfigRetryTimer = window.setTimeout(() => { publicIntercityCatalogConfigRetryTimer = null; void loadPublicIntercityCatalog({ force: true }); }, 500); if (!intercityCatalogLoadPromise) { intercityCatalogLoadPromise = Promise.resolve().finally(() => { intercityCatalogLoadPromise = null; }); } setPublicIntercityAgencyOptions(els.publicIntercityAgency); return; } if (publicIntercityCatalogConfigRetryTimer) { window.clearTimeout(publicIntercityCatalogConfigRetryTimer); publicIntercityCatalogConfigRetryTimer = null; } if (!force && intercityCatalogLoadPromise) return intercityCatalogLoadPromise; if (!force && publicIntercityCatalogLoadedAt && Date.now() - publicIntercityCatalogLoadedAt < 60 * 1000) return; publicIntercityCatalogLoading = true; if (els.publicIntercitySearchStatus) { els.publicIntercitySearchStatus.textContent = "Loading verified inter-city operators, departure cities, seats, and schedules..."; } setPublicIntercityAgencyOptions(els.publicIntercityAgency); setIntercityCityOptions(els.publicIntercityOrigin); setIntercityCityOptions(els.publicIntercityDestination); intercityCatalogLoadPromise = supabaseRestRequest("/rest/v1/rpc/get_public_cameroon_intercity_catalog", { method: "POST", body: { max_departures: 400 }, accessToken: appConfig.supabaseAnonKey }).then(async (catalog) => { const rpcAgencyRows = Array.isArray(catalog?.agencies) ? catalog.agencies : []; const rpcDepartureRows = Array.isArray(catalog?.departures) ? catalog.departures : []; const nowFilter = encodeURIComponent(new Date().toISOString()); const [promotionRows, liveAgencyRows, liveDepartureRows] = await Promise.all([ supabaseRestRequest("/rest/v1/cameroon_intercity_advertising_requests?select=*&status=eq.approved&public_active=eq.true&order=publish_start_at.desc&limit=24", { accessToken: appConfig.supabaseAnonKey }).catch((error) => { logClientWarning("Public agency promotions are not loaded yet.", error); return []; }), supabaseRestRequest("/rest/v1/cameroon_intercity_agencies?select=*&active=eq.true&status=eq.active&order=company_name.asc&limit=500", { accessToken: appConfig.supabaseAnonKey }).catch((error) => { logClientWarning("Live public agency rows are not loaded yet.", error); return []; }), supabaseRestRequest(`/rest/v1/cameroon_intercity_departures?select=*&active=eq.true&departure_at=gte.${nowFilter}&order=departure_at.asc&limit=500`, { accessToken: appConfig.supabaseAnonKey }).catch((error) => { logClientWarning("Live public departure rows are not loaded yet.", error); return []; }) ]); const agencyRows = [...new Map(rpcAgencyRows.concat(liveAgencyRows ?? []).map((row) => [row.id, row])).values()]; const departureRows = [...new Map(rpcDepartureRows.concat(liveDepartureRows ?? []).map((row) => [row.id, row])).values()]; lastPublicIntercityAgencyIds = new Set(agencyRows.map((row) => row.id).filter(Boolean)); lastPublicIntercityDepartureIds = new Set(departureRows.map((row) => row.id).filter(Boolean)); const existingAgencies = intercityAgencyRecords(); const agencies = agencyRows.map(mapIntercityAgencyFromDatabase).map((agency) => { const existing = existingAgencies.find((item) => item.id === agency.id); if (!existing) return agency; return { ...agency, ownerProfileId: existing.ownerProfileId ?? agency.ownerProfileId, businessAccountId: existing.businessAccountId ?? agency.businessAccountId }; }); const departures = (departureRows ?? []).map(mapIntercityDepartureFromDatabase); const promotions = (promotionRows ?? []).map(mapIntercityAdvertisingRequestFromDatabase); const preservedAgencies = intercityAgencyRecords().filter((agency) => !agencies.some((item) => item.id === agency.id)); state.intercityAgencies = mergeIntercityAgencyDirectory(preservedAgencies.concat(agencies)); state.intercityDepartures = intercityDepartureRecords().filter((departure) => !departures.some((item) => item.id === departure.id)); departures.forEach((departure) => { state.intercityDepartures = upsertById(state.intercityDepartures, departure); }); state.intercityAdvertisingRequests = intercityAdvertisingRequestRecords() .filter((promotion) => promotion.status !== "approved" || !promotions.some((item) => item.id === promotion.id)) .concat(promotions); publicIntercityCatalogLoadedAt = Date.now(); publicIntercityCatalogLoading = false; intercityCatalogLoadPromise = null; populateCameroonIntercityCityOptions(); setPublicIntercityAgencyOptions(els.publicIntercityAgency); setIntercityCityOptions(els.publicIntercityOrigin); setIntercityCityOptions(els.publicIntercityDestination); renderPublicIntercityCatalogSurfaces(); }).catch(async (error) => { logClientWarning("Public inter-city catalog RPC is unavailable; trying direct public table reads.", error); const nowFilter = encodeURIComponent(new Date().toISOString()); const [liveAgencyRows, liveDepartureRows, promotionRows] = await Promise.all([ supabaseRestRequest("/rest/v1/cameroon_intercity_agencies?select=*&active=eq.true&status=eq.active&order=company_name.asc&limit=500", { accessToken: appConfig.supabaseAnonKey }).catch((directError) => { logClientWarning("Direct public agency fallback is not available.", directError); return []; }), supabaseRestRequest(`/rest/v1/cameroon_intercity_departures?select=*&active=eq.true&departure_at=gte.${nowFilter}&order=departure_at.asc&limit=500`, { accessToken: appConfig.supabaseAnonKey }).catch((directError) => { logClientWarning("Direct public departure fallback is not available.", directError); return []; }), supabaseRestRequest("/rest/v1/cameroon_intercity_advertising_requests?select=*&status=eq.approved&public_active=eq.true&order=publish_start_at.desc&limit=24", { accessToken: appConfig.supabaseAnonKey }).catch(() => []) ]); const agencies = (liveAgencyRows ?? []).map(mapIntercityAgencyFromDatabase); const departures = (liveDepartureRows ?? []).map(mapIntercityDepartureFromDatabase); const promotions = (promotionRows ?? []).map(mapIntercityAdvertisingRequestFromDatabase); lastPublicIntercityAgencyIds = new Set(agencies.map((agency) => agency.id).filter(Boolean)); lastPublicIntercityDepartureIds = new Set(departures.map((departure) => departure.id).filter(Boolean)); if (agencies.length || departures.length) { const preservedAgencies = intercityAgencyRecords().filter((agency) => !agencies.some((item) => item.id === agency.id)); state.intercityAgencies = mergeIntercityAgencyDirectory(preservedAgencies.concat(agencies)); state.intercityDepartures = intercityDepartureRecords().filter((departure) => !departures.some((item) => item.id === departure.id)); departures.forEach((departure) => { state.intercityDepartures = upsertById(state.intercityDepartures, departure); }); state.intercityAdvertisingRequests = intercityAdvertisingRequestRecords() .filter((promotion) => promotion.status !== "approved" || !promotions.some((item) => item.id === promotion.id)) .concat(promotions); publicIntercityCatalogLoadedAt = Date.now(); populateCameroonIntercityCityOptions(); setPublicIntercityAgencyOptions(els.publicIntercityAgency); setIntercityCityOptions(els.publicIntercityOrigin); setIntercityCityOptions(els.publicIntercityDestination); } else { state.intercityAgencies = mergeIntercityAgencyDirectory(intercityAgencyRecords()); } publicIntercityCatalogLoading = false; intercityCatalogLoadPromise = null; renderPublicIntercityCatalogSurfaces(); setPublicIntercityAgencyOptions(els.publicIntercityAgency); if (els.publicIntercitySearchStatus) { els.publicIntercitySearchStatus.textContent = departures.length ? `${departures.length} live public departure${departures.length === 1 ? "" : "s"} loaded.` : agencies.length ? "Approved agencies loaded, but no public future departures are readable yet. Ask Waka admin to apply the inter-city public catalog SQL grant/policy repair if departures were already published." : `Inter-city departures are unavailable right now: ${error.message}`; } }).finally(() => { publicIntercityCatalogLoading = false; intercityCatalogLoadPromise = null; setPublicIntercityAgencyOptions(els.publicIntercityAgency); setIntercityCityOptions(els.publicIntercityOrigin); setIntercityCityOptions(els.publicIntercityDestination); renderPublicIntercityCatalogSurfaces(); }); return intercityCatalogLoadPromise; } function renderPublicIntercityCatalogSurfaces({ skipPlanner = false } = {}) { const renderTasks = [ ["public inter-city discovery", renderPublicIntercityDiscoverySummary], ["public agency directory", renderPublicAgencyDirectoryPanel], ["public agency page", renderPublicAgencyPage], ["public inter-city promotions", renderPublicIntercityPromotions], skipPlanner ? null : ["public inter-city planner", renderPublicIntercityPlanner] ].filter(Boolean); renderTasks.forEach(([label, renderTask]) => { try { renderTask(); } catch (error) { logClientWarning(`Could not render ${label}.`, error); } }); } let publicIntercityCatalogControlsWired = false; function publicIntercityCatalogNeedsControlRefresh() { if (!hasSupabaseConfig()) return false; if (publicIntercityCatalogIsLoading()) return false; const agencyOptionCount = els.publicIntercityAgency?.options?.length ?? 0; if (!publicIntercityCatalogLoadedAt) return true; if (agencyOptionCount <= 1 && !publicIntercityAgencies().length) return true; return false; } function requestPublicIntercityCatalogControlRefresh() { if (!publicIntercityCatalogNeedsControlRefresh()) return; if (els.publicIntercitySearchStatus) { els.publicIntercitySearchStatus.textContent = translatedValue("refreshingAgencyDepartures") || "Refreshing approved agency departures..."; } void loadPublicIntercityCatalog({ force: true }); } function ensurePublicIntercityCatalogControlsWired() { if (publicIntercityCatalogControlsWired || typeof document === "undefined") return; publicIntercityCatalogControlsWired = true; const selector = "#publicIntercityAgency,#publicIntercityOrigin,#publicIntercityDestination,#publicIntercityDate,#publicIntercitySearchForm button"; const maybeRefresh = (event) => { if (!event.target?.closest?.(selector)) return; requestPublicIntercityCatalogControlRefresh(); }; document.addEventListener("focusin", maybeRefresh); document.addEventListener("pointerdown", maybeRefresh, true); document.addEventListener("click", maybeRefresh, true); } async function loadOwnedIntercityOperatorData({ force = false } = {}) { const ownerProfileId = intercityAgencyOwnerProfileId(); if (!hasSupabaseRuntime() || !hasSignedIn("passenger") || !ownerProfileId) return; if (!force && intercityOwnerLoadPromise) return intercityOwnerLoadPromise; if (!force && intercityOwnerDataLoadedAt && Date.now() - intercityOwnerDataLoadedAt < 45 * 1000) return; const passengerId = ownerProfileId; intercityOwnerLoadPromise = supabaseRestRequest(`/rest/v1/cameroon_intercity_agencies?owner_profile_id=eq.${encodeURIComponent(passengerId)}&select=*&order=updated_at.desc`) .then(async (agencyRows) => { const agencies = (agencyRows ?? []).map(mapIntercityAgencyFromDatabase); state.intercityAgencies = intercityAgencyRecords() .filter((agency) => agency.ownerProfileId !== passengerId) .concat(agencies); const agencyIds = agencies.map((agency) => agency.id); if (!agencyIds.length) { state.intercityAgencyPayments = intercityAgencyPaymentRecords().filter((item) => !item.agencyId); state.intercityDepartures = intercityDepartureRecords().filter((item) => !item.agencyId); state.intercityBookings = intercityBookingRecords().filter((item) => !item.agencyId); state.intercityAdvertisingRequests = intercityAdvertisingRequestRecords().filter((item) => !item.agencyId); state.intercityBookingMessages = intercityBookingMessageRecords().filter((item) => !item.agencyId); state.intercityAgencyMessages = intercityAgencyMessageRecords().filter((item) => !item.agencyId); intercityOwnerDataLoadedAt = Date.now(); renderIntercityOperatorPanels(); return; } const inFilter = agencyIds.join(","); renderIntercityOperatorPanels(); const loadAgencyScopedRows = (label, path) => supabaseRestRequest(path).catch((error) => { logClientWarning(`Agency-scoped ${label} could not be loaded.`, error); return []; }); const [paymentRows, departureRows, bookingRows, advertisingRows, messageRows, agencyMessageRows] = await Promise.all([ loadAgencyScopedRows("payments", `/rest/v1/cameroon_intercity_agency_payments?agency_id=in.(${inFilter})&select=*&order=billing_period_start.desc`), loadAgencyScopedRows("departures", `/rest/v1/cameroon_intercity_departures?agency_id=in.(${inFilter})&select=*&order=departure_at.asc`), loadAgencyScopedRows("bookings", `/rest/v1/cameroon_intercity_bookings?agency_id=in.(${inFilter})&select=*&order=created_at.desc&limit=120`), loadAgencyScopedRows("advertising requests", `/rest/v1/cameroon_intercity_advertising_requests?agency_id=in.(${inFilter})&select=*&order=created_at.desc&limit=80`), loadAgencyScopedRows("booking messages", `/rest/v1/cameroon_intercity_booking_messages?agency_id=in.(${inFilter})&select=*&order=created_at.desc&limit=250`), loadAgencyScopedRows("agency messages", `/rest/v1/cameroon_intercity_agency_messages?agency_id=in.(${inFilter})&select=*&order=created_at.desc&limit=250`) ]); state.intercityAgencyPayments = intercityAgencyPaymentRecords() .filter((item) => !agencyIds.includes(item.agencyId)) .concat((paymentRows ?? []).map(mapIntercityAgencyPaymentFromDatabase)); state.intercityDepartures = intercityDepartureRecords() .filter((item) => !agencyIds.includes(item.agencyId)) .concat((departureRows ?? []).map(mapIntercityDepartureFromDatabase)); state.intercityBookings = intercityBookingRecords() .filter((item) => !agencyIds.includes(item.agencyId)) .concat((bookingRows ?? []).map(mapIntercityBookingFromDatabase)); state.intercityAdvertisingRequests = intercityAdvertisingRequestRecords() .filter((item) => !agencyIds.includes(item.agencyId)) .concat((advertisingRows ?? []).map(mapIntercityAdvertisingRequestFromDatabase)); state.intercityBookingMessages = intercityBookingMessageRecords() .filter((item) => !agencyIds.includes(item.agencyId)) .concat((messageRows ?? []).map(mapIntercityBookingMessageFromDatabase)); state.intercityAgencyMessages = intercityAgencyMessageRecords() .filter((item) => !agencyIds.includes(item.agencyId)) .concat((agencyMessageRows ?? []).map(mapIntercityAgencyMessageFromDatabase)); intercityOwnerDataLoadedAt = Date.now(); renderIntercityOperatorPanels(); }) .catch((error) => { if (els.intercityAgencyStatus) { els.intercityAgencyStatus.textContent = `Could not load operator records: ${error.message}`; } }) .finally(() => { intercityOwnerLoadPromise = null; }); return intercityOwnerLoadPromise; } async function loadPassengerIntercityTravelerData({ force = false } = {}) { if (!hasSupabaseRuntime() || !hasSignedIn("passenger") || !state.passenger?.id) return; if (!force && intercityTravelerLoadPromise) return intercityTravelerLoadPromise; if (!force && intercityTravelerDataLoadedAt && Date.now() - intercityTravelerDataLoadedAt < 45 * 1000) return; const passengerId = state.passenger.id; intercityTravelerLoadPromise = supabaseRestRequest(`/rest/v1/cameroon_intercity_bookings?passenger_profile_id=eq.${encodeURIComponent(passengerId)}&select=*&order=created_at.desc&limit=80`) .then(async (bookingRows) => { const bookings = (bookingRows ?? []).map(mapIntercityBookingFromDatabase); state.intercityBookings = intercityBookingRecords() .filter((item) => item.passengerProfileId !== passengerId) .concat(bookings); const bookingIds = bookings.map((booking) => booking.id); if (!bookingIds.length) { state.intercityBookingMessages = intercityBookingMessageRecords().filter((item) => item.passengerProfileId !== passengerId); intercityTravelerDataLoadedAt = Date.now(); renderPassengerIntercityBookings(); return; } const inFilter = bookingIds.join(","); const messageRows = await supabaseRestRequest(`/rest/v1/cameroon_intercity_booking_messages?booking_id=in.(${inFilter})&select=*&order=created_at.asc&limit=250`); state.intercityBookingMessages = intercityBookingMessageRecords() .filter((item) => item.passengerProfileId !== passengerId && !bookingIds.includes(item.bookingId)) .concat((messageRows ?? []).map(mapIntercityBookingMessageFromDatabase)); intercityTravelerDataLoadedAt = Date.now(); renderPassengerIntercityBookings(); }) .catch((error) => { if (els.passengerIntercityBookingStatus) { els.passengerIntercityBookingStatus.textContent = `Inter-city traveler updates could not load: ${error.message}`; } }) .finally(() => { intercityTravelerLoadPromise = null; }); return intercityTravelerLoadPromise; } function selectedPublicIntercityDeparture() { const earlySelectedDepartureId = typeof window !== "undefined" ? String(window.WAKA_PUBLIC_INTERCITY_SELECTED_DEPARTURE_ID || "") : ""; if (!selectedPublicIntercityDepartureId && earlySelectedDepartureId) selectedPublicIntercityDepartureId = earlySelectedDepartureId; return publicIntercityDepartures().find((departure) => departure.id === selectedPublicIntercityDepartureId) ?? null; } function publicIntercityActiveFilters() { return { agencyId: els.publicIntercityAgency?.value || "", origin: els.publicIntercityOrigin?.value || "", destination: els.publicIntercityDestination?.value || "", date: els.publicIntercityDate?.value || "" }; } function normalizeSelectedPublicIntercitySeatNumbers(departure) { if (departure?.id && !selectedPublicIntercitySeatNumbers.length && typeof window !== "undefined") { const earlySeats = window.WAKA_PUBLIC_INTERCITY_SELECTED_SEATS?.[departure.id]; if (Array.isArray(earlySeats) && earlySeats.length) selectedPublicIntercitySeatNumbers = earlySeats; } const available = new Set(intercityAvailableSeatNumbers(departure)); const normalized = [...new Set(selectedPublicIntercitySeatNumbers)] .map((value) => Number(value) || 0) .filter((value) => value > 0 && available.has(value)) .sort((a, b) => a - b) .slice(0, 12); if (normalized.length !== selectedPublicIntercitySeatNumbers.length || normalized.some((seat, index) => seat !== selectedPublicIntercitySeatNumbers[index])) { selectedPublicIntercitySeatNumbers = normalized; } return normalized; } function updatePublicIntercitySeatSelectionStatus(departure) { const selectedSeats = normalizeSelectedPublicIntercitySeatNumbers(departure); const availableSeats = availableSeatsForIntercityDeparture(departure); if (els.publicIntercitySeatCount) els.publicIntercitySeatCount.value = String(selectedSeats.length); if (els.publicIntercitySeatSelectionStatus) { if (!departure || availableSeats < 1) { els.publicIntercitySeatSelectionStatus.textContent = "No seats are available for this departure."; } else if (!selectedSeats.length) { els.publicIntercitySeatSelectionStatus.textContent = translatedValue("selectSeatsFromMap") || "Select seat numbers from the map before booking."; } else { els.publicIntercitySeatSelectionStatus.textContent = `${translatedValue("selectedSeatsLabel") || "Selected seats"}: ${selectedSeats.join(", ")}`; } } if (els.publicIntercityBookingForm) { const submitButton = els.publicIntercityBookingForm.querySelector('button[type="submit"]'); if (submitButton) submitButton.disabled = availableSeats < 1 || selectedSeats.length < 1 || !els.publicIntercityPaymentMethod?.value; } renderPublicIntercityPassengerManifest(departure); updatePublicIntercityBookingTotalSummary(departure); } function publicIntercityManifestDraftsBySeat() { const drafts = new Map(); els.publicIntercityPassengerManifestList?.querySelectorAll(".intercity-passenger-manifest-card[data-seat-number]").forEach((card) => { const seatNumber = Number(card.dataset.seatNumber || 0) || 0; if (!seatNumber) return; drafts.set(seatNumber, { travelerName: card.querySelector(".intercity-passenger-name")?.value.trim() || "", dateOfBirth: card.querySelector(".intercity-passenger-dob")?.value || "", identityNumber: card.querySelector(".intercity-passenger-id")?.value.trim() || "" }); }); return drafts; } function intercityDateOfBirthIsValid(value) { if (!/^\d{4}-\d{2}-\d{2}$/.test(String(value || ""))) return false; const date = new Date(`${value}T00:00:00Z`); if (!Number.isFinite(date.getTime())) return false; if (date.toISOString().slice(0, 10) !== value) return false; const year = Number(value.slice(0, 4)); return year >= 1900 && date.getTime() <= Date.now(); } function renderPublicIntercityPassengerManifest(departure) { const selectedSeats = normalizeSelectedPublicIntercitySeatNumbers(departure); if (!els.publicIntercityPassengerManifest || !els.publicIntercityPassengerManifestList) return; els.publicIntercityPassengerManifest.hidden = !selectedSeats.length; if (!selectedSeats.length) { els.publicIntercityPassengerManifestList.innerHTML = ""; return; } const drafts = publicIntercityManifestDraftsBySeat(); els.publicIntercityPassengerManifestList.innerHTML = selectedSeats.map((seatNumber, index) => { const draft = drafts.get(seatNumber) || {}; const defaultName = index === 0 ? (els.publicIntercityTravelerName?.value.trim() || state.passenger?.name || "") : ""; const defaultDob = index === 0 ? (state.passenger?.dateOfBirth || els.passengerDob?.value || "") : ""; const defaultId = index === 0 ? (els.publicIntercityTravelerIdentityNumber?.value.trim() || state.passenger?.nationalId || "") : ""; return `
${escapeHtml(`Seat ${seatNumber}`)}
`; }).join(""); } function publicIntercityPassengerManifestForBooking(departure) { const selectedSeats = normalizeSelectedPublicIntercitySeatNumbers(departure); const drafts = publicIntercityManifestDraftsBySeat(); return selectedSeats.map((seatNumber, index) => { const draft = drafts.get(seatNumber) || {}; return { seatNumber, travelerName: draft.travelerName || (index === 0 ? (els.publicIntercityTravelerName?.value.trim() || "") : ""), dateOfBirth: draft.dateOfBirth || (index === 0 ? (state.passenger?.dateOfBirth || els.passengerDob?.value || "") : ""), identityNumber: draft.identityNumber || (index === 0 ? (els.publicIntercityTravelerIdentityNumber?.value.trim() || "") : "") }; }); } function updatePublicIntercityBookingTotalSummary(departure) { const selectedSeats = normalizeSelectedPublicIntercitySeatNumbers(departure); const fareXaf = Number(departure?.fareXaf || 0) || 0; const totalXaf = fareXaf * selectedSeats.length; const agency = departure ? intercityAgencyRecords().find((item) => item.id === departure.agencyId) : null; if (els.publicIntercityBookingTotalPanel) els.publicIntercityBookingTotalPanel.hidden = !departure; if (els.publicIntercityFarePerSeat) { els.publicIntercityFarePerSeat.textContent = fareXaf > 0 ? `${fareXaf.toLocaleString("en-US")} FCFA` : "Confirm with agency"; } if (els.publicIntercitySelectedSeatsSummary) { els.publicIntercitySelectedSeatsSummary.textContent = selectedSeats.length ? selectedSeats.join(", ") : "None"; } if (els.publicIntercityTotalDue) { els.publicIntercityTotalDue.textContent = selectedSeats.length && fareXaf > 0 ? `${totalXaf.toLocaleString("en-US")} FCFA` : "0 FCFA"; } if (els.publicIntercityPaymentInstructions) { els.publicIntercityPaymentInstructions.hidden = !departure; els.publicIntercityPaymentInstructions.innerHTML = departure ? `Pay agency directly${escapeHtml(intercityAgencyDirectPaymentInstruction(agency, els.publicIntercityPaymentMethod?.value || "", totalXaf))}` : ""; } } function renderSelectablePublicIntercitySeatMap(departure) { const selectedSeats = normalizeSelectedPublicIntercitySeatNumbers(departure); renderIntercitySeatMap(els.publicIntercitySeatMap, departure, { selectable: true, selectedSeats, onSeatToggle: togglePublicIntercitySeatSelection }); updatePublicIntercitySeatSelectionStatus(departure); } function togglePublicIntercitySeatSelection(seat) { const departure = selectedPublicIntercityDeparture(); if (!departure) return; const available = new Set(intercityAvailableSeatNumbers(departure)); if (!available.has(seat)) return; const selected = normalizeSelectedPublicIntercitySeatNumbers(departure); if (selected.includes(seat)) { selectedPublicIntercitySeatNumbers = selected.filter((value) => value !== seat); } else if (selected.length >= 12) { if (els.publicIntercitySeatSelectionStatus) { els.publicIntercitySeatSelectionStatus.textContent = "A single booking can include up to 12 seats."; } return; } else { selectedPublicIntercitySeatNumbers = selected.concat(seat).sort((a, b) => a - b); } renderSelectablePublicIntercitySeatMap(departure); } function publicIntercityFiltersReadyForAutoSelect() { const filters = publicIntercityActiveFilters(); return Boolean(filters.origin && filters.destination && filters.date); } function publicIntercityBookingPaymentLabel(method) { if (method === "orange_money") return "Orange Money"; if (method === "mtn_momo") return "MTN Mobile Money"; return translatedValue("payOnArrival") || "Pay on arrival (seat held)"; } function intercityAgencyDirectPaymentInstruction(agency, paymentMethod = "", amountXaf = 0) { if (!agency) return "Confirm the agency payment account before sending money."; const amountLabel = Number(amountXaf || 0) > 0 ? `${Number(amountXaf).toLocaleString("en-US")} FCFA` : "the booking total"; const mtnTarget = agency.mtnMomoNumber ? `${agency.mtnMomoName || agency.companyName} on MTN Mobile Money: ${agency.mtnMomoNumber}` : ""; const orangeTarget = agency.orangeMoneyNumber ? `${agency.orangeMoneyName || agency.companyName} on Orange Money: ${agency.orangeMoneyNumber}` : ""; if (paymentMethod === "pay_later") { return "Pay on arrival keeps selected seats held for this booking. Arrive early and follow the agency payment instructions."; } if (paymentMethod === "mtn_momo" && mtnTarget) return `Pay ${amountLabel} directly to ${mtnTarget}.`; if (paymentMethod === "orange_money" && orangeTarget) return `Pay ${amountLabel} directly to ${orangeTarget}.`; const targets = [mtnTarget, orangeTarget].filter(Boolean); if (targets.length) return `Pay ${amountLabel} directly to the agency: ${targets.join(" or ")}.`; return `Pay ${amountLabel} directly to ${agency.companyName || "the agency"} after confirming the agency mobile-money number.`; } function updatePublicIntercityBookingStatusNotice(selectedDeparture) { if (!els.publicIntercityBookingStatus) return; if (selectedDeparture && availableSeatsForIntercityDeparture(selectedDeparture) < 1) { els.publicIntercityBookingStatus.textContent = "This departure is fully booked right now. Choose another agency, date, or route, or wait for the operator to reopen seats."; return; } const payOnArrivalSelected = els.publicIntercityPaymentMethod?.value === "pay_later"; if (payOnArrivalSelected) { const payOnArrivalStillAvailable = selectedDeparture ? new Date(String(selectedDeparture.departureAt || "")).getTime() - Date.now() >= 60 * 60 * 1000 : false; els.publicIntercityBookingStatus.textContent = payOnArrivalStillAvailable ? (translatedValue("payOnArrivalSelectionWarning") || "Pay on arrival keeps the selected seats held for this booking. Arrive early and follow the agency payment instructions.") : (translatedValue("payOnArrivalClosedWarning") || "Pay on arrival closes one hour before departure. Use mobile money to reserve your selected seats now."); return; } if (selectedDeparture) { if (!els.publicIntercityPaymentMethod?.value) { els.publicIntercityBookingStatus.textContent = "No valid payment option is available for this departure. Choose another departure or ask the agency to update payment settings."; return; } const selectedSeats = normalizeSelectedPublicIntercitySeatNumbers(selectedDeparture); const agency = intercityAgencyRecords().find((item) => item.id === selectedDeparture.agencyId); const totalXaf = Number(selectedDeparture.fareXaf || 0) * selectedSeats.length; els.publicIntercityBookingStatus.textContent = `${intercityAgencyDirectPaymentInstruction(agency, els.publicIntercityPaymentMethod?.value || "", totalXaf)} Your booking receipt will be sent by email.`; return; } els.publicIntercityBookingStatus.textContent = translatedValue("bookingReceiptMessage") || "Your booking receipt will be sent by email. Pay on arrival is available only until one hour before departure, and selected seats are held for the booking."; } function renderPublicIntercityDepartureSummary(selectedDeparture, selectedAgency) { if (!els.publicIntercityDepartureSummary) return; if (!selectedDeparture) { els.publicIntercityDepartureSummary.innerHTML = ` ${escapeHtml(translatedValue("selectedTripDetails") || "Selected trip details")}

${escapeHtml(translatedValue("chooseDepartureSummaryPrompt") || "Select a departure to review the agency, route, boarding point, payment options, fare, and seats left before booking.")}

`; return; } const availableSeats = availableSeatsForIntercityDeparture(selectedDeparture); const paymentOptions = [ selectedDeparture.acceptsMtnMomo && selectedAgency?.mtnMomoNumber ? "MTN Mobile Money" : "", selectedDeparture.acceptsOrangeMoney && selectedAgency?.orangeMoneyNumber ? "Orange Money" : "", selectedDeparture.acceptsPayLater ? (translatedValue("payOnArrival") || "Pay on arrival (seat held)") : "" ].filter(Boolean).join(", "); const guestBookingLabel = selectedAgency?.allowsGuestBooking !== false ? (translatedValue("guestBookingAllowed") || "Guest booking allowed") : (translatedValue("guestBookingDisabled") || "Guest booking disabled"); els.publicIntercityDepartureSummary.innerHTML = ` ${escapeHtml(translatedValue("selectedTripDetails") || "Selected trip details")}
${intercityAgencyLogoMarkup(selectedAgency)}
${escapeHtml(selectedAgency?.companyName || "Agency")} | ${escapeHtml(`${selectedDeparture.originCity || ""} -> ${selectedDeparture.destinationCity || ""}`)} ${escapeHtml(formatDateTime(selectedDeparture.departureAt || ""))} | ${escapeHtml(selectedDeparture.boardingLocation || selectedDeparture.originCity || "")} | ${escapeHtml(`${availableSeats} seat${availableSeats === 1 ? "" : "s"} left`)}
${escapeHtml(translatedValue("agency") || "Agency")}${escapeHtml(selectedAgency?.companyName || "Agency")}
${escapeHtml(translatedValue("route") || "Route")}${escapeHtml(`${selectedDeparture.originCity || ""} -> ${selectedDeparture.destinationCity || ""}`)}
${escapeHtml(translatedValue("origin") || "Departure city")}${escapeHtml(selectedDeparture.originCity || "")}
${escapeHtml(translatedValue("destination") || "Destination city")}${escapeHtml(selectedDeparture.destinationCity || "")}
${escapeHtml(translatedValue("travelDate") || "Travel date")}${escapeHtml(formatDateTime(selectedDeparture.departureAt || ""))}
Bus${escapeHtml(selectedDeparture.busLabel || intercityBusClassLabel(selectedDeparture.seatCapacity))}
Capacity${escapeHtml(intercityCapacityLabel(selectedDeparture))}
${escapeHtml(translatedValue("boardingLocationLabel") || "Boarding point")}${escapeHtml(selectedDeparture.boardingLocation || "")}
${escapeHtml(translatedValue("dropoffLocationLabel") || "Drop-off point")}${escapeHtml(selectedDeparture.dropoffLocation || selectedDeparture.destinationCity || "")}
${escapeHtml(translatedValue("seatsLeftLabel") || "Seats left")}${escapeHtml(`${availableSeats} of ${selectedDeparture.seatCapacity}`)}
Held seats${escapeHtml(intercityBlockedSeatLabel(selectedDeparture))}
${escapeHtml(translatedValue("farePerSeat") || "Fare per seat (FCFA)")}${selectedDeparture.fareXaf > 0 ? escapeHtml(`${selectedDeparture.fareXaf.toLocaleString("en-US")} FCFA`) : "Call operator"}
${escapeHtml(translatedValue("paymentOption") || "Payment option")}${escapeHtml(paymentOptions || "Ask agency")}
Payment goes to${escapeHtml(intercityAgencyDirectPaymentInstruction(selectedAgency, els.publicIntercityPaymentMethod?.value || "", 0))}
${escapeHtml(translatedValue("travelStatusLabel") || "Travel status")}${escapeHtml(selectedDeparture.travelStatus || "scheduled")}
${escapeHtml(translatedValue("guestBookingLabel") || "Guest booking")}${escapeHtml(guestBookingLabel)}
${selectedDeparture.busLabel ? `${escapeHtml(`${translatedValue("busVehicleLabel") || "Bus / vehicle label"}: ${selectedDeparture.busLabel}`)}` : ""} ${selectedAgency?.terminalAddress ? `${escapeHtml(`${translatedValue("agencyTerminalLabel") || "Agency terminal"}: ${selectedAgency.terminalAddress}`)}` : ""} ${selectedAgency?.supportPhone ? `${escapeHtml(`${translatedValue("supportPhone") || "Support phone"}: ${selectedAgency.supportPhone}`)}` : ""} ${selectedDeparture.notes ? `${escapeHtml(selectedDeparture.notes)}` : ""} `; } function renderPublicIntercityPlanner() { if (!els.publicIntercityDepartureList) return; if (!publicIntercityCatalogLoadedAt) adoptLoaderPublicIntercityCatalog({ render: false }); ensurePublicIntercityCatalogControlsWired(); if (!intercityAgencyRecords().length && !hasSupabaseConfig()) state.intercityAgencies = mergeIntercityAgencyDirectory(); populateCameroonIntercityCityOptions(); setPublicIntercityAgencyOptions(els.publicIntercityAgency); setIntercityCityOptions(els.publicIntercityOrigin); setIntercityCityOptions(els.publicIntercityDestination); const agencyPageAgency = publicAgencyPageAgency(); const requestedAgencyId = publicIntercityRequestedAgencyIdFromLocation(); if (els.publicIntercityAgency) { if (agencyPageAgency) els.publicIntercityAgency.value = agencyPageAgency.id; else if (requestedAgencyId && [...els.publicIntercityAgency.options].some((option) => option.value === requestedAgencyId)) { els.publicIntercityAgency.value = requestedAgencyId; } els.publicIntercityAgency.disabled = Boolean(agencyPageAgency) || (publicIntercityCatalogIsLoading() && !publicIntercityAgencies().length); } renderPublicIntercityDiscoverySummary(); renderPublicAgencyDirectoryPanel(); renderPublicAgencyPage(); if (!publicIntercityCatalogLoadedAt && !intercityCatalogLoadPromise) void loadPublicIntercityCatalog(); if (state.passenger) { if (els.publicIntercityTravelerName && !els.publicIntercityTravelerName.value.trim()) els.publicIntercityTravelerName.value = state.passenger.name || ""; if (els.publicIntercityTravelerEmail && !els.publicIntercityTravelerEmail.value.trim()) els.publicIntercityTravelerEmail.value = state.passenger.email || ""; if (els.publicIntercityTravelerPhone && !els.publicIntercityTravelerPhone.value.trim()) els.publicIntercityTravelerPhone.value = state.passenger.phone || ""; } let departures = publicIntercityDepartures(); if (!selectedPublicIntercityDepartureId && departures.length === 1 && publicIntercityFiltersReadyForAutoSelect()) { selectedPublicIntercityDepartureId = departures[0].id; departures = publicIntercityDepartures(); } const visibleAgencies = publicIntercityAgencies(); const allDepartures = allVisiblePublicIntercityDepartures(); els.publicIntercityDepartureList.innerHTML = ""; if (!departures.length) { const emptyMessage = publicIntercityCatalogIsLoading() ? "Loading approved transport agencies and public departures..." : publicIntercityCatalogLoadedAt && !visibleAgencies.length && !allDepartures.length ? "No transport agency has published a live departure yet. Agency setup is ready; once an operator saves active routes, travelers will see the company, travel date, route, seats, and payment options here." : "No inter-city departures match the current agency, route, or travel date."; els.publicIntercityDepartureList.append(emptyState(emptyMessage)); if (els.publicIntercitySearchStatus) { if (publicIntercityCatalogIsLoading()) { els.publicIntercitySearchStatus.textContent = "Loading verified agency departures."; } else if (publicIntercityCatalogLoadedAt && !visibleAgencies.length && !allDepartures.length) { els.publicIntercitySearchStatus.textContent = "No live inter-city operator has published a departure yet."; } else if (publicIntercityCatalogLoadedAt && visibleAgencies.length && !allDepartures.length) { els.publicIntercitySearchStatus.textContent = `${visibleAgencies.length} operator${visibleAgencies.length === 1 ? "" : "s"} listed. Live departures have not been published yet.`; } else { els.publicIntercitySearchStatus.textContent = publicIntercityCatalogLoadedAt ? "No departure matches that agency, city pair, or travel date yet." : "Loading verified agency departures."; } } if (visibleAgencies.length && !allDepartures.length) { visibleAgencies.forEach((agency) => { const item = document.createElement("article"); item.className = "intercity-departure-card"; const cities = agency.operatedCities.length ? agency.operatedCities.join(", ") : agency.hqCity || "Cameroon"; item.innerHTML = `
${intercityAgencyLogoMarkup(agency)}
${escapeHtml(agency.hqCity || "Cameroon")} ${escapeHtml(agency.companyName || "Transport agency")}

${escapeHtml(agency.description || "Agency directory entry loaded for traveler discovery.")}

Coverage${escapeHtml(cities)}
StatusDirectory listed. Waiting for live departures.
${escapeHtml(translatedValue("boardingLocationLabel") || "Boarding point")}${escapeHtml(agency.terminalAddress || "Agency boarding details will appear after schedule publishing.")}
${escapeHtml(translatedValue("supportPhone") || "Support phone")}${escapeHtml(agency.supportPhone || "Operator phone will appear after onboarding confirmation.")}
`; els.publicIntercityDepartureList.append(item); }); } } else { if (els.publicIntercitySearchStatus) { const agencyCount = new Set(departures.map((departure) => departure.agencyId)).size; els.publicIntercitySearchStatus.textContent = `${departures.length} departure${departures.length === 1 ? "" : "s"} found across ${agencyCount} agenc${agencyCount === 1 ? "y" : "ies"}. Compare the operator, date, time, route, and seats before booking.`; } departures.forEach((departure) => { const agency = intercityAgencyRecords().find((item) => item.id === departure.agencyId); const agencyPageUrl = agencyPublicPageUrl(agency); const item = document.createElement("article"); item.className = "intercity-departure-card"; const availableSeats = availableSeatsForIntercityDeparture(departure); const soldOut = availableSeats < 1; const paymentOptions = [ departure.acceptsMtnMomo && agency?.mtnMomoNumber ? "MTN Mobile Money" : "", departure.acceptsOrangeMoney && agency?.orangeMoneyNumber ? "Orange Money" : "", departure.acceptsPayLater ? (translatedValue("payOnArrival") || "Pay on arrival (seat held)") : "" ].filter(Boolean).join(", "); const tripRoute = [departure.boardingLocation, departure.dropoffLocation || departure.destinationCity].filter(Boolean).join(" -> "); item.innerHTML = `
${intercityAgencyLogoMarkup(agency)}
${escapeHtml(agency?.companyName || "Agency")} ${escapeHtml(`${departure.originCity} -> ${departure.destinationCity}`)}

${escapeHtml(departure.busLabel ? `${departure.busLabel} | ${tripRoute}` : tripRoute)}

Departure${escapeHtml(formatDateTime(departure.departureAt))}
Bus${escapeHtml(departure.busLabel || intercityBusClassLabel(departure.seatCapacity))}
Capacity${escapeHtml(intercityCapacityLabel(departure))}
${escapeHtml(translatedValue("boardingLocationLabel") || "Boarding point")}${escapeHtml(departure.boardingLocation || departure.originCity || "")}
${escapeHtml(translatedValue("dropoffLocationLabel") || "Drop-off point")}${escapeHtml(departure.dropoffLocation || departure.destinationCity || "")}
Seats left${escapeHtml(String(availableSeats))} of ${escapeHtml(String(departure.seatCapacity))}
Held seats${escapeHtml(intercityBlockedSeatLabel(departure))}
Availability${escapeHtml(soldOut ? "Sold out" : "Open for booking")}
${escapeHtml(translatedValue("farePerSeat") || "Fare per seat (FCFA)")}${departure.fareXaf > 0 ? escapeHtml(`${departure.fareXaf.toLocaleString("en-US")} FCFA / seat`) : "Call operator"}
${escapeHtml(translatedValue("paymentOption") || "Payment option")}${escapeHtml(paymentOptions || "Ask agency")}
Payment goes to${escapeHtml(intercityAgencyDirectPaymentInstruction(agency, "", 0))}
${escapeHtml(translatedValue("travelStatusLabel") || "Travel status")}${escapeHtml(departure.travelStatus || "scheduled")}
${escapeHtml(translatedValue("guestBookingLabel") || "Guest booking")}${escapeHtml(agency?.allowsGuestBooking !== false ? (translatedValue("guestBookingAllowed") || "Allowed") : (translatedValue("guestBookingDisabled") || "Disabled"))}
${departure.notes ? `${escapeHtml(departure.notes)}` : ""} ${intercityAgencyHasPublicPage(agency) ? `Open ${escapeHtml(agency.companyName || "agency")} page` : ""}
`; renderIntercitySeatMap(item.querySelector(".intercity-seat-map"), departure); item.querySelector(".public-intercity-select")?.addEventListener("click", () => { selectedPublicIntercityDepartureId = departure.id; selectedPublicIntercitySeatNumbers = []; renderPublicIntercityPlanner(); }); item.querySelector(".public-intercity-book")?.addEventListener("click", () => { selectedPublicIntercityDepartureId = departure.id; selectedPublicIntercitySeatNumbers = []; renderPublicIntercityPlanner(); els.publicIntercityBookingPanel?.scrollIntoView({ behavior: "smooth", block: "start" }); }); els.publicIntercityDepartureList.append(item); }); } const selectedDeparture = selectedPublicIntercityDeparture(); if (!selectedDeparture) { selectedPublicIntercitySeatNumbers = []; if (els.publicIntercityBookingPanel) els.publicIntercityBookingPanel.hidden = !allDepartures.length; if (els.publicIntercitySelectedDeparture) { els.publicIntercitySelectedDeparture.textContent = allDepartures.length ? translatedValue("chooseDepartureToUnlockBooking") || "Choose a departure from the list. Traveler details, seat selection, and payment open here after selection." : "Booking opens after an agency publishes a live departure."; } if (els.publicIntercityBookingForm) { els.publicIntercityBookingForm.hidden = true; const submitButton = els.publicIntercityBookingForm.querySelector('button[type="submit"]'); if (submitButton) submitButton.disabled = true; } renderIntercitySeatMap(els.publicIntercitySeatMap, null); updatePublicIntercitySeatSelectionStatus(null); renderPublicIntercityDepartureSummary(null, null); updatePublicIntercityBookingStatusNotice(null); return; } const selectedAgency = intercityAgencyRecords().find((agency) => agency.id === selectedDeparture.agencyId); if (els.publicIntercityAgency) els.publicIntercityAgency.value = selectedDeparture.agencyId || ""; if (els.publicIntercityOrigin) els.publicIntercityOrigin.value = selectedDeparture.originCity || ""; if (els.publicIntercityDestination) els.publicIntercityDestination.value = selectedDeparture.destinationCity || ""; if (els.publicIntercityDate) els.publicIntercityDate.value = intercityDepartureDateKey(selectedDeparture); const availableSeats = availableSeatsForIntercityDeparture(selectedDeparture); if (els.publicIntercityBookingPanel) els.publicIntercityBookingPanel.hidden = false; if (els.publicIntercityBookingForm) { els.publicIntercityBookingForm.hidden = false; const submitButton = els.publicIntercityBookingForm.querySelector('button[type="submit"]'); if (submitButton) submitButton.disabled = availableSeats < 1; } const payOnArrivalStillAvailable = new Date(String(selectedDeparture.departureAt || "")).getTime() - Date.now() >= 60 * 60 * 1000; const mtnOption = els.publicIntercityPaymentMethod?.querySelector('option[value="mtn_momo"]'); const orangeOption = els.publicIntercityPaymentMethod?.querySelector('option[value="orange_money"]'); const mobilePaymentAvailability = { mtn_momo: selectedDeparture.acceptsMtnMomo && Boolean(selectedAgency?.mtnMomoNumber), orange_money: selectedDeparture.acceptsOrangeMoney && Boolean(selectedAgency?.orangeMoneyNumber) }; if (mtnOption) mtnOption.disabled = !mobilePaymentAvailability.mtn_momo; if (orangeOption) orangeOption.disabled = !mobilePaymentAvailability.orange_money; const payOnArrivalOption = els.publicIntercityPaymentMethod?.querySelector('option[value="pay_later"]'); if (payOnArrivalOption) { payOnArrivalOption.disabled = !payOnArrivalStillAvailable; payOnArrivalOption.textContent = payOnArrivalStillAvailable ? (translatedValue("payOnArrival") || "Pay on arrival (seat held)") : (translatedValue("payOnArrivalClosedOption") || "Pay on arrival (closed within 1 hour)"); if (!payOnArrivalStillAvailable && els.publicIntercityPaymentMethod?.value === "pay_later") { els.publicIntercityPaymentMethod.value = mobilePaymentAvailability.mtn_momo ? "mtn_momo" : mobilePaymentAvailability.orange_money ? "orange_money" : ""; } } const selectedPaymentOption = els.publicIntercityPaymentMethod?.selectedOptions?.[0]; if (els.publicIntercityPaymentMethod && selectedPaymentOption?.disabled) { els.publicIntercityPaymentMethod.value = mobilePaymentAvailability.mtn_momo ? "mtn_momo" : mobilePaymentAvailability.orange_money ? "orange_money" : payOnArrivalStillAvailable ? "pay_later" : ""; } if (els.publicIntercitySelectedDeparture) { els.publicIntercitySelectedDeparture.textContent = availableSeats < 1 ? `${selectedAgency?.companyName || "Agency"} | ${selectedDeparture.originCity} -> ${selectedDeparture.destinationCity} | ${formatDateTime(selectedDeparture.departureAt)} | Sold out right now. Choose another departure or wait for the operator to reopen seats.` : `${selectedAgency?.companyName || "Agency"} | ${selectedDeparture.originCity} -> ${selectedDeparture.destinationCity} | ${formatDateTime(selectedDeparture.departureAt)} | ${availableSeats} seat${availableSeats === 1 ? "" : "s"} left. Guest booking is ${selectedAgency?.allowsGuestBooking !== false ? "allowed" : "disabled"}, and pay-on-arrival seat holds close one hour before departure.`; } renderPublicIntercityDepartureSummary(selectedDeparture, selectedAgency); renderSelectablePublicIntercitySeatMap(selectedDeparture); updatePublicIntercityBookingStatusNotice(selectedDeparture); } function renderPassengerIntercityBookings() { if (!els.passengerIntercityBookingList) return; if (hasSignedIn("passenger") && !intercityTravelerDataLoadedAt && !intercityTravelerLoadPromise) { void loadPassengerIntercityTravelerData(); } const passengerId = state.passenger?.id; const bookings = intercityBookingRecords() .filter((booking) => passengerId && booking.passengerProfileId === passengerId) .sort((a, b) => new Date(b.createdAt ?? 0) - new Date(a.createdAt ?? 0)); els.passengerIntercityBookingList.innerHTML = ""; if (!bookings.length) { els.passengerIntercityBookingList.append(emptyState("No inter-city bus booking is linked to this signed-in passenger yet.")); return; } if (els.passengerIntercityBookingStatus) { els.passengerIntercityBookingStatus.textContent = "Seat numbers, payment state, operator notes, and travel messages update here for signed-in traveler bookings."; } bookings.forEach((booking) => { const snapshot = booking.departureSnapshot || {}; const departure = intercityDepartureRecords().find((item) => item.id === booking.departureId) ?? null; const bookingAgency = intercityAgencyRecords().find((agency) => agency.id === booking.agencyId) || { companyName: snapshot.companyName, logoPath: snapshot.logoPath, logoUrl: snapshot.logoUrl, logoAltText: snapshot.logoAltText }; const item = document.createElement("article"); item.className = "intercity-booking-card"; const messages = intercityMessagesForBooking(booking.id); item.innerHTML = `
${intercityAgencyLogoMarkup(bookingAgency)}
${escapeHtml(snapshot.companyName || agencyNameForIntercity(booking.agencyId))} ${escapeHtml(`${snapshot.originCity || ""} -> ${snapshot.destinationCity || ""}`)}

${escapeHtml(booking.bookingReference)}${booking.seatNumbers.length ? ` | Seats ${escapeHtml(booking.seatNumbers.join(", "))}` : ""}

Departure${escapeHtml(formatDateTime(snapshot.departureAt || departure?.departureAt || ""))}
Status${escapeHtml(booking.bookingStatus)}
Payment${escapeHtml(booking.paymentStatus)}
Amount${escapeHtml(`${Number(booking.amountXaf || 0).toLocaleString("en-US")} FCFA`)}
${booking.paymentProviderMessage || booking.paymentProviderReference || booking.paymentCheckoutUrl ? `${escapeHtml([booking.paymentProviderMessage, booking.paymentProviderReference ? `Ref ${booking.paymentProviderReference}` : "", booking.agencySettlementStatus && booking.agencySettlementStatus !== "not_started" ? `Agency settlement ${booking.agencySettlementStatus}` : ""].filter(Boolean).join(" | "))}` : ""} ${booking.paymentCheckoutUrl ? `Open mobile-money checkout` : ""}
${booking.operatorNote ? `${escapeHtml(booking.operatorNote)}` : ""} ${messages.length ? `
${messages.slice(-3).map((message) => `
${escapeHtml(message.senderRole)}

${escapeHtml(message.messageBody)}

${escapeHtml(formatDateTime(message.createdAt))}
`).join("")}
` : `No operator message yet.`} `; renderIntercitySeatMap(item.querySelector(".intercity-seat-map"), departure || { id: booking.departureId, seatCapacity: Math.max(...booking.seatNumbers, booking.seatCount, 4), blockedSeatNumbers: [], reservedSeats: booking.seatCount }, { highlightSeats: booking.seatNumbers }); els.passengerIntercityBookingList.append(item); }); } async function callIntercityOperatorAction(payload) { const response = await fetch(`${appConfig.supabaseUrl}/functions/v1/${intercityOperatorActionFunctionName()}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${supabaseRestSession?.access_token || appConfig.supabaseAnonKey}`, "content-type": "application/json" }, body: JSON.stringify(payload) }); const body = await response.json().catch(() => ({})); if (!response.ok) throw new Error(body?.error || "Inter-city operator action failed."); return body; } async function updateIntercityDepartureOperations(departureId, fields = {}) { if (!departureId) return; try { if (els.intercityDepartureStatus) els.intercityDepartureStatus.textContent = "Saving departure operations update..."; try { await callIntercityOperatorAction({ action: "update_departure", departureId, ...fields }); } catch (operatorError) { logClientWarning("Inter-city operator action failed; trying owner-scoped direct departure update.", operatorError); const patch = { updated_at: new Date().toISOString() }; if (typeof fields.active === "boolean") patch.active = fields.active; if ("travelStatus" in fields) patch.travel_status = fields.travelStatus || "scheduled"; if ("statusNote" in fields) patch.status_note = fields.statusNote || ""; if ("blockedSeatNumbers" in fields) patch.blocked_seat_numbers = fields.blockedSeatNumbers || []; await supabaseRestRequest(`/rest/v1/cameroon_intercity_departures?id=eq.${encodeURIComponent(departureId)}`, { method: "PATCH", body: patch, headers: { Prefer: "return=minimal" } }); } await loadOwnedIntercityOperatorData({ force: true }); await loadPublicIntercityCatalog({ force: true }); if (els.intercityDepartureStatus) els.intercityDepartureStatus.textContent = "Departure operations updated."; } catch (error) { if (els.intercityDepartureStatus) els.intercityDepartureStatus.textContent = `Departure operations could not be updated: ${error.message}`; } } async function cancelIntercityDeparture(departure) { if (!departure?.id) return; const activeBookings = intercityDepartureBookings(departure.id) .filter((booking) => !["cancelled", "expired"].includes(String(booking.bookingStatus || "").toLowerCase())); const routeLabel = `${departure.originCity || "Origin"} -> ${departure.destinationCity || "Destination"}`; const bookingWarning = activeBookings.length ? ` This departure has ${activeBookings.length} active booking${activeBookings.length === 1 ? "" : "s"}; notify travelers before cancelling.` : ""; const ok = typeof showWakaGoodConfirm === "function" ? await showWakaGoodConfirm(`Cancel ${routeLabel}? It will be removed from public search and booking.${bookingWarning}`) : window.confirm(`Cancel ${routeLabel}? It will be removed from public search and booking.${bookingWarning}`); if (!ok) return; await updateIntercityDepartureOperations(departure.id, { active: false, travelStatus: "cancelled", statusNote: "Cancelled by agency operator." }); } async function deleteIntercityDeparture(departure) { if (!departure?.id) return; const activeBookings = intercityDepartureBookings(departure.id) .filter((booking) => !["cancelled", "expired"].includes(String(booking.bookingStatus || "").toLowerCase())); const routeLabel = `${departure.originCity || "Origin"} -> ${departure.destinationCity || "Destination"}`; if (activeBookings.length) { if (els.intercityDepartureStatus) { els.intercityDepartureStatus.textContent = `This departure has ${activeBookings.length} active booking${activeBookings.length === 1 ? "" : "s"}. Cancel or update the trip after notifying travelers; permanent deletion is blocked while bookings exist.`; } return; } const ok = typeof showWakaGoodConfirm === "function" ? await showWakaGoodConfirm(`Delete ${routeLabel}? This removes the duplicate departure from the agency workspace and public search when no bookings exist.`) : window.confirm(`Delete ${routeLabel}? This removes the duplicate departure from the agency workspace and public search when no bookings exist.`); if (!ok) return; try { if (els.intercityDepartureStatus) els.intercityDepartureStatus.textContent = "Deleting departure..."; try { await supabaseRestRequest(`/rest/v1/cameroon_intercity_departures?id=eq.${encodeURIComponent(departure.id)}`, { method: "DELETE", headers: { Prefer: "return=minimal" } }); state.intercityDepartures = intercityDepartureRecords().filter((item) => item.id !== departure.id); saveState(); await loadOwnedIntercityOperatorData({ force: true }); await loadPublicIntercityCatalog({ force: true }); if (els.intercityDepartureStatus) els.intercityDepartureStatus.textContent = "Departure deleted. It has been removed from the agency workspace and public search."; } catch (deleteError) { logClientWarning("Permanent departure delete failed; cancelling and hiding it instead.", deleteError); await updateIntercityDepartureOperations(departure.id, { active: false, travelStatus: "cancelled", statusNote: "Removed from public search by agency operator." }); state.intercityDepartures = intercityDepartureRecords().filter((item) => item.id !== departure.id); saveState(); renderIntercityOperatorPanels(); if (els.intercityDepartureStatus) { els.intercityDepartureStatus.textContent = "Departure removed from public search. The database did not allow permanent deletion from this browser session, so Waka marked it cancelled and hid it from travelers."; } } } catch (error) { if (els.intercityDepartureStatus) els.intercityDepartureStatus.textContent = `Departure could not be deleted: ${error.message}`; } } async function updateIntercityBookingOperations(bookingId, fields = {}) { if (!bookingId) return; try { if (els.intercityBookingStatus) els.intercityBookingStatus.textContent = "Saving booking action..."; await callIntercityOperatorAction({ action: "update_booking", bookingId, ...fields }); await loadOwnedIntercityOperatorData({ force: true }); await loadPublicIntercityCatalog({ force: true }); await loadPassengerIntercityTravelerData({ force: true }).catch(() => {}); if (els.intercityBookingStatus) els.intercityBookingStatus.textContent = "Booking action saved."; } catch (error) { if (els.intercityBookingStatus) els.intercityBookingStatus.textContent = `Booking action could not be saved: ${error.message}`; } } async function sendIntercityTravelerMessage(bookingId) { const messageBody = await showWakaGoodPrompt("Enter the message to send to this traveler."); if (!messageBody || !String(messageBody).trim()) return; try { if (els.intercityBookingStatus) els.intercityBookingStatus.textContent = "Sending traveler update..."; await callIntercityOperatorAction({ action: "send_message", bookingId, messageBody: String(messageBody).trim(), deliveryChannels: ["app", "email"] }); await loadOwnedIntercityOperatorData({ force: true }); await loadPassengerIntercityTravelerData({ force: true }).catch(() => {}); if (els.intercityBookingStatus) els.intercityBookingStatus.textContent = "Traveler update sent."; } catch (error) { if (els.intercityBookingStatus) els.intercityBookingStatus.textContent = `Traveler update could not be sent: ${error.message}`; } } async function sendIntercityAgencyMessage(agencyId, { subject = "", statusNode = null } = {}) { if (!agencyId) return; const messageBody = await showWakaGoodPrompt("Enter the message to send to this agency.", ""); if (!messageBody || !String(messageBody).trim()) return; const targetStatus = statusNode || els.intercityMessageStatus; try { if (targetStatus) targetStatus.textContent = "Sending agency message..."; await callIntercityOperatorAction({ action: "send_agency_message", agencyId, subject: subject || "Agency operations update", messageBody: String(messageBody).trim(), deliveryChannels: ["app", "email"] }); if (hasSignedIn("passenger")) { await loadOwnedIntercityOperatorData({ force: true }).catch(() => {}); } if (targetStatus) targetStatus.textContent = "Agency message sent."; } catch (error) { if (targetStatus) targetStatus.textContent = `Agency message could not be sent: ${error.message}`; } } function renderIntercityOperatorPanels() { if (!els.intercityAgencyList) return; ensureIntercityDepartureSaveFallbackWired(); scheduleIntercityOperatorAutoRefresh(); if (agencyWorkspaceRouteActive() && !hasSignedIn("passenger") && !agencyWorkspaceSessionPromise) { void ensureAgencyWorkspaceSession().then((passenger) => { if (passenger && typeof renderAll === "function") renderAll(); }); } if (hasSignedIn("passenger") && intercityAgencyOwnerProfileId() && !intercityOwnerDataLoadedAt && !intercityOwnerLoadPromise) { void loadOwnedIntercityOperatorData(); } populateCameroonIntercityCityOptions(); const agencies = ownedIntercityAgencies(); if (els.agencyPasswordChangeForm) { els.agencyPasswordChangeForm.hidden = !agencies.length; } if (els.intercityAgencyPaymentMonth && !els.intercityAgencyPaymentMonth.value) { els.intercityAgencyPaymentMonth.value = new Date().toISOString().slice(0, 7); } setOwnedIntercityAgencyOptions(els.intercityAgencyPaymentSelect); setOwnedIntercityAgencyOptions(els.intercityDepartureAgency); setOwnedIntercityAgencyOptions(els.intercityAdvertisingAgency); const payments = intercityAgencyPaymentRecords() .filter((payment) => agencies.some((agency) => agency.id === payment.agencyId)) .sort((a, b) => new Date(b.billingPeriodStart || 0) - new Date(a.billingPeriodStart || 0)); const departures = intercityDepartureRecords() .filter((departure) => agencies.some((agency) => agency.id === departure.agencyId)) .sort((a, b) => new Date(a.departureAt ?? 0) - new Date(b.departureAt ?? 0)); const bookings = intercityBookingRecords() .filter((booking) => agencies.some((agency) => agency.id === booking.agencyId)) .sort((a, b) => new Date(b.createdAt ?? 0) - new Date(a.createdAt ?? 0)); const reports = buildIntercityOperatorTripReports(departures, bookings); const advertisingRequests = intercityAdvertisingRequestRecords() .filter((request) => agencies.some((agency) => agency.id === request.agencyId)) .sort((a, b) => new Date(b.createdAt ?? 0) - new Date(a.createdAt ?? 0)); const messages = intercityBookingMessageRecords() .filter((message) => agencies.some((agency) => agency.id === message.agencyId)) .concat(intercityAgencyMessageRecords().filter((message) => agencies.some((agency) => agency.id === message.agencyId))) .sort((a, b) => new Date(b.createdAt ?? 0) - new Date(a.createdAt ?? 0)); const activeDepartures = departures.filter((departure) => departure.active !== false); const upcomingDepartures = activeDepartures.filter((departure) => new Date(departure.departureAt || 0).getTime() >= Date.now()); const latestPayment = payments[0] ?? null; const pendingApprovalAgencies = agencies.filter((agency) => intercityAgencyApprovalPending(agency)); const publishableAgencies = agencies.filter((agency) => intercityAgencyCanOperate(agency)); const serviceState = agencies.length ? agencies.map((agency) => intercityAgencyServiceStatus(agency)).join(" | ") : "Not started"; let nextAction = translatedValue("operatorNextActionStart") || "Start by saving the company profile with support contacts, operated cities, and terminal address."; if (pendingApprovalAgencies.length) { nextAction = "Wait for Waka admin to approve the agency profile before publishing any departure."; } else if (agencies.length && !departures.length) { nextAction = translatedValue("operatorNextActionPublish") || "Next, open Weekly departures and publish at least one route with the destination city, departure time, seats, and payment options."; } else if (agencies.length && departures.length && !payments.length) { nextAction = translatedValue("operatorNextActionPayment") || "Your free month can publish now. Submit the monthly fee record before the trial ends so future departures stay visible."; } else if (agencies.length && departures.length) { nextAction = translatedValue("operatorNextActionManage") || "Keep departures updated, block seats when needed, and manage traveler bookings below."; } const filteredAgencies = agencies.filter((agency) => ( intercityOperatorAgencyMatches(agency.id) && intercityOperatorStatusMatches("profile", agency) && intercityOperatorSearchMatches( agency.companyName, agency.description, agency.hqCity, agency.terminalAddress, agency.supportEmail, agency.supportPhone, agency.operatedCities.join(" ") ) )); const filteredPayments = payments.filter((payment) => ( intercityOperatorAgencyMatches(payment.agencyId) && intercityOperatorStatusMatches("payments", payment) && intercityOperatorSearchMatches( agencyNameForIntercity(payment.agencyId), payment.billingPeriodStart, payment.provider, payment.providerReference, payment.payerName, payment.payerPhone, payment.status, payment.reviewNote ) )); const filteredDepartures = departures.filter((departure) => ( intercityOperatorAgencyMatches(departure.agencyId) && intercityOperatorStatusMatches("departures", departure) && intercityOperatorSearchMatches( agencyNameForIntercity(departure.agencyId), departure.originCity, departure.destinationCity, departure.boardingLocation, departure.dropoffLocation, departure.busLabel, departure.statusNote, departure.notes ) )); const filteredAdvertisingRequests = advertisingRequests.filter((request) => ( intercityOperatorAgencyMatches(request.agencyId) && intercityOperatorStatusMatches("advertising", request) && intercityOperatorSearchMatches( agencyNameForIntercity(request.agencyId), request.campaignName, request.placement, request.provider, request.providerReference, request.payerName, request.payerPhone, request.note, request.status ) )); const filteredBookings = bookings.filter((booking) => { const snapshot = booking.departureSnapshot || {}; return intercityOperatorAgencyMatches(booking.agencyId) && intercityOperatorStatusMatches("bookings", booking) && intercityOperatorSearchMatches( booking.travelerName, booking.travelerIdentityNumber, booking.travelerDateOfBirth, intercityPassengerManifestSummary(booking), booking.travelerEmail, booking.travelerPhone, booking.bookingReference, snapshot.companyName, snapshot.originCity, snapshot.destinationCity, booking.operatorNote, booking.seatNumbers.join(", ") ); }); const filteredReports = reports.filter((report) => ( intercityOperatorAgencyMatches(report.agencyId) && intercityOperatorStatusMatches("reports", report) && intercityOperatorSearchMatches( agencyNameForIntercity(report.agencyId), report.routeLabel, report.busLabel, report.travelStatus, report.bookings.map((booking) => [ booking.travelerName, booking.travelerIdentityNumber, booking.travelerDateOfBirth, intercityPassengerManifestSummary(booking), booking.travelerPhone, booking.travelerEmail, booking.bookingReference, booking.paymentStatus, booking.paymentMethod, intercityPassengerManifestSummary(booking), booking.seatNumbers.join(", ") ].join(" ")).join(" ") ) )); const filteredMessages = messages.filter((message) => { const booking = bookings.find((item) => item.id === message.bookingId) ?? null; const snapshot = booking?.departureSnapshot || {}; return intercityOperatorAgencyMatches(message.agencyId) && intercityOperatorStatusMatches("messages", message) && intercityOperatorSearchMatches( message.subject, message.messageBody, message.senderRole, agencyNameForIntercity(message.agencyId), booking?.travelerName, booking?.bookingReference, snapshot.companyName, snapshot.originCity, snapshot.destinationCity ); }); renderIntercityOperatorConsole({ agencies, payments, departures, bookings, reports, advertisingRequests, messages, filteredCounts: { profile: filteredAgencies.length, departures: filteredDepartures.length, bookings: filteredBookings.length, reports: filteredReports.length, payments: filteredPayments.length, advertising: filteredAdvertisingRequests.length, messages: filteredMessages.length }, nextAction }); if (els.intercityAgencyChecklist) { els.intercityAgencyChecklist.innerHTML = `
${escapeHtml(translatedValue("agencySetupChecklistTitle") || "Agency setup checklist")}

${escapeHtml(agencies[0]?.companyName || "Agency workspace")}

${escapeHtml(translatedValue("agencySetupChecklistBody") || "Use this workspace to keep your public agency page accurate, publish departures, track bookings, manage seats, and submit operator payments.")}

${escapeHtml(serviceState)}
${escapeHtml(agencies.length ? `${agencies.length}` : "0")} ${escapeHtml(translatedValue("agencyProfileStatusLabel") || "Agency profile")}
${escapeHtml(`${upcomingDepartures.length}`)} ${escapeHtml(translatedValue("publishedDeparturesLabel") || "Upcoming departures")}
${escapeHtml(`${bookings.length}`)} ${escapeHtml(translatedValue("travelerBookingsLabel") || "Traveler bookings")}
${escapeHtml(latestPayment ? latestPayment.status : "None")} ${escapeHtml(translatedValue("latestOperatorPaymentLabel") || "Latest operator payment")}
${escapeHtml("Next action")}

${escapeHtml(nextAction)}

${escapeHtml("Public agency page")} ${escapeHtml("Your public company profile, logo, terminal details, routes, live departures, seat availability, and booking access. Waka admin can publish, hide, or remove it.")}
${escapeHtml("Advertising")} ${escapeHtml("Optional promotions, pictures, campaigns, and announcements. These are separate from the agency page and require Waka admin approval before public display.")}
`; els.intercityAgencyChecklist.querySelectorAll("[data-operator-jump]").forEach((button) => { button.addEventListener("click", () => setIntercityOperatorWorkspaceTab(button.dataset.operatorJump)); }); } els.intercityAgencyList.innerHTML = ""; if (!agencies.length) { updateIntercityAgencyLogoPreview(null); els.intercityAgencyList.append(emptyState(agencyWorkspaceActive() ? "No approved agency is linked to this agency login yet. Contact Waka admin to approve or connect the agency record before publishing departures." : "No agency profile yet. Save your company details above to begin weekly departure publishing.")); } else if (!filteredAgencies.length) { els.intercityAgencyList.append(emptyState("No agency profile matches the current filters.")); } else { const primaryAgency = (intercityOperatorAgencyFilterValue !== "all" ? agencies.find((agency) => agency.id === intercityOperatorAgencyFilterValue) : null) || filteredAgencies[0] || agencies[0]; if (els.intercityAgencyName && !els.intercityAgencyName.value.trim()) els.intercityAgencyName.value = primaryAgency.companyName || ""; if (els.intercityAgencySlug && !els.intercityAgencySlug.value.trim()) els.intercityAgencySlug.value = primaryAgency.slug || ""; if (els.intercityAgencyEmail && !els.intercityAgencyEmail.value.trim()) els.intercityAgencyEmail.value = primaryAgency.supportEmail || ""; if (els.intercityAgencyPhone && !els.intercityAgencyPhone.value.trim()) els.intercityAgencyPhone.value = primaryAgency.supportPhone || ""; if (els.intercityAgencyCity && !els.intercityAgencyCity.value.trim()) els.intercityAgencyCity.value = primaryAgency.hqCity || ""; if (els.intercityAgencyTerminal && !els.intercityAgencyTerminal.value.trim()) els.intercityAgencyTerminal.value = primaryAgency.terminalAddress || ""; if (els.intercityAgencyDescription && !els.intercityAgencyDescription.value.trim()) els.intercityAgencyDescription.value = primaryAgency.description || ""; if (els.intercityAgencyLogoAlt && !els.intercityAgencyLogoAlt.value.trim()) els.intercityAgencyLogoAlt.value = primaryAgency.logoAltText || `${primaryAgency.companyName || "Agency"} logo`; updateIntercityAgencyLogoPreview(primaryAgency); setSelectedIntercityAgencyCities(primaryAgency.operatedCities); filteredAgencies.slice(0, 12).forEach((agency) => { const item = document.createElement("article"); item.className = "notice-item"; item.innerHTML = `
${intercityAgencyLogoMarkup(agency)}
${escapeHtml(agency.companyName)}

${escapeHtml(agency.description || `${agency.hqCity} operator`)}

${escapeHtml(intercityAgencyServiceStatus(agency))} - ${escapeHtml(agency.supportEmail)} - ${escapeHtml(agency.supportPhone)} ${escapeHtml(`Cities: ${agency.operatedCities.join(", ") || agency.hqCity}`)} `; els.intercityAgencyList.append(item); }); } if (els.intercityAgencyStatus) { els.intercityAgencyStatus.textContent = agencies.length ? pendingApprovalAgencies.length ? "Agency account submitted. Waka admin approval is required before any trip can be published or shown to travelers." : "Approved agencies can publish during the first free month, then continue after the monthly 25,000 FCFA operator fee is approved. Passengers will only see active departures that you publish below." : agencyWorkspaceActive() ? "This agency login is open, but no approved agency record is linked to it yet. Waka admin must approve or connect the agency before this workspace can publish trips." : "Sign in as a passenger and save the first agency profile to begin admin review."; } if (els.intercityDepartureForm) { const publishable = publishableAgencies.length > 0; els.intercityDepartureForm.querySelectorAll("input, select, textarea, button").forEach((control) => { if (control.id === "saveIntercityDeparture") { control.disabled = false; return; } control.disabled = !publishable; }); setOwnedIntercityAgencyOptions(els.intercityDepartureAgency); } if (els.intercityDepartureStatus && !publishableAgencies.length) { els.intercityDepartureStatus.textContent = pendingApprovalAgencies.length ? "Departure publishing is locked until Waka admin approves this agency." : agencies.length ? "Departure publishing is locked until the agency is active and operational." : "Save an agency profile first, then wait for admin approval before publishing departures."; } if (els.intercityAgencyPaymentList) { els.intercityAgencyPaymentList.innerHTML = ""; if (!payments.length) { els.intercityAgencyPaymentList.append(emptyState("No monthly operator payment records submitted yet.")); } else if (!filteredPayments.length) { els.intercityAgencyPaymentList.append(emptyState("No payment record matches the current filters.")); } else { filteredPayments.slice(0, 8).forEach((payment) => { const item = document.createElement("article"); item.className = "notice-item"; item.innerHTML = ` ${escapeHtml(agencyNameForIntercity(payment.agencyId))}

${escapeHtml(`${payment.amountXaf.toLocaleString("en-US")} FCFA via ${publicIntercityBookingPaymentLabel(payment.provider)} for ${payment.billingPeriodStart}`)}

${escapeHtml(payment.status)}${payment.paymentProviderStatus ? ` - provider ${escapeHtml(payment.paymentProviderStatus)}` : ""}${payment.providerReference ? ` - ${escapeHtml(payment.providerReference)}` : ""}${payment.reviewNote ? ` - ${escapeHtml(payment.reviewNote)}` : ""} ${payment.paymentProviderMessage ? `${escapeHtml(payment.paymentProviderMessage)}` : ""} ${payment.paymentCheckoutUrl ? `Open mobile-money checkout` : ""} `; els.intercityAgencyPaymentList.append(item); }); } } if (els.intercityDepartureList) { els.intercityDepartureList.innerHTML = ""; if (!departures.length) { els.intercityDepartureList.append(emptyState("No inter-city departures have been published for your agencies yet.")); } else if (!filteredDepartures.length) { els.intercityDepartureList.append(emptyState("No departure matches the current filters.")); } else { filteredDepartures.slice(0, 16).forEach((departure) => { const availableSeats = availableSeatsForIntercityDeparture(departure); const departureAgency = intercityAgencyRecords().find((agency) => agency.id === departure.agencyId) || null; const departureIsFuture = new Date(String(departure.departureAt || "")).getTime() > Date.now(); const publicDepartureVisible = lastPublicIntercityDepartureIds.has(departure.id); const publicBookingStatus = publicDepartureVisible ? "Visible to travelers on the public booking page" : departure.active !== false && departureIsFuture && intercityAgencyCanOperate(departureAgency) ? "Saved in workspace, not returned by the public catalog yet" : "Not bookable publicly until active, future-dated, and agency-approved"; const item = document.createElement("article"); item.className = "intercity-booking-card"; item.innerHTML = `
${escapeHtml(agencyNameForIntercity(departure.agencyId))} ${escapeHtml(`${departure.originCity} -> ${departure.destinationCity}`)}

${escapeHtml(departure.busLabel ? `${departure.busLabel} | ${departure.boardingLocation}` : departure.boardingLocation)}

Departure${escapeHtml(formatDateTime(departure.departureAt))}
Bus${escapeHtml(departure.busLabel || intercityBusClassLabel(departure.seatCapacity))}
Capacity${escapeHtml(intercityCapacityLabel(departure))}
Seats${escapeHtml(String(availableSeats))} left of ${escapeHtml(String(departure.seatCapacity))}
Held seats${escapeHtml(intercityBlockedSeatLabel(departure))}
Fare${departure.fareXaf > 0 ? escapeHtml(`${departure.fareXaf.toLocaleString("en-US")} FCFA`) : "Call operator"}
Status${escapeHtml(`${departure.travelStatus}${departure.active ? "" : " / paused"}`)}
Public booking${escapeHtml(publicBookingStatus)}
${departure.statusNote ? `${escapeHtml(departure.statusNote)}` : ""}
Open public booking page
`; renderIntercitySeatMap(item.querySelector(".intercity-seat-map"), departure); item.querySelector(".intercity-departure-toggle")?.addEventListener("click", () => { void toggleIntercityDepartureActive(departure.id, departure.active !== true); }); item.querySelector(".intercity-departure-block")?.addEventListener("click", async () => { await manageIntercityDepartureSeatAvailability(departure, async (fields) => { await updateIntercityDepartureOperations(departure.id, fields); }); }); item.querySelector(".intercity-departure-cancel")?.addEventListener("click", () => { void cancelIntercityDeparture(departure); }); item.querySelector(".intercity-departure-delete")?.addEventListener("click", () => { void deleteIntercityDeparture(departure); }); item.querySelectorAll(".intercity-departure-status").forEach((button) => { button.addEventListener("click", async () => { const note = await showWakaGoodPrompt(`Optional note for ${button.dataset.nextStatus || "status"} update.`, departure.statusNote || ""); if (note === null) return; void updateIntercityDepartureOperations(departure.id, { travelStatus: button.dataset.nextStatus, statusNote: String(note || "") }); }); }); els.intercityDepartureList.append(item); }); } } if (els.intercityAdvertisingList) { els.intercityAdvertisingList.innerHTML = ""; if (!advertisingRequests.length) { els.intercityAdvertisingList.append(emptyState("No advertising request has been submitted yet.")); } else if (!filteredAdvertisingRequests.length) { els.intercityAdvertisingList.append(emptyState("No advertising request matches the current filters.")); } else { filteredAdvertisingRequests.slice(0, 12).forEach((request) => { const item = document.createElement("article"); item.className = "notice-item"; const imageUrl = intercityPromotionImageUrl(request); const publicTitle = request.publicTitle || request.campaignName; const publicBody = request.publicBody || request.note || ""; const publishWindow = [ request.publishStartAt ? `Starts ${formatDateTime(request.publishStartAt)}` : "", request.publishEndAt ? `Ends ${formatDateTime(request.publishEndAt)}` : "" ].filter(Boolean).join(" | "); item.innerHTML = `
${imageUrl ? `${escapeHtml(intercityPromotionImageAlt(request))}` : intercityAgencyLogoMarkup(intercityAgencyRecords().find((agency) => agency.id === request.agencyId))}
${escapeHtml(publicTitle)}

${escapeHtml(`${agencyNameForIntercity(request.agencyId)} | ${request.placement} | ${request.durationDays} days`)}

${publicBody ? `${escapeHtml(publicBody)}` : ""} ${publishWindow ? `${escapeHtml(publishWindow)}` : ""} ${escapeHtml(`${request.amountXaf.toLocaleString("en-US")} FCFA - ${request.status}`)}${request.providerReference ? ` - ${escapeHtml(request.providerReference)}` : ""}${request.publicActive === false ? " - hidden after approval" : ""} `; els.intercityAdvertisingList.append(item); }); } } if (els.intercityBookingList) { els.intercityBookingList.innerHTML = ""; if (!bookings.length) { els.intercityBookingList.append(emptyState("No traveler bookings have been received yet.")); } else if (!filteredBookings.length) { els.intercityBookingList.append(emptyState("No traveler booking matches the current filters.")); } else { filteredBookings.slice(0, 16).forEach((booking) => { const snapshot = booking.departureSnapshot || {}; const departure = intercityDepartureRecords().find((item) => item.id === booking.departureId) ?? null; const messages = intercityMessagesForBooking(booking.id); const bookingAmountXaf = Number(booking.amountXaf || 0) || 0; const bookingAmountLabel = bookingAmountXaf > 0 ? `${bookingAmountXaf.toLocaleString("en-US")} FCFA` : "Amount pending"; const item = document.createElement("article"); item.className = "intercity-booking-card"; item.innerHTML = ` ${escapeHtml(booking.travelerName)}

${escapeHtml(`${snapshot.companyName || agencyNameForIntercity(booking.agencyId)} | ${snapshot.originCity || ""} -> ${snapshot.destinationCity || ""}`)}

${escapeHtml(booking.bookingReference)} - ${escapeHtml(String(booking.seatCount))} seat${booking.seatCount === 1 ? "" : "s"} - ${escapeHtml(booking.paymentStatus)} - receipt ${escapeHtml(booking.receiptDeliveryStatus)} ${booking.paymentProviderStatus || booking.paymentProviderReference || booking.agencySettlementStatus !== "not_started" ? `${escapeHtml([booking.paymentProviderStatus ? `Provider ${booking.paymentProviderStatus}` : "", booking.paymentProviderReference ? `Ref ${booking.paymentProviderReference}` : "", booking.agencySettlementStatus !== "not_started" ? `Settlement ${booking.agencySettlementStatus}` : ""].filter(Boolean).join(" | "))}` : ""}
Seats${booking.seatNumbers.length ? escapeHtml(booking.seatNumbers.join(", ")) : "Assigned automatically"}
Amount${escapeHtml(bookingAmountLabel)}
Status${escapeHtml(booking.bookingStatus)}
Passengers${escapeHtml(intercityPassengerManifestSummary(booking))}
Phone${escapeHtml(booking.travelerPhone)}
Email${escapeHtml(booking.travelerEmail)}
${booking.operatorNote ? `${escapeHtml(booking.operatorNote)}` : ""} ${messages.length ? `Last message: ${escapeHtml(messages[messages.length - 1].messageBody)}` : ""}
`; renderIntercitySeatMap(item.querySelector(".intercity-seat-map"), departure || { id: booking.departureId, seatCapacity: Math.max(...booking.seatNumbers, booking.seatCount, 4), reservedSeats: booking.seatCount, blockedSeatNumbers: [] }, { highlightSeats: booking.seatNumbers }); item.querySelector(".intercity-booking-paid")?.addEventListener("click", async () => { const note = await showWakaGoodPrompt("Optional operator note after confirming payment.", booking.operatorNote || ""); if (note === null) return; void updateIntercityBookingOperations(booking.id, { paymentStatus: "paid", bookingStatus: booking.bookingStatus === "reserved" ? "confirmed" : booking.bookingStatus, operatorNote: String(note || "") }); }); item.querySelector(".intercity-booking-checkin")?.addEventListener("click", async () => { const note = await showWakaGoodPrompt("Optional check-in note.", booking.operatorNote || ""); if (note === null) return; void updateIntercityBookingOperations(booking.id, { bookingStatus: "checked_in", operatorNote: String(note || "") }); }); item.querySelector(".intercity-booking-complete")?.addEventListener("click", async () => { const note = await showWakaGoodPrompt("Optional completion note.", booking.operatorNote || ""); if (note === null) return; void updateIntercityBookingOperations(booking.id, { bookingStatus: "completed", operatorNote: String(note || "") }); }); item.querySelector(".intercity-booking-message")?.addEventListener("click", () => { void sendIntercityTravelerMessage(booking.id); }); item.querySelector(".intercity-booking-cancel")?.addEventListener("click", async () => { const note = await showWakaGoodPrompt("Reason for cancellation shown in operator history.", booking.operatorNote || ""); if (note === null) return; void updateIntercityBookingOperations(booking.id, { bookingStatus: "cancelled", operatorNote: String(note || "") }); }); els.intercityBookingList.append(item); }); } } if (els.intercityTripReportList) { els.intercityTripReportList.innerHTML = ""; if (!reports.length) { els.intercityTripReportList.append(emptyState("No departure report is available yet. Publish a departure and receive bookings to build the pre-trip overview.")); } else if (!filteredReports.length) { els.intercityTripReportList.append(emptyState("No departure report matches the current agency, status, or search filter.")); } else { filteredReports.slice(0, 16).forEach((report) => { const reportAgency = intercityAgencyRecords().find((agency) => agency.id === report.agencyId); const item = document.createElement("article"); item.className = "notice-item intercity-trip-report-card"; const manifestRows = report.bookings.length ? report.bookings.map((booking) => { const seats = booking.seatNumbers.length ? booking.seatNumbers.join(", ") : String(booking.seatCount || "pending"); const provider = booking.paymentProviderStatus ? ` | Provider ${booking.paymentProviderStatus}` : ""; return `${escapeHtml(`Seat ${seats} | ${booking.bookingReference} | ${intercityPassengerManifestSummary(booking)} | Contact ${booking.travelerPhone} | ${publicIntercityBookingPaymentLabel(booking.paymentMethod)} | ${booking.paymentStatus}${provider} | ${intercityXafLabel(booking.amountXaf)}`)}`; }).join("") : "No passenger booking has been received for this departure yet."; item.innerHTML = `
${intercityAgencyLogoMarkup(reportAgency)}
${escapeHtml(`${agencyNameForIntercity(report.agencyId)} | ${report.routeLabel}`)}

${escapeHtml(`${formatDateTime(report.departureAt)} | ${report.busLabel} | ${report.travelStatus}${report.active ? "" : " / paused"}`)}

Total booked${escapeHtml(intercityXafLabel(report.totalAmountXaf))}
Paid${escapeHtml(`${intercityXafLabel(report.paidAmountXaf)} / ${report.paidBookings} booking${report.paidBookings === 1 ? "" : "s"}`)}
Pending${escapeHtml(`${intercityXafLabel(report.pendingAmountXaf)} / ${report.pendingBookings} booking${report.pendingBookings === 1 ? "" : "s"}`)}
Passengers${escapeHtml(`${report.bookingCount} booking${report.bookingCount === 1 ? "" : "s"}`)}
Seats booked${escapeHtml(`${report.bookedSeats} of ${report.seatCapacity}`)}
Seats available${escapeHtml(String(report.availableSeats))}
Passenger manifest ${manifestRows}
`; els.intercityTripReportList.append(item); }); } } if (els.intercityMessageList) { els.intercityMessageList.innerHTML = ""; if (els.intercityMessageStatus) { els.intercityMessageStatus.textContent = messages.length ? "Passenger and agency updates appear here so operators can reply faster, keep context, and resolve travel issues without leaving the workspace." : "Traveler messages will appear here after passengers and agencies start exchanging updates."; } if (!messages.length) { els.intercityMessageList.append(emptyState("No traveler messages yet.")); } else if (!filteredMessages.length) { els.intercityMessageList.append(emptyState("No traveler message matches the current filters.")); } else { filteredMessages.slice(0, 18).forEach((message) => { const booking = bookings.find((item) => item.id === message.bookingId) ?? null; const snapshot = booking?.departureSnapshot || {}; const isAgencyMessage = message.messageKind === "agency" || !message.bookingId; const item = document.createElement("article"); item.className = "notice-item operator-message-card"; item.innerHTML = ` ${escapeHtml(message.subject || booking?.travelerName || agencyNameForIntercity(message.agencyId))}

${escapeHtml(message.messageBody)}

${escapeHtml(message.senderRole)} ${escapeHtml(isAgencyMessage ? "Agency message" : (booking?.bookingReference || "Booking pending"))} ${escapeHtml(snapshot.companyName || agencyNameForIntercity(message.agencyId))} ${escapeHtml([snapshot.originCity, snapshot.destinationCity].filter(Boolean).join(" -> ") || "Route pending")} ${escapeHtml(formatDateTime(message.createdAt))}
${isAgencyMessage ? "" : ``}
`; item.querySelector(".intercity-message-open-booking")?.addEventListener("click", () => { focusIntercityOperatorBooking(booking); }); item.querySelector(".intercity-message-reply")?.addEventListener("click", () => { if (booking) { void sendIntercityTravelerMessage(booking.id); } else if (message.agencyId) { void sendIntercityAgencyMessage(message.agencyId); } }); els.intercityMessageList.append(item); }); } } } function scheduleIntercityOperatorAutoRefresh() { if (intercityOperatorAutoRefreshTimer) return; intercityOperatorAutoRefreshTimer = window.setInterval(() => { if (document.hidden || !agencyWorkspaceActive() || !hasSignedIn("passenger") || !intercityAgencyOwnerProfileId()) return; void loadOwnedIntercityOperatorData({ force: true }).catch((error) => { logClientWarning("Agency workspace live refresh failed.", error); }); }, 30000); } async function createOrUpdateIntercityAgency(event) { event.preventDefault(); if (!hasSignedIn("passenger") || !state.passenger?.id) { if (els.intercityAgencyStatus) els.intercityAgencyStatus.textContent = "Sign in as a passenger before registering a transport agency."; return; } const companyName = els.intercityAgencyName?.value.trim() || ""; const supportEmail = els.intercityAgencyEmail?.value.trim().toLowerCase() || ""; const supportPhone = els.intercityAgencyPhone?.value.trim() || ""; const hqCity = els.intercityAgencyCity?.value.trim() || ""; const terminalAddress = els.intercityAgencyTerminal?.value.trim() || ""; const operatedCities = selectedIntercityAgencyCities(); const slug = slugifyIntercityAgency(els.intercityAgencySlug?.value || companyName); const logoFile = els.intercityAgencyLogo?.files?.[0] ?? null; const logoAltText = els.intercityAgencyLogoAlt?.value.trim() || `${companyName || "Transport agency"} logo`; if (els.intercityAgencySlug) els.intercityAgencySlug.value = slug; if (companyName.length < 2 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(supportEmail) || supportPhone.length < 7 || hqCity.length < 2 || terminalAddress.length < 4 || !slug) { if (els.intercityAgencyStatus) els.intercityAgencyStatus.textContent = "Enter company name, email, phone, head office city, terminal, and a valid company slug."; return; } try { validateIntercityAgencyLogoFile(logoFile); } catch (error) { if (els.intercityAgencyStatus) els.intercityAgencyStatus.textContent = error.message; return; } const payload = { owner_profile_id: state.passenger.id, business_account_id: passengerBusinessAccounts()[0]?.id ?? null, company_name: companyName, slug, description: els.intercityAgencyDescription?.value.trim() || "", logo_alt_text: logoAltText, support_email: supportEmail, support_phone: supportPhone, terminal_address: terminalAddress, hq_city: hqCity, operated_cities: operatedCities.length ? operatedCities : [hqCity], mtn_momo_name: els.intercityAgencyMtnName?.value.trim() || "", mtn_momo_number: els.intercityAgencyMtnNumber?.value.trim() || "", orange_money_name: els.intercityAgencyOrangeName?.value.trim() || "", orange_money_number: els.intercityAgencyOrangeNumber?.value.trim() || "", boarding_policy: "Arrive at least one hour before departure with your receipt or booking reference.", monthly_fee_xaf: 25000, subscription_status: "pending_review", status: "pending_review", updated_at: new Date().toISOString() }; try { if (els.intercityAgencyStatus) els.intercityAgencyStatus.textContent = "Saving transport agency..."; const rows = await supabaseRestRequest("/rest/v1/cameroon_intercity_agencies?on_conflict=slug", { method: "POST", body: payload, headers: { Prefer: "resolution=merge-duplicates,return=representation" } }); let saved = mapIntercityAgencyFromDatabase(Array.isArray(rows) ? rows[0] : rows); if (logoFile) { if (els.intercityAgencyStatus) els.intercityAgencyStatus.textContent = "Uploading company logo..."; saved = await saveIntercityAgencyLogoMetadata(saved, logoFile, logoAltText); if (els.intercityAgencyLogo) els.intercityAgencyLogo.value = ""; } state.intercityAgencies = upsertById(intercityAgencyRecords(), saved); saveState(); setSelectedIntercityAgencyCities(saved.operatedCities); updateIntercityAgencyLogoPreview(saved); await loadOwnedIntercityOperatorData({ force: true }); if (els.intercityAgencyStatus) els.intercityAgencyStatus.textContent = `${saved.companyName} saved and submitted for Waka admin approval.${saved.logoPath || saved.logoUrl ? " Company logo is visible on public cards, booking receipts, messages, and reports." : ""} Trip publishing stays locked until approval is completed.`; } catch (error) { if (els.intercityAgencyStatus) els.intercityAgencyStatus.textContent = `Transport agency could not be saved: ${error.message}`; } } function intercityMonthStart(value) { const raw = String(value || "").trim(); if (!/^\d{4}-\d{2}$/.test(raw)) return null; return new Date(`${raw}-01T00:00:00`); } async function submitIntercityAgencyPayment(event) { event.preventDefault(); const agencyId = els.intercityAgencyPaymentSelect?.value || ""; const month = els.intercityAgencyPaymentMonth?.value || ""; const provider = els.intercityAgencyPaymentProvider?.value || "mtn_momo"; if (!agencyId || !month) { if (els.intercityAgencyPaymentStatus) els.intercityAgencyPaymentStatus.textContent = "Choose the agency and billing month before submitting the 25,000 FCFA fee."; return; } const monthStart = intercityMonthStart(month); if (!monthStart) { if (els.intercityAgencyPaymentStatus) els.intercityAgencyPaymentStatus.textContent = "Billing month is invalid."; return; } try { if (els.intercityAgencyPaymentStatus) els.intercityAgencyPaymentStatus.textContent = "Starting monthly operator fee payment..."; const response = await fetch(`${appConfig.supabaseUrl}/functions/v1/${agencyPlatformPaymentStartFunctionName()}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${supabaseRestSession?.access_token || appConfig.supabaseAnonKey}`, "content-type": "application/json" }, body: JSON.stringify({ agencyId, billingMonth: month, provider, payerName: els.intercityAgencyPaymentPayerName?.value.trim() || "", payerPhone: els.intercityAgencyPaymentPayerPhone?.value.trim() || "", providerReference: els.intercityAgencyPaymentReference?.value.trim() || "" }) }); const payload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(payload?.error || "Agency platform payment failed."); const saved = mapIntercityAgencyPaymentFromDatabase(payload.payment || {}); state.intercityAgencyPayments = upsertById(intercityAgencyPaymentRecords(), saved); saveState(); renderIntercityOperatorPanels(); const automation = payload.paymentAutomation && typeof payload.paymentAutomation === "object" ? payload.paymentAutomation : {}; const instructions = Array.isArray(payload.paymentInstructions) ? payload.paymentInstructions.join(" ") : ""; if (els.intercityAgencyPaymentStatus) { els.intercityAgencyPaymentStatus.textContent = `${automation.message || "The 25,000 FCFA operator fee record was submitted for review."}${instructions ? ` ${instructions}` : ""}`; } } catch (error) { if (els.intercityAgencyPaymentStatus) els.intercityAgencyPaymentStatus.textContent = `Monthly operator fee could not be submitted: ${error.message}`; } } async function submitIntercityAdvertisingRequest(event) { event.preventDefault(); const agencyId = els.intercityAdvertisingAgency?.value || ""; const agency = intercityAgencyRecords().find((item) => item.id === agencyId) ?? null; const campaignName = els.intercityAdvertisingCampaignName?.value.trim() || ""; const publicTitle = els.intercityAdvertisingPublicTitle?.value.trim() || campaignName; const publicBody = els.intercityAdvertisingPublicBody?.value.trim() || ""; const ctaUrl = safePublicPromotionUrl(els.intercityAdvertisingCtaUrl?.value || ""); const rawCtaUrl = els.intercityAdvertisingCtaUrl?.value.trim() || ""; const publishStartAtRaw = els.intercityAdvertisingStartAt?.value || ""; const publishEndAtRaw = els.intercityAdvertisingEndAt?.value || ""; const publishStartAt = publishStartAtRaw ? new Date(publishStartAtRaw).toISOString() : new Date().toISOString(); const publishEndAt = publishEndAtRaw ? new Date(publishEndAtRaw).toISOString() : null; const imageFile = els.intercityAdvertisingImage?.files?.[0] ?? null; if (!agencyId || campaignName.length < 3 || publicTitle.length < 3 || publicBody.length < 10) { if (els.intercityAdvertisingStatus) els.intercityAdvertisingStatus.textContent = "Choose the agency, campaign name, public title, and public promotion details before submitting advertising."; return; } if (!agency || !intercityAgencyCanOperate(agency)) { if (els.intercityAdvertisingStatus) els.intercityAdvertisingStatus.textContent = "This agency must be approved and active before public promotions can be submitted."; return; } if (rawCtaUrl && !ctaUrl) { if (els.intercityAdvertisingStatus) els.intercityAdvertisingStatus.textContent = "Use a valid http or https link for the optional promotion button."; return; } if (publishEndAt && new Date(publishEndAt).getTime() <= new Date(publishStartAt).getTime()) { if (els.intercityAdvertisingStatus) els.intercityAdvertisingStatus.textContent = "Promotion stop time must be after the start time."; return; } try { if (els.intercityAdvertisingStatus) els.intercityAdvertisingStatus.textContent = imageFile ? "Uploading promotion image..." : "Submitting advertising request..."; const imagePath = imageFile ? await uploadIntercityAgencyPromotionImage(agencyId, imageFile) : ""; const rows = await supabaseRestRequest("/rest/v1/cameroon_intercity_advertising_requests", { method: "POST", body: { agency_id: agencyId, campaign_name: campaignName, public_title: publicTitle, public_body: publicBody, cta_label: els.intercityAdvertisingCtaLabel?.value.trim() || "", cta_url: ctaUrl, image_path: imagePath, image_alt_text: els.intercityAdvertisingImageAlt?.value.trim() || publicTitle, publish_start_at: publishStartAt, publish_end_at: publishEndAt, public_active: els.intercityAdvertisingPublicActive?.checked !== false, content_updated_at: new Date().toISOString(), placement: els.intercityAdvertisingPlacement?.value || "website_banner", duration_days: Number(els.intercityAdvertisingDurationDays?.value || 7) || 7, amount_xaf: Number(els.intercityAdvertisingAmountXaf?.value || 0) || 0, provider: els.intercityAdvertisingProvider?.value || "mtn_momo", payer_name: els.intercityAdvertisingPayerName?.value.trim() || "", payer_phone: els.intercityAdvertisingPayerPhone?.value.trim() || "", provider_reference: els.intercityAdvertisingReference?.value.trim() || "", note: els.intercityAdvertisingNote?.value.trim() || "", updated_at: new Date().toISOString() }, headers: { Prefer: "return=representation" } }); const saved = mapIntercityAdvertisingRequestFromDatabase(Array.isArray(rows) ? rows[0] : rows); state.intercityAdvertisingRequests = upsertById(intercityAdvertisingRequestRecords(), saved); saveState(); renderIntercityOperatorPanels(); if (els.intercityAdvertisingImage) els.intercityAdvertisingImage.value = ""; if (els.intercityAdvertisingStatus) els.intercityAdvertisingStatus.textContent = "Promotion submitted for WakaGood review. It becomes public only after Waka admin approves it."; } catch (error) { if (els.intercityAdvertisingStatus) els.intercityAdvertisingStatus.textContent = `Advertising request could not be submitted: ${error.message}`; } } async function submitIntercityDeparture(event) { event?.preventDefault?.(); const ownedAgencies = ownedIntercityAgencies(); const fallbackAgency = ownedAgencies.find((item) => intercityAgencyCanOperate(item)) || ownedAgencies[0] || null; const lockedAgencyId = agencyWorkspaceRouteActive() ? agencyWorkspaceLockedAgencyId() : ""; const agencyId = lockedAgencyId || els.intercityDepartureAgency?.value || fallbackAgency?.id || ""; if (els.intercityDepartureAgency && agencyId) els.intercityDepartureAgency.value = agencyId; const agency = intercityAgencyRecords().find((item) => item.id === agencyId) ?? fallbackAgency; const originCity = els.intercityDepartureOrigin?.value.trim() || ""; const destinationCity = els.intercityDepartureDestination?.value.trim() || ""; const boardingLocation = els.intercityDepartureBoarding?.value.trim() || ""; const departureAtRaw = els.intercityDepartureAt?.value || ""; const departureAt = departureAtRaw ? new Date(departureAtRaw).toISOString() : ""; const seatCapacity = Number(els.intercityDepartureSeats?.value || 0); const blockedReason = intercityAgencyApprovalBlockedReason(agency); if (blockedReason) { setIntercityDepartureSaveStatus(blockedReason, "error", { announce: true, focus: true }); return; } if (!agencyId || !originCity || !destinationCity || !boardingLocation || !departureAt || !Number.isFinite(seatCapacity) || seatCapacity < 1) { setIntercityDepartureSaveStatus("Enter route, boarding point, departure time, and seat capacity before saving this agency departure.", "error", { announce: true, focus: true }); return; } const blockedSeatNumbers = parseIntercitySeatNumberList(els.intercityDepartureBlockedSeats?.value || "", seatCapacity); if (agencyWorkspaceRouteActive() && (!agencyId || !agency || agency.id !== agencyId)) { setIntercityDepartureSaveStatus("The agency workspace is still loading the approved agency record. Refresh, sign in again from Agency portal, then save the departure under that agency only.", "error", { announce: true, focus: true }); return; } if (intercityDepartureSaveInFlight) { setIntercityDepartureSaveStatus("Departure save is already running. Please wait for the result.", "", { focus: true }); return; } intercityDepartureSaveInFlight = true; setIntercityDepartureSaveBusy(true); try { setIntercityDepartureSaveStatus(`Checking this departure for ${agency?.companyName || "this agency"}...`, "", { focus: true }); const departurePayload = { agency_id: agencyId, bus_label: els.intercityDepartureBusLabel?.value.trim() || "", origin_city: originCity, destination_city: destinationCity, boarding_location: boardingLocation, dropoff_location: els.intercityDepartureDropoff?.value.trim() || "", departure_at: departureAt, estimated_duration_minutes: Number(els.intercityDepartureDuration?.value || 0) || 0, seat_capacity: seatCapacity, reserved_seats: 0, blocked_seat_numbers: blockedSeatNumbers, fare_xaf: Number(els.intercityDepartureFare?.value || 0) || null, accepts_mtn_momo: els.intercityDepartureAllowMtn?.checked !== false, accepts_orange_money: els.intercityDepartureAllowOrange?.checked !== false, accepts_pay_later: els.intercityDepartureAllowPayLater?.checked !== false, travel_status: els.intercityDepartureTravelStatus?.value || "scheduled", status_note: els.intercityDepartureStatusNote?.value.trim() || "", notes: els.intercityDepartureNotes?.value.trim() || "", active: true, updated_at: new Date().toISOString() }; const duplicateDeparture = await findExistingIntercityDepartureDuplicate(departurePayload); if (duplicateDeparture) { intercityOperatorWorkspaceTab = "departures"; renderIntercityOperatorPanels(); setIntercityDepartureSaveStatus( "This departure already exists for this agency, bus, route, boarding point, and departure time. Use the existing departure card to hold seats, pause, or cancel it instead of saving a duplicate.", "error", { announce: true, focus: true } ); return; } setIntercityDepartureSaveStatus(`Saving departure for ${agency?.companyName || "this agency"}...`, "", { focus: true }); let rows; try { rows = await supabaseRestRequest("/rest/v1/cameroon_intercity_departures", { method: "POST", body: departurePayload, headers: { Prefer: "return=representation" } }); } catch (restError) { logClientWarning("REST departure save failed; trying Supabase client insert.", restError); if (!supabaseClient?.from) throw restError; const { data, error } = await supabaseClient .from("cameroon_intercity_departures") .insert(departurePayload) .select("*") .single(); if (error) throw new Error(`${restError.message}; Supabase client insert also failed: ${error.message}`); rows = [data]; } const saved = mapIntercityDepartureFromDatabase(Array.isArray(rows) ? rows[0] : rows); const verifiedSaved = await verifySavedIntercityDepartureFromSupabase(saved.id).catch((error) => { logClientWarning("Departure save read-back failed.", error); return null; }); const savedForState = verifiedSaved || saved; if (!verifiedSaved && hasSupabaseRuntime()) { throw new Error("Supabase accepted the save request but the departure could not be read back. It was not confirmed as persisted, so it will not be shown as published."); } state.intercityDepartures = upsertById(intercityDepartureRecords(), savedForState); saveState(); renderIntercityOperatorPanels(); await loadOwnedIntercityOperatorData({ force: true }).catch((error) => { logClientWarning("Departure saved, but agency workspace refresh failed.", error); }); await loadPublicIntercityCatalog({ force: true }).catch((error) => { logClientWarning("Departure saved, but public catalog refresh failed.", error); }); const savedAgencyName = agency?.companyName || "Agency"; const publicVisibilityExpected = savedForState.active !== false && new Date(String(savedForState.departureAt || "")).getTime() > Date.now() && intercityAgencyCanOperate(agency); const successMessage = lastPublicIntercityDepartureIds.has(savedForState.id) ? `Departure saved for ${savedAgencyName} and confirmed on the public WakaGood inter-city planner.` : publicVisibilityExpected ? `Departure saved for ${savedAgencyName}, but it was not returned by the public catalog yet. Travelers will not see it until the public catalog grants/policy return this departure.` : `Departure saved for ${savedAgencyName} in the agency workspace. It appears publicly only when active, scheduled in the future, and the agency remains active.`; setIntercityDepartureSaveStatus(successMessage, lastPublicIntercityDepartureIds.has(savedForState.id) ? "success" : "error", { announce: true, focus: true }); } catch (error) { setIntercityDepartureSaveStatus(`Departure could not be saved: ${error.message}`, "error", { announce: true, focus: true }); } finally { intercityDepartureSaveInFlight = false; setIntercityDepartureSaveBusy(false); } } async function toggleIntercityDepartureActive(departureId, nextActive) { try { if (els.intercityDepartureStatus) els.intercityDepartureStatus.textContent = nextActive ? "Restoring departure..." : "Pausing departure..."; const rows = await supabaseRestRequest(`/rest/v1/cameroon_intercity_departures?id=eq.${encodeURIComponent(departureId)}`, { method: "PATCH", body: { active: Boolean(nextActive), updated_at: new Date().toISOString() }, headers: { Prefer: "return=representation" } }); const saved = mapIntercityDepartureFromDatabase(Array.isArray(rows) ? rows[0] : rows); state.intercityDepartures = upsertById(intercityDepartureRecords(), saved); saveState(); renderIntercityOperatorPanels(); await loadPublicIntercityCatalog({ force: true }); if (els.intercityDepartureStatus) els.intercityDepartureStatus.textContent = nextActive ? "Departure restored." : "Departure paused."; } catch (error) { if (els.intercityDepartureStatus) els.intercityDepartureStatus.textContent = `Departure status could not be changed: ${error.message}`; } } async function submitPublicIntercityBooking(event) { event.preventDefault(); const departure = selectedPublicIntercityDeparture(); if (!departure) { selectedPublicIntercitySeatNumbers = []; if (els.publicIntercitySearchStatus) els.publicIntercitySearchStatus.textContent = "Choose a matching departure before entering traveler details."; if (els.publicIntercitySelectedDeparture) { els.publicIntercitySelectedDeparture.textContent = translatedValue("chooseDepartureToUnlockBooking") || "Choose a departure from the list. Traveler details, seat selection, and payment open here after selection."; } renderPublicIntercityPlanner(); return; } const travelerName = els.publicIntercityTravelerName?.value.trim() || ""; const travelerEmail = els.publicIntercityTravelerEmail?.value.trim() || ""; const travelerPhone = els.publicIntercityTravelerPhone?.value.trim() || ""; const travelerIdentityNumber = els.publicIntercityTravelerIdentityNumber?.value.trim() || ""; const paymentMethod = els.publicIntercityPaymentMethod?.value || ""; const payerPhone = els.publicIntercityPaymentPhone?.value.trim() || ""; const seatNumbers = normalizeSelectedPublicIntercitySeatNumbers(departure); const passengerManifest = publicIntercityPassengerManifestForBooking(departure); if (!seatNumbers.length) { if (els.publicIntercitySeatSelectionStatus) { els.publicIntercitySeatSelectionStatus.textContent = "Choose at least one available seat before booking."; } return; } if (!travelerName || !travelerEmail || !travelerPhone) { if (els.publicIntercityBookingStatus) els.publicIntercityBookingStatus.textContent = "Traveler name exactly as shown on the national ID card, email, and phone are required for the emailed receipt."; return; } const incompletePassenger = passengerManifest.find((passenger) => !passenger.travelerName || !intercityDateOfBirthIsValid(passenger.dateOfBirth)); if (passengerManifest.length !== seatNumbers.length || incompletePassenger) { if (els.publicIntercityBookingStatus) { els.publicIntercityBookingStatus.textContent = "Enter each passenger name exactly as shown on the national ID card and a valid date of birth for every selected seat."; } return; } if (!paymentMethod) { if (els.publicIntercityBookingStatus) els.publicIntercityBookingStatus.textContent = "No valid payment option is available for this departure. Choose another departure or ask the agency to update payment settings."; return; } if (paymentMethod !== "pay_later" && !payerPhone) { if (els.publicIntercityBookingStatus) els.publicIntercityBookingStatus.textContent = "Enter the phone number you will use for the agency mobile-money payment."; return; } try { if (els.publicIntercityBookingStatus) els.publicIntercityBookingStatus.textContent = "Booking departure and sending your receipt..."; const response = await fetch(`${appConfig.supabaseUrl}/functions/v1/${intercityBookingFunctionName()}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${supabaseRestSession?.access_token || appConfig.supabaseAnonKey}`, "content-type": "application/json" }, body: JSON.stringify({ departureId: departure.id, travelerName, travelerIdentityNumber, travelerDateOfBirth: passengerManifest[0]?.dateOfBirth || "", passengerManifest, travelerEmail, travelerPhone, seatCount: seatNumbers.length, seatNumbers, paymentMethod, payerPhone, notes: els.publicIntercityBookingNotes?.value.trim() || "" }) }); const payload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(payload?.error || "Inter-city booking failed."); if (els.publicIntercityBookingStatus) { const seatNumbers = Array.isArray(payload.seatNumbers) && payload.seatNumbers.length ? ` Seats ${payload.seatNumbers.join(", ")}.` : ""; const amountText = Number(payload.amountXaf || 0) > 0 ? ` Total ${Number(payload.amountXaf).toLocaleString("en-US")} FCFA.` : ""; const bookedAtText = payload.bookedAt ? ` Booked ${formatDateTime(payload.bookedAt)}.` : ""; const agency = intercityAgencyRecords().find((item) => item.id === departure.agencyId); const directPaymentText = Array.isArray(payload.paymentInstructions) && payload.paymentInstructions.length ? ` ${payload.paymentInstructions.join(" ")}` : ` ${intercityAgencyDirectPaymentInstruction(agency, paymentMethod, Number(payload.amountXaf || 0))}`; const paymentAutomation = payload.paymentAutomation && typeof payload.paymentAutomation === "object" ? payload.paymentAutomation : null; const automationText = paymentAutomation?.message ? ` ${paymentAutomation.message}${paymentAutomation.checkoutUrl ? ` Open checkout: ${paymentAutomation.checkoutUrl}` : ""}` : ""; const payOnArrivalWarning = (paymentMethod === "pay_later") ? ` ${translatedValue("payOnArrivalBookingSavedWarning") || "Pay on arrival was selected. The chosen seats are held for this booking; arrive early and follow the agency payment instructions."}` : ""; els.publicIntercityBookingStatus.textContent = `Booking ${payload.bookingReference} saved.${bookedAtText}${seatNumbers}${amountText}${automationText || directPaymentText} Receipt email status: ${payload.receiptStatus}.${payOnArrivalWarning}`; } if (els.publicIntercityBookingNotes) els.publicIntercityBookingNotes.value = ""; await loadPublicIntercityCatalog({ force: true }); if (hasSignedIn("passenger")) { await loadOwnedIntercityOperatorData({ force: true }).catch(() => {}); await loadPassengerIntercityTravelerData({ force: true }).catch(() => {}); } } catch (error) { if (els.publicIntercityBookingStatus) els.publicIntercityBookingStatus.textContent = `Departure could not be booked: ${error.message}`; } } function updateBusinessBillingOptions() { if (!els.rideBillingAccount) return; const selected = automaticRideBillingAccountId() ?? els.rideBillingAccount.value; els.rideBillingAccount.innerHTML = ""; const personal = document.createElement("option"); personal.value = ""; personal.textContent = "Personal ride"; els.rideBillingAccount.append(personal); passengerBusinessAccounts().forEach((account) => { const option = document.createElement("option"); option.value = account.id; option.textContent = businessAccountCanRequest(account) ? `Business: ${account.businessName} (${businessPlanLabel(account.planCode)})` : `Business review: ${account.businessName}`; option.disabled = !businessAccountCanRequest(account); option.selected = selected === account.id && !option.disabled; els.rideBillingAccount.append(option); }); if (selected && [...els.rideBillingAccount.options].some((option) => option.value === selected && !option.disabled)) { els.rideBillingAccount.value = selected; } else { els.rideBillingAccount.value = ""; } } function automaticRideBillingAccountId() { if (!passengerBusinessWorkspaceEnabled()) return null; return passengerBusinessAccounts().find((account) => businessAccountCanRequest(account))?.id ?? null; } const accountModeSelectedThisSession = { passenger: false, rider: false }; const passengerProfilePhotoUrlCache = new Map(); const passengerFareBoostDrafts = new Map(); const passengerOfferCounterDrafts = new Map(); const passengerDestinationUpdateDrafts = new Map(); const passengerDestinationUpdateEditPreserveMs = 5000; function passengerDestinationUpdateDraftKey(requestOrId) { const id = typeof requestOrId === "string" ? requestOrId : requestOrId?.id; return id ? `${id}:route-update` : ""; } function passengerDestinationUpdateCurrentDestination(request) { return String(request?.destinationFormattedAddress || request?.destination || ""); } function rememberPassengerDestinationUpdateDraft(form) { const key = form?.dataset?.routeChangeRequestId; if (!key) return; const destinationInput = form.querySelector(".destination-update-input"); const stopsInput = form.querySelector(".stops-update-input"); passengerDestinationUpdateDrafts.set(key, { destination: String(destinationInput?.value ?? ""), stops: String(stopsInput?.value ?? ""), destinationPlace: normalizedPlaceSelection(form.__destinationUpdatePlace) }); } function markPassengerDestinationUpdateEditing(form) { if (!form) return; form.dataset.routeChangeEditingAt = String(Date.now()); rememberPassengerDestinationUpdateDraft(form); } function rememberPassengerDestinationUpdateDraftInputs() { document.querySelectorAll(".destination-update-form[data-route-change-request-id]").forEach(rememberPassengerDestinationUpdateDraft); } function passengerDestinationUpdateFocusedFieldSnapshot(container = document) { const active = document.activeElement; if (!(active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement)) return null; const form = active.closest(".destination-update-form[data-route-change-request-id]"); if (!form || !container.contains(form)) return null; const field = active.classList.contains("destination-update-input") ? "destination" : active.classList.contains("stops-update-input") ? "stops" : null; if (!field) return null; return { requestId: form.dataset.routeChangeRequestId || "", field, selectionStart: active.selectionStart, selectionEnd: active.selectionEnd, selectionDirection: active.selectionDirection || "none" }; } function passengerDestinationUpdateFocusedFormForRequest(request, container = document) { const active = document.activeElement; const requestId = String(request?.id ?? ""); if (!requestId || !container) return null; if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) { const form = active.closest(".destination-update-form[data-route-change-request-id]"); if (form && container.contains(form) && form.dataset.requestId === requestId) return form; } const now = Date.now(); return [...container.querySelectorAll(".destination-update-form[data-route-change-request-id]")] .find((form) => form.dataset.requestId === requestId && now - Number(form.dataset.routeChangeEditingAt || 0) <= passengerDestinationUpdateEditPreserveMs) ?? null; } function restorePassengerDestinationUpdateFocus(snapshot, container = document) { if (!snapshot?.requestId || !container) return; const form = [...container.querySelectorAll(".destination-update-form[data-route-change-request-id]")] .find((item) => item.dataset.routeChangeRequestId === snapshot.requestId); const control = form?.querySelector(snapshot.field === "stops" ? ".stops-update-input" : ".destination-update-input"); if (!(control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement)) return; try { control.focus({ preventScroll: true }); } catch { control.focus(); } if (typeof control.setSelectionRange !== "function") return; const valueLength = control.value.length; const start = Math.max(0, Math.min(valueLength, Number(snapshot.selectionStart ?? valueLength))); const end = Math.max(start, Math.min(valueLength, Number(snapshot.selectionEnd ?? start))); try { control.setSelectionRange(start, end, snapshot.selectionDirection || "none"); } catch { control.setSelectionRange(start, end); } } function clearPassengerDestinationUpdateDraft(requestOrId) { const key = passengerDestinationUpdateDraftKey(requestOrId); if (key) passengerDestinationUpdateDrafts.delete(key); if (typeof requestOrId === "string") passengerDestinationUpdateDrafts.delete(requestOrId); } function passengerDestinationUpdateDraftForRequest(request) { const key = passengerDestinationUpdateDraftKey(request); if (!key || !passengerDestinationUpdateDrafts.has(key)) { return { destination: passengerDestinationUpdateCurrentDestination(request), stops: "", destinationPlace: normalizedPlaceSelection({ placeId: request?.destinationPlaceId, displayName: request?.destination, formattedAddress: request?.destinationFormattedAddress || request?.destination, latitude: request?.destinationLatitude, longitude: request?.destinationLongitude }) }; } const draft = passengerDestinationUpdateDrafts.get(key); return { destination: String(draft?.destination ?? ""), stops: String(draft?.stops ?? ""), destinationPlace: normalizedPlaceSelection(draft?.destinationPlace) }; } function rememberPassengerFareDraftInputs() { document.querySelectorAll(".fare-boost-input[data-request-id]").forEach((input) => { if (input.dataset.requestId) passengerFareBoostDrafts.set(input.dataset.requestId, input.value); }); document.querySelectorAll(".offer-counter-input[data-offer-counter-key]").forEach((input) => { if (input.dataset.offerCounterKey) passengerOfferCounterDrafts.set(input.dataset.offerCounterKey, input.value); }); rememberPassengerDestinationUpdateDraftInputs(); } function passengerFareBoostFocusedInputSnapshot(container = document) { const active = document.activeElement; if (!(active instanceof HTMLInputElement)) return null; if (!active.classList.contains("fare-boost-input")) return null; if (!container?.contains(active)) return null; const requestId = active.dataset.requestId || ""; if (!requestId) return null; return { requestId, selectionStart: active.selectionStart, selectionEnd: active.selectionEnd, selectionDirection: active.selectionDirection || "none" }; } function rememberPassengerFareBoostFocus(input) { if (!(input instanceof HTMLInputElement)) return; if (!input.classList.contains("fare-boost-input")) return; const requestId = input.dataset.requestId || ""; if (!requestId) return; passengerFareBoostLastFocus = { requestId, selectionStart: input.selectionStart, selectionEnd: input.selectionEnd, selectionDirection: input.selectionDirection || "none", touchedAt: Date.now() }; } function passengerFareBoostFocusSnapshot(container = document) { const focused = passengerFareBoostFocusedInputSnapshot(container); if (focused) { passengerFareBoostLastFocus = { ...focused, touchedAt: Date.now() }; return focused; } return null; } function restorePassengerFareBoostFocus(snapshot, container = document) { if (!snapshot?.requestId || !container) return; const control = [...container.querySelectorAll(".fare-boost-input[data-request-id]")] .find((input) => input.dataset.requestId === snapshot.requestId); if (!(control instanceof HTMLInputElement)) return; try { control.focus({ preventScroll: true }); } catch { control.focus(); } passengerFareBoostLastFocus = { ...snapshot, touchedAt: Date.now() }; if (typeof control.setSelectionRange !== "function") return; if (!["text", "search", "tel", "url", "password"].includes(control.type)) return; const valueLength = control.value.length; const start = Math.max(0, Math.min(valueLength, Number(snapshot.selectionStart ?? valueLength))); const end = Math.max(start, Math.min(valueLength, Number(snapshot.selectionEnd ?? start))); try { control.setSelectionRange(start, end, snapshot.selectionDirection || "none"); } catch { try { control.setSelectionRange(start, end); } catch { // Mobile keyboards may reject selection restoration even when focus restoration works. } } } function passengerOfferCounterFocusedInputSnapshot(container = document) { const active = document.activeElement; if (!(active instanceof HTMLInputElement)) return null; if (!active.classList.contains("offer-counter-input")) return null; if (!container?.contains(active)) return null; const key = active.dataset.offerCounterKey || ""; if (!key) return null; return { key, selectionStart: active.selectionStart, selectionEnd: active.selectionEnd, selectionDirection: active.selectionDirection || "none" }; } function rememberPassengerOfferCounterFocus(input) { if (!(input instanceof HTMLInputElement)) return; if (!input.classList.contains("offer-counter-input")) return; const key = input.dataset.offerCounterKey || ""; if (!key) return; passengerOfferCounterLastFocus = { key, selectionStart: input.selectionStart, selectionEnd: input.selectionEnd, selectionDirection: input.selectionDirection || "none", touchedAt: Date.now() }; } function passengerOfferCounterFocusSnapshot(container = document) { const focused = passengerOfferCounterFocusedInputSnapshot(container); if (focused) { passengerOfferCounterLastFocus = { ...focused, touchedAt: Date.now() }; return focused; } return null; } function restorePassengerOfferCounterFocus(snapshot, container = document) { if (!snapshot?.key || !container) return; const control = [...container.querySelectorAll(".offer-counter-input[data-offer-counter-key]")] .find((input) => input.dataset.offerCounterKey === snapshot.key); if (!(control instanceof HTMLInputElement)) return; try { control.focus({ preventScroll: true }); } catch { control.focus(); } passengerOfferCounterLastFocus = { ...snapshot, touchedAt: Date.now() }; if (typeof control.setSelectionRange !== "function") return; if (!["text", "search", "tel", "url", "password"].includes(control.type)) return; const valueLength = control.value.length; const start = Math.max(0, Math.min(valueLength, Number(snapshot.selectionStart ?? valueLength))); const end = Math.max(start, Math.min(valueLength, Number(snapshot.selectionEnd ?? start))); try { control.setSelectionRange(start, end, snapshot.selectionDirection || "none"); } catch { try { control.setSelectionRange(start, end); } catch { // Some mobile keyboards expose a text input without allowing programmatic selection. } } } function chatInputFocusSnapshot() { const input = els.chatInput; if (!(input instanceof HTMLInputElement) && !(input instanceof HTMLTextAreaElement)) return null; if (document.activeElement !== input) return null; const request = selectedRequest(); return { requestId: request?.id || "", value: input.value, selectionStart: input.selectionStart, selectionEnd: input.selectionEnd, selectionDirection: input.selectionDirection || "none" }; } function restoreChatInputFocus(snapshot, request, shouldRestore) { const input = els.chatInput; if (!snapshot?.requestId || !request?.id || snapshot.requestId !== request.id || !shouldRestore) return; if (!(input instanceof HTMLInputElement) && !(input instanceof HTMLTextAreaElement)) return; if (input.disabled || els.chatForm?.hidden) return; if (input.value !== snapshot.value) input.value = snapshot.value; try { input.focus({ preventScroll: true }); } catch { input.focus(); } if (typeof input.setSelectionRange !== "function") return; const valueLength = input.value.length; const start = Math.max(0, Math.min(valueLength, Number(snapshot.selectionStart ?? valueLength))); const end = Math.max(start, Math.min(valueLength, Number(snapshot.selectionEnd ?? start))); try { input.setSelectionRange(start, end, snapshot.selectionDirection || "none"); } catch { try { input.setSelectionRange(start, end); } catch { // Mobile keyboards may preserve focus but reject selection restoration. } } } function setPassengerProfileAvatarFallback(passenger = state.passenger) { if (!els.passengerProfileAvatar) return; els.passengerProfileAvatar.textContent = passengerInitials(passenger); } async function ensurePassengerProfilePhotoUrl(passenger = state.passenger) { if (!els.passengerProfileAvatar || !passenger?.profilePhotoPath || !isSupabaseMode() || !supabaseClient) return; const cacheKey = passenger.profilePhotoPath; const cached = passengerProfilePhotoUrlCache.get(cacheKey); if (cached) { els.passengerProfileAvatar.innerHTML = ``; return; } try { const { data, error } = await supabaseClient.storage .from(appConfig.buckets.profilePhotos) .createSignedUrl(passenger.profilePhotoPath, 600); if (error || !data?.signedUrl) throw error || new Error("Signed profile photo URL was not returned."); passengerProfilePhotoUrlCache.set(cacheKey, data.signedUrl); if (state.passenger?.profilePhotoPath === passenger.profilePhotoPath) { els.passengerProfileAvatar.innerHTML = ``; } } catch (error) { logClientWarning("Passenger profile picture could not be displayed.", error); setPassengerProfileAvatarFallback(passenger); } } const referralCodeCache = { passenger: null, rider: null, business: null }; const referralCodeLoads = { passenger: null, rider: null, business: null }; const referralCodeErrors = { passenger: null, rider: null, business: null }; function normalizeReferralCodeRow(row) { const item = Array.isArray(row) ? row[0] : row; return item?.code ? { code: String(item.code), ownerRole: item.owner_role ?? item.ownerRole ?? "", status: item.status ?? "active" } : null; } async function loadReferralCodeForRole(role, options = {}) { const force = options.force === true; if (!hasSupabaseRuntime() || !referralRoleSignedIn(role)) return null; if (!force && referralCodeCache[role]) return referralCodeCache[role]; if (!referralCodeLoads[role]) { referralCodeErrors[role] = null; referralCodeLoads[role] = callSupabaseRpcResult( "referral_code_for_user", { p_owner_role: role }, "Loading referral code", optionalSupabaseRequestTimeoutMs ) .then((row) => { referralCodeCache[role] = normalizeReferralCodeRow(row); return referralCodeCache[role]; }) .catch((error) => { referralCodeErrors[role] = compactUserMessage(error, "Referral code could not be loaded."); logClientWarning(`${role} referral code could not be loaded.`, error); return null; }) .finally(() => { referralCodeLoads[role] = null; }); } return referralCodeLoads[role]; } function referralCodeInputForRole(role) { if (role === "business") return els.businessReferralCode; return role === "rider" ? els.riderReferralCode : els.passengerReferralCode; } function referralRoleSignedIn(role) { if (role === "business") return Boolean(hasSignedIn("passenger") && passengerBusinessAccounts().length); return hasSignedIn(role); } function compactUserMessage(error, fallback) { const message = String(error?.message || error || fallback || "Something went wrong.").trim(); return message.length > 180 ? `${message.slice(0, 177)}...` : message; } async function claimReferralCodeValue(role, codeValue, statusEl = null) { const code = String(codeValue ?? "").trim(); if (!code || !hasSupabaseRuntime()) return null; try { const result = await callSupabaseRpcResult( "claim_referral_code", { p_code: code, p_referred_role: role }, "Claiming referral code", optionalSupabaseRequestTimeoutMs ); if (statusEl) statusEl.textContent = `${statusEl.textContent} Referral code applied.`; return result; } catch (error) { logClientWarning(`${role} referral code could not be claimed.`, error); if (statusEl) statusEl.textContent = `${statusEl.textContent} Referral code was not applied: ${error.message}`; return null; } } async function claimReferralCodeForRole(role, statusEl = null) { const input = referralCodeInputForRole(role); const result = await claimReferralCodeValue(role, input?.value, statusEl); if (result && input) input.value = ""; return result; } function referralPanelElements(role) { if (role === "business") { return { panel: els.businessReferralPanel, summary: els.businessReferralSummary, display: els.businessReferralCodeDisplay, copy: els.copyBusinessReferralCode, share: els.shareBusinessReferralCode, email: els.emailBusinessReferralCode, text: els.textBusinessReferralCode, how: els.businessReferralHowItWorks }; } return role === "rider" ? { panel: els.riderReferralPanel, summary: els.riderReferralSummary, display: els.riderReferralCodeDisplay, copy: els.copyRiderReferralCode, share: els.shareRiderReferralCode, email: els.emailRiderReferralCode, text: els.textRiderReferralCode, how: els.riderReferralHowItWorks } : { panel: els.passengerReferralPanel, summary: els.passengerReferralSummary, display: els.passengerReferralCodeDisplay, copy: els.copyPassengerReferralCode, share: els.sharePassengerReferralCode, email: els.emailPassengerReferralCode, text: els.textPassengerReferralCode, how: els.passengerReferralHowItWorks }; } function referralLinkForRole(role, code) { if (role === "business") return `${window.location.origin}/agency?passengerPage=business&business=1&ref=${encodeURIComponent(code)}`; const path = role === "rider" ? "/rider" : "/passenger"; return `${window.location.origin}${path}?ref=${encodeURIComponent(code)}`; } function referralShareMessage(role, code) { const link = referralLinkForRole(role, code); if (role === "business") return `Create a Waka agency account with my invite code ${code}: ${link}`; return role === "rider" ? `Join Waka as a rider with my invite code ${code}: ${link}` : `Try Waka with my invite code ${code}: ${link}`; } function referralRewardExplanation(role) { if (role === "business") return "Share your business invite link with hotels, clinics, employers, schools, venues, and other organizations. Waka records business invite relationships for admin review, credits, and partner-benefit decisions after verification."; return role === "rider" ? "Share your rider invite link by text, email, social share, or copy. Rewards post after the referred rider is approved and completes five paid rides; Waka records the reward in the referral ledger for admin review and payout/subscription-credit handling." : "Share your passenger invite link by text, email, social share, or copy. Ride credits post after the referred passenger creates an account and completes a paid ride; Waka records both the inviter and new-passenger credits in the referral ledger."; } function setReferralPanelStatus(elements, message) { if (elements?.summary) elements.summary.textContent = message; } function preserveReferralWorkspacePage(role) { if (role === "business") { state.activeTab = "passenger"; state.passengerPage = "business"; updatePassengerWorkspaceRoute("business", { replace: true }); saveState(); return; } if (role === "rider") { state.activeTab = "rider"; state.riderPage = "rewards"; updateRiderWorkspaceRoute("rewards", { replace: true }); saveState(); return; } if (role === "passenger") { state.activeTab = "passenger"; state.passengerPage = "rewards"; updatePassengerWorkspaceRoute("rewards", { replace: true }); saveState(); } } function referralActionLink(role, action, code) { const subject = role === "rider" ? "Join Waka as a rider" : role === "business" ? "Create a Waka business account" : "Try Waka"; const message = referralShareMessage(role, code); if (action === "email") return `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(message)}`; if (action === "text") return `sms:?&body=${encodeURIComponent(message)}`; return referralLinkForRole(role, code); } async function copyTextToClipboard(text) { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); return true; } const textarea = document.createElement("textarea"); textarea.value = text; textarea.setAttribute("readonly", ""); textarea.style.position = "fixed"; textarea.style.left = "-9999px"; textarea.style.top = "0"; document.body.append(textarea); textarea.select(); let copied = false; try { copied = document.execCommand("copy"); } finally { textarea.remove(); } if (!copied) throw new Error("Clipboard copy is not available in this browser."); return true; } async function ensureReferralForAction(role, elements) { const cached = referralCodeCache[role]; if (cached?.code) return cached; setReferralPanelStatus(elements, "Loading your invite code..."); const loaded = await loadReferralCodeForRole(role, { force: true }); if (loaded?.code) { renderReferralPanel(role); return loaded; } const message = referralCodeErrors[role] || "Referral code is not available yet. Try again after the page finishes syncing."; setReferralPanelStatus(elements, message); throw new Error(message); } function setReferralShareTargets(role, code, elements) { const applyLinkTargets = (nextCode) => { if (!nextCode) { [elements.email, elements.text].forEach((linkElement) => { if (!linkElement) return; linkElement.href = "#"; linkElement.setAttribute("aria-disabled", "true"); }); return; } const message = referralShareMessage(role, nextCode); const subject = role === "rider" ? "Join Waka as a rider" : role === "business" ? "Create a Waka business account" : "Try Waka"; if (elements.email) { elements.email.href = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(message)}`; elements.email.removeAttribute("aria-disabled"); } if (elements.text) { elements.text.href = `sms:?&body=${encodeURIComponent(message)}`; elements.text.removeAttribute("aria-disabled"); } }; applyLinkTargets(code); } async function runReferralShareAction(role, action) { const elements = referralPanelElements(role); preserveReferralWorkspacePage(role); try { const referral = await ensureReferralForAction(role, elements); const link = referralLinkForRole(role, referral.code); const subject = role === "rider" ? "Join Waka as a rider" : role === "business" ? "Create a Waka business account" : "Try Waka"; const message = referralShareMessage(role, referral.code); if (action === "copy") { await copyTextToClipboard(link); setReferralPanelStatus(elements, `Copied invite link for ${referral.code}.`); return; } if (action === "share") { if (navigator.share) { try { await navigator.share({ title: subject, text: message, url: link }); setReferralPanelStatus(elements, `Shared invite link for ${referral.code}.`); return; } catch (error) { if (error?.name === "AbortError") { preserveReferralWorkspacePage(role); return; } } } await copyTextToClipboard(link); setReferralPanelStatus(elements, `Share sheet is not available here, so Waka copied ${referral.code}.`); return; } if (action === "email" || action === "text") { const actionLink = referralActionLink(role, action, referral.code); setReferralPanelStatus(elements, `${action === "email" ? "Opening email" : "Opening text message"} for ${referral.code}.`); window.location.href = actionLink; } } catch (error) { const fallbackCode = referralCodeCache[role]?.code; if (fallbackCode && (action === "copy" || action === "share")) { const link = referralLinkForRole(role, fallbackCode); setReferralPanelStatus(elements, link); } else { setReferralPanelStatus(elements, compactUserMessage(error, "Invite action could not be completed.")); } } finally { preserveReferralWorkspacePage(role); } } function handleReferralActionClick(event) { const target = event.target?.closest?.("[data-referral-role][data-referral-action]"); if (!target) return false; const role = String(target.dataset.referralRole || "").trim().toLowerCase(); const action = String(target.dataset.referralAction || "").trim().toLowerCase(); if (!["passenger", "rider", "business"].includes(role) || !["copy", "share", "email", "text"].includes(action)) return false; event.preventDefault(); event.stopPropagation(); if (typeof event.stopImmediatePropagation === "function") event.stopImmediatePropagation(); void runReferralShareAction(role, action); return true; } function renderReferralPanel(role) { const elements = referralPanelElements(role); const { panel, summary, display, copy, share, email, text, how } = elements; if (!panel) return; const onReferralPage = role === "business" ? passengerWorkspacePage() === "business" : role === "rider" ? riderWorkspacePage() === "rewards" : passengerWorkspacePage() === "rewards"; const signedIn = referralRoleSignedIn(role); panel.hidden = !signedIn || !onReferralPage; if (!signedIn || !onReferralPage) return; const referral = referralCodeCache[role]; if (!referral) { summary.textContent = role === "business" ? "Give this code to organizations creating Waka business accounts. Admin can track business referrals after verification." : role === "rider" ? "Give this code to new riders. Rewards are credited after the referred rider completes the required launch rides." : "Give this code to new passengers. Ride credits are credited after the referred passenger completes a paid ride."; if (referralCodeErrors[role]) { summary.textContent = `${referralCodeErrors[role]} Tap Copy link or Share to try loading it again.`; } display.textContent = "Loading code"; [copy, share].forEach((button) => { if (button) button.disabled = false; }); if (how) how.textContent = referralRewardExplanation(role); setReferralShareTargets(role, null, elements); if (!referralCodeErrors[role] && !referralCodeLoads[role]) { void loadReferralCodeForRole(role).then(() => renderReferralPanel(role)); } return; } summary.textContent = role === "business" ? "Business referral benefits are tracked after Waka verifies the referred organization." : role === "rider" ? "Rider rewards unlock after a referred rider completes five paid rides." : "Passenger ride credits unlock after a referred passenger completes a paid ride."; if (how) how.textContent = referralRewardExplanation(role); display.textContent = referral.code; [copy, share].forEach((button) => { if (button) button.disabled = false; }); [email, text].forEach((link) => { if (!link) return; link.removeAttribute("aria-disabled"); }); setReferralShareTargets(role, referral.code, elements); } const passengerWorkspacePages = ["request", "trips", "agency", "payment", "business", "rewards", "profile", "notices", "support"]; const agencyWorkspacePages = ["business", "profile", "notices", "support"]; const passengerWorkspacePageLabels = { request: "Ride request", trips: "My trips", agency: "Inter-city travel", payment: "Payment", business: "Business", rewards: "Rewards", profile: "Profile", notices: "Notices", support: "Support" }; const riderWorkspacePages = ["overview", "initialize", "checks", "requests", "destination", "earnings", "payment", "ratings", "rewards", "notices", "support", "profile"]; const riderWorkspacePageLabels = { overview: "Overview", initialize: "Initialize rider availability", checks: "Eligibility checks", requests: "Ride requests", destination: "Destination", earnings: "Earnings", payment: "Payment account", ratings: "Ratings", rewards: "Rewards", notices: "Notices", support: "Support", profile: "Profile" }; const riderWorkspacePageSummaries = { overview: "Review rider readiness, account status, wallet or payout setup, and next steps.", initialize: "Set today's service region, preferred destinations, and online availability.", checks: "Monitor rider application review, eligibility, and background-check progress.", requests: "Review incoming passenger requests, accept the passenger fare, or send a higher counter-offer.", destination: "Choose an optional destination preference for the rider marketplace.", earnings: "Review completed rides, wallet/direct-payment status, payout status, and earnings by period.", payment: "Review rider wallet mode or update payout setup when an online provider is enabled.", ratings: "View anonymous passenger rating percentages and category counts.", rewards: "Share Waka with passengers or riders and track referral rewards.", notices: "Read Waka notices and enable phone notifications for rider updates.", support: "Send rider support requests directly to Waka admin.", profile: "Review rider identity, vehicle details, profile picture, and navigation preference." }; function availableRiderWorkspacePages(rider = currentRiderRecord()) { if (!rider) return riderWorkspacePages; if (rider.status === "approved") return riderWorkspacePages; if (rider.status === "pending" || rider.status === "background_pending") return ["checks"]; if (rider.status === "needs_correction") return ["profile"]; if (rider.needsApplication || rider.status === "profile only") return ["profile"]; if (rider.status === "declined" || rider.status === "suspended") return ["checks"]; return ["checks"]; } function passengerBusinessWorkspaceEnabled(passenger = state.passenger) { return Boolean( passenger?.accountUse === "business" || passengerBusinessAccounts(passenger).length || passengerBusinessIntentFromLocation() ); } function agencyWorkspaceActive() { return passengerBusinessIntentFromLocation(); } function availablePassengerWorkspacePages() { if (agencyWorkspaceActive()) return agencyWorkspacePages.slice(); return passengerWorkspacePages.filter((page) => page !== "business" || passengerBusinessWorkspaceEnabled()); } function passengerWorkspacePageLabel(page) { if (!agencyWorkspaceActive()) return passengerWorkspacePageLabels[page] ?? "Ride request"; const agencyLabels = { business: "Agency workspace", profile: "Agency profile", notices: "Agency notices", support: "Operator support" }; return agencyLabels[page] ?? passengerWorkspacePageLabels[page] ?? "Agency workspace"; } function syncPassengerAgencyTravelMount(passengerSignedIn, page) { const publicMount = els.publicIntercityExperienceMount; const host = els.passengerAgencyTravelHost; const agencyPagePanel = els.publicAgencyPagePanel; const travelBand = document.querySelector(".intercity-public-band#intercity-travel"); const promotionsPanel = els.publicIntercityPromotionsPanel; const guidanceGrid = els.publicIntercityGuidanceGrid; const resultsGrid = els.publicIntercityResultsGrid; if (!publicMount || !host || !travelBand) return; const shouldMountInWorkspace = Boolean(passengerSignedIn && !agencyWorkspaceActive() && page === "agency"); const target = shouldMountInWorkspace ? host : publicMount; [agencyPagePanel, travelBand, promotionsPanel, guidanceGrid, resultsGrid].filter(Boolean).forEach((node) => { if (node.parentElement !== target) target.append(node); }); } function passengerWorkspacePage() { const pages = availablePassengerWorkspacePages(); const requestedPage = requestedPassengerWorkspacePageFromLocation(); const requestedTripDeepLink = requestedPage === "trips" && Boolean(requestedRideRequestIdFromLocation()); const shouldHonorRequestedPage = !hasSignedIn("passenger") || passengerWorkspacePageSelectedInSession || requestedTripDeepLink; if (requestedPage && pages.includes(requestedPage) && shouldHonorRequestedPage) return requestedPage; if (hasSignedIn("passenger") && pages.includes(state.passengerPage)) return state.passengerPage; if (!hasSignedIn("passenger") && pages.includes(state.passengerPage)) return state.passengerPage; return agencyWorkspaceActive() ? "business" : "request"; } function passengerWorkspaceRouteForPage(page, requestId = "", { preferPathRoute = false } = {}) { if (!passengerWorkspacePages.includes(page)) return null; const pathTab = routePathTab(); const shouldUsePassengerPath = pathTab === "passenger" || (preferPathRoute && typeof publicHomePathIsActive === "function" && publicHomePathIsActive()); if (shouldUsePassengerPath) { const params = new URLSearchParams(window.location.search); params.set("passengerPage", page); if (requestId) params.set("requestId", requestId); if (!requestId && page !== "trips") params.delete("requestId"); const query = params.toString(); const pathname = pathTab === "passenger" ? window.location.pathname : "/passenger"; return `${pathname}${query ? `?${query}` : ""}`; } const params = new URLSearchParams(window.location.search); ["passengerPage", "passenger_page", "requestId", "rideRequestId", "ride_request_id"].forEach((key) => params.delete(key)); const cleanQuery = params.toString(); const query = requestId ? `?requestId=${encodeURIComponent(requestId)}` : ""; return `${window.location.pathname}${cleanQuery ? `?${cleanQuery}` : ""}#passenger/${page}${query}`; } function updatePassengerWorkspaceRoute(page, { replace = false, requestId = "", preferPathRoute = false } = {}) { const route = passengerWorkspaceRouteForPage(page, requestId, { preferPathRoute }); if (!route) return; const current = `${window.location.pathname}${window.location.search}${window.location.hash}`; if (route === current) return; window.history[replace ? "replaceState" : "pushState"]({ passengerPage: page, requestId }, "", route); } function setPassengerWorkspacePage(page) { if (!availablePassengerWorkspacePages().includes(page)) return; passengerWorkspacePageSelectedInSession = true; state.passengerPage = page; els.passengerWorkspaceMenu?.removeAttribute("open"); updatePassengerWorkspaceRoute(page); saveState(); renderAll(); } function riderWorkspacePage() { const pages = availableRiderWorkspacePages(); const requestedPage = requestedRiderWorkspacePageFromLocation(); if (requestedPage && pages.includes(requestedPage)) return requestedPage; if (hasSignedIn("rider") && pages.includes(state.riderPage)) { const rider = currentRiderRecord(); if (state.riderPage === "checks" && rider?.status === "approved" && isSubscriptionActive(rider)) { return pages.includes("initialize") ? "initialize" : state.riderPage; } return state.riderPage; } if (hasSignedIn("rider")) return riderWorkspaceLandingPageAfterSignIn(currentRiderRecord()); if (pages.includes(state.riderPage)) return state.riderPage; return pages.includes("overview") ? "overview" : pages[0] ?? "overview"; } function riderWorkspaceRouteForPage(page, requestId = "") { if (!riderWorkspacePages.includes(page)) return null; if (routePathTab() === "rider") { const params = new URLSearchParams(window.location.search); params.set("riderPage", page); if (requestId) params.set("requestId", requestId); if (!requestId) params.delete("requestId"); const query = params.toString(); return `${window.location.pathname}${query ? `?${query}` : ""}${window.location.hash || ""}`; } const params = new URLSearchParams(window.location.search); ["riderPage", "rider_page", "requestId", "rideRequestId", "ride_request_id"].forEach((key) => params.delete(key)); const cleanQuery = params.toString(); const query = requestId ? `?requestId=${encodeURIComponent(requestId)}` : ""; return `${window.location.pathname}${cleanQuery ? `?${cleanQuery}` : ""}#rider/${page}${query}`; } function updateRiderWorkspaceRoute(page, { replace = false, requestId = "" } = {}) { const route = riderWorkspaceRouteForPage(page, requestId); if (!route) return; const current = `${window.location.pathname}${window.location.search}${window.location.hash}`; if (route === current) return; window.history[replace ? "replaceState" : "pushState"]({ riderPage: page, requestId }, "", route); } function riderWorkspaceLandingPageAfterSignIn(rider = currentRiderRecord(), startingApplication = false, { honorRequestedPage = true } = {}) { const pages = availableRiderWorkspacePages(rider); const requestedPage = honorRequestedPage ? requestedRiderWorkspacePageFromLocation() : ""; if (requestedPage && pages.includes(requestedPage)) return requestedPage; if (startingApplication || rider?.needsApplication || rider?.status === "profile only" || rider?.status === "needs_correction") { return pages.includes("profile") ? "profile" : pages[0] ?? "overview"; } if (rider?.status === "pending" || rider?.status === "background_pending" || rider?.status === "declined" || rider?.status === "suspended") { return pages.includes("checks") ? "checks" : pages[0] ?? "overview"; } return pages.includes("initialize") ? "initialize" : pages.includes("overview") ? "overview" : pages[0] ?? "overview"; } function setRiderWorkspaceLandingAfterSignIn(startingApplication = false, { honorRequestedPage = false, replaceRoute = true, restoreStoredWorkspace = false } = {}) { if (restoreStoredWorkspace && !startingApplication && typeof restoreWorkspaceUiState === "function" && restoreWorkspaceUiState("rider", { replaceRoute })) { saveState(); return; } state.riderPage = riderWorkspaceLandingPageAfterSignIn(currentRiderRecord(), startingApplication, { honorRequestedPage }); if (replaceRoute && typeof updateRiderWorkspaceRoute === "function") { updateRiderWorkspaceRoute(state.riderPage, { replace: true }); } saveState(); } function requestedRiderWorkspacePageFromLocation() { const hashParts = window.location.hash.replace(/^#\/?/, "").toLowerCase().split(/[/?&=]/).filter(Boolean); const riderIndex = hashParts.indexOf("rider"); const hashPage = riderIndex >= 0 ? hashParts[riderIndex + 1] : ""; if (riderWorkspacePages.includes(hashPage)) return hashPage; const params = new URLSearchParams(window.location.search); const page = String(params.get("riderPage") ?? params.get("rider_page") ?? "").trim().toLowerCase(); if (riderWorkspacePages.includes(page)) return page; return null; } function requestedPassengerWorkspacePageFromLocation() { const hashParts = window.location.hash.replace(/^#\/?/, "").toLowerCase().split(/[/?&=]/).filter(Boolean); const passengerIndex = hashParts.indexOf("passenger"); const hashPage = passengerIndex >= 0 ? hashParts[passengerIndex + 1] : ""; if (passengerWorkspacePages.includes(hashPage)) return hashPage; const params = new URLSearchParams(window.location.search); const page = String(params.get("passengerPage") ?? params.get("passenger_page") ?? "").trim().toLowerCase(); if (passengerWorkspacePages.includes(page)) return page; return null; } function revealPassengerAgencyTravelPanelForRoute(page = state.passengerPage) { if (page !== "agency" && requestedPassengerWorkspacePageFromLocation() !== "agency") return; if (els.passengerAgencyTravelPanel) { els.passengerAgencyTravelPanel.hidden = false; els.passengerAgencyTravelPanel.setAttribute("aria-hidden", "false"); } } function requestedRideRequestIdFromLocation() { const params = new URLSearchParams(window.location.search); const requestId = String(params.get("requestId") ?? params.get("rideRequestId") ?? params.get("ride_request_id") ?? "").trim(); if (requestId) return requestId; const hash = window.location.hash; if (!hash.includes("?")) return null; const hashParams = new URLSearchParams(hash.slice(hash.indexOf("?") + 1)); const hashRequestId = String(hashParams.get("requestId") ?? hashParams.get("rideRequestId") ?? hashParams.get("ride_request_id") ?? "").trim(); return hashRequestId || null; } function riderPageSectionIncludes(sectionValue, page = riderWorkspacePage()) { return String(sectionValue || "").split(/\s+/).includes(page); } function setRiderWorkspacePage(page) { if (!riderWorkspacePages.includes(page)) return; const availablePages = availableRiderWorkspacePages(); if (!availablePages.includes(page)) { page = riderWorkspaceLandingPageAfterSignIn(currentRiderRecord(), false, { honorRequestedPage: false }); } state.riderPage = page; els.riderWorkspaceMenu?.removeAttribute("open"); updateRiderWorkspaceRoute(page); saveState(); renderAll(); if (page === "requests") void refreshMarketplace({ silent: true }); if (page === "notices") { void refreshAccountNotificationsFromSupabase("rider", { force: true }).then(() => renderAccountNotices("rider")); } } function passengerInitials(passenger = state.passenger) { const name = String(passenger?.name ?? passenger?.email ?? passenger?.phone ?? "Passenger").trim(); const parts = name.split(/\s+/).filter(Boolean); return (parts.length > 1 ? `${parts[0][0]}${parts[1][0]}` : parts[0]?.slice(0, 2) || "P").toUpperCase(); } function updatePassengerInitialBusinessFields() { if (!els.passengerInitialBusinessFields || !els.passengerAccountUse) return; const wantsBusiness = passengerBusinessIntentFromLocation(); els.passengerAccountUse.value = wantsBusiness ? "business" : "personal"; const accountUseField = document.querySelector("#passengerAccountUseField"); if (accountUseField) accountUseField.hidden = true; els.passengerInitialBusinessFields.hidden = !wantsBusiness; const personalIdentityFields = document.querySelector("#passengerPersonalIdentityFields"); if (personalIdentityFields) personalIdentityFields.hidden = wantsBusiness; [els.passengerNationalId, els.passengerDob].forEach((input) => { if (!input) return; input.required = !wantsBusiness; if (wantsBusiness) input.value = ""; }); } function passengerBusinessIntentFromLocation() { const pathSegments = routePathSegments(); if (pathSegments[0] === "agency") return true; const params = new URLSearchParams(window.location.search); const value = String( params.get("business") ?? params.get("agency") ?? params.get("operator") ?? "" ).trim().toLowerCase(); return ["1", "true", "yes", "business", "agency", "operator"].includes(value); } function applyRequestedPassengerAccountIntent() { if (!els.passengerAccountForm || !els.passengerAccountUse) return; if (!passengerBusinessIntentFromLocation()) { els.passengerAccountUse.value = "personal"; delete els.passengerAccountForm.dataset.businessIntentApplied; updatePassengerInitialBusinessFields(); return; } if (els.passengerAccountForm.hidden) return; if (els.passengerAccountForm.dataset.businessIntentApplied === "true") return; els.passengerAccountUse.value = "business"; els.passengerAccountForm.dataset.businessIntentApplied = "true"; updatePassengerInitialBusinessFields(); } function applyPassengerAgencyEntryCopy() { const businessIntent = passengerBusinessIntentFromLocation(); const authHero = document.querySelector("#passenger-panel .passenger-auth-hero"); const panelHeadingTitle = els.passengerPanelHeading?.querySelector("p"); const panelHeadingSummary = els.passengerPanelHeading?.querySelector("span"); const heroKicker = authHero?.querySelector(".auth-entry-copy .card-kicker"); const heroTitle = document.querySelector("#passengerAuthTitle"); const heroBody = authHero?.querySelector(".auth-entry-copy p"); const signInKicker = els.passengerSignInForm?.querySelector(".auth-form-heading .card-kicker"); const signInTitle = els.passengerSignInForm?.querySelector(".auth-form-heading h2"); const signInBody = els.passengerSignInForm?.querySelector(".auth-form-heading p"); const createTitle = els.passengerAccountForm?.querySelector("h2"); const createPrompt = els.passengerSignInForm?.querySelector(".auth-create-prompt span"); const createLink = els.passengerSignInForm?.querySelector("[data-account-create-link='passenger']"); const createSignInPrompt = document.querySelector("#passengerAccountRoutePrompt"); const createSignInLink = els.passengerAccountForm?.querySelector("[data-account-signin-link='passenger']"); const signInStatus = els.passengerSignInStatus; const workspaceKicker = els.passengerWorkspaceHeader?.querySelector(".card-kicker"); const businessNavButton = els.passengerWorkspaceNav?.querySelector('[data-passenger-page="business"]'); const profileNavButton = els.passengerWorkspaceNav?.querySelector('[data-passenger-page="profile"]'); const noticesNavButton = els.passengerWorkspaceNav?.querySelector('[data-passenger-page="notices"]'); const supportNavButton = els.passengerWorkspaceNav?.querySelector('[data-passenger-page="support"]'); const trustItems = [...(authHero?.querySelectorAll(".auth-trust-row span") ?? [])]; const authStat = authHero?.querySelector(".auth-entry-stat"); document.documentElement.dataset.agencyEntry = businessIntent ? "true" : "false"; document.body.dataset.agencyEntry = businessIntent ? "true" : "false"; document.body.dataset.agencyAccountMode = businessIntent ? accountMode("passenger") : ""; if (!businessIntent) { if (panelHeadingTitle) panelHeadingTitle.textContent = "Passenger"; if (panelHeadingSummary) panelHeadingSummary.textContent = "Request a ride and choose the best offer"; if (heroKicker) heroKicker.textContent = "Passenger access"; if (heroTitle) heroTitle.textContent = "Move with the fare already clear."; if (heroBody) heroBody.textContent = "Request landmark-based rides, compare offers, follow pickup, and keep trip support in one secure Waka Cameroon account."; if (signInKicker) signInKicker.textContent = "Welcome back"; if (signInTitle) signInTitle.textContent = "Passenger sign in"; if (signInBody) signInBody.textContent = "Sign in to your Waka Cameroon passenger account."; if (createTitle) createTitle.textContent = translatedValue("createPassenger") || "Create passenger account"; if (createPrompt) createPrompt.textContent = "New to Waka Cameroon?"; if (createLink) createLink.textContent = translatedValue("createAccount") || "Create account"; if (createLink) createLink.setAttribute("href", "/passenger/create"); if (createSignInPrompt) createSignInPrompt.textContent = "Already have a Waka Cameroon passenger account?"; if (createSignInLink) createSignInLink.setAttribute("href", "/passenger"); if (workspaceKicker) workspaceKicker.textContent = "Passenger"; if (trustItems[0]) trustItems[0].innerHTML = "F Fare first"; if (trustItems[1]) trustItems[1].innerHTML = "V Verified riders"; if (trustItems[2]) trustItems[2].innerHTML = "24 Support"; if (authStat) authStat.innerHTML = "Fare firstThen match"; if (authStat) authStat.setAttribute("data-stat-icon", "F"); if (businessNavButton) businessNavButton.textContent = "Business"; if (profileNavButton) profileNavButton.textContent = "Profile"; if (noticesNavButton) noticesNavButton.textContent = "Notices"; if (supportNavButton) supportNavButton.textContent = "Support"; if (els.passengerWorkspaceNav) els.passengerWorkspaceNav.setAttribute("aria-label", "Passenger workspace sections"); if (signInStatus && !hasSignedIn("passenger") && accountMode("passenger") === "signin" && !signInStatus.textContent.trim()) { signInStatus.textContent = "Use email and password to sign in before requesting rides."; } return; } if (panelHeadingTitle) panelHeadingTitle.textContent = "Agency"; if (panelHeadingSummary) panelHeadingSummary.textContent = "Sign in or create operator access to manage departures and bookings"; if (heroKicker) heroKicker.textContent = "Agency access"; if (heroTitle) heroTitle.textContent = "Run departures, seats, and traveler updates from one workspace."; if (heroBody) heroBody.textContent = "Create or sign in to your Waka Cameroon agency access, submit the agency for admin approval, then publish inter-city departures, manage bookings, and keep traveler communication in one operator workspace."; if (trustItems[0]) trustItems[0].innerHTML = "A Admin approval"; if (trustItems[1]) trustItems[1].innerHTML = "D Departure control"; if (trustItems[2]) trustItems[2].innerHTML = "B Bookings and messages"; if (authStat) authStat.innerHTML = "Agency accountSecure operator access"; if (authStat) authStat.setAttribute("data-stat-icon", "A"); if (signInKicker) signInKicker.textContent = translatedValue("agencySignInKicker") || "Agency access"; if (signInTitle) signInTitle.textContent = translatedValue("agencySignInTitle") || "Agency sign in"; if (signInBody) signInBody.textContent = translatedValue("agencySignInBody") || "Sign in to open your Waka Cameroon agency workspace, complete admin approval, publish departures, and manage bookings."; if (createTitle) createTitle.textContent = translatedValue("agencyCreateTitle") || "Create agency account"; if (createPrompt) createPrompt.textContent = translatedValue("agencyCreatePrompt") || "New transport company to Waka Cameroon?"; if (createLink) createLink.textContent = translatedValue("agencyCreateCta") || "Create agency account"; if (createLink) createLink.setAttribute("href", "/agency/create"); if (createSignInPrompt) createSignInPrompt.textContent = "Already have a Waka Cameroon agency account?"; if (createSignInLink) createSignInLink.setAttribute("href", "/agency"); if (workspaceKicker) workspaceKicker.textContent = "Agency"; if (businessNavButton) businessNavButton.textContent = "Agency workspace"; if (profileNavButton) profileNavButton.textContent = "Agency profile"; if (noticesNavButton) noticesNavButton.textContent = "Agency notices"; if (supportNavButton) supportNavButton.textContent = "Operator support"; if (els.passengerWorkspaceNav) els.passengerWorkspaceNav.setAttribute("aria-label", "Agency workspace sections"); if (signInStatus && !hasSignedIn("passenger") && accountMode("passenger") === "signin") { signInStatus.textContent = translatedValue("agencyEntryHelp") || "Agency sign in is for operators that already have Waka agency access. Create agency account is for a new transport company starting setup for the first time. Passenger account creation stays separate."; } } function renderPassengerWorkspacePages(passengerSignedIn) { const passengerPasswordResetActive = typeof passwordResetModeActive === "function" && passwordResetModeActive("passenger"); passengerSignedIn = Boolean(passengerSignedIn && !passengerPasswordResetActive); const requestedPage = requestedPassengerWorkspacePageFromLocation(); const page = requestedPage === "agency" ? "agency" : passengerWorkspacePage(); state.passengerPage = page; if (!passengerSignedIn) els.passengerWorkspaceMenu?.removeAttribute("open"); if (els.passengerPanelHeading) els.passengerPanelHeading.hidden = passengerSignedIn; if (els.passengerWorkspaceHeader) { els.passengerWorkspaceHeader.hidden = !passengerSignedIn; els.passengerWorkspaceHeader.classList.toggle("request-minimal", passengerSignedIn && page === "request"); } if (els.passengerWorkspaceTitle) els.passengerWorkspaceTitle.textContent = passengerWorkspacePageLabel(page); if (passengerSignedIn && page === "trips" && typeof forcePassengerApproachRefreshNow === "function") { forcePassengerApproachRefreshNow("passenger_trips_page_visible"); } if (els.passengerWorkspaceMenu) els.passengerWorkspaceMenu.hidden = !passengerSignedIn; if (els.passengerWorkspaceNav) { els.passengerWorkspaceNav.querySelectorAll("[data-passenger-page]").forEach((button) => { const available = availablePassengerWorkspacePages().includes(button.dataset.passengerPage); button.hidden = !available; const active = button.dataset.passengerPage === page; button.classList.toggle("active", active); button.setAttribute("aria-pressed", String(active)); }); } syncPassengerAgencyTravelMount(passengerSignedIn, page); document.querySelectorAll("[data-passenger-page-section]").forEach((section) => { const sectionPage = section.dataset.passengerPageSection; const publicIntercitySection = sectionPage === "agency"; const genericBusinessAccountForm = agencyWorkspaceActive() && section.id === "businessAccountForm"; section.hidden = genericBusinessAccountForm || sectionPage !== page || (!passengerSignedIn && !publicIntercitySection); }); revealPassengerAgencyTravelPanelForRoute(page); if (!passengerSignedIn) return; const paymentReady = paymentAccountReady("passenger", state.passenger); if (els.passengerRideGate) { els.passengerRideGate.textContent = paymentReady ? "Ready to publish." : "Add a passenger payment method under Payment before publishing."; } } function renderRiderWorkspacePages(riderSignedIn) { const riderPasswordResetActive = typeof passwordResetModeActive === "function" && passwordResetModeActive("rider"); riderSignedIn = Boolean(riderSignedIn && !riderPasswordResetActive); const page = riderWorkspacePage(); state.riderPage = page; if (riderSignedIn && typeof scheduleRiderProfileHydration === "function") { scheduleRiderProfileHydration("rider workspace render"); } if (!riderSignedIn) els.riderWorkspaceMenu?.removeAttribute("open"); if (els.riderWorkspaceHeader) els.riderWorkspaceHeader.hidden = !riderSignedIn; if (els.riderWorkspaceTitle) els.riderWorkspaceTitle.textContent = riderWorkspacePageLabels[page] ?? "Overview"; if (els.riderWorkspaceSummary) els.riderWorkspaceSummary.textContent = riderWorkspacePageSummaries[page] ?? riderWorkspacePageSummaries.overview; if (els.riderWorkspaceMenu) els.riderWorkspaceMenu.hidden = !riderSignedIn; if (els.riderWorkspaceNav) { const availablePages = availableRiderWorkspacePages(); els.riderWorkspaceNav.querySelectorAll("[data-rider-page]").forEach((button) => { button.hidden = !availablePages.includes(button.dataset.riderPage); const active = button.dataset.riderPage === page; button.classList.toggle("active", active); button.setAttribute("aria-pressed", String(active)); }); } document.querySelectorAll("[data-rider-page-section]").forEach((section) => { section.hidden = !riderSignedIn || !riderPageSectionIncludes(section.dataset.riderPageSection, page); }); if (riderSignedIn && page === "destination") renderRiderDestinationFilterControls(); } function ensureAccountChoiceState(type, signedIn) { if (signedIn) return; if (requestedTabFromLocation() !== type) return; const routeMode = requestedAccountModeFromLocation(type); if (routeMode) { state.accountMode[type] = routeMode; accountModeSelectedThisSession[type] = true; return; } if (accountModeSelectedThisSession[type]) return; state.accountMode[type] = accountMode(type); } function renderAccountWorkspaces() { const passengerPasswordResetActive = typeof passwordResetModeActive === "function" && passwordResetModeActive("passenger"); const agencyHintApplied = applyAgencyWorkspaceSessionHint(); if (agencyHintApplied) saveState(); if (agencyWorkspaceRouteActive() && !state.passenger?.id && !agencyWorkspaceSessionPromise) { void ensureAgencyWorkspaceSession().then((passenger) => { if (passenger && typeof renderAll === "function") renderAll(); }); } const passengerSignedIn = Boolean(hasSignedIn("passenger") && state.passenger && !passengerPasswordResetActive); if (passengerPasswordResetActive) state.accountMode.passenger = "signin"; ensureAccountChoiceState("passenger", passengerSignedIn); if (els.passengerSignInOtpPanel) els.passengerSignInOtpPanel.hidden = !phoneOtpSignInEnabled(); renderAccountModeButtons("passenger", passengerSignedIn); if (typeof updatePasswordResetFormMode === "function") updatePasswordResetFormMode("passenger", passengerPasswordResetActive); els.passengerSignInForm.hidden = passengerPasswordResetActive ? false : passengerSignedIn || accountMode("passenger") !== "signin"; els.passengerAccountForm.hidden = passengerPasswordResetActive || passengerSignedIn || accountMode("passenger") !== "create"; if (els.passengerPasswordResetPanel) { els.passengerPasswordResetPanel.hidden = passengerPasswordResetActive ? false : els.passengerPasswordResetPanel.hidden || passengerSignedIn || accountMode("passenger") !== "signin"; } applyRequestedPassengerAccountIntent(); applyPassengerAgencyEntryCopy(); updatePassengerProfileRecoveryFormControls(pendingProfileRecoveryForRole("passenger")); els.passengerSessionCard.hidden = !passengerSignedIn; renderPassengerWorkspacePages(passengerSignedIn); if (passengerSignedIn) { els.passengerSessionTitle.textContent = state.passenger.name || "Passenger signed in"; const passengerIdentity = state.sessions.passenger.email ?? state.passenger.email ?? state.passenger.phone ?? "Secure session"; const passengerLocation = [state.passenger.city, state.passenger.country].filter(Boolean).join(", "); const paymentSummary = paymentAccountReady("passenger", state.passenger) ? "Payment ready" : "Payment method needed"; els.passengerSessionSummary.textContent = `${passengerIdentity}${passengerLocation ? ` - ${passengerLocation}` : ""}. ${paymentSummary}.`; setPassengerProfileAvatarFallback(state.passenger); if (state.passenger.profilePhotoPath) void ensurePassengerProfilePhotoUrl(state.passenger); if (els.passengerProfilePhotoStatus) { els.passengerProfilePhotoStatus.textContent = state.passenger.profilePhotoPath || state.passenger.profilePhotoName ? `Profile picture: ${state.passenger.profilePhotoName || "uploaded"}` : "Profile picture not uploaded."; } els.passengerPaymentStatus.textContent = paymentAccountSummary("passenger", state.passenger); } renderRecentAddressShortcuts(); renderBusinessAccountPanel(); renderIntercityOperatorPanels(); renderPassengerIntercityBookings(); updateBusinessBillingOptions(); renderAccountNotices("passenger"); renderReferralPanel("passenger"); const riderPasswordResetActive = typeof passwordResetModeActive === "function" && passwordResetModeActive("rider"); const riderSignedIn = Boolean(hasSignedIn("rider") && state.rider && !riderPasswordResetActive); if (riderPasswordResetActive) state.accountMode.rider = "signin"; ensureAccountChoiceState("rider", riderSignedIn); if (els.riderSignInOtpPanel) els.riderSignInOtpPanel.hidden = !phoneOtpSignInEnabled(); renderAccountModeButtons("rider", riderSignedIn); if (typeof updatePasswordResetFormMode === "function") updatePasswordResetFormMode("rider", riderPasswordResetActive); const rider = currentRiderRecord(); const riderApproved = riderSignedIn && rider?.status === "approved"; const riderOperational = riderApproved && isSubscriptionActive(rider); els.riderSignInForm.hidden = riderPasswordResetActive ? false : riderSignedIn || accountMode("rider") !== "signin"; if (els.riderPasswordResetPanel) { els.riderPasswordResetPanel.hidden = riderPasswordResetActive ? false : els.riderPasswordResetPanel.hidden || riderSignedIn || accountMode("rider") !== "signin"; } renderRiderWorkspacePages(riderSignedIn); const riderPage = riderWorkspacePage(); renderRiderOverviewGrid(riderSignedIn, rider); const riderNeedsCorrection = riderSignedIn && rider?.status === "needs_correction"; const riderNeedsApplication = riderSignedIn && (rider?.needsApplication || rider?.status === "profile only"); els.riderAccountForm.hidden = (riderNeedsCorrection || riderNeedsApplication) ? riderPasswordResetActive || riderPage !== "profile" : riderPasswordResetActive || riderSignedIn || accountMode("rider") !== "create"; updateRiderCorrectionFormControls(riderNeedsCorrection, rider, riderNeedsApplication); els.riderSessionCard.hidden = !riderSignedIn || !riderPageSectionIncludes(els.riderSessionCard.dataset.riderPageSection, riderPage); els.riderPaymentForm.hidden = !riderApproved || riderPage !== "payment"; els.riderLocationForm.hidden = !riderSignedIn || riderPage !== "initialize"; if (els.riderSupportForm) els.riderSupportForm.hidden = !riderSignedIn || riderPage !== "support"; const selectedRiderRequest = riderSelectedRequestForDetail(); const canShowOfferForm = riderPage === "requests" && Boolean(selectedRiderRequest) && riderCanShowOfferControls(rider, selectedRiderRequest); const canLeaveRiderNegotiation = riderPage === "requests" && Boolean(selectedRiderRequest) && riderCanLeaveSelectedRequest(rider, selectedRiderRequest); const nonNegotiableRiderRequest = Boolean(canShowOfferForm && requestIsNonNegotiableFare(selectedRiderRequest)); const riderProposalLimitReached = Boolean(canShowOfferForm && !nonNegotiableRiderRequest && typeof riderCanSendFareProposal === "function" && !riderCanSendFareProposal(selectedRiderRequest, rider)); els.offerForm.hidden = !(canShowOfferForm || canLeaveRiderNegotiation); els.offerForm.classList.toggle("rider-leave-only", !canShowOfferForm && canLeaveRiderNegotiation); els.offerForm.classList.toggle("rider-non-negotiable-fare", nonNegotiableRiderRequest); if (els.dropRiderNegotiation) { els.dropRiderNegotiation.textContent = canShowOfferForm ? "Decline request" : "Leave request"; els.dropRiderNegotiation.dataset.requestId = selectedRiderRequest?.id ?? ""; } const riderCounterSubmit = els.offerForm.querySelector('button[type="submit"]'); if (riderCounterSubmit) riderCounterSubmit.dataset.riderCounterSubmit = "true"; els.offerForm.querySelectorAll("input, textarea, button").forEach((control) => { control.disabled = control === els.dropRiderNegotiation ? !canLeaveRiderNegotiation : control === els.counterFare || control === riderCounterSubmit ? !canShowOfferForm || nonNegotiableRiderRequest || riderProposalLimitReached : !canShowOfferForm; }); const counterFareField = els.counterFare?.closest("label"); if (counterFareField) { counterFareField.classList.add("counter-fare-field"); counterFareField.hidden = nonNegotiableRiderRequest; } if (riderCounterSubmit) riderCounterSubmit.hidden = nonNegotiableRiderRequest; if (els.acceptFare) { els.acceptFare.textContent = nonNegotiableRiderRequest ? "Accept non-negotiable fare" : "Accept passenger fare"; } const counterNoteField = els.counterNote?.closest("label"); if (counterNoteField) counterNoteField.hidden = true; if (els.counterNote) { els.counterNote.disabled = true; els.counterNote.value = ""; } els.subscriptionText.closest(".subscription-card").hidden = !riderApproved || riderPage !== "checks"; if (riderSignedIn) { els.riderSessionTitle.textContent = state.rider.name || "Rider signed in"; els.riderSessionSummary.textContent = riderWorkspaceStatusMessage(rider); els.riderPaymentStatus.textContent = paymentAccountSummary("rider", rider); if (els.startRiderStripePayoutSetup) { els.startRiderStripePayoutSetup.disabled = !riderApproved || (!directRidePaymentMode() && !hasSupabaseRuntime()); els.startRiderStripePayoutSetup.textContent = directRidePaymentMode() ? "Review wallet mode" : paymentAccountReady("rider", rider) ? "Update Stripe payout account" : "Set up Stripe payout account"; } renderRiderDailyRegionStatus(rider); } renderAccountNotices("rider"); renderReferralPanel("rider"); renderRiderTaxDocuments(); updateAccountPhoneVerificationControls(); renderRiderRouteChangeDecisionModal(); } function updatePassengerProfileRecoveryFormControls(passengerRecovery = pendingProfileRecoveryForRole("passenger")) { if (!els.passengerAccountForm) return; const businessIntent = passengerBusinessIntentFromLocation(); if (els.passengerPassword) { els.passengerPassword.required = !passengerRecovery; els.passengerPassword.placeholder = passengerRecovery ? "Already signed in; no password needed" : "Create a password"; if (passengerRecovery) els.passengerPassword.value = ""; } if (els.passengerSaveButton) { els.passengerSaveButton.textContent = passengerRecovery ? businessIntent ? "Complete agency profile" : "Complete passenger profile" : businessIntent ? "Create agency account" : "Save passenger"; } if (passengerRecovery && els.passengerStatus) { setTranslatedStatus(els.passengerStatus, "supabaseProfileMissing"); } } function setRiderApplicationOnlyFieldHidden(input, hidden) { const field = input?.closest?.("label"); if (field) field.hidden = hidden; if (input) input.disabled = hidden; const row = field?.closest?.(".field-row"); if (row) { row.hidden = ![...row.querySelectorAll("label")].some((label) => !label.hidden); } } function syncRiderApplicationOnlyMode(enabled, rider = currentRiderRecord()) { if (!els.riderAccountForm) return; els.riderAccountForm.classList.toggle("rider-application-only", enabled); els.riderAccountForm.classList.toggle("rider-account-only", false); els.riderAccountForm.querySelectorAll("[data-rider-account-only]").forEach((node) => { node.hidden = enabled; node.querySelectorAll?.("input, button, select, textarea").forEach((control) => { control.disabled = enabled; if (enabled) control.required = false; }); }); els.riderAccountForm.querySelectorAll("[data-rider-application-only]").forEach((node) => { node.hidden = false; node.querySelectorAll?.("input, button, select, textarea").forEach((control) => { control.disabled = false; }); }); if (!enabled) { if (els.riderName) els.riderName.required = true; if (els.riderEmail) els.riderEmail.required = true; if (els.riderPhone) els.riderPhone.required = true; } if (els.riderNationalId) els.riderNationalId.required = false; if (els.riderDob) els.riderDob.required = false; if (els.riderRegistration) els.riderRegistration.required = true; if (els.riderBackgroundConsent) { els.riderBackgroundConsent.required = false; if (!rider?.backgroundCheckConsentAt) els.riderBackgroundConsent.checked = true; } if (els.riderNationalIdDocument) els.riderNationalIdDocument.required = false; [ els.riderName, els.riderEmail, els.riderPhone, els.riderNationalId, els.riderDob ].forEach((input) => { const hasKnownValue = Boolean(String(input?.value || "").trim() || rider?.[input === els.riderDob ? "dateOfBirth" : input === els.riderNationalId ? "nationalId" : input === els.riderPhone ? "phone" : input === els.riderEmail ? "email" : "name"]); setRiderApplicationOnlyFieldHidden(input, enabled && hasKnownValue); }); if (els.riderApplicationModeNotice) { els.riderApplicationModeNotice.hidden = !enabled; } } function updateRiderCorrectionFormControls(riderNeedsCorrection, rider = currentRiderRecord(), riderNeedsApplication = false) { if (!els.riderAccountForm) return; const signedInApplicationEdit = riderNeedsCorrection || riderNeedsApplication; const riderRecovery = pendingProfileRecoveryForRole("rider"); const requestedDocumentKeys = new Set(riderNeedsCorrection ? riderRequestedDocumentKeys(rider) : []); const existingDocuments = riderDocuments(rider); syncRiderApplicationOnlyMode(riderNeedsApplication, rider); const accountOnlySignup = !signedInApplicationEdit && !hasSignedIn("rider") && accountMode("rider") === "create"; if (els.riderPassword) { els.riderPassword.required = !signedInApplicationEdit && !riderRecovery; els.riderPassword.placeholder = riderRecovery ? "Already signed in; no password needed" : riderNeedsApplication ? "Already signed in; no password needed" : riderNeedsCorrection ? "Leave blank to keep current password" : "Create a password"; if (riderRecovery || signedInApplicationEdit) els.riderPassword.value = ""; } [ ["nationalIdentity", els.riderNationalIdDocument], ["driverLicense", els.riderLicenseDocument], ["vehicleRegistration", els.riderRegistrationDocument], ["insurance", els.riderInsuranceDocument], ["vehicleInspection", els.riderInspectionDocument] ].forEach(([documentKey, input]) => { if (input) input.required = requestedDocumentKeys.has(documentKey) && !existingDocuments[documentKey]; }); if (els.riderSubmitButton) { els.riderSubmitButton.textContent = riderNeedsCorrection ? translatedValue("resubmitCorrectedApplication") : riderNeedsApplication ? translatedValue("submitRiderApplication") : accountOnlySignup ? translatedValue("createRiderAccountAndSubmitApplication") : translatedValue("submitReview"); } const intro = els.riderAccountForm.querySelector(".rider-form-intro"); if (intro) { intro.textContent = riderNeedsCorrection ? translatedValue("riderCorrectionIntro") : riderNeedsApplication ? translatedValue("riderApplicationOnlyIntro") : accountOnlySignup ? translatedValue("riderOneStepApplicationIntro") : translatedValue("riderApplicationIntro"); } if (riderNeedsApplication && els.riderStatus) { els.riderStatus.textContent = translatedValue("riderApplicationOnlyStatus"); } else if (riderNeedsCorrection && els.riderStatus) { const requestedLabels = riderRequestedDocumentLabels(rider); const documentNote = requestedLabels.length ? ` Required upload${requestedLabels.length === 1 ? "" : "s"}: ${requestedLabels.join(", ")}.` : ""; els.riderStatus.textContent = `Admin requested corrections before review continues.${documentNote}${rider?.reviewNote ? ` Note: ${rider.reviewNote}` : ""}`; } else if (accountOnlySignup && els.riderStatus) { els.riderStatus.textContent = translatedValue("riderOneStepApplicationStatus"); } if (typeof updateRiderVehicleSpecificFields === "function") { updateRiderVehicleSpecificFields(normalizeRideVehicle(els.riderVehicle?.value ?? rider?.vehicle)); } } function updatePassengerCityOptions() { const country = els.passengerCountry.value; const defaultCity = cityNames(country)[0]; populateSelect(els.passengerCity, cityNames(country), defaultCity); populateSelect(els.pickupCity, cityNames(country), els.passengerCity.value || defaultCity); updatePickupOptions(); updateRidePaymentOptions(country); updateFareGuidance(); } function updatePassengerActiveCityOptions() { const country = els.passengerActiveCountry.value; populateSelect(els.passengerActiveCity, cityNames(country), cityNames(country)[0]); updateRidePaymentOptions(country); } function updatePickupOptions() { const country = els.passengerCountry.value; const pickupCity = selectedRidePickupCity(); if (els.pickupCity && els.pickupCity.value !== pickupCity) els.pickupCity.value = pickupCity; const pickupAreas = areas(country, pickupCity); populateSelect( els.pickupArea, pickupAreas.map((area) => area.name), pickupAreas[0]?.name ); populateSelect( els.destinationArea, pickupAreas.map((area) => area.name), pickupAreas[1]?.name ?? pickupAreas[0]?.name ); selectedPickupPlace = null; selectedDestinationPlace = null; hidePickupSuggestions(); hideDestinationSuggestions(); updateFareGuidance(); schedulePassengerNearbyRiderCountsRefresh(); } function tabFromRouteValue(value) { const normalized = String(value ?? "").toLowerCase().trim(); const tab = workspaceTabs.find((item) => ( normalized === item || normalized.startsWith(`${item}-`) || normalized.startsWith(`${item}/`) || normalized.startsWith(`${item}:`) )); return availableWorkspaceTab(tab); } function routePathTab() { const path = window.location.pathname.toLowerCase(); const segment = path.replace(/\/+$/, "").split("/").pop(); const shellRole = String(document.documentElement.dataset.wakaShell || document.body?.dataset.wakaShell || "").toLowerCase(); const shellTab = tabFromRouteValue(shellRole); if (shellTab) return shellTab; if (publicAgencyPageRequested()) return null; if (segment === "agency" || segment === "agency.html" || path.startsWith("/agency/")) { return availableWorkspaceTab("passenger"); } if (segment === "passenger" || segment === "passenger.html" || path.startsWith("/passenger/")) { return availableWorkspaceTab("passenger"); } if (segment === "rider" || segment === "rider.html" || path.startsWith("/rider/")) { return availableWorkspaceTab("rider"); } if (segment === "admin" || segment === "admin.html" || path.startsWith("/admin/")) { return availableWorkspaceTab("admin"); } return null; } function routePathSegments() { return window.location.pathname .toLowerCase() .replace(/\/+$/, "") .split("/") .filter(Boolean); } function normalizedAccountRouteMode(value) { const mode = String(value ?? "").toLowerCase().trim(); if (["create", "signup", "register", "join", "apply"].includes(mode)) return "create"; if (["signin", "sign-in", "login", "log-in"].includes(mode)) return "signin"; return ""; } function requestedAccountModeFromLocation(type) { if (!["passenger", "rider"].includes(type)) return ""; const pathSegments = routePathSegments(); if (pathSegments[0] === type || (type === "passenger" && pathSegments[0] === "agency")) { const pathMode = normalizedAccountRouteMode(pathSegments[1]); if (pathMode) return pathMode; } const params = new URLSearchParams(window.location.search); const queryMode = normalizedAccountRouteMode( params.get("account") ?? params.get("accountMode") ?? params.get("mode") ?? params.get("action") ); if (queryMode && requestedTabFromLocation() === type) return queryMode; const hashValue = window.location.hash.replace(/^#\/?/, "").toLowerCase(); if (hashValue.startsWith(`${type}/`) || hashValue.startsWith(`${type}:`)) { const hashMode = normalizedAccountRouteMode(hashValue.split(/[/:?&=]/)[1]); if (hashMode) return hashMode; } return ""; } function requestedTabFromLocation() { const pathTab = routePathTab(); if (pathTab) return pathTab; const hashTab = tabFromRouteValue(window.location.hash.replace(/^#\/?/, "").split(/[?&=]/)[0]); if (hashTab) return hashTab; const params = new URLSearchParams(window.location.search); const explicitTab = tabFromRouteValue(params.get("tab") ?? params.get("role") ?? params.get("workspace")); if (explicitTab) return explicitTab; return workspaceTabs.find((tab) => params.has(tab) && availableWorkspaceTab(tab)) ?? null; } function updateWorkspaceHash(tab) { if (!workspaceTabs.includes(tab)) return; const currentHashTab = tabFromRouteValue(window.location.hash.replace(/^#\/?/, "").split(/[?&=]/)[0]); if (window.location.hash === `#${tab}` || currentHashTab === tab) return; if (routePathTab() === tab) return; window.history.replaceState(null, "", `${window.location.pathname}${window.location.search}#${tab}`); } function accountModeRoute(type, mode) { if (!["passenger", "rider"].includes(type)) return ""; if (type === "passenger" && passengerBusinessIntentFromLocation()) { return mode === "create" ? "/agency/create" : "/agency"; } return mode === "create" ? `/${type}/create` : `/${type}`; } function updateAccountModeRoute(type, mode) { const path = accountModeRoute(type, mode); if (!path || window.location.pathname === path) return; window.history.pushState(null, "", path); } function applyRouteTab() { const tab = requestedTabFromLocation(); if (tab && tab !== state.activeTab) switchTab(tab, { updateUrl: false }); const accountRouteRole = tab ?? state.activeTab; const accountRouteMode = requestedAccountModeFromLocation(accountRouteRole); if (accountRouteMode && ["passenger", "rider"].includes(accountRouteRole) && !roleHasSignedInAccount(accountRouteRole)) { state.accountMode[accountRouteRole] = accountRouteMode; accountModeSelectedThisSession[accountRouteRole] = true; saveState(); } if ((tab ?? state.activeTab) === "rider") { const requestedPage = requestedRiderWorkspacePageFromLocation(); const requestedRequestId = requestedRideRequestIdFromLocation(); if (requestedPage && availableRiderWorkspacePages().includes(requestedPage)) { state.riderPage = requestedPage; saveState(); } if (requestedRequestId) { state.riderPage = "requests"; state.selectedRequestId = requestedRequestId; saveState(); } } if ((tab ?? state.activeTab) === "passenger") { const requestedPage = requestedPassengerWorkspacePageFromLocation(); const requestedRequestId = requestedRideRequestIdFromLocation(); const passengerSignedIn = hasSignedIn("passenger"); const requestedTripDeepLink = requestedPage === "trips" && Boolean(requestedRequestId); const shouldHonorRequestedPage = !passengerSignedIn || passengerWorkspacePageSelectedInSession || requestedTripDeepLink; if (requestedPage && availablePassengerWorkspacePages().includes(requestedPage) && shouldHonorRequestedPage) { state.passengerPage = requestedPage; saveState(); } else if (passengerSignedIn && !availablePassengerWorkspacePages().includes(state.passengerPage)) { state.passengerPage = "request"; saveState(); } if (requestedRequestId) { state.passengerPage = "trips"; state.selectedRequestId = requestedRequestId; saveState(); } } if ((tab ?? state.activeTab) === "admin" && typeof applyRequestedAdminWorkspacePageFromLocation === "function") { const routeApplied = applyRequestedAdminWorkspacePageFromLocation(); if (routeApplied && typeof ensureAdminWorkspaceData === "function") void ensureAdminWorkspaceData(state.adminPage); } renderEntryExperience(); renderAll(); } function roleHasSignedInAccount(role) { if (!availableWorkspaceTab(role)) return false; if (role === "passenger") return hasSignedIn("passenger"); if (role === "rider") return hasSignedIn("rider"); if (role === "admin") return adminShellAvailable() && Boolean(state.adminSession); return false; } function preferredSignedInTab() { if (roleHasSignedInAccount(state.activeTab)) return state.activeTab; if (roleHasSignedInAccount("passenger")) return "passenger"; if (roleHasSignedInAccount("rider")) return "rider"; if (roleHasSignedInAccount("admin")) return "admin"; return null; } function publicHomePathIsActive() { if (runtimeRole !== "public") return false; const pathname = window.location.pathname.replace(/\/+$/, "") || "/"; return pathname === "/" || pathname === "/index.html"; } function dedicatedPublicPageIsActive() { if (runtimeRole !== "public") return false; const pathname = window.location.pathname.toLowerCase().replace(/\/+$/, "") || "/"; return publicAgencyPageRequested() || pathname === "/travel" || pathname === "/travel.html" || pathname === "/travel/index.html" || pathname === "/agencies" || pathname === "/agencies.html" || pathname === "/agencies/index.html"; } function publicPageModeFromLocation() { if (runtimeRole !== "public") return ""; const pathname = window.location.pathname.toLowerCase().replace(/\/+$/, "") || "/"; if (pathname === "/agencies" || pathname === "/agencies.html" || pathname === "/agencies/index.html") return "agencies"; if (publicAgencyPageRequested() || pathname === "/travel" || pathname === "/travel.html" || pathname === "/travel/index.html") return "travel"; return ""; } function shouldShowRoleEntry() { if (runtimeRole !== "public" || !els.roleEntry) return false; if (requestedTabFromLocation()) return false; if (dedicatedPublicPageIsActive()) return false; if (publicHomePathIsActive()) return true; if (preferredSignedInTab()) return Boolean(state.showRoleEntry); return true; } function renderEntryExperience() { const roleEntryVisible = shouldShowRoleEntry(); const publicOnlyPage = dedicatedPublicPageIsActive(); const publicPageMode = publicPageModeFromLocation(); if (publicPageMode) setPublicPageMode(publicPageMode); if (els.roleEntry) els.roleEntry.hidden = publicOnlyPage ? false : !roleEntryVisible; if (els.workspace) els.workspace.hidden = publicOnlyPage || roleEntryVisible; if (els.roleTabs) els.roleTabs.hidden = true; document.body.classList.toggle("public-only-page", publicOnlyPage); } function setAccountMode(type, mode, options = {}) { if (!["passenger", "rider"].includes(type)) return; const { updateUrl = true } = options; accountModeSelectedThisSession[type] = true; state.accountMode[type] = mode === "create" ? "create" : "signin"; state.activeTab = type; state.showRoleEntry = false; if (updateUrl) updateAccountModeRoute(type, state.accountMode[type]); renderEntryExperience(); saveState(); renderAll(); } function accountMode(type) { const mode = state.accountMode?.[type]; return mode === "signin" || mode === "create" ? mode : "signin"; } function resetAccountChoice(type) { if (!["passenger", "rider"].includes(type)) return; const routeMode = requestedAccountModeFromLocation(type); accountModeSelectedThisSession[type] = Boolean(routeMode); state.accountMode[type] = routeMode || "signin"; } function renderAccountModeButtons(type, signedIn) { const stage = type === "passenger" ? els.passengerAccountStage : els.riderAccountStage; if (stage) { stage.hidden = true; stage.dataset.mode = accountMode(type); } document.querySelectorAll(`[data-account-type="${type}"][data-account-mode]`).forEach((button) => { const active = button.dataset.accountMode === accountMode(type); button.classList.toggle("active", active); button.setAttribute("aria-pressed", String(active)); }); } function switchTab(tab, options = {}) { tab = availableWorkspaceTab(tab); if (!tab) return; const { updateUrl = true, preserveEntry = false, resetAccountMode = false } = options; state.activeTab = tab; if (resetAccountMode) resetAccountChoice(tab); if (!preserveEntry) state.showRoleEntry = false; renderEntryExperience(); document.querySelectorAll(".tab-button").forEach((button) => { button.classList.toggle("active", button.dataset.tab === tab); }); document.querySelectorAll(".tab-panel").forEach((panel) => { panel.classList.toggle("active", panel.id === `${tab}-panel`); }); if (updateUrl) updateWorkspaceHash(tab); saveState(); renderAll(); } function riderSelectedRequestForDetail() { if (activeRole() !== "rider" || riderWorkspacePage() !== "requests") return null; const request = selectedRequest(); if (request && roleCanSeeRequest(request)) return request; if (!state.rider) return null; return state.requests.find((item) => requestIsActiveForCurrentRider(item)) ?? null; } function riderRequestDetailOpen() { return Boolean(riderSelectedRequestForDetail()); } function selectedWorkspaceRequest() { if (activeRole() === "rider") return riderSelectedRequestForDetail(); const selected = selectedRequest(); if (selected) return selected; if (activeRole() !== "passenger") return null; const selectedRaw = stateLookupIndexes().requestMap.get(state.selectedRequestId) ?? null; if (selectedRaw && requestBelongsToPassenger(selectedRaw) && selectedRaw.status === "completed" && (canRateRequest(selectedRaw) || existingRatingForRequest(selectedRaw))) { return selectedRaw; } const pendingRide = passengerPendingRide(); if (!pendingRide) return null; state.selectedRequestId = pendingRide.id; rememberWorkspaceUiState("passenger", { page: "trips", selectedRequestId: pendingRide.id }); saveState(); return pendingRide; } function riderActivePickupDisplayText(request) { const directAddress = String(request?.pickupFormattedAddress ?? request?.pickupDescription ?? "").trim(); const isGenericCurrent = directAddress && (pickupUsesCurrentLocationText(directAddress) || pickupUsesGpsFallbackText(directAddress)); if (directAddress && !isGenericCurrent && !riderAddressLooksAreaOnly(directAddress, request, "pickup")) return directAddress; const displayText = requestPickupDisplayText(request, ""); if (displayText && !pickupUsesCurrentLocationText(displayText) && !pickupUsesGpsFallbackText(displayText) && !riderAddressLooksAreaOnly(displayText, request, "pickup")) return displayText; const areaText = compactLocationQuery([request?.pickupArea, request?.city, request?.country]); if (areaText) return requestPickupGps(request) ? `Shared GPS pickup near ${areaText}` : areaText; return requestPickupGps(request) ? "Verified GPS pickup" : "Pickup"; } function riderActiveDropoffDisplayText(request) { const detail = String(request?.destinationFormattedAddress || request?.destination || "").trim(); if (detail && !riderAddressLooksAreaOnly(detail, request, "destination")) return detail; const areaText = compactLocationQuery([request?.destinationArea, request?.city, request?.country]); if (areaText) return requestDestinationGps(request) ? `Destination GPS pin near ${areaText}` : areaText; return requestDestinationGps(request) ? "Destination GPS pin" : "Drop-off"; } function riderAddressLooksAreaOnly(value, request, kind) { const text = String(value ?? "").trim().toLowerCase().replace(/\s+/g, " "); if (!text) return false; const area = kind === "pickup" ? request?.pickupArea : request?.destinationArea; return [ area, request?.city, request?.country, compactLocationQuery([request?.city, request?.country]), compactLocationQuery([area, request?.city]), compactLocationQuery([area, request?.city, request?.country]) ] .map((part) => String(part ?? "").trim().toLowerCase().replace(/\s+/g, " ")) .filter(Boolean) .includes(text); } function riderActiveRouteDisplayText(request) { return `${riderActivePickupDisplayText(request)} to ${riderActiveDropoffDisplayText(request)}`; } function renderRiderActiveTripAddressDetails(node, request) { if (!node || !request) return; const chipRow = node.querySelector(".chip-row"); if (!chipRow || node.querySelector(".active-trip-address-details")) return; const displayRequest = activeRole() === "rider" ? riderVisibleRouteRequest(request) : request; const rows = [ ["Pickup", riderActivePickupDisplayText(request)], ...normalizeRideStops(displayRequest?.rideStops).map((stop, index) => [`Stop ${index + 1}`, stop]), ["Drop-off", riderActiveDropoffDisplayText(displayRequest)] ].filter(([, value]) => String(value ?? "").trim()); if (!rows.length) return; const details = document.createElement("div"); details.className = "active-trip-address-details"; details.innerHTML = rows .map(([label, value]) => `${escapeHtml(label)}${escapeHtml(value)}`) .join(""); chipRow.before(details); } function riderMarketplaceRouteDistanceChip(request) { const pickup = proximityChip(request) ?? "Pickup ETA: estimating"; const destination = destinationDriveChip(request) ?? "Destination drive: estimating"; return `${pickup} / ${destination}`; } function resetRideDockPanels({ restore = true } = {}) { passengerRideDockRequestId = null; passengerRideDockOpenPanel = null; riderRideDockRequestId = null; riderRideDockOpenPanel = null; if (restore && typeof restoreRideToolElementsToChatPanel === "function") restoreRideToolElementsToChatPanel(); } function returnRiderToMarketplace({ replace = true, refresh = false } = {}) { state.activeTab = "rider"; state.showRoleEntry = false; state.riderPage = "requests"; state.selectedRequestId = null; resetRideDockPanels(); if (typeof updateRiderWorkspaceRoute === "function") { updateRiderWorkspaceRoute("requests", { replace, requestId: "" }); } saveState(); renderAll(); window.setTimeout(() => { document.querySelector("#requestsBoard, #marketPanel") ?.scrollIntoView({ behavior: "smooth", block: "start" }); }, 0); if (refresh) void refreshMarketplace({ silent: true }); } function renderRiderRequestDetailPanel(request = riderSelectedRequestForDetail()) { if (!els.riderRequestDetailPanel) return; const isVisible = activeRole() === "rider" && riderWorkspacePage() === "requests" && Boolean(request); const activeTrip = isVisible && requestIsActiveForCurrentRider(request); els.riderRequestDetailPanel.hidden = !isVisible || activeTrip; if (!isVisible) return; if (activeTrip) { if (els.riderRequestDetailStatus) { els.riderRequestDetailStatus.textContent = ""; els.riderRequestDetailStatus.hidden = true; } return; } if (els.riderRequestDetailTitle) { const titleBlock = els.riderRequestDetailTitle.parentElement; if (titleBlock) titleBlock.hidden = false; els.riderRequestDetailTitle.textContent = "Respond to selected request"; } if (els.riderRequestDetailStatus) { els.riderRequestDetailStatus.textContent = ""; els.riderRequestDetailStatus.hidden = true; } } function destinationFilterOptions(values, anyLabel) { return [ { value: "", label: anyLabel }, ...values.map((value) => ({ value, label: value })) ]; } let riderDestinationFilterDraft = null; function normalizedRiderDestinationFilterDraft(value = {}) { const filter = normalizeRiderMarketplaceDestinationFilter({ enabled: true, consent: value.consent === true, country: value.country, city: value.city, area: value.area, query: value.query, appliedAt: new Date().toISOString() }); return { consent: value.consent === true, country: filter.country, city: filter.city, area: filter.area, query: String(value.query ?? "").trim() }; } function rememberRiderDestinationFilterDraft(draft = riderDestinationFilterDraftValues()) { riderDestinationFilterDraft = normalizedRiderDestinationFilterDraft(draft); return riderDestinationFilterDraft; } function rememberRiderDestinationFilterControlDraft() { return rememberRiderDestinationFilterDraft(riderDestinationFilterDraftValues({ preferStoredDraft: false })); } function riderDestinationFilterDraftValues({ preferStoredDraft = true } = {}) { const filter = riderMarketplaceDestinationFilter(); return { consent: els.riderDestinationFilterConsent?.checked ?? riderDestinationFilterDraft?.consent ?? filter.consent, country: els.riderDestinationFilterCountry?.value ?? (preferStoredDraft ? riderDestinationFilterDraft?.country : null) ?? filter.country, city: els.riderDestinationFilterCity?.value ?? (preferStoredDraft ? riderDestinationFilterDraft?.city : null) ?? filter.city, area: els.riderDestinationFilterArea?.value ?? (preferStoredDraft ? riderDestinationFilterDraft?.area : null) ?? filter.area, query: String(els.riderDestinationFilterText?.value ?? (preferStoredDraft ? riderDestinationFilterDraft?.query : null) ?? filter.query ?? "").trim() }; } function destinationFilterCitySourceCountry(country) { return country || selectedRiderCountry(); } function destinationFilterAreaSourceCity(country, city) { const sourceCountry = destinationFilterCitySourceCountry(country); if (city && cityNames(sourceCountry).includes(city)) return city; const riderCity = selectedRiderCity(); if (sourceCountry === selectedRiderCountry() && cityNames(sourceCountry).includes(riderCity)) return riderCity; return defaultLaunchCity(sourceCountry); } function populateRiderDestinationFilterOptions(draft = riderDestinationFilterDraftValues()) { if (!els.riderDestinationFilterCountry || !els.riderDestinationFilterCity || !els.riderDestinationFilterArea) return; const countriesList = enabledLaunchCountries(); const country = countriesList.includes(draft.country) ? draft.country : ""; const cityCountry = destinationFilterCitySourceCountry(country); const cityValues = cityNames(cityCountry); const city = cityValues.includes(draft.city) ? draft.city : ""; const areaCity = destinationFilterAreaSourceCity(country, city); const areaValues = areas(cityCountry, areaCity).map((area) => area.name); const area = areaValues.includes(draft.area) ? draft.area : ""; rememberRiderDestinationFilterDraft({ ...draft, country, city, area }); populateSelectOptions(els.riderDestinationFilterCountry, destinationFilterOptions(countriesList, "Any country"), country); populateSelectOptions(els.riderDestinationFilterCity, destinationFilterOptions(cityValues, "Any state / city"), city); populateSelectOptions(els.riderDestinationFilterArea, destinationFilterOptions(areaValues, "Any town / area"), area); } function renderRiderDestinationFilterControls() { if (!els.riderDestinationFilterPanel) return; const filter = riderMarketplaceDestinationFilter(); const draft = riderDestinationFilterDraft ?? normalizedRiderDestinationFilterDraft(filter); populateRiderDestinationFilterOptions(draft); if (els.riderDestinationFilterConsent) els.riderDestinationFilterConsent.checked = draft.consent; if (els.riderDestinationFilterText) els.riderDestinationFilterText.value = draft.query; if (els.riderDestinationFilterStatus) { els.riderDestinationFilterStatus.textContent = riderMarketplaceDestinationFilterSummary(filter); } } function refreshRiderDestinationFilterOptions() { populateRiderDestinationFilterOptions(rememberRiderDestinationFilterDraft()); } function applyRiderDestinationFilter() { const draft = riderDestinationFilterDraftValues(); if (!draft.consent) { if (els.riderDestinationFilterStatus) els.riderDestinationFilterStatus.textContent = "Check the consent box before applying a destination preference."; return; } let country = draft.country; let city = draft.city; let area = draft.area; const query = draft.query; if (!country && (city || area)) country = selectedRiderCountry(); if (!city && area) city = destinationFilterAreaSourceCity(country, city); const nextFilter = normalizeRiderMarketplaceDestinationFilter({ enabled: true, consent: true, country, city, area, query, appliedAt: new Date().toISOString() }); if (!riderMarketplaceDestinationFilterIsActive(nextFilter)) { if (els.riderDestinationFilterStatus) els.riderDestinationFilterStatus.textContent = "Choose a country, state, town, or destination text before applying."; return; } state.riderMarketplaceDestinationFilter = nextFilter; riderDestinationFilterDraft = normalizedRiderDestinationFilterDraft(nextFilter); returnRiderToMarketplace({ replace: true, refresh: true }); } function clearRiderDestinationFilter() { state.riderMarketplaceDestinationFilter = normalizeRiderMarketplaceDestinationFilter(); riderDestinationFilterDraft = null; state.selectedRequestId = null; saveState(); renderAll(); if (els.riderDestinationFilterStatus) { els.riderDestinationFilterStatus.textContent = "Cleared. Ride requests will show all nearby rides again."; } void refreshMarketplace({ silent: true }); } function showRoleEntryScreen() { state.showRoleEntry = true; state.activeTab = defaultRuntimeTab(); if (window.location.hash) window.history.replaceState({}, "", window.location.pathname || "./"); saveState(); renderAll(); } function isPassengerNegotiationRequest(request) { return Boolean(activeRole() === "passenger" && request && requestBelongsToPassenger(request) && requestIsNegotiableFare(request) && request.status === "open" && !selectedRiderIdForRequest(request)); } function isPassengerNonNegotiableWaitingRequest(request) { return Boolean(activeRole() === "passenger" && request && requestBelongsToPassenger(request) && requestIsNonNegotiableFare(request) && request.status === "open" && !selectedRiderIdForRequest(request)); } function isPassengerWaitingForRiderRequest(request) { return Boolean(isPassengerNegotiationRequest(request) || isPassengerNonNegotiableWaitingRequest(request)); } function passengerPendingRatingRequest() { if (activeRole() !== "passenger" || !state.passenger) return null; return state.requests .filter((request) => requestBelongsToPassenger(request) && canRateRequest(request)) .sort((a, b) => new Date(b.completedAt ?? b.updatedAt ?? b.createdAt ?? 0).getTime() - new Date(a.completedAt ?? a.updatedAt ?? a.createdAt ?? 0).getTime())[0] ?? null; } function openPassengerRideRating(request) { if (!request?.id || !requestBelongsToPassenger(request)) return; state.selectedRequestId = request.id; passengerRideDockRequestId = request.id; passengerRideDockOpenPanel = "rating"; rememberWorkspaceUiState("passenger", { page: "trips", selectedRequestId: request.id }); saveState(); renderAll(); window.setTimeout(() => { document.querySelector("#chatPanel, #rideActionPanel") ?.scrollIntoView({ behavior: "smooth", block: "start" }); }, 0); } function passengerRatingPromptKey(request) { return `passenger-rating-${request?.id ?? "ride"}-${selectedRiderIdForRequest(request) ?? "rider"}`; } function rememberPassengerRatingPromptDecision(request) { const key = passengerRatingPromptKey(request); state.notificationPopupIds = [...new Set([...(state.notificationPopupIds ?? []), key])].slice(-140); saveState(); } function passengerRatingPromptVisible(request) { const key = passengerRatingPromptKey(request); return [...document.querySelectorAll("[data-passenger-rating-prompt-key]")] .some((node) => node.dataset.passengerRatingPromptKey === key); } function showPassengerRatingPrompt(request) { if (!request?.id || !canRateRequest(request) || existingRatingForRequest(request)) return; const key = passengerRatingPromptKey(request); if ((state.notificationPopupIds ?? []).includes(key) || passengerRatingPromptVisible(request)) return; const riderName = selectedRiderFirstNameForRequest(request); const route = `${requestPickupDisplayText(request)} to ${requestDestinationDisplayText(request)}`; const node = document.createElement("div"); node.className = "notice-popup notice-popup-actionable"; node.dataset.passengerRatingPromptKey = key; node.innerHTML = ` Rate ${escapeHtml(riderName)}

Completed ride: ${escapeHtml(route)}

`; node.querySelector(".passenger-rating-now")?.addEventListener("click", () => { rememberPassengerRatingPromptDecision(request); node.remove(); openPassengerRideRating(request); }); node.querySelector(".passenger-rating-later")?.addEventListener("click", () => { rememberPassengerRatingPromptDecision(request); node.remove(); }); document.body.append(node); } function renderPassengerPendingRatingCard(request) { const node = document.createElement("article"); node.className = "market-card passenger-rating-prompt-card"; const riderName = selectedRiderFirstNameForRequest(request); node.innerHTML = `
Pending rider rating Rate ${escapeHtml(riderName)} ${escapeHtml(requestPickupDisplayText(request))} to ${escapeHtml(requestDestinationDisplayText(request))}

Share feedback for ${escapeHtml(riderName)} on this completed ride, or come back later from this trip.

`; node.querySelector("button")?.addEventListener("click", () => openPassengerRideRating(request)); return node; } function passengerNegotiationModeActive() { const request = selectedWorkspaceRequest(); return Boolean(isPassengerNegotiationRequest(request) && roleCanSeeRequest(request)); } function selectedPassengerNegotiationOffer(request, offers = visibleOffersForRole(request)) { if (!isPassengerNegotiationRequest(request) || !state.passengerSelectedOfferId) return null; return offers.find((offer) => offer.id === state.passengerSelectedOfferId) ?? null; } function passengerOfferResponseModeActive() { const request = selectedRequest(); return Boolean(passengerNegotiationModeActive() && selectedPassengerNegotiationOffer(request)); } function scrollPassengerOffersIntoView() { window.setTimeout(() => { document.querySelector("#offersBoard")?.scrollIntoView({ behavior: "smooth", block: "start" }); }, 0); } function openPassengerOfferResponse(offer) { if (!offer?.id) return; state.passengerSelectedOfferId = offer.id; saveState(); renderAll(); scrollPassengerOffersIntoView(); } function returnPassengerToOfferList() { if (!state.passengerSelectedOfferId) return; state.passengerSelectedOfferId = null; saveState(); renderAll(); scrollPassengerOffersIntoView(); } function renderRoleWorkspace() { const role = activeRole(); const rider = currentRiderRecord(); const riderOperational = role !== "rider" || riderCanSeeRequests(rider); els.marketPanel.dataset.role = role; els.boardGrid.classList.remove("role-passenger", "role-rider", "role-admin", "rider-marketplace-mode", "rider-detail-mode", "passenger-negotiation-mode", "passenger-offer-response-mode", "passenger-active-ride-mode", "passenger-non-negotiable-mode"); els.boardGrid.classList.add(`role-${role}`); const passengerSignedIn = roleHasSignedInAccount("passenger"); const riderSignedIn = roleHasSignedInAccount("rider"); const adminSignedIn = roleHasSignedInAccount("admin"); const signedInForActiveRole = role === "passenger" ? passengerSignedIn : role === "rider" ? riderSignedIn : adminSignedIn; const activeRolePasswordReset = typeof passwordResetModeActive === "function" && passwordResetModeActive(role); const passengerTripsPage = passengerWorkspacePage() === "trips"; const riderRequestsPage = riderWorkspacePage() === "requests"; if (els.seedDemo) els.seedDemo.hidden = !demoToolsAllowed(); if (els.clearDemo) els.clearDemo.hidden = !demoToolsAllowed(); els.marketPanel.hidden = activeRolePasswordReset || (role === "passenger" && (!passengerSignedIn || !passengerTripsPage)) || (role === "rider" && (!riderSignedIn || !riderRequestsPage)) || (role === "admin" && !adminSignedIn); const roleAuthEntry = (role === "passenger" || role === "rider") && (!signedInForActiveRole || activeRolePasswordReset); const signedInSinglePanel = els.marketPanel.hidden && signedInForActiveRole && !activeRolePasswordReset; els.workspace?.classList.toggle("account-only", roleAuthEntry); els.workspace?.classList.toggle("account-entry", roleAuthEntry); els.workspace?.classList.toggle("single-panel", signedInSinglePanel); els.workspace?.classList.toggle("password-reset-entry", activeRolePasswordReset); if (els.marketPanel.hidden) { if (els.riderRequestDetailPanel) els.riderRequestDetailPanel.hidden = true; if (els.openRiderDestinationFilter) els.openRiderDestinationFilter.hidden = true; if (els.riderAppliedDestinationSummary) els.riderAppliedDestinationSummary.hidden = true; return; } document.querySelectorAll(".role-market-section").forEach((section) => { const allowedRoles = (section.dataset.roles ?? "").split(/\s+/); section.hidden = !allowedRoles.includes(role); }); const riderDetailRequest = role === "rider" ? riderSelectedRequestForDetail() : null; const riderDetailOpen = Boolean(riderDetailRequest); const passengerWorkspaceRequest = role === "passenger" ? selectedWorkspaceRequest() : null; const passengerNegotiationMode = role === "passenger" && isPassengerNegotiationRequest(passengerWorkspaceRequest); const passengerOfferResponseMode = passengerNegotiationMode && passengerOfferResponseModeActive(); const passengerNonNegotiableMode = role === "passenger" && isPassengerNonNegotiableWaitingRequest(passengerWorkspaceRequest); const passengerWaitingDockMode = role === "passenger" && passengerRideDockMode(passengerWorkspaceRequest); els.boardGrid.classList.toggle("rider-marketplace-mode", role === "rider" && !riderDetailOpen); els.boardGrid.classList.toggle("rider-detail-mode", role === "rider" && riderDetailOpen); els.boardGrid.classList.toggle("passenger-negotiation-mode", passengerNegotiationMode); els.boardGrid.classList.toggle("passenger-offer-response-mode", passengerOfferResponseMode); els.boardGrid.classList.toggle("passenger-non-negotiable-mode", passengerNonNegotiableMode); renderRiderRequestDetailPanel(riderDetailRequest); const passengerTrackableRide = role === "passenger" && state.requests.some((request) => requestBelongsToPassenger(request) && selectedRiderIdForRequest(request) && ["matched", "arrived", "in_progress"].includes(request.status)); els.boardGrid.classList.toggle("passenger-active-ride-mode", passengerTrackableRide); const riderActiveTrip = role === "rider" && state.requests.some((request) => requestIsActiveForCurrentRider(request)); els.cityMap.hidden = role === "admin" || role === "rider" || passengerTrackableRide || riderActiveTrip; els.marketFilters.hidden = role === "admin" || role === "rider" || (role === "rider" && !riderOperational); els.refreshMarket.hidden = role === "passenger" || passengerTrackableRide || riderActiveTrip || !canRefreshMarketplace(); if (els.openRiderDestinationFilter) { els.openRiderDestinationFilter.hidden = !(role === "rider" && riderRequestsPage && !riderDetailOpen && !riderActiveTrip); els.openRiderDestinationFilter.textContent = riderMarketplaceDestinationFilterIsActive() ? "Edit destination" : "Destination"; } if (els.riderAppliedDestinationSummary) { const showAppliedDestinationSummary = role === "rider" && riderRequestsPage && !riderDetailOpen && riderMarketplaceDestinationFilterIsActive(); els.riderAppliedDestinationSummary.hidden = !showAppliedDestinationSummary; els.riderAppliedDestinationSummary.textContent = showAppliedDestinationSummary ? riderMarketplaceDestinationFilterSummary() : ""; } els.refreshMarket.disabled = marketRefreshInFlight; els.refreshMarket.textContent = lastMarketRefreshAt ? `Refresh market (${lastMarketRefreshAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })})` : "Refresh market"; if (role === "passenger") { if (els.chatPanel) els.chatPanel.hidden = passengerNegotiationMode && !passengerWaitingDockMode; els.requestBoardTitle.textContent = passengerNegotiationMode ? passengerUiText("yourRequest", "Your request") : passengerUiText("myRideRequests", "My ride requests"); els.offerBoardTitle.textContent = passengerOfferResponseMode ? passengerUiText("respondToRiderOffer", "Respond to rider offer") : passengerNegotiationMode ? passengerUiText("chooseRiderOffer", "Choose rider offer") : passengerUiText("offersForMyRequest", "Offers for my request"); if (els.offersBoard && passengerNonNegotiableMode) els.offersBoard.hidden = true; return; } if (role === "rider") { const vehicleName = passengerUiText("car", "car"); const riderActiveDetailTrip = Boolean(riderDetailRequest && requestIsActiveForCurrentRider(riderDetailRequest)); els.requestBoardTitle.textContent = riderActiveDetailTrip ? passengerUiText("activeRide", "Active ride") : passengerUiText("incomingVehicleRequests", "Incoming {vehicle} requests", { vehicle: vehicleName }); els.offerBoardTitle.textContent = passengerUiText("myVehicleOffers", "My {vehicle} offers", { vehicle: vehicleName }); if (els.requestsBoard) els.requestsBoard.hidden = riderDetailOpen && !riderActiveDetailTrip; if (els.offersBoard) els.offersBoard.hidden = !riderDetailOpen || riderActiveDetailTrip; if (els.chatPanel) els.chatPanel.hidden = !riderActiveDetailTrip; els.marketLocation.textContent = riderCurrentFreshGps(rider) ? passengerUiText("liveLocationActive", "Live location active") : passengerUiText("riderWorkspace", "Rider workspace"); if (!riderOperational) { els.selectedSummary.textContent = riderWorkspaceStatusMessage(rider); } else if (riderMarketplaceDestinationFilterIsActive()) { els.selectedSummary.textContent = riderMarketplaceDestinationFilterSummary(); } else { els.selectedSummary.textContent = riderServiceAreaSummary(rider); } return; } els.marketLocation.textContent = passengerUiText("adminWorkspace", "Admin workspace"); els.selectedSummary.textContent = state.adminSession ? passengerUiText("adminMarketplaceSummary", "View passengers, riders, approvals, subscriptions, and marketplace activity") : passengerUiText("adminVisibilityRequired", "Admin sign-in required for full passenger and rider visibility"); } function renderMap() { document.querySelectorAll(".map-pin").forEach((pin) => pin.remove()); if (activeRole() === "admin") return; const { country, city } = activeMarketLocation(); els.marketLocation.textContent = `${city}, ${country}`; visibleRequestsForRole().forEach((item) => { const point = findArea(item.country, item.city, item.pickupArea); placePin(point, item.id === state.selectedRequestId ? "S" : "R", item.id === state.selectedRequestId ? "pin-selected" : "pin-request"); }); state.riders .filter((rider) => rider.country === country && rider.city === city && rider.status === "approved") .filter((rider) => activeRole() === "passenger" || rider.id === state.rider?.id) .filter((rider) => state.filter === "all" || rider.vehicle === state.filter) .forEach((rider) => { placePin(findArea(rider.country, rider.city, rider.area), "C", "pin-rider"); }); } function placePin(point, label, className) { if (!point) return; const pin = document.createElement("div"); pin.className = `map-pin ${className}`; pin.style.left = `${point.x}%`; pin.style.top = `${point.y}%`; pin.title = point.name; pin.innerHTML = `${label}`; els.cityMap.append(pin); } function renderRiderBusyDayControls(container) { if (activeRole() !== "rider" || !container || riderWorkspacePage() !== "requests") return; if (state.requests.some((request) => requestIsActiveForCurrentRider(request))) return; const controls = document.createElement("article"); controls.className = "notice-item rider-workload-controls"; controls.innerHTML = `
Marketplace workload

${riderFocusModeActive() ? "Focus mode is holding new ride updates in a queue while you review the current request." : "Normal mode opens urgent ride updates when you are not already reviewing another request."}

`; controls.querySelectorAll("[data-rider-workload-mode]").forEach((button) => { button.addEventListener("click", () => setRiderWorkloadMode(button.dataset.riderWorkloadMode)); }); controls.querySelector("[data-rider-destination-filter]")?.addEventListener("click", () => setRiderWorkspacePage("destination")); container.append(controls); const queue = riderDecisionQueueItems(); if (!queue.length) return; const queuePanel = document.createElement("article"); queuePanel.className = "notice-item rider-decision-queue"; queuePanel.innerHTML = ` Queued ride updates

${queue.length} update${queue.length === 1 ? "" : "s"} waiting while you finish the current decision.

`; queue.forEach((item) => { const request = stateLookupIndexes().requestMap.get(item.requestId); const row = document.createElement("div"); row.className = "queued-ride-update"; row.innerHTML = `
${escapeHtml(item.title)} ${escapeHtml(request ? `${requestPickupTownText(request)} to ${requestDestinationDisplayText(request)}` : item.requestId)} - ${formatDateTime(item.createdAt)}

${escapeHtml(item.body || "Ride update is ready to review.")}

`; row.querySelector("[data-view-queued-ride]")?.addEventListener("click", () => viewRiderDecisionQueueItem(item.id)); row.querySelector("[data-dismiss-queued-ride]")?.addEventListener("click", () => dismissRiderDecisionQueueItem(item.id)); queuePanel.append(row); }); container.append(queuePanel); } function renderRequests() { const visible = visibleRequestsForRole(); const fareBoostFocus = passengerFareBoostFocusSnapshot(els.requestList); rememberPassengerFareDraftInputs(); els.requestList.innerHTML = ""; renderRiderBusyDayControls(els.requestList); if (!visible.length) { if (activeRole() === "passenger") { const pendingRating = passengerPendingRatingRequest(); if (pendingRating) { els.requestList.append(renderPassengerPendingRatingCard(pendingRating)); showPassengerRatingPrompt(pendingRating); } else { els.requestList.append(emptyState(state.passenger ? "No active ride requests. Cancelled and completed rides are kept out of this active list." : "Sign in or create a passenger account to see your ride requests.")); } } else if (activeRole() === "rider") { const rider = currentRiderRecord(); const activeRide = riderActiveImmediateRide(rider); const message = !rider ? "Create or sign in as a rider to see nearby passenger requests." : !hasSignedIn("rider") ? "Sign in as a rider to see nearby passenger requests." : rider.status !== "approved" ? "Admin approval is required before ride requests are shown." : !isSubscriptionActive(rider) ? "Your trial or subscription must be active before ride requests are shown." : !paymentAccountReady("rider", rider) ? "Save your rider payment account before receiving requests." : !riderCurrentFreshGps(rider) ? (riderCurrentGps(rider) ? `${riderLiveGpsStatusSummary(rider)} Tap Activate again from Initialize rider availability so nearby requests refresh from your current position.` : "Activate rider availability from the Initialize rider availability menu before requests appear.") : activeRide ? "Complete or cancel your active immediate ride before taking another immediate request." : riderMarketplaceDestinationFilterIsActive() ? "No rides match this destination preference right now. Clear the destination filter to see all nearby requests." : `No matching immediate requests within about ${riderPickupMaxEtaMinutes} minutes, or scheduled requests within about ${scheduledRiderPickupMaxEtaMinutes} minutes, of your active live location and ${riderDestinationScopeLabel()} yet.`; els.requestList.append(emptyState(message)); } } visible.forEach((item) => { const node = els.requestTemplate.content.firstElementChild.cloneNode(true); const button = node.querySelector(".card-select"); const isRiderIncomingRequest = activeRole() === "rider" && item.status === "open" && !requestIsActiveForCurrentRider(item); const isPassengerTripCard = activeRole() === "passenger" && requestBelongsToPassenger(item) && passengerActiveRideRequestStatuses().includes(item.status); const isMatchedPassengerTrip = isPassengerTripCard && selectedRiderIdForRequest(item); const isPassengerNegotiationTrip = isPassengerNegotiationRequest(item); const isPassengerNonNegotiableWaitingTrip = isPassengerNonNegotiableWaitingRequest(item); const isRiderActiveTrip = activeRole() === "rider" && requestIsActiveForCurrentRider(item); const isCompactTripCard = isPassengerTripCard || isRiderActiveTrip; const pendingRouteChange = pendingRouteChangeForRequest(item); const routeDisplayItem = activeRole() === "rider" ? riderVisibleRouteRequest(item) : item; const passengerLabel = activeRole() === "rider" && item.status === "open" ? passengerUiText("passenger", "Passenger") : passengerFirstNameForRequest(item); node.classList.toggle("selected", item.id === state.selectedRequestId); node.classList.toggle("active-trip-card", isCompactTripCard); node.classList.toggle("passenger-negotiation-card", isPassengerNegotiationTrip); node.classList.toggle("non-negotiable-waiting-card", isPassengerNonNegotiableWaitingTrip); node.classList.toggle("rider-request-card", isRiderIncomingRequest); const pickupLabel = isRiderActiveTrip ? riderActivePickupDisplayText(item) : requestPickupDisplayText(item); const destinationLabel = isRiderActiveTrip ? riderActiveDropoffDisplayText(routeDisplayItem) : requestDestinationDisplayText(routeDisplayItem); const riderPostPickupTrip = isRiderActiveTrip && ["in_progress", "completed"].includes(item.status); node.querySelector(".card-kicker").textContent = isRiderIncomingRequest ? (isScheduledRequest(item) ? passengerUiText("scheduledRequest", "Scheduled request") : passengerUiText("incomingRequest", "Incoming request")) : passengerUiText("vehicleRequestLabel", "{vehicle} {type}", { vehicle: String(item.vehicle || "").toUpperCase(), type: isScheduledRequest(item) ? passengerUiText("scheduled", "scheduled") : passengerUiText("request", "request") }); node.querySelector("strong").textContent = isRiderIncomingRequest ? passengerUiText("pickupLabelValue", "Pickup: {pickup}", { pickup: requestPickupTownText(item) }) : riderPostPickupTrip ? passengerUiText("statusWithPerson", "{status} with {name}", { status: rideStatusLabel(item), name: passengerLabel }) : isMatchedPassengerTrip ? passengerUiText("statusWithPerson", "{status} with {name}", { status: rideStatusLabel(item), name: selectedRiderFirstNameForRequest(item) }) : passengerUiText("routeFromTo", "{pickup} to {destination}", { pickup: pickupLabel, destination: destinationLabel }); node.querySelector("small").textContent = isRiderIncomingRequest ? passengerUiText("personOfferedFare", "{name} offered {fare}", { name: passengerLabel, fare: formatMoney(item.fareOffer) }) : isRiderActiveTrip ? passengerUiText("fareScheduleLine", "Fare {fare} - {schedule}", { fare: formatMoney(agreedFareForRequest(item), item.country), schedule: scheduleChip(item) }) : isMatchedPassengerTrip ? passengerUiText("fareScheduleLine", "Fare {fare} - {schedule}", { fare: formatMoney(agreedFareForRequest(item), item.country), schedule: scheduleChip(item) }) : isPassengerNonNegotiableWaitingTrip ? passengerUiText("personOfferedFareSchedule", "{name} offered {fare} - {schedule}", { name: passengerLabel, fare: formatMoney(item.fareOffer), schedule: scheduleChip(item) }) : passengerUiText("personOfferedFareSchedule", "{name} offered {fare} - {schedule}", { name: passengerLabel, fare: formatMoney(item.fareOffer), schedule: scheduleChip(item) }); const summary = node.querySelector("p"); if (isRiderIncomingRequest) { summary.hidden = true; summary.textContent = ""; node.querySelector(".chip-row").innerHTML = [ isScheduledRequest(item) ? scheduleChip(item) : null, riderReopenedFareChip(item), riderMarketplaceFareChangeChip(item), fareModeChipText(item), `${normalizeRideStops(routeDisplayItem.rideStops).length} stop${normalizeRideStops(routeDisplayItem.rideStops).length === 1 ? "" : "s"}`, riderMarketplaceRouteDistanceChip(item) ].filter(Boolean).filter((value) => value !== "0 stops").map(chip).join(""); const actions = node.querySelector(".request-card-actions"); if (actions) { actions.hidden = false; actions.innerHTML = ""; const view = document.createElement("button"); view.type = "button"; view.className = "secondary-action compact-action"; view.textContent = "View"; view.addEventListener("click", (event) => { event.stopPropagation(); selectRequest(item.id); }); const ignore = document.createElement("button"); ignore.type = "button"; ignore.className = "ghost-action danger compact-action"; ignore.textContent = "Decline"; ignore.addEventListener("click", (event) => { event.stopPropagation(); void ignoreRiderMarketplaceRequest(item.id); }); actions.append(view, ignore); } } else { const actions = node.querySelector(".request-card-actions"); if (actions) actions.hidden = true; const stopsSummary = rideStopsSummary(routeDisplayItem.rideStops); const waitingForOffers = isPassengerTripCard && !isMatchedPassengerTrip && !isPassengerNonNegotiableWaitingTrip && !offersForRequest(item.id).length; const waitingForAcceptance = waitingForOffers || isPassengerNonNegotiableWaitingTrip; node.classList.toggle("waiting-for-offers-card", waitingForOffers); summary.hidden = isMatchedPassengerTrip || riderPostPickupTrip; summary.textContent = isMatchedPassengerTrip || riderPostPickupTrip ? "" : isPassengerTripCard ? offersForRequest(item.id).length ? passengerUiText("reviewRiderOffersChoose", "Review rider offers and choose when ready.") : isPassengerNonNegotiableWaitingTrip ? passengerUiText("waitingForRiderToAccept", "Waiting for rider to accept...") : passengerUiText("waitingForRiderOffers", "Waiting for rider offers...") : activeRole() === "rider" && normalizeRideStops(routeDisplayItem.rideStops).length && requestIsActiveForCurrentRider(item) ? `${pickupLabel}. ${stopsSummary}.` : pickupLabel; summary.classList.toggle("waiting-offers-status", waitingForAcceptance); if (waitingForAcceptance) summary.setAttribute("aria-live", "polite"); else summary.removeAttribute("aria-live"); const tripChips = isPassengerTripCard ? isPassengerNonNegotiableWaitingTrip ? [ rideStatusLabel(item), fareModeChipText(item), passengerUiText("offerAmount", "Offer {fare}", { fare: formatMoney(item.fareOffer, item.country) }), destinationDriveChip(item), normalizeRideStops(item.rideStops).length ? passengerUiText("stopCount", "{count} stop(s)", { count: normalizeRideStops(item.rideStops).length }) : null ] : [ rideStatusLabel(item), isMatchedPassengerTrip ? riderApproachChip(item) : passengerUiText("offerCount", "{count} offer(s)", { count: offersForRequest(item.id).length }), !isMatchedPassengerTrip ? passengerOfferFareChangeChip(item) : null, !isMatchedPassengerTrip ? fareModeChipText(item) : null, isMatchedPassengerTrip ? passengerUiText("fareAmount", "Fare {fare}", { fare: formatMoney(agreedFareForRequest(item), item.country) }) : passengerUiText("offerAmount", "Offer {fare}", { fare: formatMoney(item.fareOffer, item.country) }), destinationDriveChip(item), normalizeRideStops(item.rideStops).length ? passengerUiText("stopCount", "{count} stop(s)", { count: normalizeRideStops(item.rideStops).length }) : null, Number(item.cancellationFeeAmount ?? 0) > 0 ? passengerUiText("cancelFeeAmount", "Cancel fee {fare}", { fare: formatMoney(item.cancellationFeeAmount, item.country) }) : null ] : isRiderActiveTrip ? [ rideStatusLabel(item), pendingRouteChange ? passengerUiText("routeChangePending", "Route change pending") : null, item.status === "in_progress" ? nextRideLeg(item).label : (proximityChip(item) ?? passengerUiText("pickupEtaPending", "Pickup ETA pending")), passengerUiText("fareAmount", "Fare {fare}", { fare: formatMoney(agreedFareForRequest(item), item.country) }), destinationDriveChip(item) ] : [ activeRole() === "rider" ? paymentLabel(item.paymentPreference) : null, passengerUiText("offerCount", "{count} offer(s)", { count: offersForRequest(item.id).length }), rideStatusLabel(item), reopenedRequestChip(item), riderReopenedFareChip(item), riderApproachChip(item), proximityChip(item), destinationDriveChip(item), pickupGpsQualityChip(item), confirmationChip(item), item.businessAccountId ? passengerUiText("businessRide", "Business ride") : null, passengerUiText("vehicleDesignation", "Vehicle designation: {vehicle}", { vehicle: carTypePreferenceLabel(item.carTypePreference) }), normalizeRideStops(routeDisplayItem.rideStops).length ? passengerUiText("stopCount", "{count} stop(s)", { count: normalizeRideStops(routeDisplayItem.rideStops).length }) : null, Number(item.cancellationFeeAmount ?? 0) > 0 ? passengerUiText("cancelFeeAmount", "Cancel fee {fare}", { fare: formatMoney(item.cancellationFeeAmount, item.country) }) : null ]; node.querySelector(".chip-row").innerHTML = tripChips.filter(Boolean).map(chip).join(""); } if ((isMatchedPassengerTrip || isRiderActiveTrip) && typeof appendContactActions === "function") { appendContactActions(node, item); } if (isRiderActiveTrip) renderRiderActiveTripAddressDetails(node, item); if (isMatchedPassengerTrip) { renderPassengerApproachTracker(node, item, riderApproachModel(item)); } const usePassengerRideDock = passengerRideDockMode(item); const useRiderRideDock = riderRideDockMode(item); if (!isPassengerNegotiationTrip && !usePassengerRideDock && !useRiderRideDock) { renderRideGuidance(node, item); renderScheduledRideActions(node, item); } if (isPassengerNegotiationTrip) { renderPassengerNegotiationQuickActions(node, item); } else if (!usePassengerRideDock && !useRiderRideDock) { renderRideLifecycleActions(node, item); } renderPassengerFareBoost(node, item); button.addEventListener("click", () => selectRequest(item.id)); els.requestList.append(node); }); els.requestCount.textContent = `${visible.length}`; restorePassengerFareBoostFocus(fareBoostFocus, els.requestList); } function canBoostPassengerFare(request) { return Boolean(activeRole() === "passenger" && request?.status === "open" && requestBelongsToPassenger(request) && requestIsNegotiableFare(request) && request.id === state.selectedRequestId); } function passengerFareBoostGuidance(request) { const offerCount = offersForRequest(request.id).length; const ageMinutes = Math.floor((Date.now() - new Date(request.createdAt ?? Date.now()).getTime()) / 60000); const startingFareNotice = "Updating this changes your starting fare in the marketplace. Riders who have not started negotiating this request will use the new amount as their starting point."; if (isPassengerNegotiationRequest(request)) { if (offerCount > 0) { return `${offerCount} rider offer${offerCount === 1 ? "" : "s"} received. ${startingFareNotice}`; } return `Optional. Keep waiting or raise the fare before choosing a rider. ${startingFareNotice}`; } if (offerCount > 0) { return `${offerCount} rider offer${offerCount === 1 ? "" : "s"} received. ${startingFareNotice}`; } if (ageMinutes >= 5) { return `No rider offer yet. A higher fare can reopen visibility for riders who declined the earlier price. ${startingFareNotice}`; } if (ageMinutes >= 2) { return `Waka is still checking nearby riders. You can wait or increase the fare before a rider is chosen. ${startingFareNotice}`; } return `Open requests can be boosted before a rider is chosen. ${startingFareNotice}`; } function passengerFareBoostPanelOpen(request) { return Boolean(request?.id && passengerFareBoostOpenRequestId === request.id); } function togglePassengerFareBoostPanel(request) { if (!canBoostPassengerFare(request)) return; passengerFareBoostOpenRequestId = passengerFareBoostPanelOpen(request) ? null : request.id; renderAll(); if (passengerFareBoostOpenRequestId === request.id) { window.setTimeout(() => { const input = [...document.querySelectorAll(".fare-boost-input[data-request-id]")] .find((item) => item.dataset.requestId === request.id); if (input instanceof HTMLInputElement) { input.focus(); input.select(); } }, 0); } } function renderPassengerFareBoost(node, request) { if (!canBoostPassengerFare(request)) return; if (!passengerFareBoostPanelOpen(request)) return; const form = document.createElement("form"); form.className = "fare-boost-form"; form.classList.toggle("compact-fare-boost-form", isPassengerNegotiationRequest(request)); const draftValue = passengerFareBoostDrafts.has(request.id) ? passengerFareBoostDrafts.get(request.id) : String(request.fareOffer ?? ""); const isNegotiating = isPassengerNegotiationRequest(request); form.innerHTML = `

${escapeHtml(passengerFareBoostGuidance(request))}

`; const input = form.querySelector(".fare-boost-input"); input?.addEventListener("focus", () => rememberPassengerFareBoostFocus(input)); input?.addEventListener("pointerdown", () => rememberPassengerFareBoostFocus(input)); input?.addEventListener("click", () => rememberPassengerFareBoostFocus(input)); input?.addEventListener("input", () => { passengerFareBoostDrafts.set(request.id, input.value); rememberPassengerFareBoostFocus(input); }); input?.addEventListener("change", () => { passengerFareBoostDrafts.set(request.id, input.value); rememberPassengerFareBoostFocus(input); }); form.addEventListener("submit", (event) => updatePassengerFareOffer(event, request.id)); node.append(form); } function passengerOfferCounterDraftKey(offer, request) { return `${request?.id || "request"}:${offer?.id || "offer"}`; } function addActionButton(container, label, className, handler) { const button = document.createElement("button"); button.className = className; button.type = "button"; button.textContent = label; button.addEventListener("click", handler); container.append(button); } function actionLinkShouldUseNavigationHandler(href) { return /^waka-nav:/i.test(href) || /^intent:/i.test(href) || /^google\.navigation:/i.test(href) || /^waze:/i.test(href) || /^https:\/\/waze\.com\/ul/i.test(href) || /^https:\/\/www\.google\.com\/maps\//i.test(href); } function addActionLink(container, label, className, href) { if (!href) return; if (actionLinkShouldUseNavigationHandler(href)) { addActionButton(container, label, className, () => openNavigationUrl(href, { auto: true })); return; } const link = document.createElement("a"); link.className = className; link.href = href; link.target = "_blank"; link.rel = "noopener"; link.textContent = label; container.append(link); } function addRiderPickupNavigationAction(container, request, className = "secondary-action map-action") { if (!request) return; const hasPrecisePickupNavigation = typeof requestHasPrecisePickupNavigation === "function" ? requestHasPrecisePickupNavigation(request) : true; if (!hasPrecisePickupNavigation) { addActionButton(container, passengerUiText("navigateToPickup", "Navigate to pickup"), className, () => { if (typeof showRiderPickupNavigationClarification === "function") { showRiderPickupNavigationClarification(request); } else { const message = passengerUiText( "riderPickupNavigationClarifyPopup", "Pickup GPS is not clear enough for reliable navigation. Use Contact/Open chat to clarify the exact pickup landmark with the passenger, then continue with the typed pickup address shown on this ride." ); void (typeof showWakaGoodAlert === "function" ? showWakaGoodAlert(message) : Promise.resolve(alert(message))); } window.setTimeout(() => { if (typeof setRiderRideDockPanel === "function") setRiderRideDockPanel(request, "contact"); }, 0); }); return; } addActionLink(container, passengerUiText("navigateToPickup", "Navigate to pickup"), className, riderPickupNavigationUrl(request)); } async function rejectRiderOffer(offer) { if (!offer?.id) return; try { const updatedRequest = await rejectRiderOfferInSupabase(offer.id); state.rejectedOfferIds = [...new Set([...(state.rejectedOfferIds ?? []), offer.id])].slice(-500); state.offers = state.offers.filter((item) => item.id !== offer.id); if (state.passengerSelectedOfferId === offer.id) state.passengerSelectedOfferId = null; if (updatedRequest?.id) state.requests = upsertById(state.requests, updatedRequest); pushSystemChat(offer.requestId, "Passenger ended this rider negotiation. The ride request remains open for other riders."); saveState(); renderAll(); void refreshMarketplace({ silent: true, reason: "passenger_rejected_offer" }); } catch (error) { state.rejectedOfferIds = [...new Set([...(state.rejectedOfferIds ?? []), offer.id])].slice(-500); if (state.passengerSelectedOfferId === offer.id) state.passengerSelectedOfferId = null; saveState(); renderAll(); translatedAlert("rejectOfferFailed", { message: error.message }); } } async function passengerCounterRiderOffer(event, offer, request) { event.preventDefault(); const form = event.currentTarget; const status = form.querySelector(".offer-counter-status"); const input = form.querySelector(".offer-counter-input"); const submit = form.querySelector("button[type='submit']"); const draftKey = passengerOfferCounterDraftKey(offer, request); const rawFareDraft = String(input?.value ?? passengerOfferCounterDrafts.get(draftKey) ?? ""); passengerOfferCounterDrafts.set(draftKey, rawFareDraft); const nextFare = Number(rawFareDraft.replace(/[^\d.]/g, "")); const minimumNextFare = Number(request.fareOffer ?? 0); if (!Number.isInteger(nextFare)) { status.textContent = "Use a whole-number counter-offer."; return; } if (!nextFare || nextFare <= minimumNextFare) { status.textContent = `Enter any whole-number fare higher than ${formatMoney(minimumNextFare, request.country)}.`; return; } if (typeof passengerCanSendFareProposal === "function" && !passengerCanSendFareProposal(request)) { status.textContent = fareProposalLimitMessage("passenger", request); return; } try { if (submit) submit.disabled = true; status.textContent = "Sending counter-offer..."; const savedRequest = await updateRideRequestFareInSupabase(request.id, nextFare); state.requests = state.requests.map((item) => item.id === request.id ? { ...item, ...(savedRequest ?? {}), fareOffer: nextFare, fareHistory: savedRequest?.fareHistory ?? savedRequest?.fare_history ?? fareProposalHistoryWithNextFare(item, item.fareOffer, nextFare) } : item); state.rejectedOfferIds = (state.rejectedOfferIds ?? []).filter((id) => id !== offer.id); passengerOfferCounterDrafts.delete(draftKey); passengerOfferCounterLastFocus = null; if (state.passengerSelectedOfferId === offer.id) state.passengerSelectedOfferId = null; if (input instanceof HTMLInputElement) input.blur(); pushSystemChat(request.id, `Passenger countered by increasing the fare offer to ${formatMoney(nextFare, request.country)}. Riders can accept or counter higher.`); saveState(); renderAll(); void refreshMarketplace({ silent: true }); } catch (error) { status.textContent = error.message; } finally { if (submit) submit.disabled = false; } } function riderApproachTrackPercent(model) { if (!model || !Number.isFinite(Number(model.distanceKm))) return 8; const distanceKm = Math.max(0, Number(model.distanceKm)); const rangeKm = 5; return Math.max(8, Math.min(92, Math.round((1 - Math.min(distanceKm, rangeKm) / rangeKm) * 84 + 8))); } function requestMapPoint(request, areaName, fallbackX, fallbackY) { const area = findArea(request?.country, request?.city, areaName); return { x: Number.isFinite(Number(area?.x)) ? area.x : fallbackX, y: Number.isFinite(Number(area?.y)) ? area.y : fallbackY }; } function riderApproachMapPoint(request, model) { const pickup = requestMapPoint(request, request?.pickupArea, 58, 52); const destination = requestMapPoint(request, request?.destinationArea, 82, 48); const progress = riderApproachTrackPercent(model) / 100; return { x: Math.max(8, Math.min(92, pickup.x - (pickup.x - 12) * (1 - progress))), y: Math.max(10, Math.min(88, pickup.y - (pickup.y - 14) * (1 - progress))), pickup, destination }; } function approachMapMarker(point, label, type) { const gps = normalizeGpsPoint(point); if (!gps) return null; return { latitude: gps.latitude, longitude: gps.longitude, label, type }; } function passengerApproachTileMapEnabled() { const tilesAvailable = typeof workspaceMapTileMapsAvailable === "function" ? workspaceMapTileMapsAvailable() : String(appConfig?.mapboxAccessToken || "").trim().length > 0; return Boolean( configFlagEnabled(appConfig?.passengerApproachTileMapEnabled) && tilesAvailable ); } function passengerApproachTrackerVisible(request) { return Boolean(selectedRiderIdForRequest(request) && ["matched", "arrived", "in_progress"].includes(request?.status)); } const passengerApproachRouteTraceRefreshMs = 2 * 60 * 1000; const passengerApproachRouteTraceCache = new Map(); const passengerApproachRouteTraceInFlight = new Set(); const passengerApproachRouteTraceLastFetchAt = new Map(); function passengerApproachRouteTraceEnabled() { return typeof fetchRouteEstimateFromEdge === "function" && typeof routeEstimatesEnabled === "function" && routeEstimatesEnabled() && passengerApproachTileMapEnabled(); } function passengerApproachRouteTraceKey(request, rider, pickup) { if (!request?.id || !rider || !pickup) return ""; return [ request.id, Number(rider.latitude).toFixed(3), Number(rider.longitude).toFixed(3), Number(pickup.latitude).toFixed(3), Number(pickup.longitude).toFixed(3) ].join(":"); } function passengerApproachRouteTracePolyline(key) { return passengerApproachRouteTraceCache.get(key)?.routePolyline || ""; } async function maybeRefreshPassengerApproachRouteTrace(request, rider, pickup) { if (!passengerApproachRouteTraceEnabled()) return; const key = passengerApproachRouteTraceKey(request, rider, pickup); if (!key || passengerApproachRouteTraceCache.has(key) || passengerApproachRouteTraceInFlight.has(key)) return; const now = Date.now(); const lastFetchAt = passengerApproachRouteTraceLastFetchAt.get(key) || 0; if (lastFetchAt && now - lastFetchAt < passengerApproachRouteTraceRefreshMs) return; passengerApproachRouteTraceLastFetchAt.set(key, now); passengerApproachRouteTraceInFlight.add(key); try { const estimate = await fetchRouteEstimateFromEdge({ origin: { address: `${selectedRiderFirstNameForRequest(request)} live location`, latitude: rider.latitude, longitude: rider.longitude }, destination: { address: request.pickupDescription || "Pickup location", latitude: pickup.latitude, longitude: pickup.longitude, city: request.city, country: request.country }, stops: [], travelMode: "DRIVE" }); const routePolyline = String(estimate?.routePolyline || ""); if (routePolyline) { passengerApproachRouteTraceCache.set(key, { routePolyline, updatedAt: new Date().toISOString() }); if (typeof renderAll === "function") renderAll(); } } catch (error) { if (typeof logClientWarning === "function") { logClientWarning("Passenger approach road trace could not be loaded; direct approach line remains visible.", error); } } finally { passengerApproachRouteTraceInFlight.delete(key); } } function passengerApproachLiveMapModel(request, model) { const rider = approachMapMarker(model?.riderGps ?? riderApproachGps(request), "R", "rider"); const pickup = approachMapMarker(model?.pickupGps ?? requestPickupGps(request), "P", "pickup"); if (!rider || !pickup) return null; const destination = approachMapMarker(model?.destinationGps ?? requestDestinationGps(request), "D", "destination"); const routePoints = [rider, pickup].filter(Boolean); const traceKey = passengerApproachRouteTraceKey(request, rider, pickup); const routePathPoints = typeof workspaceMapRoutePathPointsFromPolyline === "function" ? workspaceMapRoutePathPointsFromPolyline(passengerApproachRouteTracePolyline(traceKey)) : []; maybeRefreshPassengerApproachRouteTrace(request, rider, pickup); const markers = [rider, pickup, destination].filter(Boolean); const centerPoints = routePathPoints.length ? routePathPoints : routePoints; return { country: request.country, city: request.city, center: typeof workspaceMapCenterForPoints === "function" ? workspaceMapCenterForPoints(centerPoints) : rider, markers, routePoints: routePathPoints.length ? routePoints : [], routePathPoints, zoom: 15 }; } function renderPassengerApproachTracker(node, request, model) { if (!passengerApproachTrackerVisible(request)) return; const tracker = document.createElement("div"); tracker.className = "approach-tracker"; const track = document.createElement("div"); track.className = "approach-track"; track.setAttribute("aria-hidden", "true"); const riderDot = document.createElement("span"); riderDot.className = "approach-dot rider-dot"; riderDot.style.left = `${riderApproachTrackPercent(model)}%`; const pickupDot = document.createElement("span"); pickupDot.className = "approach-dot pickup-dot"; const labelRow = document.createElement("div"); labelRow.className = "approach-labels"; const riderLabel = document.createElement("span"); riderLabel.textContent = selectedRiderFirstNameForRequest(request); const pickupLabel = document.createElement("span"); pickupLabel.textContent = "Pickup"; labelRow.append(riderLabel, pickupLabel); const mapPoint = riderApproachMapPoint(request, model); const liveMapModel = passengerApproachLiveMapModel(request, model); const useMapboxApproachTiles = passengerApproachTileMapEnabled(); const meta = document.createElement("small"); if (model) { const updated = model.capturedAt ? `Updated ${formatGpsAgeLabel({ capturedAt: model.capturedAt })}.` : "Refreshes automatically."; meta.textContent = liveMapModel ? `${formatPickupEta(model.etaMinutes)}. Live rider position on the local road map. ${updated}` : `${formatPickupEta(model.etaMinutes)}. ${model.isLive ? "Live rider location." : "Rider location estimate."} ${updated}`; } else { meta.textContent = "Waiting for the rider's live GPS update."; } const map = document.createElement("div"); map.className = liveMapModel ? "approach-map approach-live-map" : "approach-map"; map.setAttribute("aria-label", "Matched rider approach map"); if (!liveMapModel) { const routeLine = document.createElement("span"); routeLine.className = "approach-map-route"; routeLine.setAttribute("aria-hidden", "true"); const riderPin = document.createElement("span"); riderPin.className = "approach-map-pin approach-map-rider"; riderPin.style.left = `${mapPoint.x}%`; riderPin.style.top = `${mapPoint.y}%`; riderPin.textContent = "R"; const pickupPin = document.createElement("span"); pickupPin.className = "approach-map-pin approach-map-pickup"; pickupPin.style.left = `${mapPoint.pickup.x}%`; pickupPin.style.top = `${mapPoint.pickup.y}%`; pickupPin.textContent = "P"; const destinationPin = document.createElement("span"); destinationPin.className = "approach-map-pin approach-map-destination"; destinationPin.style.left = `${mapPoint.destination.x}%`; destinationPin.style.top = `${mapPoint.destination.y}%`; destinationPin.textContent = "D"; map.append(routeLine, riderPin, pickupPin, destinationPin); } track.append(riderDot, pickupDot); if (liveMapModel) { tracker.append(map, meta); } else { tracker.append(map, track, labelRow, meta); } node.append(tracker); if (liveMapModel && typeof renderInlineWorkspaceMap === "function") { window.requestAnimationFrame(() => renderInlineWorkspaceMap(map, liveMapModel, { useMapboxTiles: useMapboxApproachTiles })); } } function renderRideGuidance(node, request) { if (!request || !roleCanSeeRequest(request) || !["passenger", "rider"].includes(activeRole())) return; const guidance = document.createElement("div"); guidance.className = "ride-guidance"; const copy = document.createElement("div"); const title = document.createElement("strong"); const detail = document.createElement("span"); const actions = document.createElement("div"); actions.className = "review-actions"; if (activeRole() === "rider") { const matchedToRider = requestIsActiveForCurrentRider(request); if (!matchedToRider) return; if (pendingRouteChangeForRequest(request)) { renderRouteChangeRequestPanel(node, request); return; } if (riderShouldHoldNextRideNavigation(request)) { title.textContent = selectedRiderIdForRequest(request) === state.rider?.id ? passengerUiText("nextRideQueued", "Next ride queued") : passengerUiText("nextRequestAvailable", "Next request available"); detail.textContent = selectedRiderIdForRequest(request) === state.rider?.id ? passengerUiText("nextRideQueuedDetail", "This pickup is saved for after the current trip. Finish the active ride before opening the next pickup navigation.") : passengerUiText("nextRequestAvailableDetail", "You can review or offer because you are near drop-off. Current trip navigation stays active until the ride is complete."); copy.append(title, detail); guidance.append(copy); node.append(guidance); return; } const model = pickupProximityModel(request); const pickupText = requestPickupDisplayText(request); const hasPrecisePickupNavigation = typeof requestHasPrecisePickupNavigation === "function" ? requestHasPrecisePickupNavigation(request) : true; if (request.status === "in_progress") { const leg = nextRideLeg(request); title.textContent = leg.type === "destination" ? passengerUiText("destinationNavigation", "Destination navigation") : leg.label; detail.textContent = `${leg.destination}. ${rideStopProgressText(request)}`; addActionLink(actions, riderContinueNavigationLabel(leg), "secondary-action map-action", nextRideLegNavigationUrl(request)); } else { title.textContent = request.status === "arrived" ? passengerUiText("readyAtPickup", "Ready at pickup") : passengerUiText("pickupNavigation", "Pickup navigation"); detail.textContent = model ? passengerUiText("fromLiveLocation", "{eta} from your live location.", { eta: formatPickupEta(model.etaMinutes) }) : hasPrecisePickupNavigation ? passengerUiText("openPickupNavigation", "Open navigation to the verified pickup point.") : passengerUiText("pickupNavigationNeedsClarification", "Pickup GPS is not clear enough for reliable navigation. Clarify the landmark with the passenger by chat if needed."); if (request.status === "matched") { addRiderPickupNavigationAction(actions, request, "secondary-action map-action"); } } } else { if (!requestBelongsToPassenger(request)) return; const model = riderApproachModel(request); const selectedRiderId = selectedRiderIdForRequest(request); const reopenedAfterRiderCancel = passengerRequestHasRiderCancelReopenNotice(request); title.textContent = selectedRiderId ? passengerUiText("trackRider", "Track rider") : reopenedAfterRiderCancel ? passengerUiText("riderCancelled", "Rider cancelled") : passengerUiText("rideRequest", "Ride request"); detail.textContent = selectedRiderId ? model ? passengerUiText("riderApproachWithEta", "{name}: {eta} away. {source}.", { name: selectedRiderFirstNameForRequest(request), eta: formatPickupEta(model.etaMinutes), source: model.isLive ? "Live GPS" : model.source }) : passengerUiText("selectedRiderApproachPending", "{name} selected; approach pending.", { name: selectedRiderFirstNameForRequest(request) }) : reopenedAfterRiderCancel ? passengerUiText("requestOpenAgainAfterRiderCancel", "Your request is open again for other nearby riders. You do not need to create a new ride request.") : passengerUiText("requestOpenForNearbyRiders", "Request is open for nearby riders. Destination: {destination}.", { destination: requestDestinationDisplayText(request) }); } copy.append(title, detail); guidance.append(copy); if (actions.children.length) guidance.append(actions); node.append(guidance); } function renderScheduledRideActions(node, request) { if (!isScheduledRequest(request) || request.id !== state.selectedRequestId) return; const actions = document.createElement("div"); actions.className = "review-actions"; if (activeRole() === "passenger" && requestBelongsToPassenger(request) && request.status === "matched") { addActionButton(actions, "Request rider confirmation", "secondary-action", () => requestScheduledRideConfirmation(request.id)); addActionButton(actions, "Release rider and reopen", "ghost-action danger", () => releaseScheduledRide(request.id, "Passenger released the rider and reopened the scheduled ride.")); } if (activeRole() === "rider" && selectedRiderIdForRequest(request) === state.rider?.id && request.riderConfirmationStatus === "requested") { addActionButton(actions, "Confirm scheduled ride", "secondary-action", () => confirmScheduledRide(request.id)); addActionButton(actions, "Cannot keep plan", "ghost-action danger", () => releaseScheduledRide(request.id, "Rider cannot keep the scheduled ride. Passenger can choose another rider.")); } if (actions.children.length) node.append(actions); } function riderContinueNavigationLabel(nextLeg) { if (nextLeg?.type === "destination") return passengerUiText("continueToDestination", "Continue to destination"); if (nextLeg?.type === "stop") return passengerUiText("continueToStop", "Continue to {stop}", { stop: nextLeg.label }); return passengerUiText("navigateToPlace", "Navigate to {place}", { place: nextLeg?.label ?? passengerUiText("pickup", "pickup") }); } function renderRideLifecycleActions(node, request) { if (!canSeeRideLifecycleActions(request)) return; const panel = document.createElement("div"); panel.className = "ride-guidance ride-action-panel"; const copy = document.createElement("div"); const title = document.createElement("strong"); const detail = document.createElement("span"); const actions = document.createElement("div"); actions.className = "review-actions"; title.textContent = passengerUiText("rideActions", "Ride actions"); detail.textContent = rideLifecycleActionSummary(request); if (canCancelBeforeStart(request)) { const label = activeRole() === "rider" ? passengerUiText("cancelBeforeStart", "Cancel before start") : passengerUiText("cancelRide", "Cancel ride"); addActionButton(actions, label, "ghost-action danger", () => cancelRideBeforeStart(request.id)); } if (canCancelInProgress(request)) { addActionButton(actions, activeRole() === "passenger" ? passengerUiText("cancelRide", "Cancel ride") : passengerUiText("endRideEarly", "End ride early"), "ghost-action danger", () => cancelRideInProgress(request.id)); } const riderOwnsActiveRequest = activeRole() === "rider" && selectedRiderIdForRequest(request) === state.rider?.id; if (riderOwnsActiveRequest && request.status === "matched") { addRiderPickupNavigationAction(actions, request, "secondary-action map-action"); addActionButton(actions, passengerUiText("arrivedAtPickup", "Arrived at pickup"), "secondary-action", () => changeRideLifecycle(request.id, "arrive")); } if (riderOwnsActiveRequest && request.status === "arrived") { addActionButton(actions, passengerUiText("pickedUpPassenger", "Picked up passenger"), "secondary-action", async () => { const changed = await changeRideLifecycle(request.id, "start"); if (changed) { const updated = state.requests.find((item) => item.id === request.id) ?? request; openNavigationUrl(nextRideLegNavigationUrl(updated), { auto: true }); } }); } const nextLeg = nextRideLeg(request); if (riderOwnsActiveRequest && request.status === "in_progress") { addActionLink(actions, riderContinueNavigationLabel(nextLeg), "secondary-action map-action", nextRideLegNavigationUrl(request)); } if (riderOwnsActiveRequest && request.status === "in_progress" && nextLeg.type === "stop") { addActionButton(actions, passengerUiText("arrivedAtStop", "Arrived at {stop}", { stop: nextLeg.label }), "secondary-action", () => changeRideLifecycle(request.id, "stop")); } const canComplete = request.status === "in_progress" && riderOwnsActiveRequest && nextLeg.type === "destination"; if (canComplete) { addActionButton(actions, passengerUiText("completeRideAtDropoff", "Complete ride at drop-off"), "secondary-action", () => changeRideLifecycle(request.id, "complete")); } if (activeRole() === "passenger" && !actions.children.length) return; copy.append(title, detail); panel.append(copy); if (actions.children.length) panel.append(actions); node.append(panel); } function renderPassengerNegotiationQuickActions(node, request) { if (!isPassengerNegotiationRequest(request)) return; const actions = document.createElement("div"); actions.className = "passenger-negotiation-quick-actions"; if (!passengerRideDockMode(request) && canCancelBeforeStart(request)) { addActionButton(actions, "Cancel request", "ghost-action danger compact-action", () => cancelRideBeforeStart(request.id)); } const boost = document.createElement("button"); boost.type = "button"; boost.className = "secondary-action icon-action passenger-offer-update-action"; boost.title = passengerFareBoostPanelOpen(request) ? "Hide offer editor" : "Update starting fare"; boost.setAttribute("aria-label", boost.title); boost.setAttribute("aria-pressed", passengerFareBoostPanelOpen(request) ? "true" : "false"); boost.textContent = "$"; boost.addEventListener("click", () => togglePassengerFareBoostPanel(request)); actions.append(boost); if (actions.children.length) node.append(actions); } function renderRouteChangeRequestPanel(node, request) { const change = pendingRouteChangeForRequest(request); if (!change) return; const panel = document.createElement("div"); panel.className = "ride-guidance ride-action-panel"; const copy = document.createElement("div"); const title = document.createElement("strong"); const detail = document.createElement("span"); const actions = document.createElement("div"); actions.className = "review-actions"; const label = routeChangeTypeLabel(change.type); const addOn = formatMoney(change.additionalFare, request.country); const total = formatMoney(change.totalFare, request.country); const proposedRequest = requestWithPendingRouteChange(request, change); const destination = requestDestinationDisplayText(proposedRequest); const stops = normalizeRideStops(proposedRequest.rideStops); const stopText = stops.length ? `${stops.length} stop${stops.length === 1 ? "" : "s"}: ${stops.join("; ")}` : "No added stops."; const delta = change.routeDelta; const routeLine = delta ? ` Added drive: ${formatRouteDistanceForRequest(Number(delta.addedMiles ?? 0), request)}${Number(delta.addedMinutes ?? 0) > 0 ? `, traffic-time change about ${Math.ceil(Number(delta.addedMinutes))} minutes` : ""}.` : ""; const changeSummary = change.type === "add_stop" ? "Passenger requested an added stop before continuing this ride." : "Passenger requested a final destination change."; title.textContent = activeRole() === "rider" ? `Acknowledge ${label}` : `${label} pending`; detail.textContent = activeRole() === "rider" ? `${changeSummary} Proposed route: ${destination}. ${stopText}.${routeLine} Added fare: ${addOn}. New ride total: ${total}. Acknowledge to update the route before proceeding, or decline to keep the current route.` : `Waiting for rider approval. Proposed route: ${destination}. ${stopText}.${routeLine} Added fare if accepted: ${addOn}. New ride total: ${total}.`; if (activeRole() === "rider" && riderIdentityMatches(selectedRiderIdForRequest(request))) { addActionButton(actions, "Acknowledge and update route", "secondary-action", () => acceptRouteChangeRequest(change.id)); addActionButton(actions, "Decline route change", "ghost-action danger", () => declineRouteChangeRequest(change.id)); } copy.append(title, detail); panel.append(copy); if (actions.children.length) panel.append(actions); node.append(panel); } function riderRouteChangeDecisionContext() { if (activeRole() !== "rider" || !state.rider?.id) return null; const activeRequests = state.requests .filter((request) => requestIsActiveForCurrentRider(request)) .sort((left, right) => (left.id === state.selectedRequestId ? -1 : 0) - (right.id === state.selectedRequestId ? -1 : 0)); for (const request of activeRequests) { const change = pendingRouteChangeForRequest(request); if (change) return { request, change }; } return null; } function renderRiderRouteChangeDecisionModal() { const context = riderRouteChangeDecisionContext(); const existing = document.querySelector(".route-change-decision-backdrop"); if (!context) { existing?.remove(); return; } const { request, change } = context; if (existing?.dataset.changeId === change.id) return; existing?.remove(); const label = routeChangeTypeLabel(change.type); const addOn = formatMoney(change.additionalFare, request.country); const total = formatMoney(change.totalFare, request.country); const stops = normalizeRideStops(change.rideStops); const destination = change.destinationFormattedAddress || change.destination || requestDestinationDisplayText(request); const stopLine = stops.length ? `${stops.length} stop${stops.length === 1 ? "" : "s"} on route: ${stops.join("; ")}` : "No added stop."; const delta = change.routeDelta; const distanceLine = delta ? `Added drive: ${formatRouteDistanceForRequest(Number(delta.addedMiles ?? 0), request)}${Number(delta.addedMinutes ?? 0) > 0 ? `, traffic change about ${Math.ceil(Number(delta.addedMinutes))} minutes` : ""}.` : ""; const backdrop = document.createElement("div"); backdrop.className = "route-change-decision-backdrop"; backdrop.dataset.changeId = change.id; backdrop.setAttribute("role", "dialog"); backdrop.setAttribute("aria-modal", "true"); backdrop.innerHTML = `
Route change request

${escapeHtml(change.type === "add_stop" ? "Passenger requested a stop" : "Passenger changed destination")}

${escapeHtml(change.type === "add_stop" ? "Accept to add the stop before continuing. Decline to keep the current route." : "Accept to update the final destination. Decline to keep the current route.")}

Destination
${escapeHtml(destination)}
Stops
${escapeHtml(stopLine)}
Fare
${escapeHtml(`Added ${addOn}. New total ${total}.`)}
${distanceLine ? `
Drive
${escapeHtml(distanceLine)}
` : ""}

`; const status = backdrop.querySelector(".route-change-decision-status"); const buttons = [...backdrop.querySelectorAll("button")]; const setBusy = (message) => { buttons.forEach((button) => { button.disabled = true; }); status.textContent = message; }; const stillPending = () => pendingRouteChangeForRequest(stateLookupIndexes().requestMap.get(request.id) ?? request)?.id === change.id; backdrop.querySelector(".route-change-accept")?.addEventListener("click", async () => { setBusy("Accepting route change..."); await acceptRouteChangeRequest(change.id); if (!stillPending()) backdrop.remove(); else buttons.forEach((button) => { button.disabled = false; }); }); backdrop.querySelector(".route-change-decline")?.addEventListener("click", async () => { setBusy("Declining route change..."); await declineRouteChangeRequest(change.id); if (!stillPending()) backdrop.remove(); else buttons.forEach((button) => { button.disabled = false; }); }); document.body.append(backdrop); playNearbyRideCue(); } function renderRideTipForm(node, request) { if (!canTipRequest(request)) return; const form = document.createElement("form"); form.className = "fare-boost-form"; form.innerHTML = `

Tips go to the rider after Stripe processing fees; Waka does not add a ride fee to tips.

`; form.addEventListener("submit", (event) => submitRideTip(event, request.id)); node.append(form); } function renderDestinationUpdateForm(node, request) { if (!canUpdateRideDestination(request)) return; const form = document.createElement("form"); form.className = "fare-boost-form destination-update-form"; form.dataset.routeChangeRequestId = passengerDestinationUpdateDraftKey(request); form.dataset.requestId = request.id; const destinationSuggestionId = `destinationUpdateSuggestions-${request.id}`; const stopSuggestionId = `stopsUpdateSuggestions-${request.id}`; const draft = passengerDestinationUpdateDraftForRequest(request); const windowText = request.status === "in_progress" ? "Pickup has started. Ask the rider verbally before sending; the rider must approve in the app." : routeChangeNeedsRiderApproval(request) ? "Matched rider must approve route changes. Waka calculates the added fare before sending." : "Open request route changes update the offered fare automatically."; form.innerHTML = `

${windowText}

`; setupDestinationUpdateAutocomplete(form, request); form.addEventListener("submit", (event) => submitDestinationUpdate(event, request.id)); node.append(form); } function passengerRideDockMode(request) { return Boolean(activeRole() === "passenger" && request && requestBelongsToPassenger(request) && ((selectedRiderIdForRequest(request) && ["matched", "arrived", "in_progress", "completed"].includes(request.status)) || isPassengerWaitingForRiderRequest(request))); } function passengerRideDockTools(request) { if (!passengerRideDockMode(request)) return []; const tools = []; const addTool = (id, label, icon, title) => tools.push({ id, label, icon, title }); const completed = request.status === "completed"; if (isPassengerWaitingForRiderRequest(request)) { if (canUpdateRideDestination(request) || pendingRouteChangeForRequest(request)) addTool("route", passengerUiText("route", "Route"), "R", passengerUiText("changeDestinationOrAddStop", "Change destination or add a stop")); if (canCancelBeforeStart(request)) addTool("cancel", passengerUiText("cancel", "Cancel"), "X", passengerUiText("cancelRide", "Cancel ride")); return tools; } if (completed) { if (canRateRequest(request) || existingRatingForRequest(request)) addTool("rating", passengerUiText("rate", "Rate"), "*", passengerUiText("rating", "Rating")); addTool("payment", passengerUiText("fare", "Fare"), "$", passengerUiText("paymentFareSummary", "Payment and fare summary")); if (canReportOnRequest(request)) addTool("support", passengerUiText("support", "Support"), "!", passengerUiText("supportOrReportIssue", "Support or report issue")); addTool("contact", passengerUiText("contact", "Contact"), "C", passengerUiText("contactRider", "Contact rider")); return tools; } addTool("contact", passengerUiText("contact", "Contact"), "C", passengerUiText("contactRider", "Contact rider")); if (canUpdateRideDestination(request) || pendingRouteChangeForRequest(request)) addTool("route", passengerUiText("route", "Route"), "R", passengerUiText("addStopOrChangeDestination", "Add stop or change destination")); if (canReportOnRequest(request)) addTool("support", passengerUiText("support", "Support"), "!", passengerUiText("supportOrReportIssue", "Support or report issue")); if (canCancelBeforeStart(request) || canCancelInProgress(request)) addTool("cancel", passengerUiText("cancel", "Cancel"), "X", passengerUiText("cancelRide", "Cancel ride")); return tools; } function passengerRideDockCurrentPanel(request) { if (!passengerRideDockMode(request)) { passengerRideDockRequestId = null; passengerRideDockOpenPanel = null; return null; } if (passengerRideDockRequestId !== request.id) { passengerRideDockRequestId = request.id; passengerRideDockOpenPanel = null; } const toolIds = new Set(passengerRideDockTools(request).map((tool) => tool.id)); if (!toolIds.has(passengerRideDockOpenPanel)) passengerRideDockOpenPanel = null; return passengerRideDockOpenPanel; } function setPassengerRideDockPanel(request, panelId) { if (!passengerRideDockMode(request)) return; passengerRideDockRequestId = request.id; passengerRideDockOpenPanel = passengerRideDockOpenPanel === panelId ? null : panelId; renderAll(); } function renderPassengerRideToolHeader(node, titleText, detailText) { const panel = document.createElement("div"); panel.className = "ride-tool-panel"; const title = document.createElement("strong"); const detail = document.createElement("span"); title.textContent = titleText; detail.textContent = detailText; panel.append(title, detail); node.append(panel); return panel; } function renderPassengerRideDockButtons(node, request, activePanel) { const tools = passengerRideDockTools(request); if (!tools.length) return; const dock = document.createElement("div"); dock.className = "passenger-ride-dock"; dock.setAttribute("aria-label", passengerUiText("rideTools", "Ride tools")); tools.forEach((tool) => { const button = document.createElement("button"); button.type = "button"; button.className = "ride-tool-button"; button.classList.toggle("active", activePanel === tool.id); button.title = tool.title; button.setAttribute("aria-pressed", activePanel === tool.id ? "true" : "false"); button.innerHTML = ` ${escapeHtml(tool.label)} `; button.addEventListener("click", () => setPassengerRideDockPanel(request, tool.id)); dock.append(button); }); node.append(dock); return dock; } function renderPassengerRideCancelPanel(node, request) { const panel = renderPassengerRideToolHeader(node, passengerUiText("cancelRide", "Cancel ride"), rideLifecycleActionSummary(request)); const actions = document.createElement("div"); actions.className = "review-actions"; if (canCancelBeforeStart(request)) { addActionButton(actions, passengerUiText("cancelRide", "Cancel ride"), "ghost-action danger", () => cancelRideBeforeStart(request.id)); } else if (canCancelInProgress(request)) { addActionButton(actions, passengerUiText("cancelRide", "Cancel ride"), "ghost-action danger", () => cancelRideInProgress(request.id)); } if (actions.children.length) panel.append(actions); } function renderPassengerRidePaymentPanel(node, request) { const total = formatMoney(agreedFareForRequest(request), request.country); const method = paymentLabel(request.paymentPreference); renderPassengerRideToolHeader( node, passengerUiText("paymentSummary", "Payment summary"), passengerUiText("finalFarePaymentDestination", "Final matched fare: {fare}. Payment method: {method}. Destination: {destination}.", { fare: total, method, destination: requestDestinationDisplayText(request) }) ); renderRideTipForm(node, request); } function renderPassengerRideDock(node, request, destinationUpdateFocus = null) { const activePanel = passengerRideDockCurrentPanel(request); node.innerHTML = ""; const shell = document.createElement("div"); shell.className = "passenger-ride-tool-shell"; const content = document.createElement("div"); content.className = "passenger-ride-tool-content"; content.dataset.passengerRideToolContent = "true"; if (!activePanel) content.hidden = true; if (activePanel === "route") { renderPassengerRideToolHeader( content, request.status === "in_progress" ? passengerUiText("updateRoute", "Update route") : passengerUiText("routeChange", "Route change"), passengerUiText("routeChangeFareRecalculation", "Change destination or add one stop. Waka recalculates the added fare before sending it to the rider.") ); renderRouteChangeRequestPanel(content, request); renderDestinationUpdateForm(content, request); restorePassengerDestinationUpdateFocus(destinationUpdateFocus, content); } else if (activePanel === "support") { renderPassengerRideToolHeader(content, passengerUiText("support", "Support"), passengerUiText("contactWakaOrReportRideIssue", "Contact Waka or report a ride issue.")); } else if (activePanel === "cancel") { renderPassengerRideCancelPanel(content, request); } else if (activePanel === "payment") { renderPassengerRidePaymentPanel(content, request); } else if (activePanel === "rating") { const riderName = selectedRiderFirstNameForRequest(request); renderPassengerRideToolHeader( content, `Rate ${riderName}`, `${requestPickupDisplayText(request)} to ${requestDestinationDisplayText(request)}` ); } else if (activePanel === "contact") { renderPassengerRideToolHeader(content, passengerUiText("contact", "Contact"), passengerUiText("messageRiderFromRide", "Message the rider in WakaGood from this ride.")); } renderPassengerRideDockButtons(shell, request, activePanel); shell.append(content); node.append(shell); return request; } function riderRideDockMode(request) { return Boolean(activeRole() === "rider" && request && riderIdentityMatches(selectedRiderIdForRequest(request)) && ["matched", "arrived", "in_progress", "completed"].includes(request.status)); } function riderRideDockTools(request) { if (!riderRideDockMode(request)) return []; const tools = []; const addTool = (id, label, icon, title) => tools.push({ id, label, icon, title }); const completed = request.status === "completed"; const pendingChange = pendingRouteChangeForRequest(request); if (completed) { addTool("fare", passengerUiText("fare", "Fare"), "$", passengerUiText("fareSummary", "Fare summary")); if (canReportOnRequest(request)) addTool("support", passengerUiText("support", "Support"), "!", passengerUiText("supportOrReportIssue", "Support or report issue")); addTool("contact", passengerUiText("contact", "Contact"), "C", passengerUiText("contactPassenger", "Contact passenger")); return tools; } if (pendingChange) { addTool("route", passengerUiText("route", "Route"), "R", passengerUiText("acceptOrDeclineRouteChange", "Accept or decline requested route change")); addTool("contact", passengerUiText("contact", "Contact"), "C", passengerUiText("contactPassenger", "Contact passenger")); if (canReportOnRequest(request)) addTool("support", passengerUiText("support", "Support"), "!", passengerUiText("supportOrReportIssue", "Support or report issue")); return tools; } if (request.status === "matched" || request.status === "in_progress") { addTool("navigate", passengerUiText("navigate", "Navigate"), "N", request.status === "matched" ? passengerUiText("navigateToPickup", "Navigate to pickup") : passengerUiText("navigateNextStopOrDestination", "Navigate to next stop or destination")); } addTool("progress", passengerUiText("progress", "Progress"), "P", passengerUiText("updateRideProgressStatus", "Update pickup, stop, or completion status")); if (pendingRouteChangeForRequest(request)) addTool("route", passengerUiText("route", "Route"), "R", passengerUiText("reviewRequestedRouteChange", "Review requested route change")); addTool("contact", passengerUiText("contact", "Contact"), "C", passengerUiText("contactPassenger", "Contact passenger")); if (canReportOnRequest(request)) addTool("support", passengerUiText("support", "Support"), "!", passengerUiText("supportOrReportIssue", "Support or report issue")); if (canCancelBeforeStart(request) || canCancelInProgress(request)) addTool("cancel", passengerUiText("cancel", "Cancel"), "X", passengerUiText("cancelThisRide", "Cancel this ride")); return tools; } function riderRideDockCurrentPanel(request) { if (!riderRideDockMode(request)) { riderRideDockRequestId = null; riderRideDockOpenPanel = null; return null; } if (riderRideDockRequestId !== request.id) { riderRideDockRequestId = request.id; riderRideDockOpenPanel = null; } const toolIds = new Set(riderRideDockTools(request).map((tool) => tool.id)); if (!toolIds.has(riderRideDockOpenPanel)) riderRideDockOpenPanel = null; return riderRideDockOpenPanel; } function setRiderRideDockPanel(request, panelId) { if (!riderRideDockMode(request)) return; riderRideDockRequestId = request.id; riderRideDockOpenPanel = riderRideDockOpenPanel === panelId ? null : panelId; renderAll(); } function renderRiderRideDockButtons(node, request, activePanel) { const tools = riderRideDockTools(request); if (!tools.length) return null; const dock = document.createElement("div"); dock.className = "passenger-ride-dock rider-ride-dock"; dock.setAttribute("aria-label", passengerUiText("riderRideTools", "Rider ride tools")); tools.forEach((tool) => { const button = document.createElement("button"); button.type = "button"; button.className = "ride-tool-button"; button.classList.toggle("active", activePanel === tool.id); button.title = tool.title; button.setAttribute("aria-pressed", activePanel === tool.id ? "true" : "false"); button.innerHTML = ` ${escapeHtml(tool.label)} `; button.addEventListener("click", () => setRiderRideDockPanel(request, tool.id)); dock.append(button); }); node.append(dock); return dock; } function renderRiderRideNavigationPanel(node, request) { if (pendingRouteChangeForRequest(request)) { renderPassengerRideToolHeader( node, passengerUiText("routeChangePending", "Route change pending"), passengerUiText("routeChangePendingDetail", "Accept or decline the passenger route change before opening navigation.") ); renderRouteChangeRequestPanel(node, request); return; } const nextLeg = nextRideLeg(request); const isPickupLeg = request.status === "matched"; const label = isPickupLeg ? "pickup" : nextLeg.label; const panel = renderPassengerRideToolHeader( node, passengerUiText("navigation", "Navigation"), isPickupLeg ? (typeof requestHasPrecisePickupNavigation === "function" && !requestHasPrecisePickupNavigation(request) ? passengerUiText("pickupNavigationNeedsClarification", "Pickup GPS is not clear enough for reliable navigation. Clarify the landmark with the passenger by chat if needed.") : passengerUiText("openNavigationTo", "Open navigation to {place}.", { place: riderActivePickupDisplayText(request) })) : passengerUiText("openNavigationTo", "Open navigation to {place}.", { place: label }) ); const actions = document.createElement("div"); actions.className = "review-actions"; if (isPickupLeg) { addRiderPickupNavigationAction(actions, request, "secondary-action map-action"); } else if (request.status === "in_progress") { addActionLink(actions, riderContinueNavigationLabel(nextLeg), "secondary-action map-action", nextRideLegNavigationUrl(request)); } if (actions.children.length) panel.append(actions); } function renderRiderRideProgressPanel(node, request) { const panel = renderPassengerRideToolHeader(node, passengerUiText("rideProgress", "Ride progress"), rideLifecycleActionSummary(request)); const actions = document.createElement("div"); actions.className = "review-actions"; appendRiderRideProgressActions(actions, request); if (actions.children.length) panel.append(actions); } function appendRiderRideProgressActions(actions, request) { const nextLeg = nextRideLeg(request); if (request.status === "matched") { addActionButton(actions, passengerUiText("arrivedAtPickup", "Arrived at pickup"), "secondary-action", () => changeRideLifecycle(request.id, "arrive")); } else if (request.status === "arrived") { addActionButton(actions, passengerUiText("pickedUpPassenger", "Picked up passenger"), "secondary-action", async () => { const changed = await changeRideLifecycle(request.id, "start"); if (changed) { const updated = state.requests.find((item) => item.id === request.id) ?? request; openNavigationUrl(nextRideLegNavigationUrl(updated), { auto: true }); } }); } else if (request.status === "in_progress" && nextLeg.type === "stop") { addActionLink(actions, riderContinueNavigationLabel(nextLeg), "secondary-action map-action", nextRideLegNavigationUrl(request)); addActionButton(actions, passengerUiText("arrivedAtStop", "Arrived at {stop}", { stop: nextLeg.label }), "secondary-action", () => changeRideLifecycle(request.id, "stop")); } else if (request.status === "in_progress" && nextLeg.type === "destination") { addActionLink(actions, riderContinueNavigationLabel(nextLeg), "secondary-action map-action", nextRideLegNavigationUrl(request)); addActionButton(actions, passengerUiText("completeRideAtDropoff", "Complete ride at drop-off"), "secondary-action", () => changeRideLifecycle(request.id, "complete")); } return actions.children.length > 0; } function renderRiderRideNextProgressAction(node, request) { if (!riderRideDockMode(request) || request.status === "completed") return; if (pendingRouteChangeForRequest(request)) { renderRouteChangeRequestPanel(node, request); return; } const panel = document.createElement("div"); panel.className = "ride-tool-panel rider-progress-action-panel"; const title = document.createElement("strong"); title.textContent = passengerUiText("nextRideStep", "Next ride step"); const detail = document.createElement("span"); detail.textContent = rideLifecycleActionSummary(request); const actions = document.createElement("div"); actions.className = "review-actions"; if (!appendRiderRideProgressActions(actions, request)) return; panel.append(title, detail, actions); node.append(panel); } function renderRiderRideCancelPanel(node, request) { const panel = renderPassengerRideToolHeader(node, passengerUiText("cancelRide", "Cancel ride"), rideLifecycleActionSummary(request)); const actions = document.createElement("div"); actions.className = "review-actions"; if (canCancelBeforeStart(request)) { addActionButton(actions, passengerUiText("cancelBeforeStart", "Cancel before start"), "ghost-action danger", () => cancelRideBeforeStart(request.id)); } else if (canCancelInProgress(request)) { addActionButton(actions, passengerUiText("endRideEarly", "End ride early"), "ghost-action danger", () => cancelRideInProgress(request.id)); } if (actions.children.length) panel.append(actions); } function renderRiderRideFarePanel(node, request) { renderPassengerRideToolHeader( node, passengerUiText("fareSummary", "Fare summary"), passengerUiText("finalFareRoute", "Final matched fare: {fare}. Route: {route}.", { fare: formatMoney(agreedFareForRequest(request), request.country), route: riderActiveRouteDisplayText(request) }) ); } function renderRiderRideDock(node, request) { const activePanel = riderRideDockCurrentPanel(request); node.innerHTML = ""; const shell = document.createElement("div"); shell.className = "passenger-ride-tool-shell rider-ride-tool-shell"; const content = document.createElement("div"); content.className = "passenger-ride-tool-content rider-ride-tool-content"; content.dataset.riderRideToolContent = "true"; if (!activePanel) content.hidden = true; if (activePanel === "navigate") { renderRiderRideNavigationPanel(content, request); } else if (activePanel === "progress") { renderRiderRideProgressPanel(content, request); } else if (activePanel === "route") { renderPassengerRideToolHeader(content, passengerUiText("routeUpdate", "Route update"), passengerUiText("routeUpdateReviewBeforeChanging", "Review the passenger route-change request before changing course.")); renderRouteChangeRequestPanel(content, request); } else if (activePanel === "contact") { renderPassengerRideToolHeader(content, passengerUiText("contact", "Contact"), passengerUiText("messagePassengerFromRide", "Message the passenger in WakaGood from this ride.")); } else if (activePanel === "support") { renderPassengerRideToolHeader(content, passengerUiText("support", "Support"), passengerUiText("contactWakaOrReportRideIssue", "Contact Waka or report a ride issue.")); } else if (activePanel === "cancel") { renderRiderRideCancelPanel(content, request); } else if (activePanel === "fare") { renderRiderRideFarePanel(content, request); } renderRiderRideDockButtons(shell, request, activePanel); shell.append(content); node.append(shell); return request; } function renderPersistentRideActions(request = selectedRequest()) { if (!els.rideActionPanel) return null; if (isPassengerNegotiationRequest(request) && !passengerRideDockMode(request)) { restoreRideToolElementsToChatPanel(); els.rideActionPanel.innerHTML = ""; return null; } const actionRequest = activeRideForRole(request); if (!actionRequest) resetRideDockPanels({ restore: false }); const focusedDestinationUpdateForm = passengerDestinationUpdateFocusedFormForRequest(actionRequest, els.rideActionPanel); if (focusedDestinationUpdateForm && canUpdateRideDestination(actionRequest)) { rememberPassengerDestinationUpdateDraft(focusedDestinationUpdateForm); return actionRequest; } const destinationUpdateFocus = passengerDestinationUpdateFocusedFieldSnapshot(els.rideActionPanel); restoreRideToolElementsToChatPanel(); els.rideActionPanel.innerHTML = ""; if (actionRequest && canSeeRideLifecycleActions(actionRequest)) { if (passengerRideDockMode(actionRequest)) { return renderPassengerRideDock(els.rideActionPanel, actionRequest, destinationUpdateFocus); } if (riderRideDockMode(actionRequest)) { return renderRiderRideDock(els.rideActionPanel, actionRequest); } renderRideLifecycleActions(els.rideActionPanel, actionRequest); renderRouteChangeRequestPanel(els.rideActionPanel, actionRequest); renderDestinationUpdateForm(els.rideActionPanel, actionRequest); renderRideTipForm(els.rideActionPanel, actionRequest); restorePassengerDestinationUpdateFocus(destinationUpdateFocus, els.rideActionPanel); return actionRequest; } if (activeRole() === "rider") { restorePassengerDestinationUpdateFocus(destinationUpdateFocus, els.rideActionPanel); return null; } const panel = document.createElement("div"); panel.className = "ride-guidance ride-action-panel"; const copy = document.createElement("div"); const title = document.createElement("strong"); const detail = document.createElement("span"); title.textContent = "Ride actions"; detail.textContent = activeRole() === "rider" ? "Cancel appears after a passenger chooses your offer. Complete appears once the ride is in progress." : "Ride updates and support actions appear here after you choose a rider."; copy.append(title, detail); panel.append(copy); els.rideActionPanel.append(panel); restorePassengerDestinationUpdateFocus(destinationUpdateFocus, els.rideActionPanel); return null; } function riderOfferFareTrail(offer) { const entries = Array.isArray(offer?.fareHistory) ? offer.fareHistory : []; const normalized = entries .map((entry) => ({ fare: Number(entry?.fare ?? entry?.amount ?? entry?.fare_xaf ?? entry), createdAt: entry?.createdAt ?? entry?.created_at ?? offer?.createdAt ?? null })) .filter((entry) => Number.isFinite(entry.fare) && entry.fare > 0) .sort((a, b) => new Date(a.createdAt ?? 0).getTime() - new Date(b.createdAt ?? 0).getTime()); if (!normalized.length && Number.isFinite(Number(offer?.fare))) { normalized.push({ fare: Number(offer.fare), createdAt: offer.createdAt ?? null }); } return normalized.reduce((trail, entry) => { const previous = trail[trail.length - 1]; if (!previous || Number(previous.fare) !== Number(entry.fare)) trail.push(entry); return trail; }, []); } function riderOfferFareTrailText(offer, request) { return riderOfferFareTrail(offer) .map((entry) => formatMoney(entry.fare, request?.country)) .join(" -> "); } function renderOffers() { const request = selectedWorkspaceRequest(); const visibleOffers = visibleOffersForRole(request); const isPassengerNegotiation = isPassengerNegotiationRequest(request); let selectedPassengerOffer = isPassengerNegotiation ? selectedPassengerNegotiationOffer(request, visibleOffers) : null; if (state.passengerSelectedOfferId && (!isPassengerNegotiation || !selectedPassengerOffer)) { state.passengerSelectedOfferId = null; selectedPassengerOffer = null; saveState(); } const offersToRender = selectedPassengerOffer ? [selectedPassengerOffer] : visibleOffers; const counterFocus = passengerOfferCounterFocusSnapshot(els.offerList); rememberPassengerFareDraftInputs(); els.offerList.innerHTML = ""; const hidePassengerClosedOffers = activeRole() === "passenger" && request && request.status !== "open"; const hidePassengerNonNegotiableOffers = activeRole() === "passenger" && isPassengerNonNegotiableWaitingRequest(request); const hidePassengerEmptyNegotiatedOffers = activeRole() === "passenger" && isPassengerNegotiationRequest(request) && !visibleOffers.length && !selectedPassengerOffer; const hideRiderListModeOffers = activeRole() === "rider" && riderWorkspacePage() === "requests" && !riderRequestDetailOpen(); const hideRiderActiveTripOffers = activeRole() === "rider" && request && requestIsActiveForCurrentRider(request); if (els.offersBoard) els.offersBoard.hidden = hidePassengerClosedOffers || hidePassengerNonNegotiableOffers || hidePassengerEmptyNegotiatedOffers || hideRiderListModeOffers || hideRiderActiveTripOffers; if (hidePassengerClosedOffers || hidePassengerNonNegotiableOffers || hidePassengerEmptyNegotiatedOffers || hideRiderListModeOffers || hideRiderActiveTripOffers) { els.offerCount.textContent = "0"; return; } if (!request || !roleCanSeeRequest(request)) { els.offerList.append(emptyState(activeRole() === "rider" ? "Select a nearby ride request to see or update your offer." : "Select one of your ride requests to see rider offers.")); } else if (activeRole() === "passenger" && request.status !== "open") { els.offerList.append(emptyState("This ride is matched or closed, so rider proposals are no longer shown.")); } else if (!visibleOffers.length) { els.offerList.append(emptyState(activeRole() === "rider" ? "You have not sent an offer for this request yet." : "No rider offers yet. Waka keeps checking automatically while this request is open.")); } const riderMap = stateLookupIndexes().riderMap; if (selectedPassengerOffer) { const rider = riderMap.get(selectedPassengerOffer.riderId); const header = document.createElement("div"); header.className = "passenger-offer-detail-heading"; const copy = document.createElement("div"); copy.innerHTML = ` Respond to ${escapeHtml(firstNameOnly(rider?.name, "rider"))} ${escapeHtml(formatMoney(selectedPassengerOffer.fare, request?.country))} offer selected. Accept, counter higher, reject, or return to all rider offers. `; const back = document.createElement("button"); back.type = "button"; back.className = "ghost-action"; back.textContent = `Back to all proposals (${visibleOffers.length})`; back.addEventListener("click", returnPassengerToOfferList); header.append(copy, back); els.offerList.append(header); } offersToRender.forEach((offer) => { const rider = riderMap.get(offer.riderId); const expiredOffer = offerIsExpired(offer, request); const node = els.offerTemplate.content.firstElementChild.cloneNode(true); node.classList.toggle("passenger-negotiation-offer", isPassengerNegotiation); node.classList.toggle("passenger-offer-detail-card", Boolean(selectedPassengerOffer)); const riderOfferTrail = riderOfferFareTrail(offer); const riderOfferTrailText = riderOfferFareTrailText(offer, request); node.querySelector(".card-kicker").textContent = activeRole() === "rider" ? "My fare offers" : isPassengerNegotiation ? "Rider offer" : offer.type === "accepted" ? "Accepted passenger fare" : "Counter-offer"; node.querySelector("strong").textContent = activeRole() === "rider" ? `You offered ${riderOfferTrailText || formatMoney(offer.fare, request?.country)}` : `${firstNameOnly(rider?.name, "Rider")} asks ${formatMoney(offer.fare)}`; const pickupEta = offerDistanceChip(offer, request); node.querySelector("small").textContent = activeRole() === "rider" ? `Latest ${formatMoney(offer.fare, request?.country)} - ${formatDate(offer.createdAt)}` : isPassengerNegotiation ? [rider?.vehicle ?? "Vehicle", pickupEta].filter(Boolean).join(" - ") : `${rider?.vehicle ?? "Vehicle"} - live location used for matching - rating ${rider?.rating ?? "new"}`; const offerNote = node.querySelector("p"); offerNote.textContent = offer.note || (isPassengerNegotiation ? "" : "No extra note."); offerNote.hidden = (activeRole() === "rider" && !offer.note) || (isPassengerNegotiation && !offer.note); const offerChips = activeRole() === "rider" ? [ offer.type === "accepted" ? "Accepted passenger fare" : "Counter-offer", `${riderOfferTrail.length} amount${riderOfferTrail.length === 1 ? "" : "s"}`, pickupEta, offerExpiryChip(offer, request) ] : isPassengerNegotiation ? [ offerStatusChip(offer, request), offerFareDeltaChip(offer, request), offerExpiryChip(offer, request) ] : [ offerStatusChip(offer, request), isSubscriptionActive(rider) ? "Access active" : "Not eligible", offerFareDeltaChip(offer, request), offerExpiryChip(offer, request), pickupEta, "Phone hidden", formatDate(offer.createdAt) ]; node.querySelector(".chip-row").innerHTML = offerChips.filter(Boolean).map(chip).join(""); const choose = node.querySelector(".choose-offer"); choose.hidden = activeRole() !== "passenger"; choose.disabled = activeRole() !== "passenger" || request.status !== "open" || !requestBelongsToPassenger(request) || expiredOffer; choose.textContent = expiredOffer ? "Offer expired" : selectedPassengerOffer ? "Accept rider fare" : isPassengerNegotiation ? "Respond" : "Accept this offer"; choose.addEventListener("click", () => { if (isPassengerNegotiation && !selectedPassengerOffer) { openPassengerOfferResponse(offer); return; } chooseOffer(offer.id); }); if (activeRole() === "passenger" && requestBelongsToPassenger(request) && request.status === "open" && (!isPassengerNegotiation || selectedPassengerOffer)) { const actions = document.createElement("div"); actions.className = "offer-negotiation-actions"; const counterForm = document.createElement("form"); counterForm.className = "offer-counter-form"; const counterFloorFare = Number(request.fareOffer ?? 0); const minimumNextFare = Math.floor(counterFloorFare) + 1; const draftKey = passengerOfferCounterDraftKey(offer, request); const draftValue = passengerOfferCounterDrafts.get(draftKey) ?? ""; const passengerProposalRemaining = typeof passengerFareProposalAttemptCount === "function" ? fareProposalAttemptsRemaining(passengerFareProposalAttemptCount(request)) : fareProposalAttemptLimit; const passengerProposalLimitReached = passengerProposalRemaining <= 0; counterForm.innerHTML = `

${passengerProposalLimitReached ? escapeHtml(fareProposalLimitMessage("passenger", request)) : `${isPassengerNegotiation ? `Whole amount above ${escapeHtml(formatMoney(counterFloorFare, request.country))}.` : `Enter any whole-number fare higher than ${escapeHtml(formatMoney(counterFloorFare, request.country))}.`} ${passengerProposalRemaining} passenger proposal${passengerProposalRemaining === 1 ? "" : "s"} remaining.`}

`; const counterInput = counterForm.querySelector(".offer-counter-input"); counterInput?.addEventListener("focus", () => rememberPassengerOfferCounterFocus(counterInput)); counterInput?.addEventListener("pointerdown", () => rememberPassengerOfferCounterFocus(counterInput)); counterInput?.addEventListener("click", () => rememberPassengerOfferCounterFocus(counterInput)); counterInput?.addEventListener("input", () => { passengerOfferCounterDrafts.set(draftKey, counterInput.value); rememberPassengerOfferCounterFocus(counterInput); }); counterInput?.addEventListener("change", () => { passengerOfferCounterDrafts.set(draftKey, counterInput.value); rememberPassengerOfferCounterFocus(counterInput); }); counterForm.addEventListener("submit", (event) => passengerCounterRiderOffer(event, offer, request)); const reject = document.createElement("button"); reject.type = "button"; reject.className = "ghost-action danger"; reject.textContent = "Reject offer"; reject.addEventListener("click", () => rejectRiderOffer(offer)); actions.append(counterForm, reject); node.append(actions); } els.offerList.append(node); if (selectedPassengerOffer) { const backToProposals = document.createElement("button"); backToProposals.type = "button"; backToProposals.className = "secondary-action passenger-offer-detail-return"; backToProposals.textContent = `Back to all proposals (${visibleOffers.length})`; backToProposals.addEventListener("click", returnPassengerToOfferList); els.offerList.append(backToProposals); } }); els.offerCount.textContent = `${visibleOffers.length}`; restorePassengerOfferCounterFocus(counterFocus, els.offerList); } function renderOfferRequestContext(request) { if (!els.offerRequestContext) return; if (activeRole() !== "rider") { els.offerRequestContext.textContent = "Riders see selected request details here before sending a fare offer."; return; } if (!request || !roleCanSeeRequest(request)) { els.offerRequestContext.textContent = "Select a nearby request first."; return; } if (requestIsActiveForCurrentRider(request)) { const nextStop = request.status === "in_progress" ? `Destination: ${requestDestinationDisplayText(request)}` : `Pickup: ${riderActivePickupDisplayText(request)}`; els.offerRequestContext.textContent = `Matched to you. ${nextStop}.`; return; } const distance = proximityChip(request) ?? "Distance and pickup ETA not estimated"; const destinationDistance = destinationDriveChip(request) ?? "Trip distance to destination: estimating"; const stops = normalizeRideStops(request.rideStops); const stopsText = stops.length ? ` Planned stops: ${stops.length}.` : ""; const proposalText = requestIsNegotiableFare(request) && typeof riderFareProposalAttemptCount === "function" ? ` Rider proposals remaining: ${fareProposalAttemptsRemaining(riderFareProposalAttemptCount(request, currentRiderRecord()))}.` : ""; els.offerRequestContext.textContent = `${requestPickupTownText(request)} to ${requestDestinationDisplayText(request)}. ${destinationDistance}. ${fareModeChipText(request)}. Offer ${formatMoney(request.fareOffer)}. ${distance}.${stopsText}${proposalText}`; } function renderSelectedSummary() { if (activeRole() === "admin") { renderOfferRequestContext(null); els.selectedSummary.textContent = state.adminSession ? passengerUiText("adminMarketplaceSummary", "View passengers, riders, approvals, subscriptions, and marketplace activity") : passengerUiText("adminVisibilityRequired", "Admin sign-in required for full passenger and rider visibility"); return; } const request = selectedWorkspaceRequest(); if (!request || !roleCanSeeRequest(request)) { renderOfferRequestContext(null); els.selectedSummary.textContent = activeRole() === "rider" ? passengerUiText("marketplaceChooseNearbyRequest", "Marketplace: choose a nearby {vehicle} request to review or decline", { vehicle: currentRiderRecord()?.vehicle ?? passengerUiText("vehicle", "vehicle") }) : passengerUiText("publishOrSelectRideRequest", "Publish or select one of your ride requests"); return; } const approach = riderApproachChip(request); const isPassengerMatchedRide = activeRole() === "passenger" && requestBelongsToPassenger(request) && passengerActiveRideRequestStatuses().includes(request.status) && selectedRiderIdForRequest(request); const pickupText = activeRole() === "rider" && !requestIsActiveForCurrentRider(request) ? requestPickupTownText(request) : requestPickupDisplayText(request); if (isPassengerNegotiationRequest(request)) { const offerCount = offersForRequest(request.id).length; const selectedOffer = selectedPassengerNegotiationOffer(request, offersForRequest(request.id)); const selectedRider = selectedOffer ? stateLookupIndexes().riderMap.get(selectedOffer.riderId) : null; els.selectedSummary.textContent = selectedOffer ? passengerUiText("respondToRiderFareOffer", "Respond to {name} - {fare} offer", { name: firstNameOnly(selectedRider?.name, passengerUiText("rider", "rider")), fare: formatMoney(selectedOffer.fare, request.country) }) : offerCount ? passengerUiText("chooseRiderOfferCount", "Choose rider offer - {count} offer(s) available", { count: offerCount }) : passengerUiText("waitingForRiderOffers", "Waiting for rider offers"); } else if (isPassengerMatchedRide) { els.selectedSummary.textContent = rideStatusLabel(request); } else if (isPassengerNonNegotiableWaitingRequest(request)) { els.selectedSummary.textContent = passengerUiText("waitingForRiderOffersPassengerOffered", "Waiting for rider offers - passenger offered {fare}", { fare: formatMoney(request.fareOffer, request.country) }); } else if (activeRole() === "rider" && requestIsActiveForCurrentRider(request)) { els.selectedSummary.textContent = rideStatusLabel(request); } else { els.selectedSummary.textContent = passengerUiText("selectedRequestRouteSummary", "{pickup} to {destination} - {fareMode} - offer {fare} - {schedule}{approach}", { pickup: pickupText, destination: requestDestinationDisplayText(request), fareMode: fareModeChipText(request), fare: formatMoney(request.fareOffer), schedule: scheduleChip(request), approach: approach ? ` - ${approach}` : "" }); } if (els.counterFare) { const counterFloorFare = Number(request.fareOffer ?? 0); const minimumCounterFare = Math.floor(counterFloorFare) + 1; els.counterFare.min = String(minimumCounterFare); els.counterFare.step = "1"; els.counterFare.placeholder = `Higher than ${formatMoney(counterFloorFare, request.country)}`; } renderOfferRequestContext(request); } function renderSafetyReportForm(request) { const canReport = canReportOnRequest(request); const disabledReason = !request ? "Select a ride before contacting Waka." : activeRole() === "rider" ? "Contact Waka opens after the passenger chooses your offer." : "Contact Waka opens after choosing a rider."; const target = canReport ? reportTargetForRequest(request) : null; const reporterRole = activeRole() === "rider" ? "Rider" : "Passenger"; els.safetyReportForm.hidden = !canReport; els.safetyReportCategory.disabled = !canReport; els.safetyReportSeverity.disabled = !canReport; els.safetyReportDetails.disabled = !canReport; els.safetyReportForm.querySelector("button").disabled = !canReport; els.safetyReportStatus.textContent = canReport ? `${reporterRole} report about ${target.name}. Admin reviews safety, no-show, route, payment, identity, and behavior concerns. Use this to contact Waka for support too.` : disabledReason; } function renderRideRatingForm(request) { if (!els.rideRatingForm) return; const target = ratingTargetForRequest(request); const canRate = canRateRequest(request); const existing = existingRatingForRequest(request); const ratingControls = [ els.rideRatingScore, els.rideRatingSafety, els.rideRatingPunctuality, els.rideRatingCommunication, els.rideRatingVehicle, els.rideRatingComment ].filter(Boolean); els.rideRatingForm.hidden = !canRate && !existing; ratingControls.forEach((control) => { control.disabled = !canRate; }); els.rideRatingForm.querySelector("button").disabled = !canRate; if (existing) { const setScore = (element, value) => { if (element) element.value = String(value ?? existing.score ?? 5); }; setScore(els.rideRatingScore, existing.score); setScore(els.rideRatingSafety, existing.safetyScore); setScore(els.rideRatingPunctuality, existing.punctualityScore); setScore(els.rideRatingCommunication, existing.communicationScore); setScore(els.rideRatingVehicle, existing.vehicleScore); if (els.rideRatingComment) els.rideRatingComment.value = existing.comment ?? ""; } if (!request) { els.rideRatingStatus.textContent = "Ratings open after selecting a completed ride."; } else if (existing) { els.rideRatingStatus.textContent = "Rating already submitted for this ride."; } else if (canRate) { els.rideRatingStatus.textContent = `Rating for ${target.name}. Completed ride: ${requestPickupDisplayText(request)} to ${requestDestinationDisplayText(request)}.`; } else if (request?.status === "completed" && requestBelongsToPassenger(request) && !target) { els.rideRatingStatus.textContent = "Waka is loading the matched rider details before this rating can be submitted."; } else { els.rideRatingStatus.textContent = "Ratings open after the ride is marked complete."; } } function chatDeliveryLabel(message) { if (message.sender !== activeRole() || message.sender === "system") return ""; if (message.deliveryStatus === "sending") return "Sending"; if (message.deliveryStatus === "failed") return "Failed to send"; return "Sent"; } function restoreRideToolElementsToChatPanel() { if (!els.chatPanel) return; [els.chatThread, els.chatForm, els.safetyReportForm, els.rideRatingForm] .filter(Boolean) .forEach((element) => els.chatPanel.append(element)); } function placeRideDockElements(activePanel, contentSelector) { restoreRideToolElementsToChatPanel(); if (!els.rideActionPanel) return; const content = els.rideActionPanel.querySelector(contentSelector); if (!content) return; const moveToContent = (element) => { if (element) { content.hidden = false; content.append(element); } }; if (activePanel === "contact") { moveToContent(els.chatThread); moveToContent(els.chatForm); } else if (activePanel === "support") { moveToContent(els.safetyReportForm); } else if (activePanel === "rating") { moveToContent(els.rideRatingForm); } } function placePassengerRideDockElements(activePanel) { placeRideDockElements(activePanel, "[data-passenger-ride-tool-content]"); } function placeRiderRideDockElements(activePanel) { placeRideDockElements(activePanel, "[data-rider-ride-tool-content]"); } function riderRideStageAboveToolsHost() { if (!els.chatPanel) return null; let host = els.chatPanel.querySelector("[data-rider-ride-stage-above-tools]"); if (!host) { host = document.createElement("div"); host.className = "rider-ride-stage-above-tools"; host.dataset.riderRideStageAboveTools = "true"; } const heading = els.chatPanel.querySelector(".board-heading"); if (heading && host.nextElementSibling !== heading) { els.chatPanel.insertBefore(host, heading); } else if (!heading && !host.parentNode) { els.chatPanel.prepend(host); } return host; } function clearRiderRideStageAboveTools() { const host = els.chatPanel?.querySelector("[data-rider-ride-stage-above-tools]"); if (!host) return; host.innerHTML = ""; host.hidden = true; } function renderRiderRideStageAboveTools(request) { const host = riderRideStageAboveToolsHost(); if (!host) return; host.innerHTML = ""; renderRiderRideNextProgressAction(host, request); host.hidden = !host.children.length; } function renderChat() { const request = selectedWorkspaceRequest(); const chatFocus = chatInputFocusSnapshot(); const chatSubmitButton = els.chatSendButton || els.chatForm?.querySelector('button[type="submit"]'); if (typeof pruneInactiveRideChatsForViewer === "function" && pruneInactiveRideChatsForViewer()) { saveState(); } restoreRideToolElementsToChatPanel(); const isOpen = canChatOnRequest(request); const passengerDockMode = passengerRideDockMode(request); const passengerDockPanel = passengerDockMode ? passengerRideDockCurrentPanel(request) : null; const riderDockMode = riderRideDockMode(request); const riderDockPanel = riderDockMode ? riderRideDockCurrentPanel(request) : null; const rideDockMode = passengerDockMode || riderDockMode; const activeDockPanel = passengerDockMode ? passengerDockPanel : riderDockPanel; if (riderDockMode) renderRiderRideStageAboveTools(request); else clearRiderRideStageAboveTools(); const chatHeading = els.chatPanel?.querySelector(".board-heading h2"); if (chatHeading) chatHeading.textContent = rideDockMode ? passengerUiText("rideTools", "Ride tools") : passengerUiText("postSelectionChat", "Post-selection chat"); els.chatPanel?.classList.toggle("passenger-ride-tool-mode", rideDockMode); els.chatPanel?.classList.toggle("rider-ride-tool-mode", riderDockMode); if (isPassengerNegotiationRequest(request) && !passengerDockMode) { if (chatHeading) chatHeading.textContent = "Post-selection chat"; els.chatPanel?.classList.remove("passenger-ride-tool-mode"); els.chatPanel?.classList.remove("rider-ride-tool-mode"); clearRiderRideStageAboveTools(); els.chatStatus.textContent = "Locked"; els.chatInput.disabled = true; if (chatSubmitButton) chatSubmitButton.disabled = true; if (typeof updateChatVoiceButtonState === "function") updateChatVoiceButtonState({ disabled: true, recording: false }); if (typeof setChatVoiceStatus === "function") setChatVoiceStatus(""); els.chatInput.placeholder = "Chat opens after choosing a rider"; els.chatThread.innerHTML = ""; els.chatThread.hidden = false; els.chatForm.hidden = false; restoreRideToolElementsToChatPanel(); if (els.rideActionPanel) els.rideActionPanel.innerHTML = ""; renderSafetyReportForm(null); renderRideRatingForm(null); restoreRideToolElementsToChatPanel(); return; } els.chatStatus.textContent = rideDockMode ? rideStatusLabel(request) : isOpen ? "Open" : "Locked"; const showDockContact = !rideDockMode || activeDockPanel === "contact"; els.chatInput.disabled = !isOpen || !showDockContact; if (chatSubmitButton) chatSubmitButton.disabled = !isOpen || !showDockContact; if (typeof updateChatVoiceButtonState === "function") { const recordingVoice = typeof chatVoiceIsRecording === "function" && chatVoiceIsRecording(); updateChatVoiceButtonState({ disabled: (!isOpen || !showDockContact) && !recordingVoice, recording: recordingVoice }); } els.chatInput.placeholder = isOpen ? passengerUiText("writeToSelectedRiderOrPassenger", "Write to the selected rider or passenger") : passengerUiText("chatPlaceholder", "Chat opens only after passenger chooses a rider"); els.chatThread.innerHTML = ""; els.chatThread.hidden = !showDockContact; els.chatForm.hidden = !showDockContact; const lifecycleRequest = renderPersistentRideActions(request); renderSafetyReportForm(reportableRideForRole(request) ?? lifecycleRequest ?? request); renderRideRatingForm(lifecycleRequest ?? request); if (passengerDockMode) placePassengerRideDockElements(passengerDockPanel); else if (riderDockMode) placeRiderRideDockElements(riderDockPanel); else restoreRideToolElementsToChatPanel(); if (passengerDockMode) { if (els.safetyReportForm) els.safetyReportForm.hidden = passengerDockPanel !== "support" || els.safetyReportForm.hidden; if (els.rideRatingForm) els.rideRatingForm.hidden = passengerDockPanel !== "rating" || els.rideRatingForm.hidden; if (!showDockContact) return; } if (riderDockMode) { if (els.safetyReportForm) els.safetyReportForm.hidden = riderDockPanel !== "support" || els.safetyReportForm.hidden; if (els.rideRatingForm) els.rideRatingForm.hidden = true; if (!showDockContact) return; } if (!request || !roleCanSeeRequest(request)) { els.chatThread.append(emptyState(activeRole() === "rider" ? "Select a nearby request first." : "Select one of your requests first.")); return; } const messages = state.chats.filter((message) => message.requestId === request.id); if (!isOpen) { els.chatThread.append(emptyState(activeRole() === "rider" ? "Chat opens only if the passenger chooses your offer." : "Chat is locked until you choose a rider offer.")); return; } if (typeof appendContactActions === "function") { appendContactActions(els.chatThread, request); } if (!messages.length) { els.chatThread.append(emptyState(passengerUiText( "chatOpenConfirmPickupPayment", "Chat is open. Confirm pickup details and {payment} before the ride starts.", { payment: paymentLabel(request.paymentPreference).toLowerCase() } ))); restoreChatInputFocus(chatFocus, request, isOpen && showDockContact); return; } messages.forEach((message) => { const bubble = document.createElement("div"); const status = chatDeliveryLabel(message); bubble.className = `chat-bubble ${message.sender === activeRole() ? "self" : ""}${message.deliveryStatus === "failed" ? " failed" : ""}`; const text = document.createElement("span"); text.textContent = message.text || (isChatVoiceMessage(message) ? "Voice message" : ""); bubble.append(text); if (isChatVoiceMessage(message)) { const audio = document.createElement("audio"); audio.controls = true; audio.preload = "none"; audio.className = "chat-voice-player"; audio.setAttribute("aria-label", `Voice message ${chatVoiceDurationLabel(message.mediaDurationSeconds)}`); bubble.append(audio); void ensureChatVoiceAudioUrl(message, audio); } if (status) { const delivery = document.createElement("small"); delivery.textContent = status; bubble.append(delivery); } els.chatThread.append(bubble); }); restoreChatInputFocus(chatFocus, request, isOpen && showDockContact); } function offersForRequest(requestId) { return stateLookupIndexes().offersByRequestId.get(requestId) ?? []; } function selectRequest(id) { const previousRequestId = state.selectedRequestId; state.selectedRequestId = id; if (activeRole() === "passenger" && previousRequestId !== id) state.passengerSelectedOfferId = null; if (activeRole() === "rider") { clearRiderDecisionQueueForRequest(id); clearRequestMarketplaceFareChange(id); state.riderPage = "requests"; if (typeof updateRiderWorkspaceRoute === "function") updateRiderWorkspaceRoute("requests", { requestId: id }); } saveState(); renderAll(); if (activeRole() === "rider") { window.setTimeout(() => { document.querySelector("#riderRequestDetailPanel, #marketPanel") ?.scrollIntoView({ behavior: "smooth", block: "start" }); }, 0); } } function getCurrentGpsPoint(options = {}) { if (!navigator.geolocation) { return Promise.reject(new Error("GPS is not available in this browser.")); } return new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition( (position) => { const point = gpsPointFromPosition(position); if (!point) { reject(new Error("GPS returned an invalid location.")); return; } resolve(point); }, () => reject(new Error("GPS permission was denied or the location could not be found.")), { enableHighAccuracy: options.enableHighAccuracy !== false, timeout: Number.isFinite(Number(options.timeout)) ? Number(options.timeout) : 15000, maximumAge: Number.isFinite(Number(options.maximumAge)) ? Number(options.maximumAge) : 5000 } ); }); } function gpsPointAccuracyValue(point) { const accuracy = Number(point?.accuracyMeters); return Number.isFinite(accuracy) ? accuracy : Number.POSITIVE_INFINITY; } function clearerGpsPoint(firstPoint, secondPoint) { if (!firstPoint) return secondPoint ?? null; if (!secondPoint) return firstPoint; if (gpsPointAccuracyValue(secondPoint) < gpsPointAccuracyValue(firstPoint)) return secondPoint; return firstPoint; } function waitForGpsResample(ms) { return new Promise((resolve) => window.setTimeout(resolve, ms)); } async function getBestCurrentGpsPoint(options = {}) { const desiredAccuracyMeters = Number.isFinite(Number(options.desiredAccuracyMeters)) ? Math.max(1, Number(options.desiredAccuracyMeters)) : 75; const sampleCount = Math.max(1, Math.min(6, Math.round(Number(options.samples)) || 3)); const totalTimeoutMs = Math.max(5000, Number(options.totalTimeoutMs ?? options.timeoutMs) || 20000); const sampleTimeoutMs = Math.max(3000, Number(options.sampleTimeoutMs) || 8000); const startedAt = Date.now(); let bestPoint = null; let lastError = null; for (let index = 0; index < sampleCount; index += 1) { const remainingMs = totalTimeoutMs - (Date.now() - startedAt); if (remainingMs <= 0) break; try { const point = await getCurrentGpsPoint({ enableHighAccuracy: true, timeout: Math.max(3000, Math.min(sampleTimeoutMs, remainingMs)), maximumAge: 0 }); bestPoint = clearerGpsPoint(bestPoint, point); if (gpsPointAccuracyValue(bestPoint) <= desiredAccuracyMeters) break; } catch (error) { lastError = error; if (!bestPoint && index === sampleCount - 1) throw error; } if (index < sampleCount - 1 && Date.now() - startedAt < totalTimeoutMs) { await waitForGpsResample(600); } } if (bestPoint) return bestPoint; throw lastError ?? new Error("GPS permission was denied or the location could not be found."); } function passengerPickupAutoReady() { return autoPickupGpsEnabled() && Boolean(els.pickupUseCurrentLocation?.checked) && activeRole() === "passenger" && Boolean(state.passenger) && hasSignedIn("passenger") && els.rideRequestForm && !els.rideRequestForm.hidden; } function passengerWantsCurrentPickup() { return Boolean(els.pickupUseCurrentLocation?.checked); } function pickupAddressLooksTooBroadForPublish(value) { const text = String(value ?? "").replace(/\s+/g, " ").trim(); if (!text || /\d/.test(text)) return false; if (/\b(road|rd|street|st|avenue|ave|drive|dr|lane|ln|boulevard|blvd|court|ct|circle|cir|highway|hwy|way|place|pl|terrace|ter|pike|parkway|pkwy|route|rte)\b/i.test(text)) { return false; } return /,/.test(text) && /\b(united states|usa|cameroon|nigeria|ghana|canada|united kingdom|uk|maryland|district of columbia|virginia|washington dc)\b/i.test(text); } function exactPickupAddressIssue(value = els.pickupDescription?.value) { const text = String(value ?? "").replace(/\s+/g, " ").trim(); const manualFareOnly = typeof passengerManualFareOnly === "function" && passengerManualFareOnly(); const currentGps = normalizeGpsPoint(selectedCurrentPickupGps ?? pendingPickupGps); const currentGpsReady = passengerWantsCurrentPickup() && currentGps && !pickupGpsQualityIssue(currentGps); const vaguePickupPattern = /^(here|near me|my location|pickup location|same place|current location|verified gps pickup|capturing current location)$/i; if (currentGpsReady && (pickupUsesGpsFallbackText(text) || pickupUsesCurrentLocationText(text))) return ""; if (manualFareOnly) { if (!text || vaguePickupPattern.test(text)) { return passengerUiText( "pickupLandmarkRequired", "Enter the pickup address or a recognizable landmark before publishing." ); } if (text.length < 3) { return passengerUiText( "pickupLandmarkMinLength", "Enter at least 3 characters for the pickup address or landmark." ); } return ""; } if (pickupUsesGpsFallbackText(text) && currentGpsReady) return ""; if (pickupUsesGpsFallbackText(text)) { return passengerUiText( "confirmPickupAddressBeforePublish", "Enter the pickup address or a recognizable landmark before publishing." ); } if ((!text || text === "Capturing current location..." || pickupUsesCurrentLocationText(text)) && currentGpsReady) return ""; if (!text || text === "Capturing current location..." || pickupUsesCurrentLocationText(text)) { return passengerUiText( "confirmPickupAddressBeforePublish", "Enter the pickup address or a recognizable landmark before publishing." ); } if (text.length < 10) { return passengerUiText( "pickupLandmarkMinLength", "Enter at least 3 characters for the pickup address or landmark." ); } if (/^(here|near me|my location|pickup location|same place)$/i.test(text)) { return passengerUiText( "pickupLandmarkRequired", "Enter the pickup address or a recognizable landmark before publishing." ); } if (pickupAddressLooksTooBroadForPublish(text)) { return ""; } return ""; } function waitForPassengerUiPaintBeforeAlert() { return new Promise((resolve) => { if (typeof window.requestAnimationFrame === "function") { window.requestAnimationFrame(() => window.requestAnimationFrame(resolve)); return; } window.setTimeout(resolve, 0); }); } async function capturePassengerPickupGps(options = {}) { const automatic = Boolean(options.automatic); if (automatic && !passengerPickupAutoReady()) return pendingPickupGps; if (passengerPickupGpsPromise) return passengerPickupGpsPromise; try { if (els.pickupGpsStatus) { els.pickupGpsStatus.textContent = automatic ? "Refreshing exact pickup GPS..." : "Finding your clearest pickup GPS..."; } const desiredAccuracyMeters = typeof passengerPickupGpsAccuracyLimitMeters === "function" ? passengerPickupGpsAccuracyLimitMeters() : 50; passengerPickupGpsPromise = getBestCurrentGpsPoint({ desiredAccuracyMeters, samples: automatic ? 2 : 4, totalTimeoutMs: automatic ? 12000 : 22000, sampleTimeoutMs: 8000 }); pendingPickupGps = await passengerPickupGpsPromise; const qualityIssue = pickupGpsQualityIssue(pendingPickupGps); if (els.pickupGpsStatus) { els.pickupGpsStatus.textContent = qualityIssue ? qualityIssue : passengerPickupGpsReadyLabel(pendingPickupGps); } if (!qualityIssue) applyPassengerGpsLaunchContext(pendingPickupGps, { persist: true }); updateFareGuidance(); schedulePassengerNearbyRiderCountsRefresh(100); return pendingPickupGps; } catch (error) { pendingPickupGps = null; if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = error.message; updateFareGuidance(); return null; } finally { passengerPickupGpsPromise = null; } } async function captureTypedPickupPhoneGpsForMatching() { if (typeof window !== "undefined" && window.isSecureContext === false) { setPassengerUiStatus( els.pickupGpsStatus, "typedPickupPhoneGpsNeedsHttps", "Phone GPS needs a secure HTTPS page. Waka will publish using the selected pickup city/area and typed landmark instead." ); return null; } if (typeof navigator === "undefined" || !navigator.geolocation) { setPassengerUiStatus( els.pickupGpsStatus, "typedPickupPhoneGpsUnsupported", "This browser cannot share phone GPS. Waka will publish using the selected pickup city/area and typed landmark instead." ); return null; } if (passengerPickupGpsPromise) { const existingPoint = await passengerPickupGpsPromise.catch(() => null); if (existingPoint && !pickupGpsQualityIssue(existingPoint)) return existingPoint; } setPassengerUiStatus( els.pickupGpsStatus, "typedPickupPhoneGpsCapturing", "Capturing phone GPS so nearby riders can see this request..." ); try { const desiredAccuracyMeters = typeof passengerPickupGpsAccuracyLimitMeters === "function" ? passengerPickupGpsAccuracyLimitMeters() : 50; passengerPickupGpsPromise = getBestCurrentGpsPoint({ desiredAccuracyMeters, samples: 4, totalTimeoutMs: 22000, sampleTimeoutMs: 8000 }); const point = await passengerPickupGpsPromise; const issue = pickupGpsQualityIssue(point); if (issue) { if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = issue; setPassengerUiStatus( els.pickupPlaceStatus, "typedPickupPhoneGpsUnavailable", "Phone GPS could not be captured clearly. Waka will publish using the selected pickup city/area and typed landmark; riders may clarify by chat if needed." ); return null; } pendingPickupGps = normalizeGpsPoint(point); applyPassengerGpsLaunchContext(pendingPickupGps, { persist: true }); setPassengerUiStatus( els.pickupGpsStatus, "typedPickupPhoneGpsLinked", "Phone GPS linked for nearby rider matching. Riders will still see your typed pickup landmark." ); setPassengerUiStatus( els.pickupPlaceStatus, "typedPickupPhoneGpsLinked", "Phone GPS linked for nearby rider matching. Riders will still see your typed pickup landmark." ); updateFareGuidance(); schedulePassengerNearbyRiderCountsRefresh(100); return pendingPickupGps; } catch (error) { logClientWarning("Typed pickup phone GPS capture failed.", error); setPassengerUiStatus( els.pickupGpsStatus, "typedPickupPhoneGpsUnavailable", "Phone GPS could not be captured clearly. Waka will publish using the selected pickup city/area and typed landmark; riders may clarify by chat if needed." ); return null; } finally { passengerPickupGpsPromise = null; } } async function maybeAttachPhoneGpsToTypedPickupForMatching(shouldAsk) { if (!shouldAsk) return null; const message = passengerUiText( "typedPickupUsePhoneGpsPrompt", "This pickup was not matched to a saved GPS place. Use this phone's GPS only if this phone is physically at the pickup point, so nearby riders around that pickup can see the request. If you are booking for someone elsewhere, choose Cancel, then make sure the pickup city/area and landmark are correct. Waka will still show your typed pickup text and landmark to the rider." ); const ok = typeof showWakaGoodConfirm === "function" ? await showWakaGoodConfirm(message) : window.confirm(message); if (!ok) { setPassengerUiStatus( els.pickupPlaceStatus, "typedPickupPhoneGpsSkipped", "Phone GPS was not added. Waka will publish using the selected pickup city/area and typed landmark; riders may clarify by chat if needed." ); return null; } return captureTypedPickupPhoneGpsForMatching(); } async function ensurePassengerPickupGpsForPublish() { if (!passengerWantsCurrentPickup()) return null; if (!pendingPickupGps || pickupGpsQualityIssue(pendingPickupGps)) { await capturePassengerPickupGps({ automatic: false }); } return pendingPickupGps; } function clearPassengerPickupGps() { pendingPickupGps = null; selectedCurrentPickupGps = null; if (els.pickupUseCurrentLocation) els.pickupUseCurrentLocation.checked = false; if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = "Exact pickup location is off."; setPassengerRiderAvailabilityMessage("Enter a pickup address or check exact current location to see nearby rider availability."); updateFareGuidance(); } function applyPassengerGpsLaunchContext(point, options = {}) { const inferred = inferredLaunchLocationFromGps(selectedPassengerCountry(), point); if (!inferred) return null; const previousCity = state.passenger?.city ?? els.passengerActiveCity?.value ?? els.passengerCity?.value; const previousCountry = state.passenger?.country ?? selectedPassengerCountry(); const areaNames = areas(inferred.country, inferred.city).map((area) => area.name); const inferredArea = areaNames.includes(inferred.area) ? inferred.area : areaNames[0]; if (state.passenger) { state.passenger = { ...state.passenger, country: inferred.country, city: inferred.city }; state.passengers = upsertById(state.passengers, state.passenger); } if (els.passengerCountry) els.passengerCountry.value = inferred.country; if (els.passengerActiveCountry) els.passengerActiveCountry.value = inferred.country; if (els.passengerCity) els.passengerCity.value = inferred.city; if (els.passengerActiveCity) els.passengerActiveCity.value = inferred.city; if (els.pickupCity) els.pickupCity.value = inferred.city; if (els.pickupArea && areaNames.length) populateSelect(els.pickupArea, areaNames, inferredArea); if (els.destinationArea && areaNames.length) { const currentDestinationArea = els.destinationArea.value; populateSelect(els.destinationArea, areaNames, areaNames.includes(currentDestinationArea) ? currentDestinationArea : areaNames[0]); } if (els.passengerLocationStatus) { const areaText = inferredArea ? ` near ${inferredArea}` : ""; els.passengerLocationStatus.textContent = `GPS places this request in ${inferred.city}${areaText}. Suggestions and nearby riders are narrowed to that city.`; } saveState(); if (options.persist && state.passenger?.id && (previousCity !== inferred.city || previousCountry !== inferred.country)) { void updatePassengerCurrentCityInSupabase(state.passenger.supabaseUserId ?? state.passenger.id, inferred.country, inferred.city) .catch((error) => logClientWarning("Passenger GPS city persistence failed.", error)); } return inferred; } function stopAutomaticPassengerPickupGps() { if (passengerPickupGpsWatchId != null && navigator.geolocation?.clearWatch) { navigator.geolocation.clearWatch(passengerPickupGpsWatchId); } passengerPickupGpsWatchId = null; passengerPickupGpsWatchStartedAt = 0; window.clearTimeout(passengerPickupGpsWatchFallbackTimer); passengerPickupGpsWatchFallbackTimer = null; } function ensurePassengerPickupGpsAutoCapture() { if (!passengerPickupAutoReady()) { stopAutomaticPassengerPickupGps(); return; } if (!navigator.geolocation) { if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = "GPS is not available in this browser."; return; } if (passengerPickupGpsWatchId != null) { if (!pendingPickupGps && els.pickupGpsStatus && passengerPickupGpsWatchStartedAt && Date.now() - passengerPickupGpsWatchStartedAt > 17000) { els.pickupGpsStatus.textContent = "Exact pickup location has not returned yet. Allow Location or type the pickup address."; } return; } if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = "Capturing exact pickup location..."; passengerPickupGpsWatchStartedAt = Date.now(); window.clearTimeout(passengerPickupGpsWatchFallbackTimer); passengerPickupGpsWatchFallbackTimer = window.setTimeout(() => { if (!pendingPickupGps && els.pickupGpsStatus && passengerPickupGpsWatchId != null) { els.pickupGpsStatus.textContent = "Exact pickup location has not returned yet. Allow Location or type the pickup address."; } }, 17000); passengerPickupGpsWatchId = navigator.geolocation.watchPosition( (position) => { const point = gpsPointFromPosition(position); if (!point) return; pendingPickupGps = point; window.clearTimeout(passengerPickupGpsWatchFallbackTimer); passengerPickupGpsWatchFallbackTimer = null; const qualityIssue = pickupGpsQualityIssue(point); if (els.pickupGpsStatus) { els.pickupGpsStatus.textContent = qualityIssue ? qualityIssue : passengerPickupGpsReadyLabel(point); } updateFareGuidance(); schedulePassengerNearbyRiderCountsRefresh(100); }, (error) => { const denied = Number(error?.code) === 1; if (els.pickupGpsStatus) { els.pickupGpsStatus.textContent = denied ? "GPS permission is blocked. Tap the browser location icon to allow it, or type the pickup address." : "Exact pickup location could not refresh. Type the pickup address or try again."; } stopAutomaticPassengerPickupGps(); }, { enableHighAccuracy: true, timeout: 15000, maximumAge: 5000 } ); if (!pendingPickupGps || pickupGpsQualityIssue(pendingPickupGps)) { void capturePassengerPickupGps({ automatic: true }); } } function destinationAutocompleteReady() { return placesAutocompleteEnabled() && hasSupabaseRuntime() && Boolean(state.passenger) && hasSignedIn("passenger"); } function pickupAutocompleteReady() { return destinationAutocompleteReady(); } function addressSearchLimitsRelaxedForTesting() { return configFlagEnabled(appConfig.relaxAddressSearchLimitsForTesting) && /\b(staging|pilot|test|preview)\b/i.test(String(appConfig.projectName || "")); } function addressSearchRateLimitPauseLimitMs() { return addressSearchLimitsRelaxedForTesting() ? testingAddressSearchRateLimitPauseMs : addressSearchRateLimitPauseMs; } function pickupSessionToken() { if (!pickupAutocompleteSessionToken) { pickupAutocompleteSessionToken = makeId("place-session"); } return pickupAutocompleteSessionToken; } function destinationSessionToken() { if (!destinationAutocompleteSessionToken) { destinationAutocompleteSessionToken = makeId("place-session"); } return destinationAutocompleteSessionToken; } function rideStopSessionToken() { if (!rideStopAutocompleteSessionToken) { rideStopAutocompleteSessionToken = makeId("place-session"); } return rideStopAutocompleteSessionToken; } function clearPickupPlaceSelection(message = "") { selectedPickupPlace = null; if (!pickupUsesCurrentLocationText(els.pickupDescription?.value) && !pickupUsesGpsFallbackText(els.pickupDescription?.value)) selectedCurrentPickupGps = null; if (els.pickupPlaceStatus && message) els.pickupPlaceStatus.textContent = message; } function clearDestinationPlaceSelection(message = "") { selectedDestinationPlace = null; if (els.destinationPlaceStatus && message) els.destinationPlaceStatus.textContent = message; } function passengerFacingPlaceErrorMessage(error, fallback) { const message = String(error?.message || "").trim(); if (!message) return fallback; if (/edge function returned a non-2xx status code/i.test(message)) return fallback; if (/api is not activated|enable this api|google cloud console|gmp-get-started|geocoding api|google reverse geocoding|google maps server key/i.test(message)) { return "Pickup street-address lookup is not enabled for Waka's map service yet. Type the full pickup address for now, or try current location again after address lookup is enabled."; } return message .replace(/destination searches/gi, "address searches") .replace(/destination search/gi, "address search"); } function hideDestinationSuggestions() { if (!els.destinationSuggestions) return; els.destinationSuggestions.hidden = true; els.destinationSuggestions.replaceChildren(); } function hideRideStopSuggestions() { hideInlinePlaceSuggestions(els.rideStopSuggestions); } function hidePickupSuggestions() { if (!els.pickupSuggestions) return; els.pickupSuggestions.hidden = true; els.pickupSuggestions.replaceChildren(); } function recentAddressStorageKey(type) { const passengerId = state.passenger?.id || state.sessions.passenger?.userId || state.sessions.passenger?.email || "guest"; return `${storageKey}:recent-${type}:${passengerId}`; } function recentAddressStorageDisabled() { return hasSupabaseRuntime() || strictProductionModeEnabled(); } function clearRecentAddressHistory() { try { Object.keys(localStorage) .filter((key) => key.startsWith(`${storageKey}:recent-`)) .forEach((key) => localStorage.removeItem(key)); } catch { // Address history is a convenience only; production privacy wins. } } function readRecentAddresses(type) { if (recentAddressStorageDisabled()) { clearRecentAddressHistory(); return []; } try { const value = JSON.parse(localStorage.getItem(recentAddressStorageKey(type))); if (!Array.isArray(value)) return []; const items = value .filter((item) => item?.address) .filter((item) => !pickupUsesGpsFallbackText(item.address) && !pickupUsesGpsFallbackText(item.displayName)) .slice(0, 8); if (items.length !== value.length) saveRecentAddresses(type, items); return items; } catch { return []; } } function saveRecentAddresses(type, addresses = []) { if (recentAddressStorageDisabled()) { clearRecentAddressHistory(); return; } try { localStorage.setItem(recentAddressStorageKey(type), JSON.stringify(addresses.slice(0, 8))); } catch { // Address history is a convenience only; ride publishing must not depend on storage. } } function rememberRecentAddress(type, place) { if (recentAddressStorageDisabled()) return; const address = String(place?.formattedAddress || place?.address || "").trim(); if (!address || pickupUsesCurrentLocationText(address) || pickupUsesGpsFallbackText(address)) return; const entry = { address, displayName: String(place?.displayName || address).trim(), placeId: place?.placeId ?? null, latitude: Number.isFinite(Number(place?.latitude)) ? Number(place.latitude) : null, longitude: Number.isFinite(Number(place?.longitude)) ? Number(place.longitude) : null, savedAt: new Date().toISOString() }; const existing = readRecentAddresses(type) .filter((item) => String(item.address).trim().toLowerCase() !== address.toLowerCase()); saveRecentAddresses(type, [entry, ...existing]); } function rememberRideRouteAddresses(request) { if (request?.pickupDescription && !pickupUsesGpsFallbackText(request.pickupDescription)) { rememberRecentAddress("pickup", { address: request.pickupDescription, displayName: request.pickupDescription, latitude: request.pickupLatitude, longitude: request.pickupLongitude }); } if (request?.destination) { rememberRecentAddress("destination", { address: request.destinationFormattedAddress || request.destination, displayName: request.destination, placeId: request.destinationPlaceId, latitude: request.destinationLatitude, longitude: request.destinationLongitude }); } } function normalizeRecentAddressText(value) { return String(value ?? "").replace(/\s+/g, " ").trim().toLowerCase(); } function recentAddressStillSelected(type, item) { const value = type === "pickup" ? els.pickupDescription?.value : els.destination?.value; const normalizedValue = normalizeRecentAddressText(value); return Boolean(normalizedValue) && [item?.address, item?.displayName].some((label) => normalizeRecentAddressText(label) === normalizedValue); } function bestRecentAddressSuggestion(suggestions = [], item) { const normalizedAddress = normalizeRecentAddressText(item?.address); const normalizedDisplay = normalizeRecentAddressText(item?.displayName); return suggestions.find((suggestion) => { const text = normalizeRecentAddressText(suggestion?.text); const main = normalizeRecentAddressText(suggestion?.mainText); return text === normalizedAddress || text === normalizedDisplay || (normalizedAddress && text.includes(normalizedAddress)) || (normalizedDisplay && text.includes(normalizedDisplay)) || (normalizedDisplay && main === normalizedDisplay); }) ?? suggestions[0] ?? null; } async function confirmRecentAddressPlace(type, item) { if (!destinationAutocompleteReady()) return null; const input = String(item?.address || item?.displayName || "").trim(); if (input.length < 3) return null; const country = state.passenger?.country ?? selectedPassengerCountry(); const city = typeof selectedRidePickupCity === "function" ? selectedRidePickupCity() : state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(country); const payload = await fetchPlaceAutocomplete({ action: "autocomplete", input, sessionToken: type === "pickup" ? pickupSessionToken() : destinationSessionToken(), country, city }); const suggestion = bestRecentAddressSuggestion(payload?.suggestions ?? [], item); if (!suggestion?.placeId) return null; const details = type === "pickup" ? await fetchPickupPlaceDetails(suggestion) : await fetchDestinationPlaceDetails(suggestion); const place = normalizedPlaceSelection({ ...details?.place, displayName: details?.place?.displayName || suggestion.mainText || suggestion.text }); return place?.placeId ? place : null; } function trackRecentAddressConfirmation(promise) { if (!promise?.finally) return promise; recentAddressConfirmationPromises.add(promise); promise.finally(() => recentAddressConfirmationPromises.delete(promise)); return promise; } async function waitForRecentAddressConfirmations() { const pending = [...recentAddressConfirmationPromises]; if (!pending.length) return; if (els.fareGuidance) els.fareGuidance.textContent = "Finishing recent address confirmation..."; await Promise.allSettled(pending); } async function applyRecentAddress(type, item) { const place = normalizedPlaceSelection({ placeId: item.placeId ?? null, displayName: item.displayName || item.address, formattedAddress: item.address, latitude: item.latitude, longitude: item.longitude }); clearLowFareReview(); if (type === "pickup") { if (els.pickupUseCurrentLocation) els.pickupUseCurrentLocation.checked = false; selectedCurrentPickupGps = null; selectedPickupPlace = place; els.pickupDescription.value = item.address; hidePickupSuggestions(); if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = `Confirming recent pickup: ${item.displayName || item.address}`; } else { selectedDestinationPlace = place; els.destination.value = item.address; hideDestinationSuggestions(); if (els.destinationPlaceStatus) els.destinationPlaceStatus.textContent = `Confirming recent destination: ${item.displayName || item.address}`; } renderRecentAddressShortcuts(); if (els.fareGuidance) els.fareGuidance.textContent = "Confirming recent address for fare estimate..."; try { const confirmedPlace = place?.placeId ? place : await confirmRecentAddressPlace(type, item); if (confirmedPlace && recentAddressStillSelected(type, item)) { rememberRecentAddress(type, confirmedPlace); if (type === "pickup") { selectedPickupPlace = confirmedPlace; els.pickupDescription.value = confirmedPlace.formattedAddress || confirmedPlace.displayName || item.address; if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = `Selected recent pickup: ${confirmedPlace.displayName || confirmedPlace.formattedAddress}`; } else { selectedDestinationPlace = confirmedPlace; els.destination.value = confirmedPlace.formattedAddress || confirmedPlace.displayName || item.address; if (els.destinationPlaceStatus) els.destinationPlaceStatus.textContent = `Selected recent destination: ${confirmedPlace.displayName || confirmedPlace.formattedAddress}`; } } else if (type === "pickup") { if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = `Selected recent pickup: ${item.displayName || item.address}`; } else if (els.destinationPlaceStatus) { els.destinationPlaceStatus.textContent = `Selected recent destination: ${item.displayName || item.address}`; } } catch (error) { if (type === "pickup" && els.pickupPlaceStatus) { els.pickupPlaceStatus.textContent = passengerFacingPlaceErrorMessage( error, "Selected recent pickup. Fare will use the saved address text." ); } else if (type === "destination" && els.destinationPlaceStatus) { els.destinationPlaceStatus.textContent = passengerFacingPlaceErrorMessage( error, "Selected recent destination. Fare will use the saved address text." ); } } updateFareGuidance(); if (type === "pickup") schedulePassengerNearbyRiderCountsRefresh(100); } function renderRecentAddressList(container, type, currentValue) { if (!container) return; container.replaceChildren(); const value = String(currentValue || "").trim(); const items = readRecentAddresses(type) .filter((item) => item?.address && item.address !== value) .slice(0, 4); if (!state.passenger || !hasSignedIn("passenger") || value || !items.length) { container.hidden = true; return; } for (const item of items) { const button = document.createElement("button"); button.type = "button"; button.className = "recent-address-button"; button.textContent = item.displayName || item.address; button.title = item.address; button.addEventListener("click", () => trackRecentAddressConfirmation(applyRecentAddress(type, item))); container.append(button); } container.hidden = false; } function renderRecentAddressShortcuts() { clearStaleGpsPickupFallbackText({ status: false }); renderRecentAddressList(els.pickupHistory, "pickup", els.pickupDescription?.value); renderRecentAddressList(els.destinationHistory, "destination", els.destination?.value); } function clearStaleGpsPickupFallbackText({ status = true } = {}) { if (!els.pickupDescription || !pickupUsesGpsFallbackText(els.pickupDescription.value)) return false; els.pickupDescription.value = ""; selectedPickupPlace = null; hidePickupSuggestions(); if (status && els.pickupPlaceStatus) { els.pickupPlaceStatus.textContent = "Waka cleared an old GPS-coordinate pickup label. Use current location again to find the street address, or type the full pickup address."; } updateFareGuidance(); return true; } async function fetchPlaceAutocomplete(body) { if (!destinationAutocompleteReady()) throw new Error("Destination autocomplete needs passenger sign-in and Supabase."); const action = String(body?.action || "").toLowerCase(); const cacheKey = action === "autocomplete" ? JSON.stringify({ action, input: String(body.input || "").trim().toLowerCase(), country: String(body.country || "").trim().toLowerCase(), city: String(body.city || "").trim().toLowerCase() }) : ""; if (cacheKey && placeAutocompleteCache.has(cacheKey)) { return placeAutocompleteCache.get(cacheKey); } const localPauseMs = addressSearchRateLimitPauseLimitMs(); if (action === "autocomplete" && localPauseMs > 0 && Date.now() < placesAutocompleteRateLimitedUntil) { throw new Error("Address search is temporarily paused for this account. Enter the full address manually."); } const functionName = placesAutocompleteFunctionName(); const token = await currentSupabaseAccessToken(); if (!token) throw new Error("Passenger sign-in is required for destination suggestions."); const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${functionName}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify(body) }), "Fetching destination suggestions", optionalSupabaseRequestTimeoutMs ); const payload = await response.json().catch(() => null); if (!response.ok) { const message = payload?.error || "Address autocomplete failed."; if (/too many|limit reached|rate limit/i.test(message)) { placesAutocompleteRateLimitedUntil = localPauseMs > 0 ? Date.now() + localPauseMs : 0; } throw new Error(message); } if (cacheKey) { placeAutocompleteCache.set(cacheKey, payload); while (placeAutocompleteCache.size > 80) { const oldestKey = placeAutocompleteCache.keys().next().value; if (!oldestKey) break; placeAutocompleteCache.delete(oldestKey); } } return payload; } async function fetchPlaceAutocompleteWithCameroonFallback(body) { const payload = await fetchPlaceAutocomplete(body); const suggestions = payload?.suggestions ?? []; const shouldSearchCountryWide = String(body?.action || "").toLowerCase() === "autocomplete" && String(body?.country || "").trim().toLowerCase() === "cameroon" && String(appConfig.placesAutocompleteProvider || "").trim().toLowerCase() === "cameroon-local" && String(body?.city || "").trim() && suggestions.length === 0; if (!shouldSearchCountryWide) return payload; const fallbackPayload = await fetchPlaceAutocomplete({ ...body, city: "" }); return { ...fallbackPayload, cityFallback: true }; } function destinationPlaceDetailsCacheKey(placeId) { return String(placeId ?? "").trim(); } function rememberDestinationPlaceDetails(placeId, payload) { const key = destinationPlaceDetailsCacheKey(placeId); if (!key || !payload) return; if (destinationPlaceDetailsCache.has(key)) destinationPlaceDetailsCache.delete(key); destinationPlaceDetailsCache.set(key, payload); while (destinationPlaceDetailsCache.size > placeDetailsCacheLimit) { const oldestKey = destinationPlaceDetailsCache.keys().next().value; if (!oldestKey) break; destinationPlaceDetailsCache.delete(oldestKey); } } async function fetchDestinationPlaceDetails(suggestion) { const placeId = destinationPlaceDetailsCacheKey(suggestion?.placeId); if (!placeId) throw new Error("Selected destination did not include a place id."); const cached = destinationPlaceDetailsCache.get(placeId); if (cached) return cached; const payload = await fetchPlaceAutocomplete({ action: "details", placeId, sessionToken: destinationSessionToken() }); rememberDestinationPlaceDetails(placeId, payload); return payload; } async function fetchPickupPlaceDetails(suggestion) { const placeId = destinationPlaceDetailsCacheKey(suggestion?.placeId); if (!placeId) throw new Error("Selected pickup did not include a place id."); const cached = destinationPlaceDetailsCache.get(placeId); if (cached) return cached; const payload = await fetchPlaceAutocomplete({ action: "details", placeId, sessionToken: pickupSessionToken() }); rememberDestinationPlaceDetails(placeId, payload); return payload; } async function fetchPickupReverseGeocode(point) { const gps = normalizeGpsPoint(point); if (!gps) throw new Error("A valid GPS location is required."); return fetchPlaceAutocomplete({ action: "reverse-geocode", latitude: gps.latitude, longitude: gps.longitude, country: state.passenger?.country ?? selectedPassengerCountry(), city: typeof selectedRidePickupCity === "function" ? selectedRidePickupCity() : state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(selectedPassengerCountry()) }); } function renderPickupSuggestions(suggestions = []) { if (!els.pickupSuggestions) return; els.pickupSuggestions.replaceChildren(); const cleanSuggestions = suggestions.filter((suggestion) => suggestion?.placeId && suggestion?.text); if (!cleanSuggestions.length) { els.pickupSuggestions.hidden = true; return; } for (const suggestion of cleanSuggestions) { const button = document.createElement("button"); button.type = "button"; button.className = "place-suggestion"; button.setAttribute("role", "option"); const main = document.createElement("span"); main.className = "place-suggestion-main"; main.textContent = suggestion.mainText || suggestion.text; const secondary = document.createElement("span"); secondary.className = "place-suggestion-secondary"; secondary.textContent = suggestion.secondaryText || ""; button.append(main, secondary); wirePlaceSuggestionButton(button, () => selectPickupSuggestion(suggestion)); els.pickupSuggestions.append(button); } els.pickupSuggestions.hidden = false; } function renderDestinationSuggestions(suggestions = []) { if (!els.destinationSuggestions) return; els.destinationSuggestions.replaceChildren(); const cleanSuggestions = suggestions.filter((suggestion) => suggestion?.placeId && suggestion?.text); if (!cleanSuggestions.length) { els.destinationSuggestions.hidden = true; return; } for (const suggestion of cleanSuggestions) { const button = document.createElement("button"); button.type = "button"; button.className = "place-suggestion"; button.setAttribute("role", "option"); const main = document.createElement("span"); main.className = "place-suggestion-main"; main.textContent = suggestion.mainText || suggestion.text; const secondary = document.createElement("span"); secondary.className = "place-suggestion-secondary"; secondary.textContent = suggestion.secondaryText || ""; button.append(main, secondary); wirePlaceSuggestionButton(button, () => selectDestinationSuggestion(suggestion)); els.destinationSuggestions.append(button); } els.destinationSuggestions.hidden = false; } function wirePlaceSuggestionButton(button, handler) { let handled = false; const choose = (event) => { event.preventDefault(); event.stopPropagation(); if (handled) return; handled = true; handler(); }; button.addEventListener("pointerdown", choose); button.addEventListener("mousedown", choose); button.addEventListener("click", choose); } function hideInlinePlaceSuggestions(container) { if (!container) return; container.hidden = true; container.replaceChildren(); } function renderInlinePlaceSuggestions(container, suggestions = [], onSelect) { if (!container) return; container.replaceChildren(); const cleanSuggestions = suggestions.filter((suggestion) => suggestion?.placeId && suggestion?.text); if (!cleanSuggestions.length) { container.hidden = true; return; } for (const suggestion of cleanSuggestions) { const button = document.createElement("button"); button.type = "button"; button.className = "place-suggestion"; button.setAttribute("role", "option"); const main = document.createElement("span"); main.className = "place-suggestion-main"; main.textContent = suggestion.mainText || suggestion.text; const secondary = document.createElement("span"); secondary.className = "place-suggestion-secondary"; secondary.textContent = suggestion.secondaryText || ""; button.append(main, secondary); wirePlaceSuggestionButton(button, () => onSelect(suggestion)); container.append(button); } container.hidden = false; } function destinationUpdateStatus(form, message) { const status = form.querySelector(".destination-update-status"); if (status && message) status.textContent = message; } function activeStopLineInfo(textarea) { const value = textarea?.value ?? ""; const cursor = textarea?.selectionStart ?? value.length; const lineStart = value.lastIndexOf("\n", Math.max(0, cursor - 1)) + 1; const nextBreak = value.indexOf("\n", cursor); const lineEnd = nextBreak === -1 ? value.length : nextBreak; return { lineStart, lineEnd, text: value.slice(lineStart, lineEnd).trim() }; } function replaceActiveStopLine(textarea, nextValue) { if (!textarea) return; const { lineStart, lineEnd } = activeStopLineInfo(textarea); const replacement = String(nextValue ?? "").trim(); textarea.value = `${textarea.value.slice(0, lineStart)}${replacement}${textarea.value.slice(lineEnd)}`; const cursor = lineStart + replacement.length; textarea.setSelectionRange(cursor, cursor); } function rideStopsStatusText(stops = rideStopsFormValue()) { const normalized = normalizeRideStops(stops); if (!normalized.length) { return rideStopsInputEnabled() ? "Type one stop address per line. Waka will route stops in the order shown." : "Stops are optional. Use + Add stop only when the ride needs stops before the final destination."; } if (normalized.length >= rideStopsMaxCount) { return `${normalized.length} stops added. Maximum reached; Waka will price the route in this stop order.`; } const remaining = rideStopsMaxCount - normalized.length; return `${normalized.length} stop${normalized.length === 1 ? "" : "s"} added. Press Enter to add the next stop; ${remaining} more allowed.`; } function setRideStopsInputEnabled(enabled, { focus = false } = {}) { if (!els.rideStops) return; const nextEnabled = Boolean(enabled); els.rideStops.dataset.enabled = nextEnabled ? "true" : "false"; els.rideStops.disabled = !nextEnabled; setRideRequestOptionPanel("stops", nextEnabled); if (els.rideStopsField) els.rideStopsField.hidden = !nextEnabled; if (els.clearRideStops) els.clearRideStops.hidden = !nextEnabled; if (els.addRideStop) els.addRideStop.setAttribute("aria-expanded", String(nextEnabled)); if (!nextEnabled) { els.rideStops.value = ""; hideRideStopSuggestions(); } if (els.rideStopsStatus) els.rideStopsStatus.textContent = rideStopsStatusText(); if (focus && nextEnabled) { window.setTimeout(() => { els.rideStops.focus(); const end = els.rideStops.value.length; els.rideStops.setSelectionRange(end, end); }, 0); } } function initializeRideStopsInput() { setRideStopsInputEnabled(Boolean(normalizeRideStops(els.rideStops?.value).length)); } function handleAddRideStop(event) { event?.preventDefault?.(); if (!rideStopsInputEnabled()) { setRideStopsInputEnabled(true, { focus: true }); return; } const stops = normalizeRideStops(els.rideStops.value); if (stops.length >= rideStopsMaxCount) { if (els.rideStopsStatus) els.rideStopsStatus.textContent = rideStopsStatusText(); els.rideStops.focus(); return; } if (els.rideStops.value.trim() && !els.rideStops.value.endsWith("\n")) { els.rideStops.value = `${els.rideStops.value.trimEnd()}\n`; } if (els.rideStopsStatus) els.rideStopsStatus.textContent = "Type the next stop address. Stops stay in this line-by-line order."; els.rideStops.focus(); const end = els.rideStops.value.length; els.rideStops.setSelectionRange(end, end); } function clearRideStopsInput(event) { event?.preventDefault?.(); setRideStopsInputEnabled(false); clearLowFareReview(); updateFareGuidance(); } function handleRideStopsInput() { if (!rideStopsInputEnabled() && normalizeRideStops(els.rideStops?.value).length) { setRideStopsInputEnabled(true); } clearLowFareReview(); if (els.rideStopsStatus) els.rideStopsStatus.textContent = rideStopsStatusText(); updateFareGuidance(); scheduleRideStopAutocomplete(); } async function choosePrePublishStopSuggestion(suggestion) { try { if (els.rideStopsStatus) els.rideStopsStatus.textContent = "Confirming stop address..."; const payload = await fetchDestinationPlaceDetails(suggestion); const place = normalizedPlaceSelection({ ...payload?.place, displayName: payload?.place?.displayName || suggestion.mainText || suggestion.text }); if (!place?.placeId) throw new Error("Selected stop did not return a place id."); replaceActiveStopLine(els.rideStops, place.formattedAddress || place.displayName || suggestion.text); rememberSelectedStopPlace(place); rememberRecentAddress("destination", place); hideRideStopSuggestions(); rideStopAutocompleteSessionToken = null; if (els.rideStopsStatus) els.rideStopsStatus.textContent = `Selected stop: ${place.displayName || place.formattedAddress}. ${rideStopsStatusText()}`; renderRecentAddressShortcuts(); clearLowFareReview(); updateFareGuidance(); } catch (error) { if (els.rideStopsStatus) { els.rideStopsStatus.textContent = passengerFacingPlaceErrorMessage( error, "Could not confirm that stop suggestion. Enter the full stop address." ); } } } function scheduleRideStopAutocomplete() { window.clearTimeout(rideStopAutocompleteTimer); if (!els.rideStops) return; if (!rideStopsInputEnabled()) { hideRideStopSuggestions(); if (els.rideStopsStatus) els.rideStopsStatus.textContent = rideStopsStatusText(); return; } if (!destinationAutocompleteReady()) { hideRideStopSuggestions(); if (els.rideStopsStatus && placesAutocompleteEnabled()) { els.rideStopsStatus.textContent = "Sign in as a passenger to use stop suggestions."; } return; } const input = activeStopLineInfo(els.rideStops).text; if (input.length < 3) { hideRideStopSuggestions(); if (els.rideStopsStatus) els.rideStopsStatus.textContent = rideStopsStatusText(); return; } const requestId = ++rideStopAutocompleteRequestId; rideStopAutocompleteTimer = window.setTimeout(async () => { try { if (els.rideStopsStatus) els.rideStopsStatus.textContent = "Searching stop addresses..."; const payload = await fetchPlaceAutocomplete({ action: "autocomplete", input, sessionToken: rideStopSessionToken(), country: state.passenger?.country ?? selectedPassengerCountry(), city: typeof selectedRidePickupCity === "function" ? selectedRidePickupCity() : state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(selectedPassengerCountry()) }); if (requestId !== rideStopAutocompleteRequestId) return; renderInlinePlaceSuggestions(els.rideStopSuggestions, payload?.suggestions ?? [], choosePrePublishStopSuggestion); if (els.rideStopsStatus) { els.rideStopsStatus.textContent = (payload?.suggestions ?? []).length ? "Choose the matching stop from the suggestions." : "No stop suggestion found; continue with the full address."; } } catch (error) { hideRideStopSuggestions(); if (els.rideStopsStatus) { els.rideStopsStatus.textContent = passengerFacingPlaceErrorMessage( error, "Stop suggestions are unavailable right now. Enter the full stop address." ); } } }, 350); } function setupDestinationUpdateAutocomplete(form, request) { const destinationInput = form.querySelector(".destination-update-input"); const destinationSuggestions = form.querySelector(".destination-update-suggestions"); const stopsInput = form.querySelector(".stops-update-input"); const stopSuggestions = form.querySelector(".stops-update-suggestions"); const country = request?.country ?? state.passenger?.country ?? selectedPassengerCountry(); const city = request?.city ?? state.passenger?.city ?? defaultLaunchCity(country); let destinationTimer = null; let destinationRequestId = 0; let stopTimer = null; let stopRequestId = 0; const draft = passengerDestinationUpdateDraftForRequest(request); const currentDestinationPlace = normalizedPlaceSelection({ placeId: request?.destinationPlaceId, displayName: request?.destination, formattedAddress: request?.destinationFormattedAddress || request?.destination, latitude: request?.destinationLatitude, longitude: request?.destinationLongitude }); form.__destinationUpdatePlace = destinationPlaceMatchesInput(draft.destinationPlace, destinationInput.value) ? draft.destinationPlace : destinationPlaceMatchesInput(currentDestinationPlace, destinationInput.value) ? currentDestinationPlace : null; const chooseDestination = async (suggestion) => { try { destinationUpdateStatus(form, "Confirming destination address..."); const payload = await fetchDestinationPlaceDetails(suggestion); const place = normalizedPlaceSelection({ ...payload?.place, displayName: payload?.place?.displayName || suggestion.mainText || suggestion.text }); if (!place?.placeId) throw new Error("Selected destination did not return a place id."); form.__destinationUpdatePlace = place; destinationInput.value = place.formattedAddress || place.displayName || suggestion.text; markPassengerDestinationUpdateEditing(form); rememberRecentAddress("destination", place); hideInlinePlaceSuggestions(destinationSuggestions); destinationAutocompleteSessionToken = null; destinationUpdateStatus(form, `Selected destination: ${place.displayName || place.formattedAddress}`); } catch (error) { destinationUpdateStatus(form, passengerFacingPlaceErrorMessage( error, "Could not confirm that address suggestion. Enter the full destination address." )); } }; const chooseStop = async (suggestion) => { try { destinationUpdateStatus(form, "Confirming stop address..."); const payload = await fetchDestinationPlaceDetails(suggestion); const place = normalizedPlaceSelection({ ...payload?.place, displayName: payload?.place?.displayName || suggestion.mainText || suggestion.text }); if (!place?.placeId) throw new Error("Selected stop did not return a place id."); replaceActiveStopLine(stopsInput, place.formattedAddress || place.displayName || suggestion.text); rememberSelectedStopPlace(place); markPassengerDestinationUpdateEditing(form); rememberRecentAddress("destination", place); hideInlinePlaceSuggestions(stopSuggestions); destinationAutocompleteSessionToken = null; destinationUpdateStatus(form, `Selected stop: ${place.displayName || place.formattedAddress}`); } catch (error) { destinationUpdateStatus(form, passengerFacingPlaceErrorMessage( error, "Could not confirm that stop suggestion. Enter the full stop address." )); } }; const scheduleDestinationSearch = () => { window.clearTimeout(destinationTimer); if (!destinationPlaceMatchesInput(form.__destinationUpdatePlace, destinationInput.value)) { form.__destinationUpdatePlace = null; } markPassengerDestinationUpdateEditing(form); if (!destinationAutocompleteReady()) { hideInlinePlaceSuggestions(destinationSuggestions); return; } const input = destinationInput.value.trim(); if (input.length < 3) { hideInlinePlaceSuggestions(destinationSuggestions); return; } const requestId = ++destinationRequestId; destinationTimer = window.setTimeout(async () => { try { destinationUpdateStatus(form, passengerUiText("searchingDestinationAddresses", "Searching destination addresses...")); const payload = await fetchPlaceAutocomplete({ action: "autocomplete", input, sessionToken: destinationSessionToken(), country, city }); if (requestId !== destinationRequestId || !form.isConnected) return; renderInlinePlaceSuggestions(destinationSuggestions, payload?.suggestions ?? [], chooseDestination); destinationUpdateStatus(form, (payload?.suggestions ?? []).length ? passengerUiText("destinationChooseSuggestion", "Choose the matching destination from the suggestions.") : passengerUiText("destinationNoSuggestion", "No destination suggestion found; continue with the full address.")); } catch (error) { hideInlinePlaceSuggestions(destinationSuggestions); destinationUpdateStatus(form, passengerFacingPlaceErrorMessage( error, "Address suggestions are unavailable right now. Enter the full destination address." )); } }, 350); }; const scheduleStopSearch = () => { window.clearTimeout(stopTimer); markPassengerDestinationUpdateEditing(form); if (!destinationAutocompleteReady() || stopsInput.closest(".destination-stop-field")?.hidden) { hideInlinePlaceSuggestions(stopSuggestions); return; } const input = activeStopLineInfo(stopsInput).text; if (input.length < 3) { hideInlinePlaceSuggestions(stopSuggestions); return; } const requestId = ++stopRequestId; stopTimer = window.setTimeout(async () => { try { destinationUpdateStatus(form, "Searching stop addresses..."); const payload = await fetchPlaceAutocomplete({ action: "autocomplete", input, sessionToken: destinationSessionToken(), country, city }); if (requestId !== stopRequestId || !form.isConnected) return; renderInlinePlaceSuggestions(stopSuggestions, payload?.suggestions ?? [], chooseStop); destinationUpdateStatus(form, (payload?.suggestions ?? []).length ? "Choose the matching stop from the suggestions." : "No stop suggestion found; continue with the full address."); } catch (error) { hideInlinePlaceSuggestions(stopSuggestions); destinationUpdateStatus(form, passengerFacingPlaceErrorMessage( error, "Stop suggestions are unavailable right now. Enter the full stop address." )); } }, 350); }; destinationInput.addEventListener("input", scheduleDestinationSearch); destinationInput.addEventListener("focus", scheduleDestinationSearch); destinationInput.addEventListener("blur", () => window.setTimeout(() => { delete form.dataset.routeChangeEditingAt; hideInlinePlaceSuggestions(destinationSuggestions); }, 150)); stopsInput.addEventListener("input", scheduleStopSearch); stopsInput.addEventListener("focus", scheduleStopSearch); stopsInput.addEventListener("click", scheduleStopSearch); stopsInput.addEventListener("keyup", scheduleStopSearch); stopsInput.addEventListener("blur", () => window.setTimeout(() => { delete form.dataset.routeChangeEditingAt; hideInlinePlaceSuggestions(stopSuggestions); }, 150)); } function handlePickupInput() { clearLowFareReview(); if (clearStaleGpsPickupFallbackText()) return; if (els.pickupUseCurrentLocation?.checked && !pickupUsesCurrentLocationText(els.pickupDescription.value) && !pickupUsesGpsFallbackText(els.pickupDescription.value)) { els.pickupUseCurrentLocation.checked = false; stopAutomaticPassengerPickupGps(); selectedCurrentPickupGps = null; if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = "Exact pickup location is off."; } if (!pickupPlaceMatchesInput(selectedPickupPlace, els.pickupDescription.value)) { clearPickupPlaceSelection(pickupAutocompleteReady() ? passengerUiText("pickupSuggestionOrTyped", "Choose a suggestion if one matches, or continue with the typed pickup address.") : passengerUiText("pickupTypedUnlessGps", "Pickup will use the address as typed unless exact current location is checked.")); } renderRecentAddressShortcuts(); updateFareGuidance(); schedulePickupAutocomplete(); } function handleDestinationInput() { clearLowFareReview(); if (!destinationPlaceMatchesInput(selectedDestinationPlace, els.destination.value)) { clearDestinationPlaceSelection(destinationAutocompleteReady() ? passengerUiText("destinationSuggestionOrTyped", "Choose a suggestion if one matches, or continue with the typed destination.") : passengerUiText("destinationTypedAsEntered", "Destination text will be routed as typed.")); } renderRecentAddressShortcuts(); updateFareGuidance(); scheduleDestinationAutocomplete(); } function schedulePickupAutocomplete() { window.clearTimeout(pickupAutocompleteTimer); if (!pickupAutocompleteReady()) { hidePickupSuggestions(); if (els.pickupPlaceStatus && placesAutocompleteEnabled()) { setPassengerUiStatus(els.pickupPlaceStatus, "signInPassengerPickupSuggestions", "Sign in as a passenger to use pickup suggestions."); } return; } const input = els.pickupDescription.value.trim(); if (input.length < 3) { hidePickupSuggestions(); setPassengerUiStatus(els.pickupPlaceStatus, "typePickupSearchMin", "Type at least 3 characters to search pickup addresses."); return; } const requestId = ++pickupAutocompleteRequestId; pickupAutocompleteTimer = window.setTimeout(async () => { try { setPassengerUiStatus(els.pickupPlaceStatus, "searchingPickupAddresses", "Searching pickup addresses..."); const payload = await fetchPlaceAutocompleteWithCameroonFallback({ action: "autocomplete", input, sessionToken: pickupSessionToken(), country: state.passenger?.country ?? selectedPassengerCountry(), city: typeof selectedRidePickupCity === "function" ? selectedRidePickupCity() : state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(selectedPassengerCountry()) }); if (requestId !== pickupAutocompleteRequestId) return; renderPickupSuggestions(payload?.suggestions ?? []); if (els.pickupPlaceStatus) { const hasSuggestions = (payload?.suggestions ?? []).length; setPassengerUiStatus( els.pickupPlaceStatus, hasSuggestions ? "pickupChooseSuggestion" : "pickupNoSuggestion", hasSuggestions ? "Choose the matching pickup from the suggestions." : "No Cameroon pickup match found. You can continue with the typed pickup address, or use Current GPS if you want exact pickup." ); } } catch (error) { hidePickupSuggestions(); if (els.pickupPlaceStatus) { els.pickupPlaceStatus.textContent = passengerFacingPlaceErrorMessage( error, "Address suggestions are unavailable right now. Enter the full pickup address." ); } } }, 350); } function scheduleDestinationAutocomplete() { window.clearTimeout(destinationAutocompleteTimer); if (!destinationAutocompleteReady()) { hideDestinationSuggestions(); if (els.destinationPlaceStatus && placesAutocompleteEnabled()) { setPassengerUiStatus(els.destinationPlaceStatus, "signInPassengerPlaceSuggestions", "Sign in as a passenger to use place suggestions."); } return; } const input = els.destination.value.trim(); if (input.length < 3) { hideDestinationSuggestions(); setPassengerUiStatus(els.destinationPlaceStatus, "typeAddressSearchMin", "Type at least 3 characters to search addresses."); return; } const requestId = ++destinationAutocompleteRequestId; destinationAutocompleteTimer = window.setTimeout(async () => { try { setPassengerUiStatus(els.destinationPlaceStatus, "searchingDestinationAddresses", "Searching destination addresses..."); const payload = await fetchPlaceAutocompleteWithCameroonFallback({ action: "autocomplete", input, sessionToken: destinationSessionToken(), country: state.passenger?.country ?? selectedPassengerCountry(), city: typeof selectedRidePickupCity === "function" ? selectedRidePickupCity() : state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(selectedPassengerCountry()) }); if (requestId !== destinationAutocompleteRequestId) return; renderDestinationSuggestions(payload?.suggestions ?? []); if (els.destinationPlaceStatus) { const hasSuggestions = (payload?.suggestions ?? []).length; setPassengerUiStatus( els.destinationPlaceStatus, hasSuggestions ? "destinationChooseSuggestion" : "destinationNoSuggestion", hasSuggestions ? "Choose the matching destination from the suggestions." : "No Cameroon destination match found. You can continue with the typed destination address." ); } } catch (error) { hideDestinationSuggestions(); if (els.destinationPlaceStatus) { els.destinationPlaceStatus.textContent = passengerFacingPlaceErrorMessage( error, "Address suggestions are unavailable right now. Enter the full destination address." ); } } }, 350); } async function selectPickupSuggestion(suggestion) { try { clearLowFareReview(); if (els.pickupUseCurrentLocation) els.pickupUseCurrentLocation.checked = false; selectedCurrentPickupGps = null; if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = "Confirming pickup place..."; const payload = await fetchPickupPlaceDetails(suggestion); const place = normalizedPlaceSelection({ ...payload?.place, displayName: payload?.place?.displayName || suggestion.mainText || suggestion.text }); if (!place?.placeId) throw new Error("Selected pickup did not return a place id."); selectedPickupPlace = place; rememberRecentAddress("pickup", place); els.pickupDescription.value = place.formattedAddress || place.displayName || suggestion.text; hidePickupSuggestions(); pickupAutocompleteSessionToken = null; if (els.pickupPlaceStatus) { setPassengerUiStatus( els.pickupPlaceStatus, "selectedPlaceStatus", `Selected: ${place.displayName || place.formattedAddress}`, { place: place.displayName || place.formattedAddress } ); } renderRecentAddressShortcuts(); updateFareGuidance(); schedulePassengerNearbyRiderCountsRefresh(100); } catch (error) { if (els.pickupPlaceStatus) { els.pickupPlaceStatus.textContent = passengerFacingPlaceErrorMessage( error, "Could not confirm that pickup suggestion. Enter the full pickup address." ); } } } async function useCurrentPickupLocation() { clearLowFareReview(); clearStaleGpsPickupFallbackText({ status: false }); const previousPickupAddress = String(els.pickupDescription?.value ?? "").trim(); const canRestorePreviousPickupAddress = Boolean(previousPickupAddress) && previousPickupAddress !== "Capturing current location..." && !pickupUsesCurrentLocationText(previousPickupAddress) && !pickupUsesGpsFallbackText(previousPickupAddress); if (els.pickupUseCurrentLocation) els.pickupUseCurrentLocation.checked = true; if (!window.isSecureContext) { if (els.pickupUseCurrentLocation) els.pickupUseCurrentLocation.checked = false; setPassengerUiStatus(els.pickupPlaceStatus, "currentLocationNeedsHttps", "Current location requires a secure HTTPS page."); return; } if (!navigator.geolocation) { if (els.pickupUseCurrentLocation) els.pickupUseCurrentLocation.checked = false; setPassengerUiStatus(els.pickupPlaceStatus, "browserNoCurrentLocation", "This browser does not support current location. Type the pickup address instead."); return; } const existingPoint = pendingPickupGps && !pickupGpsQualityIssue(pendingPickupGps) ? pendingPickupGps : null; if (existingPoint) { applyCurrentPickupPoint(existingPoint, passengerUiText("usingCurrentLocationStatus", "Using current location: {place}.", { place: "GPS" })); } else { els.pickupDescription.value = "Capturing current location..."; setPassengerUiStatus(els.pickupPlaceStatus, "capturingCurrentLocation", "Capturing your current location. Approve the browser location prompt if it appears."); } setButtonBusy(els.useCurrentPickup, true); const point = existingPoint ?? await capturePassengerPickupGps({ automatic: false }); setButtonBusy(els.useCurrentPickup, false); if (!point) { if (els.pickupUseCurrentLocation) els.pickupUseCurrentLocation.checked = false; if (els.pickupDescription.value === "Capturing current location...") els.pickupDescription.value = ""; setPassengerUiStatus(els.pickupPlaceStatus, "currentLocationCaptureFailed", "Current location could not be captured. In Chrome, allow Location for this site or type the pickup address."); return; } applyCurrentPickupPoint(point, passengerUiText("usingCurrentLocationStatus", "Using current location: {place}.", { place: "GPS" })); try { const payload = await fetchPickupReverseGeocode(point); const place = normalizedPlaceSelection(payload?.place); if (!place?.formattedAddress) throw new Error("No street address was found for this GPS point."); selectedPickupPlace = { ...place, placeId: null, latitude: point.latitude, longitude: point.longitude, source: "browser-gps" }; rememberRecentAddress("pickup", { ...selectedPickupPlace, formattedAddress: place.formattedAddress }); els.pickupDescription.value = place.formattedAddress; if (els.pickupPlaceStatus) { setPassengerUiStatus( els.pickupPlaceStatus, "usingCurrentLocationVerifiedStatus", `Using current location: ${place.displayName || place.formattedAddress}. Riders will see the verified pickup point.`, { place: place.displayName || place.formattedAddress } ); } } catch (error) { const fallbackLabel = gpsPickupFallbackLabel(point); selectedPickupPlace = { placeId: null, displayName: fallbackLabel, formattedAddress: fallbackLabel, latitude: point.latitude, longitude: point.longitude, accuracyMeters: point.accuracyMeters ?? null, capturedAt: point.capturedAt ?? null, source: "browser-gps" }; selectedCurrentPickupGps = point; if (els.pickupDescription) { els.pickupDescription.value = fallbackLabel || (canRestorePreviousPickupAddress ? previousPickupAddress : currentPickupLocationLabel(point)); } if (els.pickupPlaceStatus) { setPassengerUiStatus( els.pickupPlaceStatus, "exactGpsNoLandmarkStatus", "Exact GPS was captured. No nearby saved landmark was found, so riders will navigate to the GPS point and see your pickup note." ); } } updateFareGuidance(); schedulePassengerNearbyRiderCountsRefresh(100); } function gpsPickupFallbackLabel(point) { const inferred = inferredLaunchLocationFromGps(selectedPassengerCountry(), point); const parts = [inferred?.area, inferred?.city].filter(Boolean).join(", "); return parts ? `Verified GPS pickup near ${parts}` : "Verified GPS pickup"; } async function resolveCurrentPickupAddressForPublish(point, fallbackLabel) { const gps = normalizeGpsPoint(point); const label = String(fallbackLabel ?? "").trim(); const needsStreetAddressLookup = !label || pickupUsesCurrentLocationText(label) || pickupUsesGpsFallbackText(label); if (!gps || !needsStreetAddressLookup) return fallbackLabel; try { if (els.pickupPlaceStatus) { els.pickupPlaceStatus.textContent = "Confirming nearest pickup address from current location..."; } const payload = await fetchPickupReverseGeocode(gps); const place = normalizedPlaceSelection(payload?.place); if (!place?.formattedAddress) throw new Error("No street address was found for this GPS point."); selectedCurrentPickupGps = gps; selectedPickupPlace = { ...place, placeId: null, latitude: gps.latitude, longitude: gps.longitude, source: "browser-gps" }; rememberRecentAddress("pickup", { ...selectedPickupPlace, formattedAddress: place.formattedAddress }); els.pickupDescription.value = place.formattedAddress; if (els.pickupPlaceStatus) { setPassengerUiStatus( els.pickupPlaceStatus, "usingCurrentLocationStatus", `Using current location: ${place.displayName || place.formattedAddress}.`, { place: place.displayName || place.formattedAddress } ); } updateFareGuidance(); schedulePassengerNearbyRiderCountsRefresh(100); return place.formattedAddress; } catch (error) { logClientWarning("Current pickup reverse lookup did not find a nearby saved Cameroon location; using exact GPS fallback.", error); const fallback = gpsPickupFallbackLabel(gps); selectedCurrentPickupGps = gps; selectedPickupPlace = { placeId: null, displayName: fallback, formattedAddress: fallback, latitude: gps.latitude, longitude: gps.longitude, accuracyMeters: gps.accuracyMeters ?? null, capturedAt: gps.capturedAt ?? null, source: "browser-gps" }; if (els.pickupDescription) els.pickupDescription.value = fallback; if (els.pickupPlaceStatus) { setPassengerUiStatus( els.pickupPlaceStatus, "exactGpsFallbackNoLandmarkStatus", "Using exact GPS pickup. No nearby saved landmark was found, so riders will navigate to the GPS point and see your typed pickup note." ); } updateFareGuidance(); schedulePassengerNearbyRiderCountsRefresh(100); return fallback; } } async function ensureCurrentPickupAddressForPublish() { clearStaleGpsPickupFallbackText({ status: false }); const capturedPoint = await ensurePassengerPickupGpsForPublish(); const gps = normalizeGpsPoint(selectedCurrentPickupGps ?? capturedPoint ?? pendingPickupGps); const enteredPickupDescription = String(els.pickupDescription?.value ?? "").trim(); const enteredPickupIsExactAddress = Boolean(enteredPickupDescription) && !exactPickupAddressIssue(enteredPickupDescription); if (!passengerWantsCurrentPickup() || enteredPickupIsExactAddress) return enteredPickupDescription; if (!gps || pickupGpsQualityIssue(gps)) return enteredPickupDescription; if (pickupFieldCanUseGpsAutofill()) { applyCurrentPickupPoint(gps, passengerUiText("usingCurrentLocationStatus", "Using current location: {place}.", { place: "GPS" })); } return resolveCurrentPickupAddressForPublish(gps, currentPickupLocationLabel(gps)); } function pickupFieldCanUseGpsAutofill() { const value = els.pickupDescription?.value.trim() ?? ""; return !value || pickupUsesCurrentLocationText(value) || pickupUsesGpsFallbackText(value) || value === "Capturing current location..."; } function applyCurrentPickupPoint(point, statusText = "") { const gps = normalizeGpsPoint(point); if (!gps || !els.pickupDescription) return false; selectedPickupPlace = null; selectedCurrentPickupGps = gps; applyPassengerGpsLaunchContext(gps); els.pickupDescription.value = currentPickupLocationLabel(gps); hidePickupSuggestions(); if (statusText && els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = statusText; updateFareGuidance(); return true; } function activateUseCurrentPickup(event) { event?.preventDefault?.(); if (useCurrentPickupActivationInFlight) return; useCurrentPickupActivationInFlight = true; Promise.resolve(useCurrentPickupLocation()).finally(() => { window.setTimeout(() => { useCurrentPickupActivationInFlight = false; }, 0); }); } function handlePickupUseCurrentLocationChange(event) { const checked = Boolean(event?.target?.checked ?? els.pickupUseCurrentLocation?.checked); clearLowFareReview(); clearStaleGpsPickupFallbackText({ status: false }); if (checked) { void useCurrentPickupLocation(); ensurePassengerPickupGpsAutoCapture(); return; } stopAutomaticPassengerPickupGps(); selectedCurrentPickupGps = null; if (pickupUsesCurrentLocationText(els.pickupDescription?.value) || pickupUsesGpsFallbackText(els.pickupDescription?.value) || els.pickupDescription?.value === "Capturing current location...") { els.pickupDescription.value = ""; } if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = "Exact pickup location is off."; setPassengerUiStatus(els.pickupPlaceStatus, "pickupEntryPrompt", "Enter a pickup address, or check exact current location."); updateFareGuidance(); schedulePassengerNearbyRiderCountsRefresh(100); } async function selectDestinationSuggestion(suggestion) { try { clearLowFareReview(); if (els.destinationPlaceStatus) els.destinationPlaceStatus.textContent = "Confirming destination place..."; const payload = await fetchDestinationPlaceDetails(suggestion); const place = normalizedPlaceSelection({ ...payload?.place, displayName: payload?.place?.displayName || suggestion.mainText || suggestion.text }); if (!place?.placeId) throw new Error("Selected destination did not return a place id."); selectedDestinationPlace = place; rememberRecentAddress("destination", place); els.destination.value = place.formattedAddress || place.displayName || suggestion.text; hideDestinationSuggestions(); destinationAutocompleteSessionToken = null; if (els.destinationPlaceStatus) { setPassengerUiStatus( els.destinationPlaceStatus, "selectedPlaceStatus", `Selected: ${place.displayName || place.formattedAddress}`, { place: place.displayName || place.formattedAddress } ); } renderRecentAddressShortcuts(); updateFareGuidance(); } catch (error) { if (els.destinationPlaceStatus) { els.destinationPlaceStatus.textContent = passengerFacingPlaceErrorMessage( error, "Could not confirm that address suggestion. Enter the full destination address." ); } } } function updatePassengerFareModeControls() { const mode = "negotiable"; state.passengerFareMode = mode; syncPassengerFareModeInputs(mode); document.querySelectorAll("[data-passenger-fare-mode]").forEach((button) => { const active = normalizePassengerFareMode(button.dataset.passengerFareMode) === mode; button.classList.toggle("active", active); button.setAttribute("aria-pressed", active ? "true" : "false"); }); if (els.passengerFareModePanel) els.passengerFareModePanel.hidden = true; setRideRequestOptionPanelState("vehicle", true); setRideRequestOptionPanelState("fare", true); if (els.fareOffer) { els.fareOffer.readOnly = false; els.fareOffer.required = true; els.fareOffer.setAttribute("aria-readonly", "false"); els.fareOffer.placeholder = "Enter fare"; } if (els.passengerFareModeStatus) { els.passengerFareModeStatus.textContent = "Fare is negotiated. Enter the FCFA offer; riders can accept or send counter-offers."; } syncNegotiatedFareInlinePanel(); } function setPassengerFareMode(value) { const mode = normalizePassengerFareMode(value); state.passengerFareMode = mode; syncPassengerFareModeInputs(mode, { userSelected: true }); saveState(); const guidance = updateFareGuidance(); updatePassengerFareModeControls(guidance); syncNegotiatedFareInlinePanel({ focus: mode === "negotiable" }); } function handlePassengerFareModeSelection(event) { const control = event.currentTarget; if (control instanceof HTMLInputElement && control.id === "passengerFareNegotiable") { setPassengerFareMode("negotiable"); return; } if (control instanceof HTMLSelectElement && control.id === "passengerFareMode") { setPassengerFareMode(control.value); return; } setPassengerFareMode(control?.dataset?.passengerFareMode); } function handlePassengerFareModeButtonActivation(event) { const now = Date.now(); if (event?.type === "click" && now - passengerFareModeLastPointerAt < 700) { event.preventDefault?.(); return; } if (event?.type && event.type !== "click" && event.type !== "keydown") { if (now - passengerFareModeLastPointerAt < 250) { event.preventDefault?.(); return; } passengerFareModeLastPointerAt = now; } event?.preventDefault?.(); const button = event?.currentTarget; setPassengerFareMode(button?.dataset?.passengerFareMode); } function handlePassengerFareModeButtonKeyActivation(event) { if (!["Enter", " "].includes(event?.key)) return; handlePassengerFareModeButtonActivation(event); } function currentFareReviewKey(guidance, fareOffer) { return JSON.stringify({ pickup: els.pickupDescription?.value.trim() ?? "", destination: els.destination?.value.trim() ?? "", fareOffer: Number(fareOffer) || 0, min: guidance?.min ?? null, max: guidance?.max ?? null, distanceMiles: guidance?.distanceMiles ? Number(guidance.distanceMiles).toFixed(1) : null, minutes: guidance?.minutes ?? null }); } function clearLowFareReview() { pendingLowFareOverrideKey = ""; if (!els.fareReviewPanel) return; els.fareReviewPanel.hidden = true; els.fareReviewPanel.replaceChildren(); } function showLowFareReview(guidance, fareOffer) { if (!els.fareReviewPanel || !guidance) return; setRideRequestOptionPanel("fare", true); const reviewKey = currentFareReviewKey(guidance, fareOffer); els.fareReviewPanel.hidden = false; els.fareReviewPanel.replaceChildren(); const message = document.createElement("div"); message.textContent = `Your ${formatMoney(fareOffer, state.passenger?.country)} offer is below the suggested ${formatMoney(guidance.min, state.passenger?.country)}-${formatMoney(guidance.max, state.passenger?.country)} range for this route. You can adjust it or publish anyway.`; const actions = document.createElement("div"); actions.className = "fare-review-actions"; const useMinimum = document.createElement("button"); useMinimum.type = "button"; useMinimum.className = "secondary-action"; useMinimum.textContent = `Use ${formatMoney(guidance.min, state.passenger?.country)}`; useMinimum.addEventListener("click", () => { els.fareOffer.value = String(guidance.min); clearLowFareReview(); updateFareGuidance(); els.fareOffer.focus(); }); const publishAnyway = document.createElement("button"); publishAnyway.type = "button"; publishAnyway.className = "ghost-action"; publishAnyway.textContent = "Publish anyway"; publishAnyway.addEventListener("click", () => { pendingLowFareOverrideKey = reviewKey; els.rideRequestForm.requestSubmit(); }); actions.append(useMinimum, publishAnyway); els.fareReviewPanel.append(message, actions); els.fareReviewPanel.scrollIntoView({ behavior: "smooth", block: "center" }); } function passengerMinimumAllowedFare(guidance, country) { const baselineMinimum = Number.isFinite(Number(guidance?.min)) ? Number(guidance.min) : minimumFareOffer(country); return Math.max(minimumFareOffer(country), Math.ceil(baselineMinimum - 4)); } function showPassengerMinimumFareBlock(guidance, fareOffer, minimumAllowedFare) { if (!els.fareReviewPanel || !guidance) { translatedAlert("publishRideFailed", { message: `Increase the passenger fare to at least ${formatMoney(minimumAllowedFare)} before publishing.` }); return; } setRideRequestOptionPanel("fare", true); els.fareReviewPanel.hidden = false; els.fareReviewPanel.replaceChildren(); const message = document.createElement("div"); message.textContent = `Your ${formatMoney(fareOffer, state.passenger?.country)} offer is more than ${formatMoney(4, state.passenger?.country)} below the suggested minimum of ${formatMoney(guidance.min, state.passenger?.country)}. Increase it to at least ${formatMoney(minimumAllowedFare, state.passenger?.country)} before publishing.`; const actions = document.createElement("div"); actions.className = "fare-review-actions"; const useMinimumAllowed = document.createElement("button"); useMinimumAllowed.type = "button"; useMinimumAllowed.className = "secondary-action"; useMinimumAllowed.textContent = `Use ${formatMoney(minimumAllowedFare, state.passenger?.country)}`; useMinimumAllowed.addEventListener("click", () => { els.fareOffer.value = String(minimumAllowedFare); clearLowFareReview(); updateFareGuidance(); els.fareOffer.focus(); }); actions.append(useMinimumAllowed); els.fareReviewPanel.append(message, actions); els.fareReviewPanel.scrollIntoView({ behavior: "smooth", block: "center" }); } function xlSpecialFareFloorFromGuidance(guidance) { return guidance?.max != null ? Number(guidance.max) : null; } function requestFareGuidance(request) { const distance = Number(request?.estimatedDistanceMiles); const minutes = Number(request?.estimatedTravelMinutes); if (!Number.isFinite(distance) || distance <= 0) return null; return fareGuidanceFromDistance(distance, Number.isFinite(minutes) ? minutes : null, request?.rideStops, { source: request?.routeEstimateSource, provider: request?.routeEstimateProvider, cached: request?.routeEstimateCached, routeKey: request?.routeEstimateKey, routePolyline: request?.routeEstimatePolyline, country: request?.country, city: request?.city, destinationFingerprint: request?.routeEstimateDestinationFingerprint, estimatedAt: request?.routeEstimateCreatedAt }); } function xlSpecialFareFloorForRequest(request) { if (normalizeCarTypePreference(request?.carTypePreference) !== "suv") return null; return xlSpecialFareFloorFromGuidance(requestFareGuidance(request)); } function showXlSpecialFareBlock(guidance, fareOffer) { if (!els.fareReviewPanel || !guidance) return; setRideRequestOptionPanel("fare", true); els.fareReviewPanel.hidden = false; els.fareReviewPanel.replaceChildren(); const message = document.createElement("div"); message.textContent = `XL/Special rides must start above the normal maximum of $${guidance.max}. Increase the passenger fare before publishing.`; const actions = document.createElement("div"); actions.className = "fare-review-actions"; const useXlMinimum = document.createElement("button"); useXlMinimum.type = "button"; useXlMinimum.className = "secondary-action"; useXlMinimum.textContent = `Use $${guidance.max + 1}`; useXlMinimum.addEventListener("click", () => { els.fareOffer.value = String(guidance.max + 1); clearLowFareReview(); updateFareGuidance(); els.fareOffer.focus(); }); actions.append(useXlMinimum); els.fareReviewPanel.append(message, actions); els.fareReviewPanel.scrollIntoView({ behavior: "smooth", block: "center" }); } function passengerRidePublishedMessage(request) { const fare = formatMoney(request.fareOffer, request.country); return `Ride request published for ${fare}. Waiting for rider offers.`; } function passengerPhoneVerifiedForRidePublishing() { return Boolean(state.passenger?.phoneVerified || smsVerificationRelaxedForTesting()); } function passengerRequestHasRiderCancelReopenNotice(request) { if (!request?.id || !requestBelongsToPassenger(request) || request.status !== "open" || selectedRiderIdForRequest(request)) return false; return (state.notifications ?? []).some((notice) => { if (notice?.recipientRole !== "passenger" || notice.requestId !== request.id) return false; const text = `${notice.eventType || ""} ${notice.id || ""} ${notice.title || ""} ${notice.body || ""}`.toLowerCase(); return /ride_reopened|rider_cancelled_before_pickup|rider_canceled_before_pickup|cancelled_before_pickup|canceled_before_pickup|rider cancelled|rider canceled|open again|reopened/.test(text); }); } function addPassengerRideNotice(title, body, requestId, { deliver = false, eventKey = "" } = {}) { if (deliver && typeof addRideAccountNotice === "function") { return addRideAccountNotice("passenger", title, body, requestId, eventKey); } if (!state.passenger?.id) return; const notice = { id: eventKey ? `notice-passenger-${eventKey}` : makeId("notice"), recipientId: state.passenger.id, recipientRole: "passenger", title, body, requestId, actionUrl: typeof workspaceNotificationUrl === "function" ? workspaceNotificationUrl("passenger", "trips", requestId) : "", eventType: eventKey ? eventKey.split("-")[0] : "passenger_action_status", createdAt: new Date().toISOString(), readAt: null }; state.notifications = upsertById(state.notifications, notice); saveState(); return notice; } async function createBusinessAccount(event) { event.preventDefault(); if (!state.passenger || !hasSignedIn("passenger")) { els.businessAccountStatus.textContent = "Sign in as a passenger before creating a business account."; return; } const businessName = els.businessName.value.trim(); const billingEmail = els.businessBillingEmail.value.trim().toLowerCase(); const businessCategory = els.businessCategory?.value || "other"; const businessAddress = els.businessAddress?.value.trim() || ""; const contactName = els.businessContactName?.value.trim() || state.passenger.name; const contactPhone = els.businessContactPhone?.value.trim() || state.passenger.phone; const planCode = normalizeBusinessPlanCode(els.businessPlan?.value); const referralCode = els.businessReferralCode?.value.trim() || ""; if (businessName.length < 2 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(billingEmail) || businessAddress.length < 4 || contactName.length < 2 || contactPhone.length < 7) { els.businessAccountStatus.textContent = "Enter business name, billing email, address, and authorized contact details."; return; } const localAccount = { id: makeId("business"), ownerId: state.passenger.id, ownerName: state.passenger.name, businessName, billingEmail, businessCategory, businessAddress, contactName, contactPhone, planCode, referralCode, verificationStatus: "pending_review", status: "pending_review", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; try { els.businessAccountStatus.textContent = "Creating business account..."; const savedAccount = await saveBusinessAccountToSupabase(localAccount); state.businessAccounts = upsertById(state.businessAccounts, savedAccount); els.businessName.value = ""; els.businessBillingEmail.value = ""; if (els.businessAddress) els.businessAddress.value = ""; if (els.businessContactName) els.businessContactName.value = ""; if (els.businessContactPhone) els.businessContactPhone.value = ""; if (els.businessReferralCode) els.businessReferralCode.value = ""; if (referralCode) await claimReferralCodeValue("business", referralCode, els.businessAccountStatus); saveState(); renderAll(); els.businessAccountStatus.textContent = businessAccountSummary(savedAccount); } catch (error) { els.businessAccountStatus.textContent = `Business account was not created: ${error.message}`; } } async function updatePassengerActiveLocation(event) { event.preventDefault(); if (!state.passenger || !hasSignedIn("passenger")) return; const country = els.passengerActiveCountry.value; const city = els.passengerActiveCity.value; try { const subdivision = locationSubdivisionLabel(country); els.passengerLocationStatus.textContent = `Updating passenger ${subdivision}...`; await updatePassengerCurrentCityInSupabase(state.passenger.supabaseUserId ?? state.passenger.id, country, city); state.passenger = { ...state.passenger, country, city }; state.passengers = upsertById(state.passengers, state.passenger); clearSelectedRequestOutsideLocation(country, city); clearPassengerPickupGps(); saveState(); populateLocationFields(); hydrateForms(); renderAll(); void refreshMarketplace({ silent: true }); els.passengerLocationStatus.textContent = `Ride requests now publish in ${city} ${subdivision}, ${country}.`; } catch (error) { els.passengerLocationStatus.textContent = error.message; } } async function ensurePassengerProfileMatchesRidePickupLocation(country, city) { if (!hasSupabaseRuntime() || !state.passenger?.id || !hasSignedIn("passenger")) return; const targetCountry = String(country || "").trim(); const targetCity = String(city || "").trim(); if (!targetCountry || !targetCity) return; if (state.passenger.country === targetCountry && state.passenger.city === targetCity) return; if (els.fareGuidance) { els.fareGuidance.textContent = passengerUiText( "preparingPickupCityForPublish", `Preparing this request for ${targetCity} before publishing...`, { city: targetCity } ); } await updatePassengerCurrentCityInSupabase(state.passenger.supabaseUserId ?? state.passenger.id, targetCountry, targetCity); state.passenger = { ...state.passenger, country: targetCountry, city: targetCity }; state.passengers = upsertById(state.passengers, state.passenger); if (els.passengerCity) els.passengerCity.value = targetCity; if (els.passengerActiveCity) els.passengerActiveCity.value = targetCity; if (els.pickupCity) els.pickupCity.value = targetCity; saveState(); } function passengerRidePublishErrorMessage(error) { const message = String(error?.message || "").trim(); if (/update the passenger city before publishing this ride request/i.test(message)) { return passengerUiText( "pickupCityCouldNotBePrepared", "Waka could not prepare the selected pickup city for publishing. Choose the pickup city and area again, then publish the ride request." ); } return message || "The ride request could not be published. Please try again."; } function initialBusinessAccountValues() { const wantsBusiness = passengerBusinessIntentFromLocation(); return { wantsBusiness, businessName: els.passengerInitialBusinessName?.value.trim() ?? "", billingEmail: els.passengerInitialBusinessBillingEmail?.value.trim().toLowerCase() ?? "", businessCategory: els.passengerInitialBusinessCategory?.value || "other", businessAddress: els.passengerInitialBusinessAddress?.value.trim() ?? "", logoFile: els.passengerInitialBusinessLogo?.files?.[0] ?? null, logoAltText: els.passengerInitialBusinessLogoAlt?.value.trim() ?? "", planCode: normalizeBusinessPlanCode(els.passengerInitialBusinessPlan?.value), referralCode: els.passengerInitialBusinessReferralCode?.value.trim() || els.passengerReferralCode?.value.trim() || "" }; } function validBusinessAccountFields(values) { if (!values.wantsBusiness) return true; return values.businessName.length >= 2 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.billingEmail) && values.businessAddress.length >= 4; } async function createInitialBusinessAccountForPassenger(values) { if (!values.wantsBusiness || !state.passenger) return null; const localAccount = { id: makeId("business"), ownerId: state.passenger.id, ownerName: state.passenger.name, businessName: values.businessName, billingEmail: values.billingEmail, businessCategory: values.businessCategory, businessAddress: values.businessAddress, contactName: state.passenger.name, contactPhone: state.passenger.phone, planCode: values.planCode, referralCode: values.referralCode, verificationStatus: "pending_review", status: "pending_review", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; const savedAccount = await saveBusinessAccountToSupabase(localAccount); if (values.referralCode) await claimReferralCodeValue("business", values.referralCode, els.passengerStatus); state.businessAccounts = upsertById(state.businessAccounts, savedAccount); return savedAccount; } async function createInitialIntercityAgencyForPassenger(values, businessAccount = null) { if (!values.wantsBusiness || !passengerBusinessIntentFromLocation() || !state.passenger) return null; const companyName = values.businessName; const hqCity = state.passenger.city || "Bamenda"; const slugBase = slugifyIntercityAgency(companyName); const slug = `${slugBase}-${crypto.randomUUID().slice(0, 8)}`; const logoAltText = values.logoAltText || `${companyName} logo`; validateIntercityAgencyLogoFile(values.logoFile); const rows = await supabaseRestRequest("/rest/v1/cameroon_intercity_agencies", { method: "POST", body: { owner_profile_id: state.passenger.id, business_account_id: businessAccount?.id ?? null, company_name: companyName, slug, description: "Agency profile submitted during Waka account creation.", logo_alt_text: logoAltText, support_email: values.billingEmail || state.passenger.email, support_phone: state.passenger.phone || "", terminal_address: values.businessAddress, hq_city: hqCity, operated_cities: [hqCity], boarding_policy: "Arrive at least one hour before departure with your receipt or booking reference.", monthly_fee_xaf: 25000, subscription_status: "pending_review", status: "pending_review", updated_at: new Date().toISOString() }, headers: { Prefer: "return=representation" } }); let saved = mapIntercityAgencyFromDatabase(Array.isArray(rows) ? rows[0] : rows); if (values.logoFile) { saved = await saveIntercityAgencyLogoMetadata(saved, values.logoFile, logoAltText); } state.intercityAgencies = upsertById(intercityAgencyRecords(), saved); if (els.passengerInitialBusinessLogo) els.passengerInitialBusinessLogo.value = ""; return saved; } function passengerEmailConfirmationGuidance(email, agencyAccess = false) { const key = agencyAccess ? "agencyEmailConfirmationPopup" : "passengerEmailConfirmationPopup"; const fallback = agencyAccess ? `Good news - your required agency access details were accepted.\n\nWaka Cameroon has sent a confirmation link to ${email}. Open your email and click the link to complete account creation. If you do not see it, check Spam or Junk. Also verify that the email address you entered is correct. After confirming, return here and sign in with the same email and password. Do not press Save account again for this same email.` : `Good news - your required passenger account details were accepted.\n\nWaka Cameroon has sent a confirmation link to ${email}. Open your email and click the link to complete account creation. If you do not see it, check Spam or Junk. Also verify that the email address you entered is correct. After confirming, return here and sign in with the same email and password. Do not press Save account again for this same email.`; if (typeof translatedMessage !== "function") return fallback; return translatedMessage(key, { email }) || fallback; } function showPassengerEmailConfirmationGuidance(email, agencyAccess = false) { const message = passengerEmailConfirmationGuidance(email, agencyAccess); if (typeof showWakaGoodAlert === "function") { void showWakaGoodAlert(message); return; } if (typeof window !== "undefined" && typeof window.alert === "function") { window.alert(message); } } async function createPassenger(event) { event.preventDefault(); updatePassengerInitialBusinessFields(); setTranslatedStatus(els.passengerStatus, "checkingPassengerAccount"); const phone = els.passengerPhone.value.trim(); const profilePhotoName = els.passengerPhoto.files[0]?.name ?? state.passenger?.profilePhotoName ?? ""; const businessValues = initialBusinessAccountValues(); const agencyAccess = businessValues.wantsBusiness; const enteredDateOfBirth = businessValues.wantsBusiness ? "" : normalizeDateOfBirthInput(els.passengerDob); const dateOfBirth = enteredDateOfBirth || null; const email = els.passengerEmail.value.trim().toLowerCase(); let addingPassengerRoleToExistingLogin = false; if (!validateAccountForm(els.passengerAccountForm, els.passengerStatus)) return; if (!validBusinessAccountFields(businessValues)) { els.passengerStatus.textContent = agencyAccess ? "Enter agency name, billing email, and terminal or office address before creating the agency account." : "Enter business name, billing email, and business address, or choose Personal rides."; return; } try { validateIntercityAgencyLogoFile(businessValues.logoFile); } catch (error) { els.passengerStatus.textContent = error.message; return; } if (!businessValues.wantsBusiness && enteredDateOfBirth && !validDateOfBirth(enteredDateOfBirth)) { setTranslatedStatus(els.passengerStatus, "validDateOfBirthRequired"); return; } if (hasSupabaseRuntime()) { try { const excludeUserId = await profileAvailabilityExcludeUserId(email, state.passenger?.id ?? null); const availability = await profileContactAvailability(email, phone, excludeUserId, "passenger"); if (!availability.emailAvailable || !availability.phoneAvailable) { const roleSetup = await existingProfileRoleSetup( email, els.passengerPassword.value, phone, "passenger", availability, (message) => { els.passengerStatus.textContent = message; } ); if (roleSetup.action === "existing_role" && roleSetup.user && roleSetup.profile) { const passengerProfile = profileForWorkspaceRole(roleSetup.profile, { role: "passenger" }); applySignedInProfile("passenger", passengerProfile, roleSetup.user); rememberRoleSetupPhoneVerification("passenger", phone, roleSetup.profile); state.accountMode.passenger = "signin"; state.activeTab = "passenger"; state.showRoleEntry = false; routePassengerToRequestAfterSignIn(); clearPendingProfileRecovery("passenger"); saveState(); renderAll(); els.passengerStatus.textContent = agencyAccess ? "Agency access already exists for this Waka login. Signed in and opened the agency workspace." : "Passenger account already exists for this Waka login. Signed in and opened Ride request."; return; } if (roleSetup.action !== "can_add_role") { els.passengerStatus.textContent = roleSetupBlockedMessage("passenger", roleSetup.reason); return; } rememberRoleSetupPhoneVerification("passenger", phone, roleSetup.profile); addingPassengerRoleToExistingLogin = true; els.passengerStatus.textContent = agencyAccess ? "Existing Waka login confirmed. Creating agency access for this account..." : "Existing Waka login confirmed. Adding passenger access to this account..."; } } catch (error) { const availabilityMessage = profileAvailabilityErrorMessage(error); if (availabilityMessage) { els.passengerStatus.textContent = availabilityMessage; return; } logClientWarning("Profile contact availability check was skipped.", error); } } if (!(await ensureVerifiedPhoneForAccount("passenger", phone, els.passengerStatus))) return; const passenger = { id: state.passenger?.id ?? makeId("passenger"), name: els.passengerName.value.trim(), email, password: els.passengerPassword.value, phone, phoneVerified: true, phoneVerifiedAt: state.verification.passenger?.verifiedAt ?? state.passenger?.phoneVerifiedAt ?? new Date().toISOString(), phoneVerificationProvider: state.verification.passenger?.provider ?? "manual-pilot", accountUse: businessValues.wantsBusiness ? "business" : "personal", nationalId: businessValues.wantsBusiness ? "" : els.passengerNationalId.value.trim(), dateOfBirth, preferredLanguage: state.language, country: els.passengerCountry.value, city: els.passengerCity.value, profilePhotoName, profilePhotoPath: state.passenger?.profilePhotoPath ?? null, createdAt: state.passenger?.createdAt ?? new Date().toISOString() }; try { setButtonBusy(els.passengerSaveButton, true); const setPassengerStage = (message) => { els.passengerStatus.textContent = message; }; setTranslatedStatus(els.passengerStatus, isSupabaseMode() ? "startingPassengerSupabase" : "savingPassenger"); let user = null; let onboardingFunctionResult = null; if (hasSupabaseRuntime() && !addingPassengerRoleToExistingLogin) { onboardingFunctionResult = await submitPassengerAccountViaOnboardingFunction(passenger, setPassengerStage); user = await passengerOnboardingFunctionUserResult(passenger, onboardingFunctionResult, setPassengerStage); } else { user = await saveProfileToSupabase({ ...passenger, role: "passenger" }, setPassengerStage, { waitForProfile: true, preventExistingAccount: false, requireExplicitPasswordSignIn: true }); } state.passenger = { ...passenger, password: undefined, id: user?.id ?? passenger.id, profilePhotoPath: user?.profilePhotoPath ?? passenger.profilePhotoPath, supabaseUserId: user?.id ?? null }; if (user?.emailSetupPending) { state.sessions.passenger = null; } else { activateWorkspaceRoleSession("passenger", { phone: state.passenger.phone, email: state.passenger.email, userId: state.passenger.supabaseUserId, signedInAt: new Date().toISOString() }); await claimReferralCodeForRole("passenger", els.passengerStatus); } els.passengerPassword.value = ""; els.passengerPhoto.value = ""; state.passengers = upsertById(state.passengers, state.passenger); state.accountMode.passenger = "signin"; clearPendingProfileRecovery("passenger"); let businessCreated = null; let agencyCreated = null; if (!user?.emailSetupPending) { try { businessCreated = await createInitialBusinessAccountForPassenger(businessValues); } catch (error) { els.passengerStatus.textContent = `Passenger account was created, but the business account was not created: ${error.message}`; } if (agencyAccess) { try { agencyCreated = await createInitialIntercityAgencyForPassenger(businessValues, businessCreated); } catch (error) { els.passengerStatus.textContent = `Agency login was created, but the agency profile or logo was not saved: ${error.message}`; } } } if (!user?.emailSetupPending && typeof routePassengerToRequestAfterSignIn === "function") { routePassengerToRequestAfterSignIn(); } else { state.activeTab = "passenger"; state.showRoleEntry = false; state.passengerPage = agencyAccess ? "business" : "request"; } saveState(); renderAll(); if (user?.emailSetupPending) { const emailPendingMessage = passengerEmailConfirmationGuidance(passenger.email, agencyAccess); if (els.passengerSignInEmail) els.passengerSignInEmail.value = passenger.email; if (els.passengerSignInPassword) els.passengerSignInPassword.value = ""; const status = els.passengerSignInStatus ?? els.passengerStatus; if (status) status.textContent = emailPendingMessage; showPassengerEmailConfirmationGuidance(passenger.email, agencyAccess); } else if (agencyAccess) { els.passengerStatus.textContent = agencyCreated ? `Agency account for ${agencyCreated.companyName} was created and submitted for Waka admin approval.${agencyCreated.logoPath || agencyCreated.logoUrl ? " Company logo is already attached." : ""}` : "Agency account was created and the agency workspace is now open."; } else { setTranslatedStatus(els.passengerStatus, "passengerCreated", { name: state.passenger.name }); } if (businessCreated) els.businessAccountStatus.textContent = businessAccountSummary(businessCreated); } catch (error) { if (passengerAccountCreationRequiresSignIn(error)) { routePassengerAccountCreationToSignIn(passenger, error); return; } setTranslatedStatus(els.passengerStatus, "passengerAccountFailed", { message: passengerAccountErrorMessage(error) }); } finally { setButtonBusy(els.passengerSaveButton, false); } } function passengerAccountCreationRequiresSignIn(error) { const message = String(error?.message || error || ""); return /Waka Cameroon created the .*login|email confirmation|confirmed.*sign in|already has a Supabase login|existing password|Supabase did not accept the sign-in/i.test(message); } function routePassengerAccountCreationToSignIn(passenger, error) { const message = String(error?.message || error || ""); state.passenger = { ...passenger, password: undefined, pendingEmailConfirmation: true, status: "email_confirmation_pending" }; state.accountMode.passenger = "signin"; state.activeTab = "passenger"; state.showRoleEntry = false; clearPendingProfileRecovery("passenger"); saveState(); renderAll(); if (els.passengerSignInEmail) els.passengerSignInEmail.value = passenger.email; if (els.passengerSignInPassword) els.passengerSignInPassword.value = ""; const status = els.passengerSignInStatus ?? els.passengerStatus; const needsEmailConfirmation = /email confirmation|Waka Cameroon created the .*login|confirmed.*sign in|Supabase did not accept the sign-in/i.test(message); if (status) { status.textContent = needsEmailConfirmation ? passengerEmailConfirmationGuidance(passenger.email, passenger.accountUse === "business") : "This email already has a login. Sign in here with the existing password; Waka will open the passenger workspace."; } if (needsEmailConfirmation) { showPassengerEmailConfirmationGuidance(passenger.email, passenger.accountUse === "business"); } } async function createRideRequest(event) { event.preventDefault(); if (els.rideRequestForm?.dataset.submitting === "true") return; if (els.rideRequestForm) els.rideRequestForm.dataset.submitting = "true"; try { clearStaleGpsPickupFallbackText({ status: false }); if (!state.passenger) { translatedAlert("passengerAccountRequired"); return; } if (!hasSignedIn("passenger")) { translatedAlert("passengerSignInRequired"); return; } if (!passengerPhoneVerifiedForRidePublishing()) { translatedAlert("passengerPhoneRequired"); return; } await assertPlatformFeatureEnabled("ride_publishing_enabled", "Ride publishing"); if (paymentSetupRelaxedForTesting()) { await ensureStagingPaymentAccountForTesting("passenger", state.passenger).catch((error) => { logClientWarning("Staging passenger payment setup could not be prepared before ride publishing.", error); }); } if (hasSupabaseRuntime()) { await loadMarketplaceFromSupabase().catch((error) => { logClientWarning("Marketplace refresh before active ride check was skipped.", error); }); } const pendingRide = passengerPendingRide(); if (pendingRide) { state.selectedRequestId = pendingRide.id; state.passengerPage = "trips"; saveState(); renderAll(); translatedAlert("publishRideFailed", { message: passengerPendingRideMessage(pendingRide) }); return; } if (!paymentAccountReady("passenger", state.passenger) && hasSupabaseRuntime()) { await refreshPaymentAccountsFromSupabase("passenger"); await loadMarketplaceFromSupabase().catch((error) => { logClientWarning("Marketplace refresh before ride publish was skipped.", error); }); } if (!paymentAccountReady("passenger", state.passenger)) { state.passengerPage = "payment"; saveState(); renderAll(); translatedAlert("passengerPaymentRequired"); return; } const fareMode = passengerFareMode(); const manualFareOnly = typeof passengerManualFareOnly === "function" && passengerManualFareOnly(); const vehicleDesignation = normalizeCarTypePreference(els.vehiclePreference.value); const vehicle = vehicleDesignation === "bike" ? "bike" : "car"; const tripDetails = currentRideTripDetails(); const requestCountry = selectedPassengerCountry(); const requestCity = selectedRidePickupCity(); let fareOffer = Number(String(els.fareOffer.value).replace(/[^\d]/g, "")); const minimumFare = minimumFareOffer(requestCountry); if (!fareOffer || fareOffer < minimumFare) { translatedAlert("publishRideFailed", { message: `Enter a fare offer of at least ${formatMoney(minimumFare, requestCountry)}.` }); return; } const destinationAddress = els.destination.value.trim(); if (!destinationAddress) { const message = passengerUiText("destinationRequiredBeforePublishing", "Enter a destination address before publishing."); if (els.destinationPlaceStatus) els.destinationPlaceStatus.textContent = message; translatedAlert("publishRideFailed", { message }); return; } if (manualFareOnly && destinationAddress.length < 3) { const message = passengerUiText("destinationLandmarkRequiredBeforePublishing", "Enter the destination address or a recognizable landmark before publishing."); if (els.destinationPlaceStatus) els.destinationPlaceStatus.textContent = message; translatedAlert("publishRideFailed", { message }); return; } const rideTiming = els.rideTiming.value; const scheduledDate = rideTiming === "scheduled" ? new Date(els.scheduledAt.value) : null; if (rideTiming === "scheduled" && (!els.scheduledAt.value || Number.isNaN(scheduledDate.getTime()))) { translatedAlert("scheduledTimeRequired"); return; } if (scheduledDate && scheduledDate.getTime() <= Date.now() + 30 * 60000) { translatedAlert("scheduleThirtyMinutes"); return; } await waitForRecentAddressConfirmations(); let useExactPickupLocation = passengerWantsCurrentPickup(); if (useExactPickupLocation) { await ensureCurrentPickupAddressForPublish(); } else { selectedCurrentPickupGps = null; } let enteredPickupDescription = els.pickupDescription.value.trim(); let gpsBeforeAddressLookup = useExactPickupLocation ? normalizeGpsPoint(selectedCurrentPickupGps ?? pendingPickupGps) : null; if (useExactPickupLocation) { const exactPickupIssue = gpsBeforeAddressLookup ? pickupGpsQualityIssue(gpsBeforeAddressLookup) : "Exact current location is required before publishing. Allow Location for this site, then capture pickup again."; if (exactPickupIssue) { const typedPickupFallbackReady = manualFareOnly && enteredPickupDescription && !pickupUsesCurrentLocationText(enteredPickupDescription) && !pickupUsesGpsFallbackText(enteredPickupDescription); if (typedPickupFallbackReady) { useExactPickupLocation = false; selectedCurrentPickupGps = null; gpsBeforeAddressLookup = null; if (els.pickupUseCurrentLocation) els.pickupUseCurrentLocation.checked = false; setPassengerUiStatus( els.pickupGpsStatus, "pickupGpsFallbackStatus", "Exact pickup GPS was not clear enough, so Waka will publish using the typed pickup address instead." ); setPassengerUiStatus( els.pickupPlaceStatus, "publishTypedPickupStatus", "Publishing with the typed pickup address and selected city/area." ); } else { if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = exactPickupIssue; if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = exactPickupIssue; await waitForPassengerUiPaintBeforeAlert(); translatedAlert("publishRideFailed", { message: exactPickupIssue }); return; } } } let pickupDescription = useExactPickupLocation ? enteredPickupDescription || currentPickupLocationLabel(selectedCurrentPickupGps ?? pendingPickupGps) : enteredPickupDescription; if (useExactPickupLocation) { pickupDescription = await resolveCurrentPickupAddressForPublish(gpsBeforeAddressLookup, pickupDescription); } let currentPickupGps = useExactPickupLocation ? normalizeGpsPoint(selectedCurrentPickupGps) ?? gpsBeforeAddressLookup : null; enteredPickupDescription = els.pickupDescription.value.trim(); const pickupAddressIssue = exactPickupAddressIssue(pickupDescription); if (pickupAddressIssue) { if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = pickupAddressIssue; if (els.pickupDescription instanceof HTMLElement) { window.setTimeout(() => els.pickupDescription.focus(), 0); } await waitForPassengerUiPaintBeforeAlert(); translatedAlert("publishRideFailed", { message: pickupAddressIssue }); return; } let pickupGpsForRoute = useExactPickupLocation ? currentPickupGps : null; let pickupOriginDescription = useExactPickupLocation && currentPickupGps ? currentPickupLocationLabel(currentPickupGps) : pickupDescription; let pickupOrigin = routeOriginForEstimate( requestCountry, requestCity, els.pickupArea.value, pickupOriginDescription, pickupGpsForRoute ); if (!manualFareOnly && !routeOriginIsSpecific(pickupOrigin)) { const message = useExactPickupLocation ? passengerUiText("currentLocationCouldNotConfirmPublish", "Exact current location could not be confirmed. Enter the pickup address or a recognizable landmark.") : passengerUiText("completePickupAddressBeforePublish", "Enter a complete pickup address or recognizable landmark before publishing."); if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = message; translatedAlert("publishRideFailed", { message }); return; } if (!enteredPickupDescription && pickupOrigin.source === "browser-gps" && els.pickupDescription) { els.pickupDescription.value = pickupDescription; setPassengerUiStatus( els.pickupPlaceStatus, "currentPickupConfirmedStatus", "Using the confirmed pickup address from current location." ); } let requestPickupGps = requestPickupGpsFromRouteOrigin( pickupOrigin, useExactPickupLocation ? currentPickupGps ?? pendingPickupGps : null ); let landmarkReminderNeeded = false; let pickupLandmarkReminderNeeded = false; let destinationLandmarkReminderNeeded = false; const markPickupLandmarkReminder = (key = "typedPickupStatusBeforePublish", fallback = "Pickup was not matched to a saved GPS place. Waka can still publish it, but add a clear landmark so the rider can find you.") => { landmarkReminderNeeded = true; pickupLandmarkReminderNeeded = true; setPassengerUiStatus(els.pickupPlaceStatus, key, fallback); }; if (manualFareOnly && !useExactPickupLocation && !validGpsCoordinate(Number(pickupOrigin?.latitude), Number(pickupOrigin?.longitude))) { requestPickupGps = null; } const pickupPlace = typeof pickupPlaceForRoute === "function" ? pickupPlaceForRoute(enteredPickupDescription) : null; const pickupHasSavedMapPoint = Boolean( pickupPlace?.placeId || validGpsCoordinate(Number(pickupPlace?.latitude), Number(pickupPlace?.longitude)) ); const pickupHasReliableGps = Boolean(requestPickupGps && !pickupGpsQualityIssue(requestPickupGps)); const typedPickupNeedsLandmarkReminder = Boolean( enteredPickupDescription && !pickupHasSavedMapPoint && !pickupHasReliableGps && !pickupUsesCurrentLocationText(enteredPickupDescription) && !pickupUsesGpsFallbackText(enteredPickupDescription) ); const phoneGpsForTypedPickup = await maybeAttachPhoneGpsToTypedPickupForMatching( typedPickupNeedsLandmarkReminder && !useExactPickupLocation && !requestPickupGps ); if (phoneGpsForTypedPickup) { requestPickupGps = phoneGpsForTypedPickup; pickupGpsForRoute = phoneGpsForTypedPickup; pickupOriginDescription = currentPickupLocationLabel(phoneGpsForTypedPickup); pickupOrigin = routeOriginForEstimate( requestCountry, requestCity, els.pickupArea.value, pickupOriginDescription, pickupGpsForRoute ); } const pickupHasReliableGpsAfterAssist = Boolean(requestPickupGps && !pickupGpsQualityIssue(requestPickupGps)); if ((typedPickupNeedsLandmarkReminder || (!requestPickupGps && pickupOrigin?.source === "typed-address")) && !pickupHasReliableGpsAfterAssist) { markPickupLandmarkReminder(); } if (pickupOrigin.source === "browser-gps" || currentPickupGps) { const pickupGpsIssue = pickupGpsQualityIssue(requestPickupGps); if (pickupGpsIssue) { const canPublishTypedPickup = Boolean( enteredPickupDescription && !pickupUsesCurrentLocationText(enteredPickupDescription) && !pickupUsesGpsFallbackText(enteredPickupDescription) ); if (canPublishTypedPickup) { requestPickupGps = null; pickupGpsForRoute = null; if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = pickupGpsIssue; markPickupLandmarkReminder( "pickupGpsFallbackStatus", "Exact pickup GPS was not clear enough, so Waka will publish using the typed pickup address instead." ); } else { const message = passengerUiText( "currentLocationCouldNotConfirmPublish", "Exact current location could not be confirmed. Enter the pickup address or a recognizable landmark." ); if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = pickupGpsIssue; if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = message; await waitForPassengerUiPaintBeforeAlert(); translatedAlert("publishRideFailed", { message }); return; } } } let guidance = updateFareGuidance(); const previewGuidance = guidance; if (routeEstimatesEnabled()) { const publishGuidanceKey = routeGuidanceInputKey( requestCountry, requestCity, els.pickupArea.value, els.destinationArea.value, pickupOriginDescription, els.destination.value.trim(), rideStopsFormValue(), pickupGpsForRoute, selectedDestinationPlace, tripDetails ); let confirmedGuidance = cachedConfirmedFareGuidanceForKey(publishGuidanceKey); if (!confirmedGuidance && fareGuidanceInFlightKey === publishGuidanceKey) { if (els.fareGuidance) els.fareGuidance.textContent = "Finishing accurate driving distance before publishing..."; confirmedGuidance = await waitForConfirmedFareGuidance(publishGuidanceKey); } if (confirmedGuidance) { guidance = confirmedGuidance; rememberStablePassengerFareGuidance(publishGuidanceKey, guidance); if (els.fareGuidance) els.fareGuidance.textContent = fareGuidanceMessage(guidance); } else { if (els.fareGuidance) els.fareGuidance.textContent = "Checking accurate driving distance before publishing..."; try { const accurateGuidance = await accurateFareGuidanceForRide( requestCountry, requestCity, els.pickupArea.value, els.destinationArea.value, els.destination.value.trim(), pickupGpsForRoute, rideStopsFormValue(), pickupOriginDescription, selectedDestinationPlace, tripDetails ); guidance = accurateGuidance ?? previewGuidance; if (routeGuidanceConfirmedForPublish(guidance)) { lastRouteFareGuidance = guidance; lastRouteFareGuidanceKey = publishGuidanceKey; rememberStablePassengerFareGuidance(publishGuidanceKey, guidance); } if (els.fareGuidance) { els.fareGuidance.textContent = guidance ? fareGuidanceMessage(guidance) : routeEstimateErrorMessage(lastRouteEstimateError); } } catch (error) { if (els.fareGuidance) els.fareGuidance.textContent = routeEstimateErrorMessage(error); translatedAlert("publishRideFailed", { message: error.message }); return; } } if (!routeGuidanceConfirmedForPublish(guidance)) { const message = routeEstimateErrorMessage(lastRouteEstimateError); if (els.fareGuidance) els.fareGuidance.textContent = message; translatedAlert("publishRideFailed", { message }); return; } } if (guidance && fareMode === "negotiable") { const minimumAllowedFare = passengerMinimumAllowedFare(guidance, requestCountry); if (fareOffer < minimumAllowedFare) { showPassengerMinimumFareBlock(guidance, fareOffer, minimumAllowedFare); return; } } if (guidance && fareMode === "negotiable" && vehicleDesignation === "suv" && fareOffer <= guidance.max) { showXlSpecialFareBlock(guidance, fareOffer); return; } clearLowFareReview(); const paymentPreference = validPaymentPreferenceForCountry(els.paymentPreference.value || "online_card", requestCountry); els.paymentPreference.value = paymentPreference; const rideStops = normalizeRideStops(rideStopsFormValue()); const rideStopPoints = rideStopPointsForRoute(rideStops); if (!manualFareOnly && !rideStopPointsComplete(rideStops, rideStopPoints)) { translatedAlert("publishRideFailed", { message: "Choose each stop from the address suggestions so Waka can verify rider arrival at every stop." }); return; } const destinationPlace = destinationPlaceForRoute(els.destination.value.trim()); if (!destinationPlace) { landmarkReminderNeeded = true; destinationLandmarkReminderNeeded = true; setPassengerUiStatus( els.destinationPlaceStatus, "typedDestinationStatusBeforePublish", "Destination was not matched to a saved GPS place. Waka can still publish it, but riders need a clear destination landmark." ); } if (landmarkReminderNeeded) { const message = passengerUiText( pickupLandmarkReminderNeeded && destinationLandmarkReminderNeeded ? "landmarkReminderConfirmBoth" : pickupLandmarkReminderNeeded ? "landmarkReminderConfirmPickup" : "landmarkReminderConfirmDestination", "The pickup or destination was not matched to a saved GPS place. Continue only if the typed address includes a clear landmark. Choose Continue to publish, or Cancel to return and correct the address." ); const ok = typeof showWakaGoodConfirm === "function" ? await showWakaGoodConfirm(message) : confirm(message); if (!ok) { const target = pickupLandmarkReminderNeeded ? els.pickupDescription : els.destination; window.setTimeout(() => target?.focus?.(), 0); return; } } const publishedPickupArea = pickupAreaForPublish( requestCountry, requestCity, els.pickupArea.value, pickupDescription, requestPickupGps ); const publishedDestinationArea = destinationAreaForPublish( requestCountry, requestCity, els.destinationArea.value, els.destination.value.trim(), destinationPlace ); const businessAccountId = automaticRideBillingAccountId(); if (els.rideBillingAccount) els.rideBillingAccount.value = businessAccountId ?? ""; const businessAccount = businessAccountId ? passengerBusinessAccounts().find((account) => account.id === businessAccountId) : null; if (businessAccountId && !businessAccountCanRequest(businessAccount)) { translatedAlert("publishRideFailed", { message: "Business account must be Waka-verified with an active free month, Starter billing, or active Partner billing before posting a business ride." }); return; } const request = { id: makeId("request"), passengerId: state.passenger.id, passengerName: state.passenger.name, passengerPhone: state.passenger.phone, businessAccountId, country: requestCountry, city: requestCity, pickupArea: publishedPickupArea, pickupDescription, destinationArea: publishedDestinationArea, destination: els.destination.value.trim(), destinationPlaceId: destinationPlace?.placeId ?? null, destinationFormattedAddress: destinationPlace?.formattedAddress ?? null, destinationLatitude: destinationPlace?.latitude ?? null, destinationLongitude: destinationPlace?.longitude ?? null, vehicle, carTypePreference: vehicleDesignation, rideStops, rideStopPoints, passengerCount: tripDetails.passengerCount, luggageCount: tripDetails.luggageCount, luggageNote: tripDetails.luggageNote, fareOffer, fareMode, estimatedDistanceMiles: guidance?.distanceMiles ?? null, estimatedTravelMinutes: guidance?.minutes ?? null, routeEstimateSource: normalizedRouteEstimateSourceForDatabase(guidance?.source ?? (manualFareOnly ? "manual" : null)), routeEstimateProvider: normalizedRouteEstimateProviderForDatabase(guidance?.source ?? (manualFareOnly ? "manual" : null), guidance?.provider ?? (manualFareOnly ? "manual-fare" : null)), routeEstimateCached: Boolean(guidance?.cached), routeEstimateKey: guidance?.routeKey ?? null, routeEstimatePolyline: guidance?.routePolyline ?? null, routeEstimateDestinationFingerprint: guidance?.destinationFingerprint ?? null, routeEstimateCreatedAt: guidance?.estimatedAt ?? null, paymentPreference, pickupGps: requestPickupGps, pickupLatitude: requestPickupGps?.latitude ?? null, pickupLongitude: requestPickupGps?.longitude ?? null, pickupGpsAccuracyMeters: requestPickupGps?.accuracyMeters ?? null, pickupGpsCapturedAt: requestPickupGps?.capturedAt ?? null, rideTiming, scheduledAt: scheduledDate?.toISOString() ?? null, riderConfirmationStatus: null, riderConfirmationRequestedAt: null, riderConfirmedAt: null, releasedAt: null, status: "open", selectedOfferId: null, createdAt: new Date().toISOString() }; try { await ensurePassengerProfileMatchesRidePickupLocation(requestCountry, requestCity); const savedRequest = await saveRideRequestToSupabase(request); rememberRideRouteAddresses(savedRequest); state.requests.unshift(savedRequest); state.selectedRequestId = savedRequest.id; state.passengerPage = "trips"; passengerWorkspacePageSelectedInSession = true; if (typeof updatePassengerWorkspaceRoute === "function") { updatePassengerWorkspaceRoute("trips", { replace: true, requestId: savedRequest.id, preferPathRoute: true }); } pendingPickupGps = null; selectedPickupPlace = null; selectedCurrentPickupGps = null; selectedDestinationPlace = null; hidePickupSuggestions(); hideDestinationSuggestions(); if (els.pickupUseCurrentLocation) els.pickupUseCurrentLocation.checked = false; if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = "Exact pickup location is off."; setPassengerUiStatus(els.pickupPlaceStatus, "pickupEntryPrompt", "Enter a pickup address, or check exact current location."); setPassengerUiStatus(els.destinationPlaceStatus, "destinationEntryPrompt", "Start typing a destination address."); els.rideRequestForm.reset(); if (els.pickupCity) els.pickupCity.value = state.passenger.city; if (els.ridePassengerCount) els.ridePassengerCount.value = "1"; if (els.rideLuggageCount) els.rideLuggageCount.value = "0"; if (els.rideLuggageNote) els.rideLuggageNote.value = ""; updatePassengerFareModeControls(); updateScheduledRideControls(); setPassengerRiderAvailabilityMessage("Enter a pickup address or check exact current location to see nearby rider availability."); setRideStopsInputEnabled(false); populateLocationFields(); saveState(); renderAll(); void refreshMarketplace({ silent: true }); const publishedMessage = passengerRidePublishedMessage(savedRequest); els.selectedSummary.textContent = publishedMessage; } catch (error) { translatedAlert("publishRideFailed", { message: passengerRidePublishErrorMessage(error) }); } } catch (error) { const message = passengerRidePublishErrorMessage(error); if (els.fareGuidance) els.fareGuidance.textContent = message; translatedAlert("publishRideFailed", { message }); } finally { if (els.rideRequestForm) delete els.rideRequestForm.dataset.submitting; } } async function signOutRole(type) { if (typeof rememberWorkspaceUiState === "function") rememberWorkspaceUiState(type); if (type === "passenger" || type === "rider") { if (typeof clearPasswordResetMode === "function") clearPasswordResetMode(type); if (typeof clearPasswordResetLocationFlag === "function") clearPasswordResetLocationFlag(); } if (type === "passenger") { stopAutomaticPassengerPickupGps(); stopPassengerApproachAutoRefresh(); clearRecentAddressHistory(); pendingPickupGps = null; selectedPickupPlace = null; } if (type === "rider") { stopAutomaticRiderGps(); const rider = currentRiderRecord(); if (riderCurrentGps(rider)) { try { await clearRiderLiveGpsInSupabase(clearRiderLiveGpsFields(rider)); } catch (error) { logClientWarning("Could not clear rider live GPS before sign-out.", error); } } } if (isSupabaseMode()) { await clearStaleSupabaseSession(); } supabaseRestSession = null; updateConnectionStatus(); state.sessions[type] = null; state.accountMode[type] = "signin"; if (type === "passenger") { state.passenger = null; state.selectedRequestId = null; pendingPickupGps = null; selectedPickupPlace = null; selectedDestinationPlace = null; hidePickupSuggestions(); hideDestinationSuggestions(); els.passengerSignInPassword.value = ""; setTranslatedStatus(els.passengerSignInStatus, "signedOut"); } if (type === "rider") { state.riderAvailabilityActivated = false; riderAutoGpsPaused = true; lastRiderAutoGpsSyncAt = 0; lastRiderAutoGpsSyncPoint = null; state.rider = null; els.riderSignInPassword.value = ""; setTranslatedStatus(els.riderSignInStatus, "signedOut"); } saveState(); populateLocationFields(); hydrateForms(); renderAll(); } // Rider-facing onboarding, vehicle, eligibility, tax, subscription, availability, and marketplace UI. const subscriptionReminderCheckStorageKey = "waka-subscription-reminder-check-v1"; const subscriptionReminderChecksInFlight = new Set(); const riderProfilePhotoUrlCache = new Map(); function riderUiText(key, fallback = "", values = {}) { const translated = typeof translatedMessage === "function" ? translatedMessage(key, values) : typeof translatedValue === "function" ? translatedValue(key) : ""; return translated || fallback; } function riderSelfBackgroundCheckRecords(riderId = state.rider?.id) { if (!riderId) return []; return (state.backgroundChecks ?? []) .filter((record) => record.riderId === riderId) .sort((a, b) => new Date(b.completedAt ?? b.createdAt ?? 0) - new Date(a.completedAt ?? a.createdAt ?? 0)); } function latestRiderSelfBackgroundCheck(rider = currentRiderRecord()) { return riderSelfBackgroundCheckRecords(rider?.id)[0] ?? null; } function normalizedRiderBackgroundStatus(rider = currentRiderRecord()) { const latest = latestRiderSelfBackgroundCheck(rider); return String(latest?.status ?? rider?.backgroundCheckStatus ?? "not requested").replace(/_/g, " ").toLowerCase(); } function normalizedRiderBackgroundDecision(rider = currentRiderRecord()) { const latest = latestRiderSelfBackgroundCheck(rider); return String(latest?.decision ?? rider?.backgroundCheckDecision ?? "pending").replace(/_/g, " ").toLowerCase(); } function riderTestingRelaxationEnabled(flagName) { return (appConfig[flagName] === true || String(appConfig[flagName] ?? "").toLowerCase() === "true") && /\b(staging|pilot|test|preview)\b/i.test(String(appConfig.projectName || "")); } function riderBackgroundCheckRelaxedForTesting() { return riderTestingRelaxationEnabled("relaxBackgroundCheckForTesting"); } function riderManualBackgroundReviewMode() { return /\b(manual|admin|not[-_ ]?required|none|disabled|cameroon)/i.test(String(appConfig.backgroundCheckProvider || "")); } function riderCanStartBackgroundCheckFromStatus(rider = currentRiderRecord()) { if (riderManualBackgroundReviewMode()) return false; return rider?.status === "background_pending" || (rider?.status === "pending" && riderBackgroundCheckRelaxedForTesting()); } function riderBackgroundCheckStep(rider = currentRiderRecord()) { if (riderManualBackgroundReviewMode()) { if (rider?.status === "approved") return ["complete", "Admin safety review", "Waka admin review is complete."]; if (rider?.status === "background_pending") return ["current", "Admin safety review", "Waka admin is reviewing local documents, vehicle details, permit, and safety information."]; return ["locked", "Admin safety review", "Waka admin reviews local documents before approval."]; } const status = normalizedRiderBackgroundStatus(rider); const decision = normalizedRiderBackgroundDecision(rider); const riderStatus = rider?.status ?? ""; if (riderStatus === "pending" && riderBackgroundCheckRelaxedForTesting()) { return ["current", "Background check", "Testing mode allows the rider to start the Checkr step from Eligibility while admin review is pending."]; } if (["pending", "needs_correction", "profile only"].includes(riderStatus)) { return ["locked", "Background check", "Admin will unlock Checkr only after the application details pass initial review."]; } if (!["background_pending", "approved"].includes(riderStatus)) { return ["locked", "Background check", "Provider screening opens only after admin invites the rider to Checkr."]; } if (decision === "clear") return ["complete", "Background check", "Provider screening is clear and available to admin."]; if (decision === "consider") return ["current", "Background check", "Provider screening needs admin review before approval."]; if (decision === "adverse") return ["locked", "Background check", "Provider returned an adverse status; contact Waka support."]; if (["requested", "running", "review"].includes(status)) return ["current", "Background check", "Provider screening is in progress."]; if (rider?.status === "background_pending") return ["current", "Background check", "Complete Checkr screening from Eligibility checks."]; return ["locked", "Background check", "Admin will unlock Checkr only after the application details pass initial review."]; } function riderBackgroundCheckReadyForAdminReview(rider = currentRiderRecord()) { return ["clear", "consider"].includes(normalizedRiderBackgroundDecision(rider)); } function riderWorkspaceStatusMessage(rider = currentRiderRecord()) { if (!rider) return riderUiText("riderStatusSignInOrApply", "Sign in or submit an application to access the rider platform."); if (rider.needsApplication || rider.status === "profile only") { return riderUiText("riderStatusProfileOnly", "Your rider login is active, but Waka does not yet have a rider application for admin review. Complete only the application details on Profile; do not create a second account."); } if (rider.status === "pending") { if (directRidePaymentMode()) { return riderUiText("riderStatusPendingDirect", "Your rider application is waiting for Waka Cameroon admin review. Ride requests, offers, and chat unlock after the required document and safety review steps."); } return riderBackgroundCheckRelaxedForTesting() ? riderUiText("riderStatusPendingReviewTesting", "Your rider application is waiting for admin review. Stay on Eligibility to monitor progress and start the relaxed Checkr testing step.") : riderUiText("riderStatusPendingReview", "Your rider application is waiting for admin document review. Checkr, Stripe, ride requests, offers, and chat unlock only after the required review steps."); } if (rider.status === "background_pending") { return directRidePaymentMode() ? riderUiText("riderStatusBackgroundPendingDirect", "Your rider application passed initial admin review. Complete any requested local document or permit review so admin can make the final decision.") : riderUiText("riderStatusBackgroundPendingProvider", "Your rider application passed initial admin review. Complete the Checkr background check from Eligibility checks so admin can make the final decision."); } if (rider.status === "needs_correction") { return rider.reviewNote ? riderUiText("riderStatusCorrectionsWithNote", "Admin requested rider application corrections. Update the rider form and resubmit before review continues. Note: {note}", { note: rider.reviewNote }) : riderUiText("riderStatusCorrectionsNoNote", "Admin requested rider application corrections. Update the rider form and resubmit before review continues."); } if (rider.status === "declined") { return riderUiText("riderStatusDeclined", "Your rider application was declined by admin. Contact Waka support before submitting new documents."); } if (rider.status !== "approved") { return riderUiText("riderStatusApprovalRequired", "Admin approval is required before the rider platform unlocks."); } if (!riderComplianceReady(rider)) { return riderComplianceStatusText(rider); } const end = riderAccessEnd(rider); const remaining = daysUntil(end); const label = riderAccessLabel(rider); if (isSubscriptionActive(rider)) { const gpsSetupMessage = riderCurrentFreshGps(rider) ? null : riderCurrentGps(rider) ? riderUiText("riderGpsTapActivateAgain", "{summary} Tap Activate again so nearby requests refresh from your current position.", { summary: riderLiveGpsStatusSummary(rider) }) : riderUiText("riderGpsNeededBeforeRequests", "Live GPS is still needed before nearby requests can appear. Tap Activate when you are ready to receive rides."); const setupGaps = [ paymentAccountReady("rider", rider) ? null : "payment account", riderCurrentFreshGps(rider) ? null : "availability activation" ].filter(Boolean); if (remaining > subscriptionRenewalNoticeDays) { const accessName = label === "free trial" ? "Free trial" : "Paid rider access"; return setupGaps.length ? riderUiText("riderApprovedSetupActive", "Approved. {accessName} is active until {date}. Complete {gaps} before requests appear. {gps}", { accessName, date: formatDate(end), gaps: setupGaps.join(", "), gps: gpsSetupMessage ?? "" }).trim() : directRidePaymentMode() ? riderUiText("riderApprovedRenewalDirect", "Approved. {accessName} is active until {date}. MTN/Orange renewal opens 3 days before expiry.", { accessName, date: formatDate(end) }) : riderUiText("riderApprovedRenewalProvider", "Approved. {accessName} is active until {date}. Payment choices open 3 days before expiry.", { accessName, date: formatDate(end) }); } const reminder = remaining <= subscriptionRenewalNoticeDays ? (directRidePaymentMode() ? ` ${riderUiText("riderRenewalReminderDirect", "Top up the rider wallet or pay monthly access before the free period ends.")}` : ` ${riderUiText("riderRenewalReminderProvider", "Renewal is due soon; choose manual payment or automatic renewal so paid access starts after this period ends.")}`) : ""; const setup = setupGaps.length ? ` ${riderUiText("riderCompleteSetupBeforeRequests", "Complete {gaps} before requests appear. {gps}", { gaps: setupGaps.join(", "), gps: gpsSetupMessage ?? "" }).trim()}` : ""; return riderUiText("riderApprovedRemaining", "Approved. Your {label} has {remaining} left, until {date}.{reminder}{setup}", { label, remaining: pluralDays(remaining), date: formatDate(end), reminder, setup }); } return directRidePaymentMode() ? riderUiText("riderApprovedWalletModel", "Approved. The Cameroon wallet/free-rides model applies after the free period; top up the rider wallet or pay monthly access from Eligibility checks.") : riderUiText("riderApprovedPaidInactive", "Approved, but paid rider access is inactive. Choose a Waka Rider Access payment before receiving and responding to ride requests."); } function riderFlowModel(rider = currentRiderRecord()) { const vehicleName = "Car"; const location = rider?.city && rider?.country ? `${rider.city}, ${rider.country}` : "Market not set"; const baseMeta = rider ? [`${vehicleName} platform`, location, `Status: ${rider.status ?? "not submitted"}`] : []; if (!rider) { const manualReview = riderManualBackgroundReviewMode(); return { title: "Application not submitted", summary: "Create a rider account and submit required documents for admin review.", meta: baseMeta, steps: [ ["current", "Account", "Create rider profile and upload required documents."], ["locked", "Admin document review", manualReview ? "Admin checks local documents, vehicle details, permit, and emergency contact." : "Admin checks the application before Waka pays for Checkr screening."], ["locked", manualReview ? "Manual safety review" : "Background check", manualReview ? "Provider billing is disabled for the Cameroon MVP." : "Provider screening opens only after admin invites you."], ["locked", "Admin review", manualReview ? "Admin approval depends on documents, local safety review, and eligibility." : "Admin approval depends on documents, background check, and eligibility."], ["locked", directRidePaymentMode() ? "Free period and rider wallet" : "Free trial and paid rider access", directRidePaymentMode() ? "The free period starts after admin approval. Then 5 rides per day are free and ride 6+ uses the rider wallet unless monthly access is active." : "The free trial starts after approval. Waka Rider Access applies after the trial."], ["locked", "Ride requests", "Vehicle-matching requests unlock after approval and active access."] ] }; } if (rider.status === "pending") { const manualReview = riderManualBackgroundReviewMode(); const relaxedBackground = riderBackgroundCheckRelaxedForTesting(); return { title: "Application pending", summary: manualReview ? "Waka admin is reviewing your Cameroon rider application, documents, vehicle details, permit, and emergency contact." : relaxedBackground ? "Admin is reviewing your application. Eligibility remains the only rider page during review, and staging lets you start the Checkr testing step without production provider cost." : "Admin is reviewing your application details and documents first. Checkr stays locked until admin invites you, so Waka does not pay for a background check before corrections are handled.", meta: baseMeta, steps: [ ["complete", "Application submitted", "Profile and rider documents are saved for review."], ["current", "Admin document review", manualReview ? "Admin checks your application before approving or requesting corrections." : "Admin checks your application before releasing the paid background-check step."], manualReview ? ["current", "Manual safety review", "No Checkr/provider step is required in the Cameroon MVP."] : relaxedBackground ? ["current", "Checkr testing step", "Open Eligibility checks and start the relaxed Checkr background-check flow for staging."] : ["locked", "Checkr background check", "This unlocks only after admin confirms the application is ready."], ["locked", "Final admin decision", manualReview ? "Admin approves, declines, or requests corrections after local review." : "Admin approves or declines after Checkr results return."], ["locked", directRidePaymentMode() ? "Free period" : "Free trial", directRidePaymentMode() ? "The rider free period starts after admin approval." : "The rider free trial starts after admin approval."], ["locked", "Ride requests", "Marketplace access is blocked while pending."] ] }; } if (rider.status === "background_pending") { const manualReview = riderManualBackgroundReviewMode(); return { title: manualReview ? "Manual safety review" : "Background check invited", summary: manualReview ? "Admin has moved your application into final local safety review. Watch for corrections or approval." : "Admin has approved your application details for Checkr screening. Open Eligibility checks, start the background check, and follow the provider email, SMS, or hosted instructions so admin can see the result.", meta: [...baseMeta, manualReview ? "Manual review" : "Checkr unlocked"], steps: [ ["complete", "Application submitted", "Profile and rider documents are saved."], ["complete", "Admin document review", manualReview ? "Admin moved the application into final local review." : "Admin released the background-check step."], riderBackgroundCheckStep(rider), [manualReview || riderBackgroundCheckReadyForAdminReview(rider) ? "current" : "locked", "Final admin decision", manualReview ? "Admin approves, declines, or requests corrections after local review." : "Admin approves or declines after Checkr results return."], ["locked", directRidePaymentMode() ? "Free period" : "Free trial", directRidePaymentMode() ? "The rider free period starts after final admin approval." : "The rider free trial starts after final approval."], ["locked", "Ride requests", "Marketplace access is blocked until approval."] ] }; } if (rider.status === "needs_correction") { return { title: "Application corrections needed", summary: rider.reviewNote || "Admin reopened the rider application for updates. Make corrections and resubmit from Profile.", meta: [...baseMeta, "Corrections requested"], steps: [ ["complete", "Application submitted", "Admin reviewed the first submission."], ["current", "Rider corrections", "Update the rider form and resubmit for admin review."], ["locked", "Admin document review", "Review continues after corrected resubmission."], ["locked", riderManualBackgroundReviewMode() ? "Manual safety review" : "Checkr background check", riderManualBackgroundReviewMode() ? "Admin resumes local review after corrections pass initial review." : "Admin unlocks Checkr only after corrections pass initial review."], ["locked", "Final admin decision", riderManualBackgroundReviewMode() ? "Admin approves, declines, or asks for more correction after local review." : "Admin approves or declines after Checkr results return."], ["locked", "Ride requests", "Marketplace access remains blocked until approval."] ] }; } if (rider.status === "declined") { return { title: "Application declined", summary: "Rider access is blocked until Waka support or admin resolves the application.", meta: baseMeta, steps: [ ["complete", "Application submitted", "The rider application was reviewed."], riderBackgroundCheckStep(rider), ["locked", "Admin decision", "The current decision is declined."], ["locked", directRidePaymentMode() ? "Rider wallet/payment" : "Paid rider access", "No rider access is active."], ["locked", "Ride requests", "Marketplace access remains blocked."] ] }; } if (rider.status === "approved" && isSubscriptionActive(rider)) { if (!riderComplianceReady(rider)) { return { title: "Compliance renewal required", summary: riderComplianceStatusText(rider), meta: [...baseMeta, "License or insurance renewal needed"], steps: [ ["complete", "Application approved", "Admin approval is recorded."], ["locked", "License and insurance compliance", riderComplianceStatusText(rider)], ["locked", "Rider availability", "Service is blocked until compliance dates are current."], ["locked", "Ride requests", "Marketplace access remains blocked while compliance is expired or missing."] ] }; } const end = riderAccessEnd(rider); const walletModelActive = directRidePaymentMode() && !end; const remaining = walletModelActive ? Number.POSITIVE_INFINITY : daysUntil(end); const label = riderAccessLabel(rider); const paymentReady = paymentAccountReady("rider", rider); const directPayment = directRidePaymentMode(); const accessHealthy = walletModelActive || remaining > subscriptionRenewalNoticeDays; const paymentLabel = directPayment ? "Direct fare payment active" : paymentSetupRelaxedForTesting() ? "Staging payout relaxed" : "Payment linked"; const regionsReady = true; const gpsReady = Boolean(riderCurrentFreshGps(rider)); const canActivate = riderCanActivateAvailability(rider); const readyForRequests = paymentReady && regionsReady && gpsReady; return { title: readyForRequests ? `${vehicleName} rider platform active` : "Rider setup required", summary: readyForRequests ? (walletModelActive ? "Wallet/free-ride model active. First 5 completed rides each day are free; later rides use wallet commission unless monthly access is active." : accessHealthy ? `${label === "free trial" ? "Free trial" : "Paid rider access"} active until ${formatDate(end)}.` : `${label === "free trial" ? "Free trial" : "Paid rider access"} has ${pluralDays(remaining)} left.`) : (directPayment ? "Activate rider availability before requests appear. Rider platform access renews through MTN/Orange." : paymentSetupRelaxedForTesting() ? "Activate rider availability before requests appear. Stripe payout setup is relaxed for staging." : "Payment account and rider activation are required before requests appear."), meta: [ ...baseMeta, walletModelActive ? "Wallet/free-rides model active" : `Access until ${formatDate(end)}`, walletModelActive ? "Wallet top-up available" : remaining <= subscriptionRenewalNoticeDays ? "Renewal reminder active" : "Renewal reminder off", paymentReady ? paymentLabel : "Payment needed", "Nearby pickup scope active", riderDestinationScopeLabel(), gpsReady ? "Available online" : "Availability offline" ], steps: [ ["complete", "Eligibility approved", "Documents and background-check decision passed admin review."], ["complete", "Application approved", "Admin approval is complete."], ["current", walletModelActive ? "Wallet/free rides" : label === "free trial" ? "Free period" : "Paid rider access", walletModelActive ? "5 rides/day are free after the trial; ride 6+ deducts 15% from the wallet." : accessHealthy ? "Payment choices open 3 days before expiry." : `${pluralDays(remaining)} left before payment is required.`], [paymentReady ? "complete" : "current", directPayment ? "Direct fare payment" : "Payout account", paymentReady ? (directPayment ? "Passenger-to-rider cash/mobile-money fare payment is active." : paymentSetupRelaxedForTesting() ? "Staging allows testing without Stripe Connect." : "Payout account is saved.") : "Save a payout account before receiving requests."], ["complete", "Marketplace scope", "Showing all nearby pickups within the pickup radius."], [gpsReady ? "complete" : canActivate ? "current" : "locked", "Rider availability", gpsReady ? "You are online for nearby requests." : "Activate availability from the Initialize rider availability menu before requests appear."], [readyForRequests ? "complete" : "current", "Ride requests", readyForRequests ? `${vehicleName} rider sees matching passenger requests and can accept or counter-offer.` : (paymentSetupRelaxedForTesting() ? "Incoming requests appear after availability is activated." : "Incoming requests appear after payout and activation are ready.")] ] }; } return { title: "Rider access payment required", summary: directRidePaymentMode() ? "The free period has ended. The Cameroon wallet/free-rides model applies: 5 completed rides per day are free, then ride 6+ uses the rider wallet unless monthly access is active." : "The free trial has ended. Rider access is paused until the provider confirms Waka Rider Access payment.", meta: [...baseMeta, riderPlanSummary()], steps: [ ["complete", "Eligibility approved", "Documents and background-check decision passed admin review."], ["complete", "Application approved", "Admin approval is complete."], ["locked", directRidePaymentMode() ? "Free period ended" : "Trial or paid access inactive", directRidePaymentMode() ? "Daily free rides and wallet deductions now apply." : "The free trial has ended and paid access is not active."], ["current", directRidePaymentMode() ? "Rider wallet/payment" : "Rider access payment", directRidePaymentMode() ? "Top up the wallet from 5,000 FCFA or pay 15,000 FCFA monthly access." : "Open Waka Rider Access checkout to restore rider tools."], ["locked", "Ride requests", directRidePaymentMode() ? "Marketplace access follows the Cameroon wallet/free-rides model." : "Marketplace access is blocked until subscription is active."] ] }; } function riderOverviewCard({ label, value, detail, target, tone = "neutral" }) { const targetAttribute = target ? ` data-rider-overview-target="${escapeHtml(target)}"` : ""; return ` `; } function riderOverviewStatusCards(rider = currentRiderRecord()) { const availablePages = typeof availableRiderWorkspacePages === "function" ? availableRiderWorkspacePages(rider) : riderWorkspacePages; const targetOrNull = (page) => availablePages.includes(page) ? page : null; const status = rider?.status ?? "not submitted"; const eligibility = { pending: ["Admin review", riderManualBackgroundReviewMode() ? "Monitor local document and safety review." : "Monitor review and Checkr testing progress.", "warning", "checks"], background_pending: [riderManualBackgroundReviewMode() ? "Final review" : "Checkr ready", riderManualBackgroundReviewMode() ? "Waka admin is completing final local review." : "Open Eligibility checks to complete provider screening.", "current", "checks"], needs_correction: ["Corrections needed", "Update the Profile form and resubmit.", "warning", "profile"], approved: ["Approved", "Eligibility is complete. Keep setup active.", "ready", "checks"], declined: ["Declined", "Contact Waka support before continuing.", "danger", "support"], suspended: ["Suspended", "Rider access is paused by admin.", "danger", "support"], "profile only": ["Application needed", "Complete the rider application from Profile.", "warning", "profile"] }[status] ?? ["Not submitted", "Create or finish the rider application.", "warning", "profile"]; const requestsReady = riderCanSeeRequests(rider); const regionsReady = true; const payoutReady = paymentAccountReady("rider", rider); const activeAccess = rider?.status === "approved" && isSubscriptionActive(rider); const availabilityReady = Boolean(riderCurrentFreshGps(rider)); const blockingRide = riderBlockingImmediateRide(rider); let requestValue = "Locked"; let requestDetail = riderWorkspaceStatusMessage(rider); let requestTarget = rider?.status === "approved" ? "initialize" : "checks"; if (blockingRide) { requestValue = "On a ride"; requestDetail = blockingRide.status === "in_progress" ? "New immediate requests resume when this ride is about 7 minutes from drop-off." : "New immediate requests pause while the matched ride is active."; requestTarget = "requests"; } else if (requestsReady) { requestValue = "Receiving"; requestDetail = "Incoming passenger requests appear here for accept or counter-offer."; requestTarget = "requests"; } else if (rider?.status !== "approved") { requestValue = "Admin review"; requestDetail = "Ride requests unlock after final admin approval."; requestTarget = "checks"; } else if (!activeAccess) { requestValue = directRidePaymentMode() ? "Payment needed" : "Subscription needed"; requestDetail = directRidePaymentMode() ? "Renew or restore rider access with MTN/Orange before ride requests appear." : "Renew or restore rider access before ride requests appear."; requestTarget = "checks"; } else if (!riderComplianceReady(rider)) { requestValue = "Renewal needed"; requestDetail = riderComplianceStatusText(rider); requestTarget = "profile"; } else if (!payoutReady) { requestValue = directRidePaymentMode() ? "Fare mode ready" : "Payout needed"; requestDetail = directRidePaymentMode() ? "Direct cash/mobile-money fare mode is active. Then use Initialize rider availability before opening Ride requests." : "Set up Stripe payout first. Then use Initialize rider availability before opening Ride requests."; requestTarget = "payment"; } else if (!regionsReady) { requestValue = "Choose regions"; requestDetail = "Open Initialize rider availability to choose preferred destinations or show all nearby pickups."; requestTarget = "initialize"; } else if (!availabilityReady) { requestValue = "Activate"; requestDetail = "Open Initialize rider availability and tap Activate when you are ready to receive nearby requests."; requestTarget = "initialize"; } const records = riderCompletedRideEarningRecords(rider); const now = new Date(); const monthTotal = periodEarningsTotal(records, new Date(now.getFullYear(), now.getMonth(), 1)); const earningsValue = records.length ? formatMoney(monthTotal, rider?.country) : formatMoney(0, rider?.country); const earningsDetail = records.length ? `${records.length} completed ride${records.length === 1 ? "" : "s"} loaded.` : "Completed ride earnings will appear after trips finish."; const payoutValue = payoutReady ? (directRidePaymentMode() ? "Fare mode ready" : "Stripe ready") : rider?.status === "approved" ? "Action needed" : "Locked"; const payoutDetail = payoutReady ? paymentAccountSummary("rider", rider) : rider?.status === "approved" ? (directRidePaymentMode() ? "Direct fare payment mode is active after approval; rider access is handled above." : "Set up Stripe payout before earnings can be sent.") : (directRidePaymentMode() ? "Direct fare payment mode opens after approval." : "Payout setup opens after approval."); return [ { label: "Eligibility", value: eligibility[0], detail: eligibility[1], tone: eligibility[2], target: targetOrNull(eligibility[3]) }, { label: "Ride access", value: requestValue, detail: requestDetail, tone: requestsReady ? "ready" : "warning", target: targetOrNull(requestTarget) }, { label: "Earnings this month", value: earningsValue, detail: earningsDetail, tone: records.length ? "ready" : "neutral", target: targetOrNull("earnings") }, { label: "Payout account", value: payoutValue, detail: payoutDetail, tone: payoutReady ? "ready" : rider?.status === "approved" ? "warning" : "neutral", target: targetOrNull("payment") } ]; } function renderRiderOverviewGrid(riderSignedIn = Boolean(hasSignedIn("rider") && state.rider), rider = currentRiderRecord()) { if (!els.riderOverviewGrid) return; const onOverview = typeof riderWorkspacePage !== "function" || riderWorkspacePage() === "overview"; els.riderOverviewGrid.hidden = !riderSignedIn || !onOverview; if (!riderSignedIn || !onOverview) { els.riderOverviewGrid.innerHTML = ""; return; } els.riderOverviewGrid.innerHTML = riderOverviewStatusCards(rider).map(riderOverviewCard).join(""); els.riderOverviewGrid.querySelectorAll("[data-rider-overview-target]").forEach((card) => { card.addEventListener("click", () => setRiderWorkspacePage(card.dataset.riderOverviewTarget)); }); } function openRiderCorrectionForm() { if (typeof setRiderWorkspacePage === "function") { setRiderWorkspacePage("profile"); } else { state.riderPage = "profile"; saveState(); renderAll(); } hydrateForms(); const note = currentRiderRecord()?.reviewNote; if (els.riderStatus) { els.riderStatus.textContent = `Admin requested corrections. Update the rider application form and resubmit.${note ? ` Note: ${note}` : ""}`; } setTimeout(() => { els.riderAccountForm?.scrollIntoView({ block: "start", behavior: "smooth" }); const firstEditable = els.riderAccountForm?.querySelector("input:not([type='hidden']), select, textarea"); firstEditable?.focus({ preventScroll: true }); }, 0); } function renderRiderFlow() { const riderSignedIn = Boolean(hasSignedIn("rider") && state.rider); const page = typeof riderWorkspacePage === "function" ? riderWorkspacePage() : "overview"; const rider = currentRiderRecord(); const showProgressOnChecks = page === "checks" && rider?.status !== "approved"; els.riderFlowCard.hidden = !riderSignedIn || !(page === "overview" || showProgressOnChecks); if (!riderSignedIn) return; const model = riderFlowModel(rider); const flowText = (text) => (typeof translatedStaticText === "function" ? translatedStaticText(text) : text); els.riderFlowTitle.textContent = flowText(model.title); els.riderFlowSummary.textContent = flowText(model.summary); els.riderFlowSteps.innerHTML = model.steps.map(([status, label, detail]) => `
${escapeHtml(flowText(label))} ${escapeHtml(flowText(detail))}
${escapeHtml(flowText(status))}
`).join(""); els.riderFlowMeta.innerHTML = model.meta.map((item) => chip(flowText(item))).join(""); if (els.riderFlowActions) { const needsCorrection = rider?.status === "needs_correction"; const backgroundPending = rider?.status === "background_pending"; els.riderFlowActions.hidden = !needsCorrection && !backgroundPending; els.riderFlowActions.innerHTML = needsCorrection ? `` : backgroundPending ? `` : ""; els.riderFlowActions.querySelector("#openRiderCorrectionForm")?.addEventListener("click", openRiderCorrectionForm); els.riderFlowActions.querySelector("#openRiderEligibilityChecks")?.addEventListener("click", () => setRiderWorkspacePage("checks")); } } function populateRiderDailyRegionOptions(country = els.riderActiveCountry?.value, city = els.riderActiveCity?.value) { const rider = currentRiderRecord(); populateMultiSelect(els.riderDailyRegions, areas(country, city).map((area) => area.name), riderDailyDestinationRegions(rider)); const preference = riderDayPreferenceFor(rider); if (preference?.showAllNearbyPickups) state.riderDestinationScope = "all"; if (els.riderDestinationScope) els.riderDestinationScope.value = state.riderDestinationScope; } function vehicleYearOptions() { const currentYear = new Date().getFullYear() + 1; const years = []; for (let year = currentYear; year >= minimumVehicleYear; year -= 1) years.push(String(year)); return years; } const riderBikeMakeCatalog = { Bajaj: ["Boxer", "Pulsar", "Discover", "Other"], Yamaha: ["YBR", "FZ", "Crypton", "Other"], TVS: ["HLX", "Apache", "Star", "Other"], Haojue: ["HJ", "DK", "Other"], Suzuki: ["GN", "AX100", "Other"], Honda: ["CG", "Wave", "Dream", "Other"], Other: ["Motorbike", "Scooter", "Other"] }; function riderVehicleCatalogFor(vehicle = normalizeRideVehicle(els.riderVehicle?.value)) { if (vehicle === "bike") return riderBikeMakeCatalog; const motorcycleMakes = new Set(Object.keys(riderBikeMakeCatalog).filter((make) => make !== "Honda" && make !== "Other")); return Object.fromEntries(Object.entries(carMakeCatalog).filter(([make]) => !motorcycleMakes.has(make))); } function syncCurrentRiderVehicleDraft(vehicle = normalizeRideVehicle(els.riderVehicle?.value)) { if (!state.rider) return; state.rider.vehicle = vehicle; state.rider.carMake = els.riderCarMake?.value || state.rider.carMake || ""; state.rider.carModel = els.riderCarModel?.value || state.rider.carModel || ""; if (vehicle === "bike") { state.rider.carBodyType = "motorbike"; state.rider.vehicleDesignation = "normal"; state.rider.vehicleVin = ""; state.rider.insuranceProvider = ""; state.rider.insuranceNumber = ""; state.rider.insuranceExpiresOn = ""; } } function populateVehicleCatalogFields(rider = state.rider) { if (!els.riderCarMake || !els.riderCarModel || !els.riderCarBodyType || !els.riderCarYear || !els.riderCarColor) return; const vehicle = normalizeRideVehicle(rider?.vehicle ?? els.riderVehicle?.value); if (els.riderVehicle) els.riderVehicle.value = vehicle; const catalog = riderVehicleCatalogFor(vehicle); const makes = Object.keys(catalog); const selectedMake = makes.includes(rider?.carMake) ? rider.carMake : makes[0]; const models = catalog[selectedMake] ?? catalog.Other ?? ["Other"]; const selectedModel = models.includes(rider?.carModel) ? rider.carModel : models[0]; populateSelect(els.riderCarMake, makes, selectedMake); populateSelect(els.riderCarModel, models, selectedModel); populateSelectOptions(els.riderCarBodyType, carBodyTypeOptions, vehicle === "bike" ? "motorbike" : normalizeCarBodyType(rider?.carBodyType)); populateRiderVehicleDesignationOptions(rider); populateSelect(els.riderCarYear, vehicleYearOptions(), String(rider?.carYear ?? new Date().getFullYear())); populateSelect(els.riderCarColor, carColors, rider?.carColor ?? carColors[0]); updateRiderVehicleSpecificFields(vehicle); syncCurrentRiderVehicleDraft(vehicle); } function setRiderFieldHidden(input, hidden) { const field = input?.closest?.("label"); if (field) field.hidden = hidden; if (input) input.disabled = hidden; const row = field?.closest?.(".field-row"); if (row) { row.hidden = ![...row.querySelectorAll("label")].some((label) => !label.hidden); } } function setRiderCarOnlyFieldHidden(input, hidden) { setRiderFieldHidden(input, hidden); if (!input) return; input.required = false; if (hidden) input.value = ""; } function updateRiderVehicleSpecificFields(vehicle = normalizeRideVehicle(els.riderVehicle?.value)) { const bikeMode = vehicle === "bike"; els.riderAccountForm?.classList.toggle("rider-bike-mode", bikeMode); if (els.riderAccountForm) els.riderAccountForm.dataset.riderVehicle = vehicle; if (bikeMode) { if (els.riderCarBodyType) els.riderCarBodyType.value = "motorbike"; if (els.riderVehicleDesignation) els.riderVehicleDesignation.value = "normal"; } const makeLabel = translatedValue(bikeMode ? "bikeMake" : "vehicleMake"); const modelLabel = translatedValue(bikeMode ? "bikeModel" : "vehicleModel"); const typeLabel = translatedValue(bikeMode ? "bikeType" : "vehicleCategory"); const sectionTitle = translatedValue(bikeMode ? "bikeSectionTitle" : "vehicleSectionTitle"); const sectionHelp = translatedValue(bikeMode ? "bikeSectionHelp" : "vehicleSectionHelp"); const plateLabel = translatedValue(bikeMode ? "bikePlateNumber" : "vehicleRegistration"); const registrationDocumentLabel = translatedValue(bikeMode ? "bikeRegistrationDocument" : "vehicleRegistrationDocument"); if (els.riderVehicleSectionTitle) els.riderVehicleSectionTitle.textContent = sectionTitle; if (els.riderVehicleSectionHelp) els.riderVehicleSectionHelp.textContent = sectionHelp; if (els.riderVehicleCategoryLabel) els.riderVehicleCategoryLabel.textContent = typeLabel; if (els.riderVehicleMakeLabel) els.riderVehicleMakeLabel.textContent = makeLabel; if (els.riderVehicleModelLabel) els.riderVehicleModelLabel.textContent = modelLabel; if (els.riderRegistrationLabel) els.riderRegistrationLabel.textContent = plateLabel; if (els.riderRegistration) els.riderRegistration.placeholder = plateLabel; if (els.riderRegistrationDocumentLabel) els.riderRegistrationDocumentLabel.textContent = `${registrationDocumentLabel} (${translatedValue("optional").toLowerCase()})`; if (els.riderBackgroundConsentText) { els.riderBackgroundConsentText.textContent = translatedValue(bikeMode ? "bikeBackgroundConsent" : "vehicleBackgroundConsent"); } els.riderAccountForm?.querySelectorAll("[data-rider-car-only]").forEach((node) => { node.hidden = bikeMode; node.style.display = bikeMode ? "none" : ""; node.querySelectorAll?.("input, select, textarea").forEach((control) => { control.disabled = bikeMode; if (bikeMode) control.required = false; }); }); setRiderFieldHidden(els.riderCarBodyType, bikeMode); setRiderFieldHidden(els.riderVehicleDesignation, bikeMode); [ els.riderInsuranceProvider, els.riderInsuranceNumber, els.riderInsuranceExpiresOn, els.riderVehicleVin, els.riderInsuranceDocument, els.riderInspectionDocument ].forEach((input) => setRiderCarOnlyFieldHidden(input, bikeMode)); if (els.riderVehicleModeNote) { els.riderVehicleModeNote.textContent = translatedValue(bikeMode ? "bikeModeNote" : "carModeNote"); } } function riderVehicleDesignationSelection(rider, bodyType) { const current = els.riderVehicleDesignation?.value; const stored = rider?.vehicleDesignation || riderDocumentMetadata(rider).vehicleDesignation; const candidate = current && current !== "normal" ? current : stored || current; const selected = normalizeRiderVehicleDesignation(candidate, bodyType); if (carBodyTypeAllowsXlSpecial(bodyType)) return selected === "normal" ? "both" : selected; return "normal"; } function populateRiderVehicleDesignationOptions(rider = state.rider) { if (!els.riderVehicleDesignation) return; const bodyType = normalizeCarBodyType(els.riderCarBodyType?.value ?? rider?.carBodyType); const allowsXlSpecial = carBodyTypeAllowsXlSpecial(bodyType); const selected = riderVehicleDesignationSelection(rider, bodyType); populateSelectOptions(els.riderVehicleDesignation, riderVehicleDesignationOptions, selected); [...els.riderVehicleDesignation.options].forEach((option) => { option.disabled = !allowsXlSpecial && option.value !== "normal"; }); els.riderVehicleDesignation.disabled = false; els.riderVehicleDesignation.value = selected; } function updateRiderVehicleDesignationForBodyType() { const bodyType = normalizeCarBodyType(els.riderCarBodyType?.value); populateRiderVehicleDesignationOptions({ ...state.rider, carBodyType: bodyType, vehicleDesignation: riderVehicleDesignationSelection(state.rider, bodyType) }); if (state.rider) { state.rider.carBodyType = bodyType; state.rider.vehicleDesignation = normalizeRiderVehicleDesignation(els.riderVehicleDesignation?.value, bodyType); } } function riderProfileDetailRow(label, value) { return `
${escapeHtml(label)}${escapeHtml(value || "not captured")}
`; } function riderProfileDetailSection(title, rows) { return `

${escapeHtml(title)}

${rows.map(([label, value]) => riderProfileDetailRow(label, value)).join("")}
`; } function riderProfileRatingSection(rider) { const categories = typeof riderRatingCategorySummaries === "function" ? riderRatingCategorySummaries(rider?.id) : []; const count = categories.find((category) => category.key === "overall")?.count ?? 0; const rows = categories.map((category) => [ category.label, Number(category.percent) ? `${Math.round(category.percent)}%` : "not enough ratings yet" ]); rows.push(["Ratings counted", count ? `${count}` : "0"]); return riderProfileDetailSection("Anonymous ratings", rows); } function renderRiderRatingsPanel(rider = currentRiderRecord()) { if (!els.riderRatingsPanel) return; const categories = typeof riderRatingCategorySummaries === "function" ? riderRatingCategorySummaries(rider?.id) : []; const count = categories.find((category) => category.key === "overall")?.count ?? 0; const categoryRows = categories.map((category) => riderProfileDetailRow( category.label, Number(category.percent) ? `${Math.round(category.percent)}%` : "not enough ratings yet" )).join(""); els.riderRatingsPanel.innerHTML = ` Anonymous passenger feedback

Rider ratings

Ratings are shown as category percentages. Waka does not show which passenger submitted a rating.

${categoryRows} ${riderProfileDetailRow("Ratings counted", count ? `${count}` : "0")}
`; } function riderInitials(rider = state.rider) { const name = String(rider?.name ?? rider?.email ?? rider?.phone ?? "Rider").trim(); const parts = name.split(/\s+/).filter(Boolean); return (parts.length > 1 ? `${parts[0][0]}${parts[1][0]}` : parts[0]?.slice(0, 2) || "R").toUpperCase(); } function setRiderProfileAvatarFallback(rider = state.rider) { if (!els.riderProfileAvatar) return; els.riderProfileAvatar.textContent = riderInitials(rider); } async function ensureRiderProfilePhotoUrl(rider = state.rider) { if (!els.riderProfileAvatar || !rider?.profilePhotoPath || !isSupabaseMode() || !supabaseClient) return; const cacheKey = rider.profilePhotoPath; const cached = riderProfilePhotoUrlCache.get(cacheKey); if (cached) { els.riderProfileAvatar.innerHTML = ``; return; } try { const { data, error } = await supabaseClient.storage .from(appConfig.buckets.profilePhotos) .createSignedUrl(rider.profilePhotoPath, 600); if (error || !data?.signedUrl) throw error || new Error("Signed profile photo URL was not returned."); riderProfilePhotoUrlCache.set(cacheKey, data.signedUrl); if (currentRiderRecord()?.profilePhotoPath === rider.profilePhotoPath) { els.riderProfileAvatar.innerHTML = ``; } } catch (error) { logClientWarning("Rider profile picture could not be displayed.", error); setRiderProfileAvatarFallback(rider); } } function renderRiderProfileSummary(rider = currentRiderRecord()) { if (!rider) return; setRiderProfileAvatarFallback(rider); if (rider.profilePhotoPath) void ensureRiderProfilePhotoUrl(rider); if (els.riderProfilePhotoStatus) { els.riderProfilePhotoStatus.textContent = rider.profilePhotoPath || rider.profilePhotoName ? `Profile picture: ${rider.profilePhotoName || "uploaded"}` : "Profile picture not uploaded."; } syncRiderNavigationPreferenceInput(riderNavigationPreference(rider)); if (!els.riderProfileDetailList) return; if (rider.status !== "approved" && rider.status !== "needs_correction" && !rider.needsApplication && rider.status !== "profile only") { els.riderProfileDetailList.innerHTML = riderProfileDetailSection("Application", [ ["Application status", rider.status || "pending"], ["Next step", riderWorkspaceStatusMessage(rider)] ]); renderRiderComplianceRenewalForm(rider); return; } const metadata = riderDocumentMetadata(rider); const bikeMode = normalizeRideVehicle(rider.vehicle) === "bike"; const vehicle = `${bikeMode ? "" : rider.carYear || ""} ${rider.carMake || ""} ${rider.carModel || ""}`.trim() || (bikeMode ? "Bike not complete" : "Vehicle not complete"); const vehicleRows = bikeMode ? [ ["Bike", vehicle], ["Bike color", rider.carColor || "not captured"], ["Bike plate number", rider.registration || "not captured"] ] : [ ["Vehicle", vehicle], ["Body type", carBodyTypeLabel(rider.carBodyType)], ["Vehicle designation", riderVehicleDesignationLabel(metadata.vehicleDesignation)], ["Plate number", rider.registration || "not captured"], ["Insurance expires", rider.insuranceExpiresOn ? formatDate(rider.insuranceExpiresOn) : "not captured"] ]; els.riderProfileDetailList.innerHTML = [ riderProfileDetailSection("Identity", [ ["Driver's license", rider.nationalId || "not captured"], ["License expires", rider.driverLicenseExpiresOn ? formatDate(rider.driverLicenseExpiresOn) : "not captured"], ["Birth month", storedDateToYearMonth(rider.dateOfBirth) || "not captured"] ]), riderProfileDetailSection(bikeMode ? "Bike" : "Vehicle", vehicleRows), riderProfileDetailSection("Service preferences", [ ["Service state", `${rider.city || "not set"}, ${rider.country || "not set"}`], ["Compliance", riderComplianceStatusText(rider)], ["Navigation", metadata.navigationPreference === "waze" ? "Waze" : "Google Maps"] ]), riderProfileRatingSection(rider) ].join(""); renderRiderRatingsPanel(rider); renderRiderComplianceRenewalForm(rider); } function renderRiderComplianceRenewalForm(rider = currentRiderRecord()) { if (!els.riderComplianceRenewalForm) return; const canRenew = Boolean(rider && !rider.needsApplication && rider.status !== "profile only" && rider.status !== "declined"); els.riderComplianceRenewalForm.hidden = !canRenew; if (!canRenew || !els.riderComplianceRenewalStatus) return; if (rider.status === "suspended") { els.riderComplianceRenewalStatus.textContent = "Upload current documents. Admin approval restores rider service after review."; return; } if (!riderComplianceReady(rider)) { els.riderComplianceRenewalStatus.textContent = "Upload renewed documents. Rider service remains blocked until the expired item is renewed and reviewed."; return; } const upcoming = riderUpcomingComplianceItems(rider); els.riderComplianceRenewalStatus.textContent = upcoming.length ? "Renew before expiration to avoid automatic suspension." : "Renew documents any time before expiration. Uploading a new document is required when changing an expiration date."; } function complianceRenewalPair(dateValue, file, label) { const hasDate = Boolean(String(dateValue ?? "").trim()); const hasFile = Boolean(file); if (!hasDate && !hasFile) return null; if (!hasDate) throw new Error(`Enter the new ${label} expiration date.`); if (!hasFile) throw new Error(`Upload the new ${label} document.`); const days = daysUntilDate(dateValue); if (days === null || days < 0) throw new Error(`Enter a current ${label} expiration date.`); return { dateValue, file }; } async function submitRiderComplianceRenewal(event) { event.preventDefault(); const rider = currentRiderRecord(); if (!rider) { if (els.riderComplianceRenewalStatus) els.riderComplianceRenewalStatus.textContent = "Sign in as a rider before uploading renewed documents."; return; } try { const license = complianceRenewalPair( els.riderRenewLicenseExpiresOn?.value, els.riderRenewLicenseDocument?.files?.[0] ?? null, "driver's license" ); const insurance = complianceRenewalPair( els.riderRenewInsuranceExpiresOn?.value, els.riderRenewInsuranceDocument?.files?.[0] ?? null, "insurance" ); if (!license && !insurance) { els.riderComplianceRenewalStatus.textContent = "Choose at least one renewed document and expiration date."; return; } setButtonBusy(els.riderSubmitComplianceRenewal, true); els.riderComplianceRenewalStatus.textContent = "Uploading renewed documents..."; const result = await submitRiderComplianceRenewalToSupabase(rider, { driverLicenseExpiresOn: license?.dateValue ?? null, driverLicenseFile: license?.file ?? null, insuranceExpiresOn: insurance?.dateValue ?? null, insuranceFile: insurance?.file ?? null }); const application = result?.application ?? {}; const documents = { ...riderDocuments(rider), ...(result?.documents ?? {}) }; const nextStatus = application.status ?? (rider.status === "suspended" ? "pending" : rider.status); const updatedRider = { ...rider, status: nextStatus, reviewNote: application.review_note ?? (nextStatus === "pending" ? "Renewed compliance documents submitted for admin review." : rider.reviewNote ?? ""), driverLicenseExpiresOn: application.driver_license_expires_on ?? license?.dateValue ?? rider.driverLicenseExpiresOn, insuranceExpiresOn: application.insurance_expires_on ?? insurance?.dateValue ?? rider.insuranceExpiresOn, complianceSuspendedAt: application.compliance_suspended_at ?? (nextStatus === "approved" ? null : rider.complianceSuspendedAt ?? null), complianceSuspensionReason: application.compliance_suspension_reason ?? (nextStatus === "approved" ? null : rider.complianceSuspensionReason ?? null), documentName: application.document_path ?? riderDocumentPayload(documents), documents: { ...documents, vehicleDesignation: normalizeRiderVehicleDesignation(rider.vehicleDesignation, rider.carBodyType), navigationPreference: riderNavigationPreference(rider) }, driverLicenseDocumentPath: documents.driverLicense, insuranceDocumentPath: documents.insurance }; saveCurrentRiderRecord(updatedRider); if (els.riderRenewLicenseExpiresOn) els.riderRenewLicenseExpiresOn.value = ""; if (els.riderRenewLicenseDocument) els.riderRenewLicenseDocument.value = ""; if (els.riderRenewInsuranceExpiresOn) els.riderRenewInsuranceExpiresOn.value = ""; if (els.riderRenewInsuranceDocument) els.riderRenewInsuranceDocument.value = ""; saveState(); renderAll(); els.riderComplianceRenewalStatus.textContent = nextStatus === "approved" ? "Renewed documents saved. Rider service remains active while documents are current." : "Renewed documents submitted. Admin review is required before rider service resumes."; } catch (error) { if (els.riderComplianceRenewalStatus) els.riderComplianceRenewalStatus.textContent = error.message; } finally { setButtonBusy(els.riderSubmitComplianceRenewal, false); } } function renderRiderBackgroundCheckPanel() { if (!els.riderBackgroundCheckPanel) return; const signedIn = Boolean(hasSignedIn("rider") && state.rider); const onChecksPage = typeof riderWorkspacePage !== "function" || riderWorkspacePage() === "checks"; els.riderBackgroundCheckPanel.hidden = !signedIn || !onChecksPage; if (!signedIn) return; const rider = currentRiderRecord() ?? state.rider; const relaxedBackground = riderBackgroundCheckRelaxedForTesting(); const backgroundUnlocked = riderCanStartBackgroundCheckFromStatus(rider) || rider?.status === "approved"; const manualReview = riderManualBackgroundReviewMode(); if (manualReview && !["background_pending", "approved"].includes(rider?.status)) { els.riderBackgroundCheckPanel.hidden = true; return; } if (!backgroundUnlocked) { if (!manualReview) { els.riderBackgroundCheckPanel.hidden = true; return; } } const latest = latestRiderSelfBackgroundCheck(rider); const provider = latest?.provider || rider?.backgroundCheckProvider || appConfig.backgroundCheckProvider || "background-check provider"; const status = normalizedRiderBackgroundStatus(rider); const decision = normalizedRiderBackgroundDecision(rider); const hasConsent = Boolean(rider?.backgroundCheckConsentAt); const alreadyStarted = !["", "not requested", "not-requested", "not_requested"].includes(status); const finalDecision = ["clear", "consider", "adverse"].includes(decision) || ["clear", "review", "adverse", "failed"].includes(status); const canStart = hasSupabaseRuntime() && hasConsent && !alreadyStarted && riderCanStartBackgroundCheckFromStatus(rider); if (els.riderBackgroundCheckBadge) { els.riderBackgroundCheckBadge.textContent = `${status || "not requested"} / ${decision || "pending"}`; } if (els.riderBackgroundCheckSummary) { if (manualReview) { els.riderBackgroundCheckSummary.textContent = rider?.status === "approved" ? "Manual Waka admin review is complete for this Cameroon rider account." : "Waka admin is reviewing local documents, vehicle details, permit, emergency contact, and safety information. No Checkr/provider step is required in this Cameroon MVP."; } else if (!hasConsent) { els.riderBackgroundCheckSummary.textContent = "Submit the rider application with background-check consent before provider screening can start."; } else if (decision === "clear") { els.riderBackgroundCheckSummary.textContent = "Provider screening is clear. Admin can use this result with your documents when deciding eligibility."; } else if (decision === "consider") { els.riderBackgroundCheckSummary.textContent = "Provider screening needs admin review. Admin will consider the report with your documents before deciding eligibility."; } else if (decision === "adverse" || status === "adverse") { els.riderBackgroundCheckSummary.textContent = "Provider screening returned an adverse status. Contact Waka support for next steps before eligibility can be approved."; } else if (alreadyStarted) { els.riderBackgroundCheckSummary.textContent = "Provider screening has started. Watch for provider email, SMS, or hosted instructions and complete any requested steps."; } else if (rider?.status === "pending" && relaxedBackground) { els.riderBackgroundCheckSummary.textContent = `Testing mode is on for this staging account. Select Start ${provider} background check to record the relaxed Checkr step while admin review is pending. Production still requires the real provider result.`; } else { els.riderBackgroundCheckSummary.textContent = `Admin has unlocked ${provider} screening. Select Start background check, then follow the provider email, SMS, or hosted instructions. Waka pays for this required screening.`; } } if (els.startRiderBackgroundCheck) { els.startRiderBackgroundCheck.disabled = manualReview || !canStart; els.startRiderBackgroundCheck.hidden = manualReview || finalDecision; els.startRiderBackgroundCheck.textContent = manualReview ? "Manual review" : alreadyStarted ? "Background check started" : `Start ${provider} background check`; } if (els.riderBackgroundCheckStatus && !els.riderBackgroundCheckStatus.dataset.busy) { els.riderBackgroundCheckStatus.textContent = manualReview ? "Waka Cameroon uses manual admin review for the MVP. Admin can approve, decline, or request corrections." : !hasSupabaseRuntime() ? "Background checks require the Supabase production runtime." : !hasConsent ? "Consent is captured during rider application submission." : alreadyStarted ? `Latest ${provider} status: ${status}; decision: ${decision}.` : rider?.status === "pending" && relaxedBackground ? `Ready to start the relaxed ${provider} testing step from Eligibility.` : rider?.status === "background_pending" ? `Ready to start ${provider} screening. Instructions are also sent by email when delivery is configured.` : "Admin has not unlocked provider screening yet."; } if (!els.riderBackgroundCheckList) return; els.riderBackgroundCheckList.innerHTML = ""; const records = riderSelfBackgroundCheckRecords(rider?.id); if (!records.length) { const item = document.createElement("article"); item.className = "notice-item"; item.innerHTML = ` ${escapeHtml(manualReview ? "Manual review" : `${provider} next steps`)}

${manualReview ? "No provider action is needed from the rider. Waka admin reviews the submitted Cameroon documents and will send corrections or approval." : rider?.status === "pending" && relaxedBackground ? `Select Start ${escapeHtml(provider)} background check. Staging records a relaxed Checkr testing step for admin review without using production provider billing.` : `Select Start ${escapeHtml(provider)} background check. If a provider page opens, complete it there. If the provider sends email or SMS instructions, complete those steps and return to Waka. Admin will see the returned status here and in Rider approvals.`}

${manualReview ? "Manual review is tracked by admin." : "No provider result has been returned yet."} `; els.riderBackgroundCheckList.append(item); return; } records.slice(0, 3).forEach((record) => { const item = document.createElement("article"); item.className = "notice-item"; item.innerHTML = ` ${escapeHtml(record.provider || provider)} - ${escapeHtml(record.status || "requested")}

${escapeHtml(record.summary || `Decision: ${record.decision || "pending"}.`)}

${record.completedAt ? `Completed ${formatDateTime(record.completedAt)}` : `Started ${formatDateTime(record.createdAt)}`} `; els.riderBackgroundCheckList.append(item); }); } function riderTaxDocumentAccessAction(taxDocument) { const providerUrl = normalizeHttpsUrl(taxDocument.providerDocumentUrl); if (providerUrl) { return `Open provider form`; } return storageReviewButton(`${taxDocument.documentType} ${taxDocument.taxYear}`, appConfig.buckets.riderDocuments, taxDocument.storagePath); } function riderTaxDocumentDeliveryText(taxDocument) { const delivery = taxDocument.deliveryMethod ? String(taxDocument.deliveryMethod).replace(/_/g, " ") : "provider portal"; const filing = taxDocument.filingStatus ? ` Filing: ${String(taxDocument.filingStatus).replace(/_/g, " ")}.` : ""; const reference = taxDocument.providerDocumentId ? ` Reference: ${taxDocument.providerDocumentId}.` : ""; return `Provider: ${taxDocument.provider || "Waka"}. Delivery: ${delivery}.${filing}${reference}`; } function renderRiderTaxDocuments() { if (!els.riderTaxPanel || !els.riderTaxList) return; const signedIn = Boolean(hasSignedIn("rider") && state.rider); const onChecksPage = typeof riderWorkspacePage !== "function" || riderWorkspacePage() === "checks"; const rider = currentRiderRecord() ?? state.rider; const riderApproved = rider?.status === "approved"; els.riderTaxPanel.hidden = !signedIn || !onChecksPage || !riderApproved; els.riderTaxList.innerHTML = ""; if (!signedIn || !onChecksPage || !riderApproved) return; const taxIdentity = taxIdentityForRider(rider?.id); const provider = appConfig.taxOnboardingProvider || "tax provider"; if (els.riderTaxOnboardingSummary) { els.riderTaxOnboardingSummary.textContent = taxIdentity ? `${taxIdentityStatusText(taxIdentity)} Provider reference: ${taxIdentity.providerSubjectId || "provider-held"}.` : `Use ${provider} hosted onboarding for tax setup. Waka does not collect or store raw SSN, EIN, ITIN, W-9, or full TIN values.`; } if (els.startRiderTaxOnboarding) { els.startRiderTaxOnboarding.disabled = !riderApproved || !hasSupabaseRuntime(); els.startRiderTaxOnboarding.textContent = taxIdentity?.status === "verified" ? "Update hosted tax setup" : "Open hosted tax setup"; } if (els.riderTaxOnboardingStatus && !els.riderTaxOnboardingStatus.dataset.busy) { els.riderTaxOnboardingStatus.textContent = riderApproved ? "Complete tax setup only inside the provider-hosted flow." : "Tax onboarding opens after admin approval, before payouts or annual tax documents."; } const documents = taxDocumentsForRider(state.rider.id); if (!documents.length) { els.riderTaxList.append(emptyState("No tax documents are available yet. Annual tax documents will appear here when issued by Waka or its tax provider.")); return; } documents.forEach((taxDocument) => { const item = document.createElement("article"); item.className = "notice-item"; const openButton = riderTaxDocumentAccessAction(taxDocument); item.innerHTML = ` ${escapeHtml(taxDocument.documentType)} - ${escapeHtml(String(taxDocument.taxYear))}

Status: ${escapeHtml(taxDocument.status)}. ${escapeHtml(riderTaxDocumentDeliveryText(taxDocument))}

${taxDocument.availableAt ? `Available ${formatDate(taxDocument.availableAt)}` : taxDocument.issuedAt ? `Issued ${formatDate(taxDocument.issuedAt)}` : "Not available yet"} ${openButton ? `
${openButton}
` : ""} `; els.riderTaxList.append(item); }); wireStorageReviewButtons(els.riderTaxList); } function riderEarningsRouteLabel(request) { if (!request) return "Completed ride"; const pickup = requestPickupDisplayText(request, "Pickup"); const destination = request.destinationFormattedAddress || request.destination || request.destinationArea || "Destination"; return `${pickup} -> ${destination}`; } function riderEarningRecordTimestamp(record) { return record.processedAt || record.completedAt || record.createdAt || record.updatedAt; } function riderCompletedTelemetryMileageMiles(requestId, riderId) { if (!requestId || !riderId) return null; const miles = (state.riderCompletedMileageSegments ?? []) .filter((segment) => segment.requestId === requestId && segment.riderId === riderId && segment.status === "closed") .reduce((total, segment) => total + Number(segment.distanceMiles || 0), 0); return Number.isFinite(miles) && miles > 0 ? miles : null; } function riderCompletedWakaMileageMiles(request, settlement = null, riderId = null) { if (!request || request.status !== "completed") return null; const resolvedRiderId = riderId || settlement?.riderId || selectedRiderIdForRequest(request); const telemetryMiles = riderCompletedTelemetryMileageMiles(request.id, resolvedRiderId); if (telemetryMiles != null) return telemetryMiles; const miles = Number(request.estimatedDistanceMiles ?? settlement?.distanceMiles); return Number.isFinite(miles) && miles > 0 ? miles : null; } function formatCompletedWakaMileage(value, fallback = "0.0 mi") { const miles = Number(value); if (!Number.isFinite(miles) || miles <= 0) return fallback; return `${miles.toFixed(miles < 10 ? 1 : 0)} mi`; } function riderCompletedRideEarningRecords(rider = currentRiderRecord()) { const riderId = rider?.id; if (!riderId) return []; const requests = state.requests.filter((request) => selectedRiderIdForRequest(request) === riderId && request.status === "completed"); const requestMap = new Map(state.requests.map((request) => [request.id, request])); const tipsByRequest = new Map(); rideTipRecords() .filter((tip) => tip.riderId === riderId && !["failed", "refunded"].includes(tip.status)) .forEach((tip) => { const current = tipsByRequest.get(tip.requestId) ?? { amount: 0, payout: 0, fee: 0, count: 0 }; current.amount += Number(tip.amount || 0); current.payout += Number(tip.riderPayoutAmount || 0); current.fee += Number(tip.stripeFeeAmount || 0); current.count += 1; tipsByRequest.set(tip.requestId, current); }); const settledRequestIds = new Set(); const settlementRecords = rideSettlementRecords() .filter((settlement) => settlement.riderId === riderId) .map((settlement) => { settledRequestIds.add(settlement.requestId); const request = requestMap.get(settlement.requestId); const tip = tipsByRequest.get(settlement.requestId) ?? { amount: 0, payout: 0, fee: 0, count: 0 }; const completedWakaMiles = riderCompletedWakaMileageMiles(request, settlement, riderId); return { id: `settlement-${settlement.id}`, requestId: settlement.requestId, route: riderEarningsRouteLabel(request), country: request?.country || rider.country, completedWakaMiles, fareAmount: Number(settlement.fareAmount || 0), stripeFeeAmount: Number(settlement.stripeFeeAmount || 0) + tip.fee, riderPayoutAmount: Number(settlement.riderPayoutAmount || 0), tipPayoutAmount: tip.payout, totalEarned: Number(settlement.riderPayoutAmount || 0) + tip.payout, tipAmount: tip.amount, tipCount: tip.count, status: settlement.status || "pending_provider_payout", providerReference: settlement.providerTransferReference || settlement.providerReference || "", failureReason: settlement.failureReason || "", completedAt: request?.completedAt, processedAt: settlement.processedAt, createdAt: settlement.createdAt || request?.completedAt, updatedAt: settlement.updatedAt }; }); const fallbackRecords = requests .filter((request) => !settledRequestIds.has(request.id)) .map((request) => { const breakdown = rideFinancialBreakdown(request, totalTipAmountForRequest(request.id)); return { id: `completed-${request.id}`, requestId: request.id, route: riderEarningsRouteLabel(request), country: request.country || rider.country, completedWakaMiles: riderCompletedWakaMileageMiles(request, null, riderId), fareAmount: centsToDollars(breakdown.fareCents), stripeFeeAmount: centsToDollars(breakdown.stripeFeeCents), riderPayoutAmount: centsToDollars(breakdown.riderPayoutCents), tipPayoutAmount: 0, totalEarned: centsToDollars(breakdown.riderPayoutCents), tipAmount: centsToDollars(breakdown.tipCents), tipCount: breakdown.tipCents > 0 ? 1 : 0, status: "pending_provider_payout", providerReference: "", failureReason: "", completedAt: request.completedAt, processedAt: null, createdAt: request.completedAt || request.createdAt, updatedAt: request.updatedAt }; }); return [...settlementRecords, ...fallbackRecords] .sort((a, b) => new Date(riderEarningRecordTimestamp(b) || 0) - new Date(riderEarningRecordTimestamp(a) || 0)); } function startOfLocalDay(value = new Date()) { const date = new Date(value); date.setHours(0, 0, 0, 0); return date; } function startOfLocalWeek(value = new Date()) { const date = startOfLocalDay(value); date.setDate(date.getDate() - date.getDay()); return date; } function periodEarningsTotal(records, startDate) { const startMs = startDate.getTime(); return records .filter((record) => new Date(riderEarningRecordTimestamp(record) || 0).getTime() >= startMs) .reduce((total, record) => total + Number(record.totalEarned || 0), 0); } function renderRiderEarnings() { if (!els.riderEarningsPanel || !els.riderEarningsSummary || !els.riderEarningsList) return; const rider = currentRiderRecord(); const signedIn = Boolean(hasSignedIn("rider") && rider); const onEarningsPage = typeof riderWorkspacePage !== "function" || riderWorkspacePage() === "earnings"; els.riderEarningsPanel.hidden = !signedIn || !onEarningsPage; els.riderEarningsSummary.innerHTML = ""; els.riderEarningsList.innerHTML = ""; if (!signedIn || !onEarningsPage) { if (els.riderEarningsCount) els.riderEarningsCount.textContent = riderUiText("zeroRides", "0 rides"); return; } const records = riderCompletedRideEarningRecords(rider); const now = new Date(); const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); const startOfYear = new Date(now.getFullYear(), 0, 1); const completedWakaMiles = records.reduce((total, record) => total + (Number(record.completedWakaMiles) || 0), 0); const summary = [ [riderUiText("today", "Today"), periodEarningsTotal(records, startOfLocalDay(now)), riderUiText("completedRidesToday", "Completed rides paid or pending today")], [riderUiText("thisWeek", "This week"), periodEarningsTotal(records, startOfLocalWeek(now)), riderUiText("sundayThroughToday", "Sunday through today")], [riderUiText("thisMonth", "This month"), periodEarningsTotal(records, startOfMonth), riderUiText("monthToDateRiderEarnings", "Month-to-date rider earnings")], [riderUiText("thisYear", "This year"), periodEarningsTotal(records, startOfYear), riderUiText("yearToDateRiderEarnings", "Year-to-date rider earnings")] ]; if (els.riderEarningsCount) { els.riderEarningsCount.textContent = riderUiText("completedRideCount", "{count} completed ride(s)", { count: records.length }); } els.riderEarningsSummary.innerHTML = `
${escapeHtml(riderUiText("completedWakaMiles", "Completed Waka miles"))} ${escapeHtml(formatCompletedWakaMileage(completedWakaMiles))} ${escapeHtml(riderUiText("onlyCompletedWakaRidesCounted", "Only completed Waka rides are counted."))}
` + summary.map(([label, amount, detail]) => `
${escapeHtml(label)} ${escapeHtml(formatMoney(amount, rider.country))} ${escapeHtml(detail)}
`).join(""); if (!records.length) { els.riderEarningsList.append(emptyState(riderUiText("completedRidesPayoutsAppearHere", "Completed rides and rider payouts will appear here after trips are finished."))); return; } records.slice(0, 20).forEach((record) => { const item = document.createElement("article"); item.className = `market-card platform-issue-${record.status === "failed" ? "critical" : record.status === "paid_out" ? "info" : "warning"}`; const tipText = record.tipPayoutAmount > 0 ? ` Tip payout ${formatMoney(record.tipPayoutAmount, record.country)}.` : ""; item.innerHTML = `
${escapeHtml(riderUiText("rideEarningsStatus", "Ride earnings - {status}", { status: record.status.replace(/_/g, " ") }))} ${escapeHtml(riderUiText("amountEarned", "{amount} earned", { amount: formatMoney(record.totalEarned, record.country) }))} ${escapeHtml(formatDateTime(riderEarningRecordTimestamp(record)))}

${escapeHtml(record.route)}

${escapeHtml(riderUiText("actualWakaMileage", "Actual Waka mileage: {mileage}", { mileage: formatCompletedWakaMileage(record.completedWakaMiles, riderUiText("mileagePending", "Mileage pending")) }))}

${directRidePaymentMode() ? `Fare ${escapeHtml(formatMoney(record.fareAmount, record.country))}; paid directly by passenger; wallet commission review handled by Waka operations.${tipText}` : `Fare ${escapeHtml(formatMoney(record.fareAmount, record.country))}; Stripe fee ${escapeHtml(formatMoney(record.stripeFeeAmount, record.country))}; ride payout ${escapeHtml(formatMoney(record.riderPayoutAmount, record.country))}.${tipText}`}

${[ `Ride ID: ${record.requestId}`, record.completedWakaMiles != null ? `Waka miles: ${formatCompletedWakaMileage(record.completedWakaMiles)}` : "Waka miles pending", directRidePaymentMode() ? (record.providerReference ? `Wallet ref: ${record.providerReference}` : "Direct payment recorded") : (record.providerReference ? `Stripe ref: ${record.providerReference}` : "Stripe payout pending"), record.failureReason ? `Issue: ${record.failureReason}` : "" ].filter(Boolean).map(chip).join("")}
`; els.riderEarningsList.append(item); }); } function riderPickupRadiusForEtaMinutes(minutes, rider = currentRiderRecord()) { const speedKmh = riderPickupEtaSpeedKmh[rider?.vehicle] ?? riderPickupEtaSpeedKmh.car; return (Number(minutes) * speedKmh) / (riderPickupEtaRoadFactor * 60); } function riderServiceRadius(rider = currentRiderRecord(), request = null) { const baseRadius = riderProximityLimit[rider?.vehicle] ?? riderProximityLimit.car; if (!isScheduledRequest(request)) return baseRadius; return Math.max(baseRadius, riderPickupRadiusForEtaMinutes(scheduledRiderPickupMaxEtaMinutes, rider)); } function riderCanActivateAvailability(rider = currentRiderRecord()) { return Boolean(rider && hasSignedIn("rider") && rider.status === "approved" && isSubscriptionActive(rider) && riderComplianceReady(rider)); } function riderRequestSetupGaps(rider = currentRiderRecord()) { if (!rider) return []; return [ paymentAccountReady("rider", rider) ? null : "payout account" ].filter(Boolean); } function riderAvailabilityStatusText(rider = currentRiderRecord()) { if (rider && !riderComplianceReady(rider)) return riderComplianceStatusText(rider); if (!riderCanActivateAvailability(rider)) return riderWorkspaceStatusMessage(rider); const currentGps = riderCurrentFreshGps(rider); const activated = state.riderAvailabilityActivated === true; const blockingRide = riderBlockingImmediateRide(rider); const setupGaps = riderRequestSetupGaps(rider); const setupNote = setupGaps.length ? ` ${riderUiText("riderCompleteSetupBeforeRequests", "Complete {gaps} before ride requests appear.", { gaps: setupGaps.join(" and ") })}` : ""; if (blockingRide) { return blockingRide.status === "in_progress" ? riderUiText("onlineActiveRideResumeNearDropoff", "Online for this active ride. New requests resume when you are about 7 minutes from drop-off.{setup}", { setup: setupNote }) : riderUiText("onlineActiveRidePausedUntilNearDropoff", "Online for this active ride. New immediate requests are paused until the ride starts and you are about 7 minutes from drop-off.{setup}", { setup: setupNote }); } if (currentGps) { return riderUiText("onlineAvailableGpsPrivate", "Online and available. {gps} is used privately to show only nearby requests.{setup}", { gps: gpsStatusLabel(currentGps, riderUiText("locationActive", "Location active")), setup: setupNote }); } if (activated) { return riderUiText("activatedWaitingFreshLocation", "Activated. Waka is waiting for a fresh location update before nearby ride requests appear.{setup}", { setup: setupNote }); } return riderUiText("offlineActivateForNearbyRequests", "Offline. Activate when you are ready to receive nearby ride requests.{setup}", { setup: setupNote }); } function riderServiceAreaSummary(rider = currentRiderRecord()) { if (!rider) return riderUiText("nearbyRequestsUseAvailability", "Nearby requests use your availability and pickup radius."); const regions = riderDailyDestinationRegions(rider); const destinations = regions.length ? ` ${riderUiText("optionalPreferredDestinationsSaved", "Optional preferred destinations saved: {regions}.", { regions: regions.join(", ") })}` : ""; return riderUiText( "riderServiceAreaSummary", "Requests use your live location while you are online: immediate pickups within about {immediate} minutes, and scheduled rides within about {scheduled} minutes in the active launch market.{destinations} {status}", { immediate: riderPickupMaxEtaMinutes, scheduled: scheduledRiderPickupMaxEtaMinutes, destinations, status: riderAvailabilityStatusText(rider) } ); } function renderRiderDailyRegionStatus(rider = currentRiderRecord()) { if (!els.riderDailyRegionStatus) return; if (!rider) { els.riderDailyRegionStatus.textContent = riderUiText("destinationPreferencesOnDestinationPage", "Destination preferences are on the Destination page. Nearby requests use your active location."); return; } if (rider.status !== "approved") { els.riderDailyRegionStatus.textContent = riderWorkspaceStatusMessage(rider); return; } const regions = riderDailyDestinationRegions(rider); const remaining = riderDailyRegionUpdatesRemaining(rider); els.riderDailyRegionStatus.textContent = regions.length ? riderUiText("preferredDestinationsAndNearbyPickups", "Optional preferred destinations saved: {regions}. Showing all nearby pickups within the pickup radius. {remaining} update(s) remaining today.", { regions: regions.join(", "), remaining }) : riderUiText("showingNearbyPickups", "Showing all nearby pickups within the pickup radius."); } function updateRiderAreas() { const country = els.riderCountry.value || state.rider?.country || selectedPassengerCountry(); const city = els.riderCity.value || cityNames(country)[0]; const selectedCity = cityNames(country).includes(city) ? city : cityNames(country)[0]; populateSelect(els.riderCity, cityNames(country), selectedCity); populateSelect(els.riderArea, areas(country, selectedCity).map((area) => area.name), state.rider?.area); } function updateRiderActiveAreas() { const country = els.riderActiveCountry.value || state.rider?.country || selectedPassengerCountry(); const city = els.riderActiveCity.value || cityNames(country)[0]; const selectedCity = cityNames(country).includes(city) ? city : cityNames(country)[0]; populateSelect(els.riderActiveCity, cityNames(country), selectedCity); populateSelect(els.riderActiveArea, areas(country, selectedCity).map((area) => area.name), state.rider?.area ?? areas(country, selectedCity)[0]?.name); populateRiderDailyRegionOptions(country, selectedCity); } function updateRiderCityOptions() { const country = els.riderCountry.value; populateSelect(els.riderCity, cityNames(country), cityNames(country)[0]); populateSelect(els.riderArea, areas(country, els.riderCity.value).map((area) => area.name), areas(country, els.riderCity.value)[0]?.name); } function updateRiderActiveCityOptions() { const country = els.riderActiveCountry.value; populateSelect(els.riderActiveCity, cityNames(country), cityNames(country)[0]); populateSelect(els.riderActiveArea, areas(country, els.riderActiveCity.value).map((area) => area.name), areas(country, els.riderActiveCity.value)[0]?.name); populateRiderDailyRegionOptions(country, els.riderActiveCity.value); } function renderRiderStatus() { ensureRiderScreenWakeLockEvents(); renderRiderBackgroundCheckPanel(); renderRiderProfileSummary(); if (!state.rider) { els.riderStatus.textContent = "No rider application saved yet."; els.subscriptionText.textContent = directRidePaymentMode() ? `Approved Cameroon riders get a ${trialDays}-day free period after admin approval. After that, the first ${riderDailyFreeRideAllowance} completed rides each day are free and ride ${riderDailyFreeRideAllowance + 1}+ uses the rider wallet unless monthly access is active.` : `Approved riders get a ${trialDays}-day free trial, then choose ${formatMoney(riderMonthlySubscriptionFee)} monthly for 30 days or ${formatMoney(riderWeeklySubscriptionFee)} weekly for 7 days.`; els.subscriptionPaymentStatus.textContent = directRidePaymentMode() ? "Create and approve a rider account before starting MTN/Orange wallet top-up or monthly access payment." : "Create and approve a rider account before opening Waka Rider Access checkout."; els.paySubscription.disabled = true; renderRiderEarnings(); renderRiderAvailabilityControls(null); return; } const rider = state.riders.find((item) => item.id === state.rider.id) ?? state.rider; const statusText = { pending: "waiting for admin review", background_pending: riderManualBackgroundReviewMode() ? "in final manual admin review" : "invited to complete the Checkr background check", needs_correction: "needs corrections before resubmission", approved: "approved", declined: "declined by admin", suspended: "suspended", "profile only": "not yet submitted for admin review" }[rider.status] || "not submitted for admin review yet"; const bikeMode = normalizeRideVehicle(rider.vehicle) === "bike"; const vehicleLabel = bikeMode ? "Bike" : "Vehicle"; const vehicleName = `${bikeMode ? "" : rider.carYear || ""} ${rider.carMake || ""} ${rider.carModel || (bikeMode ? "bike" : "car")}`.trim(); els.riderStatus?.classList.remove("success-status", "error-status"); if (["pending", "background_pending", "approved"].includes(rider.status)) { els.riderStatus?.classList.add("success-status"); } if (["needs_correction", "declined", "suspended"].includes(rider.status)) { els.riderStatus?.classList.add("error-status"); } els.riderStatus.textContent = `${rider.name} is ${statusText}. ${vehicleLabel}: ${vehicleName}. Plate: ${rider.registration}.`; if (rider.status !== "approved") { els.subscriptionText.textContent = riderWorkspaceStatusMessage(rider); els.subscriptionPaymentStatus.textContent = directRidePaymentMode() ? "MTN/Orange wallet top-up and monthly access payment open only after admin approval." : "Rider plan checkout opens only after admin approval."; els.paySubscription.disabled = true; renderRiderEarnings(); renderRiderAvailabilityControls(rider); return; } const end = riderAccessEnd(rider); const walletModelActive = directRidePaymentMode() && !end; const remaining = walletModelActive ? Number.POSITIVE_INFINITY : daysUntil(end); const label = riderAccessLabel(rider); if (isSubscriptionActive(rider)) { const accessHealthy = walletModelActive || remaining > subscriptionRenewalNoticeDays; const accessName = label === "free trial" ? (directRidePaymentMode() ? "Free period" : "Free trial") : "Rider access"; if (walletModelActive) { els.subscriptionText.textContent = `Rider wallet/free-rides model is active. ${riderPlanSummary()}`; } else if (accessHealthy) { els.subscriptionText.textContent = `${accessName} active until ${formatDate(end)}. ${riderPlanSummary()} ${directRidePaymentMode() ? "Payment choices open 3 days before expiry." : "Payment choices open 3 days before expiry."}`; } else { const reminder = remaining <= subscriptionRenewalNoticeDays ? (directRidePaymentMode() ? " Start an MTN/Orange rider access payment before access changes." : ` Renewal is due soon; choose manual payment or automatic renewal so paid access starts after this period ends.`) : (directRidePaymentMode() ? ` Payment opens when ${subscriptionRenewalNoticeDays} days or fewer remain.` : ` Checkout opens when ${subscriptionRenewalNoticeDays} days or fewer remain.`); els.subscriptionText.textContent = `Rider access renewal: ${pluralDays(remaining)} left, until ${formatDate(end)}. ${riderPlanSummary()}${reminder}`; } els.subscriptionPaymentStatus.textContent = accessHealthy ? (directRidePaymentMode() ? "You may top up the rider wallet from 5,000 FCFA or pay 15,000 FCFA monthly access at any time." : "No rider payment action is needed until this access period is close to ending.") : (directRidePaymentMode() ? "Renewal reminder: pay with MTN Mobile Money or Orange Money." : "Renewal reminder: pay manually or keep your provider payment method active for automatic renewal."); els.paySubscription.disabled = directRidePaymentMode() ? false : accessHealthy; } else { els.subscriptionText.textContent = directRidePaymentMode() ? `Rider wallet payment is needed${end ? ` since ${formatDate(end)}` : ""}. ${riderPlanSummary()} Top up the rider wallet or pay monthly access with MTN/Orange.` : `Rider access is inactive${end ? ` since ${formatDate(end)}` : ""}. ${riderPlanSummary()} Open checkout to continue receiving and responding to ride requests.`; els.subscriptionPaymentStatus.textContent = directRidePaymentMode() ? "Choose MTN Mobile Money or Orange Money, enter the payer phone, and start wallet top-up or monthly access payment." : "Open Waka Rider Access checkout. Paid access starts after the free trial period or the current access period ends."; els.paySubscription.disabled = false; } renderRiderEarnings(); renderRiderAvailabilityControls(rider); scheduleRiderSubscriptionReminderCheck(rider); } function renderRiderAvailabilityControls(rider = currentRiderRecord()) { if (!els.riderGpsStatus || !els.captureRiderGps || !els.clearRiderGps) return; const canActivate = riderCanActivateAvailability(rider); const activated = canActivate && state.riderAvailabilityActivated === true; if (!els.riderGpsStatus.dataset.busy) { els.riderGpsStatus.textContent = canActivate ? riderAvailabilityStatusText(rider) : riderWorkspaceStatusMessage(rider); } els.captureRiderGps.textContent = "Activate"; els.clearRiderGps.textContent = "Deactivate"; els.captureRiderGps.disabled = !canActivate; els.clearRiderGps.disabled = !canActivate; els.captureRiderGps.setAttribute("aria-pressed", String(activated)); } function subscriptionReminderFunctionName() { return String(appConfig.subscriptionReminderFunctionName || "subscription-reminders").trim() || "subscription-reminders"; } function subscriptionReminderDueForRider(rider) { if (!rider || rider.status !== "approved") return false; const end = riderAccessEnd(rider); if (!end) return false; return daysUntil(end) <= subscriptionRenewalNoticeDays; } function subscriptionReminderLocalKey(rider) { return `${subscriptionReminderCheckStorageKey}:${rider.id}:${riderAccessEnd(rider) || "none"}:${localDateKey()}`; } async function sendRiderSubscriptionReminderCheck() { const token = await currentSupabaseAccessToken(); if (!token) throw new Error("Sign in before checking subscription reminders."); const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${subscriptionReminderFunctionName()}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ mode: "self" }) }), "Checking subscription reminders", optionalSupabaseRequestTimeoutMs ); const payload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(payload?.error || "Subscription reminder check failed."); return payload; } function scheduleRiderSubscriptionReminderCheck(rider = currentRiderRecord()) { if (!hasSupabaseRuntime() || !hasSignedIn("rider") || !subscriptionReminderDueForRider(rider)) return; const key = subscriptionReminderLocalKey(rider); try { if (localStorage.getItem(key)) return; } catch { // Storage can be unavailable; the reminder can still be checked once per render cycle. } if (subscriptionReminderChecksInFlight.has(key)) return; subscriptionReminderChecksInFlight.add(key); window.setTimeout(async () => { try { const result = await sendRiderSubscriptionReminderCheck(); try { localStorage.setItem(key, JSON.stringify({ checkedAt: new Date().toISOString(), created: result?.created ?? 0 })); } catch { // Non-critical; reminders are idempotent server-side. } await refreshAccountNotificationsFromSupabase("rider", { force: true }); renderAccountNotices("rider"); } catch (error) { logClientWarning("Rider subscription reminder check failed.", error); } finally { subscriptionReminderChecksInFlight.delete(key); } }, 0); } async function startRiderBackgroundCheck() { const status = els.riderBackgroundCheckStatus; const rider = currentRiderRecord() ?? state.rider; if (!rider || !state.sessions.rider) { if (status) status.textContent = "Sign in as a rider before starting the background check."; return; } if (!rider.backgroundCheckConsentAt) { if (status) status.textContent = "Submit the rider application with background-check consent before starting provider screening."; return; } if (riderManualBackgroundReviewMode()) { if (status) status.textContent = "Waka Cameroon uses manual admin review for this MVP. No provider background-check flow is started from the rider app."; return; } if (!riderCanStartBackgroundCheckFromStatus(rider)) { if (status) status.textContent = riderBackgroundCheckRelaxedForTesting() ? "Eligibility must be pending admin review or Checkr-invited before starting the testing background-check step." : "Admin must review the application and invite you to Checkr before provider screening starts."; return; } if (!hasSupabaseRuntime()) { if (status) status.textContent = "Background checks require the Supabase production runtime."; return; } try { setButtonBusy(els.startRiderBackgroundCheck, true); if (status) { status.dataset.busy = "true"; status.textContent = `Starting ${appConfig.backgroundCheckProvider || "provider"} background check...`; } const payload = { riderId: rider.id }; let responsePayload = null; if (supabaseClient?.functions?.invoke) { const { data, error } = await withSupabaseTimeout( supabaseClient.functions.invoke("background-check-start", { body: payload }), "Starting the provider background check", optionalSupabaseRequestTimeoutMs ); if (error) throw error; responsePayload = data; } else { const token = await currentSupabaseAccessToken(); if (!token) throw new Error("Sign in before starting the background check."); const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/background-check-start`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify(payload) }), "Starting the provider background check", optionalSupabaseRequestTimeoutMs ); responsePayload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(responsePayload?.error || "Background-check Edge Function failed."); } const savedCheck = responsePayload?.check ? mapRiderBackgroundCheckFromDatabase(responsePayload.check) : null; if (savedCheck?.id) { state.backgroundChecks = upsertById(state.backgroundChecks.filter((item) => item.id !== savedCheck.id), savedCheck); const riderPatch = { backgroundCheckStatus: savedCheck.status, backgroundCheckDecision: savedCheck.decision, backgroundCheckProvider: savedCheck.provider, backgroundCheckSummary: savedCheck.summary }; state.rider = { ...state.rider, ...riderPatch }; state.riders = state.riders.map((item) => item.id === rider.id ? { ...item, ...riderPatch } : item); saveState(); } if (responsePayload?.url) { const opened = window.open(responsePayload.url, "_blank", "noopener,noreferrer"); if (status) { status.textContent = opened ? "Background check opened in a separate tab. Complete it there, then return to Waka." : "Popup was blocked. Copy the provider link from your email or allow popups for Waka, then start again."; } return; } if (status) status.textContent = responsePayload?.relaxed ? "Relaxed Checkr testing step recorded. Stay on Eligibility to monitor admin review." : "Background check started. Complete any provider email, SMS, or hosted instructions. Admin will see the result when the provider returns it to Waka."; renderAll(); } catch (error) { const providerHint = /edge function|failed to send|provider secrets|not configured|function/i.test(error.message) ? " Waka still has your admin invitation; contact Waka support if no provider email or hosted page arrives." : ""; if (status) status.textContent = `Could not start background check: ${error.message}.${providerHint}`; } finally { if (status) delete status.dataset.busy; setButtonBusy(els.startRiderBackgroundCheck, false); } } async function startRiderStripeConnectOnboarding({ status = els.riderTaxOnboardingStatus, button = els.startRiderTaxOnboarding, label = "Stripe Connect setup" } = {}) { const rider = currentRiderRecord() ?? state.rider; if (!rider || !state.sessions.rider) { if (status) status.textContent = "Sign in as a rider before starting Stripe setup."; return; } if (rider.status !== "approved") { if (status) status.textContent = "Stripe setup opens after admin approval."; return; } if (!hasSupabaseRuntime()) { if (status) status.textContent = "Stripe setup requires the Supabase production runtime."; return; } try { if (button) setButtonBusy(button, true); if (status) { status.dataset.busy = "true"; status.textContent = `Opening ${label}...`; } const payload = {}; let responsePayload = null; if (supabaseClient?.functions?.invoke) { const { data, error } = await withSupabaseTimeout( supabaseClient.functions.invoke("tax-onboarding-start", { body: payload }), "Starting hosted tax onboarding", supabaseProfileSaveTimeoutMs ); if (error) throw error; responsePayload = data; } else { const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/tax-onboarding-start`, { method: "POST", headers: { "content-type": "application/json", apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${supabaseRestSession?.access_token || appConfig.supabaseAnonKey}` }, body: JSON.stringify(payload) }), "Starting hosted tax onboarding", supabaseProfileSaveTimeoutMs ); responsePayload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(responsePayload?.error || "Hosted tax onboarding Edge Function failed."); } if (responsePayload?.reference?.id) { const reference = mapTaxIdentityReferenceFromDatabase({ id: responsePayload.reference.id, rider_id: responsePayload.reference.rider_id ?? rider.id, provider: responsePayload.reference.provider, provider_subject_id: responsePayload.reference.provider_subject_id, tax_profile_status: responsePayload.reference.tax_profile_status, tin_last4: responsePayload.reference.tin_last4, legal_name: responsePayload.reference.legal_name, business_name: responsePayload.reference.business_name, tax_classification: responsePayload.reference.tax_classification, last_verified_at: responsePayload.reference.last_verified_at, created_at: responsePayload.reference.created_at, updated_at: responsePayload.reference.updated_at }); state.taxIdentityReferences = upsertById( state.taxIdentityReferences.filter((item) => item.riderId !== rider.id), reference ); } if (responsePayload?.paymentAccount?.id) { const paymentAccount = mapPaymentAccountFromDatabase(responsePayload.paymentAccount, new Map([[rider.id, { full_name: rider.name }]])); state.paymentAccounts = upsertById( state.paymentAccounts.filter((item) => !(item.role === "rider" && item.userId === rider.id)), paymentAccount ); } saveState(); renderAll(); if (!responsePayload?.url) throw new Error("Stripe did not return a hosted onboarding URL."); const opened = window.open(responsePayload.url, "_blank", "noopener,noreferrer"); if (status) { status.textContent = opened ? "Stripe opened in a separate tab. Finish onboarding there, then return to Waka." : "Popup was blocked. Allow popups for Waka, then try Stripe setup again."; } } catch (error) { if (button === els.startRiderStripePayoutSetup && paymentSetupRelaxedForTesting()) { try { if (status) status.textContent = `Stripe payout setup could not open: ${error.message}. Staging is linking a test payout account...`; const stagingAccount = { id: paymentAccountFor("rider", rider.id)?.id ?? makeId("payacct"), userId: rider.id, userName: rider.name, role: "rider", provider: "stripe-connect-test", accountType: "test_payout_account", accountHolder: rider.name || "Waka rider", accountLast4: "0000", institutionName: "Stripe Connect staging payout", reference: `staging-rider-payout-${rider.id}`, status: "linked", createdAt: paymentAccountFor("rider", rider.id)?.createdAt ?? new Date().toISOString(), updatedAt: new Date().toISOString() }; let savedAccount = stagingAccount; let localOnly = false; try { savedAccount = await savePaymentAccountToSupabase(stagingAccount); } catch (saveError) { localOnly = true; logClientWarning("Staging rider payout account could not be saved to Supabase; keeping it local for pilot testing.", saveError); } state.paymentAccounts = upsertById( state.paymentAccounts.filter((item) => !(item.role === "rider" && item.userId === rider.id)), savedAccount ); saveState(); renderAll(); void refreshMarketplace({ silent: true }); if (status) { status.textContent = localOnly ? `Staging test payout account is linked on this device because Stripe setup returned: ${error.message}` : `Staging test payout account is linked because Stripe setup returned: ${error.message}`; } return; } catch (fallbackError) { if (status) status.textContent = `Stripe setup failed, and the staging test payout account could not be linked: ${fallbackError.message}`; return; } } const setupHint = /edge function|failed to send|function|not configured|provider secrets/i.test(error.message) ? " Stripe Connect setup needs the Supabase Edge Function and Stripe secrets configured; Waka still keeps you in this rider workspace." : ""; if (status) status.textContent = `Could not start Stripe setup: ${error.message}.${setupHint}`; } finally { if (status) delete status.dataset.busy; if (button) setButtonBusy(button, false); } } async function startRiderTaxOnboarding() { if (String(appConfig.taxOnboardingMode || "").toLowerCase() === "disabled") { if (els.riderTaxOnboardingStatus) { els.riderTaxOnboardingStatus.textContent = "No hosted tax-provider flow is required for this Cameroon MVP package."; } return; } return startRiderStripeConnectOnboarding({ status: els.riderTaxOnboardingStatus, button: els.startRiderTaxOnboarding, label: `${appConfig.taxOnboardingProvider || "Stripe Connect"} hosted tax and payout setup` }); } async function startRiderStripePayoutSetup() { const rider = currentRiderRecord(); if (directRidePaymentMode()) { if (els.riderPaymentStatus) { els.riderPaymentStatus.textContent = paymentAccountSummary("rider", rider); } return; } return startRiderStripeConnectOnboarding({ status: els.riderPaymentStatus, button: els.startRiderStripePayoutSetup, label: "Stripe Connect payout setup" }); } function automaticRiderGpsReady() { return autoRiderGpsEnabled() && state.riderAvailabilityActivated === true && activeRole() === "rider" && riderCanActivateAvailability(currentRiderRecord()); } function riderAutoGpsSyncPolicy() { const activeRide = riderActiveImmediateRide(currentRiderRecord()); if (activeRide) { return { mode: "active", intervalMs: riderAutoGpsActiveRideSyncIntervalMs, minElapsedMs: riderAutoGpsActiveRideMinElapsedMs, movementMeters: riderAutoGpsActiveRideMinMovementMeters }; } return { mode: "matching", intervalMs: riderAutoGpsMovingSyncIntervalMs, movementMeters: riderAutoGpsMovingMinMovementMeters, idleIntervalMs: riderAutoGpsIdleSyncIntervalMs, idleMovementMeters: riderAutoGpsIdleHeartbeatMeters }; } function shouldSyncRiderGpsPoint(point, options = {}) { if (options.force) return true; if (!lastRiderAutoGpsSyncPoint || !lastRiderAutoGpsSyncAt) return true; const policy = riderAutoGpsSyncPolicy(); const elapsedMs = Date.now() - lastRiderAutoGpsSyncAt; const movedMeters = gpsDistanceMetersBetween(point, lastRiderAutoGpsSyncPoint); if (policy.mode === "active") { return elapsedMs >= policy.intervalMs || (movedMeters != null && movedMeters >= policy.movementMeters && elapsedMs >= policy.minElapsedMs); } if (movedMeters != null && movedMeters >= policy.movementMeters && elapsedMs >= policy.intervalMs) return true; if (elapsedMs >= policy.idleIntervalMs) return true; return movedMeters != null && movedMeters >= policy.idleMovementMeters && elapsedMs >= policy.intervalMs; } async function saveRiderLiveGpsPoint(currentGps, options = {}) { const rider = currentRiderRecord(); if (!rider || !hasSignedIn("rider")) return null; const qualityIssue = riderLiveGpsQualityIssue(currentGps); if (qualityIssue) { if (els.riderGpsStatus) els.riderGpsStatus.textContent = qualityIssue; return null; } if (!shouldSyncRiderGpsPoint(currentGps, options)) return rider; if (riderAutoGpsSyncPromise) return riderAutoGpsSyncPromise; const inferredLocation = inferredLaunchLocationFromGps(rider.country ?? selectedRiderCountry(), currentGps); const inferredCountry = inferredLocation?.country ?? rider.country; const inferredCity = inferredLocation?.city ?? rider.city; const inferredArea = inferredLocation?.area ?? rider.area; const locationChanged = inferredLocation && (rider.country !== inferredCountry || rider.city !== inferredCity || rider.area !== inferredArea); if (locationChanged) { try { await updateRiderCurrentAreaInSupabase(rider.supabaseUserId ?? rider.id, inferredCountry, inferredCity, inferredArea); } catch (error) { logClientWarning("Rider GPS city persistence failed; using local inferred city for this session.", error); } } const nextRider = { ...rider, country: inferredCountry, city: inferredCity, area: inferredArea, currentGps, currentLatitude: currentGps.latitude, currentLongitude: currentGps.longitude, currentGpsAccuracyMeters: currentGps.accuracyMeters, currentGpsCapturedAt: currentGps.capturedAt }; riderAutoGpsSyncPromise = (async () => { await updateRiderLocationPresenceInSupabase(nextRider); const savedRider = { ...nextRider, supabaseUserId: state.rider?.supabaseUserId ?? nextRider.supabaseUserId }; saveCurrentRiderRecord(savedRider); if (els.riderActiveCountry) els.riderActiveCountry.value = savedRider.country; if (els.riderActiveCity) els.riderActiveCity.value = savedRider.city; if (els.riderActiveArea) populateSelect(els.riderActiveArea, areas(savedRider.country, savedRider.city).map((area) => area.name), savedRider.area); lastRiderAutoGpsSyncAt = Date.now(); lastRiderAutoGpsSyncPoint = currentGps; void refreshMarketplace({ silent: true }); if (els.riderGpsStatus) { const locationText = savedRider.city ? ` in ${[savedRider.area, savedRider.city].filter(Boolean).join(", ")}` : ""; els.riderGpsStatus.textContent = hasSupabaseRuntime() ? `Online and available${locationText}. ${gpsStatusLabel(currentGps, "Location active")} is used to match nearby requests.` : `Online in this local workspace${locationText}. ${gpsStatusLabel(currentGps, "Location active")}.`; } renderRiderAvailabilityControls(savedRider); return savedRider; })(); try { return await riderAutoGpsSyncPromise; } finally { riderAutoGpsSyncPromise = null; } } function stopAutomaticRiderGps() { if (riderGpsWatchId != null && navigator.geolocation?.clearWatch) { navigator.geolocation.clearWatch(riderGpsWatchId); } riderGpsWatchId = null; if (riderGpsHeartbeatTimer != null) { window.clearInterval(riderGpsHeartbeatTimer); } riderGpsHeartbeatTimer = null; } let riderScreenWakeLockEventsWired = false; function riderGpsHeartbeatShouldBeActive() { return Boolean( autoRiderGpsEnabled() && automaticRiderGpsReady() && activeRole() === "rider" && !document.hidden && hasSignedIn("rider") && currentRiderRecord() ); } async function runRiderGpsHeartbeat() { if (!riderGpsHeartbeatShouldBeActive()) return; try { const point = typeof getBestCurrentGpsPoint === "function" ? await getBestCurrentGpsPoint({ desiredAccuracyMeters: riderLiveGpsAccuracyLimitMeters(), samples: 2, totalTimeoutMs: 12000, sampleTimeoutMs: 7000 }) : await getCurrentGpsPoint({ maximumAge: 0, timeout: 12000, enableHighAccuracy: true }); await saveRiderLiveGpsPoint(point, { automatic: true, force: true }); } catch (error) { logClientWarning("Background rider GPS heartbeat could not refresh location.", error); } } function refreshRiderGpsHeartbeat() { if (!riderGpsHeartbeatShouldBeActive()) { if (riderGpsHeartbeatTimer != null) window.clearInterval(riderGpsHeartbeatTimer); riderGpsHeartbeatTimer = null; return; } if (riderGpsHeartbeatTimer != null) return; riderGpsHeartbeatTimer = window.setInterval(() => { void runRiderGpsHeartbeat(); }, 4 * 60 * 1000); } function ensureRiderScreenWakeLockEvents() { if (riderScreenWakeLockEventsWired) return; riderScreenWakeLockEventsWired = true; window.addEventListener("focus", () => void ensureRiderScreenWakeLock()); document.addEventListener("visibilitychange", () => { if (!document.hidden) void ensureRiderScreenWakeLock(); refreshRiderGpsHeartbeat(); }); ["pointerdown", "touchstart", "keydown"].forEach((eventName) => { document.addEventListener(eventName, () => void ensureRiderScreenWakeLock(), { passive: true }); }); } function riderScreenWakeLockShouldBeActive() { return Boolean( activeRole() === "rider" && !document.hidden && hasSignedIn("rider") && currentRiderRecord() ); } async function releaseRiderScreenWakeLock() { if (!riderScreenWakeLock) return; const lock = riderScreenWakeLock; riderScreenWakeLock = null; try { await lock.release?.(); } catch (error) { logClientWarning("Rider screen wake lock could not be released.", error); } } async function ensureRiderScreenWakeLock() { ensureRiderScreenWakeLockEvents(); if (!riderScreenWakeLockShouldBeActive()) { await releaseRiderScreenWakeLock(); return; } if (!navigator.wakeLock?.request) return; if (riderScreenWakeLock) return; if (Date.now() < riderScreenWakeLockRetryAfter) return; try { riderScreenWakeLock = await navigator.wakeLock.request("screen"); riderScreenWakeLockRetryAfter = 0; riderScreenWakeLock.addEventListener?.("release", () => { riderScreenWakeLock = null; if (riderScreenWakeLockShouldBeActive()) { window.setTimeout(() => void ensureRiderScreenWakeLock(), 1000); } }, { once: true }); } catch (error) { riderScreenWakeLockRetryAfter = Date.now() + riderScreenWakeLockRetryDelayMs; logClientWarning("Rider screen wake lock is unavailable in this browser session.", error); } } function ensureAutomaticRiderGps() { if (!autoRiderGpsEnabled()) return; if (!navigator.geolocation) { if (activeRole() === "rider" && els.riderGpsStatus) els.riderGpsStatus.textContent = "Availability cannot be activated because location is not available in this browser."; return; } if (!automaticRiderGpsReady()) { stopAutomaticRiderGps(); return; } refreshRiderGpsHeartbeat(); if (riderGpsWatchId != null) return; if (els.riderGpsStatus) els.riderGpsStatus.textContent = "Activating high-accuracy GPS..."; riderGpsWatchId = navigator.geolocation.watchPosition( (position) => { const currentGps = gpsPointFromPosition(position); if (!currentGps) return; void ensureRiderScreenWakeLock(); void saveRiderLiveGpsPoint(currentGps, { automatic: true }); }, () => { if (els.riderGpsStatus) els.riderGpsStatus.textContent = "Availability could not be activated because location permission was denied or unavailable."; }, { enableHighAccuracy: true, timeout: 20000, maximumAge: 0 } ); } async function captureRiderLiveGps() { const rider = currentRiderRecord(); if (!rider || !hasSignedIn("rider")) { els.riderGpsStatus.textContent = "Sign in as a rider before activating availability."; return; } if (!riderCanActivateAvailability(rider)) { els.riderGpsStatus.textContent = riderWorkspaceStatusMessage(rider); return; } try { await assertPlatformFeatureEnabled("rider_activation_enabled", "Rider availability"); state.riderAvailabilityActivated = true; riderAutoGpsPaused = false; saveState(); els.riderGpsStatus.dataset.busy = "true"; els.riderGpsStatus.textContent = "Finding your clearest live GPS..."; if (paymentSetupRelaxedForTesting()) { await ensureStagingPaymentAccountForTesting("rider", rider, { localFallback: false }); } const currentGps = typeof getBestCurrentGpsPoint === "function" ? await getBestCurrentGpsPoint({ desiredAccuracyMeters: riderLiveGpsAccuracyLimitMeters(), samples: 4, totalTimeoutMs: 22000, sampleTimeoutMs: 8000 }) : await getCurrentGpsPoint({ maximumAge: 0, timeout: 15000, enableHighAccuracy: true }); await saveRiderLiveGpsPoint(currentGps, { automatic: false, force: true }); void ensureRiderScreenWakeLock(); ensureAutomaticRiderGps(); refreshRiderGpsHeartbeat(); state.riderPage = "requests"; state.selectedRequestId = null; if (typeof updateRiderWorkspaceRoute === "function") { updateRiderWorkspaceRoute("requests", { replace: true }); } saveState(); els.riderGpsStatus.textContent = "Activated. Opening marketplace."; renderAll(); void refreshMarketplace({ silent: true, reason: "rider_activated" }); } catch (error) { state.riderAvailabilityActivated = false; riderAutoGpsPaused = true; saveState(); els.riderGpsStatus.textContent = error.message; } finally { delete els.riderGpsStatus.dataset.busy; } } async function clearRiderLiveGps() { state.riderAvailabilityActivated = false; riderAutoGpsPaused = true; saveState(); stopAutomaticRiderGps(); const rider = currentRiderRecord(); if (!rider || !hasSignedIn("rider")) { els.riderGpsStatus.textContent = "Sign in as a rider before changing availability."; return; } try { els.riderGpsStatus.dataset.busy = "true"; els.riderGpsStatus.textContent = "Deactivating availability..."; const clearedRider = clearRiderLiveGpsFields(rider); await clearRiderLiveGpsInSupabase(clearedRider); saveCurrentRiderRecord(clearedRider); renderAll(); void refreshMarketplace({ silent: true }); els.riderGpsStatus.textContent = "Offline. Activate when you are ready to receive nearby ride requests."; renderRiderAvailabilityControls(clearedRider); } catch (error) { els.riderGpsStatus.textContent = error.message; } finally { delete els.riderGpsStatus.dataset.busy; } } async function updateRiderActiveLocation(event) { event.preventDefault(); if (!state.rider || !hasSignedIn("rider")) return; const country = els.riderActiveCountry.value; const city = els.riderActiveCity.value; const area = els.riderActiveArea.value; state.riderDestinationScope = "all"; if (els.riderDestinationScope) els.riderDestinationScope.value = "all"; const showAllNearbyPickups = true; const regionsToSave = []; const shouldSavePreference = true; const existingPreference = riderDayPreferenceFor(state.rider); const updatesUsed = existingPreference?.updatesUsed ?? 0; if (shouldSavePreference && updatesUsed >= 2) { els.riderDailyRegionStatus.textContent = "Today's destination regions were already set and updated once. Try again tomorrow."; return; } try { els.riderLocationStatus.textContent = !showAllNearbyPickups ? "Saving today's preferred rider regions..." : "Saving all-nearby pickup visibility..."; const riderId = state.rider.supabaseUserId ?? state.rider.id; await updateRiderCurrentAreaInSupabase(riderId, country, city, area); let savedPreference = existingPreference ?? state.rider.dailyRegions ?? null; if (shouldSavePreference) { const preference = { id: existingPreference?.id ?? makeId("day"), riderId: state.rider.id, riderName: state.rider.name, serviceDate: localDateKey(), country, city, originArea: area, regions: regionsToSave, showAllNearbyPickups, updatesUsed: updatesUsed + 1, createdAt: existingPreference?.createdAt ?? new Date().toISOString(), updatedAt: new Date().toISOString() }; savedPreference = await saveRiderDayPreferenceToSupabase(preference); } state.rider = { ...state.rider, country, city, area, dailyRegions: savedPreference ?? null }; state.riders = upsertById(state.riders, state.rider); if (savedPreference) { state.riderDayPreferences = upsertById( state.riderDayPreferences.filter((item) => !(item.riderId === state.rider.id && item.serviceDate === savedPreference.serviceDate)), savedPreference ); } clearSelectedRequestOutsideLocation(country, city); saveState(); if (lastLocationUpdateSource !== "location update RPC") { await updateRiderLocationPresenceInSupabase(state.rider); } populateLocationFields(); hydrateForms(); renderAll(); void refreshMarketplace({ silent: true }); renderRiderAvailabilityControls(state.rider); els.riderLocationStatus.textContent = riderServiceAreaSummary(state.rider); renderRiderDailyRegionStatus(state.rider); } catch (error) { els.riderLocationStatus.textContent = error.message; } } function riderEmailConfirmationGuidance(email) { const fallback = `Good news - your Waka Cameroon rider account and application were received.\n\nWaka Cameroon has sent a confirmation link to ${email}. Open your email and click the link to complete account access. If you do not see it, check Spam or Junk. Also verify that the email address you entered is correct. After confirming, return here and sign in with the same email and password. Your application will be waiting for Waka admin review.`; if (typeof translatedMessage !== "function") return fallback; return translatedMessage("riderEmailConfirmationPopup", { email }) || fallback; } function riderApplicationSubmittedGuidance(name) { const fallback = `${name || "Your rider application"} has been submitted for Waka admin review.\n\nYou do not need to fill this form again. Waka admin will review the account, identity, vehicle or bike, and uploaded documents. If anything else is needed, Waka will request corrections from your rider workspace.`; if (typeof translatedMessage !== "function") return fallback; return translatedMessage("riderApplicationSubmittedPopup", { name: name || "Your rider application" }) || fallback; } function showRiderEmailConfirmationGuidance(email) { const message = riderEmailConfirmationGuidance(email); const openGuidance = () => { if (typeof showWakaGoodAlert === "function") { void showWakaGoodAlert(message); return; } if (typeof window !== "undefined" && typeof window.alert === "function") { window.alert(message); } }; if (typeof window !== "undefined" && typeof window.setTimeout === "function") { window.setTimeout(openGuidance, 0); } else { openGuidance(); } } function showRiderApplicationSubmittedGuidance(name) { const message = riderApplicationSubmittedGuidance(name); const openGuidance = () => { if (typeof showWakaGoodAlert === "function") { void showWakaGoodAlert(message); return; } if (typeof window !== "undefined" && typeof window.alert === "function") { window.alert(message); } }; if (typeof window !== "undefined" && typeof window.setTimeout === "function") { window.setTimeout(openGuidance, 0); } else { openGuidance(); } } function clearOptionalRiderUploadRequirements() { [ els.riderLicenseDocument, els.riderRegistrationDocument, els.riderInsuranceDocument, els.riderInspectionDocument ].forEach((input) => { if (input) input.required = false; }); } async function createRiderLoginOnly() { if (typeof syncRiderApplicationOnlyMode === "function") { syncRiderApplicationOnlyMode(false, state.rider); } if (!validateAccountForm(els.riderAccountForm, els.riderStatus)) return; const phone = els.riderPhone.value.trim(); if (!(await ensureVerifiedPhoneForAccount("rider", phone, els.riderStatus))) { if (!configFlagEnabled(appConfig.relaxSmsVerificationForTesting)) return; markSmsRelaxedPhoneVerified("rider", phone, els.riderStatus); } const country = els.riderCountry?.value || state.rider?.country || defaultLaunchCountry(); const city = els.riderCity?.value || state.rider?.city || defaultLaunchCity(country); const profilePhotoName = els.riderPhoto.files[0]?.name ?? state.rider?.profilePhotoName ?? ""; const rider = { id: state.rider?.id ?? makeId("rider"), name: els.riderName.value.trim(), email: els.riderEmail.value.trim().toLowerCase(), password: els.riderPassword.value, phone, phoneVerified: true, phoneVerifiedAt: state.verification.rider?.verifiedAt ?? state.rider?.phoneVerifiedAt ?? new Date().toISOString(), phoneVerificationProvider: state.verification.rider?.provider ?? "manual-pilot", nationalId: state.rider?.nationalId ?? "", dateOfBirth: state.rider?.dateOfBirth ?? null, preferredLanguage: state.language, country, city, area: state.rider?.area ?? "", profilePhotoName, profilePhotoPath: state.rider?.profilePhotoPath ?? null, needsApplication: true, status: "profile only", createdAt: state.rider?.createdAt ?? new Date().toISOString() }; try { setButtonBusy(els.riderSubmitButton, true); const setRiderStage = (message) => { els.riderStatus.textContent = message; }; setRiderStage("Creating rider login..."); let user = null; if (hasSupabaseRuntime()) { const onboardingResult = await submitRiderAccountViaOnboardingFunction(rider, setRiderStage); user = await riderOnboardingFunctionUserResult(rider, onboardingResult, setRiderStage); } else { user = await saveProfileToSupabase({ ...rider, role: "rider" }, setRiderStage, { waitForProfile: true, preventExistingAccount: true, requireExplicitPasswordSignIn: true }); } state.rider = { ...rider, password: undefined, id: user?.id ?? rider.id, supabaseUserId: user?.id ?? null, profilePhotoPath: user?.profilePhotoPath ?? rider.profilePhotoPath }; if (user?.emailSetupPending) { state.sessions.rider = null; setPendingProfileRecovery("rider", { id: user?.id ?? state.rider.id, email: state.rider.email, phone: state.rider.phone, user_metadata: { full_name: state.rider.name, phone: state.rider.phone } }, state.rider.email); } else { activateWorkspaceRoleSession("rider", { phone: state.rider.phone, email: state.rider.email, userId: state.rider.supabaseUserId, signedInAt: new Date().toISOString() }); clearPendingProfileRecovery("rider"); await claimReferralCodeForRole("rider", els.riderStatus); } els.riderPassword.value = ""; els.riderPhoto.value = ""; state.riders = upsertById(state.riders, state.rider); state.accountMode.rider = "signin"; state.riderPage = "profile"; saveState(); renderAll(); if (user?.emailSetupPending) { if (els.riderSignInEmail) els.riderSignInEmail.value = state.rider.email; if (els.riderSignInPassword) els.riderSignInPassword.value = ""; const status = els.riderSignInStatus ?? els.riderStatus; if (status) { status.textContent = riderEmailConfirmationGuidance(state.rider.email); status.classList.add("success-status"); status.classList.remove("error-status"); } showRiderEmailConfirmationGuidance(state.rider.email); routeRiderAccountCreationToSignIn(state.rider, new Error("email confirmation is required before sign-in")); } else { setTranslatedStatus(els.riderStatus, "signedInRiderLoaded", { email: state.rider.email }); els.riderStatus.textContent = "Rider login created. Complete the rider application once from Profile for Waka admin review."; els.riderStatus?.classList.add("success-status"); if (els.riderSessionSummary) els.riderSessionSummary.textContent = riderWorkspaceStatusMessage(state.rider); } } catch (error) { if (riderAccountCreationRequiresSignIn(error)) { routeRiderAccountCreationToSignIn(rider, error); return; } setTranslatedStatus(els.riderStatus, "riderAccountFailed", { message: riderApplicationErrorMessage(error) }); els.riderStatus?.classList.add("error-status"); } finally { setButtonBusy(els.riderSubmitButton, false); } } async function createRider(event) { event.preventDefault(); els.riderStatus?.classList.remove("success-status", "error-status"); setTranslatedStatus(els.riderStatus, "checkingRiderApplication"); clearOptionalRiderUploadRequirements(); if (!hasSignedIn("rider") && accountMode("rider") === "create") { if (typeof syncRiderApplicationOnlyMode === "function") { syncRiderApplicationOnlyMode(false, state.rider); } } const resubmittingCorrections = state.rider?.status === "needs_correction"; const finishingExistingApplication = Boolean(hasSignedIn("rider") && state.rider && (state.rider.needsApplication || state.rider.status === "profile only")); const country = els.riderCountry.value || defaultLaunchCountry(); const city = els.riderCity.value || cityNames(country)[0] || defaultLaunchCity(country) || ""; const area = els.riderArea.value || areas(country, city)[0]?.name || city || country; if (els.riderCity && !els.riderCity.value && city) els.riderCity.value = city; if (els.riderArea && !els.riderArea.value && area) els.riderArea.value = area; const vehicle = normalizeRideVehicle(els.riderVehicle?.value); const documentFiles = selectedRiderDocumentFiles(); const selectedDocumentNames = Object.fromEntries(Object.entries(documentFiles) .filter(([, file]) => Boolean(file)) .map(([key, file]) => [key, file.name])); const documentNames = { ...((resubmittingCorrections || finishingExistingApplication) ? riderDocuments(state.rider) : emptyRiderDocuments()), ...selectedDocumentNames }; if (vehicle === "bike") { documentNames.insurance = ""; documentNames.vehicleInspection = ""; } const profilePhotoName = els.riderPhoto.files[0]?.name ?? state.rider?.profilePhotoName ?? ""; const phone = els.riderPhone.value.trim() || state.rider?.phone || ""; const birthMonth = normalizeYearMonthOfBirthInput(els.riderDob); const dateOfBirth = yearMonthToStoredDate(birthMonth) || state.rider?.dateOfBirth || null; const enteredNationalId = els.riderNationalId.value.trim(); const pendingIdentityReference = `PENDING-${phone.replace(/\D/g, "").slice(-12) || String(Date.now()).slice(-8)}`; const riderIdentityReference = enteredNationalId || state.rider?.credential || state.rider?.nationalId || pendingIdentityReference; const driverLicenseExpiresOn = els.riderLicenseExpiresOn?.value ?? ""; const insuranceExpiresOn = vehicle === "bike" ? "" : els.riderInsuranceExpiresOn?.value ?? ""; const vehicleBodyType = vehicle === "bike" ? "motorbike" : normalizeCarBodyType(els.riderCarBodyType.value); const vehicleDesignation = vehicle === "bike" ? "normal" : normalizeRiderVehicleDesignation(els.riderVehicleDesignation?.value, vehicleBodyType); const missingDocuments = resubmittingCorrections ? riderMissingRequestedDocumentLabels(documentNames, vehicle) : []; if (!validateAccountForm(els.riderAccountForm, els.riderStatus)) return; if (birthMonth && !dateOfBirth) { els.riderStatus.textContent = "Enter the year and month of birth as YYYY-MM, or leave it blank for admin to request later."; return; } if (driverLicenseExpiresOn && daysUntilDate(driverLicenseExpiresOn) < 0) { els.riderStatus.textContent = "Driver's license expiration date cannot be in the past."; return; } if (insuranceExpiresOn && daysUntilDate(insuranceExpiresOn) < 0) { els.riderStatus.textContent = "Insurance expiration date cannot be in the past."; return; } if (missingDocuments.length) { setTranslatedStatus(els.riderStatus, "missingRiderDocuments", { documents: missingDocuments.join(", ") }); els.riderStatus?.classList.add("error-status"); return; } if (!(await ensureVerifiedPhoneForAccount("rider", phone, els.riderStatus))) { if (!configFlagEnabled(appConfig.relaxSmsVerificationForTesting)) return; markSmsRelaxedPhoneVerified("rider", phone, els.riderStatus); } const rider = { id: state.rider?.id ?? makeId("rider"), name: els.riderName.value.trim() || state.rider?.name || "", email: (els.riderEmail.value.trim().toLowerCase() || state.rider?.email || ""), password: els.riderPassword.value, phone: phone || state.rider?.phone || "", phoneVerified: true, phoneVerifiedAt: state.verification.rider?.verifiedAt ?? state.rider?.phoneVerifiedAt ?? new Date().toISOString(), phoneVerificationProvider: state.verification.rider?.provider ?? "manual-pilot", nationalId: enteredNationalId || state.rider?.nationalId || "", dateOfBirth, preferredLanguage: state.language, country, city, area, vehicle, credential: riderIdentityReference, driverLicenseExpiresOn, registration: els.riderRegistration.value.trim(), carMake: els.riderCarMake.value, carModel: els.riderCarModel.value, carBodyType: vehicleBodyType, vehicleDesignation, navigationPreference: normalizeRiderNavigationPreference(els.riderNavigationPreference?.value), carYear: els.riderCarYear.value, carColor: els.riderCarColor.value.trim(), vehicleVin: els.riderVehicleVin.value.trim().toUpperCase(), insuranceProvider: vehicle === "bike" ? "" : els.riderInsuranceProvider.value.trim(), insuranceNumber: vehicle === "bike" ? "" : els.riderInsuranceNumber.value.trim(), insuranceExpiresOn, backgroundCheckConsentAt: new Date().toISOString(), backgroundCheckProvider: appConfig.backgroundCheckProvider || "not-required-cameroon", backgroundCheckConsentVersion: "cameroon-pilot-admin-review", profilePhotoName, profilePhotoPath: state.rider?.profilePhotoPath ?? null, documentName: riderApplicationDocumentPayload(documentNames, { carBodyType: vehicleBodyType, vehicleDesignation, navigationPreference: normalizeRiderNavigationPreference(els.riderNavigationPreference?.value) }), documents: { ...documentNames, vehicleDesignation, navigationPreference: normalizeRiderNavigationPreference(els.riderNavigationPreference?.value) }, nationalIdentityDocumentName: documentNames.nationalIdentity, driverLicenseDocumentName: documentNames.driverLicense, vehicleRegistrationDocumentName: documentNames.vehicleRegistration, insuranceDocumentName: documentNames.insurance, vehicleInspectionDocumentName: documentNames.vehicleInspection, backgroundCheckStatus: resubmittingCorrections ? state.rider?.backgroundCheckStatus ?? "not requested" : "not requested", backgroundCheckDecision: resubmittingCorrections ? state.rider?.backgroundCheckDecision ?? "pending" : "pending", status: "pending", reviewNote: "", approvedAt: null, trialEndsAt: null, subscriptionPaidUntil: null, rating: "new", createdAt: new Date().toISOString() }; const missingApplicationFields = []; if (!rider.registration) missingApplicationFields.push(vehicle === "bike" ? "Bike plate number" : "Plate number"); if (!rider.carMake) missingApplicationFields.push(vehicle === "bike" ? "Bike make" : "Vehicle make"); if (!rider.carModel) missingApplicationFields.push(vehicle === "bike" ? "Bike model" : "Vehicle model"); if (!rider.carColor) missingApplicationFields.push("Color"); if (!rider.area) missingApplicationFields.push("Operating area"); if (missingApplicationFields.length) { els.riderStatus.textContent = `Complete these rider application fields before submitting: ${missingApplicationFields.join(", ")}.`; els.riderStatus?.classList.add("error-status"); return; } let syncedRiderUser = null; try { setButtonBusy(els.riderSubmitButton, true); const setRiderStage = (message) => { els.riderStatus.textContent = message; }; setTranslatedStatus(els.riderStatus, isSupabaseMode() ? "startingRiderSupabase" : "savingRiderApplication"); const signedInUser = hasSupabaseRuntime() ? await getSupabaseUser().catch((error) => { logClientWarning("Current Supabase user could not be checked before rider onboarding.", error); return null; }) : null; const signedInUserOwnsEmail = authUserEmail(signedInUser) === rider.email; const signedInUserOwnsVerifiedPhone = authUserMatchesVerifiedPhone(signedInUser, rider); if (hasSupabaseRuntime() && !finishingExistingApplication) { try { const excludeUserId = await profileAvailabilityExcludeUserId(rider.email, state.rider?.id ?? null); const availability = await profileContactAvailability(rider.email, rider.phone, excludeUserId, "rider"); if (!availability.emailAvailable || !availability.phoneAvailable) { const secureOnboardingCanVerifyOwner = !resubmittingCorrections && !signedInUserOwnsEmail && !signedInUserOwnsVerifiedPhone; if (!secureOnboardingCanVerifyOwner) { els.riderStatus.textContent = !availability.emailAvailable ? "A rider account already exists with this email address. Sign in with that rider account instead of creating a duplicate." : "A rider account already exists with this phone number. Sign in with that rider account instead of creating a duplicate."; return; } logClientWarning("Rider contact availability is reserved for secure onboarding owner verification.", availability); } } catch (error) { const availabilityMessage = profileAvailabilityErrorMessage(error); if (availabilityMessage) { els.riderStatus.textContent = availabilityMessage; return; } logClientWarning("Profile contact availability check was skipped.", error); } } let onboardingFunctionResult = null; let user = null; if (hasSupabaseRuntime() && finishingExistingApplication) { user = signedInUser; if (!user?.id) throw new Error("Sign in with the rider account before submitting the application details."); if (authUserEmail(user) && authUserEmail(user) !== rider.email) { throw new Error("The signed-in rider account does not match the email on this application."); } } else if (hasSupabaseRuntime() && !resubmittingCorrections) { onboardingFunctionResult = await submitRiderApplicationViaOnboardingFunction(rider, setRiderStage); user = await riderOnboardingFunctionUserResult(rider, onboardingFunctionResult, setRiderStage); } else { user = await saveProfileToSupabase({ ...rider, role: "rider" }, setRiderStage, { waitForProfile: true, preventExistingAccount: false, requireExplicitPasswordSignIn: true }); } syncedRiderUser = user; setTranslatedStatus(els.riderStatus, "submittingRiderApplication"); let savedDocuments = onboardingFunctionResult?.documents ?? null; if (!savedDocuments) { savedDocuments = await saveRiderApplicationToSupabase(rider, user?.id) ?? rider.documents; } state.rider = { ...rider, password: undefined, id: user?.id ?? rider.id, profilePhotoPath: user?.profilePhotoPath ?? rider.profilePhotoPath, documentName: riderApplicationDocumentPayload(savedDocuments, rider), documents: { ...savedDocuments, vehicleDesignation: rider.vehicleDesignation, navigationPreference: rider.navigationPreference }, nationalIdentityDocumentPath: savedDocuments.nationalIdentity, driverLicenseDocumentPath: savedDocuments.driverLicense, vehicleRegistrationDocumentPath: savedDocuments.vehicleRegistration, insuranceDocumentPath: savedDocuments.insurance, vehicleInspectionDocumentPath: savedDocuments.vehicleInspection, supabaseUserId: user?.id ?? null }; if (user?.emailSetupPending) { state.sessions.rider = null; } else { activateWorkspaceRoleSession("rider", { phone: state.rider.phone, email: state.rider.email, userId: state.rider.supabaseUserId, signedInAt: new Date().toISOString() }); await claimReferralCodeForRole("rider", els.riderStatus); } els.riderPassword.value = ""; els.riderPhoto.value = ""; if (els.riderNationalIdDocument) els.riderNationalIdDocument.value = ""; els.riderLicenseDocument.value = ""; els.riderRegistrationDocument.value = ""; els.riderInsuranceDocument.value = ""; els.riderInspectionDocument.value = ""; state.riders = state.riders.filter((item) => item.id !== rider.id && item.id !== user?.id); state.riders.unshift(state.rider); state.accountMode.rider = "signin"; clearPendingProfileRecovery("rider"); state.riderPage = user?.emailSetupPending ? "profile" : "checks"; saveState(); renderAll(); const emailPendingMessage = riderEmailConfirmationGuidance(rider.email); if (user?.emailSetupPending) { if (els.riderSignInEmail) els.riderSignInEmail.value = rider.email; if (els.riderSignInPassword) els.riderSignInPassword.value = ""; const status = els.riderSignInStatus ?? els.riderStatus; if (status) status.textContent = emailPendingMessage; status?.classList?.add("success-status"); showRiderEmailConfirmationGuidance(rider.email); } else { setTranslatedStatus(els.riderStatus, "riderCreatedPending", { name: state.rider.name }); els.riderStatus?.classList.add("success-status"); showRiderApplicationSubmittedGuidance(state.rider.name); } if (els.riderBackgroundCheckStatus) { els.riderBackgroundCheckStatus.textContent = riderManualBackgroundReviewMode() ? "Application submitted. Eligibility checks show Waka admin review." : "Application submitted. Eligibility checks show admin review and Checkr testing progress."; } if (user?.emailSetupPending) { if (els.riderSessionSummary) els.riderSessionSummary.textContent = emailPendingMessage; } else { setTranslatedStatus(els.riderSessionSummary, "riderCreatedPending", { name: state.rider.name }); } } catch (error) { if (syncedRiderUser?.id) { state.rider = { ...rider, password: undefined, id: syncedRiderUser.id, supabaseUserId: syncedRiderUser.id, profilePhotoPath: syncedRiderUser.profilePhotoPath ?? rider.profilePhotoPath, needsApplication: true, status: "profile only" }; if (syncedRiderUser.emailSetupPending) { state.sessions.rider = null; } else { activateWorkspaceRoleSession("rider", { phone: state.rider.phone, email: state.rider.email, userId: state.rider.supabaseUserId, signedInAt: new Date().toISOString() }); } state.riders = upsertById(state.riders, state.rider); state.accountMode.rider = "signin"; clearPendingProfileRecovery("rider"); state.riderPage = "profile"; saveState(); renderAll(); els.riderStatus.textContent = `The rider application was not submitted for admin review: ${riderApplicationErrorMessage(error)} Review the highlighted details and submit again.`; els.riderStatus?.classList.add("error-status"); } else { if (riderAccountCreationRequiresSignIn(error)) { routeRiderAccountCreationToSignIn(rider, error); return; } setTranslatedStatus(els.riderStatus, "riderAccountFailed", { message: riderApplicationErrorMessage(error) }); els.riderStatus?.classList.add("error-status"); } } finally { setButtonBusy(els.riderSubmitButton, false); } } function riderAccountCreationRequiresSignIn(error) { const message = String(error?.message || error || ""); return /Waka Cameroon created the .*login|email confirmation|email is confirmed|confirmed.*sign in|already has a Supabase login|existing password|Supabase did not accept the sign-in/i.test(message); } function routeRiderAccountCreationToSignIn(rider, error) { const message = String(error?.message || error || ""); state.rider = { ...rider, password: undefined, pendingEmailConfirmation: true, needsApplication: true, status: "profile only" }; state.accountMode.rider = "signin"; state.activeTab = "rider"; state.showRoleEntry = false; setPendingProfileRecovery("rider", { id: rider.supabaseUserId ?? rider.id ?? null, email: rider.email, phone: rider.phone, user_metadata: { full_name: rider.name, phone: rider.phone } }, rider.email); saveState(); renderAll(); if (els.riderSignInEmail) els.riderSignInEmail.value = rider.email; if (els.riderSignInPassword) els.riderSignInPassword.value = ""; const status = els.riderSignInStatus ?? els.riderStatus; const needsEmailConfirmation = /email confirmation|Waka Cameroon created the .*login|confirmed.*sign in|Supabase did not accept the sign-in/i.test(message); if (status) { status.textContent = needsEmailConfirmation ? riderEmailConfirmationGuidance(rider.email) : "This email already has a login. Sign in here with the existing password; Waka will open the rider profile or application form."; status.classList.toggle("success-status", needsEmailConfirmation); status.classList.toggle("error-status", !needsEmailConfirmation); } if (needsEmailConfirmation) { showRiderEmailConfirmationGuidance(rider.email); } } // Runtime render loop, event wiring, install flow, service worker registration, and startup. let serviceWorkerRefreshPending = false; let deploymentUpdateApplying = false; let pendingServiceWorkerRegistration = null; const appCacheName = "waka-negotiated-static-v612"; const deploymentUpdateActiveStatuses = new Set(["open", "matched", "arrived", "in_progress"]); async function clearOldAppCaches() { if (!("caches" in window)) return; try { const keys = await caches.keys(); await Promise.all(keys .filter((key) => key.startsWith("waka-negotiated-static-") && key !== appCacheName) .map((key) => caches.delete(key))); } catch (error) { logClientWarning("Could not clear old app caches.", error); } } function deploymentUpdateCurrentRole() { try { return typeof activeRole === "function" ? activeRole() : state.activeTab; } catch { return state.activeTab; } } function deploymentUpdateRequestMap() { if (typeof stateLookupIndexes === "function") return stateLookupIndexes().requestMap; return new Map((state.requests || []).map((request) => [request.id, request])); } function deploymentUpdateHasOpenRiderOffer() { if (!state.rider?.id || !Array.isArray(state.offers)) return false; const requestMap = deploymentUpdateRequestMap(); return state.offers.some((offer) => { if (offer.riderId !== state.rider.id) return false; if (["withdrawn", "declined", "expired", "accepted"].includes(offer.status)) return false; const request = requestMap.get(offer.requestId); return request?.status === "open"; }); } function deploymentUpdateUserBusy() { const role = deploymentUpdateCurrentRole(); if (!["passenger", "rider"].includes(role)) return false; try { const selected = typeof selectedRequest === "function" ? selectedRequest() : null; const activeRide = typeof activeRideForRole === "function" ? activeRideForRole(selected) : selected; if (activeRide && deploymentUpdateActiveStatuses.has(activeRide.status)) return true; if (role === "passenger") { const ownsOpenRequest = selected?.status === "open" && (typeof requestBelongsToPassenger !== "function" || requestBelongsToPassenger(selected)); if (ownsOpenRequest) return true; } if (role === "rider") { const canSeeOpenRequest = selected?.status === "open" && (typeof roleCanSeeRequest !== "function" || roleCanSeeRequest(selected)); const canActOnSelectedRequest = typeof riderCanShowOfferControls === "function" && riderCanShowOfferControls(undefined, selected || undefined); if (canSeeOpenRequest || canActOnSelectedRequest || deploymentUpdateHasOpenRiderOffer()) return true; } } catch (error) { logClientWarning("Could not determine whether a deployment update can apply immediately.", error); return true; } return false; } function deploymentUpdateCopy(busy = deploymentUpdateUserBusy()) { if (busy) { return { title: "Waka update ready after this step", message: "A verified Waka security and reliability update is ready. Finish this fare decision or ride step, then update." }; } return { title: "Waka update ready", message: "A verified Waka security and reliability update is ready. Updating keeps fares, privacy, and ride screens current." }; } function showDeploymentUpdateNotice(registration = pendingServiceWorkerRegistration) { pendingServiceWorkerRegistration = registration || pendingServiceWorkerRegistration; if (!els.deploymentUpdateNotice) return; const busy = deploymentUpdateUserBusy(); const copy = deploymentUpdateCopy(busy); els.deploymentUpdateNotice.dataset.mode = busy ? "active" : "ready"; if (els.deploymentUpdateTitle) els.deploymentUpdateTitle.textContent = copy.title; if (els.deploymentUpdateMessage) els.deploymentUpdateMessage.textContent = copy.message; if (els.deploymentUpdateNow) { els.deploymentUpdateNow.disabled = false; els.deploymentUpdateNow.textContent = "Update now"; } els.deploymentUpdateNotice.hidden = false; } function hideDeploymentUpdateNotice() { if (els.deploymentUpdateNotice) els.deploymentUpdateNotice.hidden = true; } function applyDeploymentUpdate(registration = pendingServiceWorkerRegistration) { if (deploymentUpdateApplying) return; deploymentUpdateApplying = true; if (els.deploymentUpdateNow) { els.deploymentUpdateNow.disabled = true; els.deploymentUpdateNow.textContent = "Updating..."; } const waitingWorker = registration?.waiting || pendingServiceWorkerRegistration?.waiting; if (waitingWorker) { waitingWorker.postMessage({ type: "SKIP_WAITING" }); window.setTimeout(() => { if (!serviceWorkerRefreshPending) window.location.reload(); }, 4000); return; } window.location.reload(); } function handleDeploymentUpdateAvailable(registration) { pendingServiceWorkerRegistration = registration; if (!registration?.waiting) return; if (deploymentUpdateUserBusy()) { showDeploymentUpdateNotice(registration); return; } applyDeploymentUpdate(registration); } function tryApplyPendingDeploymentUpdate() { if (!pendingServiceWorkerRegistration?.waiting || deploymentUpdateApplying) return; if (deploymentUpdateUserBusy()) { showDeploymentUpdateNotice(pendingServiceWorkerRegistration); return; } applyDeploymentUpdate(pendingServiceWorkerRegistration); } async function refreshServiceWorkerUpdate() { if (!("serviceWorker" in navigator)) return; try { const registration = await navigator.serviceWorker.getRegistration(); if (!registration) return; await registration.update().catch(() => {}); if (registration.waiting) handleDeploymentUpdateAvailable(registration); } catch (error) { logClientWarning("Could not check for a Waka deployment update.", error); } } function renderAll() { applyLanguage(); renderEntryExperience(); renderAccountWorkspaces(); renderRiderFlow(); renderRiderStatus(); renderRoleWorkspace(); if (typeof renderPublicIntercityPlanner === "function") renderPublicIntercityPlanner(); if (typeof renderWorkspaceBackgroundMap === "function") renderWorkspaceBackgroundMap(); renderMap(); renderRequests(); renderOffers(); renderSelectedSummary(); renderChat(); updateConnectionStatus(); ensureAutomaticLocationServices(); } function renderWorkspacePanelSafely(label, renderFn) { try { renderFn(); } catch (error) { logClientWarning(`Admin ${label} panel failed to render.`, error); if (els.adminStatus) { els.adminStatus.textContent = `Admin ${label} could not render: ${compactAdminErrorMessage(error)}`; } } } function ensureAutomaticLocationServices() { if (typeof ensurePassengerPickupGpsAutoCapture === "function") ensurePassengerPickupGpsAutoCapture(); if (typeof ensurePassengerNearbyRiderCountsAutoRefresh === "function") ensurePassengerNearbyRiderCountsAutoRefresh(); if (typeof ensurePassengerApproachAutoRefresh === "function") ensurePassengerApproachAutoRefresh(); if (typeof ensureRiderMarketplaceAutoRefresh === "function") ensureRiderMarketplaceAutoRefresh(); if (typeof ensureAccountNoticeAutoRefreshes === "function") ensureAccountNoticeAutoRefreshes(); if (typeof ensureMarketplaceRealtimeSubscription === "function") ensureMarketplaceRealtimeSubscription(); if (typeof ensureAutomaticRiderGps === "function") ensureAutomaticRiderGps(); if (typeof ensureRiderScreenWakeLock === "function") void ensureRiderScreenWakeLock(); if (typeof ensureRiderActiveRideNavigation === "function") ensureRiderActiveRideNavigation(); } function emptyState(text) { const div = document.createElement("div"); div.className = "empty-state"; div.textContent = text; return div; } function escapeHtml(value) { return String(value ?? "").replace(/[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" })[character]); } function normalizeHttpsUrl(value) { const trimmed = String(value ?? "").trim(); if (!trimmed) return ""; try { const url = new URL(trimmed); return url.protocol === "https:" ? url.href : ""; } catch (_error) { return ""; } } function chip(text) { const value = String(text ?? ""); const fareMovement = /^(?:Rider fare\s+)?[\u2191\u2193]\s/.test(value); const classNames = []; if (fareMovement) { classNames.push("fare-movement-chip"); if (/\u2191\s/.test(value)) classNames.push("fare-movement-up"); if (/\u2193\s/.test(value)) classNames.push("fare-movement-down"); } if (/^Pickup ETA: .+ \/ Destination drive:/i.test(value)) classNames.push("rider-route-distance-chip"); const className = classNames.length ? ` class="${classNames.join(" ")}"` : ""; return `${escapeHtml(value)}`; } function wireOnce(element, key, eventName, handler) { if (!element || element.dataset?.[key] === "true") return; element.addEventListener(eventName, handler); if (element.dataset) element.dataset[key] = "true"; } function wireAccountAuthEvents() { document.querySelectorAll("[data-account-type][data-account-mode]").forEach((button) => { wireOnce(button, "wakaAccountModeWired", "click", () => setAccountMode(button.dataset.accountType, button.dataset.accountMode)); }); document.querySelectorAll("[data-account-create-link]").forEach((link) => { wireOnce(link, "wakaAccountCreateRouteWired", "click", (event) => { event.preventDefault(); setAccountMode(link.dataset.accountCreateLink, "create"); }); }); document.querySelectorAll("[data-account-signin-link]").forEach((link) => { wireOnce(link, "wakaAccountSigninRouteWired", "click", (event) => { event.preventDefault(); setAccountMode(link.dataset.accountSigninLink, "signin"); }); }); wireOnce(els.passengerSignInForm, "wakaSignInSubmitWired", "submit", (event) => { event.preventDefault(); verifySignIn("passenger"); }); wireOnce(els.sendPassengerSignInCode, "wakaSignInCodeWired", "click", () => sendSignInCode("passenger")); wireOnce(els.verifyPassengerSignIn, "wakaSignInVerifyWired", "click", () => verifySignIn("passenger")); wireOnce(els.forgotPassengerPassword, "wakaPasswordResetRequestWired", "click", () => requestPasswordReset("passenger")); wireOnce(els.sendPassengerPasswordResetPhoneOtp, "wakaPasswordResetPhoneOtpSendWired", "click", () => sendPasswordResetPhoneOtp("passenger")); wireOnce(els.verifyPassengerPasswordResetPhoneOtp, "wakaPasswordResetPhoneOtpVerifyWired", "click", () => verifyPasswordResetPhoneOtp("passenger")); wireOnce(els.savePassengerResetPassword, "wakaPasswordResetSaveWired", "click", () => completePasswordReset("passenger")); wireOnce(els.passengerSignOut, "wakaSignOutWired", "click", () => signOutRole("passenger")); wireOnce(els.passengerMenuSignOut, "wakaSignOutWired", "click", () => signOutRole("passenger")); wireOnce(els.passengerPasswordChangeForm, "wakaPasswordChangeWired", "submit", (event) => { event.preventDefault(); changeSignedInPassword("passenger"); }); wireOnce(els.riderSignInForm, "wakaSignInSubmitWired", "submit", (event) => { event.preventDefault(); verifySignIn("rider"); }); wireOnce(els.sendRiderSignInCode, "wakaSignInCodeWired", "click", () => sendSignInCode("rider")); wireOnce(els.verifyRiderSignIn, "wakaSignInVerifyWired", "click", () => verifySignIn("rider")); wireOnce(els.forgotRiderPassword, "wakaPasswordResetRequestWired", "click", () => requestPasswordReset("rider")); wireOnce(els.sendRiderPasswordResetPhoneOtp, "wakaPasswordResetPhoneOtpSendWired", "click", () => sendPasswordResetPhoneOtp("rider")); wireOnce(els.verifyRiderPasswordResetPhoneOtp, "wakaPasswordResetPhoneOtpVerifyWired", "click", () => verifyPasswordResetPhoneOtp("rider")); wireOnce(els.saveRiderResetPassword, "wakaPasswordResetSaveWired", "click", () => completePasswordReset("rider")); wireOnce(els.riderSignOut, "wakaSignOutWired", "click", () => signOutRole("rider")); wireOnce(els.riderMenuSignOutTop, "wakaSignOutWired", "click", () => signOutRole("rider")); wireOnce(els.riderMenuSignOut, "wakaSignOutWired", "click", () => signOutRole("rider")); wireOnce(els.riderPasswordChangeForm, "wakaPasswordChangeWired", "submit", (event) => { event.preventDefault(); changeSignedInPassword("rider"); }); wireOnce(els.agencyPasswordChangeForm, "wakaPasswordChangeWired", "submit", (event) => { event.preventDefault(); changeSignedInPassword("agency"); }); } function wireDeploymentUpdateEvents() { wireOnce(els.deploymentUpdateNow, "wakaDeploymentUpdateWired", "click", () => applyDeploymentUpdate()); window.addEventListener("online", () => { updateConnectionStatus(); void refreshServiceWorkerUpdate(); }); window.addEventListener("focus", tryApplyPendingDeploymentUpdate); document.addEventListener("visibilitychange", () => { if (!document.hidden) tryApplyPendingDeploymentUpdate(); }); } let riderNavigationPreferenceSyncChain = Promise.resolve(); function updateRiderNavigationPreferenceFromProfile() { const preference = normalizeRiderNavigationPreference(els.riderNavigationPreference?.value); syncRiderNavigationPreferenceInput(preference, { userSelected: true }); const riderId = state.rider?.id ?? state.rider?.supabaseUserId ?? state.sessions?.rider?.userId; rememberRiderNavigationPreferenceOverride(preference, riderId); if (state.rider) { const riderIds = state.riderNavigationPreferenceOverride?.riderIds?.length ? state.riderNavigationPreferenceOverride.riderIds : [riderId, ...currentRiderIdentityAliases()].filter(Boolean); const withNavigationPreference = (rider) => { const documents = { ...riderDocuments(rider), vehicleDesignation: normalizeRiderVehicleDesignation(rider.vehicleDesignation, rider.carBodyType), navigationPreference: preference }; return { ...rider, navigationPreference: preference, documents, documentName: riderDocumentPayload(documents) }; }; state.rider = withNavigationPreference(state.rider); const documents = state.rider.documents; state.riders = upsertById( (state.riders ?? []).map((rider) => ( riderRecordMatchesIdentityAliases(rider, riderIds) ? withNavigationPreference(rider) : rider )), state.rider ); if (riderId) { const syncRiderIds = [...riderIds]; const syncDocuments = { ...documents }; riderNavigationPreferenceSyncChain = riderNavigationPreferenceSyncChain .catch(() => {}) .then(() => { const latestPreference = riderNavigationPreferenceOverrideForRider(syncRiderIds) ?? riderNavigationPreference(); if (latestPreference !== preference) return null; return updateRiderApplicationDocumentsInSupabase(riderId, syncDocuments); }) .catch((error) => logClientWarning("Rider navigation preference could not be synced.", error)); } } saveState(); renderAll(); syncRiderNavigationPreferenceInput(preference, { userSelected: true }); } function wireEvents() { wireAccountAuthEvents(); document.querySelectorAll(".tab-button").forEach((button) => { button.addEventListener("click", () => switchTab(button.dataset.tab)); }); document.querySelectorAll("[data-entry-role]").forEach((button) => { button.addEventListener("click", () => switchTab(button.dataset.entryRole, { resetAccountMode: true })); }); document.querySelectorAll("[data-passenger-page]").forEach((button) => { button.addEventListener("click", () => setPassengerWorkspacePage(button.dataset.passengerPage)); }); document.querySelectorAll(".filter-button").forEach((button) => { button.addEventListener("click", () => { state.filter = button.dataset.filter; document.querySelectorAll(".filter-button").forEach((item) => item.classList.toggle("active", item === button)); saveState(); renderAll(); }); }); wireDateOfBirthInput(els.passengerDob); wireYearMonthInput(els.riderDob); els.passengerCountry.addEventListener("change", updatePassengerCityOptions); els.passengerCity.addEventListener("change", updatePickupOptions); els.pickupCity?.addEventListener("change", updatePickupOptions); els.pickupArea.addEventListener("change", () => { updateFareGuidance(); schedulePassengerNearbyRiderCountsRefresh(); }); els.destinationArea.addEventListener("change", updateFareGuidance); els.rideStops.addEventListener("input", updateFareGuidance); els.vehiclePreference.addEventListener("change", () => { clearLowFareReview(); updateFareGuidance(); schedulePassengerNearbyRiderCountsRefresh(); renderAll(); }); els.passengerActiveCountry.addEventListener("change", updatePassengerActiveCityOptions); const handleLanguageChange = (event) => { state.language = event.currentTarget.value; saveState(); els.languageSelects?.forEach((select) => { select.value = state.language; }); if (els.languageSelect) els.languageSelect.value = state.language; renderAll(); }; els.languageSelect?.addEventListener("change", handleLanguageChange); els.languageSelects?.forEach((select) => { select.addEventListener("change", handleLanguageChange); }); els.sendPassengerCode.addEventListener("click", () => sendVerificationCode("passenger")); els.verifyPassengerPhone.addEventListener("click", () => verifyPhone("passenger")); els.passengerEnablePush?.addEventListener("click", () => enableAccountPushNotifications("passenger")); els.destination.addEventListener("input", handleDestinationInput); els.destination.addEventListener("keyup", scheduleDestinationAutocomplete); els.destination.addEventListener("focus", scheduleDestinationAutocomplete); els.destination.addEventListener("blur", () => window.setTimeout(hideDestinationSuggestions, 150)); els.pickupDescription.addEventListener("input", handlePickupInput); els.pickupDescription.addEventListener("keyup", schedulePickupAutocomplete); els.pickupDescription.addEventListener("focus", schedulePickupAutocomplete); els.pickupDescription.addEventListener("blur", () => window.setTimeout(hidePickupSuggestions, 150)); els.rideRequestForm.addEventListener("focusin", ensurePassengerPickupGpsAutoCapture); els.pickupUseCurrentLocation?.addEventListener("change", handlePickupUseCurrentLocationChange); els.useCurrentPickup?.addEventListener("click", activateUseCurrentPickup); els.useCurrentPickup?.addEventListener("pointerup", activateUseCurrentPickup); document.addEventListener("pointerdown", closeWorkspaceMenusFromOutside); document.addEventListener("click", (event) => { if (event.target?.closest?.("#useCurrentPickup")) activateUseCurrentPickup(event); closeWorkspaceMenusFromOutside(event); }); document.addEventListener("keydown", (event) => { if (!["Enter", " "].includes(event.key)) return; if (event.target?.closest?.("#useCurrentPickup")) activateUseCurrentPickup(event); }); els.useCurrentPickup?.addEventListener("click", activateUseCurrentPickup); els.useCurrentPickup?.addEventListener("pointerup", activateUseCurrentPickup); document.addEventListener("click", (event) => { if (event.target?.closest?.("#useCurrentPickup")) activateUseCurrentPickup(event); }); document.addEventListener("keydown", (event) => { if (!["Enter", " "].includes(event.key)) return; if (event.target?.closest?.("#useCurrentPickup")) activateUseCurrentPickup(event); }); els.rideStops.addEventListener("input", handleRideStopsInput); els.rideStops.addEventListener("focus", scheduleRideStopAutocomplete); els.rideStops.addEventListener("click", scheduleRideStopAutocomplete); els.rideStops.addEventListener("keyup", scheduleRideStopAutocomplete); els.rideStops.addEventListener("blur", () => window.setTimeout(hideRideStopSuggestions, 150)); ["pointerup", "touchend", "click"].forEach((eventName) => { els.addRideStop?.addEventListener(eventName, activateRideStopTool); }); els.addRideStop?.addEventListener("keydown", activateRideStopToolFromKey); els.clearRideStops?.addEventListener("click", clearRideStopsInput); [ [els.toggleRideTiming, "timing"], [els.toggleVehiclePreference, "vehicle"], [els.toggleFareDetails, "fare"] ].forEach(([button, optionName]) => { ["pointerup", "touchend", "click"].forEach((eventName) => { button?.addEventListener(eventName, (event) => handleRideRequestOptionToggle(optionName, event)); }); button?.addEventListener("keydown", (event) => handleRideRequestOptionKeyToggle(optionName, event)); }); document.querySelectorAll("[data-passenger-fare-mode]").forEach((button) => { ["pointerup", "touchend", "click"].forEach((eventName) => { button.addEventListener(eventName, handlePassengerFareModeButtonActivation); }); button.addEventListener("keydown", handlePassengerFareModeButtonKeyActivation); }); els.passengerFareMode?.addEventListener("input", handlePassengerFareModeSelection); els.passengerFareMode?.addEventListener("change", handlePassengerFareModeSelection); updatePassengerFareModeControls(); initializeRideRequestOptionPanels(); els.fareOffer.addEventListener("input", clearLowFareReview); els.rideTiming?.addEventListener("change", handleRideTimingChange); updateScheduledRideControls(); els.sendRiderCode.addEventListener("click", () => sendVerificationCode("rider")); els.verifyRiderPhone.addEventListener("click", () => verifyPhone("rider")); els.riderEnablePush?.addEventListener("click", () => enableAccountPushNotifications("rider")); els.riderCountry.addEventListener("change", updateRiderCityOptions); els.riderCity.addEventListener("change", updateRiderAreas); els.riderCarMake.addEventListener("change", () => populateSelect(els.riderCarModel, carMakeCatalog[els.riderCarMake.value] ?? carMakeCatalog.Other, carMakeCatalog[els.riderCarMake.value]?.[0])); els.riderCarBodyType.addEventListener("change", () => { updateRiderVehicleDesignationForBodyType(); saveState(); }); els.riderVehicleDesignation?.addEventListener("change", () => { if (state.rider) state.rider.vehicleDesignation = normalizeRiderVehicleDesignation(els.riderVehicleDesignation.value, els.riderCarBodyType.value); saveState(); }); els.riderActiveCountry.addEventListener("change", updateRiderActiveCityOptions); els.riderActiveCity.addEventListener("change", updateRiderActiveAreas); els.passengerAccountForm.addEventListener("submit", createPassenger); els.passengerAccountUse?.addEventListener("change", updatePassengerInitialBusinessFields); els.passengerPaymentForm.addEventListener("submit", startPassengerPaymentSetup); els.startPassengerPaymentSetup?.addEventListener("click", startPassengerPaymentSetup); els.passengerLocationForm.addEventListener("submit", updatePassengerActiveLocation); els.passengerSupportForm?.addEventListener("submit", submitAccountSupportTicket); if (els.businessAccountForm) els.businessAccountForm.addEventListener("submit", createBusinessAccount); els.capturePickupGps?.addEventListener("click", capturePassengerPickupGps); els.clearPickupGps?.addEventListener("click", clearPassengerPickupGps); els.rideRequestForm.addEventListener("submit", createRideRequest); els.riderAccountForm.addEventListener("submit", createRider); els.riderPaymentForm.addEventListener("submit", (event) => event.preventDefault()); els.startRiderStripePayoutSetup?.addEventListener("click", startRiderStripePayoutSetup); els.riderLocationForm.addEventListener("submit", updateRiderActiveLocation); els.riderSupportForm?.addEventListener("submit", submitAccountSupportTicket); els.riderDestinationScope?.addEventListener("change", () => { state.riderDestinationScope = els.riderDestinationScope.value === "all" ? "all" : "preferred"; saveState(); renderAll(); void refreshMarketplace({ silent: true }); }); els.riderDestinationFilterCountry?.addEventListener("change", refreshRiderDestinationFilterOptions); els.riderDestinationFilterCity?.addEventListener("change", refreshRiderDestinationFilterOptions); els.riderDestinationFilterArea?.addEventListener("change", rememberRiderDestinationFilterControlDraft); els.riderDestinationFilterConsent?.addEventListener("change", rememberRiderDestinationFilterControlDraft); els.riderDestinationFilterText?.addEventListener("input", rememberRiderDestinationFilterControlDraft); els.riderDestinationFilterText?.addEventListener("change", rememberRiderDestinationFilterControlDraft); els.openRiderDestinationFilter?.addEventListener("click", () => setRiderWorkspacePage("destination")); els.riderDestinationFilterApply?.addEventListener("click", applyRiderDestinationFilter); els.riderDestinationFilterClear?.addEventListener("click", clearRiderDestinationFilter); els.riderDestinationFilterText?.addEventListener("keydown", (event) => { if (event.key !== "Enter") return; event.preventDefault(); applyRiderDestinationFilter(); }); els.captureRiderGps.addEventListener("click", captureRiderLiveGps); els.clearRiderGps.addEventListener("click", clearRiderLiveGps); els.startRiderTaxOnboarding?.addEventListener("click", startRiderTaxOnboarding); els.paySubscription.addEventListener("click", paySubscription); els.offerForm.addEventListener("submit", sendOffer); els.acceptFare.addEventListener("click", acceptPassengerFare); const declineRiderRequestFromButton = (event) => { event?.preventDefault?.(); event?.stopPropagation?.(); const sourceButton = event?.target?.closest?.("#dropRiderNegotiation") || event?.currentTarget; const requestId = sourceButton?.dataset?.requestId || selectedRequest()?.id || state.selectedRequestId; if (requestId && typeof ignoreRiderMarketplaceRequest === "function") { void ignoreRiderMarketplaceRequest(requestId); return; } void dropRiderNegotiation(); }; const returnRiderMarketplaceFromButton = (event) => { event?.preventDefault?.(); event?.stopPropagation?.(); if (typeof returnRiderToMarketplace === "function") { returnRiderToMarketplace({ replace: true, refresh: true }); } }; els.dropRiderNegotiation?.addEventListener("click", declineRiderRequestFromButton); els.riderMarketplaceBack?.addEventListener("click", returnRiderMarketplaceFromButton); document.addEventListener("click", (event) => { if (!event.target?.closest?.("#dropRiderNegotiation")) return; declineRiderRequestFromButton(event); }, true); document.addEventListener("click", (event) => { if (!event.target?.closest?.("#riderMarketplaceBack, [data-rider-marketplace-back]")) return; returnRiderMarketplaceFromButton(event); }, true); els.refreshMarket.addEventListener("click", () => refreshMarketplace()); els.chatForm.addEventListener("submit", sendChat); els.safetyReportForm.addEventListener("submit", submitSafetyReport); els.rideRatingForm.addEventListener("submit", submitRideRating); els.installApp.addEventListener("click", installApp); wireDeploymentUpdateEvents(); window.addEventListener("online", updateConnectionStatus); window.addEventListener("offline", updateConnectionStatus); window.addEventListener("hashchange", applyRouteTab); window.addEventListener("popstate", applyRouteTab); window.addEventListener("beforeinstallprompt", (event) => { event.preventDefault(); deferredInstallPrompt = event; updateInstallButton(); }); window.addEventListener("appinstalled", () => { deferredInstallPrompt = null; updateInstallButton(); }); } function closeWorkspaceMenus() { document.querySelectorAll(".workspace-menu-wrap[open]").forEach((menu) => { menu.removeAttribute("open"); }); } function closeWorkspaceMenusFromOutside(event) { if (!event.target?.closest?.(".workspace-menu-wrap")) closeWorkspaceMenus(); } async function installApp() { if (deferredInstallPrompt) { deferredInstallPrompt.prompt(); await deferredInstallPrompt.userChoice; deferredInstallPrompt = null; updateInstallButton(); return; } translatedAlert("androidInstallHelp"); } async function registerServiceWorker() { if (!("serviceWorker" in navigator)) return; try { await clearOldAppCaches(); navigator.serviceWorker.addEventListener("controllerchange", () => { if (serviceWorkerRefreshPending) return; serviceWorkerRefreshPending = true; window.location.reload(); }); const registration = await navigator.serviceWorker.register("sw.js", { updateViaCache: "none" }); registration.addEventListener("updatefound", () => { const installingWorker = registration.installing; if (!installingWorker) return; installingWorker.addEventListener("statechange", () => { if (installingWorker.state === "installed" && navigator.serviceWorker.controller) { handleDeploymentUpdateAvailable(registration); } }); }); await registration.update().catch(() => {}); if (registration.waiting && navigator.serviceWorker.controller) handleDeploymentUpdateAvailable(registration); } catch { if (appConfig.mode === "supabase") { updateConnectionStatus(); } else { setTranslatedStatus(els.connectionStatus, "localMode"); } } } async function finishSupabaseStartup() { await initSupabaseClient(); if (typeof restoreSignedInRoleFromSupabaseSession === "function") { await restoreSignedInRoleFromSupabaseSession(); } if (typeof handlePaymentSetupReturnFromLocation === "function") { await handlePaymentSetupReturnFromLocation(); } updateConnectionStatus(); renderAll(); } async function boot() { installClientRuntimeErrorReporting(); await loadRuntimeConfig(); hardenStateForRuntime(); if (typeof clearStalePasswordResetModeForCurrentLocation === "function") { clearStalePasswordResetModeForCurrentLocation(); } const passwordResetBootRole = typeof preparePasswordResetReturnFromLocation === "function" ? preparePasswordResetReturnFromLocation() : ""; const bootingPasswordReset = Boolean(passwordResetBootRole); const requestedTab = requestedTabFromLocation(); state.activeTab = passwordResetBootRole || availableWorkspaceTab(requestedTab ?? preferredSignedInTab() ?? state.activeTab) || defaultRuntimeTab(); if (state.activeTab === "admin" && typeof applyRequestedAdminWorkspacePageFromLocation === "function") { applyRequestedAdminWorkspacePageFromLocation(); } wireAccountAuthEvents(); populateLocationFields(); hydrateForms(); if (typeof initializeRideStopsInput === "function" && document.querySelector("#rideStops")) { initializeRideStopsInput(); } const showEntryOnBoot = bootingPasswordReset ? false : shouldShowRoleEntry(); switchTab(state.activeTab, { updateUrl: !showEntryOnBoot && !bootingPasswordReset, preserveEntry: showEntryOnBoot, resetAccountMode: Boolean(requestedTab) && !bootingPasswordReset }); updateConnectionStatus(); updateInstallButton(); wireEvents(); renderAll(); if (appConfig.mode === "supabase") { void finishSupabaseStartup().catch((error) => { els.connectionStatus.textContent = error.message; }); } registerServiceWorker(); } void boot().finally(() => { window.WAKA_RUNTIME_READY = true; });