clean up repo, remove stadia integration
13 files changed,  +106, -2997
D client/lib/es6/src/ffi/Stadia.res.mjs
+0, -574
  1@@ -1,574 +0,0 @@
  2-// Generated by ReScript, PLEASE EDIT WITH CARE
  3-
  4-import * as Fetch from "./Fetch.res.mjs";
  5-import * as Core__JSON from "@rescript/core/lib/es6/src/Core__JSON.res.mjs";
  6-import * as Core__Array from "@rescript/core/lib/es6/src/Core__Array.res.mjs";
  7-import * as Core__Option from "@rescript/core/lib/es6/src/Core__Option.res.mjs";
  8-import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
  9-
 10-function $$encodeURIComponent(str) {
 11-  return encodeURIComponent(str);
 12-}
 13-
 14-function getApiKey() {
 15-  let key = import.meta.env.VITE_STADIA_KEY;
 16-  if (key === undefined) {
 17-    return;
 18-  }
 19-  let s = Core__JSON.Decode.string(Primitive_option.valFromOption(key));
 20-  if (s !== undefined && s.length > 0) {
 21-    return s;
 22-  }
 23-}
 24-
 25-function layerToString(layer) {
 26-  if (layer === "county") {
 27-    return "county";
 28-  } else if (layer === "macroregion") {
 29-    return "macroregion";
 30-  } else if (layer === "postalcode") {
 31-    return "postalcode";
 32-  } else if (layer === "locality") {
 33-    return "locality";
 34-  } else if (layer === "neighbourhood") {
 35-    return "neighbourhood";
 36-  } else if (layer === "poi") {
 37-    return "poi";
 38-  } else if (layer === "region") {
 39-    return "region";
 40-  } else if (layer === "street") {
 41-    return "street";
 42-  } else if (layer === "macrocounty") {
 43-    return "macrocounty";
 44-  } else if (layer === "address") {
 45-    return "address";
 46-  } else if (layer === "country") {
 47-    return "country";
 48-  } else if (layer === "borough") {
 49-    return "borough";
 50-  } else if (layer === "coarse") {
 51-    return "coarse";
 52-  } else {
 53-    return "localadmin";
 54-  }
 55-}
 56-
 57-function parseLayer(str) {
 58-  switch (str) {
 59-    case "address" :
 60-      return "address";
 61-    case "borough" :
 62-      return "borough";
 63-    case "coarse" :
 64-      return "coarse";
 65-    case "country" :
 66-      return "country";
 67-    case "county" :
 68-      return "county";
 69-    case "localadmin" :
 70-      return "localadmin";
 71-    case "locality" :
 72-      return "locality";
 73-    case "macrocounty" :
 74-      return "macrocounty";
 75-    case "macroregion" :
 76-      return "macroregion";
 77-    case "neighbourhood" :
 78-      return "neighbourhood";
 79-    case "poi" :
 80-      return "poi";
 81-    case "postalcode" :
 82-      return "postalcode";
 83-    case "region" :
 84-      return "region";
 85-    case "street" :
 86-      return "street";
 87-    default:
 88-      return;
 89-  }
 90-}
 91-
 92-function parseBaseProperties(props) {
 93-  let n = props["name"];
 94-  let name;
 95-  if (n !== undefined) {
 96-    let s = Core__JSON.Decode.string(n);
 97-    name = s !== undefined ? s : "";
 98-  } else {
 99-    name = "";
100-  }
101-  let g = props["gid"];
102-  let gid;
103-  if (g !== undefined) {
104-    let s$1 = Core__JSON.Decode.string(g);
105-    gid = s$1 !== undefined ? s$1 : "";
106-  } else {
107-    gid = "";
108-  }
109-  let l = props["layer"];
110-  let layer;
111-  if (l !== undefined) {
112-    let s$2 = Core__JSON.Decode.string(l);
113-    layer = s$2 !== undefined ? parseLayer(s$2) : undefined;
114-  } else {
115-    layer = undefined;
116-  }
117-  let c = props["coarse_location"];
118-  let coarseLocation;
119-  if (c !== undefined) {
120-    let s$3 = Core__JSON.Decode.string(c);
121-    coarseLocation = s$3 !== undefined ? s$3 : "";
122-  } else {
123-    coarseLocation = "";
124-  }
125-  return [
126-    name,
127-    gid,
128-    layer,
129-    coarseLocation
130-  ];
131-}
132-
133-function checkGeocodingError(obj) {
134-  let g = obj["geocoding"];
135-  if (g === undefined) {
136-    return;
137-  }
138-  let geoObj = Core__JSON.Decode.object(g);
139-  if (geoObj === undefined) {
140-    return;
141-  }
142-  let e = geoObj["error"];
143-  if (e !== undefined) {
144-    return Core__JSON.Decode.string(e);
145-  }
146-}
147-
148-function handleStadiaResponse(json, parseFeature, wrapResults, emptyResponse, serverError) {
149-  let obj = Core__JSON.Decode.object(json);
150-  if (obj === undefined) {
151-    return serverError("Invalid JSON response");
152-  }
153-  let errMsg = checkGeocodingError(obj);
154-  if (errMsg !== undefined) {
155-    return serverError(errMsg);
156-  }
157-  let f = obj["features"];
158-  let features;
159-  if (f !== undefined) {
160-    let arr = Core__JSON.Decode.array(f);
161-    features = arr !== undefined ? arr : [];
162-  } else {
163-    features = [];
164-  }
165-  let results = Core__Array.filterMap(features, parseFeature);
166-  if (results.length === 0) {
167-    return emptyResponse;
168-  } else {
169-    return wrapResults(results);
170-  }
171-}
172-
173-function buildQueryString(apiKey, params) {
174-  let parts = [];
175-  parts.push(`text=` + $$encodeURIComponent(params.text));
176-  let match = params.focusPoint;
177-  if (match !== undefined) {
178-    parts.push(`focus.point.lat=` + match[0].toString());
179-    parts.push(`focus.point.lon=` + match[1].toString());
180-  }
181-  let match$1 = params.boundaryRect;
182-  if (match$1 !== undefined) {
183-    parts.push(`boundary.rect.min_lon=` + match$1[0].toString());
184-    parts.push(`boundary.rect.max_lon=` + match$1[1].toString());
185-    parts.push(`boundary.rect.min_lat=` + match$1[2].toString());
186-    parts.push(`boundary.rect.max_lat=` + match$1[3].toString());
187-  }
188-  let match$2 = params.boundaryCircle;
189-  if (match$2 !== undefined) {
190-    parts.push(`boundary.circle.lat=` + match$2[0].toString());
191-    parts.push(`boundary.circle.lon=` + match$2[1].toString());
192-    parts.push(`boundary.circle.radius=` + match$2[2].toString());
193-  }
194-  let layers = params.layers;
195-  if (layers !== undefined && layers.length > 0) {
196-    let layerStr = layers.map(layerToString).join(",");
197-    parts.push(`layers=` + $$encodeURIComponent(layerStr));
198-  }
199-  let sources = params.sources;
200-  if (sources !== undefined && sources.length > 0) {
201-    let sourceStr = sources.join(",");
202-    parts.push(`sources=` + $$encodeURIComponent(sourceStr));
203-  }
204-  let countries = params.boundaryCountry;
205-  if (countries !== undefined && countries.length > 0) {
206-    let countryStr = countries.join(",");
207-    parts.push(`boundary.country=` + $$encodeURIComponent(countryStr));
208-  }
209-  let gid = params.boundaryGid;
210-  if (gid !== undefined) {
211-    parts.push(`boundary.gid=` + $$encodeURIComponent(gid));
212-  }
213-  let size = params.size;
214-  if (size !== undefined) {
215-    parts.push(`size=` + size.toString());
216-  }
217-  let lang = params.lang;
218-  if (lang !== undefined) {
219-    parts.push(`lang=` + $$encodeURIComponent(lang));
220-  }
221-  if (apiKey !== undefined) {
222-    parts.push(`api_key=` + $$encodeURIComponent(apiKey));
223-  }
224-  return parts.join("&");
225-}
226-
227-function parseFeature(feature) {
228-  let obj = Core__JSON.Decode.object(feature);
229-  if (obj === undefined) {
230-    return;
231-  }
232-  let p = obj["properties"];
233-  let properties = p !== undefined ? Core__JSON.Decode.object(p) : undefined;
234-  if (properties === undefined) {
235-    return;
236-  }
237-  let match = parseBaseProperties(properties);
238-  let layer = match[2];
239-  let d = properties["distance"];
240-  let distance;
241-  if (d !== undefined) {
242-    let n = Core__JSON.Decode.float(d);
243-    distance = n !== undefined ? n : undefined;
244-  } else {
245-    distance = undefined;
246-  }
247-  if (layer !== undefined) {
248-    return {
249-      name: match[0],
250-      gid: match[1],
251-      layer: layer,
252-      coarseLocation: match[3],
253-      distance: distance
254-    };
255-  }
256-}
257-
258-async function search(params) {
259-  let apiKey = getApiKey();
260-  let queryString = buildQueryString(apiKey, params);
261-  let url = `https://api.stadiamaps.com/geocoding/v2/autocomplete?` + queryString;
262-  try {
263-    let response = await Fetch.send(Fetch.onResponse(Fetch.get(url), json => json, (status, message) => Object.fromEntries([
264-      [
265-        "__error",
266-        true
267-      ],
268-      [
269-        "__status",
270-        status
271-      ],
272-      [
273-        "__message",
274-        message
275-      ]
276-    ])));
277-    let obj = Core__JSON.Decode.object(response);
278-    if (obj === undefined) {
279-      return {
280-        TAG: "ServerError",
281-        _0: "Invalid JSON response"
282-      };
283-    }
284-    let match = obj["__error"];
285-    if (match === undefined) {
286-      return handleStadiaResponse(response, parseFeature, results => ({
287-        TAG: "Results",
288-        _0: results
289-      }), "Empty", msg => ({
290-        TAG: "ServerError",
291-        _0: msg
292-      }));
293-    }
294-    let s = obj["__status"];
295-    let status;
296-    if (s !== undefined) {
297-      let n = Core__JSON.Decode.float(s);
298-      status = n !== undefined ? (n | 0).toString() : "0";
299-    } else {
300-      status = "0";
301-    }
302-    let m = obj["__message"];
303-    let message;
304-    if (m !== undefined) {
305-      let s$1 = Core__JSON.Decode.string(m);
306-      message = s$1 !== undefined ? s$1 : "Unknown error";
307-    } else {
308-      message = "Unknown error";
309-    }
310-    return {
311-      TAG: "ServerError",
312-      _0: status + `: ` + message
313-    };
314-  } catch (exn) {
315-    return {
316-      TAG: "ServerError",
317-      _0: "Network error"
318-    };
319-  }
320-}
321-
322-function buildQueryString$1(apiKey, params) {
323-  let parts = [];
324-  parts.push(`ids=` + $$encodeURIComponent(params.gid));
325-  let lang = params.lang;
326-  if (lang !== undefined) {
327-    parts.push(`lang=` + $$encodeURIComponent(lang));
328-  }
329-  if (apiKey !== undefined) {
330-    parts.push(`api_key=` + $$encodeURIComponent(apiKey));
331-  }
332-  return parts.join("&");
333-}
334-
335-function parseFeature$1(feature) {
336-  let obj = Core__JSON.Decode.object(feature);
337-  if (obj === undefined) {
338-    return;
339-  }
340-  let p = obj["properties"];
341-  let properties = p !== undefined ? Core__JSON.Decode.object(p) : undefined;
342-  let g = obj["geometry"];
343-  let geometry = g !== undefined ? Core__JSON.Decode.object(g) : undefined;
344-  if (properties === undefined) {
345-    return;
346-  }
347-  if (geometry === undefined) {
348-    return;
349-  }
350-  let match = parseBaseProperties(properties);
351-  let layer = match[2];
352-  let c = geometry["coordinates"];
353-  let coordinates;
354-  if (c !== undefined) {
355-    let arr = Core__JSON.Decode.array(c);
356-    if (arr !== undefined && arr.length >= 2) {
357-      let lonJson = arr[0];
358-      let lon;
359-      if (lonJson !== undefined) {
360-        let f = Core__JSON.Decode.float(lonJson);
361-        lon = f !== undefined ? f : 0.0;
362-      } else {
363-        lon = 0.0;
364-      }
365-      let latJson = arr[1];
366-      let lat;
367-      if (latJson !== undefined) {
368-        let f$1 = Core__JSON.Decode.float(latJson);
369-        lat = f$1 !== undefined ? f$1 : 0.0;
370-      } else {
371-        lat = 0.0;
372-      }
373-      coordinates = [
374-        lon,
375-        lat
376-      ];
377-    } else {
378-      coordinates = undefined;
379-    }
380-  } else {
381-    coordinates = undefined;
382-  }
383-  let v = properties["formatted_address_line"];
384-  let formattedAddressLine = v !== undefined ? Core__JSON.Decode.string(v) : undefined;
385-  let v$1 = properties["formatted_address_lines"];
386-  let formattedAddressLines;
387-  if (v$1 !== undefined) {
388-    let arr$1 = Core__JSON.Decode.array(v$1);
389-    formattedAddressLines = arr$1 !== undefined ? Core__Array.filterMap(arr$1, item => Core__JSON.Decode.string(item)) : undefined;
390-  } else {
391-    formattedAddressLines = undefined;
392-  }
393-  let ac = properties["address_components"];
394-  let addressComponents;
395-  if (ac !== undefined) {
396-    let obj$1 = Core__JSON.Decode.object(ac);
397-    if (obj$1 !== undefined) {
398-      let number = Core__Option.flatMap(obj$1["number"], Core__JSON.Decode.string);
399-      let street = Core__Option.flatMap(obj$1["street"], Core__JSON.Decode.string);
400-      let postal_code = Core__Option.flatMap(obj$1["postal_code"], Core__JSON.Decode.string);
401-      addressComponents = {
402-        number: number,
403-        street: street,
404-        postal_code: postal_code
405-      };
406-    } else {
407-      addressComponents = undefined;
408-    }
409-  } else {
410-    addressComponents = undefined;
411-  }
412-  let ctx = properties["context"];
413-  let context;
414-  if (ctx !== undefined) {
415-    let ctxObj = Core__JSON.Decode.object(ctx);
416-    if (ctxObj !== undefined) {
417-      let parseEntity = key => {
418-        let e = ctxObj[key];
419-        if (e === undefined) {
420-          return;
421-        }
422-        let eObj = Core__JSON.Decode.object(e);
423-        if (eObj === undefined) {
424-          return;
425-        }
426-        let gidOpt = Core__Option.flatMap(eObj["gid"], Core__JSON.Decode.string);
427-        let nameOpt = Core__Option.flatMap(eObj["name"], Core__JSON.Decode.string);
428-        if (gidOpt !== undefined && nameOpt !== undefined) {
429-          return {
430-            gid: gidOpt,
431-            name: nameOpt
432-          };
433-        }
434-      };
435-      let country = parseEntity("country");
436-      let region = parseEntity("region");
437-      let county = parseEntity("county");
438-      let locality = parseEntity("locality");
439-      let neighbourhood = parseEntity("neighbourhood");
440-      let whosonfirst;
441-      let exit = 0;
442-      if (country !== undefined || region !== undefined || county !== undefined || locality !== undefined || neighbourhood !== undefined) {
443-        exit = 1;
444-      } else {
445-        whosonfirst = undefined;
446-      }
447-      if (exit === 1) {
448-        whosonfirst = {
449-          country: country,
450-          region: region,
451-          county: county,
452-          locality: locality,
453-          neighbourhood: neighbourhood
454-        };
455-      }
456-      let iso_3166_a2 = Core__Option.flatMap(ctxObj["iso_3166_a2"], Core__JSON.Decode.string);
457-      let iso_3166_a3 = Core__Option.flatMap(ctxObj["iso_3166_a3"], Core__JSON.Decode.string);
458-      let exit$1 = 0;
459-      if (whosonfirst !== undefined || iso_3166_a2 !== undefined || iso_3166_a3 !== undefined) {
460-        exit$1 = 1;
461-      } else {
462-        context = undefined;
463-      }
464-      if (exit$1 === 1) {
465-        context = {
466-          whosonfirst: whosonfirst,
467-          iso_3166_a2: iso_3166_a2,
468-          iso_3166_a3: iso_3166_a3
469-        };
470-      }
471-    } else {
472-      context = undefined;
473-    }
474-  } else {
475-    context = undefined;
476-  }
477-  if (layer !== undefined && coordinates !== undefined) {
478-    return {
479-      name: match[0],
480-      gid: match[1],
481-      layer: layer,
482-      coarseLocation: match[3],
483-      coordinates: coordinates,
484-      formattedAddressLine: formattedAddressLine,
485-      formattedAddressLines: formattedAddressLines,
486-      addressComponents: addressComponents,
487-      context: context
488-    };
489-  }
490-}
491-
492-async function getDetails(params) {
493-  let apiKey = getApiKey();
494-  let queryString = buildQueryString$1(apiKey, params);
495-  let url = `https://api.stadiamaps.com/geocoding/v2/place_details?` + queryString;
496-  try {
497-    let response = await Fetch.send(Fetch.onResponse(Fetch.get(url), json => json, (status, message) => Object.fromEntries([
498-      [
499-        "__error",
500-        true
501-      ],
502-      [
503-        "__status",
504-        status
505-      ],
506-      [
507-        "__message",
508-        message
509-      ]
510-    ])));
511-    let obj = Core__JSON.Decode.object(response);
512-    if (obj === undefined) {
513-      return {
514-        TAG: "ServerError",
515-        _0: "Invalid JSON response"
516-      };
517-    }
518-    let match = obj["__error"];
519-    if (match === undefined) {
520-      return handleStadiaResponse(response, parseFeature$1, results => {
521-        let result = results[0];
522-        if (result !== undefined) {
523-          return {
524-            TAG: "Found",
525-            _0: result
526-          };
527-        } else {
528-          return "NotFound";
529-        }
530-      }, "NotFound", msg => ({
531-        TAG: "ServerError",
532-        _0: msg
533-      }));
534-    }
535-    let s = obj["__status"];
536-    let status;
537-    if (s !== undefined) {
538-      let n = Core__JSON.Decode.float(s);
539-      status = n !== undefined ? (n | 0).toString() : "0";
540-    } else {
541-      status = "0";
542-    }
543-    let m = obj["__message"];
544-    let message;
545-    if (m !== undefined) {
546-      let s$1 = Core__JSON.Decode.string(m);
547-      message = s$1 !== undefined ? s$1 : "Unknown error";
548-    } else {
549-      message = "Unknown error";
550-    }
551-    return {
552-      TAG: "ServerError",
553-      _0: status + `: ` + message
554-    };
555-  } catch (exn) {
556-    return {
557-      TAG: "ServerError",
558-      _0: "Network error"
559-    };
560-  }
561-}
562-
563-let Autocomplete = {
564-  search: search
565-};
566-
567-let Place = {
568-  getDetails: getDetails
569-};
570-
571-export {
572-  Autocomplete,
573-  Place,
574-}
575-/* Fetch Not a pure module */
M client/lib/es6/src/ffi/Waldo.res.mjs
+2, -9
 1@@ -1,5 +1,6 @@
 2 // Generated by ReScript, PLEASE EDIT WITH CARE
 3 
 4+import * as Log from "../utils/Log.res.mjs";
 5 import * as Fetch from "./Fetch.res.mjs";
 6 import * as Core__JSON from "@rescript/core/lib/es6/src/Core__JSON.res.mjs";
 7 import * as Core__Array from "@rescript/core/lib/es6/src/Core__Array.res.mjs";
 8@@ -74,11 +75,9 @@ function decodeResult(json) {
 9 function decodeGeocodeResponse(json) {
10   let dict = Core__JSON.Decode.object(json);
11   if (dict !== undefined) {
12-    console.log("[Waldo] decodeGeocodeResponse: Got JSON object, extracting fields...");
13     let results = Core__Option.map(Core__Option.flatMap(dict["results"], Core__JSON.Decode.array), arr => Core__Array.filterMap(arr, decodeResult));
14     let numResults = Core__Option.map(Core__Option.flatMap(dict["num_results"], Core__JSON.Decode.float), f => f | 0);
15     if (results !== undefined && numResults !== undefined) {
16-      console.log(`[Waldo] decodeGeocodeResponse: Success with ` + numResults.toString() + ` results`);
17       return {
18         results: results,
19         numResults: numResults
20@@ -124,7 +123,6 @@ function buildUrl(params) {
21 async function geocode(params) {
22   let url = buildUrl(params);
23   try {
24-    console.log("[Waldo] Forward: Starting fetch request...");
25     let response = await Fetch.send(Fetch.onResponse(Fetch.get(url), json => json, (status, message) => Object.fromEntries([
26       [
27         "__error",
28@@ -139,17 +137,15 @@ async function geocode(params) {
29         message
30       ]
31     ])));
32-    console.log("[Waldo] Forward: Raw response received");
33     let responseStr;
34     try {
35       responseStr = JSON.stringify(response);
36     } catch (exn) {
37       responseStr = "[Failed to stringify response]";
38     }
39-    console.log(`[Waldo] Forward: Response content: ` + responseStr);
40+    Log.debug(`[Waldo] Forward: Response content: ` + responseStr, undefined);
41     let obj = Core__JSON.Decode.object(response);
42     if (obj !== undefined) {
43-      console.log("[Waldo] Forward: Response is JSON object");
44       let match = obj["__error"];
45       if (match !== undefined) {
46         let s = obj["__status"];
47@@ -174,11 +170,8 @@ async function geocode(params) {
48           _0: status + `: ` + message
49         };
50       }
51-      console.log("[Waldo] Forward: No __error field found, attempting to decode geocode response...");
52-      console.log(`[Waldo] Forward: Response object keys: ` + Object.keys(obj).join(", "));
53       let resp = decodeGeocodeResponse(response);
54       if (resp !== undefined) {
55-        console.log(`[Waldo] Forward: Decoded ` + resp.numResults.toString() + ` results`);
56         return {
57           TAG: "Success",
58           _0: resp.results
A client/lib/es6/src/ui/EditWaypoint.res.mjs
+2, -0
1@@ -0,0 +1,2 @@
2+// Generated by ReScript, PLEASE EDIT WITH CARE
3+/* This output is empty. Its source's type definitions, externals and/or unused code got optimized away. */
M client/lib/es6/src/ui/Editor.res.mjs
+1, -6
 1@@ -6,7 +6,6 @@ import * as NanoID from "../ffi/NanoID.res.mjs";
 2 import * as Preact from "../ffi/Preact.res.mjs";
 3 import * as Tiptap from "../ffi/Tiptap.res.mjs";
 4 import * as PreactDOM from "../ffi/PreactDOM.res.mjs";
 5-import * as ContentJs from "./content.js";
 6 import * as Core from "@tiptap/core";
 7 import * as Core__Option from "@rescript/core/lib/es6/src/Core__Option.res.mjs";
 8 import * as React from "@tiptap/react";
 9@@ -15,8 +14,6 @@ import * as Signals from "@preact/signals";
10 import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
11 import * as JsxRuntime from "preact/jsx-runtime";
12 
13-let content = ContentJs.content;
14-
15 let FloatingUI = {};
16 
17 let Instance = {};
18@@ -52,7 +49,6 @@ function Editor(props) {
19         }
20       },
21       extensions: Tiptap.extensions(ydoc, doc),
22-      content: content,
23       onUpdate: _event => {
24         let ed = editorRef.contents;
25         if (ed === undefined) {
26@@ -187,7 +183,6 @@ let FloatingMenu = {};
27 let make = Editor;
28 
29 export {
30-  content,
31   FloatingUI,
32   Instance,
33   onSearchSelectCache,
34@@ -196,4 +191,4 @@ export {
35   BubbleMenu,
36   FloatingMenu,
37 }
38-/* content Not a pure module */
39+/* onSearchSelectCache Not a pure module */
D client/lib/es6/src/ui/content.js
+0, -19
 1@@ -1,19 +0,0 @@
 2-export const content = {
 3-  type: "doc",
 4-  content: [
 5-    {
 6-      type: "heading",
 7-      attrs: { level: 1 },
 8-      content: [{ type: "text", text: "Start with a title..." }],
 9-    },
10-    {
11-      type: "paragraph",
12-      content: [
13-        {
14-          type: "text",
15-          text: "To add a waypoint type @ or click on the menu button at the start of an empty line...",
16-        },
17-      ],
18-    },
19-  ],
20-};
D client/lib/es6/test/Stadia_test.res.mjs
+0, -902
  1@@ -1,902 +0,0 @@
  2-// Generated by ReScript, PLEASE EDIT WITH CARE
  3-
  4-import * as Stadia from "../src/ffi/Stadia.res.mjs";
  5-import * as Vitest from "vitest";
  6-import * as Core__Option from "@rescript/core/lib/es6/src/Core__Option.res.mjs";
  7-
  8-let mockFetchJson = (function(json, status, statusText) {
  9-    globalThis.fetch = vi.fn(() => Promise.resolve({
 10-      ok: status >= 200 && status < 300,
 11-      status: status,
 12-      statusText: statusText,
 13-      json: () => Promise.resolve(json)
 14-    }));
 15-  });
 16-
 17-let setupMockWithCapture = (function() {
 18-    const calls = [];
 19-    globalThis.fetch = vi.fn((url, init) => {
 20-      calls.push({url, init});
 21-      return Promise.resolve({
 22-        ok: true,
 23-        status: 200,
 24-        statusText: 'OK',
 25-        json: () => Promise.resolve({
 26-          type: 'FeatureCollection',
 27-          features: [],
 28-          geocoding: { attribution: 'test' }
 29-        })
 30-      });
 31-    });
 32-    return calls;
 33-  });
 34-
 35-let getUrl = (function(calls) {
 36-    return calls[0].url;
 37-  });
 38-
 39-Vitest.describe("Stadia.Autocomplete", () => {
 40-  Vitest.test("returns Results with parsed data for successful search", async t => {
 41-    let responseJson = Object.fromEntries([
 42-      [
 43-        "type",
 44-        "FeatureCollection"
 45-      ],
 46-      [
 47-        "geocoding",
 48-        Object.fromEntries([
 49-          [
 50-            "attribution",
 51-            "https://stadiamaps.com/attribution/"
 52-          ],
 53-          [
 54-            "query",
 55-            Object.fromEntries([[
 56-                "text",
 57-                "Põhja pst 27"
 58-              ]])
 59-          ]
 60-        ])
 61-      ],
 62-      [
 63-        "features",
 64-        [
 65-          Object.fromEntries([
 66-            [
 67-              "type",
 68-              "Feature"
 69-            ],
 70-            [
 71-              "geometry",
 72-              null
 73-            ],
 74-            [
 75-              "properties",
 76-              Object.fromEntries([
 77-                [
 78-                  "name",
 79-                  "Põhja Puiestee 27"
 80-                ],
 81-                [
 82-                  "gid",
 83-                  "openaddresses:address:ee/countrywide-addresses-country:5cbc00647aa5c976"
 84-                ],
 85-                [
 86-                  "layer",
 87-                  "address"
 88-                ],
 89-                [
 90-                  "coarse_location",
 91-                  "Tallinn, Harju, Estonia"
 92-                ],
 93-                [
 94-                  "precision",
 95-                  "point"
 96-                ]
 97-              ])
 98-            ]
 99-          ]),
100-          Object.fromEntries([
101-            [
102-              "type",
103-              "Feature"
104-            ],
105-            [
106-              "geometry",
107-              null
108-            ],
109-            [
110-              "properties",
111-              Object.fromEntries([
112-                [
113-                  "name",
114-                  "Põhja pst 27"
115-                ],
116-                [
117-                  "gid",
118-                  "openstreetmap:address:way/224094258"
119-                ],
120-                [
121-                  "layer",
122-                  "address"
123-                ],
124-                [
125-                  "coarse_location",
126-                  "Viljandi, Estonia"
127-                ],
128-                [
129-                  "precision",
130-                  "centroid"
131-                ]
132-              ])
133-            ]
134-          ])
135-        ]
136-      ]
137-    ]);
138-    mockFetchJson(responseJson, 200, "OK");
139-    let result = await Stadia.Autocomplete.search({
140-      text: "Põhja pst 27"
141-    });
142-    if (typeof result !== "object") {
143-      t.expect(true).toEqual(false);
144-      return;
145-    }
146-    if (result.TAG === "Results") {
147-      let results = result._0;
148-      t.expect(results.length).toEqual(2);
149-      let first = results[0];
150-      if (first !== undefined) {
151-        t.expect(first.name).toEqual("Põhja Puiestee 27");
152-        t.expect(first.gid).toEqual("openaddresses:address:ee/countrywide-addresses-country:5cbc00647aa5c976");
153-        t.expect(first.layer).toEqual("address");
154-        t.expect(first.coarseLocation).toEqual("Tallinn, Harju, Estonia");
155-        t.expect(first.distance).toEqual(undefined);
156-      } else {
157-        t.expect(true).toEqual(false);
158-      }
159-      let second = results[1];
160-      if (second !== undefined) {
161-        t.expect(second.name).toEqual("Põhja pst 27");
162-        t.expect(second.layer).toEqual("address");
163-      } else {
164-        t.expect(true).toEqual(false);
165-      }
166-      return;
167-    }
168-    t.expect(true).toEqual(false);
169-  });
170-  Vitest.test("returns Results with distance when provided", async t => {
171-    let responseJson = Object.fromEntries([
172-      [
173-        "type",
174-        "FeatureCollection"
175-      ],
176-      [
177-        "geocoding",
178-        Object.fromEntries([[
179-            "attribution",
180-            "test"
181-          ]])
182-      ],
183-      [
184-        "features",
185-        [Object.fromEntries([
186-            [
187-              "type",
188-              "Feature"
189-            ],
190-            [
191-              "geometry",
192-              null
193-            ],
194-            [
195-              "properties",
196-              Object.fromEntries([
197-                [
198-                  "name",
199-                  "Test Location"
200-                ],
201-                [
202-                  "gid",
203-                  "test:123"
204-                ],
205-                [
206-                  "layer",
207-                  "address"
208-                ],
209-                [
210-                  "coarse_location",
211-                  "City, Country"
212-                ],
213-                [
214-                  "distance",
215-                  1.5
216-                ]
217-              ])
218-            ]
219-          ])]
220-      ]
221-    ]);
222-    mockFetchJson(responseJson, 200, "OK");
223-    let result = await Stadia.Autocomplete.search({
224-      text: "test",
225-      focusPoint: [
226-        59.437,
227-        24.753
228-      ]
229-    });
230-    if (typeof result !== "object") {
231-      t.expect(true).toEqual(false);
232-      return;
233-    }
234-    if (result.TAG === "Results") {
235-      let results = result._0;
236-      t.expect(results.length).toEqual(1);
237-      let first = results[0];
238-      if (first !== undefined) {
239-        t.expect(first.distance).toEqual(1.5);
240-      } else {
241-        t.expect(true).toEqual(false);
242-      }
243-      return;
244-    }
245-    t.expect(true).toEqual(false);
246-  });
247-  Vitest.test("returns Empty for empty features array", async t => {
248-    let responseJson = Object.fromEntries([
249-      [
250-        "type",
251-        "FeatureCollection"
252-      ],
253-      [
254-        "geocoding",
255-        Object.fromEntries([[
256-            "attribution",
257-            "test"
258-          ]])
259-      ],
260-      [
261-        "features",
262-        []
263-      ]
264-    ]);
265-    mockFetchJson(responseJson, 200, "OK");
266-    let result = await Stadia.Autocomplete.search({
267-      text: "xyznonexistent12345"
268-    });
269-    t.expect(result).toEqual("Empty");
270-  });
271-  Vitest.test("returns ServerError on HTTP error", async t => {
272-    let errorJson = Object.fromEntries([[
273-        "error",
274-        "Invalid API key"
275-      ]]);
276-    mockFetchJson(errorJson, 401, "Unauthorized");
277-    let result = await Stadia.Autocomplete.search({
278-      text: "test"
279-    });
280-    if (typeof result !== "object") {
281-      t.expect(true).toEqual(false);
282-      return;
283-    }
284-    if (result.TAG === "Results") {
285-      t.expect(true).toEqual(false);
286-      return;
287-    }
288-    t.expect(result._0).toEqual("401: Invalid API key");
289-  });
290-  Vitest.test("returns ServerError on geocoding error", async t => {
291-    let responseJson = Object.fromEntries([
292-      [
293-        "type",
294-        "FeatureCollection"
295-      ],
296-      [
297-        "geocoding",
298-        Object.fromEntries([
299-          [
300-            "attribution",
301-            "test"
302-          ],
303-          [
304-            "error",
305-            "Invalid parameter: text is required"
306-          ]
307-        ])
308-      ],
309-      [
310-        "features",
311-        []
312-      ]
313-    ]);
314-    mockFetchJson(responseJson, 200, "OK");
315-    let result = await Stadia.Autocomplete.search({
316-      text: "test"
317-    });
318-    if (typeof result !== "object") {
319-      t.expect(true).toEqual(false);
320-      return;
321-    }
322-    if (result.TAG === "Results") {
323-      t.expect(true).toEqual(false);
324-      return;
325-    }
326-    t.expect(result._0).toEqual("Invalid parameter: text is required");
327-  });
328-  Vitest.test("encodes text parameter in query string", async t => {
329-    let _calls = setupMockWithCapture();
330-    await Stadia.Autocomplete.search({
331-      text: "Põhja pst 27"
332-    });
333-    let url = getUrl(_calls);
334-    t.expect(url.includes("text=P%C3%B5hja%20pst%2027")).toEqual(true);
335-  });
336-  Vitest.test("encodes focus point parameters", async t => {
337-    let _calls = setupMockWithCapture();
338-    await Stadia.Autocomplete.search({
339-      text: "test",
340-      focusPoint: [
341-        59.437,
342-        24.753
343-      ]
344-    });
345-    let url = getUrl(_calls);
346-    t.expect(url.includes("focus.point.lat=59.437")).toEqual(true);
347-    t.expect(url.includes("focus.point.lon=24.753")).toEqual(true);
348-  });
349-  Vitest.test("encodes boundary rect parameters", async t => {
350-    let _calls = setupMockWithCapture();
351-    await Stadia.Autocomplete.search({
352-      text: "test",
353-      boundaryRect: [
354-        139.2794,
355-        140.1471,
356-        35.53308,
357-        35.81346
358-      ]
359-    });
360-    let url = getUrl(_calls);
361-    t.expect(url.includes("boundary.rect.min_lon=139.2794")).toEqual(true);
362-    t.expect(url.includes("boundary.rect.max_lon=140.1471")).toEqual(true);
363-    t.expect(url.includes("boundary.rect.min_lat=35.53308")).toEqual(true);
364-    t.expect(url.includes("boundary.rect.max_lat=35.81346")).toEqual(true);
365-  });
366-  Vitest.test("encodes boundary circle parameters", async t => {
367-    let _calls = setupMockWithCapture();
368-    await Stadia.Autocomplete.search({
369-      text: "test",
370-      boundaryCircle: [
371-        43.818156,
372-        -79.186484,
373-        35.0
374-      ]
375-    });
376-    let url = getUrl(_calls);
377-    t.expect(url.includes("boundary.circle.lat=43.818156")).toEqual(true);
378-    t.expect(url.includes("boundary.circle.lon=-79.186484")).toEqual(true);
379-    t.expect(url.includes("boundary.circle.radius=35")).toEqual(true);
380-  });
381-  Vitest.test("encodes layers as comma-separated", async t => {
382-    let _calls = setupMockWithCapture();
383-    await Stadia.Autocomplete.search({
384-      text: "test",
385-      layers: [
386-        "address",
387-        "poi",
388-        "coarse"
389-      ]
390-    });
391-    let url = getUrl(_calls);
392-    t.expect(url.includes("layers=address%2Cpoi%2Ccoarse") || url.includes("layers=address,poi,coarse")).toEqual(true);
393-  });
394-  Vitest.test("encodes sources as comma-separated", async t => {
395-    let _calls = setupMockWithCapture();
396-    await Stadia.Autocomplete.search({
397-      text: "test",
398-      sources: [
399-        "openstreetmap",
400-        "openaddresses"
401-      ]
402-    });
403-    let url = getUrl(_calls);
404-    t.expect(url.includes("sources=openstreetmap%2Copenaddresses") || url.includes("sources=openstreetmap,openaddresses")).toEqual(true);
405-  });
406-  Vitest.test("encodes boundary country as comma-separated", async t => {
407-    let _calls = setupMockWithCapture();
408-    await Stadia.Autocomplete.search({
409-      text: "test",
410-      boundaryCountry: [
411-        "GBR",
412-        "FRA"
413-      ]
414-    });
415-    let url = getUrl(_calls);
416-    t.expect(url.includes("boundary.country=GBR%2CFRA") || url.includes("boundary.country=GBR,FRA")).toEqual(true);
417-  });
418-  Vitest.test("encodes boundary gid", async t => {
419-    let _calls = setupMockWithCapture();
420-    await Stadia.Autocomplete.search({
421-      text: "test",
422-      boundaryGid: "whosonfirst:locality:101748355"
423-    });
424-    let url = getUrl(_calls);
425-    t.expect(url.includes("boundary.gid=whosonfirst%3Alocality%3A101748355")).toEqual(true);
426-  });
427-  Vitest.test("encodes size parameter", async t => {
428-    let _calls = setupMockWithCapture();
429-    await Stadia.Autocomplete.search({
430-      text: "test",
431-      size: 5
432-    });
433-    let url = getUrl(_calls);
434-    t.expect(url.includes("size=5")).toEqual(true);
435-  });
436-  Vitest.test("encodes lang parameter", async t => {
437-    let _calls = setupMockWithCapture();
438-    await Stadia.Autocomplete.search({
439-      text: "test",
440-      lang: "en"
441-    });
442-    let url = getUrl(_calls);
443-    t.expect(url.includes("lang=en")).toEqual(true);
444-  });
445-  Vitest.test("includes api_key in URL when env var is present", async t => {
446-    let _calls = setupMockWithCapture();
447-    await Stadia.Autocomplete.search({
448-      text: "test"
449-    });
450-    let url = getUrl(_calls);
451-    t.expect(url.length > 0).toEqual(true);
452-    t.expect(url.includes("https://api.stadiamaps.com/geocoding/v2/autocomplete")).toEqual(true);
453-  });
454-  Vitest.test("handles all layer types correctly", async t => {
455-    let testCases = [
456-      [
457-        "poi",
458-        "poi"
459-      ],
460-      [
461-        "address",
462-        "address"
463-      ],
464-      [
465-        "street",
466-        "street"
467-      ],
468-      [
469-        "country",
470-        "country"
471-      ],
472-      [
473-        "macroregion",
474-        "macroregion"
475-      ],
476-      [
477-        "region",
478-        "region"
479-      ],
480-      [
481-        "macrocounty",
482-        "macrocounty"
483-      ],
484-      [
485-        "county",
486-        "county"
487-      ],
488-      [
489-        "locality",
490-        "locality"
491-      ],
492-      [
493-        "localadmin",
494-        "localadmin"
495-      ],
496-      [
497-        "borough",
498-        "borough"
499-      ],
500-      [
501-        "neighbourhood",
502-        "neighbourhood"
503-      ],
504-      [
505-        "postalcode",
506-        "postalcode"
507-      ],
508-      [
509-        "coarse",
510-        "coarse"
511-      ]
512-    ];
513-    let features = testCases.map((param, idx) => Object.fromEntries([
514-      [
515-        "type",
516-        "Feature"
517-      ],
518-      [
519-        "geometry",
520-        null
521-      ],
522-      [
523-        "properties",
524-        Object.fromEntries([
525-          [
526-            "name",
527-            `Test ` + idx.toString()
528-          ],
529-          [
530-            "gid",
531-            `test:` + idx.toString()
532-          ],
533-          [
534-            "layer",
535-            param[0]
536-          ],
537-          [
538-            "coarse_location",
539-            "Test City"
540-          ]
541-        ])
542-      ]
543-    ]));
544-    let responseJson = Object.fromEntries([
545-      [
546-        "type",
547-        "FeatureCollection"
548-      ],
549-      [
550-        "geocoding",
551-        Object.fromEntries([[
552-            "attribution",
553-            "test"
554-          ]])
555-      ],
556-      [
557-        "features",
558-        features
559-      ]
560-    ]);
561-    mockFetchJson(responseJson, 200, "OK");
562-    let result = await Stadia.Autocomplete.search({
563-      text: "test"
564-    });
565-    if (typeof result !== "object") {
566-      t.expect(true).toEqual(false);
567-      return;
568-    }
569-    if (result.TAG === "Results") {
570-      let results = result._0;
571-      t.expect(results.length).toEqual(testCases.length);
572-      testCases.forEach((param, idx) => {
573-        t.expect(Core__Option.map(results[idx], r => r.layer)).toEqual(param[1]);
574-      });
575-      return;
576-    }
577-    t.expect(true).toEqual(false);
578-  });
579-});
580-
581-Vitest.describe("Stadia.Place", () => {
582-  Vitest.test("returns Found with coordinates for valid gid", async t => {
583-    let responseJson = Object.fromEntries([
584-      [
585-        "type",
586-        "FeatureCollection"
587-      ],
588-      [
589-        "geocoding",
590-        Object.fromEntries([[
591-            "attribution",
592-            "https://stadiamaps.com/attribution/"
593-          ]])
594-      ],
595-      [
596-        "features",
597-        [Object.fromEntries([
598-            [
599-              "type",
600-              "Feature"
601-            ],
602-            [
603-              "geometry",
604-              Object.fromEntries([
605-                [
606-                  "type",
607-                  "Point"
608-                ],
609-                [
610-                  "coordinates",
611-                  [
612-                    24.753,
613-                    59.437
614-                  ]
615-                ]
616-              ])
617-            ],
618-            [
619-              "properties",
620-              Object.fromEntries([
621-                [
622-                  "name",
623-                  "Põhja Puiestee 27"
624-                ],
625-                [
626-                  "gid",
627-                  "openaddresses:address:ee/countrywide-addresses-country:5cbc00647aa5c976"
628-                ],
629-                [
630-                  "layer",
631-                  "address"
632-                ],
633-                [
634-                  "coarse_location",
635-                  "Tallinn, Harju, Estonia"
636-                ]
637-              ])
638-            ]
639-          ])]
640-      ]
641-    ]);
642-    mockFetchJson(responseJson, 200, "OK");
643-    let result = await Stadia.Place.getDetails({
644-      gid: "openaddresses:address:ee/countrywide-addresses-country:5cbc00647aa5c976"
645-    });
646-    if (typeof result !== "object") {
647-      t.expect(true).toEqual(false);
648-      return;
649-    }
650-    if (result.TAG === "Found") {
651-      let place = result._0;
652-      t.expect(place.name).toEqual("Põhja Puiestee 27");
653-      t.expect(place.gid).toEqual("openaddresses:address:ee/countrywide-addresses-country:5cbc00647aa5c976");
654-      t.expect(place.layer).toEqual("address");
655-      t.expect(place.coarseLocation).toEqual("Tallinn, Harju, Estonia");
656-      let match = place.coordinates;
657-      t.expect(match[0]).toEqual(24.753);
658-      t.expect(match[1]).toEqual(59.437);
659-      return;
660-    }
661-    t.expect(true).toEqual(false);
662-  });
663-  Vitest.test("returns NotFound for empty features", async t => {
664-    let responseJson = Object.fromEntries([
665-      [
666-        "type",
667-        "FeatureCollection"
668-      ],
669-      [
670-        "geocoding",
671-        Object.fromEntries([[
672-            "attribution",
673-            "test"
674-          ]])
675-      ],
676-      [
677-        "features",
678-        []
679-      ]
680-    ]);
681-    mockFetchJson(responseJson, 200, "OK");
682-    let result = await Stadia.Place.getDetails({
683-      gid: "nonexistent:123"
684-    });
685-    t.expect(result).toEqual("NotFound");
686-  });
687-  Vitest.test("returns ServerError on HTTP error", async t => {
688-    let errorJson = Object.fromEntries([[
689-        "error",
690-        "Not found"
691-      ]]);
692-    mockFetchJson(errorJson, 404, "Not Found");
693-    let result = await Stadia.Place.getDetails({
694-      gid: "test:123"
695-    });
696-    if (typeof result !== "object") {
697-      t.expect(true).toEqual(false);
698-      return;
699-    }
700-    if (result.TAG === "Found") {
701-      t.expect(true).toEqual(false);
702-      return;
703-    }
704-    t.expect(result._0).toEqual("404: Not found");
705-  });
706-  Vitest.test("returns ServerError on geocoding error", async t => {
707-    let responseJson = Object.fromEntries([
708-      [
709-        "type",
710-        "FeatureCollection"
711-      ],
712-      [
713-        "geocoding",
714-        Object.fromEntries([
715-          [
716-            "attribution",
717-            "test"
718-          ],
719-          [
720-            "error",
721-            "Invalid GID format"
722-          ]
723-        ])
724-      ],
725-      [
726-        "features",
727-        []
728-      ]
729-    ]);
730-    mockFetchJson(responseJson, 200, "OK");
731-    let result = await Stadia.Place.getDetails({
732-      gid: "invalid"
733-    });
734-    if (typeof result !== "object") {
735-      t.expect(true).toEqual(false);
736-      return;
737-    }
738-    if (result.TAG === "Found") {
739-      t.expect(true).toEqual(false);
740-      return;
741-    }
742-    t.expect(result._0).toEqual("Invalid GID format");
743-  });
744-  Vitest.test("encodes gid in ids parameter", async t => {
745-    let _calls = setupMockWithCapture();
746-    await Stadia.Place.getDetails({
747-      gid: "openstreetmap:address:way/123"
748-    });
749-    let url = getUrl(_calls);
750-    t.expect(url.includes("ids=openstreetmap%3Aaddress%3Away%2F123")).toEqual(true);
751-  });
752-  Vitest.test("encodes lang parameter", async t => {
753-    let _calls = setupMockWithCapture();
754-    await Stadia.Place.getDetails({
755-      gid: "test:123",
756-      lang: "en"
757-    });
758-    let url = getUrl(_calls);
759-    t.expect(url.includes("lang=en")).toEqual(true);
760-  });
761-  Vitest.test("handles all layer types", async t => {
762-    let testCases = [
763-      [
764-        "poi",
765-        "poi"
766-      ],
767-      [
768-        "address",
769-        "address"
770-      ],
771-      [
772-        "street",
773-        "street"
774-      ],
775-      [
776-        "country",
777-        "country"
778-      ],
779-      [
780-        "macroregion",
781-        "macroregion"
782-      ],
783-      [
784-        "region",
785-        "region"
786-      ],
787-      [
788-        "macrocounty",
789-        "macrocounty"
790-      ],
791-      [
792-        "county",
793-        "county"
794-      ],
795-      [
796-        "locality",
797-        "locality"
798-      ],
799-      [
800-        "localadmin",
801-        "localadmin"
802-      ],
803-      [
804-        "borough",
805-        "borough"
806-      ],
807-      [
808-        "neighbourhood",
809-        "neighbourhood"
810-      ],
811-      [
812-        "postalcode",
813-        "postalcode"
814-      ],
815-      [
816-        "coarse",
817-        "coarse"
818-      ]
819-    ];
820-    let features = testCases.map((param, idx) => Object.fromEntries([
821-      [
822-        "type",
823-        "Feature"
824-      ],
825-      [
826-        "geometry",
827-        Object.fromEntries([
828-          [
829-            "type",
830-            "Point"
831-          ],
832-          [
833-            "coordinates",
834-            [
835-              idx,
836-              idx
837-            ]
838-          ]
839-        ])
840-      ],
841-      [
842-        "properties",
843-        Object.fromEntries([
844-          [
845-            "name",
846-            `Test ` + idx.toString()
847-          ],
848-          [
849-            "gid",
850-            `test:` + idx.toString()
851-          ],
852-          [
853-            "layer",
854-            param[0]
855-          ],
856-          [
857-            "coarse_location",
858-            "Test City"
859-          ]
860-        ])
861-      ]
862-    ]));
863-    let f = features[0];
864-    let firstFeature = f !== undefined ? f : null;
865-    let responseJson = Object.fromEntries([
866-      [
867-        "type",
868-        "FeatureCollection"
869-      ],
870-      [
871-        "geocoding",
872-        Object.fromEntries([[
873-            "attribution",
874-            "test"
875-          ]])
876-      ],
877-      [
878-        "features",
879-        [firstFeature]
880-      ]
881-    ]);
882-    mockFetchJson(responseJson, 200, "OK");
883-    let result = await Stadia.Place.getDetails({
884-      gid: "test:0"
885-    });
886-    if (typeof result !== "object") {
887-      t.expect(true).toEqual(false);
888-      return;
889-    }
890-    if (result.TAG === "Found") {
891-      t.expect(result._0.layer).toEqual("poi");
892-      return;
893-    }
894-    t.expect(true).toEqual(false);
895-  });
896-});
897-
898-export {
899-  mockFetchJson,
900-  setupMockWithCapture,
901-  getUrl,
902-}
903-/*  Not a pure module */
D client/src/ffi/Stadia.res
+0, -699
  1@@ -1,699 +0,0 @@
  2-// Internal helpers (not exposed in .resi)
  3-
  4-let encodeURIComponent = (str: string): string => {
  5-  %raw(`encodeURIComponent`)(str)
  6-}
  7-
  8-let getApiKey = (): option<string> => {
  9-  let key = %raw(`import.meta.env.VITE_STADIA_KEY`)
 10-  switch key {
 11-  | None => None
 12-  | Some(k) =>
 13-    switch k->JSON.Decode.string {
 14-    | Some(s) if s->String.length > 0 => Some(s)
 15-    | _ => None
 16-    }
 17-  }
 18-}
 19-
 20-// Shared layer type and conversion functions
 21-type layer = [
 22-  | #poi
 23-  | #address
 24-  | #street
 25-  | #country
 26-  | #macroregion
 27-  | #region
 28-  | #macrocounty
 29-  | #county
 30-  | #locality
 31-  | #localadmin
 32-  | #borough
 33-  | #neighbourhood
 34-  | #postalcode
 35-  | #coarse
 36-]
 37-
 38-let layerToString = (layer: layer): string => {
 39-  switch layer {
 40-  | #poi => "poi"
 41-  | #address => "address"
 42-  | #street => "street"
 43-  | #country => "country"
 44-  | #macroregion => "macroregion"
 45-  | #region => "region"
 46-  | #macrocounty => "macrocounty"
 47-  | #county => "county"
 48-  | #locality => "locality"
 49-  | #localadmin => "localadmin"
 50-  | #borough => "borough"
 51-  | #neighbourhood => "neighbourhood"
 52-  | #postalcode => "postalcode"
 53-  | #coarse => "coarse"
 54-  }
 55-}
 56-
 57-let parseLayer = (str: string): option<layer> => {
 58-  switch str {
 59-  | "poi" => Some(#poi)
 60-  | "address" => Some(#address)
 61-  | "street" => Some(#street)
 62-  | "country" => Some(#country)
 63-  | "macroregion" => Some(#macroregion)
 64-  | "region" => Some(#region)
 65-  | "macrocounty" => Some(#macrocounty)
 66-  | "county" => Some(#county)
 67-  | "locality" => Some(#locality)
 68-  | "localadmin" => Some(#localadmin)
 69-  | "borough" => Some(#borough)
 70-  | "neighbourhood" => Some(#neighbourhood)
 71-  | "postalcode" => Some(#postalcode)
 72-  | "coarse" => Some(#coarse)
 73-  | _ => None
 74-  }
 75-}
 76-
 77-// Parse common properties from feature properties object
 78-let parseBaseProperties = (props: Dict.t<JSON.t>): (string, string, option<layer>, string) => {
 79-  // Extract name
 80-  let name = switch props->Dict.get("name") {
 81-  | Some(n) =>
 82-    switch n->JSON.Decode.string {
 83-    | Some(s) => s
 84-    | None => ""
 85-    }
 86-  | None => ""
 87-  }
 88-
 89-  // Extract gid
 90-  let gid = switch props->Dict.get("gid") {
 91-  | Some(g) =>
 92-    switch g->JSON.Decode.string {
 93-    | Some(s) => s
 94-    | None => ""
 95-    }
 96-  | None => ""
 97-  }
 98-
 99-  // Extract layer
100-  let layer = switch props->Dict.get("layer") {
101-  | Some(l) =>
102-    switch l->JSON.Decode.string {
103-    | Some(s) => parseLayer(s)
104-    | None => None
105-    }
106-  | None => None
107-  }
108-
109-  // Extract coarse_location
110-  let coarseLocation = switch props->Dict.get("coarse_location") {
111-  | Some(c) =>
112-    switch c->JSON.Decode.string {
113-    | Some(s) => s
114-    | None => ""
115-    }
116-  | None => ""
117-  }
118-
119-  (name, gid, layer, coarseLocation)
120-}
121-
122-// Check for geocoding error in response
123-let checkGeocodingError = (obj: Dict.t<JSON.t>): option<string> => {
124-  switch obj->Dict.get("geocoding") {
125-  | Some(g) =>
126-    switch g->JSON.Decode.object {
127-    | Some(geoObj) =>
128-      switch geoObj->Dict.get("error") {
129-      | Some(e) => e->JSON.Decode.string
130-      | None => None
131-      }
132-    | None => None
133-    }
134-  | None => None
135-  }
136-}
137-
138-// Common response handling for Stadia API calls
139-let handleStadiaResponse = (
140-  json: JSON.t,
141-  ~parseFeature: JSON.t => option<'a>,
142-  ~wrapResults: array<'a> => 'response,
143-  ~emptyResponse: 'response,
144-  ~serverError: string => 'response,
145-): 'response => {
146-  switch json->JSON.Decode.object {
147-  | Some(obj) => {
148-      // Check for geocoding error
149-      switch checkGeocodingError(obj) {
150-      | Some(errMsg) => serverError(errMsg)
151-      | None => {
152-          // Parse features
153-          let features = switch obj->Dict.get("features") {
154-          | Some(f) =>
155-            switch f->JSON.Decode.array {
156-            | Some(arr) => arr
157-            | None => []
158-            }
159-          | None => []
160-          }
161-
162-          let results = features->Array.filterMap(parseFeature)
163-
164-          if results->Array.length == 0 {
165-            emptyResponse
166-          } else {
167-            wrapResults(results)
168-          }
169-        }
170-      }
171-    }
172-  | None => serverError("Invalid JSON response")
173-  }
174-}
175-
176-// Public modules
177-
178-module Autocomplete = {
179-  type layer = layer
180-
181-  type params = {
182-    text: string,
183-    focusPoint?: (float, float),
184-    boundaryRect?: (float, float, float, float),
185-    boundaryCircle?: (float, float, float),
186-    layers?: array<layer>,
187-    sources?: array<string>,
188-    boundaryCountry?: array<string>,
189-    boundaryGid?: string,
190-    size?: int,
191-    lang?: string,
192-  }
193-
194-  type result = {
195-    name: string,
196-    gid: string,
197-    layer: layer,
198-    coarseLocation: string,
199-    distance?: float,
200-  }
201-
202-  type response =
203-    | Results(array<result>)
204-    | Empty
205-    | ServerError(string)
206-
207-  let buildQueryString = (~apiKey: option<string>, params: params): string => {
208-    let parts = []
209-
210-    // Required text parameter
211-    parts->Array.push(`text=${encodeURIComponent(params.text)}`)
212-
213-    // Optional focus point
214-    switch params.focusPoint {
215-    | Some((lat, lon)) => {
216-        parts->Array.push(`focus.point.lat=${Float.toString(lat)}`)
217-        parts->Array.push(`focus.point.lon=${Float.toString(lon)}`)
218-      }
219-    | None => ()
220-    }
221-
222-    // Optional boundary rect
223-    switch params.boundaryRect {
224-    | Some((minLon, maxLon, minLat, maxLat)) => {
225-        parts->Array.push(`boundary.rect.min_lon=${Float.toString(minLon)}`)
226-        parts->Array.push(`boundary.rect.max_lon=${Float.toString(maxLon)}`)
227-        parts->Array.push(`boundary.rect.min_lat=${Float.toString(minLat)}`)
228-        parts->Array.push(`boundary.rect.max_lat=${Float.toString(maxLat)}`)
229-      }
230-    | None => ()
231-    }
232-
233-    // Optional boundary circle
234-    switch params.boundaryCircle {
235-    | Some((lat, lon, radius)) => {
236-        parts->Array.push(`boundary.circle.lat=${Float.toString(lat)}`)
237-        parts->Array.push(`boundary.circle.lon=${Float.toString(lon)}`)
238-        parts->Array.push(`boundary.circle.radius=${Float.toString(radius)}`)
239-      }
240-    | None => ()
241-    }
242-
243-    // Optional layers
244-    switch params.layers {
245-    | Some(layers) if layers->Array.length > 0 => {
246-        let layerStr = layers->Array.map(layerToString)->Array.join(",")
247-        parts->Array.push(`layers=${encodeURIComponent(layerStr)}`)
248-      }
249-    | _ => ()
250-    }
251-
252-    // Optional sources
253-    switch params.sources {
254-    | Some(sources) if sources->Array.length > 0 => {
255-        let sourceStr = sources->Array.join(",")
256-        parts->Array.push(`sources=${encodeURIComponent(sourceStr)}`)
257-      }
258-    | _ => ()
259-    }
260-
261-    // Optional boundary country
262-    switch params.boundaryCountry {
263-    | Some(countries) if countries->Array.length > 0 => {
264-        let countryStr = countries->Array.join(",")
265-        parts->Array.push(`boundary.country=${encodeURIComponent(countryStr)}`)
266-      }
267-    | _ => ()
268-    }
269-
270-    // Optional boundary gid
271-    switch params.boundaryGid {
272-    | Some(gid) => parts->Array.push(`boundary.gid=${encodeURIComponent(gid)}`)
273-    | None => ()
274-    }
275-
276-    // Optional size
277-    switch params.size {
278-    | Some(size) => parts->Array.push(`size=${Int.toString(size)}`)
279-    | None => ()
280-    }
281-
282-    // Optional lang
283-    switch params.lang {
284-    | Some(lang) => parts->Array.push(`lang=${encodeURIComponent(lang)}`)
285-    | None => ()
286-    }
287-
288-    // API key if provided
289-    switch apiKey {
290-    | Some(key) => parts->Array.push(`api_key=${encodeURIComponent(key)}`)
291-    | None => ()
292-    }
293-
294-    parts->Array.join("&")
295-  }
296-
297-  let parseFeature = (feature: JSON.t): option<result> => {
298-    switch feature->JSON.Decode.object {
299-    | Some(obj) => {
300-        let properties = switch obj->Dict.get("properties") {
301-        | Some(p) => p->JSON.Decode.object
302-        | None => None
303-        }
304-
305-        switch properties {
306-        | Some(props) => {
307-            let (name, gid, layer, coarseLocation) = parseBaseProperties(props)
308-
309-            // Extract optional distance
310-            let distance = switch props->Dict.get("distance") {
311-            | Some(d) =>
312-              switch d->JSON.Decode.float {
313-              | Some(n) => Some(n)
314-              | None => None
315-              }
316-            | None => None
317-            }
318-
319-            switch layer {
320-            | Some(l) =>
321-              Some({
322-                name: name,
323-                gid: gid,
324-                layer: l,
325-                coarseLocation: coarseLocation,
326-                distance: ?distance,
327-              })
328-            | None => None
329-            }
330-          }
331-        | None => None
332-        }
333-      }
334-    | None => None
335-    }
336-  }
337-
338-  let search = async (~params: params): response => {
339-    let apiKey = getApiKey()
340-    let queryString = buildQueryString(~apiKey, params)
341-    let url = `https://api.stadiamaps.com/geocoding/v2/autocomplete?${queryString}`
342-
343-    try {
344-      let response = await Fetch.get(url)
345-        ->Fetch.onResponse(
346-          ~onSuccess=json => json,
347-          ~onError=(status, message) => {
348-            JSON.Encode.object(Dict.fromArray([
349-              ("__error", JSON.Encode.bool(true)),
350-              ("__status", JSON.Encode.float(Int.toFloat(status))),
351-              ("__message", JSON.Encode.string(message)),
352-            ]))
353-          }
354-        )
355-        ->Fetch.send
356-
357-      // Check if we got an error response from Fetch
358-      switch response->JSON.Decode.object {
359-      | Some(obj) =>
360-        switch obj->Dict.get("__error") {
361-        | Some(_) => {
362-            let status = switch obj->Dict.get("__status") {
363-            | Some(s) =>
364-              switch s->JSON.Decode.float {
365-              | Some(n) => n->Float.toInt->Int.toString
366-              | None => "0"
367-              }
368-            | None => "0"
369-            }
370-            let message = switch obj->Dict.get("__message") {
371-            | Some(m) =>
372-              switch m->JSON.Decode.string {
373-              | Some(s) => s
374-              | None => "Unknown error"
375-              }
376-            | None => "Unknown error"
377-            }
378-            ServerError(`${status}: ${message}`)
379-          }
380-        | None => {
381-            handleStadiaResponse(
382-              response,
383-              ~parseFeature=parseFeature,
384-              ~wrapResults=results => Results(results),
385-              ~emptyResponse=Empty,
386-              ~serverError=msg => ServerError(msg),
387-            )
388-          }
389-        }
390-      | None => ServerError("Invalid JSON response")
391-      }
392-    } catch {
393-    | _ => ServerError("Network error")
394-    }
395-  }
396-}
397-
398-// End of Autocomplete module
399-
400-module Place = {
401-  type addressComponents = {
402-    number?: string,
403-    street?: string,
404-    postal_code?: string,
405-  }
406-
407-  type whosonfirstEntity = {
408-    gid: string,
409-    name: string,
410-  }
411-
412-  type whosonfirstContext = {
413-    country?: whosonfirstEntity,
414-    region?: whosonfirstEntity,
415-    county?: whosonfirstEntity,
416-    locality?: whosonfirstEntity,
417-    neighbourhood?: whosonfirstEntity,
418-  }
419-
420-  type context = {
421-    whosonfirst?: whosonfirstContext,
422-    iso_3166_a2?: string,
423-    iso_3166_a3?: string,
424-  }
425-
426-  type params = {
427-    gid: string,
428-    lang?: string,
429-  }
430-
431-  type result = {
432-    name: string,
433-    gid: string,
434-    layer: Autocomplete.layer,
435-    coarseLocation: string,
436-    coordinates: (float, float),  // (lon, lat)
437-    formattedAddressLine?: string,
438-    formattedAddressLines?: array<string>,
439-    addressComponents?: addressComponents,
440-    context?: context,
441-  }
442-
443-  type response =
444-    | Found(result)
445-    | NotFound
446-    | ServerError(string)
447-
448-  let buildQueryString = (~apiKey: option<string>, params: params): string => {
449-    let parts = []
450-
451-    // Required ids parameter (single gid)
452-    parts->Array.push(`ids=${encodeURIComponent(params.gid)}`)
453-
454-    // Optional lang
455-    switch params.lang {
456-    | Some(lang) => parts->Array.push(`lang=${encodeURIComponent(lang)}`)
457-    | None => ()
458-    }
459-
460-    // API key if provided
461-    switch apiKey {
462-    | Some(key) => parts->Array.push(`api_key=${encodeURIComponent(key)}`)
463-    | None => ()
464-    }
465-
466-    parts->Array.join("&")
467-  }
468-
469-  let parseFeature = (feature: JSON.t): option<result> => {
470-    switch feature->JSON.Decode.object {
471-    | Some(obj) => {
472-        let properties = switch obj->Dict.get("properties") {
473-        | Some(p) => p->JSON.Decode.object
474-        | None => None
475-        }
476-
477-        let geometry = switch obj->Dict.get("geometry") {
478-        | Some(g) => g->JSON.Decode.object
479-        | None => None
480-        }
481-
482-        switch (properties, geometry) {
483-        | (Some(props), Some(geo)) => {
484-            let (name, gid, layer, coarseLocation) = parseBaseProperties(props)
485-
486-            // Parse geometry coordinates
487-            let coordinates = switch geo->Dict.get("coordinates") {
488-            | Some(c) =>
489-              switch c->JSON.Decode.array {
490-              | Some(arr) if arr->Array.length >= 2 => {
491-                  let lon = switch arr[0] {
492-                  | Some(lonJson) =>
493-                    switch lonJson->JSON.Decode.float {
494-                    | Some(f) => f
495-                    | None => 0.0
496-                    }
497-                  | None => 0.0
498-                  }
499-                  let lat = switch arr[1] {
500-                  | Some(latJson) =>
501-                    switch latJson->JSON.Decode.float {
502-                    | Some(f) => f
503-                    | None => 0.0
504-                    }
505-                  | None => 0.0
506-                  }
507-                  Some((lon, lat))
508-                }
509-              | _ => None
510-              }
511-            | None => None
512-            }
513-
514-            // Parse formatted addresses
515-            let formattedAddressLine = switch props->Dict.get("formatted_address_line") {
516-            | Some(v) => v->JSON.Decode.string
517-            | None => None
518-            }
519-
520-            let formattedAddressLines = switch props->Dict.get("formatted_address_lines") {
521-            | Some(v) =>
522-              switch v->JSON.Decode.array {
523-              | Some(arr) =>
524-                arr->Array.filterMap(item => item->JSON.Decode.string)->Some
525-              | None => None
526-              }
527-            | None => None
528-            }
529-
530-            // Parse address_components
531-            let addressComponents = switch props->Dict.get("address_components") {
532-            | Some(ac) =>
533-              switch ac->JSON.Decode.object {
534-              | Some(obj) => {
535-                  let number = obj->Dict.get("number")->Option.flatMap(JSON.Decode.string)
536-                  let street = obj->Dict.get("street")->Option.flatMap(JSON.Decode.string)
537-                  let postal_code = obj->Dict.get("postal_code")->Option.flatMap(JSON.Decode.string)
538-                  Some({
539-                    ?number,
540-                    ?street,
541-                    ?postal_code,
542-                  })
543-                }
544-              | None => None
545-              }
546-            | None => None
547-            }
548-
549-            // Parse context (whosonfirst and ISO codes)
550-            let context = switch props->Dict.get("context") {
551-            | Some(ctx) =>
552-              switch ctx->JSON.Decode.object {
553-              | Some(ctxObj) => {
554-                  let parseEntity = key => {
555-                    switch ctxObj->Dict.get(key) {
556-                    | Some(e) =>
557-                      switch e->JSON.Decode.object {
558-                      | Some(eObj) => {
559-                          let gidOpt = eObj->Dict.get("gid")->Option.flatMap(JSON.Decode.string)
560-                          let nameOpt = eObj->Dict.get("name")->Option.flatMap(JSON.Decode.string)
561-                          switch (gidOpt, nameOpt) {
562-                          | (Some(gid), Some(name)) => Some({gid, name})
563-                          | _ => None
564-                          }
565-                        }
566-                      | None => None
567-                      }
568-                    | None => None
569-                    }
570-                  }
571-
572-                  let whosonfirst = {
573-                    let country = parseEntity("country")
574-                    let region = parseEntity("region")
575-                    let county = parseEntity("county")
576-                    let locality = parseEntity("locality")
577-                    let neighbourhood = parseEntity("neighbourhood")
578-                    switch (country, region, county, locality, neighbourhood) {
579-                    | (None, None, None, None, None) => None
580-                    | _ =>
581-                      Some({
582-                        ?country,
583-                        ?region,
584-                        ?county,
585-                        ?locality,
586-                        ?neighbourhood,
587-                      })
588-                    }
589-                  }
590-
591-                  let iso_3166_a2 = ctxObj->Dict.get("iso_3166_a2")->Option.flatMap(JSON.Decode.string)
592-                  let iso_3166_a3 = ctxObj->Dict.get("iso_3166_a3")->Option.flatMap(JSON.Decode.string)
593-
594-                  switch (whosonfirst, iso_3166_a2, iso_3166_a3) {
595-                  | (None, None, None) => None
596-                  | _ =>
597-                    Some({
598-                      ?whosonfirst,
599-                      ?iso_3166_a2,
600-                      ?iso_3166_a3,
601-                    })
602-                  }
603-                }
604-              | None => None
605-              }
606-            | None => None
607-            }
608-
609-            switch (layer, coordinates) {
610-            | (Some(l), Some(coords)) =>
611-              Some({
612-                name: name,
613-                gid: gid,
614-                layer: l,
615-                coarseLocation: coarseLocation,
616-                coordinates: coords,
617-                ?formattedAddressLine,
618-                ?formattedAddressLines,
619-                ?addressComponents,
620-                ?context,
621-              })
622-            | _ => None
623-            }
624-          }
625-        | _ => None
626-        }
627-      }
628-    | None => None
629-    }
630-  }
631-
632-  let getDetails = async (~params: params): response => {
633-    let apiKey = getApiKey()
634-    let queryString = buildQueryString(~apiKey, params)
635-    let url = `https://api.stadiamaps.com/geocoding/v2/place_details?${queryString}`
636-
637-    try {
638-      let response = await Fetch.get(url)
639-        ->Fetch.onResponse(
640-          ~onSuccess=json => json,
641-          ~onError=(status, message) => {
642-            JSON.Encode.object(Dict.fromArray([
643-              ("__error", JSON.Encode.bool(true)),
644-              ("__status", JSON.Encode.float(Int.toFloat(status))),
645-              ("__message", JSON.Encode.string(message)),
646-            ]))
647-          }
648-        )
649-        ->Fetch.send
650-
651-      // Check if we got an error response from Fetch
652-      switch response->JSON.Decode.object {
653-      | Some(obj) =>
654-        switch obj->Dict.get("__error") {
655-        | Some(_) => {
656-            let status = switch obj->Dict.get("__status") {
657-            | Some(s) =>
658-              switch s->JSON.Decode.float {
659-              | Some(n) => n->Float.toInt->Int.toString
660-              | None => "0"
661-              }
662-            | None => "0"
663-            }
664-            let message = switch obj->Dict.get("__message") {
665-            | Some(m) =>
666-              switch m->JSON.Decode.string {
667-              | Some(s) => s
668-              | None => "Unknown error"
669-              }
670-            | None => "Unknown error"
671-            }
672-            ServerError(`${status}: ${message}`)
673-          }
674-        | None => {
675-            // For Place details, we expect exactly one result
676-            switch handleStadiaResponse(
677-              response,
678-              ~parseFeature=parseFeature,
679-              ~wrapResults=results => {
680-                switch results[0] {
681-                | Some(result) => Found(result)
682-                | None => NotFound
683-                }
684-              },
685-              ~emptyResponse=NotFound,
686-              ~serverError=msg => ServerError(msg),
687-            ) {
688-            | Found(_) as found => found
689-            | NotFound as notFound => notFound
690-            | ServerError(_) as err => err
691-            }
692-          }
693-        }
694-      | None => ServerError("Invalid JSON response")
695-      }
696-    } catch {
697-    | _ => ServerError("Network error")
698-    }
699-  }
700-}
D client/src/ffi/Stadia.resi
+0, -100
  1@@ -1,100 +0,0 @@
  2-// Public interface for Stadia Maps API bindings
  3-// Internal implementation details are hidden
  4-
  5-module Autocomplete: {
  6-  type layer = [
  7-    | #poi
  8-    | #address
  9-    | #street
 10-    | #country
 11-    | #macroregion
 12-    | #region
 13-    | #macrocounty
 14-    | #county
 15-    | #locality
 16-    | #localadmin
 17-    | #borough
 18-    | #neighbourhood
 19-    | #postalcode
 20-    | #coarse
 21-  ]
 22-
 23-  type params = {
 24-    text: string,
 25-    focusPoint?: (float, float),
 26-    boundaryRect?: (float, float, float, float),
 27-    boundaryCircle?: (float, float, float),
 28-    layers?: array<layer>,
 29-    sources?: array<string>,
 30-    boundaryCountry?: array<string>,
 31-    boundaryGid?: string,
 32-    size?: int,
 33-    lang?: string,
 34-  }
 35-
 36-  type result = {
 37-    name: string,
 38-    gid: string,
 39-    layer: layer,
 40-    coarseLocation: string,
 41-    distance?: float,
 42-  }
 43-
 44-  type response =
 45-    | Results(array<result>)
 46-    | Empty
 47-    | ServerError(string)
 48-
 49-  let search: (~params: params) => promise<response>
 50-}
 51-
 52-module Place: {
 53-  type addressComponents = {
 54-    number?: string,
 55-    street?: string,
 56-    postal_code?: string,
 57-  }
 58-
 59-  type whosonfirstEntity = {
 60-    gid: string,
 61-    name: string,
 62-  }
 63-
 64-  type whosonfirstContext = {
 65-    country?: whosonfirstEntity,
 66-    region?: whosonfirstEntity,
 67-    county?: whosonfirstEntity,
 68-    locality?: whosonfirstEntity,
 69-    neighbourhood?: whosonfirstEntity,
 70-  }
 71-
 72-  type context = {
 73-    whosonfirst?: whosonfirstContext,
 74-    iso_3166_a2?: string,
 75-    iso_3166_a3?: string,
 76-  }
 77-
 78-  type params = {
 79-    gid: string,
 80-    lang?: string,
 81-  }
 82-
 83-  type result = {
 84-    name: string,
 85-    gid: string,
 86-    layer: Autocomplete.layer,
 87-    coarseLocation: string,
 88-    coordinates: (float, float),  // (lon, lat)
 89-    formattedAddressLine?: string,
 90-    formattedAddressLines?: array<string>,
 91-    addressComponents?: addressComponents,
 92-    context?: context,
 93-  }
 94-
 95-  type response =
 96-    | Found(result)
 97-    | NotFound
 98-    | ServerError(string)
 99-
100-  let getDetails: (~params: params) => promise<response>
101-}
M client/src/ffi/Waldo.res
+101, -81
  1@@ -44,34 +44,64 @@ type response =
  2 let decodeResult: JSON.t => option<result> = json => {
  3   switch json->JSON.Decode.object {
  4   | Some(dict) => {
  5-      let gid = dict->Dict.get("gid")->Option.flatMap(JSON.Decode.float)->Option.map(f => f->Float.toInt)
  6+      let gid =
  7+        dict->Dict.get("gid")->Option.flatMap(JSON.Decode.float)->Option.map(f => f->Float.toInt)
  8       let name = dict->Dict.get("name")->Option.flatMap(JSON.Decode.string)
  9       let placetype = dict->Dict.get("placetype")->Option.flatMap(JSON.Decode.string)
 10       let lon = dict->Dict.get("lon")->Option.flatMap(JSON.Decode.float)
 11       let lat = dict->Dict.get("lat")->Option.flatMap(JSON.Decode.float)
 12       let distance = dict->Dict.get("distance")->Option.flatMap(JSON.Decode.float)
 13       let bearing = dict->Dict.get("bearing")->Option.flatMap(JSON.Decode.float)
 14-      let region = dict
 15+      let region =
 16+        dict
 17         ->Dict.get("region")
 18         ->Option.flatMap(JSON.Decode.array)
 19         ->Option.map(arr => arr->Array.filterMap(JSON.Decode.string))
 20       let country = dict->Dict.get("country")->Option.flatMap(JSON.Decode.string)
 21 
 22       switch (gid, name, placetype, lon, lat, distance, bearing, region, country) {
 23-      | (Some(gid), Some(name), Some(placetype), Some(lon), Some(lat), Some(distance), Some(bearing), Some(region), Some(country)) =>
 24+      | (
 25+          Some(gid),
 26+          Some(name),
 27+          Some(placetype),
 28+          Some(lon),
 29+          Some(lat),
 30+          Some(distance),
 31+          Some(bearing),
 32+          Some(region),
 33+          Some(country),
 34+        ) =>
 35         Some({gid, name, placetype, lon, lat, distance, bearing, region, country})
 36       | _ => {
 37           // DEBUG: Log which fields are missing
 38           let missing = []
 39-          if gid->Option.isNone { missing->Array.push("gid") }
 40-          if name->Option.isNone { missing->Array.push("name") }
 41-          if placetype->Option.isNone { missing->Array.push("placetype") }
 42-          if lon->Option.isNone { missing->Array.push("lon") }
 43-          if lat->Option.isNone { missing->Array.push("lat") }
 44-          if distance->Option.isNone { missing->Array.push("distance") }
 45-          if bearing->Option.isNone { missing->Array.push("bearing") }
 46-          if region->Option.isNone { missing->Array.push("region") }
 47-          if country->Option.isNone { missing->Array.push("country") }
 48+          if gid->Option.isNone {
 49+            missing->Array.push("gid")
 50+          }
 51+          if name->Option.isNone {
 52+            missing->Array.push("name")
 53+          }
 54+          if placetype->Option.isNone {
 55+            missing->Array.push("placetype")
 56+          }
 57+          if lon->Option.isNone {
 58+            missing->Array.push("lon")
 59+          }
 60+          if lat->Option.isNone {
 61+            missing->Array.push("lat")
 62+          }
 63+          if distance->Option.isNone {
 64+            missing->Array.push("distance")
 65+          }
 66+          if bearing->Option.isNone {
 67+            missing->Array.push("bearing")
 68+          }
 69+          if region->Option.isNone {
 70+            missing->Array.push("region")
 71+          }
 72+          if country->Option.isNone {
 73+            missing->Array.push("country")
 74+          }
 75           if missing->Array.length > 0 {
 76             Console.error(`[Waldo] decodeResult: Missing fields: ${missing->Array.join(", ")}`)
 77           }
 78@@ -92,26 +122,30 @@ let decodeResult: JSON.t => option<result> = json => {
 79 let decodeGeocodeResponse: JSON.t => option<geocodeResponse> = json => {
 80   switch json->JSON.Decode.object {
 81   | Some(dict) => {
 82-      Console.log("[Waldo] decodeGeocodeResponse: Got JSON object, extracting fields...")
 83-      let results = dict
 84+      let results =
 85+        dict
 86         ->Dict.get("results")
 87         ->Option.flatMap(JSON.Decode.array)
 88         ->Option.map(arr => arr->Array.filterMap(decodeResult))
 89-      let numResults = dict
 90+      let numResults =
 91+        dict
 92         ->Dict.get("num_results")
 93         ->Option.flatMap(JSON.Decode.float)
 94         ->Option.map(f => f->Float.toInt)
 95 
 96       switch (results, numResults) {
 97-      | (Some(results), Some(numResults)) => {
 98-          Console.log(`[Waldo] decodeGeocodeResponse: Success with ${numResults->Int.toString} results`)
 99-          Some({results, numResults})
100-        }
101+      | (Some(results), Some(numResults)) => Some({results, numResults})
102       | _ => {
103           let missing = []
104-          if results->Option.isNone { missing->Array.push("results") }
105-          if numResults->Option.isNone { missing->Array.push("num_results") }
106-          Console.error(`[Waldo] decodeGeocodeResponse: Missing fields: ${missing->Array.join(", ")}`)
107+          if results->Option.isNone {
108+            missing->Array.push("results")
109+          }
110+          if numResults->Option.isNone {
111+            missing->Array.push("num_results")
112+          }
113+          Console.error(
114+            `[Waldo] decodeGeocodeResponse: Missing fields: ${missing->Array.join(", ")}`,
115+          )
116           None
117         }
118       }
119@@ -128,11 +162,9 @@ let decodeGeocodeResponse: JSON.t => option<geocodeResponse> = json => {
120  */
121 let decodeErrorResponse: JSON.t => option<errorResponse> = json => {
122   switch json->JSON.Decode.object {
123-  | Some(dict) => {
124-      switch dict->Dict.get("error")->Option.flatMap(JSON.Decode.string) {
125-      | Some(error) => Some({error: error})
126-      | None => None
127-      }
128+  | Some(dict) => switch dict->Dict.get("error")->Option.flatMap(JSON.Decode.string) {
129+    | Some(error) => Some({error: error})
130+    | None => None
131     }
132   | None => None
133   }
134@@ -163,22 +195,20 @@ module Forward = {
135     let url = buildUrl(params)
136 
137     try {
138-      Console.log("[Waldo] Forward: Starting fetch request...")
139       let response = await Fetch.get(url)
140-        ->Fetch.onResponse(
141-          ~onSuccess=json => json,
142-          ~onError=(status, message) => {
143-            JSON.Encode.object(Dict.fromArray([
144+      ->Fetch.onResponse(
145+        ~onSuccess=json => json,
146+        ~onError=(status, message) => {
147+          JSON.Encode.object(
148+            Dict.fromArray([
149               ("__error", JSON.Encode.bool(true)),
150               ("__status", JSON.Encode.float(Int.toFloat(status))),
151               ("__message", JSON.Encode.string(message)),
152-            ]))
153-          }
154-        )
155-        ->Fetch.send
156-
157-      // DEBUG: Log raw response
158-      Console.log("[Waldo] Forward: Raw response received")
159+            ]),
160+          )
161+        },
162+      )
163+      ->Fetch.send
164 
165       // DEBUG: Log the actual response content
166       let responseStr = try {
167@@ -186,12 +216,11 @@ module Forward = {
168       } catch {
169       | _ => "[Failed to stringify response]"
170       }
171-      Console.log(`[Waldo] Forward: Response content: ${responseStr}`)
172+      Log.debug(`[Waldo] Forward: Response content: ${responseStr}`)
173 
174       // Check if we got an error response from Fetch
175       switch response->JSON.Decode.object {
176       | Some(obj) =>
177-        Console.log("[Waldo] Forward: Response is JSON object")
178         switch obj->Dict.get("__error") {
179         | Some(_) => {
180             let status = switch obj->Dict.get("__status") {
181@@ -213,27 +242,20 @@ module Forward = {
182             Console.error(`[Waldo] Forward: Fetch error: ${status} - ${message}`)
183             Error(`${status}: ${message}`)
184           }
185-        | None => {
186-            // Try to decode as success response
187-            Console.log("[Waldo] Forward: No __error field found, attempting to decode geocode response...")
188-            Console.log(`[Waldo] Forward: Response object keys: ${obj->Dict.keysToArray->Array.join(", ")}`)
189-            switch decodeGeocodeResponse(response) {
190-            | Some(resp) => {
191-                Console.log(`[Waldo] Forward: Decoded ${resp.numResults->Int.toString} results`)
192-                Success(resp.results)
193-              }
194-            | None => {
195-                Console.error("[Waldo] Forward: Failed to decode geocode response")
196-                // Try to decode as error response
197-                switch decodeErrorResponse(response) {
198-                | Some(err) => {
199-                    Console.error(`[Waldo] Forward: API error: ${err.error}`)
200-                    Error(err.error)
201-                  }
202-                | None => {
203-                    Console.error("[Waldo] Forward: Unknown response format")
204-                    Error("Invalid response format")
205-                  }
206+        | None => // Try to decode as success response
207+          switch decodeGeocodeResponse(response) {
208+          | Some(resp) => Success(resp.results)
209+          | None => {
210+              Console.error("[Waldo] Forward: Failed to decode geocode response")
211+              // Try to decode as error response
212+              switch decodeErrorResponse(response) {
213+              | Some(err) => {
214+                  Console.error(`[Waldo] Forward: API error: ${err.error}`)
215+                  Error(err.error)
216+                }
217+              | None => {
218+                  Console.error("[Waldo] Forward: Unknown response format")
219+                  Error("Invalid response format")
220                 }
221               }
222             }
223@@ -270,17 +292,19 @@ module Reverse = {
224 
225     try {
226       let response = await Fetch.get(url)
227-        ->Fetch.onResponse(
228-          ~onSuccess=json => json,
229-          ~onError=(status, message) => {
230-            JSON.Encode.object(Dict.fromArray([
231+      ->Fetch.onResponse(
232+        ~onSuccess=json => json,
233+        ~onError=(status, message) => {
234+          JSON.Encode.object(
235+            Dict.fromArray([
236               ("__error", JSON.Encode.bool(true)),
237               ("__status", JSON.Encode.float(Int.toFloat(status))),
238               ("__message", JSON.Encode.string(message)),
239-            ]))
240-          }
241-        )
242-        ->Fetch.send
243+            ]),
244+          )
245+        },
246+      )
247+      ->Fetch.send
248 
249       // Check if we got an error response from Fetch
250       switch response->JSON.Decode.object {
251@@ -305,17 +329,13 @@ module Reverse = {
252             }
253             Error(`${status}: ${message}`)
254           }
255-        | None => {
256-            // Try to decode as success response
257-            switch decodeGeocodeResponse(response) {
258-            | Some(resp) => Success(resp.results)
259-            | None => {
260-                // Try to decode as error response
261-                switch decodeErrorResponse(response) {
262-                | Some(err) => Error(err.error)
263-                | None => Error("Invalid response format")
264-                }
265-              }
266+        | None => // Try to decode as success response
267+          switch decodeGeocodeResponse(response) {
268+          | Some(resp) => Success(resp.results)
269+          | None => // Try to decode as error response
270+            switch decodeErrorResponse(response) {
271+            | Some(err) => Error(err.error)
272+            | None => Error("Invalid response format")
273             }
274           }
275         }
A client/src/ui/EditWaypoint.res
+0, -0
M client/src/ui/Editor.res
+0, -2
 1@@ -1,6 +1,5 @@
 2 // src/ui/Editor.res
 3 // Preact-compatible FFI bindings for @tiptap/react composable API
 4-@module("./content.js") @val external content: RescriptCore.JSON.t = "content"
 5 
 6 // Re-export editor type for idiomatic Rescript usage
 7 type t = Tiptap.t
 8@@ -124,7 +123,6 @@ let make = (
 9           | None => ()
10           }
11         },
12-        content,
13       })
14 
15       // Store reference for onUpdate callback
D client/src/ui/content.js
+0, -19
 1@@ -1,19 +0,0 @@
 2-export const content = {
 3-  type: "doc",
 4-  content: [
 5-    {
 6-      type: "heading",
 7-      attrs: { level: 1 },
 8-      content: [{ type: "text", text: "Start with a title..." }],
 9-    },
10-    {
11-      type: "paragraph",
12-      content: [
13-        {
14-          type: "text",
15-          text: "To add a waypoint type @ or click on the menu button at the start of an empty line...",
16-        },
17-      ],
18-    },
19-  ],
20-};
D client/test/Stadia_test.res
+0, -586
  1@@ -1,586 +0,0 @@
  2-open VitestHelpers
  3-
  4-@module("vitest") @val external vi: 'a = "vi"
  5-
  6-let mockFetchJson: (JSON.t, int, string) => unit = %raw(`
  7-  function(json, status, statusText) {
  8-    globalThis.fetch = vi.fn(() => Promise.resolve({
  9-      ok: status >= 200 && status < 300,
 10-      status: status,
 11-      statusText: statusText,
 12-      json: () => Promise.resolve(json)
 13-    }));
 14-  }
 15-`)
 16-
 17-let setupMockWithCapture: unit => 'a = %raw(`
 18-  function() {
 19-    const calls = [];
 20-    globalThis.fetch = vi.fn((url, init) => {
 21-      calls.push({url, init});
 22-      return Promise.resolve({
 23-        ok: true,
 24-        status: 200,
 25-        statusText: 'OK',
 26-        json: () => Promise.resolve({
 27-          type: 'FeatureCollection',
 28-          features: [],
 29-          geocoding: { attribution: 'test' }
 30-        })
 31-      });
 32-    });
 33-    return calls;
 34-  }
 35-`)
 36-
 37-let getUrl: 'a => string = %raw(`
 38-  function(calls) {
 39-    return calls[0].url;
 40-  }
 41-`)
 42-
 43-describe("Stadia.Autocomplete", () => {
 44-  testAsync("returns Results with parsed data for successful search", async t => {
 45-    let responseJson = JSON.Encode.object(Dict.fromArray([
 46-      ("type", JSON.Encode.string("FeatureCollection")),
 47-      ("geocoding", JSON.Encode.object(Dict.fromArray([
 48-        ("attribution", JSON.Encode.string("https://stadiamaps.com/attribution/")),
 49-        ("query", JSON.Encode.object(Dict.fromArray([("text", JSON.Encode.string("Põhja pst 27"))])))
 50-      ]))),
 51-      ("features", JSON.Encode.array([
 52-        JSON.Encode.object(Dict.fromArray([
 53-          ("type", JSON.Encode.string("Feature")),
 54-          ("geometry", JSON.Encode.null),
 55-          ("properties", JSON.Encode.object(Dict.fromArray([
 56-            ("name", JSON.Encode.string("Põhja Puiestee 27")),
 57-            ("gid", JSON.Encode.string("openaddresses:address:ee/countrywide-addresses-country:5cbc00647aa5c976")),
 58-            ("layer", JSON.Encode.string("address")),
 59-            ("coarse_location", JSON.Encode.string("Tallinn, Harju, Estonia")),
 60-            ("precision", JSON.Encode.string("point"))
 61-          ])))
 62-        ])),
 63-        JSON.Encode.object(Dict.fromArray([
 64-          ("type", JSON.Encode.string("Feature")),
 65-          ("geometry", JSON.Encode.null),
 66-          ("properties", JSON.Encode.object(Dict.fromArray([
 67-            ("name", JSON.Encode.string("Põhja pst 27")),
 68-            ("gid", JSON.Encode.string("openstreetmap:address:way/224094258")),
 69-            ("layer", JSON.Encode.string("address")),
 70-            ("coarse_location", JSON.Encode.string("Viljandi, Estonia")),
 71-            ("precision", JSON.Encode.string("centroid"))
 72-          ])))
 73-        ]))
 74-      ]))
 75-    ]))
 76-    mockFetchJson(responseJson, 200, "OK")
 77-
 78-    let result = await Stadia.Autocomplete.search(~params={
 79-      text: "Põhja pst 27"
 80-    })
 81-
 82-    switch result {
 83-    | Results(results) => {
 84-        t->expect(results->Array.length)->toEqual(2)
 85-        
 86-        switch results[0] {
 87-        | Some(first) => {
 88-            t->expect(first.name)->toEqual("Põhja Puiestee 27")
 89-            t->expect(first.gid)->toEqual("openaddresses:address:ee/countrywide-addresses-country:5cbc00647aa5c976")
 90-            t->expect(first.layer)->toEqual(#address)
 91-            t->expect(first.coarseLocation)->toEqual("Tallinn, Harju, Estonia")
 92-            t->expect(first.distance)->toEqual(None)
 93-          }
 94-        | None => t->expect(true)->toEqual(false)
 95-        }
 96-
 97-        switch results[1] {
 98-        | Some(second) => {
 99-            t->expect(second.name)->toEqual("Põhja pst 27")
100-            t->expect(second.layer)->toEqual(#address)
101-          }
102-        | None => t->expect(true)->toEqual(false)
103-        }
104-      }
105-    | _ => t->expect(true)->toEqual(false) // Should be Results
106-    }
107-  })
108-
109-  testAsync("returns Results with distance when provided", async t => {
110-    let responseJson = JSON.Encode.object(Dict.fromArray([
111-      ("type", JSON.Encode.string("FeatureCollection")),
112-      ("geocoding", JSON.Encode.object(Dict.fromArray([
113-        ("attribution", JSON.Encode.string("test"))
114-      ]))),
115-      ("features", JSON.Encode.array([
116-        JSON.Encode.object(Dict.fromArray([
117-          ("type", JSON.Encode.string("Feature")),
118-          ("geometry", JSON.Encode.null),
119-          ("properties", JSON.Encode.object(Dict.fromArray([
120-            ("name", JSON.Encode.string("Test Location")),
121-            ("gid", JSON.Encode.string("test:123")),
122-            ("layer", JSON.Encode.string("address")),
123-            ("coarse_location", JSON.Encode.string("City, Country")),
124-            ("distance", JSON.Encode.float(1.5))
125-          ])))
126-        ]))
127-      ]))
128-    ]))
129-    mockFetchJson(responseJson, 200, "OK")
130-
131-    let result = await Stadia.Autocomplete.search(~params={
132-      text: "test",
133-      focusPoint: (59.437, 24.753)
134-    })
135-
136-    switch result {
137-    | Results(results) => {
138-        t->expect(results->Array.length)->toEqual(1)
139-        switch results[0] {
140-        | Some(first) => t->expect(first.distance)->toEqual(Some(1.5))
141-        | None => t->expect(true)->toEqual(false)
142-        }
143-      }
144-    | _ => t->expect(true)->toEqual(false)
145-    }
146-  })
147-
148-  testAsync("returns Empty for empty features array", async t => {
149-    let responseJson = JSON.Encode.object(Dict.fromArray([
150-      ("type", JSON.Encode.string("FeatureCollection")),
151-      ("geocoding", JSON.Encode.object(Dict.fromArray([
152-        ("attribution", JSON.Encode.string("test"))
153-      ]))),
154-      ("features", JSON.Encode.array([]))
155-    ]))
156-    mockFetchJson(responseJson, 200, "OK")
157-
158-    let result = await Stadia.Autocomplete.search(~params={
159-      text: "xyznonexistent12345"
160-    })
161-
162-    t->expect(result)->toEqual(Stadia.Autocomplete.Empty)
163-  })
164-
165-  testAsync("returns ServerError on HTTP error", async t => {
166-    let errorJson = JSON.Encode.object(Dict.fromArray([
167-      ("error", JSON.Encode.string("Invalid API key"))
168-    ]))
169-    mockFetchJson(errorJson, 401, "Unauthorized")
170-
171-    let result = await Stadia.Autocomplete.search(~params={
172-      text: "test"
173-    })
174-
175-    switch result {
176-    | ServerError(msg) => t->expect(msg)->toEqual("401: Invalid API key")
177-    | _ => t->expect(true)->toEqual(false)
178-    }
179-  })
180-
181-  testAsync("returns ServerError on geocoding error", async t => {
182-    let responseJson = JSON.Encode.object(Dict.fromArray([
183-      ("type", JSON.Encode.string("FeatureCollection")),
184-      ("geocoding", JSON.Encode.object(Dict.fromArray([
185-        ("attribution", JSON.Encode.string("test")),
186-        ("error", JSON.Encode.string("Invalid parameter: text is required"))
187-      ]))),
188-      ("features", JSON.Encode.array([]))
189-    ]))
190-    mockFetchJson(responseJson, 200, "OK")
191-
192-    let result = await Stadia.Autocomplete.search(~params={
193-      text: "test"
194-    })
195-
196-    switch result {
197-    | ServerError(msg) => t->expect(msg)->toEqual("Invalid parameter: text is required")
198-    | _ => t->expect(true)->toEqual(false)
199-    }
200-  })
201-
202-  testAsync("encodes text parameter in query string", async t => {
203-    let _calls = setupMockWithCapture()
204-
205-    let _result = await Stadia.Autocomplete.search(~params={
206-      text: "Põhja pst 27"
207-    })
208-
209-    let url = getUrl(_calls)
210-    t->expect(url->String.includes("text=P%C3%B5hja%20pst%2027"))->toEqual(true)
211-  })
212-
213-  testAsync("encodes focus point parameters", async t => {
214-    let _calls = setupMockWithCapture()
215-
216-    let _result = await Stadia.Autocomplete.search(~params={
217-      text: "test",
218-      focusPoint: (59.437, 24.753)
219-    })
220-
221-    let url = getUrl(_calls)
222-    t->expect(url->String.includes("focus.point.lat=59.437"))->toEqual(true)
223-    t->expect(url->String.includes("focus.point.lon=24.753"))->toEqual(true)
224-  })
225-
226-  testAsync("encodes boundary rect parameters", async t => {
227-    let _calls = setupMockWithCapture()
228-
229-    let _result = await Stadia.Autocomplete.search(~params={
230-      text: "test",
231-      boundaryRect: (139.2794, 140.1471, 35.53308, 35.81346)
232-    })
233-
234-    let url = getUrl(_calls)
235-    t->expect(url->String.includes("boundary.rect.min_lon=139.2794"))->toEqual(true)
236-    t->expect(url->String.includes("boundary.rect.max_lon=140.1471"))->toEqual(true)
237-    t->expect(url->String.includes("boundary.rect.min_lat=35.53308"))->toEqual(true)
238-    t->expect(url->String.includes("boundary.rect.max_lat=35.81346"))->toEqual(true)
239-  })
240-
241-  testAsync("encodes boundary circle parameters", async t => {
242-    let _calls = setupMockWithCapture()
243-
244-    let _result = await Stadia.Autocomplete.search(~params={
245-      text: "test",
246-      boundaryCircle: (43.818156, -79.186484, 35.0)
247-    })
248-
249-    let url = getUrl(_calls)
250-    t->expect(url->String.includes("boundary.circle.lat=43.818156"))->toEqual(true)
251-    t->expect(url->String.includes("boundary.circle.lon=-79.186484"))->toEqual(true)
252-    t->expect(url->String.includes("boundary.circle.radius=35"))->toEqual(true)
253-  })
254-
255-  testAsync("encodes layers as comma-separated", async t => {
256-    let _calls = setupMockWithCapture()
257-
258-    let _result = await Stadia.Autocomplete.search(~params={
259-      text: "test",
260-      layers: [#address, #poi, #coarse]
261-    })
262-
263-    let url = getUrl(_calls)
264-    t->expect(url->String.includes("layers=address%2Cpoi%2Ccoarse") || url->String.includes("layers=address,poi,coarse"))->toEqual(true)
265-  })
266-
267-  testAsync("encodes sources as comma-separated", async t => {
268-    let _calls = setupMockWithCapture()
269-
270-    let _result = await Stadia.Autocomplete.search(~params={
271-      text: "test",
272-      sources: ["openstreetmap", "openaddresses"]
273-    })
274-
275-    let url = getUrl(_calls)
276-    t->expect(url->String.includes("sources=openstreetmap%2Copenaddresses") || url->String.includes("sources=openstreetmap,openaddresses"))->toEqual(true)
277-  })
278-
279-  testAsync("encodes boundary country as comma-separated", async t => {
280-    let _calls = setupMockWithCapture()
281-
282-    let _result = await Stadia.Autocomplete.search(~params={
283-      text: "test",
284-      boundaryCountry: ["GBR", "FRA"]
285-    })
286-
287-    let url = getUrl(_calls)
288-    t->expect(url->String.includes("boundary.country=GBR%2CFRA") || url->String.includes("boundary.country=GBR,FRA"))->toEqual(true)
289-  })
290-
291-  testAsync("encodes boundary gid", async t => {
292-    let _calls = setupMockWithCapture()
293-
294-    let _result = await Stadia.Autocomplete.search(~params={
295-      text: "test",
296-      boundaryGid: "whosonfirst:locality:101748355"
297-    })
298-
299-    let url = getUrl(_calls)
300-    t->expect(url->String.includes("boundary.gid=whosonfirst%3Alocality%3A101748355"))->toEqual(true)
301-  })
302-
303-  testAsync("encodes size parameter", async t => {
304-    let _calls = setupMockWithCapture()
305-
306-    let _result = await Stadia.Autocomplete.search(~params={
307-      text: "test",
308-      size: 5
309-    })
310-
311-    let url = getUrl(_calls)
312-    t->expect(url->String.includes("size=5"))->toEqual(true)
313-  })
314-
315-  testAsync("encodes lang parameter", async t => {
316-    let _calls = setupMockWithCapture()
317-
318-    let _result = await Stadia.Autocomplete.search(~params={
319-      text: "test",
320-      lang: "en"
321-    })
322-
323-    let url = getUrl(_calls)
324-    t->expect(url->String.includes("lang=en"))->toEqual(true)
325-  })
326-
327-  testAsync("includes api_key in URL when env var is present", async t => {
328-    // This test documents that the API key from VITE_STADIA_KEY will be included
329-    // We cannot easily mock import.meta.env in vitest, but we verify the URL construction
330-    // logic works by checking the actual URL format via the capture mechanism
331-    // The implementation reads from import.meta.env.VITE_STADIA_KEY
332-
333-    // For this test, we simply verify that if an API key were present,
334-    // the query string construction would handle it properly
335-    // (We test query string construction in other tests)
336-
337-    let _calls = setupMockWithCapture()
338-
339-    let _result = await Stadia.Autocomplete.search(~params={
340-      text: "test"
341-    })
342-
343-    // Verify the URL was constructed and fetch was called
344-    let url = getUrl(_calls)
345-    t->expect(url->String.length > 0)->toEqual(true)
346-    t->expect(url->String.includes("https://api.stadiamaps.com/geocoding/v2/autocomplete"))->toEqual(true)
347-  })
348-
349-  testAsync("handles all layer types correctly", async t => {
350-    let testCases = [
351-      ("poi", #poi),
352-      ("address", #address),
353-      ("street", #street),
354-      ("country", #country),
355-      ("macroregion", #macroregion),
356-      ("region", #region),
357-      ("macrocounty", #macrocounty),
358-      ("county", #county),
359-      ("locality", #locality),
360-      ("localadmin", #localadmin),
361-      ("borough", #borough),
362-      ("neighbourhood", #neighbourhood),
363-      ("postalcode", #postalcode),
364-      ("coarse", #coarse),
365-    ]
366-
367-    let features = testCases->Array.mapWithIndex(((layerStr, _layerVariant), idx) => {
368-      JSON.Encode.object(Dict.fromArray([
369-        ("type", JSON.Encode.string("Feature")),
370-        ("geometry", JSON.Encode.null),
371-        ("properties", JSON.Encode.object(Dict.fromArray([
372-          ("name", JSON.Encode.string(`Test ${idx->Int.toString}`)),
373-          ("gid", JSON.Encode.string(`test:${idx->Int.toString}`)),
374-          ("layer", JSON.Encode.string(layerStr)),
375-          ("coarse_location", JSON.Encode.string("Test City"))
376-        ])))
377-      ]))
378-    })
379-
380-    let responseJson = JSON.Encode.object(Dict.fromArray([
381-      ("type", JSON.Encode.string("FeatureCollection")),
382-      ("geocoding", JSON.Encode.object(Dict.fromArray([
383-        ("attribution", JSON.Encode.string("test"))
384-      ]))),
385-      ("features", JSON.Encode.array(features))
386-    ]))
387-    mockFetchJson(responseJson, 200, "OK")
388-
389-    let result = await Stadia.Autocomplete.search(~params={
390-      text: "test"
391-    })
392-
393-    switch result {
394-    | Results(results) => {
395-        t->expect(results->Array.length)->toEqual(testCases->Array.length)
396-        testCases->Array.forEachWithIndex(((_, expectedLayer), idx) => {
397-          t->expect(results[idx]->Option.map(r => r.layer))->toEqual(Some(expectedLayer))
398-        })
399-      }
400-    | _ => t->expect(true)->toEqual(false)
401-    }
402-  })
403-})
404-
405-describe("Stadia.Place", () => {
406-  testAsync("returns Found with coordinates for valid gid", async t => {
407-    let responseJson = JSON.Encode.object(Dict.fromArray([
408-      ("type", JSON.Encode.string("FeatureCollection")),
409-      ("geocoding", JSON.Encode.object(Dict.fromArray([
410-        ("attribution", JSON.Encode.string("https://stadiamaps.com/attribution/"))
411-      ]))),
412-      ("features", JSON.Encode.array([
413-        JSON.Encode.object(Dict.fromArray([
414-          ("type", JSON.Encode.string("Feature")),
415-          ("geometry", JSON.Encode.object(Dict.fromArray([
416-            ("type", JSON.Encode.string("Point")),
417-            ("coordinates", JSON.Encode.array([
418-              JSON.Encode.float(24.753),
419-              JSON.Encode.float(59.437)
420-            ]))
421-          ]))),
422-          ("properties", JSON.Encode.object(Dict.fromArray([
423-            ("name", JSON.Encode.string("Põhja Puiestee 27")),
424-            ("gid", JSON.Encode.string("openaddresses:address:ee/countrywide-addresses-country:5cbc00647aa5c976")),
425-            ("layer", JSON.Encode.string("address")),
426-            ("coarse_location", JSON.Encode.string("Tallinn, Harju, Estonia"))
427-          ])))
428-        ]))
429-      ]))
430-    ]))
431-    mockFetchJson(responseJson, 200, "OK")
432-
433-    let result = await Stadia.Place.getDetails(~params={
434-      gid: "openaddresses:address:ee/countrywide-addresses-country:5cbc00647aa5c976"
435-    })
436-
437-    switch result {
438-    | Found(place) => {
439-        t->expect(place.name)->toEqual("Põhja Puiestee 27")
440-        t->expect(place.gid)->toEqual("openaddresses:address:ee/countrywide-addresses-country:5cbc00647aa5c976")
441-        t->expect(place.layer)->toEqual(#address)
442-        t->expect(place.coarseLocation)->toEqual("Tallinn, Harju, Estonia")
443-        let (lon, lat) = place.coordinates
444-        t->expect(lon)->toEqual(24.753)
445-        t->expect(lat)->toEqual(59.437)
446-      }
447-    | _ => t->expect(true)->toEqual(false) // Should be Found
448-    }
449-  })
450-
451-  testAsync("returns NotFound for empty features", async t => {
452-    let responseJson = JSON.Encode.object(Dict.fromArray([
453-      ("type", JSON.Encode.string("FeatureCollection")),
454-      ("geocoding", JSON.Encode.object(Dict.fromArray([
455-        ("attribution", JSON.Encode.string("test"))
456-      ]))),
457-      ("features", JSON.Encode.array([]))
458-    ]))
459-    mockFetchJson(responseJson, 200, "OK")
460-
461-    let result = await Stadia.Place.getDetails(~params={
462-      gid: "nonexistent:123"
463-    })
464-
465-    t->expect(result)->toEqual(Stadia.Place.NotFound)
466-  })
467-
468-  testAsync("returns ServerError on HTTP error", async t => {
469-    let errorJson = JSON.Encode.object(Dict.fromArray([
470-      ("error", JSON.Encode.string("Not found"))
471-    ]))
472-    mockFetchJson(errorJson, 404, "Not Found")
473-
474-    let result = await Stadia.Place.getDetails(~params={
475-      gid: "test:123"
476-    })
477-
478-    switch result {
479-    | ServerError(msg) => t->expect(msg)->toEqual("404: Not found")
480-    | _ => t->expect(true)->toEqual(false)
481-    }
482-  })
483-
484-  testAsync("returns ServerError on geocoding error", async t => {
485-    let responseJson = JSON.Encode.object(Dict.fromArray([
486-      ("type", JSON.Encode.string("FeatureCollection")),
487-      ("geocoding", JSON.Encode.object(Dict.fromArray([
488-        ("attribution", JSON.Encode.string("test")),
489-        ("error", JSON.Encode.string("Invalid GID format"))
490-      ]))),
491-      ("features", JSON.Encode.array([]))
492-    ]))
493-    mockFetchJson(responseJson, 200, "OK")
494-
495-    let result = await Stadia.Place.getDetails(~params={
496-      gid: "invalid"
497-    })
498-
499-    switch result {
500-    | ServerError(msg) => t->expect(msg)->toEqual("Invalid GID format")
501-    | _ => t->expect(true)->toEqual(false)
502-    }
503-  })
504-
505-  testAsync("encodes gid in ids parameter", async t => {
506-    let _calls = setupMockWithCapture()
507-
508-    let _result = await Stadia.Place.getDetails(~params={
509-      gid: "openstreetmap:address:way/123"
510-    })
511-
512-    let url = getUrl(_calls)
513-    t->expect(url->String.includes("ids=openstreetmap%3Aaddress%3Away%2F123"))->toEqual(true)
514-  })
515-
516-  testAsync("encodes lang parameter", async t => {
517-    let _calls = setupMockWithCapture()
518-
519-    let _result = await Stadia.Place.getDetails(~params={
520-      gid: "test:123",
521-      lang: "en"
522-    })
523-
524-    let url = getUrl(_calls)
525-    t->expect(url->String.includes("lang=en"))->toEqual(true)
526-  })
527-
528-  testAsync("handles all layer types", async t => {
529-    let testCases = [
530-      ("poi", #poi),
531-      ("address", #address),
532-      ("street", #street),
533-      ("country", #country),
534-      ("macroregion", #macroregion),
535-      ("region", #region),
536-      ("macrocounty", #macrocounty),
537-      ("county", #county),
538-      ("locality", #locality),
539-      ("localadmin", #localadmin),
540-      ("borough", #borough),
541-      ("neighbourhood", #neighbourhood),
542-      ("postalcode", #postalcode),
543-      ("coarse", #coarse),
544-    ]
545-
546-    let features = testCases->Array.mapWithIndex(((layerStr, _layerVariant), idx) => {
547-      JSON.Encode.object(Dict.fromArray([
548-        ("type", JSON.Encode.string("Feature")),
549-        ("geometry", JSON.Encode.object(Dict.fromArray([
550-          ("type", JSON.Encode.string("Point")),
551-          ("coordinates", JSON.Encode.array([
552-            JSON.Encode.float(idx->Int.toFloat),
553-            JSON.Encode.float(idx->Int.toFloat)
554-          ]))
555-        ]))),
556-        ("properties", JSON.Encode.object(Dict.fromArray([
557-          ("name", JSON.Encode.string(`Test ${idx->Int.toString}`)),
558-          ("gid", JSON.Encode.string(`test:${idx->Int.toString}`)),
559-          ("layer", JSON.Encode.string(layerStr)),
560-          ("coarse_location", JSON.Encode.string("Test City"))
561-        ])))
562-      ]))
563-    })
564-
565-    let firstFeature = switch features[0] {
566-    | Some(f) => f
567-    | None => JSON.Encode.null
568-    }
569-    let responseJson = JSON.Encode.object(Dict.fromArray([
570-      ("type", JSON.Encode.string("FeatureCollection")),
571-      ("geocoding", JSON.Encode.object(Dict.fromArray([
572-        ("attribution", JSON.Encode.string("test"))
573-      ]))),
574-      ("features", JSON.Encode.array([firstFeature]))
575-    ]))
576-    mockFetchJson(responseJson, 200, "OK")
577-
578-    let result = await Stadia.Place.getDetails(~params={
579-      gid: "test:0"
580-    })
581-
582-    switch result {
583-    | Found(place) => t->expect(place.layer)->toEqual(#poi)
584-    | _ => t->expect(true)->toEqual(false)
585-    }
586-  })
587-})