2026-10-14 11:16:43 +00:00
// garmin2osm: original Garmin geometry -> OSM XML (not raster), GPL-3.0-or-later.
// Shares the tested IMG/TRE/RGN/TYP decoders with garmin2osmand.
#define GARMIN_LIBRARY_MODE
#include "main.cpp"
#include <chrono>
#include <unordered_set>
#include <cctype>
#include <atomic>
#include <thread>
#include <mutex>
#include <optional>
namespace vector_export {
using Tags = std :: vector < std :: pair < std :: string , std :: string >> ;
std :: string xmlEscape ( const std :: string & s ){ std :: string v ; v . reserve ( s . size ()); for ( unsigned char c : s ){ switch ( c ){ case '&' : v += "&" ; break ; case '<' : v += "<" ; break ; case '>' : v += ">" ; break ; case '"' : v += """ ; break ; case '\'' : v += "'" ; break ; default : if ( c >= 32 || c == 9 || c == 10 || c == 13 ) v += char ( c );}} return v ;}
std :: string code ( int t ){ char b [ 20 ]; std :: snprintf ( b , sizeof ( b ), "0x%x" , t ); return b ;}
std :: string hexColor ( U32 c ){ char b [ 16 ]; std :: snprintf ( b , sizeof ( b ), "#%02x%02x%02x" ,( c >> 24 ) & 255 ,( c >> 16 ) & 255 ,( c >> 8 ) & 255 ); return b ;}
void tag ( Tags & ts , std :: string k , std :: string v ){ if ( ! v . empty ()) ts . emplace_back ( std :: move ( k ), std :: move ( v ));}
std :: string makeTags ( const Tags & tags ){ std :: string out ; for ( auto & [ k , v ] : tags ) out += "<tag k= \" " + xmlEscape ( k ) + " \" v= \" " + xmlEscape ( v ) + " \" />" ; return out ;}
2026-10-14 17:57:55 +00:00
struct Options { std :: string input , output , temp , poiOverrides , poiTypesOut , poiManifest , iconsDir , poiAdditions , poiNameFixes , poiLabels ; BBox bbox ; bool clip = false , inspect = false , contours = false , allLevels = false , keepTemp = false ; int limitMaps = 0 , jobs = 1 , examples = 5 ;};
2026-10-14 11:16:43 +00:00
Options parse ( int argc , char ** argv ){ Options o ; for ( int i = 1 ; i < argc ; i ++ ){ std :: string a = argv [ i ]; auto next = [ & ](){ if ( i + 1 >= argc ) fail ( "Missing value for " + a ); return std :: string ( argv [ ++ i ]);};
2026-10-14 17:57:55 +00:00
if ( a == "--help" || a == "-h" ){ std :: cout << "garmin2osm v0.6 --input MAP.img [--output map.osm] [--inspect] [--poi-overrides rules.csv] [--poi-name-fixes fixes.csv] [--poi-labels labels.csv] [--poi-additions additions.csv] [--poi-types-out unique-poi-types.csv] [--poi-manifest source-pois.csv] [--poi-examples N] [--icons-dir path] [--jobs N] [--bbox W,S,E,N] [--contours] [--keep-temp] \n " ; std :: exit ( 0 );} else if ( a == "--input" ) o . input = next (); else if ( a == "--output" ) o . output = next (); else if ( a == "--temp" ) o . temp = next (); else if ( a == "--inspect" ) o . inspect = true ; else if ( a == "--contours" ) o . contours = true ; else if ( a == "--all-levels" ) o . allLevels = true ; else if ( a == "--keep-temp" ) o . keepTemp = true ; else if ( a == "--limit-maps" ) o . limitMaps = std :: stoi ( next ()); else if ( a == "--poi-overrides" ) o . poiOverrides = next (); else if ( a == "--poi-name-fixes" ) o . poiNameFixes = next (); else if ( a == "--poi-labels" ) o . poiLabels = next (); else if ( a == "--poi-additions" ) o . poiAdditions = next (); else if ( a == "--poi-types-out" ) o . poiTypesOut = next (); else if ( a == "--poi-manifest" ) o . poiManifest = next (); else if ( a == "--icons-dir" ) o . iconsDir = next (); else if ( a == "--poi-examples" ) o . examples = std :: stoi ( next ()); else if ( a == "--jobs" ) o . jobs = std :: stoi ( next ()); else if ( a == "--bbox" ){ std :: string s = next (); std :: replace ( s . begin (), s . end (), ',' , ' ' ); double w , ss , e , n ; if ( std :: sscanf ( s . c_str (), "%lf%lf%lf%lf" , & w , & ss , & e , & n ) != 4 ) fail ( "Bad bbox" ); o . bbox = { w , ss , e , n }; o . clip = true ;} else fail ( "Unknown argument: " + a );}
2026-10-14 11:16:43 +00:00
if ( o . input . empty ()) fail ( "--input required" ); if ( o . output . empty () &&! o . inspect && o . poiTypesOut . empty () && o . poiManifest . empty () && o . iconsDir . empty ()) fail ( "--output, --inspect or an inventory output required" ); if ( ! o . poiAdditions . empty () && o . output . empty ()) fail ( "--poi-additions requires --output" ); if ( o . jobs < 1 || o . jobs > 64 ) fail ( "--jobs must be 1..64" ); if ( o . examples < 1 || o . examples > 20 ) fail ( "--poi-examples must be 1..20" ); if ( o . clip && ( ! o . bbox . valid () || o . bbox . w < - 180 || o . bbox . e > 180 || o . bbox . s < - 90 || o . bbox . n > 90 )) fail ( "Invalid bbox" ); return o ;}
Tags lineTags ( int t , bool ext , const std :: string & name ){ Tags v ; tag ( v , "garmin:type" , code ( t )); tag ( v , "garmin:feature" , ext ? "extended_polyline" : "polyline" ); tag ( v , "name" , name );
if ( ext ){ // Physical trail identity not proven: preserve Garmin overlay, mark it as a hiking path geometry.
tag ( v , "highway" , "path" ); tag ( v , "foot" , "yes" ); tag ( v , "trailblazed" , "yes" ); tag ( v , "garmin:route_overlay" , "yes" );
const auto it = gStyles . lines . find ( t ); if ( it != gStyles . lines . end () && it -> second . color ) tag ( v , "colour" , hexColor ( it -> second . color )); return v ;
}
if ( t <= 0x0c ){ const char * klass = nullptr ;
switch ( t ){ case 0 : klass = "road" ; break ; case 1 : klass = "motorway" ; break ; case 2 : klass = "primary" ; break ; case 3 : klass = "secondary" ; break ; case 4 : klass = "tertiary" ; break ; case 5 : klass = "unclassified" ; break ; case 6 : klass = "residential" ; break ; case 7 : klass = "service" ; break ; case 8 : case 9 : case 11 : klass = "motorway_link" ; break ; case 10 : klass = "track" ; break ; case 12 : klass = "residential" ; break ;}
if ( klass ) tag ( v , "highway" , klass ); if ( t == 10 ) tag ( v , "surface" , "unpaved" ); if ( t == 12 ) tag ( v , "junction" , "roundabout" ); return v ;}
switch ( t ){ case 0x0f : case 0x13 : case 0x16 : tag ( v , "highway" , "path" ); break ;
case 0x14 : tag ( v , "railway" , "rail" ); break ; case 0x15 : tag ( v , "natural" , "coastline" ); break ;
case 0x18 : case 0x26 : tag ( v , "waterway" , "stream" ); if ( t == 0x26 ) tag ( v , "intermittent" , "yes" ); break ;
case 0x1f : tag ( v , "waterway" , "river" ); break ; case 0x1a : case 0x1b : tag ( v , "route" , "ferry" ); break ;
case 0x1c : case 0x1d : case 0x1e : tag ( v , "boundary" , "administrative" ); break ;
case 0x20 : case 0x21 : case 0x22 : case 0x23 : case 0x24 : case 0x25 : tag ( v , "contour" , "elevation" ); break ;
case 0x27 : tag ( v , "aeroway" , "runway" ); break ; case 0x29 : tag ( v , "power" , "line" ); break ;
default : break ;} return v ;
}
Tags areaTags ( int t , const std :: string & name ){ Tags v ; tag ( v , "garmin:type" , code ( t )); tag ( v , "name" , name );
switch ( t ){ case 0x01 : case 0x02 : case 0x03 : tag ( v , "landuse" , "residential" ); break ;
case 0x05 : tag ( v , "amenity" , "parking" ); break ; case 0x07 : tag ( v , "aeroway" , "aerodrome" ); break ;
case 0x0b : tag ( v , "amenity" , "hospital" ); break ; case 0x0c : tag ( v , "landuse" , "industrial" ); break ;
case 0x13 : tag ( v , "building" , "yes" ); break ; case 0x17 : tag ( v , "leisure" , "park" ); break ;
case 0x18 : tag ( v , "leisure" , "golf_course" ); break ; case 0x09 : tag ( v , "leisure" , "marina" ); break ;
case 0x14 : case 0x15 : case 0x16 : tag ( v , "boundary" , "national_park" ); break ;
case 0x28 : case 0x32 : tag ( v , "natural" , "water" ); tag ( v , "water" , "sea" ); break ;
case 0x3c : case 0x3d : case 0x40 : case 0x41 : tag ( v , "natural" , "water" ); break ;
case 0x4c : tag ( v , "natural" , "water" ); tag ( v , "intermittent" , "yes" ); break ;
case 0x4d : tag ( v , "natural" , "glacier" ); break ; case 0x4e : tag ( v , "landuse" , "orchard" ); break ;
case 0x4f : tag ( v , "natural" , "scrub" ); break ; case 0x50 : tag ( v , "natural" , "wood" ); break ;
case 0x51 : tag ( v , "natural" , "wetland" ); break ; case 0x52 : tag ( v , "natural" , "grassland" ); break ;
default : break ;} return v ;
}
Tags poiTags ( int t , const std :: string & name ){ Tags v ; tag ( v , "garmin:type" , code ( t )); tag ( v , "garmin:feature" , "poi" ); tag ( v , "name" , name );
int category = ( t >> 8 ) & 255 , subtype = t & 255 ;
if ( t == 0x6616 ){ tag ( v , "natural" , "peak" );}
else if ( t == 0x6412 || t == 0x6503 ){ tag ( v , "tourism" , "information" ); tag ( v , "information" , "guidepost" );}
else if ( t == 0x650b || t == 0x660b ){ tag ( v , "tourism" , "information" ); tag ( v , "information" , "board" );}
else if ( t == 0x6511 || t == 0x6414 ){ tag ( v , "amenity" , "drinking_water" );}
else if ( t == 0x6617 ){ tag ( v , "natural" , "peak" );}
else if ( t == 0x6609 || t == 0x660a || t == 0x6405 ){ tag ( v , "tourism" , t == 0x660a ? "wilderness_hut" : "alpine_hut" );}
else if ( t == 0x6408 ){ tag ( v , "emergency" , "mountain_rescue" );}
else if ( t == 0x661e || t == 0x6415 ){ tag ( v , "amenity" , "shelter" );}
else if ( t == 0x660c ){ tag ( v , "natural" , "saddle" );}
else if ( t == 0x6613 ){ tag ( v , "waterway" , "waterfall" );}
else if ( t == 0x6407 ){ tag ( v , "tourism" , "picnic_site" );}
else if ( t == 0x640d ){ tag ( v , "aerialway" , "station" );}
else if ( t == 0x6404 || t == 0x640b || t == 0x641a || t == 0x2c0b ){ tag ( v , "amenity" , "place_of_worship" );}
else if ( t == 0x6602 || t == 0x6608 ){ tag ( v , "tourism" , "viewpoint" );}
else if ( t == 0x2800 ){ tag ( v , "place" , "locality" );}
else if ( t == 0x6600 ){ // Geographic spot heights; don't misclassify elevations as villages.
try { size_t parsed = 0 ; int n = std :: stoi ( name , & parsed ); if ( parsed == name . size () && n >= 0 && n <= 8848 ) tag ( v , "ele" , std :: to_string ( n ));} catch (...){}
}
else if ( t == 0x6614 ){ tag ( v , "garmin:marker" , "distance_or_index" );}
else if ( t == 0x6500 ){ tag ( v , "man_made" , "water_works" );}
else if ( t == 0x6402 && name . find ( "Посетителски" ) != std :: string :: npos ){ tag ( v , "tourism" , "information" ); tag ( v , "information" , "office" );}
else if ( t == 0x6411 ){ tag ( v , "man_made" , "tower" );}
// 0x6414 handled above as drinking_water: conflicting old water_well branch removed.
// Do not infer well vs spring without TYP icon or field observation.
else if ( category == 0x2b ){ tag ( v , "tourism" , subtype == 5 ? "camp_site" : "hotel" );}
else if ( category == 0x2f && subtype == 0x05 ){ tag ( v , "amenity" , "post_office" );}
else if ( category == 0x2d && subtype == 0x09 ){ tag ( v , "leisure" , "swimming_pool" );}
else if ( t == 0x6501 ){ tag ( v , "amenity" , name . find ( "Ресторант" ) != std :: string :: npos ? "restaurant" : "cafe" );}
// Other type 0x65/0x66 map symbols are custom to this Bulgarian TYP.
// Preserve their Garmin type without asserting a false OSM POI category.
// Some Bulgarian peak labels contain elevation in metres: "Мальовица (2729)".
if ( t == 0x6616 || t == 0x6617 ){
const auto a = name . rfind ( '(' ), b = name . rfind ( ')' );
if ( a != std :: string :: npos && b == name . size () - 1 && b > a + 1 ){
auto digits = name . substr ( a + 1 , b - a - 1 );
if ( digits . size () >= 2 && digits . size () <= 4 && std :: all_of ( digits . begin (), digits . end (),[]( unsigned char c ){ return std :: isdigit ( c ) != 0 ;})) tag ( v , "ele" , digits );
}
}
// Unknown Garmin categories retain original code, not falsely assigned to an OSM POI class.
2026-10-14 12:42:58 +00:00
// BGMountains v0.6: the published legend is authoritative for types with an
// extracted TYP bitmap match. Unknown codes remain unclassified, never guessed.
static const std :: unordered_map < int , std :: pair < std :: string , std :: string >> legendVerified = {
{ 0x200 ,{ "capital_city" , "place=city;capital=yes" }},
{ 0x800 ,{ "large_city" , "place=city" }},
{ 0x900 ,{ "town" , "place=town" }},
{ 0x2c05 ,{ "school" , "amenity=school" }},
{ 0x2c0b ,{ "church" , "amenity=place_of_worship" }},
{ 0x2d04 ,{ "casino" , "amenity=casino" }},
{ 0x2d08 ,{ "ice_rink" , "leisure=ice_rink" }},
{ 0x2e05 ,{ "pharmacy" , "amenity=pharmacy" }},
{ 0x2e07 ,{ "clothes_shop" , "shop=clothes" }},
{ 0x2f0a ,{ "synagogue" , "amenity=place_of_worship;religion=jewish" }},
{ 0x2f0c ,{ "mosque" , "amenity=place_of_worship;religion=muslim" }},
{ 0x2f0e ,{ "car_wash" , "amenity=car_wash" }},
{ 0x3004 ,{ "courthouse" , "amenity=courthouse" }},
{ 0x3008 ,{ "fire_station" , "amenity=fire_station" }},
{ 0x4700 ,{ "port" , "industrial=port" }},
{ 0x6400 ,{ "quarry" , "landuse=quarry" }},
{ 0x6401 ,{ "bridge" , "man_made=bridge" }},
{ 0x6402 ,{ "building" , "building=yes" }},
{ 0x6403 ,{ "graveyard_tombstone" , "" }},
{ 0x6404 ,{ "church" , "amenity=place_of_worship" }},
{ 0x6405 ,{ "hunting_lodge" , "" }},
{ 0x6406 ,{ "barrier_gate" , "barrier=gate" }},
{ 0x6407 ,{ "picnic_site" , "tourism=picnic_site" }},
{ 0x6408 ,{ "mountain_rescue" , "emergency=mountain_rescue" }},
{ 0x6409 ,{ "monument" , "historic=monument" }},
{ 0x640a ,{ "danger" , "" }},
{ 0x640b ,{ "chapel" , "amenity=place_of_worship" }},
{ 0x640c ,{ "mine_shaft" , "man_made=mine_shaft" }},
{ 0x640d ,{ "lift" , "" }},
{ 0x640e ,{ "bus_stop_station" , "highway=bus_stop" }},
{ 0x640f ,{ "graveyard_tombstone" , "" }},
{ 0x6411 ,{ "tower" , "man_made=tower" }},
{ 0x6412 ,{ "trailhead" , "highway=trailhead" }},
{ 0x6413 ,{ "tunnel" , "" }},
{ 0x6414 ,{ "drinking_water" , "amenity=drinking_water" }},
{ 0x6415 ,{ "camp_site" , "tourism=camp_site" }},
{ 0x6416 ,{ "ruins" , "historic=ruins" }},
{ 0x6417 ,{ "well" , "man_made=water_well" }},
{ 0x641a ,{ "church" , "amenity=place_of_worship" }},
{ 0x641b ,{ "church" , "amenity=place_of_worship" }},
{ 0x641c ,{ "bus_stop_station" , "highway=bus_stop" }},
{ 0x6500 ,{ "reservoir" , "" }},
{ 0x6501 ,{ "restaurant" , "amenity=restaurant" }},
{ 0x6502 ,{ "river_passage" , "" }},
{ 0x6503 ,{ "crossroad" , "" }},
{ 0x6505 ,{ "shop" , "shop=yes" }},
{ 0x6506 ,{ "museum" , "tourism=museum" }},
{ 0x650b ,{ "winter_marking" , "" }},
{ 0x650c ,{ "dead_end" , "noexit=yes" }},
{ 0x6511 ,{ "spring" , "natural=spring" }},
{ 0x6600 ,{ "triangulation" , "" }},
{ 0x6601 ,{ "natural_phenomenon" , "" }},
{ 0x6602 ,{ "viewpoint" , "tourism=viewpoint" }},
{ 0x6603 ,{ "triangulation" , "man_made=survey_point" }},
{ 0x6608 ,{ "hotel" , "tourism=hotel" }},
{ 0x6609 ,{ "hut" , "tourism=alpine_hut" }},
{ 0x660a ,{ "refuge" , "amenity=shelter;shelter_type=basic_hut" }},
{ 0x660b ,{ "mark" , "" }},
{ 0x6610 ,{ "meadow" , "" }},
{ 0x6613 ,{ "waterfall" , "waterway=waterfall" }},
{ 0x6614 ,{ "rock" , "" }},
{ 0x6615 ,{ "cave" , "natural=cave_entrance" }},
{ 0x6617 ,{ "peak" , "natural=peak" }},
{ 0x6618 ,{ "tree" , "natural=tree" }},
{ 0x6619 ,{ "prehistoric_site" , "historic=archaeological_site" }},
{ 0x661a ,{ "antiquity_site" , "historic=archaeological_site" }},
{ 0x661b ,{ "medieval_site" , "historic=archaeological_site" }},
};
const auto hit = legendVerified . find ( t );
if ( hit != legendVerified . end ()){
Tags corrected ;
for ( const auto & kv : v ){
if ( kv . first == "garmin:type" || kv . first == "garmin:feature" || kv . first == "name" ||
( t == 0x6600 && kv . first == "ele" )) corrected . push_back ( kv );
}
tag ( corrected , "garmin:legend" , hit -> second . first );
std :: stringstream rules ( hit -> second . second ); std :: string item ;
while ( std :: getline ( rules , item , ';' )){
auto eq = item . find ( '=' ); if ( eq != std :: string :: npos && eq > 0 )
tag ( corrected , item . substr ( 0 , eq ), item . substr ( eq + 1 ));
}
return corrected ;
}
2026-10-14 11:16:43 +00:00
return v ;
}
// User-supplied CSV mapping: Garmin type, optional name substring, mode, OSM tags, note.
// Every field follows RFC4180-style CSV quoting; mode=replace clears only semantic defaults,
// retaining type/name/mapset and an optional coordinate-derived elevation.
std :: string csvQuote ( const std :: string & s ){ std :: string r = " \" " ; for ( char c : s ){ if ( c == '\"' ) r += " \"\" " ; else if ( c != '\r' && c != '\n' ) r += c ; else r += ' ' ;} return r + " \" " ;}
std :: vector < std :: string > csvParse ( const std :: string & line ){
std :: vector < std :: string > v ; std :: string cur ; bool quoted = false ;
for ( size_t j = 0 ; j < line . size (); j ++ ){ char c = line [ j ]; if ( c == '\"' ){ if ( quoted && j + 1 < line . size () && line [ j + 1 ] == '\"' ){ cur += '\"' ; j ++ ;} else quoted =! quoted ;}
else if ( c == ',' &&! quoted ){ v . push_back ( cur ); cur . clear ();} else cur += c ;}
if ( quoted ) fail ( "Unterminated CSV quotation" ); v . push_back ( cur ); return v ;
}
struct PoiOverride { int type = 0 ; std :: string nameContains , mode , note ; Tags tags ;};
std :: unordered_map < int , std :: vector < PoiOverride >> overrides ;
int parseType ( const std :: string & s ){ size_t n = 0 ; int t = std :: stoi ( s , & n , 0 ); if ( n != s . size () || t < 0 || t > 0x1ffff ) fail ( "Invalid Garmin type: " + s ); return t ;}
void loadOverrides ( const std :: string & file ){ if ( file . empty ()) return ; std :: ifstream in ( file ); if ( ! in ) fail ( "Cannot open POI override file: " + file );
std :: string line ; int n = 0 ; while ( std :: getline ( in , line )){ n ++ ; if ( ! line . empty () && line . back () == '\r' ) line . pop_back (); if ( line . empty () || line [ 0 ] == '#' ) continue ;
auto c = csvParse ( line ); if ( c [ 0 ] == "code" ) continue ; if ( c . size () != 5 ) fail ( "Expected 5 CSV columns at " + file + ":" + std :: to_string ( n ));
PoiOverride rule ; try { rule . type = parseType ( c [ 0 ]);} catch (...){ fail ( "Bad code at " + file + ":" + std :: to_string ( n ));}
rule . nameContains = c [ 1 ]; rule . mode = c [ 2 ]; rule . note = c [ 4 ];
if ( rule . mode != "replace" && rule . mode != "append" && rule . mode != "keep_raw" && rule . mode != "drop" ) fail ( "Bad mode at line " + std :: to_string ( n ));
std :: stringstream kvs ( c [ 3 ]); std :: string item ; std :: set < std :: string > keys ;
while ( std :: getline ( kvs , item , ';' )){ if ( item . empty ()) continue ; auto eq = item . find ( '=' ); if ( eq == std :: string :: npos || eq == 0 ) fail ( "Bad tag in override line " + std :: to_string ( n )); auto k = item . substr ( 0 , eq ), v = item . substr ( eq + 1 ); if ( keys . count ( k )) fail ( "Duplicate override tag: " + k ); keys . insert ( k );
if ( k == "garmin:type" || k == "garmin:feature" || k == "garmin:mapset" || k == "name" ) fail ( "Cannot override provenance field " + k ); rule . tags . emplace_back ( k , v );}
overrides [ rule . type ]. push_back ( std :: move ( rule ));}
size_t nRules = 0 ; for ( auto & kv : overrides ) nRules += kv . second . size (); std :: cerr << "Loaded " << nRules << " POI override rules from " << file << " \n " ;
}
2026-10-14 12:42:58 +00:00
// Coordinate-specific name fixes are deliberately opt-in and never inferred from
// similar-looking strings. A wrong-coordinate phantom POI must NOT be renamed.
using NameFixKey = std :: tuple < std :: string , int , int32_t , int32_t , std :: string > ;
std :: map < NameFixKey , std :: string > nameFixes ;
void loadNameFixes ( const std :: string & file ){ if ( file . empty ()) return ;
std :: ifstream in ( file ); if ( ! in ) fail ( "Cannot open name corrections file: " + file );
std :: string line ; size_t n = 0 ; while ( std :: getline ( in , line )){
++ n ; if ( ! line . empty () && line . back () == '\r' ) line . pop_back ();
if ( line . empty () || line [ 0 ] == '#' ) continue ;
auto col = csvParse ( line ); if ( col . size () != 7 ) fail ( "Name fixes: expected 7 columns at line " + std :: to_string ( n ));
if ( col [ 0 ] == "mapset" ) continue ;
int t = parseType ( col [ 1 ]); double lat = std :: stod ( col [ 2 ]), lon = std :: stod ( col [ 3 ]);
if ( ! std :: isfinite ( lat ) ||! std :: isfinite ( lon ) || std :: abs ( lat ) > 90 || std :: abs ( lon ) > 180 )
fail ( "Bad name-fix coordinates at line " + std :: to_string ( n ));
if ( col [ 0 ]. empty () || col [ 5 ]. empty ()) fail ( "Name fix needs mapset and replacement at line " + std :: to_string ( n ));
NameFixKey key { col [ 0 ], t , int32_t ( std :: llround ( lat * 1e7 )), int32_t ( std :: llround ( lon * 1e7 )), col [ 4 ]};
if ( ! nameFixes . emplace ( key , col [ 5 ]). second ) fail ( "Duplicate name-fix row at line " + std :: to_string ( n ));
}
std :: cerr << "Loaded " << nameFixes . size () << " coordinate-specific POI name fixes \n " ;
}
std :: string resolveNameFix ( const std :: string & map , int type , double lat , double lon , const std :: string & old ){
auto it = nameFixes . find ({ map , type , int32_t ( std :: llround ( lat * 1e7 )), int32_t ( std :: llround ( lon * 1e7 )), old });
return it == nameFixes . end () ? old : it -> second ;
}
2026-10-14 17:57:55 +00:00
// Display names for otherwise anonymous Garmin POIs. Stable across serial/parallel
// runs and repeated exports; does not depend on output node IDs or iteration order.
std :: unordered_map < int , std :: string > fallbackLabels ;
void loadFallbackLabels ( const std :: string & filename ){
if ( filename . empty ()) return ;
std :: ifstream in ( filename ); if ( ! in ) fail ( "Cannot open fallback POI labels: " + filename );
std :: string line ; size_t n = 0 ;
while ( std :: getline ( in , line )){
++ n ; if ( ! line . empty () && line . back () == '\r' ) line . pop_back ();
if ( line . empty () || line [ 0 ] == '#' ) continue ;
auto cols = csvParse ( line );
if ( cols . size () < 2 ) fail ( "POI labels require code,label_bg columns at line " + std :: to_string ( n ));
if ( cols [ 0 ] == "code" ) continue ;
int t = parseType ( cols [ 0 ]);
if ( cols [ 1 ]. empty ()) continue ;
fallbackLabels [ t ] = cols [ 1 ];
}
std :: cerr << "Loaded " << fallbackLabels . size () << " POI fallback descriptions \n " ;
}
std :: string compactBase36 ( uint64_t x ){
constexpr char digits [] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" ;
std :: string v ;
do { v . push_back ( digits [ x % 36 ]); x /= 36 ;} while ( x );
std :: reverse ( v . begin (), v . end ()); return v ;
}
std :: string fallbackPoiName ( const std :: string & map , int type , double lat , double lon ){
auto it = fallbackLabels . find ( type );
std :: string desc = it == fallbackLabels . end () ? ( "Garmin обект " + code ( type )) : it -> second ;
std :: ostringstream input ;
input << map << '|' << code ( type ) << '|' << int64_t ( std :: llround ( lat * 1e7 )) << '|' << int64_t ( std :: llround ( lon * 1e7 ));
uint64_t h = 14695981039346656037ULL ;
for ( unsigned char c : input . str ()){ h ^= c ; h *= 1099511628211ULL ;}
// 11 base36 characters retain ~57 bits. Collision risk for a full map is tiny,
// but coordinates + mapset are also present in the original data for disambiguation.
auto suffix = compactBase36 ( h );
if ( suffix . size () > 11 ) suffix = suffix . substr ( suffix . size () - 11 );
return desc + " - " + suffix ;
}
2026-10-14 11:16:43 +00:00
std :: optional < Tags > configuredPoi ( int type , const std :: string & name ){
Tags result = poiTags ( type , name );
2026-10-14 17:04:46 +00:00
auto hit = overrides . find ( type );
if ( hit != overrides . end ()) for ( const auto & rule : hit -> second ){
if ( ! rule . nameContains . empty () && name . find ( rule . nameContains ) == std :: string :: npos ) continue ;
2026-10-14 11:16:43 +00:00
if ( rule . mode == "drop" ) return std :: nullopt ;
if ( rule . mode == "replace" || rule . mode == "keep_raw" ){
result . erase ( std :: remove_if ( result . begin (), result . end (),[]( const auto & kv ){ return kv . first != "garmin:type" && kv . first != "garmin:feature" && kv . first != "name" ;}), result . end ());}
for ( const auto & [ k , v ] : rule . tags ){ result . erase ( std :: remove_if ( result . begin (), result . end (),[ & ]( const auto & kv ){ return kv . first == k ;}), result . end ()); if ( ! v . empty ()) result . emplace_back ( k , v );}
}
2026-10-14 17:04:46 +00:00
// BGM-specific stable classifier: register in custom OsmAnd POI and map indexes.
// Never disguise unknown objects as a fake OSM tourism/amenity category.
result . erase ( std :: remove_if ( result . begin (), result . end (),[]( const auto & kv ){ return kv . first == "bgm_poi" ;}), result . end ());
tag ( result , "bgm_poi" , "type_" + code ( type ));
2026-10-14 11:16:43 +00:00
return result ;
}
struct Sample { std :: string map , name ; double lat = 0 , lon = 0 ;};
struct PoiTypeInfo { uint64_t count = 0 , named = 0 ; std :: vector < Sample > examples ;};
// Deterministic nationwide samples, favoring named points; no "first five" local bias.
uint64_t sampleHash ( const Sample & s ){
std :: ostringstream os ; os << s . map << "/" << std :: fixed << std :: setprecision ( 7 ) << s . lat << "/" << s . lon << "/" << s . name ;
uint64_t h = 14695981039346656037ULL ; for ( unsigned char c : os . str ()){ h ^= c ; h *= 1099511628211ULL ;} return h ;
}
void addSample ( PoiTypeInfo & info , const Sample & sample , int maxExamples ){
for ( auto & prev : info . examples ) if ( std :: abs ( prev . lat - sample . lat ) < 0.00000005 && std :: abs ( prev . lon - sample . lon ) < 0.00000005 && prev . name == sample . name ) return ;
info . examples . push_back ( sample );
std :: sort ( info . examples . begin (), info . examples . end (),[]( const Sample & a , const Sample & b ){
if ( a . name . empty () != b . name . empty ()) return ! a . name . empty ();
auto x = sampleHash ( a ), y = sampleHash ( b ); return x == y ? a . map < b . map : x < y ;
});
if (( int ) info . examples . size () > maxExamples ) info . examples . resize ( maxExamples );
}
std :: string semanticTags ( int type ){ const auto t = poiTags ( type , "" ); std :: string out ; for ( const auto & [ k , v ] : t ) if ( k != "garmin:type" && k != "garmin:feature" && k != "name" ){ if ( ! out . empty ()) out += ';' ; out += k + "=" + v ;} return out ;}
void writePoiTypes ( const std :: string & file , const std :: map < int , PoiTypeInfo >& types , int n ){
if ( file . empty ()) return ; std :: ofstream out ( file ); if ( ! out ) fail ( "Cannot write " + file );
out << "code,kind,count,named,default_tags,has_typ_icon,configured_rules" ;
for ( int i = 1 ; i <= n ; i ++ ) out << ",example" << i << "_lat,example" << i << "_lon,example" << i << "_name,example" << i << "_map" ; out << " \n " ;
for ( auto & [ type , entry ] : types ){ int rules = 0 ; auto it = overrides . find ( type ); if ( it != overrides . end ()) rules = ( int ) it -> second . size ();
out << csvQuote ( code ( type )) << "," << ( type >= 0x10000 ? "extended" : "classic" ) << "," << entry . count << "," << entry . named << "," << csvQuote ( semanticTags ( type )) << "," << ( gStyles . icons . count ( type ) ? "yes" : "no" ) << "," << rules ;
for ( int i = 0 ; i < n ; i ++ ){ if ( i < ( int ) entry . examples . size ()){
auto & s = entry . examples [ i ]; out << "," << std :: fixed << std :: setprecision ( 7 ) << std :: llround ( s . lat * 10000000. ) / 10000000. << "," << std :: llround ( s . lon * 10000000. ) / 10000000. << "," << csvQuote ( s . name ) << "," << csvQuote ( s . map );
} else out << ",,,," ;} out << " \n " ;
}
if ( ! out ) fail ( "Cannot complete type CSV" ); std :: cerr << "POI unique-type catalog: " << types . size () << " types -> " << file << " \n " ;
}
void saveIcons ( const std :: string & dir ){ if ( dir . empty ()) return ; fs :: create_directories ( dir );
for ( const auto & [ type , ico ] : gStyles . icons ){ std :: string path = ( fs :: path ( dir ) / ( code ( type ) + ".png" )). string (); FILE * f = std :: fopen ( path . c_str (), "wb" ); if ( ! f ) fail ( "Cannot open icon " + path );
png_structp png = png_create_write_struct ( PNG_LIBPNG_VER_STRING , nullptr , nullptr , nullptr ); png_infop info = png_create_info_struct ( png );
if ( ! png ||! info ){ std :: fclose ( f ); fail ( "PNG alloc failed" );}
if ( setjmp ( png_jmpbuf ( png ))){ png_destroy_write_struct ( & png , & info ); std :: fclose ( f ); fail ( "PNG write failed: " + path );}
png_init_io ( png , f ); png_set_IHDR ( png , info , ico . w , ico . h , 8 , PNG_COLOR_TYPE_RGBA , PNG_INTERLACE_NONE , PNG_COMPRESSION_TYPE_DEFAULT , PNG_FILTER_TYPE_DEFAULT );
png_write_info ( png , info ); std :: vector < U8 > img ( size_t ( ico . w ) * ico . h * 4 ); for ( size_t i = 0 ; i < ico . pixels . size (); i ++ ){ U32 p = ico . pixels [ i ]; img [ i * 4 ] = p >> 24 ; img [ i * 4 + 1 ] = p >> 16 ; img [ i * 4 + 2 ] = p >> 8 ; img [ i * 4 + 3 ] = p ;}
std :: vector < png_bytep > rows ( ico . h ); for ( int y = 0 ; y < ico . h ; y ++ ) rows [ y ] = img . data () + size_t ( y ) * ico . w * 4 ;
png_write_image ( png , rows . data ()); png_write_end ( png , nullptr ); png_destroy_write_struct ( & png , & info ); std :: fclose ( f );
}
std :: cerr << "Exported " << gStyles . icons . size () << " TYP icons to " << dir << " \n " ;
}
struct Store {
sqlite3 * db = nullptr ; sqlite3_stmt * insNode = nullptr , * getNode = nullptr , * insPoi = nullptr , * insWay = nullptr ; int64_t nodes = 0 , ways = 0 , poi = 0 ;
explicit Store ( const std :: string & path ){ if ( sqlite3_open ( path . c_str (), & db ) != SQLITE_OK ) fail ( "Unable to open staging database" );
sql ( "PRAGMA journal_mode=DELETE; PRAGMA synchronous=NORMAL; PRAGMA temp_store=FILE; PRAGMA cache_size=-16000;" );
sql ( "CREATE TABLE nodes(id INTEGER PRIMARY KEY,lon INTEGER NOT NULL,lat INTEGER NOT NULL,tags TEXT NOT NULL DEFAULT '',UNIQUE(lon,lat));" );
sql ( "CREATE TABLE ways(id INTEGER PRIMARY KEY,refs TEXT NOT NULL,tags TEXT NOT NULL);" );
sql ( "CREATE TABLE pois(id INTEGER PRIMARY KEY,lon INTEGER NOT NULL,lat INTEGER NOT NULL,tags TEXT NOT NULL);" );
auto prepare = [ & ]( sqlite3_stmt ** st , const char * q ){ if ( sqlite3_prepare_v2 ( db , q , - 1 , st , nullptr ) != SQLITE_OK ) fail ( sqlite3_errmsg ( db ));};
prepare ( & insNode , "INSERT OR IGNORE INTO nodes(lon,lat) VALUES(?1,?2)" );
prepare ( & getNode , "SELECT id FROM nodes WHERE lon=?1 AND lat=?2" );
prepare ( & insPoi , "INSERT INTO pois(id,lon,lat,tags) VALUES(?1,?2,?3,?4)" );
prepare ( & insWay , "INSERT INTO ways(refs,tags) VALUES(?1,?2)" ); sql ( "BEGIN" );}
~ Store (){ if ( db ){ sqlite3_finalize ( insNode ); sqlite3_finalize ( getNode ); sqlite3_finalize ( insPoi ); sqlite3_finalize ( insWay ); sqlite3_close ( db );}}
void sql ( const char * q ){ char * err = nullptr ; int res = sqlite3_exec ( db , q , nullptr , nullptr , & err ); if ( res != SQLITE_OK ){ std :: string message = err ? err : "SQLite error" ; sqlite3_free ( err ); fail ( message );}}
static int32_t quant ( double v ){ return int32_t ( std :: llround ( v * 10000000. ));}
int64_t node ( double lon , double lat ){ if ( ! std :: isfinite ( lon ) ||! std :: isfinite ( lat ) || lon < - 180 || lon > 180 || lat < - 90 || lat > 90 ) return 0 ;
int32_t x = quant ( lon ), y = quant ( lat ); sqlite3_reset ( insNode ); sqlite3_clear_bindings ( insNode ); sqlite3_bind_int ( insNode , 1 , x ); sqlite3_bind_int ( insNode , 2 , y ); if ( sqlite3_step ( insNode ) != SQLITE_DONE ) fail ( sqlite3_errmsg ( db ));
sqlite3_reset ( getNode ); sqlite3_clear_bindings ( getNode ); sqlite3_bind_int ( getNode , 1 , x ); sqlite3_bind_int ( getNode , 2 , y );
if ( sqlite3_step ( getNode ) != SQLITE_ROW ) fail ( "Failed to fetch OSM node" ); int64_t id = sqlite3_column_int64 ( getNode , 0 ); sqlite3_reset ( getNode ); return id ;
}
void addPoi ( double lon , double lat , const Tags & t ){
if ( ! std :: isfinite ( lon ) ||! std :: isfinite ( lat ) || std :: abs ( lon ) > 180 || std :: abs ( lat ) > 90 ) return ;
// POIs are separate OSM nodes, even when co-located with a road vertex or another POI.
// This avoids merging distinct POI identities into an intersection node.
const auto text = makeTags ( t ); sqlite3_reset ( insPoi ); sqlite3_clear_bindings ( insPoi );
sqlite3_bind_int64 ( insPoi , 1 , 1000000000000LL + ( ++ poi ));
sqlite3_bind_int ( insPoi , 2 , quant ( lon )); sqlite3_bind_int ( insPoi , 3 , quant ( lat ));
sqlite3_bind_text ( insPoi , 4 , text . c_str (), - 1 , SQLITE_TRANSIENT );
if ( sqlite3_step ( insPoi ) != SQLITE_DONE ) fail ( sqlite3_errmsg ( db ));
}
void way ( std :: vector < std :: pair < double , double >>& pts , bool closed , const Tags & tags ){ if ( pts . size () < ( closed ? 3u : 2u ) || tags . size () < 2 ) return ;
// Suppress malformed geographic coordinates rather than silently connecting disjoint valid vertices.
for ( const auto & p : pts ) if ( ! std :: isfinite ( p . first ) ||! std :: isfinite ( p . second ) || std :: abs ( p . first ) > 180 || std :: abs ( p . second ) > 90 ) return ;
// Ignore consecutive coordinate duplicates, but retain all true Garmin vertices.
std :: vector < int64_t > ids ; ids . reserve ( pts . size () + 1 ); for ( const auto & p : pts ){ auto id = node ( p . first , p . second ); if ( id && ( ids . empty () || ids . back () != id )) ids . push_back ( id );}
if ( closed && ids . size () >= 3 && ids . front () != ids . back ()) ids . push_back ( ids . front ());
if ( ids . size () < ( closed ? 4u : 2u )) return ;
std :: ostringstream ss ; for ( auto i : ids ){ ss << "<nd ref= \" " << i << " \" />" ;}
auto refs = ss . str (), txt = makeTags ( tags );
sqlite3_reset ( insWay ); sqlite3_clear_bindings ( insWay ); sqlite3_bind_text ( insWay , 1 , refs . c_str (), - 1 , SQLITE_TRANSIENT ); sqlite3_bind_text ( insWay , 2 , txt . c_str (), - 1 , SQLITE_TRANSIENT );
if ( sqlite3_step ( insWay ) != SQLITE_DONE ) fail ( sqlite3_errmsg ( db )); ways ++ ;
}
void commit (){ sql ( "COMMIT" ); sql ( "BEGIN" );}
void output ( const std :: string & file ){ sql ( "COMMIT" ); std :: ofstream out ( file , std :: ios :: binary ); if ( ! out ) fail ( "Cannot write " + file );
2026-10-14 12:42:58 +00:00
out << "<?xml version= \" 1.0 \" encoding= \" UTF-8 \" ?> \n <osm version= \" 0.6 \" generator= \" garmin2osm-cpp-0.6 \" upload= \" never \" > \n " ;
2026-10-14 11:16:43 +00:00
sqlite3_stmt * st = nullptr ;
if ( sqlite3_prepare_v2 ( db , "SELECT id,lat,lon,tags FROM nodes ORDER BY id" , - 1 , & st , nullptr ) != SQLITE_OK ) fail ( sqlite3_errmsg ( db ));
while ( sqlite3_step ( st ) == SQLITE_ROW ){ auto id = sqlite3_column_int64 ( st , 0 ); double lat = sqlite3_column_int ( st , 1 ) / 10000000. , lon = sqlite3_column_int ( st , 2 ) / 10000000. ; const char * tags = ( const char * ) sqlite3_column_text ( st , 3 );
out << "<node id= \" " << id << " \" version= \" 1 \" lat= \" " << std :: fixed << std :: setprecision ( 7 ) << lat << " \" lon= \" " << lon << " \" >" << ( tags ? tags : "" ) << "</node> \n " ;}
sqlite3_finalize ( st ); st = nullptr ;
if ( sqlite3_prepare_v2 ( db , "SELECT id,lat,lon,tags FROM pois ORDER BY id" , - 1 , & st , nullptr ) != SQLITE_OK ) fail ( sqlite3_errmsg ( db ));
while ( sqlite3_step ( st ) == SQLITE_ROW ){
out << "<node id= \" " << sqlite3_column_int64 ( st , 0 ) << " \" version= \" 1 \" lat= \" " << std :: fixed << std :: setprecision ( 7 )
<< sqlite3_column_int ( st , 1 ) / 10000000. << " \" lon= \" " << sqlite3_column_int ( st , 2 ) / 10000000. << " \" >"
<< ( const char * ) sqlite3_column_text ( st , 3 ) << "</node> \n " ;
}
sqlite3_finalize ( st ); st = nullptr ;
if ( sqlite3_prepare_v2 ( db , "SELECT id,refs,tags FROM ways ORDER BY id" , - 1 , & st , nullptr ) != SQLITE_OK ) fail ( sqlite3_errmsg ( db ));
while ( sqlite3_step ( st ) == SQLITE_ROW ){ out << "<way id= \" " << sqlite3_column_int64 ( st , 0 ) << " \" version= \" 1 \" >" << ( const char * ) sqlite3_column_text ( st , 1 ) << ( const char * ) sqlite3_column_text ( st , 2 ) << "</way> \n " ;}
sqlite3_finalize ( st ); out << "</osm> \n " ; out . close (); if ( ! out ) fail ( "Output write failed" );
}
};
2026-10-14 12:42:58 +00:00
struct Stats { std :: map < std :: pair < int , int > , uint64_t > byType ; uint64_t lines = 0 , areas = 0 , points = 0 , skipped = 0 , extendedOutsideBounds = 0 ; std :: map < int , PoiTypeInfo > poiTypes ;};
2026-10-14 11:16:43 +00:00
void processMap ( const Map & m , Store * db , Stats & stats , const Options & o , std :: ostream * poiManifest = nullptr ){
const auto & r = m . sections . at ( "RGN" );
2026-10-14 12:42:58 +00:00
auto recordPoi = [ & ]( int typ , double lon , double lat , const std :: string & rawName , bool extended ){
2026-10-14 17:57:55 +00:00
const std :: string originalName = resolveNameFix ( m . name , typ , lat , lon , rawName );
const bool generatedName = originalName . empty ();
const std :: string name = generatedName ? fallbackPoiName ( m . name , typ , lat , lon ) : originalName ;
2026-10-14 11:16:43 +00:00
stats . byType [{ 3 , typ }] ++ ; stats . points ++ ;
auto & info = stats . poiTypes [ typ ]; info . count ++ ; if ( ! name . empty ()) info . named ++ ;
addSample ( info ,{ m . name , name , lat , lon }, o . examples );
auto tags = configuredPoi ( typ , name );
if ( poiManifest )( * poiManifest ) << csvQuote ( m . name ) << "," << csvQuote ( extended ? "extended" : "classic" ) << "," << csvQuote ( code ( typ )) << "," << std :: fixed << std :: setprecision ( 7 ) << Store :: quant ( lat ) / 10000000. << "," << Store :: quant ( lon ) / 10000000. << "," << csvQuote ( name ) << " \n " ;
2026-10-14 17:57:55 +00:00
if ( tags && db ){ if ( generatedName ) tag ( * tags , "bgm:generated_name" , "yes" ); tag ( * tags , "garmin:mapset" , m . name ); db -> addPoi ( lon , lat , * tags );}
2026-10-14 11:16:43 +00:00
};
int finest =- 1 , routeBpc =- 1 ; uint64_t largest = 0 ; std :: map < int , uint64_t > extBytes ;
for ( const auto & lev : m . levels ) finest = std :: max ( finest , lev . bits );
for ( const auto & s : m . subs ) extBytes [ s . bits ] += s . extLen [ 1 ];
for ( auto [ b , n ] : extBytes ) if ( n > largest ){ largest = n ; routeBpc = b ;}
if ( routeBpc < 0 ) routeBpc = finest ;
std :: cerr << " map " << m . name << " finest=" << finest << " extended-route-bpc=" << routeBpc << " " ;
for ( auto [ b , n ] : extBytes ) if ( n ) std :: cerr << b << ":" << n / 1024 << "KiB " ; std :: cerr << " \n " ;
for ( size_t index = 0 ; index < m . subs . size (); ++ index ){ const auto & s = m . subs [ index ]; if ( o . clip &&! s . bounds . overlaps ( o . bbox )) continue ;
const bool classic = ( o . allLevels || s . bits == finest ), extended = ( o . allLevels || s . bits == routeBpc );
if ( ! classic &&! extended ) continue ;
auto record = [ & ]( bool line , bool ext , bool polygon , int ty , std :: vector < std :: pair < double , double >>& ps , const std :: string & name ){
if ( line &&! o . contours && ty >= 0x20 && ty <= 0x25 &&! ext ) return ;
stats . byType [{ ext ? 2 : ( polygon ? 1 : 0 ), ty }] ++ ;
if ( polygon ){ stats . areas ++ ; if ( db ) db -> way ( ps , true , areaTags ( ty , name ));}
else { stats . lines ++ ; if ( db ) db -> way ( ps , false , lineTags ( ty , ext , name ));}
};
auto parsePoly = [ & ]( const std :: vector < U8 >& buf , size_t from , size_t to , bool line , bool ext ){
if ( from >= to || to > buf . size ()) return ; size_t off = from ; int guard = 0 ;
while ( off < to && guard ++< 100000 ){ int ty = 0 ; size_t used = 0 ; const U8 * p = buf . data () + off ;
auto coords = decodePoly ( p , to - off , s . cx , s . cy , s . shift , line , ext , ty , used );
if ( ! used ){ stats . skipped ++ ; break ;} off += used ;
if ( coords . size () < ( line ? 2u : 3u )) continue ;
std :: string name ;
if ( ! ext && used >= 4 ){ U32 lbl = u24 ( p + 1 ); name = ( lbl & 0x800000 ) ? m . netLabel ( lbl & 0x3fffff ) : m . label ( lbl & 0x3fffff );}
if ( ext ){ // Garmin RGN2 V2 label resides at end of bitstream when subtype bit 0x20 is set.
if (( p [ 1 ] & 0x20 ) && used >= 3 ){ U32 lbl = u24 ( p + used - 3 ); name = m . label ( lbl & 0x3fffff );}
}
record ( line , ext , ! line , ty , coords , name );
}
};
if ( classic && s . to >= s . from && s . to - s . from <= 8 * 1024 * 1024 ){
auto buf = r . get ( s . from , s . to - s . from ); int n = bool ( s . flags & 0x10 ) + bool ( s . flags & 0x20 ) + bool ( s . flags & 0x40 ) + bool ( s . flags & 0x80 );
if ( n && buf . size () >= size_t (( n - 1 ) * 2 )){
std :: array < size_t , 4 > start {}; size_t p = 0 , last = std :: max ( 0 ,( n - 1 ) * 2 );
for ( int k = 0 ; k < 4 ; k ++ ) if ( s . flags & ( 0x10 << k )){ size_t at = ( p == 0 ? last : u16 ( buf . data () + 2 * ( p - 1 ))); start [ size_t ( k )] = at ; p ++ ;}
bool valid = true ; size_t previous = 0 ; for ( int k = 0 ; k < 4 ; k ++ ) if ( s . flags & ( 0x10 << k )){ if ( start [ size_t ( k )] < previous || start [ size_t ( k )] > buf . size ()) valid = false ; previous = start [ size_t ( k )];}
if ( valid ){
for ( int k = 0 ; k < 2 ; k ++ ) if ( s . flags & ( 0x10 << k )){
size_t begin = start [ k ], end = buf . size (); for ( int kk = k + 1 ; kk < 4 ; kk ++ ) if ( s . flags & ( 0x10 << kk )){ end = start [ kk ]; break ;}
for ( size_t j = begin ; j + 8 <= end ;){ const U8 * q = buf . data () + j ; U32 inf = u24 ( q + 1 ); size_t len = ( inf & 0x800000 ) ? 9 : 8 ; if ( j + len > end ) break ;
int typ = ( int ( q [ 0 ]) << 8 ) | (( inf & 0x800000 ) ? q [ 8 ] : 0 );
double lon = geo ( s . cx + s16 ( q + 4 ) * ( 1 << s . shift )), lat = geo ( s . cy + s16 ( q + 6 ) * ( 1 << s . shift ));
if ( ! o . clip || ( lon >= o . bbox . w && lon <= o . bbox . e && lat >= o . bbox . s && lat <= o . bbox . n )){
std :: string name = ( inf & 0x400000 ) ? m . poiLabel ( inf & 0x3fffff ) : m . label ( inf & 0x3fffff ); recordPoi ( typ , lon , lat , name , false );}
j += len ;
}
}
if ( s . flags & 0x40 ) parsePoly ( buf , start [ 2 ],( s . flags & 0x80 ) ? start [ 3 ] : buf . size (), true , false );
if ( s . flags & 0x80 ) parsePoly ( buf , start [ 3 ], buf . size (), false , false );
}
}
}
if ( extended ){ for ( int k = 0 ; k < 3 ; k ++ ){
if ( ! s . extLen [ k ] || s . extLen [ k ] > 8 * 1024 * 1024 ) continue ;
auto ext = r . get ( s . extFrom [ k ], s . extLen [ k ]);
if ( k < 2 ) parsePoly ( ext , 0 , ext . size (), k == 1 , true );
else for ( size_t at = 0 ; at + 6 <= ext . size ();){ const U8 * p = ext . data () + at ; int sub = p [ 1 ]; size_t len = 6 + bool ( sub & 0x80 ) + 3 * bool ( sub & 0x20 ); if ( at + len > ext . size ()) break ;
int typ = 0x10000 + ( int ( p [ 0 ]) << 8 ) + ( sub & 31 );
double lon = geo ( s . cx + s16 ( p + 2 ) * ( 1 << s . shift )), lat = geo ( s . cy + s16 ( p + 4 ) * ( 1 << s . shift ));
2026-10-14 12:42:58 +00:00
// Avoid malformed extended records that escape their actual TRE subdivision.
// A small ~5-metre tolerance accommodates quantization at Garmin zoom levels.
if ( lon < s . bounds . w - 0.00005 || lon > s . bounds . e + 0.00005 ||
lat < s . bounds . s - 0.00005 || lat > s . bounds . n + 0.00005 ){ stats . extendedOutsideBounds ++ ; at += len ; continue ;}
2026-10-14 11:16:43 +00:00
if ( ! o . clip || ( lon >= o . bbox . w && lon <= o . bbox . e && lat >= o . bbox . s && lat <= o . bbox . n )){
2026-10-14 12:42:58 +00:00
std :: string name ; if ( sub & 0x20 ){ U32 inf = u24 ( p + 6 ); name = ( inf & 0x400000 ) ? m . poiLabel ( inf & 0x3fffff ) : m . label ( inf & 0x3fffff );}
2026-10-14 11:16:43 +00:00
recordPoi ( typ , lon , lat , name , true );}
at += len ;}
}}
if ( db && ( index % 60 == 0 )) db -> commit ();
}
if ( db ) db -> commit ();
}
// Large parallel jobs write a separate SQLite stage per worker, then stream all OSM
// nodes before all OSM ways. Node IDs are offset by 10^10 per worker, including refs.
void outputStages ( const std :: string & outFile , const std :: vector < std :: string >& paths ){
std :: ofstream out ( outFile , std :: ios :: binary ); if ( ! out ) fail ( "Cannot open " + outFile );
2026-10-14 12:42:58 +00:00
out << "<?xml version= \" 1.0 \" encoding= \" UTF-8 \" ?> \n <osm version= \" 0.6 \" generator= \" garmin2osm-cpp-0.6 \" upload= \" never \" > \n " ;
2026-10-14 11:16:43 +00:00
auto walk = [ & ]( int pass ){ for ( size_t k = 0 ; k < paths . size (); k ++ ){
sqlite3 * db = nullptr ; if ( sqlite3_open_v2 ( paths [ k ]. c_str (), & db , SQLITE_OPEN_READONLY , nullptr ) != SQLITE_OK ) fail ( "Cannot read parallel stage" );
const int64_t delta = int64_t ( k ) * 10000000000LL ;
auto query = [ & ]( const char * sql , auto callback ){ sqlite3_stmt * st = nullptr ;
if ( sqlite3_prepare_v2 ( db , sql , - 1 , & st , nullptr ) != SQLITE_OK ) fail ( sqlite3_errmsg ( db ));
while ( sqlite3_step ( st ) == SQLITE_ROW ) callback ( st ); sqlite3_finalize ( st );};
if ( pass == 0 ){
query ( "SELECT id,lat,lon,tags FROM nodes ORDER BY id" ,[ & ]( sqlite3_stmt * st ){
out << "<node id= \" " << sqlite3_column_int64 ( st , 0 ) + delta << " \" version= \" 1 \" lat= \" " << std :: fixed << std :: setprecision ( 7 ) << sqlite3_column_int ( st , 1 ) / 10000000. << " \" lon= \" " << sqlite3_column_int ( st , 2 ) / 10000000. << " \" >" << ( const char * ) sqlite3_column_text ( st , 3 ) << "</node> \n " ;});
query ( "SELECT id,lat,lon,tags FROM pois ORDER BY id" ,[ & ]( sqlite3_stmt * st ){
out << "<node id= \" " << sqlite3_column_int64 ( st , 0 ) + delta << " \" version= \" 1 \" lat= \" " << std :: fixed << std :: setprecision ( 7 ) << sqlite3_column_int ( st , 1 ) / 10000000. << " \" lon= \" " << sqlite3_column_int ( st , 2 ) / 10000000. << " \" >" << ( const char * ) sqlite3_column_text ( st , 3 ) << "</node> \n " ;});
} else {
query ( "SELECT id,refs,tags FROM ways ORDER BY id" ,[ & ]( sqlite3_stmt * st ){
std :: string refs = ( const char * ) sqlite3_column_text ( st , 1 );
if ( delta ){ size_t from = 0 ; while (( from = refs . find ( "<nd ref= \" " , from )) != std :: string :: npos ){
auto begin = from + 9 , end = refs . find ( '"' , begin ); if ( end == std :: string :: npos ) fail ( "Bad internal way reference" );
auto old = std :: stoll ( refs . substr ( begin , end - begin )); auto val = std :: to_string ( old + delta );
refs . replace ( begin , end - begin , val ); from = begin + val . size ();}}
out << "<way id= \" " << sqlite3_column_int64 ( st , 0 ) + delta << " \" version= \" 1 \" >" << refs << ( const char * ) sqlite3_column_text ( st , 2 ) << "</way> \n " ;});
}
sqlite3_close ( db );
}};
walk ( 0 ); walk ( 1 ); out << "</osm> \n " ; out . close (); if ( ! out ) fail ( "Failed writing merged OSM" );
}
// Extra user-authored POIs are not claims about original IMG contents.
// Mark as manual_poi so source-vs-IMG validator can distinguish them.
void injectManualPois ( const std :: string & file , const std :: string & dbPath ){
if ( file . empty ()) return ; std :: ifstream input ( file ); if ( ! input ) fail ( "Cannot read additions file: " + file );
sqlite3 * db = nullptr ; if ( sqlite3_open ( dbPath . c_str (), & db ) != SQLITE_OK ) fail ( "Cannot open stage for additions" );
auto sql = [ & ]( const char * q ){ if ( sqlite3_exec ( db , q , nullptr , nullptr , nullptr ) != SQLITE_OK ) fail ( sqlite3_errmsg ( db ));};
sqlite3_stmt * findId = nullptr ; sqlite3_prepare_v2 ( db , "SELECT COALESCE(MAX(id),999999999999) FROM pois" , - 1 , & findId , nullptr );
int64_t seq = 999999999999LL ; if ( sqlite3_step ( findId ) == SQLITE_ROW ) seq = std :: max < int64_t > ( seq , sqlite3_column_int64 ( findId , 0 )); sqlite3_finalize ( findId );
sqlite3_stmt * insert = nullptr ; if ( sqlite3_prepare_v2 ( db , "INSERT INTO pois(id,lon,lat,tags) VALUES(?1,?2,?3,?4)" , - 1 , & insert , nullptr ) != SQLITE_OK ) fail ( sqlite3_errmsg ( db ));
sql ( "BEGIN" ); std :: string row ; size_t count = 0 ; int lineno = 0 ;
while ( std :: getline ( input , row )){ lineno ++ ; if ( ! row . empty () && row . back () == '\r' ) row . pop_back (); if ( row . empty () || row [ 0 ] == '#' ) continue ;
auto cols = csvParse ( row ); if ( cols [ 0 ] == "lat" ) continue ; if ( cols . size () != 4 ) fail ( "Additions need lat,lon,name,tags at line " + std :: to_string ( lineno ));
size_t parsed = 0 ; double lat = std :: stod ( cols [ 0 ], & parsed ); if ( parsed != cols [ 0 ]. size ()) fail ( "Bad latitude" ); double lon = std :: stod ( cols [ 1 ], & parsed ); if ( parsed != cols [ 1 ]. size () ||! std :: isfinite ( lat ) ||! std :: isfinite ( lon ) || std :: abs ( lat ) > 90 || std :: abs ( lon ) > 180 ) fail ( "Bad manual POI coordinates" );
Tags tags ; tag ( tags , "garmin:feature" , "manual_poi" ); tag ( tags , "source" , "user_manual" ); tag ( tags , "name" , cols [ 2 ]);
std :: stringstream vals ( cols [ 3 ]); std :: string kv ; while ( std :: getline ( vals , kv , ';' )){ if ( kv . empty ()) continue ; auto eq = kv . find ( '=' ); if ( eq == std :: string :: npos || eq == 0 ) fail ( "Bad manual tag at line " + std :: to_string ( lineno ));
auto k = kv . substr ( 0 , eq ); if ( k == "garmin:feature" || k == "source" || k == "name" ) fail ( "Cannot override manual provenance fields" ); tag ( tags , k , kv . substr ( eq + 1 ));}
auto txt = makeTags ( tags ); sqlite3_reset ( insert ); sqlite3_clear_bindings ( insert );
sqlite3_bind_int64 ( insert , 1 , ++ seq ); sqlite3_bind_int ( insert , 2 , Store :: quant ( lon )); sqlite3_bind_int ( insert , 3 , Store :: quant ( lat )); sqlite3_bind_text ( insert , 4 , txt . c_str (), - 1 , SQLITE_TRANSIENT );
if ( sqlite3_step ( insert ) != SQLITE_DONE ) fail ( "Manual POI insertion failed: " + std :: string ( sqlite3_errmsg ( db ))); count ++ ;
}
sql ( "COMMIT" ); sqlite3_finalize ( insert ); sqlite3_close ( db ); std :: cerr << "Injected " << count << " manually supplied POIs (not IMG-derived) \n " ;
}
int run ( int argc , char ** argv ){
2026-10-14 17:57:55 +00:00
auto o = parse ( argc , argv ); loadOverrides ( o . poiOverrides ); loadNameFixes ( o . poiNameFixes ); loadFallbackLabels ( o . poiLabels ); MapSet set ( o . input );
2026-10-14 11:16:43 +00:00
for ( const auto & [ n , parts ] : set . parts ){ auto it = parts . find ( "TYP" ); if ( it != parts . end ()){ gStyles . load ( it -> second ); break ;}}
std :: cerr << "Maps=" << set . maps . size () << " TYP icons=" << gStyles . pointStyles << " jobs=" << o . jobs << " \n " ;
saveIcons ( o . iconsDir );
std :: vector < const Map *> selected ; for ( const auto & m : set . maps ){ if ( o . clip &&! m . bounds . overlaps ( o . bbox )) continue ; if ( o . limitMaps > 0 && ( int ) selected . size () >= o . limitMaps ) break ; selected . push_back ( & m );}
int jobs = std :: min < int > ( o . jobs , std :: max < size_t > ( 1 , selected . size ()));
const bool exporting =! o . output . empty ();
std :: string temp = o . temp . empty () ? ( o . output . empty () ? o . input + ".stage.sqlite" : o . output + ".stage.sqlite" ) : o . temp ;
if ( exporting && fs :: exists ( o . output )) fail ( "Output already exists; refuse overwrite: " + o . output );
std :: vector < std :: string > dbPaths ( jobs ), manifestPaths ( jobs ); for ( int i = 0 ; i < jobs ; i ++ ){
dbPaths [ i ] = temp + ".worker" + std :: to_string ( i ) + ".sqlite" ; if ( exporting && fs :: exists ( dbPaths [ i ])) fail ( "Stage exists: " + dbPaths [ i ]);
if ( ! o . poiManifest . empty ()){ manifestPaths [ i ] = o . poiManifest + ".worker" + std :: to_string ( i ) + ".tmp" ; if ( fs :: exists ( manifestPaths [ i ])) fail ( "Manifest temp exists: " + manifestPaths [ i ]);}
}
std :: vector < Stats > stats ( jobs ); std :: atomic < size_t > next { 0 }; std :: exception_ptr error ; std :: mutex errorLock ;
auto worker = [ & ]( int id ){ try {
std :: unique_ptr < Store > db ; if ( exporting ) db = std :: make_unique < Store > ( dbPaths [ id ]);
std :: ofstream manifest ; if ( ! manifestPaths [ id ]. empty ()){ manifest . open ( manifestPaths [ id ]); if ( ! manifest ) fail ( "Cannot write POI manifest" );}
for (;;){ auto idx = next . fetch_add ( 1 ); if ( idx >= selected . size ()) break ; processMap ( * selected [ idx ], db . get (), stats [ id ], o , manifest . is_open () ?& manifest : nullptr );}
if ( db ) db -> sql ( "COMMIT" );
} catch (...){ std :: lock_guard < std :: mutex > guard ( errorLock ); if ( ! error ) error = std :: current_exception ();}};
if ( jobs == 1 ) worker ( 0 ); else { std :: vector < std :: thread > threads ; for ( int k = 0 ; k < jobs ; k ++ ) threads . emplace_back ( worker , k ); for ( auto & t : threads ) t . join ();}
if ( error ) std :: rethrow_exception ( error );
2026-10-14 12:42:58 +00:00
Stats total ; for ( const auto & st : stats ){ total . lines += st . lines ; total . areas += st . areas ; total . points += st . points ; total . skipped += st . skipped ; total . extendedOutsideBounds += st . extendedOutsideBounds ;
2026-10-14 11:16:43 +00:00
for ( const auto & [ k , v ] : st . byType ) total . byType [ k ] += v ;
for ( const auto & [ k , v ] : st . poiTypes ){ auto & dest = total . poiTypes [ k ]; dest . count += v . count ; dest . named += v . named ;
for ( const auto & ex : v . examples ) addSample ( dest , ex , o . examples );}}
writePoiTypes ( o . poiTypesOut , total . poiTypes , o . examples );
if ( ! o . poiManifest . empty ()){
if ( fs :: exists ( o . poiManifest )) fail ( "Manifest exists; refuse overwrite" ); std :: ofstream out ( o . poiManifest ); if ( ! out ) fail ( "Cannot write " + o . poiManifest );
out << "mapset,kind,code,lat,lon,name \n " ;
for ( const auto & p : manifestPaths ){ std :: ifstream in ( p ); if ( in . peek () != std :: char_traits < char >:: eof ()) out << in . rdbuf (); in . close (); fs :: remove ( p );}
}
2026-10-14 12:42:58 +00:00
std :: cerr << "Finished " << selected . size () << " maps; lines=" << total . lines << " areas=" << total . areas << " rawPOIs=" << total . points << " uniquePOItypes=" << total . poiTypes . size () << " parseStops=" << total . skipped << " extendedOutsideBounds=" << total . extendedOutsideBounds << " \n " ;
2026-10-14 11:16:43 +00:00
if ( exporting ){ injectManualPois ( o . poiAdditions , dbPaths [ 0 ]); outputStages ( o . output , dbPaths ); if ( ! o . keepTemp ) for ( auto & p : dbPaths ) fs :: remove ( p ); std :: cerr << "Wrote " << o . output << " \n " ;}
return 0 ;
}
} // namespace vector_export
int main ( int argc , char ** argv ){ try { return vector_export :: run ( argc , argv );} catch ( const std :: exception & e ){ std :: cerr << "ERROR: " << e . what () << " \n " ; return 1 ;}}