1(function initFindApiClient(root, factory) { 2 const api = factory(root); 3 if (typeof module === "object" && module.exports) { 4 module.exports = api; 5 return; 6 } 7 const namespace = root.CrashCountFindCore = root.CrashCountFindCore || {}; 8 Object.assign(namespace, api); 9})(typeof globalThis !== "undefined" ? globalThis : this, function factory(root) { 10 function createApiClient(deps) { 11 const config = deps.config || {}; 12 const state = deps.state; 13 const fetchImpl = deps.fetchImpl || root.fetch; 14 const warn = deps.warn || function noop() {}; 15 16 function cacheKey(prefix, id, version) { 17 return `${prefix}:${String(version || "latest")}:${String(id || "").trim()}`; 18 } 19 20 async function fetchHex(hexId) { 21 if (state.hexCache.has(hexId)) return state.hexCache.get(hexId); 22 const version = deps.resolvedDataVersion(); 23 const intId = deps.h3ToIntString(hexId); 24 const url = `${config.apiBase}/hex/${deps.dataRes}/${intId}?version=${encodeURIComponent(version)}`; 25 const res = await fetchImpl(url); 26 if (res.status === 404) { 27 state.hexCache.set(hexId, null); 28 return null; 29 } 30 if (!res.ok) throw new Error(`hex ${res.status}`); 31 const data = await res.json(); 32 const item = data.hex || null; 33 state.hexCache.set(hexId, item); 34 return item; 35 } 36 37 async function fetchHexBatch(hexIds, options) { 38 if (!hexIds.length) return []; 39 const version = deps.resolvedDataVersion(); 40 const out = []; 41 const chunks = []; 42 const onChunk = options && typeof options.onChunk === "function" ? options.onChunk : null; 43 for (let i = 0; i < hexIds.length; i += deps.hexBatchChunkSize) { 44 chunks.push(hexIds.slice(i, i + deps.hexBatchChunkSize)); 45 } 46 47 const fetchChunk = async function fetchChunk(chunk) { 48 const map = new Map(); 49 const payload = { 50 res: deps.dataRes, 51 h3: chunk.map(function mapCell(cell) { 52 const intId = deps.h3ToIntString(cell); 53 map.set(intId, cell); 54 return intId; 55 }), 56 version: version 57 }; 58 try { 59 const res = await fetchImpl(`${config.apiBase}/hex/batch?version=${encodeURIComponent(version)}`, { 60 method: "POST", 61 headers: { "content-type": "application/json" }, 62 body: JSON.stringify(payload) 63 }); 64 if (!res.ok) throw new Error(`hex batch ${res.status}`); 65 const data = await res.json(); 66 const items = data.hexes || []; 67 items.forEach(function cacheItem(item) { 68 const key = map.get(String(item.h3)); 69 if (key) state.hexCache.set(key, item); 70 }); 71 const missing = Array.isArray(data.missing) ? data.missing : []; 72 missing.forEach(function cacheMissing(id) { 73 const key = map.get(String(id)); 74 if (key && !state.hexCache.has(key)) state.hexCache.set(key, null); 75 }); 76 if (onChunk) onChunk(items); 77 return items; 78 } catch (err) { 79 warn("[hex-find] hex batch", err); 80 return []; 81 } 82 }; 83 84 let index = 0; 85 const workerCount = Math.min(deps.hexBatchConcurrency, chunks.length); 86 const workers = Array.from({ length: workerCount }, async function worker() { 87 while (index < chunks.length) { 88 const chunk = chunks[index]; 89 index += 1; 90 const items = await fetchChunk(chunk); 91 if (items.length) out.push.apply(out, items); 92 } 93 }); 94 await Promise.all(workers); 95 return out; 96 } 97 98 async function fetchStreetSuggestions(query, options) { 99 if (!config.apiBase) return []; 100 const versionOverride = options && options.version ? String(options.version).trim() : ""; 101 const version = deps.resolvedDataVersion(versionOverride); 102 const url = `${config.apiBase}/search?q=${encodeURIComponent(query)}&version=${encodeURIComponent(version)}`; 103 const res = await fetchImpl(url); 104 if (!res.ok) throw new Error(`street search ${res.status}`); 105 const data = await res.json(); 106 const streets = Array.isArray(data.streets) ? data.streets : []; 107 return streets.map(function mapStreet(street) { 108 return { 109 type: "Feature", 110 geometry: Array.isArray(street.center) ? { type: "Point", coordinates: street.center } : null, 111 properties: { 112 layer: "street", 113 label: street.label || street.name || "Street", 114 street_id: street.street_id, 115 borough: street.borough 116 } 117 }; 118 }); 119 } 120 121 async function fetchStreetMeta(streetId, options) {
122 const allowFallback = !options || options.allowFallback !== false; 123 const versionOverride = options && options.version ? String(options.version).trim() : ""; 124 const version = deps.resolvedDataVersion(versionOverride); 125 const key = cacheKey("street", streetId, version); 126 if (state.streetCache.has(key)) return state.streetCache.get(key); 127 const url = `${config.apiBase}/street/${encodeURIComponent(streetId)}?version=${encodeURIComponent(version)}`; 128 const res = await fetchImpl(url); 129 if (res.ok) { 130 const data = await res.json(); 131 const item = data.street || null; 132 state.streetCache.set(key, item); 133 return item; 134 } 135 if (allowFallback && res.status === 404 && streetId.endsWith("|all")) { 136 const fallbackId = streetId.replace(/\|all$/, "|0"); 137 if (fallbackId !== streetId) { 138 const fallback = await fetchStreetMeta(fallbackId, { allowFallback: false, version: version }); 139 state.streetCache.set(key, fallback); 140 return fallback; 141 } 142 } 143 throw new Error(`street ${res.status}`); 144 } 145 146 async function resolveStreetMeta(streetId, label, options) { 147 const resolvedOptions = options && typeof options === "object" ? options : {}; 148 const item = await fetchStreetMeta(streetId, resolvedOptions); 149 return { streetId: streetId, item: item }; 150 } 151 152 async function fetchBikeLaneSuggestions(query, options) { 153 if (!config.apiBase) return []; 154 const versionOverride = options && options.version ? String(options.version).trim() : ""; 155 const version = deps.resolvedDataVersion(versionOverride); 156 const url = `${config.apiBase}/bike-lanes/search?q=${encodeURIComponent(query)}&version=${encodeURIComponent(version)}`; 157 const res = await fetchImpl(url); 158 if (!res.ok) throw new Error(`bike lane search ${res.status}`); 159 const data = await res.json(); 160 const bikeLanes = Array.isArray(data.bike_lanes) ? data.bike_lanes : []; 161 return bikeLanes.map(function mapBikeLane(lane) { 162 return { 163 type: "Feature", 164 geometry: Array.isArray(lane.center) ? { type: "Point", coordinates: lane.center } : null, 165 properties: { 166 layer: "bike_lane", 167 label: lane.label || "Bike lane", 168 bike_lane_id: lane.bike_lane_id, 169 borough: lane.borough, 170 facility_group: lane.facility_group || "" 171 } 172 }; 173 }); 174 } 175 176 async function fetchBikeLaneMeta(bikeLaneId, options) { 177 const allowFallback = !options || options.allowFallback !== false; 178 const versionOverride = options && options.version ? String(options.version).trim() : ""; 179 const version = deps.resolvedDataVersion(versionOverride); 180 const key = cacheKey("bike_lane", bikeLaneId, version); 181 if (state.bikeLaneCache.has(key)) return state.bikeLaneCache.get(key); 182 const url = `${config.apiBase}/bike-lane/${encodeURIComponent(bikeLaneId)}?version=${encodeURIComponent(version)}`; 183 const res = await fetchImpl(url); 184 if (res.ok) { 185 const data = await res.json(); 186 const item = data.bike_lane || null; 187 state.bikeLaneCache.set(key, item); 188 return item; 189 } 190 if (allowFallback && res.status === 404 && bikeLaneId.endsWith("|all")) { 191 const fallbackId = bikeLaneId.replace(/\|all$/, "|0"); 192 if (fallbackId !== bikeLaneId) { 193 const fallback = await fetchBikeLaneMeta(fallbackId, { allowFallback: false, version: version }); 194 state.bikeLaneCache.set(key, fallback); 195 return fallback; 196 } 197 } 198 throw new Error(`bike lane ${res.status}`); 199 } 200 201 async function resolveBikeLaneMeta(bikeLaneId, label, options) { 202 const resolvedOptions = options && typeof options === "object" ? options : {}; 203 const item = await fetchBikeLaneMeta(bikeLaneId, resolvedOptions); 204 return { bikeLaneId: bikeLaneId, item: item }; 205 } 206 207 async function fetchCollision(collisionId) { 208 if (state.collisionCache.has(collisionId)) return state.collisionCache.get(collisionId); 209 const version = deps.resolvedDataVersion(); 210 const url = `${config.apiBase}/crash/${collisionId}?version=${encodeURIComponent(version)}`; 211 const res = await fetchImpl(url); 212 if (res.status === 404) { 213 if (version) { 214 try { 215 const fallbackRes = await fetchImpl(`${config.apiBase}/crash/${collisionId}`); 216 if (fallbackRes.ok) { 217 const fallbackData = await fallbackRes.json(); 218 const fallbackItem = fallbackData.collision || null; 219 state.collisionCache.set(collisionId, fallbackItem); 220 return fallbackItem; 221 } 222 } catch (err) { 223 warn("[hex-find] collision fallback failed", err); 224 } 225 } 226 state.collisionCache.set(collisionId, null); 227 return null; 228 } 229 if (!res.ok) throw new Error(`collision ${res.status}`); 230 const data = await res.json(); 231 const item = data.collision || null; 232 state.collisionCache.set(collisionId, item); 233 return item; 234 } 235 236 return { 237 fetchBikeLaneMeta: fetchBikeLaneMeta, 238 fetchBikeLaneSuggestions: fetchBikeLaneSuggestions, 239 fetchCollision: fetchCollision, 240 fetchHex: fetchHex, 241 fetchHexBatch: fetchHexBatch, 242 fetchStreetMeta: fetchStreetMeta, 243 fetchStreetSuggestions: fetchStreetSuggestions, 244 resolveBikeLaneMeta: resolveBikeLaneMeta, 245 resolveStreetMeta: resolveStreetMeta 246 }; 247 } 248 249 return { 250 createApiClient: createApiClient 251 }; 252});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.