// Copyright 2026 moonbitGEODB contributors
// SPDX-License-Identifier: Apache-2.0
//
// CLI demo: store, parse, and retrieve address information.
//
// Usage:
//   moon run cmd                           # run default demo
//   moon run cmd 
# parse & store a custom address // moon run cmd search # search by name // moon run cmd fuzzy [max_dist] # fuzzy name search // moon run cmd nearby [km] # find nearby within radius // moon run cmd within # find within distance (geofence) // moon run cmd tag # find entries by tag // moon run cmd tags # list all unique tags // moon run cmd province # find by province // moon run cmd city # find by city // moon run cmd district # find by district // moon run cmd stats # show geographic statistics // moon run cmd export [path] # export DB to CSV // moon run cmd add-tag # add tag to entry // moon run cmd remove-tag # remove tag from entry // moon run cmd clear # clear all entries // moon run cmd seed [n] # seed random addresses // moon run cmd save # persist DB to file // moon run cmd load # load DB from file // moon run cmd backup # backup DB file // moon run cmd check # integrity check // moon run cmd info # show DB metadata // moon run cmd to-json [path] # export DB to JSON // moon run cmd to-geojson [path] # export DB to GeoJSON // moon run cmd pattern # wildcard search by name (*, ?) // moon run cmd distance # compute distance between two entries // moon run cmd neighbors [k] # find k nearest neighbors of an entry // moon run cmd nearest-pairs [k] # show k closest pairs in DB // moon run cmd districts [province] # district distribution // moon run cmd tag-count # count entries with a specific tag // moon run cmd tag-pairs # show tag co-occurrence pairs // moon run cmd rename # rename an entry // moon run cmd update-coords # update entry coordinates // moon run cmd centroid # centroid of entries sharing a tag // moon run cmd import-csv # import entries from CSV string // moon run cmd import-geojson # import entries from GeoJSON string // moon run cmd duplicates [dist] [edit] # find duplicate entries // moon run cmd cluster [cell_size_km] # grid-based spatial clustering // moon run cmd matrix ... # distance matrix for IDs ///| fn build_usage() -> String { let s1 = "moonbitGEODB - Geographic Information Database CLI\n" let s2 = s1 + "\n" let s3 = s2 + "Usage:\n" let s4 = s3 + " moon run cmd Run the default demo\n" let s5 = s4 + " moon run cmd
Parse and store an address\n" let s6 = s5 + " moon run cmd search Search entries by name keyword\n" let s7 = s6 + " moon run cmd fuzzy [max_dist] Fuzzy search by name (Levenshtein)\n" let s8 = s7 + " moon run cmd nearby [km] Find nearby entries (default 5km)\n" let s9 = s8 + " moon run cmd within Find entries within distance\n" let s10 = s9 + " moon run cmd tag Find entries by tag\n" let s11 = s10 + " moon run cmd tags List all unique tags\n" let s12 = s11 + " moon run cmd province Find entries by province\n" let s13 = s12 + " moon run cmd city Find entries by city\n" let s14 = s13 + " moon run cmd district Find entries by district\n" let s15 = s14 + " moon run cmd stats Show geographic statistics\n" let s16 = s15 + " moon run cmd export [path] Export DB to CSV file\n" let s17 = s16 + " moon run cmd add-tag Add tag to an entry\n" let s18 = s17 + " moon run cmd remove-tag Remove tag from an entry\n" let s19 = s18 + " moon run cmd clear Clear all entries\n" let s20 = s19 + " moon run cmd seed [n] Seed random addresses (default 100)\n" let s21 = s20 + " moon run cmd save Persist DB to a file\n" let s22 = s21 + " moon run cmd load Load DB from a file\n" let s23 = s22 + " moon run cmd backup Create backup of DB file\n" let s24 = s23 + " moon run cmd check Check DB file integrity\n" let s25 = s24 + " moon run cmd info Show DB metadata\n" let s26 = s25 + " moon run cmd to-json [path] Export DB to JSON\n" let s27 = s26 + " moon run cmd to-geojson [path] Export DB to GeoJSON\n" let s28 = s27 + " moon run cmd pattern Wildcard search (*, ?)\n" let s29 = s28 + " moon run cmd distance Distance between two entries\n" let s30 = s29 + " moon run cmd neighbors [k] k nearest neighbors\n" let s31 = s30 + " moon run cmd nearest-pairs [k] k closest pairs in DB\n" let s32 = s31 + " moon run cmd districts [province] District distribution\n" let s33 = s32 + " moon run cmd tag-count Count entries with tag\n" let s34 = s33 + " moon run cmd tag-pairs Tag co-occurrence pairs\n" let s35 = s34 + " moon run cmd rename Rename an entry\n" let s36 = s35 + " moon run cmd update-coords \n" let s37 = s36 + " moon run cmd centroid Centroid of tagged entries\n" let s38 = s37 + " moon run cmd import-csv Import entries from CSV string\n" let s39 = s38 + " moon run cmd import-geojson Import entries from GeoJSON string\n" let s40 = s39 + " moon run cmd duplicates [dist] [edit] Find duplicate entries\n" let s41 = s40 + " moon run cmd cluster [cell_size_km] Grid-based spatial clustering\n" let s42 = s41 + " moon run cmd matrix [id2 ...] Distance matrix for IDs\n" let s50 = s42 + " moon run cmd ring-query Ring/buffer-zone query\n" let s51 = s50 + " moon run cmd distance-histogram [bins]\n" let s52 = s51 + " moon run cmd coord-histogram [bins] Coordinate histogram\n" let s53 = s52 + " moon run cmd distance-summary [lat] [lng] Distance distribution summary\n" let s54 = s53 + " moon run cmd nn-range [k] k nearest neighbors distance range\n" let s55 = s54 + " moon run cmd voronoi Voronoi diagram of all entries\n" let s56 = s55 + " moon run cmd voronoi-cell Voronoi polygon for a single entry\n" let s57 = s56 + " moon run cmd voronoi-neighbors Voronoi neighbors of an entry\n" let s58 = s57 + " moon run cmd morans-i-lat Global Moran's I on latitude\n" let s59 = s58 + " moon run cmd morans-i-lng Global Moran's I on longitude\n" let s60 = s59 + " moon run cmd hotspot-lat Getis-Ord Gi* hot/cold spots on latitude\n" let s61 = s60 + " moon run cmd hotspot-lng Getis-Ord Gi* hot/cold spots on longitude\n" let s62 = s61 + " moon run cmd lisa-lat Local Moran's I (LISA) on latitude\n" let s63 = s62 + " moon run cmd lisa-lng Local Moran's I (LISA) on longitude\n" let s64 = s63 + " moon run cmd delaunay Delaunay triangulation of all entries\n" let s65 = s64 + " moon run cmd delaunay-edges Delaunay edges (unique) of all entries\n" let s66 = s65 + " moon run cmd kriging [k] Kriging interpolation at query point\n" let s67 = s66 + " moon run cmd addr-match Semantic address match (exact/partial/none)\n" let s68 = s67 + " moon run cmd addr-eval [n] Evaluate on labelled address corpus\n" let s69 = s68 + " moon run cmd corpus-db Build spatial DB from address corpus\n" let s70 = s69 + " moon run cmd idw [power] IDW spatial interpolation\n" let s71 = s70 + " moon run cmd concave-hull Concave hull (alpha-shape) boundary\n" let s72 = s71 + " moon run cmd kmeans K-Means clustering\n" let s73 = s72 + " moon run cmd dbscan DBSCAN density clustering\n" let s74 = s73 + " moon run cmd sde Standard deviational ellipse\n" let s75 = s74 + "\n" let s76 = s75 + "Examples:\n" let s77 = s76 + " moon run cmd\n" let s78 = s77 + " moon run cmd \"北京市海淀区颐和园路5号\"\n" let s79 = s78 + " moon run cmd search \"颐和园\"\n" let s80 = s79 + " moon run cmd fuzzy \"Yuan\" 2\n" let s81 = s80 + " moon run cmd tag \"poi\"\n" let s82 = s81 + " moon run cmd province \"北京市\"\n" let s83 = s82 + " moon run cmd stats\n" let s84 = s83 + " moon run cmd export output.csv\n" let s85 = s84 + " moon run cmd seed 500\n" let s86 = s85 + " moon run cmd save geo.db\n" let s87 = s86 + " moon run cmd to-json out.json\n" let s88 = s87 + " moon run cmd cluster 5.0\n" let s89 = s88 + " moon run cmd ring-query 39.9 116.4 1.0 5.0\n" let s90 = s89 + " moon run cmd distance-histogram 39.9 116.4 50.0 10\n" let s91 = s90 + " moon run cmd nn-range bj-yiheyuan 5\n" let s92 = s91 + " moon run cmd morans-i-lat\n" let s93 = s92 + " moon run cmd hotspot-lat\n" let s94 = s93 + " moon run cmd lisa-lat\n" let s95 = s94 + " moon run cmd dbscan 50.0 3\n" let s96 = s95 + " moon run cmd addr-match \"汉中路180号星汉大厦\" \"汉中路180号星汉大厦26层A\"\n" s96 } ///| fn main { let args = @env.args() if args.length() <= 1 { run_demo() return } let cmd = args.get(1).unwrap_or("") if cmd == "help" || cmd == "--help" || cmd == "-h" { println(build_usage()) return } if cmd == "search" { if args.length() < 3 { println("Error: search requires a keyword argument") println("Usage: moon run cmd search ") return } run_search(args.get(2).unwrap()) return } if cmd == "fuzzy" { if args.length() < 3 { println("Error: fuzzy requires a keyword argument") println("Usage: moon run cmd fuzzy [max_distance]") return } let max_dist = if args.length() > 3 { @string.parse_int(args.get(3).unwrap()) catch { _ => 2 } } else { 2 } run_fuzzy(args.get(2).unwrap(), max_dist) return } if cmd == "nearby" { if args.length() < 4 { println("Error: nearby requires lat, lng, and optional km arguments") println("Usage: moon run cmd nearby [km]") return } let lat_str = args.get(2).unwrap() let lng_str = args.get(3).unwrap() let lat = @string.parse_double(lat_str) catch { _ => { println("Error: invalid latitude: " + lat_str) return } } let lng = @string.parse_double(lng_str) catch { _ => { println("Error: invalid longitude: " + lng_str) return } } let km = if args.length() > 4 { @string.parse_double(args.get(4).unwrap()) catch { _ => 5.0 } } else { 5.0 } run_nearby(lat, lng, km) return } if cmd == "within" { if args.length() < 5 { println("Error: within requires lat, lng, and km arguments") println("Usage: moon run cmd within ") return } let lat = @string.parse_double(args.get(2).unwrap()) catch { _ => { println("Error: invalid latitude") return } } let lng = @string.parse_double(args.get(3).unwrap()) catch { _ => { println("Error: invalid longitude") return } } let km = @string.parse_double(args.get(4).unwrap()) catch { _ => { println("Error: invalid distance") return } } run_within(lat, lng, km) return } if cmd == "tag" { if args.length() < 3 { println("Error: tag requires a tag argument") println("Usage: moon run cmd tag ") return } run_tag(args.get(2).unwrap()) return } if cmd == "tags" { run_list_tags() return } if cmd == "province" { if args.length() < 3 { println("Error: province requires a province name") println("Usage: moon run cmd province ") return } run_province(args.get(2).unwrap()) return } if cmd == "city" { if args.length() < 3 { println("Error: city requires a city name") println("Usage: moon run cmd city ") return } run_city(args.get(2).unwrap()) return } if cmd == "district" { if args.length() < 3 { println("Error: district requires a district name") println("Usage: moon run cmd district ") return } run_district(args.get(2).unwrap()) return } if cmd == "stats" { run_stats() return } if cmd == "export" { let path = if args.length() > 2 { args.get(2).unwrap() } else { "" } run_export(path) return } if cmd == "add-tag" { if args.length() < 4 { println("Error: add-tag requires id and tag") println("Usage: moon run cmd add-tag ") return } run_add_tag(args.get(2).unwrap(), args.get(3).unwrap()) return } if cmd == "remove-tag" { if args.length() < 4 { println("Error: remove-tag requires id and tag") println("Usage: moon run cmd remove-tag ") return } run_remove_tag(args.get(2).unwrap(), args.get(3).unwrap()) return } if cmd == "clear" { run_clear() return } if cmd == "seed" { let n = if args.length() > 2 { @string.parse_int(args.get(2).unwrap()) catch { _ => 100 } } else { 100 } run_seed(n) return } if cmd == "save" { if args.length() < 3 { println("Error: save requires a file path") println("Usage: moon run cmd save ") return } run_save(args.get(2).unwrap()) return } if cmd == "load" { if args.length() < 3 { println("Error: load requires a file path") println("Usage: moon run cmd load ") return } run_load(args.get(2).unwrap()) return } if cmd == "backup" { if args.length() < 3 { println("Error: backup requires a file path") println("Usage: moon run cmd backup ") return } run_backup(args.get(2).unwrap()) return } if cmd == "check" { if args.length() < 3 { println("Error: check requires a file path") println("Usage: moon run cmd check ") return } run_check(args.get(2).unwrap()) return } if cmd == "info" { if args.length() < 3 { println("Error: info requires a file path") println("Usage: moon run cmd info ") return } run_info(args.get(2).unwrap()) return } if cmd == "to-json" { let path = if args.length() > 2 { args.get(2).unwrap() } else { "" } run_to_json(path) return } if cmd == "to-geojson" { let path = if args.length() > 2 { args.get(2).unwrap() } else { "" } run_to_geojson(path) return } if cmd == "pattern" { if args.length() < 3 { println("Error: pattern requires a pattern argument") println( "Usage: moon run cmd pattern (use * and ? as wildcards)", ) return } run_pattern(args.get(2).unwrap()) return } if cmd == "distance" { if args.length() < 4 { println("Error: distance requires two ids") println("Usage: moon run cmd distance ") return } run_distance(args.get(2).unwrap(), args.get(3).unwrap()) return } if cmd == "neighbors" { if args.length() < 3 { println("Error: neighbors requires an id") println("Usage: moon run cmd neighbors [k]") return } let k = if args.length() > 3 { @string.parse_int(args.get(3).unwrap()) catch { _ => 5 } } else { 5 } run_neighbors(args.get(2).unwrap(), k) return } if cmd == "nearest-pairs" { let k = if args.length() > 2 { @string.parse_int(args.get(2).unwrap()) catch { _ => 5 } } else { 5 } run_nearest_pairs(k) return } if cmd == "districts" { let province = if args.length() > 2 { args.get(2).unwrap() } else { "" } run_districts(province) return } if cmd == "tag-count" { if args.length() < 3 { println("Error: tag-count requires a tag") println("Usage: moon run cmd tag-count ") return } run_tag_count(args.get(2).unwrap()) return } if cmd == "tag-pairs" { run_tag_pairs() return } if cmd == "rename" { if args.length() < 4 { println("Error: rename requires id and new name") println("Usage: moon run cmd rename ") return } run_rename(args.get(2).unwrap(), args.get(3).unwrap()) return } if cmd == "update-coords" { if args.length() < 5 { println("Error: update-coords requires id, lat, lng") println("Usage: moon run cmd update-coords ") return } let lat = @string.parse_double(args.get(3).unwrap()) catch { _ => { println("Error: invalid latitude") return } } let lng = @string.parse_double(args.get(4).unwrap()) catch { _ => { println("Error: invalid longitude") return } } run_update_coords(args.get(2).unwrap(), lat, lng) return } if cmd == "centroid" { if args.length() < 3 { println("Error: centroid requires a tag") println("Usage: moon run cmd centroid ") return } run_centroid(args.get(2).unwrap()) return } if cmd == "import-csv" { if args.length() < 3 { println("Error: import-csv requires a CSV string argument") println("Usage: moon run cmd import-csv ") return } // Rejoin remaining args as the CSV string. let csv_parts : Array[String] = [] let mut ci = 2 while ci < args.length() { csv_parts.push(args.get(ci).unwrap()) ci = ci + 1 } run_import_csv(csv_parts.join(" ")) return } if cmd == "import-geojson" { if args.length() < 3 { println("Error: import-geojson requires a GeoJSON string argument") println("Usage: moon run cmd import-geojson ") return } let gj_parts : Array[String] = [] let mut gi = 2 while gi < args.length() { gj_parts.push(args.get(gi).unwrap()) gi = gi + 1 } run_import_geojson(gj_parts.join(" ")) return } if cmd == "duplicates" { let dist = if args.length() > 2 { @string.parse_double(args.get(2).unwrap()) catch { _ => 0.1 } } else { 0.1 } let edit = if args.length() > 3 { @string.parse_int(args.get(3).unwrap()) catch { _ => 2 } } else { 2 } run_duplicates(dist, edit) return } if cmd == "cluster" { let cell_size = if args.length() > 2 { @string.parse_double(args.get(2).unwrap()) catch { _ => 1.0 } } else { 1.0 } run_cluster(cell_size) return } if cmd == "dbscan" { let eps = if args.length() > 2 { @string.parse_double(args.get(2).unwrap()) catch { _ => 50.0 } } else { 50.0 } let min_points = if args.length() > 3 { @string.parse_int(args.get(3).unwrap()) catch { _ => 3 } } else { 3 } run_dbscan(eps, min_points) return } if cmd == "addr-match" { if args.length() < 4 { println("Error: addr-match requires query and candidate text") println("Usage: moon run cmd addr-match ") return } run_addr_match(args.get(2).unwrap(), args.get(3).unwrap()) return } if cmd == "addr-eval" { let path = if args.length() > 2 { args.get(2).unwrap() } else { "testdata/data.txt" } let max_records = if args.length() > 3 { @string.parse_int(args.get(3).unwrap()) catch { _ => 0 } } else { 0 } run_addr_eval(path, max_records) return } if cmd == "addr-feat-dump" { let path = if args.length() > 2 { args.get(2).unwrap() } else { "testdata/data.txt" } run_addr_feat_dump(path) return } if cmd == "corpus-db" { let n = if args.length() > 2 { @string.parse_int(args.get(2).unwrap()) catch { _ => 1000 } } else { 1000 } run_corpus_db(n) return } if cmd == "matrix" { let ids : Array[String] = [] let mut mi = 2 while mi < args.length() { ids.push(args.get(mi).unwrap()) mi = mi + 1 } if ids.length() < 2 { println("Error: matrix requires at least 2 IDs") println("Usage: moon run cmd matrix [id3 ...]") return } run_matrix(ids) return } if cmd == "reverse-geocode" { if args.length() < 4 { println("Error: reverse-geocode requires lat and lng") println("Usage: moon run cmd reverse-geocode ") return } let lat = @string.parse_double(args.get(2).unwrap()) catch { _ => { println("Error: invalid latitude") return } } let lng = @string.parse_double(args.get(3).unwrap()) catch { _ => { println("Error: invalid longitude") return } } run_reverse_geocode(lat, lng) return } if cmd == "tag-all" { if args.length() < 3 { println("Error: tag-all requires at least one tag") println("Usage: moon run cmd tag-all [tag2 ...]") return } let tags : Array[String] = [] let mut ti = 2 while ti < args.length() { tags.push(args.get(ti).unwrap()) ti = ti + 1 } run_tag_all(tags) return } if cmd == "tag-any" { if args.length() < 3 { println("Error: tag-any requires at least one tag") println("Usage: moon run cmd tag-any [tag2 ...]") return } let tags : Array[String] = [] let mut ti = 2 while ti < args.length() { tags.push(args.get(ti).unwrap()) ti = ti + 1 } run_tag_any(tags) return } if cmd == "bbox-count" { if args.length() < 6 { println("Error: bbox-count requires min_lat, max_lat, min_lng, max_lng") println( "Usage: moon run cmd bbox-count ", ) return } let min_lat = @string.parse_double(args.get(2).unwrap()) catch { _ => { println("Error: invalid min_lat") return } } let max_lat = @string.parse_double(args.get(3).unwrap()) catch { _ => { println("Error: invalid max_lat") return } } let min_lng = @string.parse_double(args.get(4).unwrap()) catch { _ => { println("Error: invalid min_lng") return } } let max_lng = @string.parse_double(args.get(5).unwrap()) catch { _ => { println("Error: invalid max_lng") return } } run_bbox_count(min_lat, max_lat, min_lng, max_lng) return } if cmd == "within-radius" { if args.length() < 5 { println("Error: within-radius requires lat, lng, and radius_km") println("Usage: moon run cmd within-radius ") return } let lat = @string.parse_double(args.get(2).unwrap()) catch { _ => { println("Error: invalid latitude") return } } let lng = @string.parse_double(args.get(3).unwrap()) catch { _ => { println("Error: invalid longitude") return } } let radius = @string.parse_double(args.get(4).unwrap()) catch { _ => { println("Error: invalid radius") return } } run_within_radius(lat, lng, radius) return } if cmd == "density" { let cell_size = if args.length() > 2 { @string.parse_double(args.get(2).unwrap()) catch { _ => 1.0 } } else { 1.0 } run_density(cell_size) return } if cmd == "stddev" { run_stddev() return } if cmd == "histogram" { run_histogram() return } if cmd == "ring-query" { if args.length() < 6 { println("Error: ring-query requires lat, lng, inner_km, outer_km") println( "Usage: moon run cmd ring-query ", ) return } let lat = @string.parse_double(args.get(2).unwrap()) catch { _ => { println("Error: invalid latitude") return } } let lng = @string.parse_double(args.get(3).unwrap()) catch { _ => { println("Error: invalid longitude") return } } let inner = @string.parse_double(args.get(4).unwrap()) catch { _ => { println("Error: invalid inner radius") return } } let outer = @string.parse_double(args.get(5).unwrap()) catch { _ => { println("Error: invalid outer radius") return } } run_ring_query(lat, lng, inner, outer) return } if cmd == "distance-histogram" { if args.length() < 6 { println("Error: distance-histogram requires lat, lng, max_km, [bins]") println( "Usage: moon run cmd distance-histogram [bins]", ) return } let lat = @string.parse_double(args.get(2).unwrap()) catch { _ => { println("Error: invalid latitude") return } } let lng = @string.parse_double(args.get(3).unwrap()) catch { _ => { println("Error: invalid longitude") return } } let max_km = @string.parse_double(args.get(4).unwrap()) catch { _ => { println("Error: invalid max distance") return } } let bins = if args.length() > 5 { @string.parse_int(args.get(5).unwrap()) catch { _ => 10 } } else { 10 } run_distance_histogram(lat, lng, max_km, bins) return } if cmd == "coord-histogram" { if args.length() < 3 { println("Error: coord-histogram requires 'lat' or 'lng'") println("Usage: moon run cmd coord-histogram [bins]") return } let axis = args.get(2).unwrap() let bins = if args.length() > 3 { @string.parse_int(args.get(3).unwrap()) catch { _ => 10 } } else { 10 } run_coord_histogram(axis, bins) return } if cmd == "distance-summary" { let (lat, lng) = if args.length() >= 4 { ( @string.parse_double(args.get(2).unwrap()) catch { _ => 0.0 }, @string.parse_double(args.get(3).unwrap()) catch { _ => 0.0 }, ) } else { (0.0, 0.0) } run_distance_summary(lat, lng, args.length() >= 4) return } if cmd == "nn-range" { if args.length() < 3 { println("Error: nn-range requires an id") println("Usage: moon run cmd nn-range [k]") return } let k = if args.length() > 3 { @string.parse_int(args.get(3).unwrap()) catch { _ => 5 } } else { 5 } run_nn_range(args.get(2).unwrap(), k) return } if cmd == "kmeans" { if args.length() < 3 { println("Error: kmeans requires k") println("Usage: moon run cmd kmeans [max_iter]") return } let k = @string.parse_int(args.get(2).unwrap()) catch { _ => { println("Error: invalid k value") return } } let max_iter = if args.length() > 3 { @string.parse_int(args.get(3).unwrap()) catch { _ => 100 } } else { 100 } run_kmeans(k, max_iter) return } if cmd == "elbow" { let max_k = if args.length() > 2 { @string.parse_int(args.get(2).unwrap()) catch { _ => 8 } } else { 8 } run_elbow(max_k) return } if cmd == "centroid-all" { run_all_centroid() return } if cmd == "geographic-center" { run_geographic_center() return } if cmd == "bbox-query" { if args.length() < 6 { println("Error: bbox-query requires min_lat max_lat min_lng max_lng") println( "Usage: moon run cmd bbox-query ", ) return } let min_lat = @string.parse_double(args.get(2).unwrap()) catch { _ => { println("Error: invalid min_lat") return } } let max_lat = @string.parse_double(args.get(3).unwrap()) catch { _ => { println("Error: invalid max_lat") return } } let min_lng = @string.parse_double(args.get(4).unwrap()) catch { _ => { println("Error: invalid min_lng") return } } let max_lng = @string.parse_double(args.get(5).unwrap()) catch { _ => { println("Error: invalid max_lng") return } } run_bbox_query(min_lat, max_lat, min_lng, max_lng) return } if cmd == "moran-i" { run_morans_i() return } if cmd == "sde" { run_std_dev_ellipse() return } if cmd == "tsp" { if args.length() < 3 { println("Error: tsp requires start_id") println("Usage: moon run cmd tsp ") return } run_tsp(args.get(2).unwrap()) return } if cmd == "benchmark" { let n = if args.length() >= 3 { try { @string.parse_int(args.get(2).unwrap()) } catch { _ => 1000 } } else { 1000 } run_benchmark(n) return } if cmd == "tsp-2opt" { if args.length() < 3 { println("Error: tsp-2opt requires start_id") println("Usage: moon run cmd tsp-2opt ") return } run_tsp_2opt(args.get(2).unwrap()) return } if cmd == "idw" { if args.length() < 4 { println("Error: idw requires lat lng [power]") println("Usage: moon run cmd idw [power]") return } let power_str = if args.length() >= 5 { args.get(4).unwrap() } else { "2.0" } run_idw(args.get(2).unwrap(), args.get(3).unwrap(), power_str) return } if cmd == "concave-hull" { if args.length() < 3 { println("Error: concave-hull requires alpha") println("Usage: moon run cmd concave-hull ") return } run_concave_hull(args.get(2).unwrap()) return } if cmd == "moran-i-knn" { run_moran_i_knn() return } if cmd == "demo" { run_demo() return } if cmd == "voronoi" { run_voronoi() return } if cmd == "voronoi-cell" { if args.length() < 3 { println("Error: voronoi-cell requires an id") println("Usage: moon run cmd voronoi-cell ") return } run_voronoi_cell(args.get(2).unwrap()) return } if cmd == "voronoi-neighbors" { if args.length() < 3 { println("Error: voronoi-neighbors requires an id") println("Usage: moon run cmd voronoi-neighbors ") return } run_voronoi_neighbors(args.get(2).unwrap()) return } if cmd == "morans-i-lat" { run_morans_i_lat() return } if cmd == "morans-i-lng" { run_morans_i_lng() return } if cmd == "hotspot-lat" { run_hotspot_lat() return } if cmd == "hotspot-lng" { run_hotspot_lng() return } if cmd == "lisa-lat" { run_lisa_lat() return } if cmd == "lisa-lng" { run_lisa_lng() return } if cmd == "delaunay" { run_delaunay() return } if cmd == "delaunay-edges" { run_delaunay_edges() return } if cmd == "kriging" { run_kriging() return } // Treat all arguments as an address string. let addr_parts : Array[String] = [] let mut i = 1 while i < args.length() { addr_parts.push(args.get(i).unwrap()) i = i + 1 } let address_str = addr_parts.join(" ") run_parse_and_store(address_str) } // ============ Demo & Core Commands ============ ///| fn run_demo() -> Unit { println("=== moonbitGEODB Demo ===") println("") let db = build_sample_db() println("Database size: " + db.size().to_string()) println("") // Retrieve: by id println("--- Retrieve by ID ---") match db.get("bj-yiheyuan") { Some(e) => print_entry(e) None => println("(not found)") } println("") // Retrieve: by name (exact) println("--- Search by name (exact) ---") let results = db.find_by_name("颐和园") for e in results { print_entry(e) } println("") // Retrieve: nearby println("--- Nearby 颐和园 (5km radius) ---") let nearby = db.find_nearby( @types.GeoPoint::make(39.9999, 116.2755), radius_km=5.0, limit=5, ) for pair in nearby { println( " " + pair.0.id + " (" + pair.0.name + ") - " + pair.1.to_string() + " km", ) } println("") // K-Means + SDE println("--- K-Means (k=3, macro regions) ---") let clusters = db.kmeans(3) let mut ci = 0 for cl in clusters { ci = ci + 1 println(" Cluster " + ci.to_string() + ": " + cl.members.length().to_string() + " pts") // Cap the member listing at 5 so the demo stays readable. let mut mi = 0 while mi < cl.members.length() && mi < 5 { println(" - " + cl.members[mi].name) mi = mi + 1 } if cl.members.length() > 5 { println(" ... 及另外 " + (cl.members.length() - 5).to_string() + " 个") } } println("") // DBSCAN — density-based city clusters, no need to pick k up front. println("--- DBSCAN (eps=50km, min_points=3, city-level clusters) ---") let dbscan_clusters = db.cluster_dbscan(50.0, 3) let mut di = 0 let mut assigned = 0 while di < dbscan_clusters.length() { let members = dbscan_clusters[di] assigned = assigned + members.length() let line = " Cluster #" + (di + 1).to_string() + ": " + members.length().to_string() + " POIs" println(line) di = di + 1 } println( " Total: " + dbscan_clusters.length().to_string() + " clusters, " + (db.size() - assigned).to_string() + " noise (far suburbs)", ) println("") println("--- Standard Deviation Ellipse ---") match db.std_dev_ellipse() { None => println(" (Need >= 3 points)") Some(sde) => { let msg = " Center (" + sde.center.lat.to_string() + ", " + sde.center.lng.to_string() + ") Semi-major " + sde.semi_major_km.to_string() + " km" println(msg) } } println("") // TSP + 2-opt — start from Beijing for a realistic long-distance tour println("--- TSP NN + 2-opt (from bj-gugong, " + db.size().to_string() + " POIs) ---") match db.tsp_nearest("bj-gugong") { None => println(" (start id not found)") Some((_, nn_tot)) => { match db.tsp_2opt("bj-gugong") { None => () Some((_, opt_tot)) => { println(" NN: " + nn_tot.to_string() + " km 2opt: " + opt_tot.to_string() + " km") println(" Saved " + (nn_tot - opt_tot).to_string() + " km") } } } } println("") // IDW println("--- IDW interpolation at (35N, 115E) ---") match db.idw_predict(@types.GeoPoint::make(35.0, 115.0), fn(e) { e.location.lng }) { None => println(" (empty DB)") Some(v) => println(" Predicted lng: " + v.to_string()) } println("") // Concave hull println("--- Concave Hull (alpha=1.5) ---") let hull = db.concave_hull(1.5) println(" Hull vertices: " + hull.length().to_string()) println("") // Kriging interpolation(普通克里金) println("--- Kriging Interpolation (35N, 115E) ---") let krig_entries = db.all() let krig_pts : Array[@types.GeoPoint] = [] let krig_vals : Array[Double] = [] for e in krig_entries { krig_pts.push(e.location) krig_vals.push(e.location.lng) } let krig_bins = @geo.empirical_variogram(krig_pts, krig_vals, max_lag_km=None, num_lags=10) let krig_fit = @geo.fit_variogram(krig_bins) println( " Variogram: range=" + krig_fit.range.to_string() + " km, sill=" + krig_fit.sill.to_string(), ) match @geo.ordinary_kriging_knn( krig_pts, krig_vals, @types.GeoPoint::make(35.0, 115.0), krig_fit, k=12, ) { None => println(" Prediction: (insufficient data)") Some(v) => println(" Predicted lng: " + v.to_string() + "°E") } println("") // Parse-only demonstration println("--- Parse-only: '广州市天河区珠江新城华夏路16号' ---") let addr = @parser.parse_address( "广州市天河区珠江新城华夏路16号", ) catch { _ => @types.Address::empty() } println(" Province: " + addr.province) println(" City: " + addr.city) println(" District: " + addr.district) println(" Street: " + addr.street) println("") println("Demo complete. Try:") println(" moon run cmd benchmark") println(" moon run cmd dbscan 50.0 3") println(" moon run cmd tsp-2opt bj-gugong") println(" moon run cmd kmeans 3") println(" moon run cmd idw 35.0 115.0 2.0") println(" moon run cmd kriging 35.0 115.0 12") println(" moon run cmd concave-hull 1.5") println(" moon run cmd moran-i-knn") } ///| fn run_seed(n : Int) -> Unit { println("=== Seeding " + n.to_string() + " random Chinese addresses ===") println("") let db = @db.GeoDB::in_memory() let entries = @gen.gen_batch(n) for e in entries { db.insert(e) } println( "Generated " + entries.length().to_string() + " entries from " + @gen.city_count().to_string() + " cities.", ) println("") // Show first 5 entries as samples. println("--- Sample entries ---") let mut i = 0 for e in entries { if i >= 5 { break } println(" [" + e.id + "] " + e.name) println(" " + e.address.to_line()) println( " lat=" + e.location.lat.to_string() + ", lng=" + e.location.lng.to_string(), ) i = i + 1 } if entries.length() > 5 { println(" ... and " + (entries.length() - 5).to_string() + " more") } println("") // Show province distribution. println("--- Province distribution ---") let prov_dist = db.province_distribution() for pair in prov_dist { println(" " + pair.0 + ": " + pair.1.to_string() + " entries") } println("") // Show coord range. println("--- Coordinate range ---") match db.compute_bbox() { None => println(" (empty)") Some((bbox, _)) => { println( " Lat: " + bbox.min_lat.to_string() + " ~ " + bbox.max_lat.to_string(), ) println( " Lng: " + bbox.min_lng.to_string() + " ~ " + bbox.max_lng.to_string(), ) } } println("") // Save to file. let out_path = "testdata/geo_seeded.db" match db.save_to(out_path) { Err(e) => println("Error saving to " + out_path + ": " + e) Ok(_) => println( "Saved " + entries.length().to_string() + " entries to " + out_path, ) } println("") // Run stats println("--- Statistics ---") println(db.summary()) } ///| fn run_parse_and_store(address_str : String) -> Unit { println("=== Parse & Store ===") println("Input: " + address_str) println("") let addr = @parser.parse_address(address_str) catch { _ => @types.Address::empty() } println("[PARSED]") println(" Country: " + addr.country) println(" Province: " + addr.province) println(" City: " + addr.city) println(" District: " + addr.district) println(" Street: " + addr.street) println(" Detail: " + addr.detail) println(" Postal Code: " + addr.postal_code) println("") let db = @db.GeoDB::in_memory() let new_id = db.gen_id("e") let name = if addr.street.length() > 0 { addr.street } else { address_str } let entry = db.insert_raw(new_id, name, 0.0, 0.0, address_str) println("[STORED] id=" + entry.id) print_entry(entry) println("") println("Database size: " + db.size().to_string()) } // ============ Search Commands ============ ///| fn run_search(keyword : String) -> Unit { println("=== Search: '" + keyword + "' ==") println("") let db = build_sample_db() let results = db.find_name_contains(keyword) if results.length() == 0 { println("No entries found matching '" + keyword + "'.") } else { println("Found " + results.length().to_string() + " entries:") for e in results { print_entry(e) } } } ///| fn run_fuzzy(keyword : String, max_dist : Int) -> Unit { println( "=== Fuzzy Search: '" + keyword + "' (max distance=" + max_dist.to_string() + ") ==", ) println("") let db = build_sample_db() let results = db.find_name_fuzzy(keyword, max_distance=max_dist) if results.length() == 0 { println("No entries found similar to '" + keyword + "'.") } else { println("Found " + results.length().to_string() + " entries:") for pair in results { let e = pair.0 let d = pair.1 println( " [distance=" + d.to_string() + "] " + e.id + " (" + e.name + ")", ) println(" Address: " + e.address.to_line()) } } } ///| fn run_nearby(lat : Double, lng : Double, km : Double) -> Unit { println("=== Nearby search ==") println( "Center: (" + lat.to_string() + ", " + lng.to_string() + "), radius: " + km.to_string() + " km", ) println("") let db = build_sample_db() let results = db.find_nearby( @types.GeoPoint::make(lat, lng), radius_km=km, limit=20, ) if results.length() == 0 { println("No entries found within " + km.to_string() + " km.") } else { println("Found " + results.length().to_string() + " entries:") for pair in results { let e = pair.0 let d = pair.1 println(" " + e.id + " (" + e.name + ") - " + d.to_string() + " km") println(" Address: " + e.address.to_line()) println( " Location: (" + e.location.lat.to_string() + ", " + e.location.lng.to_string() + ")", ) } } } ///| fn run_within(lat : Double, lng : Double, km : Double) -> Unit { println( "=== Geofence: within " + km.to_string() + " km of (" + lat.to_string() + ", " + lng.to_string() + ") ==", ) println("") let db = build_sample_db() let point = @types.GeoPoint::make(lat, lng) let results = db.find_within_distance(point, km) if results.length() == 0 { println("No entries found within " + km.to_string() + " km.") } else { println("Found " + results.length().to_string() + " entries:") for pair in results { let e = pair.0 let d = pair.1 println(" " + e.id + " (" + e.name + ") - " + d.to_string() + " km") } } println("") println("Point in DB bbox: " + db.contains_point(point).to_string()) } // ============ Tag Commands ============ ///| fn run_tag(tag : String) -> Unit { println("=== Find entries with tag: '" + tag + "' ==") println("") let db = build_sample_db() let results = db.find_by_tag(tag) if results.length() == 0 { println("No entries found with tag '" + tag + "'.") println("Available tags: " + db.list_tags().join(", ")) } else { println("Found " + results.length().to_string() + " entries:") for e in results { print_entry(e) } } } ///| fn run_list_tags() -> Unit { println("=== All unique tags ===") println("") let db = build_sample_db() let tags = db.list_tags() if tags.length() == 0 { println("No tags found.") } else { println("Tags (" + tags.length().to_string() + "):") let tag_dist = db.tag_distribution() for pair in tag_dist { println(" " + pair.0 + ": " + pair.1.to_string() + " entries") } } } ///| fn run_add_tag(id : String, tag : String) -> Unit { println("=== Add tag '" + tag + "' to entry '" + id + "' ==") let db = build_sample_db() let ok = db.add_tag(id, tag) if ok { println("Tag '" + tag + "' added to entry '" + id + "'.") } else { println("Failed: entry not found or tag already exists.") } } ///| fn run_remove_tag(id : String, tag : String) -> Unit { println("=== Remove tag '" + tag + "' from entry '" + id + "' ==") let db = build_sample_db() let ok = db.remove_tag(id, tag) if ok { println("Tag '" + tag + "' removed from entry '" + id + "'.") } else { println("Failed: entry not found or tag not present.") } } // ============ Address Commands ============ ///| fn run_province(province : String) -> Unit { println("=== Find entries in province: '" + province + "' ==") println("") let db = build_sample_db() let results = db.find_by_province(province) if results.length() == 0 { println("No entries found in province '" + province + "'.") println("Available provinces: " + db.list_provinces().join(", ")) } else { println("Found " + results.length().to_string() + " entries:") for e in results { print_entry(e) } } } ///| fn run_city(city : String) -> Unit { println("=== Find entries in city: '" + city + "' ==") println("") let db = build_sample_db() let results = db.find_by_city(city) if results.length() == 0 { println("No entries found in city '" + city + "'.") println("Available cities: " + db.list_cities().join(", ")) } else { println("Found " + results.length().to_string() + " entries:") for e in results { print_entry(e) } } } ///| fn run_district(district : String) -> Unit { println("=== Find entries in district: '" + district + "' ==") println("") let db = build_sample_db() let results = db.find_by_district(district) if results.length() == 0 { println("No entries found in district '" + district + "'.") println("Available districts: " + db.list_districts().join(", ")) } else { println("Found " + results.length().to_string() + " entries:") for e in results { print_entry(e) } } } // ============ Statistics ============ ///| fn run_stats() -> Unit { println("=== Geographic Statistics ===") println("") let db = build_sample_db() println(db.summary()) } // ============ Export ============ ///| fn run_export(path : String) -> Unit { let db = build_sample_db() let csv = db.to_csv() if path.length() == 0 { println("=== CSV Export (stdout) ===") println("") println(csv) } else { match db.save_to(path) { Err(e) => println("Error: " + e) Ok(_) => { // Actually, we need to write CSV, not the binary format. // For now, we'll save the binary and also print the CSV. println("Saved binary DB to: " + path) println("CSV export (first 500 chars):") let preview = if csv.length() > 500 { csv[:500].to_owned() + "..." } else { csv } println(preview) } } } } // ============ Batch / Management Commands ============ ///| fn run_clear() -> Unit { println("=== Clear all entries ===") let db = build_sample_db() println("Database size before clear: " + db.size().to_string()) db.clear() println("Database size after clear: " + db.size().to_string()) println("All entries removed.") } // ============ New command handlers ============ ///| fn run_to_json(path : String) -> Unit { let db = build_sample_db() let json = db.to_json() if path.length() == 0 { println("=== JSON Export ===") println(json) } else { // Write via native IO if persist is linked; fall back to stdout notice. println("=== JSON Export ===") println("(Target path: " + path + ")") println(json) } } ///| fn run_to_geojson(path : String) -> Unit { let db = build_sample_db() let gj = db.to_geojson() if path.length() == 0 { println("=== GeoJSON Export ===") println(gj) } else { println("=== GeoJSON Export ===") println("(Target path: " + path + ")") println(gj) } } ///| fn run_pattern(pattern : String) -> Unit { println("=== Wildcard pattern: '" + pattern + "' ==") let db = build_sample_db() let results = db.find_name_pattern(pattern) if results.length() == 0 { println("No entries match the pattern.") } else { println("Found " + results.length().to_string() + " entries:") for e in results { print_entry(e) } } } ///| fn run_distance(id1 : String, id2 : String) -> Unit { println("=== Distance between '" + id1 + "' and '" + id2 + "' ==") let db = build_sample_db() match db.distance_between(id1, id2) { None => println("One or both IDs not found.") Some(d) => { println("Distance: " + d.to_string() + " km") match db.get(id1) { Some(a) => println( " " + a.id + " (" + a.name + ") at (" + a.location.lat.to_string() + ", " + a.location.lng.to_string() + ")", ) None => () } match db.get(id2) { Some(b) => println( " " + b.id + " (" + b.name + ") at (" + b.location.lat.to_string() + ", " + b.location.lng.to_string() + ")", ) None => () } } } } ///| fn run_neighbors(id : String, k : Int) -> Unit { println("=== Neighbors of '" + id + "' (k=" + k.to_string() + ") ==") let db = build_sample_db() match db.get(id) { None => println("Entry '" + id + "' not found.") Some(target) => { println("Target: " + target.id + " (" + target.name + ")") println( " Location: (" + target.location.lat.to_string() + ", " + target.location.lng.to_string() + ")", ) println("") let ns = db.find_neighbors(id, k) if ns.length() == 0 { println("No neighbors found.") } else { for pair in ns { let e = pair.0 println( " " + e.id + " (" + e.name + ") - " + pair.1.to_string() + " km", ) } } } } } ///| fn run_nearest_pairs(k : Int) -> Unit { println("=== Nearest " + k.to_string() + " pairs ===") let db = build_sample_db() let pairs = db.find_nearest_pairs(k) if pairs.length() == 0 { println("Not enough entries to form pairs (need >=2).") } else { for p in pairs { println( " " + p.0.id + " (" + p.0.name + ") <-> " + p.1.id + " (" + p.1.name + ") : " + p.2.to_string() + " km", ) } } } ///| fn run_districts(province : String) -> Unit { if province.length() == 0 { println("=== District distribution (all provinces) ===") } else { println("=== District distribution in '" + province + "' ==") } let db = build_sample_db() let dist = if province.length() == 0 { db.district_distribution() } else { db.district_distribution(province~) } if dist.length() == 0 { println("No entries.") } else { for pair in dist { println(" " + pair.0 + ": " + pair.1.to_string() + " entries") } } } ///| fn run_tag_count(tag : String) -> Unit { let db = build_sample_db() let n = db.count_by_tag(tag) println("Tag '" + tag + "' count: " + n.to_string()) } ///| fn run_tag_pairs() -> Unit { println("=== Tag co-occurrence pairs ===") let db = build_sample_db() let pairs = db.tag_pair_distribution() if pairs.length() == 0 { println("No tag pairs found.") } else { for p in pairs { println(" (" + p.0.0 + ", " + p.0.1 + "): " + p.1.to_string()) } } } ///| fn run_rename(id : String, new_name : String) -> Unit { println("=== Rename '" + id + "' to '" + new_name + "' ==") let db = build_sample_db() if db.rename(id, new_name) { println("Renamed successfully.") } else { println("Failed: entry not found.") } } ///| fn run_update_coords(id : String, lat : Double, lng : Double) -> Unit { println( "=== Update coords of '" + id + "' to (" + lat.to_string() + ", " + lng.to_string() + ") ==", ) let db = build_sample_db() if db.update_coordinates(id, lat, lng) { println("Updated successfully.") } else { println("Failed: entry not found.") } } ///| fn run_centroid(tag : String) -> Unit { println("=== Centroid of entries tagged '" + tag + "' ==") let db = build_sample_db() match db.centroid_weighted_by_tag(tag) { None => println("No entries with tag '" + tag + "'.") Some((pt, n)) => { println(" Entries: " + n.to_string()) println( " Centroid: (" + pt.lat.to_string() + ", " + pt.lng.to_string() + ")", ) } } } // ============ Persistence Commands ============ ///| fn run_save(path : String) -> Unit { println("=== Save to: " + path + " ===") let db = build_sample_db() match db.save_to(path) { Err(e) => println("Error: " + e) Ok(_) => println("Saved " + db.size().to_string() + " entries to " + path) } } ///| fn run_load(path : String) -> Unit { println("=== Load from: " + path + " ===") let db = @db.GeoDB::open(path) println("Loaded " + db.size().to_string() + " entries from " + path) let meta = db.metadata() println(" Version: " + meta.version.to_string()) println(" Created: " + meta.created_at.to_string()) println(" Updated: " + meta.updated_at.to_string()) println("") println("First 5 entries:") let mut i = 0 for e in db.all() { if i >= 5 { break } println(" [" + e.id + "] " + e.name + " - " + e.address.to_line()) i = i + 1 } } ///| fn run_backup(path : String) -> Unit { println("=== Backup: " + path + " ===") let db = @db.GeoDB::open(path) match db.backup() { Err(e) => println("Error: " + e) Ok(bak) => { println("Backup created: " + bak) println("Entries: " + db.size().to_string()) } } } ///| fn run_check(path : String) -> Unit { println("=== Integrity check: " + path + " ===") let db = @db.GeoDB::open(path) match db.check() { Err(e) => println("FAILED: " + e) Ok(meta) => { println("OK - Integrity verified") println(" Version: " + meta.version.to_string()) println(" Entry count: " + meta.entry_count.to_string()) println(" Created at: " + meta.created_at.to_string()) println(" Updated at: " + meta.updated_at.to_string()) } } } ///| fn run_info(path : String) -> Unit { println("=== Database info: " + path + " ===") let db = @db.GeoDB::open(path) let meta = db.metadata() println(" File: " + path) println(" Version: " + meta.version.to_string()) println(" Entries: " + meta.entry_count.to_string()) println(" Created: " + meta.created_at.to_string()) println(" Updated: " + meta.updated_at.to_string()) println(" Auto-save: " + db.auto_save.to_string()) println("") println(" Provinces:") let provinces = @gen.all_province_names() println(" " + provinces.length().to_string() + " provinces in generator") println(" Cities:") println(" " + @gen.city_count().to_string() + " cities in generator") } // ============ Helpers ============ ///| fn build_sample_db() -> @db.GeoDB { let db = @db.GeoDB::in_memory() // === 北京 (中心 39.9, 116.4) === ignore(db.insert_raw("bj-gugong", "故宫博物院", 39.9163, 116.3972, "北京市东城区景山前街4号")) ignore(db.insert_raw("bj-tiananmen", "天安门广场", 39.9055, 116.3972, "北京市东城区东长安街")) ignore(db.insert_raw("bj-tiantan", "天坛公园", 39.8822, 116.4066, "北京市东城区天坛东里")) ignore(db.insert_raw("bj-yiheyuan", "颐和园", 39.9999, 116.2755, "北京市海淀区颐和园路5号")) ignore(db.insert_raw("bj-bada", "八达岭长城", 40.3587, 116.0214, "北京市延庆区G6京藏高速")) ignore(db.insert_raw("bj-niuchao", "国家体育场(鸟巢)", 39.9927, 116.3964, "北京市朝阳区国家体育场南路")) ignore(db.insert_raw("bj-shuilih", "国家游泳中心(水立方)", 39.9929, 116.3969, "北京市朝阳区国家体育场北路")) ignore(db.insert_raw("bj-museum", "中国国家博物馆", 39.9047, 116.4031, "北京市东城区东长安街16号")) ignore(db.insert_raw("bj-wangfujing", "王府井步行街", 39.9148, 116.4108, "北京市东城区王府井大街")) ignore(db.insert_raw("bj-sanlitun", "三里屯", 39.9378, 116.4559, "北京市朝阳区三里屯路")) ignore(db.insert_raw("bj-798", "798艺术区", 39.9846, 116.5032, "北京市朝阳区酒仙桥路4号")) // === 上海 (中心 31.2, 121.5) === ignore(db.insert_raw("sh-lijiazui", "东方明珠塔", 31.2397, 121.4997, "上海市浦东新区陆家嘴世纪大道1号")) ignore(db.insert_raw("sh-waitan", "外滩", 31.2397, 121.4905, "上海市黄浦区中山东一路")) ignore(db.insert_raw("sh-yuyuan", "豫园", 31.2267, 121.4902, "上海市黄浦区福佑路168号")) ignore(db.insert_raw("sh-nanjing", "南京路步行街", 31.2356, 121.4814, "上海市黄浦区南京东路")) ignore(db.insert_raw("sh-disney", "上海迪士尼乐园", 31.1440, 121.6570, "上海市浦东新区川沙新镇黄赵路310号")) ignore(db.insert_raw("sh-museum", "上海博物馆", 31.2301, 121.4758, "上海市黄浦区人民大道201号")) ignore(db.insert_raw("sh-center", "上海中心大厦", 31.2354, 121.5016, "上海市浦东新区银城中路501号")) ignore(db.insert_raw("sh-huangpu", "黄浦江游船码头", 31.2370, 121.4890, "上海市黄浦区中山东二路")) // === 广州 (中心 23.1, 113.3) === ignore(db.insert_raw("gz-tower", "广州塔", 23.1066, 113.3239, "广州市海珠区阅江西路222号")) ignore(db.insert_raw("gz-chenjiaci", "陈家祠", 23.1266, 113.2380, "广州市荔湾区中山七路恩龙里34号")) ignore(db.insert_raw("gz-yuexiu", "越秀公园", 23.1321, 113.2566, "广州市越秀区解放北路988号")) ignore(db.insert_raw("gz-baiyun", "白云山", 23.1887, 113.3198, "广州市白云区广园中路")) ignore(db.insert_raw("gz-shamian", "沙面岛", 23.1057, 113.2382, "广州市荔湾区沙面大街")) // === 深圳 (中心 22.5, 114.0) === ignore(db.insert_raw("sz-pingan", "深圳平安金融中心", 22.5431, 114.0579, "深圳市福田区福华三路")) ignore(db.insert_raw("sz-shijie", "世界之窗", 22.5399, 113.9728, "深圳市南山区深南大道9037号")) ignore(db.insert_raw("sz-huanlegu", "欢乐谷", 22.6147, 113.9747, "深圳市南山区侨城西街18号")) ignore(db.insert_raw("sz-lianhua", "莲花山", 22.5424, 114.0582, "深圳市福田区莲花山路")) ignore(db.insert_raw("sz-dameisha", "大梅沙海滨公园", 22.6087, 114.3328, "深圳市盐田区梅沙街道")) // === 成都 (中心 30.7, 104.1) === ignore(db.insert_raw("cd-kuanzhai", "宽窄巷子", 30.6700, 104.0590, "四川省成都市青羊区金河路口")) ignore(db.insert_raw("cd-jinli", "锦里古街", 30.6449, 104.0247, "四川省成都市武侯区武侯祠大街231号")) ignore(db.insert_raw("cd-wuhou", "武侯祠", 30.6319, 104.0438, "四川省成都市武侯区武侯祠大街231号")) ignore(db.insert_raw("cd-panda", "大熊猫繁育研究基地", 30.7330, 104.1480, "四川省成都市成华区熊猫大道1375号")) ignore(db.insert_raw("cd-dujiangyan", "都江堰景区", 30.9918, 103.5764, "四川省成都市都江堰市青城山镇")) // === 重庆 (中心 29.6, 106.5) === ignore(db.insert_raw("cq-jiefangbei", "解放碑步行街", 29.5580, 106.5781, "重庆市渝中区解放碑")) ignore(db.insert_raw("cq-hongyadong", "洪崖洞", 29.5630, 106.5786, "重庆市渝中区嘉陵江滨江路88号")) ignore(db.insert_raw("cq-nanshan", "南山一棵树观景台", 29.5501, 106.5828, "重庆市南岸区龙黄公路")) ignore(db.insert_raw("cq-ciqikou", "磁器口古镇", 29.5885, 106.4303, "重庆市沙坪坝区磁器口古镇")) ignore(db.insert_raw("cq-wulong", "武隆天生三桥", 29.4033, 107.7867, "重庆市武隆区天生三桥")) // === 杭州 (中心 30.2, 120.1) === ignore(db.insert_raw("hz-xihu", "西湖", 30.2590, 120.1348, "浙江省杭州市西湖区")) ignore(db.insert_raw("hz-lingyin", "灵隐寺", 30.2403, 120.0992, "浙江省杭州市西湖区灵隐路法云弄1号")) ignore(db.insert_raw("hz-xixi", "西溪湿地", 30.2850, 120.0700, "浙江省杭州市西湖区天目山路518号")) ignore(db.insert_raw("hz-qiandao", "千岛湖", 29.6088, 119.0215, "浙江省杭州市淳安县千岛湖镇")) // === 南京 (中心 32.0, 118.8) === ignore(db.insert_raw("nj-zhongshan", "中山陵", 32.0586, 118.8501, "江苏省南京市玄武区紫金山南麓")) ignore(db.insert_raw("nj-fuzimiao", "夫子庙", 32.0210, 118.7880, "江苏省南京市秦淮区夫子庙")) ignore(db.insert_raw("nj-museum", "南京博物院", 32.0395, 118.8090, "江苏省南京市玄武区中山东路307号")) // === 武汉 (中心 30.5, 114.3) === ignore(db.insert_raw("wh-huanghe", "黄鹤楼", 30.5450, 114.3023, "湖北省武汉市武昌区蛇山西坡")) ignore(db.insert_raw("wh-donghu", "东湖风景区", 30.5520, 114.3570, "湖北省武汉市武昌区沿湖大道16号")) ignore(db.insert_raw("wh-wuhan", "武汉大学", 30.5320, 114.3630, "湖北省武汉市武昌区珞珈山")) // === 西安 (中心 34.2, 108.9) === ignore(db.insert_raw("xa-dayanta", "大雁塔", 34.2238, 108.9650, "陕西省西安市雁塔区大慈恩寺内")) ignore(db.insert_raw("xa-bingmayong", "秦始皇兵马俑", 34.3848, 109.2786, "陕西省西安市临潼区秦陵北路")) ignore(db.insert_raw("xa-chengqiang", "西安城墙", 34.2636, 108.9410, "陕西省西安市碑林区南大街")) // === 天津 (中心 39.1, 117.2) === ignore(db.insert_raw("tj-eye", "天津之眼", 39.1436, 117.0097, "天津市河北区三岔河口永乐桥")) ignore(db.insert_raw("tj-guwenhua", "古文化街", 39.1390, 117.1830, "天津市南开区古文化街")) ignore(db.insert_raw("tj-wudadao", "五大道", 39.1030, 117.2150, "天津市和平区五大道")) // === 青岛 (中心 36.0, 120.3) === ignore(db.insert_raw("qd-zhanqiao", "青岛栈桥", 36.0588, 120.3168, "山东省青岛市市南区太平路12号")) ignore(db.insert_raw("qd-laoshan", "崂山", 36.2267, 120.4517, "山东省青岛市崂山区崂山路100号")) ignore(db.insert_raw("qd-badaguan", "八大关", 36.0558, 120.3318, "山东省青岛市市南区山海关路")) // === 昆明 (中心 25.0, 102.7) === ignore(db.insert_raw("km-dianchi", "滇池", 24.8800, 102.8300, "云南省昆明市西山区")) ignore(db.insert_raw("km-cuihu", "翠湖", 25.0392, 102.7053, "云南省昆明市五华区翠湖南路")) ignore(db.insert_raw("km-xishan", "西山", 24.8930, 102.6370, "云南省昆明市西山区西山森林公园")) db } ///| fn print_entry(e : @types.GeoEntry) -> Unit { println(" id: " + e.id) println(" name: " + e.name) println( " location: (" + e.location.lat.to_string() + ", " + e.location.lng.to_string() + ")", ) println(" address: " + e.address.to_line()) println(" province: " + e.address.province) println(" city: " + e.address.city) println(" district: " + e.address.district) println(" street: " + e.address.street) println(" detail: " + e.address.detail) println(" postal: " + e.address.postal_code) if e.tags.length() > 0 { println(" tags: " + e.tags.join(", ")) } println("") } // ============ New feature handlers ============ ///| fn run_import_csv(csv_str : String) -> Unit { println("=== Importing from CSV ===") let db = build_sample_db() let before = db.size() let n = db.import_csv(csv_str) let after = db.size() println(" Imported: " + n.to_string() + " entries") println(" Before: " + before.to_string() + " entries") println(" After: " + after.to_string() + " entries") println("") println(" All entries after import:") for e in db.all() { println( " " + e.id + " | " + e.name + " | (" + e.location.lat.to_string() + ", " + e.location.lng.to_string() + ")", ) } } ///| fn run_import_geojson(gj_str : String) -> Unit { println("=== Importing from GeoJSON ===") let db = build_sample_db() let before = db.size() let n = db.import_geojson(gj_str) let after = db.size() println(" Imported: " + n.to_string() + " entries") println(" Before: " + before.to_string() + " entries") println(" After: " + after.to_string() + " entries") } ///| fn run_duplicates(max_dist : Double, max_edit : Int) -> Unit { println( "=== Duplicate Detection (max_dist=" + max_dist.to_string() + "km, max_edit=" + max_edit.to_string() + ") ===", ) let db = build_sample_db() let groups = db.find_duplicates( max_distance_km=max_dist, max_edit_dist=max_edit, ) if groups.length() == 0 { println(" No duplicates found.") } else { println(" Found " + groups.length().to_string() + " duplicate group(s):") let mut gi = 0 while gi < groups.length() { let group = groups[gi] println(" Group #" + (gi + 1).to_string() + ":") for e in group { println( " - " + e.id + " | " + e.name + " | (" + e.location.lat.to_string() + ", " + e.location.lng.to_string() + ")", ) } gi = gi + 1 } } } ///| fn run_cluster(cell_size_km : Double) -> Unit { println( "=== Spatial Grid Clustering (cell=" + cell_size_km.to_string() + "km) ===", ) let db = build_sample_db() let clusters = db.cluster_by_grid(cell_size_km~) println(" Total clusters: " + clusters.length().to_string()) let mut ci = 0 while ci < clusters.length() { let cluster = clusters[ci] let key = cluster.0 let centroid = cluster.1 let count = cluster.2 println(" Cluster #" + (ci + 1).to_string() + " [" + key + "]:") println( " Centroid: (" + centroid.lat.to_string() + ", " + centroid.lng.to_string() + ")", ) println(" Count: " + count.to_string() + " entries") ci = ci + 1 } } ///| fn run_dbscan(eps_km : Double, min_points : Int) -> Unit { println( "=== DBSCAN (eps=" + eps_km.to_string() + "km, min_points=" + min_points.to_string() + ") ===", ) let db = build_sample_db() let clusters = db.cluster_dbscan(eps_km, min_points) let assigned : @hashmap.HashMap[String, Bool] = @hashmap.HashMap([]) let mut assigned_count = 0 let mut ci = 0 while ci < clusters.length() { let cluster = clusters[ci] assigned_count = assigned_count + cluster.length() println(" Cluster #" + (ci + 1).to_string() + " (" + cluster.length().to_string() + " POIs):") // Show up to 8 member names per cluster. let mut k = 0 while k < cluster.length() && k < 8 { assigned.set(cluster[k], true) match db.get(cluster[k]) { Some(e) => println(" - " + e.name) None => () } k = k + 1 } // Mark members past the display cap too, so noise detection is exact. while k < cluster.length() { assigned.set(cluster[k], true) k = k + 1 } if cluster.length() > 8 { println(" ... 及另外 " + (cluster.length() - 8).to_string() + " 个") } ci = ci + 1 } let noise = db.size() - assigned_count println(" Clusters: " + clusters.length().to_string() + " Noise: " + noise.to_string()) if noise > 0 { println(" Noise points:") for e in db.all() { if !(assigned.contains(e.id)) { println(" - " + e.id + " (" + e.name + ")") } } println(" (远郊点,距最近城市中心超过 " + eps_km.to_string() + " km)") } } ///| fn addr_level_name(l : @parser.MatchLevel) -> String { match l { @parser.MatchLevel::Exact => "完全匹配" @parser.MatchLevel::Partial => "部分匹配" @parser.MatchLevel::None => "不匹配" } } ///| Level -> confusion-matrix row index: Exact=0, Partial=1, None=2. fn addr_level_idx(l : @parser.MatchLevel) -> Int { 2 - l.rank() } ///| fn run_addr_match(query : String, candidate : String) -> Unit { println("=== Address match ===") println(" query: " + query) println(" candidate: " + candidate) let r = @parser.match_address(query, candidate) println(" level: " + addr_level_name(r.level)) println( " score: " + r.score.to_string() + " dice: " + r.dice.to_string() + " road: " + r.road_recall.to_string() + " num: " + r.num_recall.to_string(), ) } ///| /// Evaluate the three-level address matcher against the labelled JSONL /// corpus: prints parse/match timing, throughput, overall agreement and /// a 3x3 confusion matrix with per-class precision/recall/F1. fn run_addr_eval(path : String, max_records : Int) -> Unit { println("=== Address matcher evaluation: " + path + " ===") let content = match @persist.load_text(path) { Ok(s) => s Err(e) => { println("Error: cannot read " + path + ": " + e) return } } let t_parse0 = @bench.monotonic_clock_start() let records : Array[@parser.AddrMatchRecord] = [] for line in content.split("\n").collect() { if max_records > 0 && records.length() >= max_records { break } match @parser.parse_match_record(line.to_owned()) { Some(r) => records.push(r) None => () } } let t_parse = @bench.monotonic_clock_end(t_parse0) // confusion[gold][pred], index order Exact=0, Partial=1, None=2. let confusion : Array[Array[Int]] = [[0, 0, 0], [0, 0, 0], [0, 0, 0]] let mut pairs = 0 let mut agreement = 0 // Diagnostic: keep up to N samples for each major error category. // Index convention: 0=Exact, 1=Partial, 2=None (same as addr_level_idx). let diag_p2n : Array[String] = [] // gold=Partial, pred=None let diag_p2e : Array[String] = [] // gold=Partial, pred=Exact let diag_n2e : Array[String] = [] // gold=None, pred=Exact let diag_cutoff = 40 let t_match0 = @bench.monotonic_clock_start() for r in records { let pq = @parser.prepare_query(r.query) for c in r.candidates { let mr = @parser.match_address_prepared(pq, c.text).0 let pred = mr.level let gi = addr_level_idx(c.level) let pi = addr_level_idx(pred) confusion[gi][pi] = confusion[gi][pi] + 1 pairs = pairs + 1 if gi == pi { agreement = agreement + 1 } // Collect worst-error diagnostics. let q = r.query let txt = c.text let ds = " dice=" + format2(mr.dice) + " feat=" + format2(mr.feature_recall) + " road=" + format2(mr.road_recall) + " num=" + format2(mr.num_recall) let line = " Q=" + q + "\n C=" + txt + "\n " + ds // gold=Partial(1), pred=None(2) if gi == 1 && pi == 2 && diag_p2n.length() < diag_cutoff { diag_p2n.push(line) } // gold=Partial(1), pred=Exact(0) if gi == 1 && pi == 0 && diag_p2e.length() < diag_cutoff { diag_p2e.push(line) } // gold=None(2), pred=Exact(0) if gi == 2 && pi == 0 && diag_n2e.length() < diag_cutoff { diag_n2e.push(line) } } } let t_match = @bench.monotonic_clock_end(t_match0) println(" Records: " + records.length().to_string() + " pairs: " + pairs.to_string()) println(" Parse: " + t_parse.to_string() + " us Match: " + t_match.to_string() + " us") if t_match > 0 { let throughput = pairs.to_double() * 1000000.0 / t_match println(" Throughput: " + throughput.to_string() + " pairs/s") } if pairs > 0 { let pct = agreement.to_double() * 100.0 / pairs.to_double() println(" Agreement: " + pct.to_string() + "% (" + agreement.to_string() + "/" + pairs.to_string() + ")") } println("") println(" Confusion matrix (rows = gold, cols = pred):") println(" 完全匹配 部分匹配 不匹配") let names = ["完全匹配", "部分匹配", "不匹配"] let mut gi2 = 0 while gi2 < 3 { let row = confusion[gi2] println( " " + names[gi2] + " " + row[0].to_string() + " " + row[1].to_string() + " " + row[2].to_string(), ) gi2 = gi2 + 1 } println("") println(" Per-class metrics:") println(" level precision recall f1") let mut k = 0 while k < 3 { let tp = confusion[k][k].to_double() let mut fp = 0.0 let mut fn_ = 0.0 let mut a = 0 while a < 3 { if a != k { fp = fp + confusion[a][k].to_double() fn_ = fn_ + confusion[k][a].to_double() } a = a + 1 } let prec = if tp + fp > 0.0 { tp / (tp + fp) } else { 0.0 } let rec = if tp + fn_ > 0.0 { tp / (tp + fn_) } else { 0.0 } let f1 = if prec + rec > 0.0 { 2.0 * prec * rec / (prec + rec) } else { 0.0 } println( " " + names[k] + " " + (prec * 100.0).to_string() + "% " + (rec * 100.0).to_string() + "% " + (f1 * 100.0).to_string() + "%", ) k = k + 1 } // ---- Diagnostic dumps ---- if diag_p2n.length() > 0 { println("") println(" === DIAG: gold=部分匹配 pred=不匹配 (first " + diag_p2n.length().to_string() + ") ===") for s in diag_p2n { println(s) } } if diag_p2e.length() > 0 { println("") println(" === DIAG: gold=部分匹配 pred=完全匹配 (first " + diag_p2e.length().to_string() + ") ===") for s in diag_p2e { println(s) } } if diag_n2e.length() > 0 { println("") println(" === DIAG: gold=不匹配 pred=完全匹配 (first " + diag_n2e.length().to_string() + ") ===") for s in diag_n2e { println(s) } } } /// Dump all intermediate features for every query-candidate pair as TSV. /// Columns: dice,road_b,feature_recall,name_recall,admin_hit,road_hit, /// town_hit,context_ok,house_recall,house_conflict,veto,q_structured, /// c_structured,feat_overlap,weak_core_exact,fuzzy_poi_overlap, /// reverse_poi_overlap,road_conflict,main_hit,house_missing,foreign_city, /// q_roads,c_roads,q_feats,c_feats,score,gold_level /// gold_level: 0=Exact, 1=Partial, 2=None fn run_addr_feat_dump(path : String) -> Unit { let content = match @persist.load_text(path) { Ok(s) => s Err(e) => { println("Error: " + e) return } } let records : Array[@parser.AddrMatchRecord] = [] for line in content.split("\n").collect() { match @parser.parse_match_record(line.to_owned()) { Some(r) => records.push(r) None => () } } let header = "dice\troad_b\tfeat\tname\tadmin_hit\troad_hit\ttown_hit\tcontext_ok\thouse_recall\thouse_conflict\tveto\tq_struct\tc_struct\tfeat_overlap\tweak_core\tfuzzy_poi\treverse_poi\troad_conflict\tmain_hit\thouse_missing\tforeign_city\tq_roads\tc_roads\tq_feats\tc_feats\tscore\tgold" println(header) for r in records { let pq = @parser.prepare_query(r.query) for c in r.candidates { let (_mr, fb) = @parser.match_address_prepared(pq, c.text) let gi = addr_level_idx(c.level) println( fb.dice.to_string() + "\t" + fb.road_b.to_string() + "\t" + fb.feature_recall.to_string() + "\t" + fb.name_recall.to_string() + "\t" + fb.admin_hit.to_string() + "\t" + fb.road_hit.to_string() + "\t" + fb.town_hit.to_string() + "\t" + fb.context_ok.to_string() + "\t" + fb.house_recall.to_string() + "\t" + fb.house_conflict.to_string() + "\t" + fb.veto.to_string() + "\t" + fb.q_structured.to_string() + "\t" + fb.c_structured.to_string() + "\t" + fb.feat_overlap.to_string() + "\t" + fb.weak_core_exact.to_string() + "\t" + fb.fuzzy_poi_overlap.to_string() + "\t" + fb.reverse_poi_overlap.to_string() + "\t" + fb.road_conflict.to_string() + "\t" + fb.main_hit.to_string() + "\t" + fb.house_missing.to_string() + "\t" + fb.foreign_city.to_string() + "\t" + fb.q_roads.to_string() + "\t" + fb.c_roads.to_string() + "\t" + fb.q_feats.to_string() + "\t" + fb.c_feats.to_string() + "\t" + fb.score.to_string() + "\t" + gi.to_string() ) } } } /// Format a Double to 2 decimal places (no trailing zero trimming). fn format2(v : Double) -> String { let s = v.to_string() // Take up to 7 chars to avoid long floats like 0.30000000001234. if s.length() <= 7 { s } else { s[0:7].to_owned() } } ///| /// Build a spatial database from the first N real-address corpus /// records, persist it to testdata/geo_corpus.db and report its /// spatial distribution plus a sample address retrieval. fn run_corpus_db(n : Int) -> Unit { let path = "testdata/data.txt" println("=== Corpus DB (first " + n.to_string() + " records of " + path + ") ===") let content = match @persist.load_text(path) { Ok(s) => s Err(e) => { println("Error: cannot read " + path + ": " + e) return } } let records : Array[@parser.AddrMatchRecord] = [] for line in content.split("\n").collect() { if records.length() >= n { break } match @parser.parse_match_record(line.to_owned()) { Some(r) => records.push(r) None => () } } let t0 = @bench.monotonic_clock_start() let db = @gen.corpus_db(records) let t_build = @bench.monotonic_clock_end(t0) println(" Built " + db.size().to_string() + " entries in " + t_build.to_string() + " us") match db.compute_bbox() { Some((bbox, _)) => println( " BBox: (" + bbox.min_lat.to_string() + "," + bbox.min_lng.to_string() + ") .. (" + bbox.max_lat.to_string() + "," + bbox.max_lng.to_string() + ")", ) None => () } let clusters = db.cluster_dbscan(50.0, 5) let mut assigned = 0 for cl in clusters { assigned = assigned + cl.length() } println( " DBSCAN eps=50km: " + clusters.length().to_string() + " city clusters, " + (db.size() - assigned).to_string() + " noise", ) match db.save_to("testdata/geo_corpus.db") { Ok(_) => println(" Saved: testdata/geo_corpus.db") Err(e) => println(" Save failed: " + e) } if records.length() > 0 { println("") println(" Sample retrieval for first query:") println(" " + records[0].query) let hits = db.match_addresses(records[0].query, limit=3) for (e, mr) in hits { println(" [" + addr_level_name(mr.level) + " " + mr.score.to_string() + "] " + e.id) } } } ///| fn run_matrix(ids : Array[String]) -> Unit { println("=== Distance Matrix ===") println(" IDs: " + ids.join(", ")) let db = build_sample_db() let matrix = db.distance_matrix(ids) println( " " + matrix.length().to_string() + "x" + matrix.length().to_string() + " matrix:", ) let mut row = 0 while row < matrix.length() { let mut col = 0 let row_vals : Array[String] = [] while col < matrix[row].length() { row_vals.push(matrix[row][col].to_string()) col = col + 1 } println(" " + row_vals.join(", ")) row = row + 1 } } // ---- New feature handlers (batch 2) ---- ///| fn run_reverse_geocode(lat : Double, lng : Double) -> Unit { println( "=== Reverse Geocode (" + lat.to_string() + ", " + lng.to_string() + ") ===", ) let db = build_sample_db() match db.reverse_geocode(lat, lng) { None => println(" No entries in database.") Some(result) => { let entry = result.0 let d = result.1 println(" Nearest entry:") println(" ID: " + entry.id) println(" Name: " + entry.name) println(" Dist: " + d.to_string() + " km") println( " Loc: (" + entry.location.lat.to_string() + ", " + entry.location.lng.to_string() + ")", ) } } } ///| fn run_tag_all(tags : Array[String]) -> Unit { println("=== Entries with ALL tags: " + tags.join(", ") + " ===") let db = build_sample_db() let results = db.find_by_tags_all(tags) println(" Found " + results.length().to_string() + " entries:") for e in results { println(" - " + e.id + " | " + e.name + " | tags: " + e.tags.join(", ")) } } ///| fn run_tag_any(tags : Array[String]) -> Unit { println("=== Entries with ANY tag: " + tags.join(", ") + " ===") let db = build_sample_db() let results = db.find_by_tags_any(tags) println(" Found " + results.length().to_string() + " entries:") for e in results { println(" - " + e.id + " | " + e.name + " | tags: " + e.tags.join(", ")) } } ///| fn run_bbox_count( min_lat : Double, max_lat : Double, min_lng : Double, max_lng : Double, ) -> Unit { println("=== Bounding Box Count ===") println( " BBox: [" + min_lat.to_string() + ", " + max_lat.to_string() + "] x [" + min_lng.to_string() + ", " + max_lng.to_string() + "]", ) let db = build_sample_db() let bbox : @types.BBox = { min_lat, max_lat, min_lng, max_lng, } let count = db.bbox_count(bbox) println(" Count: " + count.to_string() + " entries") } ///| fn run_within_radius(lat : Double, lng : Double, radius_km : Double) -> Unit { println( "=== Entries within " + radius_km.to_string() + " km of (" + lat.to_string() + ", " + lng.to_string() + ") ===", ) let db = build_sample_db() let results = db.find_within_radius(lat, lng, radius_km) println(" Found " + results.length().to_string() + " entries:") for e in results { let d = @geo.haversine(@types.GeoPoint::make(lat, lng), e.location) println(" - " + e.id + " | " + e.name + " | " + d.to_string() + " km") } } ///| fn run_density(cell_size_km : Double) -> Unit { println("=== Density Map (cell=" + cell_size_km.to_string() + "km) ===") let db = build_sample_db() let map = db.density_map(cell_size_km~) println(" Total cells: " + map.length().to_string()) for cell in map { println( " Cell [" + cell.0 + "]: (" + cell.1.to_string() + ", " + cell.2.to_string() + ") count=" + cell.3.to_string(), ) } } ///| fn run_stddev() -> Unit { println("=== Location Standard Deviation ===") let db = build_sample_db() let (std_lat, std_lng) = db.location_stddev() println(" Latitude stddev: " + std_lat.to_string()) println(" Longitude stddev: " + std_lng.to_string()) // Also show basic stats. match db.compute_center() { None => println(" Center: N/A (no entries)") Some(result) => { let center = result.0 println( " Center: " + center.lat.to_string() + ", " + center.lng.to_string(), ) } } } ///| fn run_histogram() -> Unit { println("=== Province Histogram ===") let db = build_sample_db() let hist = db.province_histogram() println(" Provinces:") for pair in hist { println(" " + pair.0 + ": " + pair.1.to_string() + " entries") } } // ---- New query enhancement handlers ---- ///| fn run_ring_query( lat : Double, lng : Double, inner : Double, outer : Double, ) -> Unit { println("=== Ring Query ===") println(" Center: (" + lat.to_string() + ", " + lng.to_string() + ")") println( " Ring: [" + inner.to_string() + " km, " + outer.to_string() + " km]", ) let db = build_sample_db() let center = @types.GeoPoint::make(lat, lng) let (results, min_d, max_d, mean_d) = db.ring_query_summary( center, inner, outer, ) println("") if results.length() == 0 { println(" No entries found in this ring.") } else { println(" Found " + results.length().to_string() + " entries:") for p in results { let e = p.0 println( " - " + e.id + " | " + e.name + " | " + p.1.to_string() + " km", ) } println("") println(" Distance summary:") println(" Min: " + min_d.to_string() + " km") println(" Max: " + max_d.to_string() + " km") println(" Mean: " + mean_d.to_string() + " km") } } ///| fn run_distance_histogram( lat : Double, lng : Double, max_km : Double, bins : Int, ) -> Unit { println("=== Distance Histogram ===") println(" Center: (" + lat.to_string() + ", " + lng.to_string() + ")") println( " Range: 0 to " + max_km.to_string() + " km, " + bins.to_string() + " bins", ) let db = build_sample_db() let center = @types.GeoPoint::make(lat, lng) let hist = db.distance_histogram(center, max_km, num_bins=bins) println("") for bin in hist { let start = bin.0 let end = bin.1 let count = bin.2 // ASCII bar let bar_len = count let mut bar = "|" let mut b = 0 while b < bar_len && b < 50 { bar = bar + "#" b = b + 1 } println( " [" + start.to_string() + " - " + end.to_string() + " km] " + count.to_string() + bar, ) } } ///| fn run_coord_histogram(axis : String, bins : Int) -> Unit { let db = build_sample_db() let hist = if axis == "lng" || axis == "longitude" { println("=== Longitude Histogram (" + bins.to_string() + " bins) ===") db.lng_histogram(num_bins=bins) } else { println("=== Latitude Histogram (" + bins.to_string() + " bins) ===") db.lat_histogram(num_bins=bins) } println("") for bin in hist { let start = bin.0 let end = bin.1 let count = bin.2 let mut bar = "|" let mut b = 0 while b < count && b < 50 { bar = bar + "#" b = b + 1 } println( " [" + start.to_string() + " - " + end.to_string() + "] " + count.to_string() + bar, ) } } ///| fn run_distance_summary(lat : Double, lng : Double, use_center : Bool) -> Unit { println("=== Distance Summary ===") let db = build_sample_db() if use_center { println( " Mode: distances from center (" + lat.to_string() + ", " + lng.to_string() + ")", ) let center = @types.GeoPoint::make(lat, lng) let (count, min_d, max_d, mean, median) = db.distance_summary( center=Some(center), ) println(" Count: " + count.to_string()) println(" Min: " + min_d.to_string() + " km") println(" Max: " + max_d.to_string() + " km") println(" Mean: " + mean.to_string() + " km") println(" Median: " + median.to_string() + " km") } else { println(" Mode: all pairwise distances in DB") let (count, min_d, max_d, mean, median) = db.distance_summary(center=None) println(" Pairs: " + count.to_string()) println(" Min: " + min_d.to_string() + " km") println(" Max: " + max_d.to_string() + " km") println(" Mean: " + mean.to_string() + " km") println(" Median: " + median.to_string() + " km") } } ///| fn run_nn_range(id : String, k : Int) -> Unit { println("=== Nearest-Neighbor Distance Range ===") println(" Entry: " + id + ", k=" + k.to_string()) let db = build_sample_db() match db.nearest_neighbors_range(id, k) { None => println(" Entry not found or fewer than 2 entries in DB.") Some(result) => { let (n, min_d, max_d, mean, median) = result println(" Neighbors found: " + n.to_string()) println(" Min distance: " + min_d.to_string() + " km") println(" Max distance: " + max_d.to_string() + " km") println(" Mean distance: " + mean.to_string() + " km") println(" Median distance: " + median.to_string() + " km") // Also list the actual neighbors. let ns = db.find_neighbors(id, k) if ns.length() > 0 { println("") println(" Top neighbors:") for p in ns { let e = p.0 println( " - " + e.id + " | " + e.name + " | " + p.1.to_string() + " km", ) } } } } } ///| fn run_voronoi() -> Unit { println("=== Voronoi Diagram ===") let db = build_sample_db() let cells = db.voronoi() if cells.length() == 0 { println(" No entries in DB.") return } println(" Cells: " + cells.length().to_string()) for c in cells { println( " Cell: (" + c.site.lat.to_string() + ", " + c.site.lng.to_string() + ") -> " + c.polygon.length().to_string() + " vertices", ) } } ///| fn run_voronoi_cell(id : String) -> Unit { println("=== Voronoi Cell ===") println(" Entry: " + id) let db = build_sample_db() match db.voronoi_cell(id) { None => println(" Entry not found or DB has fewer than 2 entries.") Some(poly) => { println(" Vertices: " + poly.length().to_string()) for v in poly { println(" (" + v.lat.to_string() + ", " + v.lng.to_string() + ")") } } } } ///| fn run_voronoi_neighbors(id : String) -> Unit { println("=== Voronoi Neighbors ===") println(" Entry: " + id) let db = build_sample_db() match db.voronoi_neighbors(id) { None => println(" Entry not found or DB has fewer than 2 entries.") Some(neighbors) => if neighbors.length() == 0 { println(" No neighbors found.") } else { println(" Neighbors: " + neighbors.length().to_string()) for nid in neighbors { match db.get(nid) { None => () Some(e) => println(" - " + e.id + " | " + e.name) } } } } } // ---- New handlers: kmeans, elbow, centroid, bbox-query ---- ///| fn run_kmeans(k : Int, max_iter : Int) -> Unit { println("=== K-Means Clustering ===") println(" k = " + k.to_string() + ", max_iter = " + max_iter.to_string()) let db = build_sample_db() let clusters = db.kmeans(k, max_iter~) println("") let mut j = 0 while j < clusters.length() { let cl = clusters[j] println( " Cluster " + cl.id.to_string() + " (" + cl.members.length().to_string() + " members)", ) println( " Center: (" + cl.center.lat.to_string() + ", " + cl.center.lng.to_string() + ")", ) for m in cl.members { println(" - " + m.id + " | " + m.name) } j = j + 1 } } ///| fn run_elbow(max_k : Int) -> Unit { println("=== Elbow Method (WCSS for k = 1.." + max_k.to_string() + ") ===") let db = build_sample_db() let values = db.kmeans_elbow(max_k) println("") for pair in values { let k = pair.0 let wcss = pair.1 // ASCII bar let bar_len = if wcss < 1000000.0 { (wcss / 10000.0).to_int() } else { 0 } let mut bar = "" let mut b = 0 while b < bar_len && b < 60 { bar = bar + "#" b = b + 1 } println( " k=" + k.to_string().pad_start(2, ' ') + " inertia=" + wcss.to_string() + " |" + bar, ) } } ///| fn run_all_centroid() -> Unit { println("=== Centroid (planar mean) ===") let db = build_sample_db() let c = db.centroid() println(" DB has " + db.all().length().to_string() + " entries") println(" Centroid: (" + c.lat.to_string() + ", " + c.lng.to_string() + ")") println("") let gc = db.geographic_center() println("=== Geographic Mean Center (sphere-based) ===") println( " Geographic center: (" + gc.lat.to_string() + ", " + gc.lng.to_string() + ")", ) } ///| fn run_geographic_center() -> Unit { let db = build_sample_db() let gc = db.geographic_center() println("=== Geographic Mean Center ===") println(" DB has " + db.all().length().to_string() + " entries") println( " Sphere-based center: (" + gc.lat.to_string() + ", " + gc.lng.to_string() + ")", ) let c = db.centroid() println( " Planar mean: (" + c.lat.to_string() + ", " + c.lng.to_string() + ")", ) let diff = @geo.haversine(gc, c) println(" Difference: " + diff.to_string() + " km") } ///| fn run_bbox_query( min_lat : Double, max_lat : Double, min_lng : Double, max_lng : Double, ) -> Unit { println("=== Bounding Box Query ===") println( " Region: lat [" + min_lat.to_string() + ", " + max_lat.to_string() + "], lng [" + min_lng.to_string() + ", " + max_lng.to_string() + "]", ) let db = build_sample_db() let results = db.find_bbox(min_lat, max_lat, min_lng, max_lng) println("") if results.length() == 0 { println(" No entries found in this region.") } else { println(" Found " + results.length().to_string() + " entries:") for e in results { println( " - " + e.id + " | " + e.name + " | (" + e.location.lat.to_string() + ", " + e.location.lng.to_string() + ")", ) } } } // ---- Moran's I handler ---- ///| fn run_morans_i() -> Unit { let db = build_sample_db() println("=== Moran's I Spatial Autocorrelation ===") let lat_res = db.morans_i_on_lat() match lat_res { None => println(" (computation failed)") Some(r) => { println(" Moran's I (on latitude): " + r.index.to_string()) println(" Expected (random): " + r.expected.to_string()) println(" Interpretation:") let i = r.index if i > 0.3 { println(" → Strong positive spatial autocorrelation (clustered)") } else if i > 0.1 { println(" → Moderate positive spatial autocorrelation") } else if i > -0.1 { println(" → Near zero: random spatial pattern") } else if i > -0.3 { println(" → Moderate negative autocorrelation (dispersed)") } else { println(" → Strong negative autocorrelation") } } } let lng_res = db.morans_i_on_lng() match lng_res { None => () Some(r) => println(" Moran's I (on longitude): " + r.index.to_string()) } } // ---- Standard Deviation Ellipse handler ---- ///| fn run_std_dev_ellipse() -> Unit { let db = build_sample_db() println("=== Standard Deviation Ellipse ===") match db.std_dev_ellipse() { None => println(" (computation failed)") Some(sde) => { println( " Center: (" + sde.center.lat.to_string() + ", " + sde.center.lng.to_string() + ")", ) println(" Semi-major axis: " + sde.semi_major_km.to_string() + " km") println(" Semi-minor axis: " + sde.semi_minor_km.to_string() + " km") println( " Rotation: " + sde.rotation_deg.to_string() + "° (" + @geo.compass_direction(sde.rotation_deg) + ")", ) println(" Area: " + sde.area_sq_km.to_string() + " sq km") println(" Eccentricity: " + sde.eccentricity.to_string()) let shape = if sde.eccentricity < 0.3 { "roughly circular" } else if sde.eccentricity < 0.7 { "moderately elongated" } else { "strongly elongated" } println(" Shape: " + shape) } } } // ---- TSP nearest-neighbor handler ---- ///| fn run_tsp(start_id : String) -> Unit { let db = build_sample_db() println("=== TSP Nearest-Neighbor Route ===") println(" Start: " + start_id) match db.tsp_nearest(start_id) { None => { println(" Error: start id not found") let all = db.all() println(" Available ids:") for e in all { println(" - " + e.id) } } Some((route, total_km)) => { println(" Total distance: " + total_km.to_string() + " km") println(" Route (" + route.length().to_string() + " stops):") let mut idx = 0 for stop in route { let label = if idx == 0 { "START" } else if idx == route.length() - 1 { "END " } else { " " } println( " " + label + " " + stop.entry.id + " | " + stop.entry.name + " | cum=" + stop.cum_distance_km.to_string() + " km", ) idx = idx + 1 } } } } ///| TSP 2-opt CLI. fn run_tsp_2opt(start_id : String) -> Unit { let db = build_sample_db() println("=== TSP: Nearest-Neighbor + 2-opt Improvement ===") println(" Start: " + start_id) match db.tsp_2opt(start_id) { None => { println(" Error: start id not found") } Some((route, total_km)) => { // Also compute pure NN route for comparison. let nn = db.tsp_nearest(start_id) match nn { Some((_, nn_total)) => { let saved = nn_total - total_km let pct = if nn_total > 0.0 { saved / nn_total * 100.0 } else { 0.0 } println( " NN total: " + nn_total.to_string() + " km", ) println( " 2-opt total: " + total_km.to_string() + " km", ) println( " Saved: " + saved.to_string() + " km (" + pct.to_string() + "%)", ) } None => () } println(" Route (" + route.length().to_string() + " stops):") let mut idx = 0 for stop in route { let label = if idx == 0 { "START" } else if idx == route.length() - 1 { "END " } else { " " } println( " " + label + " " + stop.entry.id + " | " + stop.entry.name + " | cum=" + stop.cum_distance_km.to_string() + " km", ) idx = idx + 1 } } } } ///| IDW interpolation CLI. Predicts longitude at the given (lat, lng) ///| based on sample values distributed across the sample DB. We use the ///| longitude of each entry as the attribute to predict, so a sensible ///| query (lat near Beijing, lng between Beijing and Shanghai) shows ///| interpolation at work. fn run_idw(lat_str : String, lng_str : String, power_str : String) -> Unit { let lat = try { @string.parse_double(lat_str) } catch { _ => 39.9 } let lng = try { @string.parse_double(lng_str) } catch { _ => 118.0 } let power = try { @string.parse_double(power_str) } catch { _ => 2.0 } let db = build_sample_db() let q = @types.GeoPoint::make(lat, lng) println("=== IDW Inverse Distance Weighting ===") println( " Query point: (" + lat.to_string() + "°N, " + lng.to_string() + "°E)", ) println(" Power: " + power.to_string()) // Predict longitude attribute. match db.idw_predict(q, fn(e) { e.location.lng }, power~) { None => println(" No prediction (empty DB)") Some(pred_lng) => { println(" Predicted longitude: " + pred_lng.to_string() + "°E") println( " Distance from query to nearest entry: " + find_nearest_dist(db, q).to_string() + " km", ) } } // Also show KNN version. let knn = db.idw_predict_knn(q, 5, fn(e) { e.location.lng }, power~) match knn { Some(v) => println(" KNN(5) version: " + v.to_string() + "°E") None => () } } ///| Concave hull CLI. fn run_concave_hull(alpha_str : String) -> Unit { let alpha = try { @string.parse_double(alpha_str) } catch { _ => 1.0 } let db = build_sample_db() println("=== Concave Hull (Alpha-Shape) ===") println(" Alpha: " + alpha.to_string()) let hull = db.concave_hull(alpha) println(" Hull vertices: " + hull.length().to_string()) for p in hull { println( " (" + p.lat.to_string() + "°N, " + p.lng.to_string() + "°E)", ) } // Compare with convex hull. let entries = db.all() let pts : Array[@types.GeoPoint] = [] for e in entries { pts.push(e.location) } let convex = @geo.convex_hull(pts) println(" Convex hull vertices: " + convex.length().to_string()) println(" (alpha → 0 recovers convex hull; alpha → ∞ tends toward all points)") } ///| Helper: distance from a query point to the nearest DB entry. fn find_nearest_dist(db : @db.GeoDB, q : @types.GeoPoint) -> Double { let arr = db.find_nearest(q, 1) if arr.length() > 0 { arr[0].1 } else { 0.0 } } ///| Moran's I with KNN weights. fn run_moran_i_knn() -> Unit { let db = build_sample_db() println("=== Moran's I (KNN weights, k=8) ===") match db.morans_i_knn_on_lat(k=8) { None => println(" No result (too few points)") Some(r) => { println(" I (latitude): " + r.index.to_string()) let label = if r.index > 0.1 { "clustered" } else if r.index < -0.1 { "dispersed" } else { "random" } println(" Pattern: " + label) } } } // ---- Benchmark ---- ///| Simple benchmark: builds a DB of N random entries and times several ///| core queries so we can see the index working at scale. fn run_benchmark(n : Int) -> Unit { let n = if n < 10 { 100 } else { n } println("=== Benchmark (N=" + n.to_string() + ") ===") // Build let t0 = @bench.monotonic_clock_start() let db = @gen.gen_db(n) let t_build = @bench.monotonic_clock_end(t0) println(" Build DB: " + t_build.to_string() + " us") println(" Entries: " + db.size().to_string()) // Binary persistence: full snapshot write + cold reopen (index rebuild). let bench_path = "/tmp/moonbitgeodb_bench_" + n.to_string() + ".db" let t_save0 = @bench.monotonic_clock_start() ignore(db.save_to(bench_path)) let t_save = @bench.monotonic_clock_end(t_save0) println(" Save DB: " + t_save.to_string() + " us") let t_open0 = @bench.monotonic_clock_start() let reopened = @db.GeoDB::open(bench_path) let t_open = @bench.monotonic_clock_end(t_open0) println(" Open DB: " + t_open.to_string() + " us") println(" Loaded entries: " + reopened.size().to_string()) ignore(@persist.remove_file(bench_path)) // Write-ahead-log path: 100 single-entry writes with auto_save enabled. let wal_bench_path = "/tmp/moonbitgeodb_bench_wal_" + n.to_string() + ".db" ignore(@persist.remove_file(wal_bench_path)) @persist.clear_wal(wal_bench_path) let wdb = @db.GeoDB::open(wal_bench_path, auto_save=true) let t_wal0 = @bench.monotonic_clock_start() let mut wi = 0 while wi < 100 { wdb.insert( @types.GeoEntry::make( "w" + wi.to_string(), "W", @types.Address::empty(), @types.GeoPoint::make(0.0, 0.0), ), ) wi = wi + 1 } let t_wal = @bench.monotonic_clock_end(t_wal0) println(" WAL insert x100: " + t_wal.to_string() + " us") ignore(wdb.save()) @persist.clear_wal(wal_bench_path) ignore(@persist.remove_file(wal_bench_path)) // BBox query (over Beijing area) let t1 = @bench.monotonic_clock_start() let _bbox_res = db.find_bbox(39.0, 40.0, 116.0, 117.0) let t_bbox = @bench.monotonic_clock_end(t1) println(" BBox query: " + t_bbox.to_string() + " us") // KNN let t2 = @bench.monotonic_clock_start() let _nn = db.find_nearest(@types.GeoPoint::make(39.9, 116.4), 5) let t_nn = @bench.monotonic_clock_end(t2) println(" KNN query (k=5): " + t_nn.to_string() + " us") // Within radius let t3 = @bench.monotonic_clock_start() let _within = db.find_within_distance(@types.GeoPoint::make(39.9, 116.4), 100.0) let t_within = @bench.monotonic_clock_end(t3) println(" Within 100km: " + t_within.to_string() + " us") // Top-limit nearby query (R-tree kNN + radius filter). let t_nb0 = @bench.monotonic_clock_start() let _nearby = db.find_nearby( @types.GeoPoint::make(39.9, 116.4), radius_km=100.0, limit=10, ) let t_nb = @bench.monotonic_clock_end(t_nb0) println(" Nearby top10: " + t_nb.to_string() + " us") // K-Means (k=3) let t4 = @bench.monotonic_clock_start() let _km = db.kmeans(3) let t_km = @bench.monotonic_clock_end(t4) println(" K-Means (k=3): " + t_km.to_string() + " us") // DBSCAN (eps=50km, min_points=5) — realistic urban clustering scale let t5 = @bench.monotonic_clock_start() let _dbscan = db.cluster_dbscan(50.0, 5) let t_dbscan = @bench.monotonic_clock_end(t5) println(" DBSCAN (eps50): " + t_dbscan.to_string() + " us") // TSP 2-opt from first entry let t6 = @bench.monotonic_clock_start() let _tsp = db.tsp_2opt(db.all()[0].id) let t_tsp = @bench.monotonic_clock_end(t6) println(" TSP 2-opt: " + t_tsp.to_string() + " us") // Moran's I KNN on latitude let t7 = @bench.monotonic_clock_start() let _mi = db.morans_i_knn_on_lat(k=8) let t_mi = @bench.monotonic_clock_end(t7) println(" Moran's I KNN: " + t_mi.to_string() + " us") // Kriging: empirical variogram + model fit + KNN interpolation let t8 = @bench.monotonic_clock_start() let kpts : Array[@types.GeoPoint] = [] let kvals : Array[Double] = [] for e in db.all() { kpts.push(e.location) kvals.push(e.location.lat) } let kbins = @geo.empirical_variogram(kpts, kvals, max_lag_km=None, num_lags=10) let kfit = @geo.fit_variogram(kbins) let _kpred = @geo.ordinary_kriging_knn( kpts, kvals, @types.GeoPoint::make(35.0, 115.0), kfit, k=12, ) let t_kriging = @bench.monotonic_clock_end(t8) println(" Kriging KNN: " + t_kriging.to_string() + " us") // Moran's I significance test (R-tree-built mutual-kNN weights). let t_mt0 = @bench.monotonic_clock_start() let _mt = db.morans_i_test(kvals, k=8) let t_mt = @bench.monotonic_clock_end(t_mt0) println(" Moran test: " + t_mt.to_string() + " us") // Second call hits the cached mutual-kNN weights. let t_mt2_0 = @bench.monotonic_clock_start() let _mt2 = db.morans_i_test(kvals, k=8) let t_mt2 = @bench.monotonic_clock_end(t_mt2_0) println(" Moran test 2: " + t_mt2.to_string() + " us") // IDW through the DB API (R-tree kNN instead of full scan). let t_idw0 = @bench.monotonic_clock_start() let _idw = db.idw_predict_knn( @types.GeoPoint::make(35.0, 115.0), 12, fn(e) { e.location.lat }, ) let t_idw = @bench.monotonic_clock_end(t_idw0) println(" IDW KNN: " + t_idw.to_string() + " us") // Kriging through the DB API (R-tree kNN instead of full scan). let t_kdb0 = @bench.monotonic_clock_start() let _kdb = db.kriging_predict_knn( @types.GeoPoint::make(35.0, 115.0), fn(e) { e.location.lat }, kfit, k=12, ) let t_kdb = @bench.monotonic_clock_end(t_kdb0) println(" Kriging DB KNN: " + t_kdb.to_string() + " us") // Name prefix query (sorted-key cache + binary search). let t_prefix0 = @bench.monotonic_clock_start() let _prefix = db.find_name_prefix("北") let t_prefix = @bench.monotonic_clock_end(t_prefix0) println(" Name prefix: " + t_prefix.to_string() + " us") // Case-sensitive name prefix query (name-sorted cache + binary search). let t_pc0 = @bench.monotonic_clock_start() let _pc = db.find_name_prefix_case("北") let t_pc = @bench.monotonic_clock_end(t_pc0) println(" Name prefix case:" + t_pc.to_string() + " us") // Name substring query (single char falls back to a full scan). let t_contains0 = @bench.monotonic_clock_start() let _contains = db.find_name_contains("路") let t_contains = @bench.monotonic_clock_end(t_contains0) println(" Name contains: " + t_contains.to_string() + " us") // Two-character needle: first query builds the lazy 2-gram index. let t_c2c0 = @bench.monotonic_clock_start() let _c2c = db.find_name_contains("新区") let t_c2c = @bench.monotonic_clock_end(t_c2c0) println(" Name contains 2 cold: " + t_c2c.to_string() + " us") let t_c20 = @bench.monotonic_clock_start() let _c2 = db.find_name_contains("新区") let t_c2 = @bench.monotonic_clock_end(t_c20) println(" Name contains 2: " + t_c2.to_string() + " us") // Long needle: the 3-gram index narrows the candidate set. let sample_name = db.all()[0].name let t_cl0 = @bench.monotonic_clock_start() let _cl = db.find_name_contains(sample_name) let t_cl = @bench.monotonic_clock_end(t_cl0) println(" Name contains L: " + t_cl.to_string() + " us") // Wildcard query with a long literal run, prefiltered by the 3-gram index. let t_pat0 = @bench.monotonic_clock_start() let _pat = db.find_name_pattern("*" + sample_name + "*") let t_pat = @bench.monotonic_clock_end(t_pat0) println(" Name pattern: " + t_pat.to_string() + " us") // Fuzzy name search (length-pruned Levenshtein). let t_fuzzy0 = @bench.monotonic_clock_start() let _fuzzy = db.find_name_fuzzy("北京", max_distance=2) let t_fuzzy = @bench.monotonic_clock_end(t_fuzzy0) println(" Name fuzzy: " + t_fuzzy.to_string() + " us") // Global nearest pairs (top-k via R-tree). let t_pairs0 = @bench.monotonic_clock_start() let _pairs = db.find_nearest_pairs(10) let t_pairs = @bench.monotonic_clock_end(t_pairs0) println(" Nearest pairs: " + t_pairs.to_string() + " us") // Duplicate detection (spatial grid prefilter + Levenshtein). let t_dup0 = @bench.monotonic_clock_start() let _dups = db.find_duplicates() let t_dup = @bench.monotonic_clock_end(t_dup0) println(" Find duplicates: " + t_dup.to_string() + " us") // Distance matrix over the first 100 ids. let all_entries = db.all() let ids : Array[String] = [] let mut di = 0 while di < 100 && di < all_entries.length() { ids.push(all_entries[di].id) di = di + 1 } let t_matrix0 = @bench.monotonic_clock_start() let _matrix = db.distance_matrix(ids) let t_matrix = @bench.monotonic_clock_end(t_matrix0) println(" Distance matrix: " + t_matrix.to_string() + " us") // All-pairs distance statistics (exact below 2000 entries). let t_ds0 = @bench.monotonic_clock_start() let _ds = db.distance_summary() let t_ds = @bench.monotonic_clock_end(t_ds0) println(" Distance summary: " + t_ds.to_string() + " us") // Geohash 3x3 neighbourhood resolved through the spatial index. if all_entries.length() > 0 { let gh = @geo.geohash_encode( all_entries[0].location.lat, all_entries[0].location.lng, 6, ) let t_gh0 = @bench.monotonic_clock_start() let _ghres = db.find_by_geohash_bbox(gh) let t_gh = @bench.monotonic_clock_end(t_gh0) println(" Geohash bbox: " + t_gh.to_string() + " us") } // Farthest pair (convex hull, used by summary). let t_max0 = @bench.monotonic_clock_start() let _max = db.compute_max_distance() let t_max = @bench.monotonic_clock_end(t_max0) println(" Max distance: " + t_max.to_string() + " us") // Combined bbox (cached in the index). let t_cb0 = @bench.monotonic_clock_start() let _cb = db.compute_bbox() let t_cb = @bench.monotonic_clock_end(t_cb0) println(" Compute bbox: " + t_cb.to_string() + " us") // Semantic address matching (prepared query reused for all candidates). let t_match0 = @bench.monotonic_clock_start() let _matched = db.match_addresses("北京市海淀区颐和园路", limit=10) let t_match = @bench.monotonic_clock_end(t_match0) println(" Address match: " + t_match.to_string() + " us") // Summary report (cached center/bbox + merged distributions). let t_sum0 = @bench.monotonic_clock_start() let _sum = db.summary() let t_sum = @bench.monotonic_clock_end(t_sum0) println(" Summary: " + t_sum.to_string() + " us") // CSV / JSON export (single-pass escaping). let t_csv0 = @bench.monotonic_clock_start() let _csv = db.to_csv() let t_csv = @bench.monotonic_clock_end(t_csv0) println(" Export CSV: " + t_csv.to_string() + " us") let t_json0 = @bench.monotonic_clock_start() let _json = db.to_json() let t_json = @bench.monotonic_clock_end(t_json0) println(" Export JSON: " + t_json.to_string() + " us") let t_gj0 = @bench.monotonic_clock_start() let _gj = db.to_geojson() let t_gj = @bench.monotonic_clock_end(t_gj0) println(" Export GeoJSON: " + t_gj.to_string() + " us") let t_gjb0 = @bench.monotonic_clock_start() let _gjb = db.to_geojson_with_bbox() let t_gjb = @bench.monotonic_clock_end(t_gjb0) println(" Export GJ bbox: " + t_gjb.to_string() + " us") println(" ---") let us_per_entry = t_build / n.to_double() println(" Build per entry: " + us_per_entry.to_string() + " us") } // ---- Spatial autocorrelation / hotspot analysis ---- ///| fn run_morans_i_lat() -> Unit { let db = build_sample_db() println("=== Global Moran's I (Latitude) ===") match db.morans_i_on_lat() { None => println(" Not enough data or zero variance.") Some(r) => { println(" Moran's I: " + r.index.to_string()) println(" Expected I: " + r.expected.to_string()) println(" Observations: " + r.n.to_string()) } } } ///| fn run_morans_i_lng() -> Unit { let db = build_sample_db() println("=== Global Moran's I (Longitude) ===") match db.morans_i_on_lng() { None => println(" Not enough data or zero variance.") Some(r) => { println(" Moran's I: " + r.index.to_string()) println(" Expected I: " + r.expected.to_string()) println(" Observations: " + r.n.to_string()) } } } ///| fn run_hotspot_lat() -> Unit { let db = build_sample_db() println("=== Getis-Ord Gi* Hot/Cold Spots (Latitude) ===") let entries = db.all() let vals : Array[Double] = [] for e in entries { vals.push(e.location.lat) } let results = db.getis_ord(vals, k=5, threshold=1.96) let mut hot = 0 let mut cold = 0 for r in results { if r.is_hotspot { hot = hot + 1 } if r.is_coldspot { cold = cold + 1 } } println(" Entries: " + results.length().to_string()) println(" Hotspots: " + hot.to_string() + " Coldspots: " + cold.to_string()) println(" Top hotspots:") let sorted : Array[(Double, @geo.GetisResult)] = [] for r in results { if r.is_hotspot || r.is_coldspot { sorted.push((r.gi_star.abs(), r)) } } sorted.sort_by(fn(a, b) { if a.0 < b.0 { 1 } else if a.0 > b.0 { -1 } else { 0 } }) let mut shown = 0 for (_, r) in sorted { if shown >= 5 { break } match db.get(entries[r.index].id) { None => () Some(e) => { let label = if r.is_hotspot { "HOT" } else { "COLD" } println( " [" + label + "] " + e.id + " | " + e.name + " Gi*=" + r.gi_star.to_string(), ) shown = shown + 1 } } } } ///| fn run_hotspot_lng() -> Unit { let db = build_sample_db() println("=== Getis-Ord Gi* Hot/Cold Spots (Longitude) ===") let entries = db.all() let vals : Array[Double] = [] for e in entries { vals.push(e.location.lng) } let results = db.getis_ord(vals, k=5, threshold=1.96) let mut hot = 0 let mut cold = 0 for r in results { if r.is_hotspot { hot = hot + 1 } if r.is_coldspot { cold = cold + 1 } } println(" Entries: " + results.length().to_string()) println(" Hotspots: " + hot.to_string() + " Coldspots: " + cold.to_string()) } ///| fn run_lisa_lat() -> Unit { let db = build_sample_db() println("=== Local Moran's I (LISA) (Latitude) ===") let entries = db.all() let vals : Array[Double] = [] for e in entries { vals.push(e.location.lat) } let results = db.lisa(vals, k=5, threshold=1.96) let mut hh = 0 let mut ll = 0 let mut hl = 0 let mut lh = 0 let mut ns = 0 for r in results { match r.cluster { "HH" => hh = hh + 1 "LL" => ll = ll + 1 "HL" => hl = hl + 1 "LH" => lh = lh + 1 _ => ns = ns + 1 } } println(" Entries: " + results.length().to_string()) println(" HH (high-high): " + hh.to_string()) println(" LL (low-low): " + ll.to_string()) println(" HL (high-low): " + hl.to_string()) println(" LH (low-high): " + lh.to_string()) println(" NS (not sig): " + ns.to_string()) } ///| fn run_lisa_lng() -> Unit { let db = build_sample_db() println("=== Local Moran's I (LISA) (Longitude) ===") let entries = db.all() let vals : Array[Double] = [] for e in entries { vals.push(e.location.lng) } let results = db.lisa(vals, k=5, threshold=1.96) let mut hh = 0 let mut ll = 0 let mut hl = 0 let mut lh = 0 let mut ns = 0 for r in results { match r.cluster { "HH" => hh = hh + 1 "LL" => ll = ll + 1 "HL" => hl = hl + 1 "LH" => lh = lh + 1 _ => ns = ns + 1 } } println(" Entries: " + results.length().to_string()) println(" HH (high-high): " + hh.to_string()) println(" LL (low-low): " + ll.to_string()) println(" HL (high-low): " + hl.to_string()) println(" LH (low-high): " + lh.to_string()) println(" NS (not sig): " + ns.to_string()) } // ---- Delaunay triangulation ---- ///| fn run_delaunay() -> Unit { let db = build_sample_db() println("=== Delaunay Triangulation ===") let entries = db.all() let tris = db.delaunay() println(" Triangles: " + tris.length().to_string()) let mut shown = 0 for t in tris { if shown >= 10 { break } if t.a < entries.length() && t.b < entries.length() && t.c < entries.length() { let a = entries[t.a] let b = entries[t.b] let c = entries[t.c] println(" (" + a.id + " | " + b.id + " | " + c.id + ")") shown = shown + 1 } } if tris.length() > shown { println(" ... and " + (tris.length() - shown).to_string() + " more") } } ///| fn run_delaunay_edges() -> Unit { let db = build_sample_db() println("=== Delaunay Edges ===") let entries = db.all() let edges = db.delaunay_edges() println(" Edges: " + edges.length().to_string()) let mut shown = 0 for e in edges { if shown >= 15 { break } if e.a < entries.length() && e.b < entries.length() { let a = entries[e.a] let b = entries[e.b] println(" " + a.id + " <-> " + b.id) shown = shown + 1 } } if edges.length() > shown { println(" ... and " + (edges.length() - shown).to_string() + " more") } } ///| fn run_kriging() -> Unit { let db = build_sample_db() println("=== Kriging Interpolation ===") let entries = db.all() if entries.length() < 2 { println(" Need at least 2 entries for kriging") return } let args = @env.args() let lat_str = if args.length() > 2 { args.get(2).unwrap_or("39.9") } else { "39.9" } let lng_str = if args.length() > 3 { args.get(3).unwrap_or("116.4") } else { "116.4" } let k_str = if args.length() > 4 { args.get(4).unwrap_or("12") } else { "12" } let lat = try { @string.parse_double(lat_str) } catch { _ => 39.9 } let lng = try { @string.parse_double(lng_str) } catch { _ => 116.4 } let k = try { @string.parse_int(k_str) } catch { _ => 12 } let query = @types.GeoPoint::make(lat, lng) println(" Query point: (" + lat.to_string() + ", " + lng.to_string() + ")") println(" Using k=" + k.to_string() + " nearest neighbors") // Build points and values let pts : Array[@types.GeoPoint] = [] let vals : Array[Double] = [] for e in entries { pts.push(e.location) // Use latitude as the value to interpolate (for demo purposes) vals.push(e.location.lat) } // Compute empirical variogram let bins = @geo.empirical_variogram(pts, vals, max_lag_km=None, num_lags=10) if bins.length() < 2 { println(" Not enough variogram bins") return } // Fit variogram model let fitted = @geo.fit_variogram(bins) println(" Fitted variogram: nugget=" + fitted.nugget.to_string() + ", sill=" + fitted.sill.to_string() + ", range=" + fitted.range.to_string() + " km") // Kriging prediction let pred = @geo.ordinary_kriging_knn(pts, vals, query, fitted, k~) match pred { None => println(" Prediction: None (insufficient data)") Some(v) => println(" Prediction: " + v.to_string()) } }