316 adg() : _tables{make_tables_()} {}
321 adg& operator=(
adg&&) noexcept = default;
331 static adg inst{auto_probe_tag{}};
360 const core::rng call{call_seed};
362 v._signature = {.seed = call_seed, .data =
data_hash()};
366 const std::string& country = r.country;
368 const format_data& fmt = format_of_(r);
372 v.
region = region_text_(fmt, r.region_code, r.region);
375 auto sr = call.split(
"street");
376 const street_pick pick = pick_street_(r, request.
weighted, sr);
377 v.
street = request.
script == address_script::latin && !pick.latin.empty() ? pick.latin : pick.name;
379 auto nr = call.split(
"number");
380 if (fmt.number_blocks)
383 v.
number = core::uniform_int<std::uint32_t>(nr, 1, 9);
385 const int ban = core::uniform_int<int>(nr, 1, 30);
386 const int go = core::uniform_int<int>(nr, 1, 20);
387 v._number_text = std::format(
"{}-{}-{}", v.
number, ban, go);
391 v.
number = house_number_(nr, pick.lo, pick.hi);
395 auto ur = call.split(
"unit");
396 const double chance = unit_chance_(request, fmt, r.population);
397 if (core::bernoulli(ur, chance)) v.
unit = unit_text_(fmt.unit_style, ur);
400 auto zr = call.split(
"postcode");
401 v.
postcode = pick_postcode_(r, fmt, zr);
403 v._lines = layout_(fmt, v, request.
script);
432 const core::rng call{call_seed};
434 v._signature = {.seed = call_seed, .data =
data_hash()};
436 auto sr = call.split(
"street");
437 const street_pick pick = pick_street_(r, request.
weighted, sr);
438 v.
name = request.
script == address_script::latin && !pick.latin.empty() ? pick.latin : pick.name;
451 const core::batch_options& options)
465 const core::batch_options& options,
466 std::uint64_t batch_seed)
const
468 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
478 template <
typename Sink>
480 std::uint64_t batch_seed, Sink&& sink)
const
482 core::for_each_batch(options, batch_seed,
483 [&](std::uint64_t item_seed) {
return get_street(request, item_seed); },
484 std::forward<Sink>(sink));
492 template <
typename Sink>
495 for_each_street(request, options, draw_seed_(), std::forward<Sink>(sink));
522 const core::rng call{call_seed};
524 auto zr = call.split(
"postcode");
525 return pick_postcode_(r, format_of_(r), zr);
533 const core::batch_options& options)
547 const core::batch_options& options,
548 std::uint64_t batch_seed)
const
550 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
560 template <
typename Sink>
562 std::uint64_t batch_seed, Sink&& sink)
const
564 core::for_each_batch(options, batch_seed,
565 [&](std::uint64_t item_seed) {
return get_postcode(request, item_seed); },
566 std::forward<Sink>(sink));
574 template <
typename Sink>
592 const core::batch_options& options)
613 const core::batch_options& options,
614 std::uint64_t batch_seed)
const
616 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
633 template <
typename Sink>
635 std::uint64_t batch_seed, Sink&& sink)
const
637 core::for_each_batch(options, batch_seed,
638 [&](std::uint64_t item_seed) {
return get_address(request, item_seed); },
639 std::forward<Sink>(sink));
648 template <
typename Sink>
659 [[nodiscard]]
bool has_data()
const {
return !_model.places.empty(); }
663 [[nodiscard]] std::size_t
place_count()
const {
return _model.places.size(); }
667 [[nodiscard]] std::size_t
country_count()
const {
return _model.codes.size(); }
671 [[nodiscard]]
const std::vector<std::string>&
countries()
const {
return _model.codes; }
678 const auto it = _model.countries.find(code);
679 return it != _model.countries.end() && it->second.has_streets;
696 const shard* s = shard_of_(code);
697 return s !=
nullptr && !s->streets.empty();
707 const auto it = _shards.find(code);
708 return it != _shards.end() && it->second->loaded.load();
720 void load(
const std::filesystem::path& dir)
723 if (!std::filesystem::is_directory(dir, ec) ||
724 !std::filesystem::is_regular_file(dir / file_name_(address_table::places), ec))
728 auto docs = read_directory_(dir);
729 std::optional<shard_config> shards;
730 const auto manifest = dir / manifest_name_;
731 if (!docs.at(index_(table::streets)) && std::filesystem::is_regular_file(manifest, ec))
733 shards = make_shard_config_(core::tsv_document::load(manifest), dir,
"");
735 apply_(std::move(docs), core::layer_mode::replace,
true, std::move(shards));
748 [[nodiscard]]
bool load(dataset tier)
750 const std::string tier_dir{core::tier_name(tier)};
751 const auto marker = tier_dir +
"/" + std::string{file_name_(address_table::places)};
752 const auto root = core::find_resources(library_name_, marker);
754 for (
const auto t : all_tables_)
756 docs.at(index_(t)) = core::load_resource(tier_dir +
"/" + std::string{file_name_(t)},
757 embedded_files_(), root);
759 if (!docs.at(index_(address_table::places)))
return false;
760 std::optional<shard_config> shards;
761 if (!docs.at(index_(address_table::streets)))
763 if (
auto manifest = core::load_resource(tier_dir +
"/" + std::string{manifest_name_},
764 embedded_files_(), root))
766 shards = make_shard_config_(*manifest, root, tier_dir +
"/");
769 apply_(std::move(docs), core::layer_mode::replace,
true, std::move(shards));
784 void extend(
const std::filesystem::path& path, core::layer_mode mode = core::layer_mode::override)
787 if (std::filesystem::is_directory(path, ec))
789 apply_(read_directory_(path), mode,
false);
792 for (
const auto t : all_tables_)
794 if (path.filename() == file_name_(t))
796 (void)
extend(t, core::tsv_document::load(path), mode);
800 throw std::invalid_argument(
"extend: expected places.tsv, formats.tsv, streets.tsv, postcodes.tsv, "
801 "street_types.tsv or street_names.tsv, got " + path.filename().string());
816 core::layer_mode mode = core::layer_mode::override)
819 docs.at(index_(which)).emplace(std::move(doc));
820 apply_(std::move(docs), mode,
false);
821 return _tables.at(index_(which)).layers().back();
833 core::layer_mode mode = core::layer_mode::override)
835 return extend(which, core::tsv_document::parse(text, std::move(source)), mode);
843 return _tables.at(index_(which)).layers();
853 for (
const auto& t : _tables)
855 any = any || !t.layers().empty();
856 h = core::hash_combine(h, t.data_hash());
860 if (_shard_cfg.active) h = core::hash_combine(h, _shard_cfg.hash);
869 static constexpr std::string_view library_name_ =
"address-generator";
870 static constexpr std::string_view manifest_name_ =
"streets.manifest.tsv";
871 static constexpr std::size_t table_count_ = 6;
872 static constexpr std::array<table, table_count_> all_tables_{table::places, table::formats,
873 table::streets, table::postcodes,
874 table::street_types, table::street_names};
876 using documents = std::array<std::optional<core::tsv_document>, table_count_>;
877 using tables = std::array<core::layered_table, table_count_>;
879 static constexpr std::size_t index_(table t) {
return static_cast<std::size_t
>(t); }
881 static constexpr std::string_view file_name_(table t)
885 case table::places:
return "places.tsv";
886 case table::formats:
return "formats.tsv";
887 case table::streets:
return "streets.tsv";
888 case table::postcodes:
return "postcodes.tsv";
889 case table::street_types:
return "street_types.tsv";
890 case table::street_names:
return "street_names.tsv";
895 static core::table_schema schema_(table t)
900 return {.key = {
"geonameid"},
901 .required = {
"country_code",
"name",
"population"},
902 .optional = {
"region_code",
"region"}};
904 return {.key = {
"country_code"},
905 .required = {
"lines",
"number_after"},
906 .optional = {
"lines_latin",
"require",
"region_style",
"zip_pattern",
"zip_completion",
907 "zip_examples",
"number_sep",
"unit_style",
"number_style"}};
909 return {.key = {
"geonameid",
"name"},
910 .required = {
"country_code",
"weight"},
911 .optional = {
"class",
"min_number",
"max_number",
"name_latin"}};
912 case table::postcodes:
913 return {.key = {
"geonameid",
"postcode"}, .required = {
"country_code",
"weight"}, .optional = {
"partial"}};
914 case table::street_types:
915 return {.key = {
"country_code",
"type"}, .required = {
"position",
"weight"}, .optional = {}};
916 case table::street_names:
917 return {.key = {
"country_code",
"name"}, .required = {
"weight"}, .optional = {}};
922 static tables make_tables_()
924 return {core::layered_table{schema_(table::places)}, core::layered_table{schema_(table::formats)},
925 core::layered_table{schema_(table::streets)}, core::layered_table{schema_(table::postcodes)},
926 core::layered_table{schema_(table::street_types)}, core::layered_table{schema_(table::street_names)}};
931 static constexpr std::size_t none_ = std::numeric_limits<std::size_t>::max();
938 std::string region_code;
940 std::uint64_t population{0};
943 enum class position : std::uint8_t { before, after, suffix };
947 std::vector<std::string> lines;
948 std::vector<std::string> lines_latin;
949 bool number_blocks{
false};
950 bool region_as_code{
false};
951 std::string zip_pattern;
952 std::string zip_completion;
953 std::vector<std::string> zip_examples;
954 bool number_after{
false};
955 bool number_comma{
false};
956 std::string unit_style;
963 std::uint32_t min_number{0};
964 std::uint32_t max_number{0};
969 std::vector<street_data>
streets;
970 core::weighted_table<std::size_t> weights;
979 std::uint64_t hash{0};
984 core::tsv_document doc;
985 core::layer_mode mode;
991 std::optional<std::filesystem::path> root;
993 std::map<std::string, shard_entry, std::less<>> entries;
994 std::uint64_t hash{0};
995 std::vector<layer_doc> layers;
1000 std::once_flag once;
1001 std::atomic<bool> loaded{
false};
1002 std::unordered_map<std::uint32_t, place_streets>
streets;
1005 using shard_map = std::map<std::string, std::unique_ptr<shard>, std::less<>>;
1007 struct postcode_data
1010 bool partial{
false};
1013 struct place_postcodes
1015 std::vector<postcode_data> codes;
1016 core::weighted_table<std::size_t> weights;
1022 position pos{position::before};
1027 std::vector<std::size_t>
places;
1028 core::weighted_table<std::size_t> place_weights;
1029 double population{0.0};
1031 bool has_format{
false};
1032 bool has_streets{
false};
1033 std::vector<type_data> types;
1034 core::weighted_table<std::size_t> type_weights;
1035 std::vector<std::string> names;
1036 core::weighted_table<std::size_t> name_weights;
1041 std::vector<place_data>
places;
1042 std::unordered_map<std::uint32_t, std::size_t> place_of_id;
1043 std::unordered_multimap<std::string, std::size_t> places_by_name;
1044 std::map<std::string, country_data, std::less<>> countries;
1045 std::vector<std::string> codes;
1046 core::weighted_table<std::string> country_weights;
1047 format_data generic_format;
1048 std::unordered_map<std::uint32_t, place_streets>
streets;
1049 std::unordered_map<std::uint32_t, place_postcodes>
postcodes;
1052 static void check_code_(
const core::tsv_row& row)
1054 if (!core::is_alpha2(row[
"country_code"]))
1056 throw row.error(
"country_code",
"expected two capital letters (ISO 3166-1 alpha-2)");
1060 static std::uint32_t parse_id_(
const core::tsv_row& row)
1062 const auto id = row.get<std::int64_t>(
"geonameid");
1063 if (
id <= 0 || std::cmp_greater(
id, std::numeric_limits<std::uint32_t>::max()))
1065 throw row.error(
"geonameid",
"must be a positive 32-bit id");
1067 return static_cast<std::uint32_t
>(id);
1070 static double parse_weight_(
const core::tsv_row& row)
1072 const auto w = row.get<
double>(
"weight");
1073 if (!(w >= 0.0))
throw row.error(
"weight",
"must not be negative");
1077 static place_data parse_place_(
const core::tsv_row& row)
1081 p.id = parse_id_(row);
1082 p.country = row.get<std::string>(
"country_code");
1083 p.name = row.get<std::string>(
"name");
1084 if (p.name.empty())
throw row.error(
"name",
"must not be empty");
1085 p.region_code = row.get_or<std::string>(
"region_code",
"");
1086 p.region = row.get_or<std::string>(
"region",
"");
1087 const auto pop = row.get<std::int64_t>(
"population");
1088 if (pop < 0)
throw row.error(
"population",
"must not be negative");
1089 p.population =
static_cast<std::uint64_t
>(pop);
1093 static std::vector<std::string> split_(std::string_view text,
char sep)
1095 std::vector<std::string> out;
1096 std::size_t start = 0;
1097 while (start <= text.size())
1099 const auto end = text.find(sep, start);
1100 const auto stop = end == std::string_view::npos ? text.size() : end;
1101 if (stop > start) out.emplace_back(text.substr(start, stop - start));
1102 if (end == std::string_view::npos)
break;
1108 static std::pair<std::string, format_data> parse_format_(
const core::tsv_row& row)
1112 f.lines = split_(row.get<std::string>(
"lines"),
'|');
1113 if (f.lines.empty())
throw row.error(
"lines",
"must have at least one line");
1114 f.lines_latin = split_(row.get_or<std::string>(
"lines_latin",
""),
'|');
1115 const auto number_style = row.get_or<std::string>(
"number_style",
"");
1116 if (!number_style.empty() && number_style !=
"plain" && number_style !=
"blocks")
1118 throw row.error(
"number_style",
"expected plain or blocks");
1120 f.number_blocks = number_style ==
"blocks";
1121 f.region_as_code = row.get_or<std::string>(
"region_style",
"name") ==
"code";
1122 f.zip_pattern = row.get_or<std::string>(
"zip_pattern",
"");
1123 f.zip_completion = row.get_or<std::string>(
"zip_completion",
"");
1124 f.zip_examples = split_(row.get_or<std::string>(
"zip_examples",
""),
'~');
1125 const auto after = row.get<
int>(
"number_after");
1126 if (after != 0 && after != 1)
throw row.error(
"number_after",
"expected 0 or 1");
1127 f.number_after = after == 1;
1128 const auto sep = row.get_or<std::string>(
"number_sep",
"space");
1129 if (sep !=
"space" && sep !=
"comma")
throw row.error(
"number_sep",
"expected space or comma");
1130 f.number_comma = sep ==
"comma";
1131 f.unit_style = row.get_or<std::string>(
"unit_style",
"none");
1132 return {row.get<std::string>(
"country_code"), std::move(f)};
1137 std::uint32_t
id{0};
1138 std::string country;
1143 static shard_config make_shard_config_(
const core::tsv_document& manifest,
1144 std::optional<std::filesystem::path> root, std::string prefix)
1148 c.root = std::move(root);
1149 c.prefix = std::move(prefix);
1150 c.hash = manifest.content_hash();
1151 for (
const auto& row : manifest.rows())
1154 const auto rows = row.get<std::int64_t>(
"rows");
1155 if (rows < 0)
throw row.error(
"rows",
"must not be negative");
1156 const auto text = row[
"hash"];
1157 std::uint64_t h = 0;
1158 bool valid = !text.empty() && text.size() <= 16;
1159 for (
const char d : text)
1161 const auto v = std::string_view{
"0123456789abcdef"}.find(d);
1162 valid = valid && v != std::string_view::npos;
1163 if (valid) h = (h << 4U) | v;
1165 if (!valid)
throw row.error(
"hash",
"expected the file's content hash in hexadecimal (up to 16 digits)");
1166 c.entries[row.get<std::string>(
"country_code")] = {.rows =
static_cast<std::size_t
>(rows), .hash = h};
1171 static street_row parse_street_(
const core::tsv_row& row)
1175 s.id = parse_id_(row);
1176 s.country = row.get<std::string>(
"country_code");
1177 s.street.name = row.get<std::string>(
"name");
1178 if (s.street.name.empty())
throw row.error(
"name",
"must not be empty");
1179 s.weight = parse_weight_(row);
1180 const auto lo = row.get_or<
int>(
"min_number", 0);
1181 const auto hi = row.get_or<
int>(
"max_number", 0);
1182 if (lo < 0 || hi < lo)
throw row.error(
"max_number",
"must not be below min_number, and numbers not negative");
1183 s.street.latin = row.get_or<std::string>(
"name_latin",
"");
1184 s.street.min_number =
static_cast<std::uint32_t
>(lo);
1185 s.street.max_number =
static_cast<std::uint32_t
>(hi);
1191 std::uint32_t
id{0};
1196 static postcode_row parse_postcode_(
const core::tsv_row& row)
1200 p.id = parse_id_(row);
1201 p.code.code = row.get<std::string>(
"postcode");
1202 if (p.code.code.empty())
throw row.error(
"postcode",
"must not be empty");
1203 p.code.partial = row.get_or<
int>(
"partial", 0) == 1;
1204 p.weight = parse_weight_(row);
1208 static std::pair<type_data, double> parse_type_(
const core::tsv_row& row, std::string& country)
1210 if (row[
"country_code"] !=
"*") check_code_(row);
1211 country = row.get<std::string>(
"country_code");
1213 t.type = row.get<std::string>(
"type");
1214 if (t.type.empty())
throw row.error(
"type",
"must not be empty");
1215 const auto pos = row[
"position"];
1216 if (pos ==
"before") t.pos = position::before;
1217 else if (pos ==
"after") t.pos = position::after;
1218 else if (pos ==
"suffix") t.pos = position::suffix;
1219 else throw row.error(
"position", std::format(
"expected before, after or suffix, got '{}'", pos));
1220 return {std::move(t), parse_weight_(row)};
1223 static void validate_(table t,
const core::tsv_row& row)
1225 std::string scratch;
1228 case table::places: (void)parse_place_(row);
break;
1229 case table::formats: (void)parse_format_(row);
break;
1230 case table::streets: (void)parse_street_(row);
break;
1231 case table::postcodes: (void)parse_postcode_(row);
break;
1232 case table::street_types: (void)parse_type_(row, scratch);
break;
1233 case table::street_names:
1234 if (row[
"country_code"] !=
"*") check_code_(row);
1235 if (row[
"name"].empty())
throw row.error(
"name",
"must not be empty");
1236 (void)parse_weight_(row);
1245 shard_config _shard_cfg;
1247 core::rng _engine{core::rng::from_entropy()};
1249 struct auto_probe_tag {};
1251 explicit adg(auto_probe_tag ) : _tables{make_tables_()}
1253 if (!
load(dataset::lite)) (void)
load(dataset::full);
1258 static std::span<const core::embedded_file> embedded_files_()
1260#ifdef DASMIG_ADDRESS_EMBED
1261 return embedded::addressgen::files;
1267 std::uint64_t draw_seed_() {
return _engine(); }
1269 static documents read_directory_(
const std::filesystem::path& dir)
1272 for (
const auto t : all_tables_)
1274 const auto file = dir / file_name_(t);
1276 if (std::filesystem::is_regular_file(file, ec))
1278 docs.at(index_(t)) = core::tsv_document::load(file);
1286 void apply_(documents docs, core::layer_mode mode,
bool reset, std::optional<shard_config> shards = {})
1289 tables next = reset ? make_tables_() : _tables;
1290 shard_config cfg = reset ? shard_config{} : _shard_cfg;
1291 if (reset && shards) cfg = std::move(*shards);
1294 if (!reset && cfg.active)
1296 if (
const auto& d = docs.at(index_(table::streets))) cfg.layers.push_back({.doc = *d, .mode = mode});
1298 for (
const auto t : all_tables_)
1300 auto& doc = docs.at(index_(t));
1301 auto& tbl = next.at(index_(t));
1304 if (reset) tbl.clear();
1308 if (t == table::street_types || t == table::street_names)
1310 (void)tbl.extend(std::move(*doc), mode, [t](
const core::tsv_row& row) { validate_(t, row); });
1314 (void)tbl.extend(std::move(*doc), mode);
1317 auto built = build_model_(next, cfg);
1318 auto built_shards = make_shards_(next, cfg);
1319 _tables = std::move(next);
1320 _model = std::move(built);
1321 _shard_cfg = std::move(cfg);
1322 _shards = std::move(built_shards);
1325 static std::string lower_(std::string_view s)
1330 if (c >=
'A' && c <=
'Z') c =
static_cast<char>(c -
'A' +
'a');
1335 static shard_map make_shards_(
const tables& tbls,
const shard_config& cfg)
1338 if (!cfg.active)
return out;
1339 for (
const auto& [code, entry] : cfg.entries) out.emplace(code, std::make_unique<shard>());
1340 for (
const auto& row : tbls.at(index_(table::
streets)).rows())
1342 out.emplace(std::string{row[
"country_code"]}, std::make_unique<shard>());
1347 static model build_model_(
const tables& tbls,
const shard_config& cfg)
1350 const auto place_rows = tbls.at(index_(table::places)).size();
1351 m.places.reserve(place_rows);
1352 m.place_of_id.reserve(place_rows);
1353 m.places_by_name.reserve(place_rows);
1354 m.postcodes.reserve(place_rows);
1355 if (!cfg.active) m.streets.reserve(place_rows);
1356 for (
const auto& row : tbls.at(index_(table::
places)).rows())
1358 m.places.push_back(parse_place_(row));
1360 std::map<std::string, std::vector<std::size_t>, std::less<>> by_country;
1361 for (std::size_t i = 0; i < m.places.size(); ++i)
1363 const auto& p = m.places[i];
1364 m.place_of_id[p.id] = i;
1365 m.places_by_name.emplace(lower_(p.name), i);
1366 by_country[p.country].push_back(i);
1368 for (
auto& [code, idx] : by_country)
1370 auto& c = m.countries[code];
1371 c.places = std::move(idx);
1372 for (
const auto i : c.
places)
1374 const double w =
static_cast<double>(std::max<std::uint64_t>(m.places[i].population, 1));
1375 c.place_weights.add(i, w);
1378 m.codes.push_back(code);
1379 m.country_weights.add(code, c.population);
1381 std::ranges::sort(m.codes);
1383 for (
const auto& row : tbls.at(index_(table::
formats)).rows())
1385 auto [code, f] = parse_format_(row);
1386 auto& c = m.countries[code];
1387 c.format = std::move(f);
1388 c.has_format =
true;
1390 m.generic_format.lines = {
"{A}",
"{Z} {C}"};
1392 for (
const auto& row : tbls.at(index_(table::
streets)).rows())
1394 auto s = parse_street_(row);
1395 m.countries[s.country].has_streets =
true;
1396 if (!cfg.active) add_street_(m.streets, std::move(s));
1398 if (cfg.active && !std::ranges::any_of(cfg.layers, [](
const layer_doc& l)
1399 { return l.mode == core::layer_mode::replace; }))
1401 for (
const auto& [code, entry] : cfg.entries)
1403 if (entry.rows > 0) m.countries[code].has_streets =
true;
1406 for (
const auto& row : tbls.at(index_(table::
postcodes)).rows())
1408 auto p = parse_postcode_(row);
1409 auto& pp = m.postcodes[p.id];
1410 pp.weights.add(pp.codes.size(), p.weight);
1411 pp.codes.push_back(std::move(p.code));
1415 m.countries.try_emplace(
"*");
1416 fill_composed_(tbls, m.countries);
1420 static void add_street_(std::unordered_map<std::uint32_t, place_streets>& into, street_row s)
1422 auto& ps = into[s.id];
1423 ps.weights.add(ps.streets.size(), s.weight);
1424 ps.streets.push_back(std::move(s.street));
1428 void fill_shard_(
const std::string& country, shard& s)
const
1430 std::optional<core::tsv_document> base;
1431 if (
const auto e = _shard_cfg.entries.find(country); e != _shard_cfg.entries.end() && e->second.rows > 0)
1433 const auto path = std::format(
"{}streets/{}.tsv", _shard_cfg.prefix, country);
1434 base = core::load_resource(path, embedded_files_(), _shard_cfg.root);
1437 throw std::runtime_error(
"Street data for " + country +
" is listed in " +
1438 std::string{manifest_name_} +
" but " + path +
" was not found");
1440 if (base->content_hash() != e->second.hash || base->size() != e->second.rows)
1442 throw std::runtime_error(std::format(
1443 "{} does not match {} (hash {:016x}, {} rows; expected {:016x}, {})", path, manifest_name_,
1444 base->content_hash(), base->size(), e->second.hash, e->second.rows));
1447 if (_shard_cfg.layers.empty())
1451 for (
const auto& row : base->rows()) add_street_(s.
streets, parse_street_(row));
1456 core::layered_table t{schema_(table::streets)};
1457 if (base) (void)t.extend(std::move(*base), core::layer_mode::replace);
1458 for (
const auto& l : _shard_cfg.
layers)
1460 (void)t.extend(l.doc, l.mode, [](
const core::tsv_row& row) { validate_(table::streets, row); });
1462 for (
const auto& row : t.rows())
1464 if (row[
"country_code"] == country) add_street_(s.streets, parse_street_(row));
1467 s.loaded.store(
true);
1472 [[nodiscard]]
const shard* shard_of_(std::string_view country)
const
1474 const auto it = _shards.find(country);
1475 if (it == _shards.end())
return nullptr;
1476 shard& s = *it->second;
1477 std::call_once(s.once, [&] { fill_shard_(std::string{country}, s); });
1483 static void fill_composed_(
const tables& tbls, std::map<std::string, country_data, std::less<>>& countries)
1485 for (
const auto& row : tbls.at(index_(table::
street_types)).rows())
1487 std::string country;
1488 auto [t, w] = parse_type_(row, country);
1489 const auto it = countries.find(country);
1490 if (it == countries.end())
continue;
1491 auto& c = it->second;
1492 c.type_weights.add(c.types.size(), w);
1493 c.types.push_back(std::move(t));
1495 for (
const auto& row : tbls.at(index_(table::
street_names)).rows())
1497 const auto it = countries.find(row[
"country_code"]);
1498 if (it == countries.end())
continue;
1499 auto& c = it->second;
1500 c.name_weights.add(c.names.size(), parse_weight_(row));
1501 c.names.push_back(row.get<std::string>(
"name"));
1505 void require_data_()
const
1507 if (_model.places.empty())
1509 throw std::runtime_error(
"No address data loaded. Call load() first.");
1515 std::size_t index{none_};
1516 std::uint32_t
id{0};
1517 std::string country;
1519 std::string region_code;
1521 std::uint64_t population{0};
1522 const country_data* data{
nullptr};
1525 [[nodiscard]]
const country_data* country_of_(
const std::string& code)
const
1527 const auto it = _model.countries.find(code);
1528 return it != _model.countries.end() ? &it->second :
nullptr;
1531 [[nodiscard]] resolved from_place_(std::size_t index)
const
1533 const auto& p = _model.places[index];
1534 return {.index = index, .id = p.id, .country = p.country, .city = p.name,
1535 .region_code = p.region_code, .region = p.region, .population = p.population,
1536 .data = country_of_(p.country)};
1541 [[nodiscard]] resolved resolve_place_(
const std::optional<std::string>& want_country,
1542 const std::optional<place_ref>& want_place,
bool weighted,
1543 const core::rng& call)
const
1547 if (
const auto index = find_place_(want_country, *want_place); index != none_)
return from_place_(index);
1550 throw std::invalid_argument(
"place '" + want_place->city +
1551 "' is not in the data; give a country to use it as given");
1553 return {.index = none_, .id = 0, .country = *want_country, .city = want_place->city,
1554 .region_code = want_place->region, .region = want_place->region, .population = 0,
1555 .data = country_of_(*want_country)};
1557 auto cr = call.split(
"country");
1558 const std::string country = want_country ? *want_country : pick_country_(cr, weighted);
1559 const country_data* cd = country_of_(country);
1560 if (cd ==
nullptr || cd->places.empty())
1562 throw std::invalid_argument(
"no places for country '" + country +
"'");
1564 auto pr = call.split(
"place");
1565 const auto& c = *cd;
1566 return from_place_(weighted ? c.place_weights.pick(pr)
1567 : c.
places[core::uniform_int<std::size_t>(pr, 0, c.
places.size() - 1)]);
1570 [[nodiscard]]
const format_data& format_of_(
const resolved& r)
const
1572 return r.data !=
nullptr && r.data->has_format ? r.data->format : _model.generic_format;
1579 std::uint32_t lo{0};
1580 std::uint32_t hi{0};
1585 [[nodiscard]] street_pick pick_street_(
const resolved& r,
bool weighted, core::rng& sr)
const
1587 const place_streets* ps = r.index != none_ ? streets_of_(r.country, r.id) : nullptr;
1588 if (ps !=
nullptr && weighted && !(ps->weights.total_weight() > 0.0)) ps =
nullptr;
1589 if (ps ==
nullptr)
return {.name = composed_street_(r, sr), .latin = {}, .lo = 0, .hi = 0, .real =
false};
1590 const auto& s = ps->streets[weighted ? ps->weights.pick_index(sr)
1591 : core::uniform_int<std::size_t>(sr, 0, ps->streets.size() - 1)];
1592 return {.name = s.name, .latin = s.latin, .lo = s.min_number, .hi = s.max_number, .real =
true};
1595 [[nodiscard]] std::string pick_postcode_(
const resolved& r,
const format_data& fmt, core::rng& zr)
const
1597 const place_postcodes* pc = r.index != none_ ? postcodes_of_(r.id) : nullptr;
1598 if (pc !=
nullptr && !(pc->weights.total_weight() > 0.0)) pc =
nullptr;
1599 return postcode_(fmt, pc, zr);
1602 [[nodiscard]] std::string pick_country_(core::rng& r,
bool weighted)
const
1604 if (weighted)
return _model.country_weights.pick(r);
1605 return _model.codes[core::uniform_int<std::size_t>(r, 0, _model.codes.size() - 1)];
1608 [[nodiscard]] std::size_t find_place_(
const std::optional<std::string>& want_country,
const place_ref& ref)
const
1610 const auto range = _model.places_by_name.equal_range(lower_(ref.city));
1611 std::size_t best = none_;
1612 bool best_region =
false;
1613 for (
auto it = range.first; it != range.second; ++it)
1615 const auto& p = _model.places[it->second];
1616 if (want_country && p.country != *want_country)
continue;
1617 const bool region_ok = !ref.region.empty() && (lower_(p.region) == lower_(ref.region) ||
1618 lower_(p.region_code) == lower_(ref.region));
1619 if (!ref.region.empty() && !region_ok && best != none_ && best_region)
continue;
1620 const bool better = best == none_ || (region_ok && !best_region) ||
1621 (region_ok == best_region && p.population > _model.places[best].population) ||
1622 (region_ok == best_region && p.population == _model.places[best].population &&
1623 p.id < _model.places[best].id);
1627 best_region = region_ok;
1633 [[nodiscard]]
const place_streets* streets_of_(std::string_view country, std::uint32_t
id)
const
1635 if (_shard_cfg.active)
1637 const shard* s = shard_of_(country);
1638 if (s ==
nullptr)
return nullptr;
1639 const auto it = s->streets.find(
id);
1640 return it == s->streets.end() ? nullptr : &it->second;
1642 const auto it = _model.streets.find(
id);
1643 return it == _model.streets.end() ? nullptr : &it->second;
1646 [[nodiscard]]
const place_postcodes* postcodes_of_(std::uint32_t
id)
const
1648 const auto it = _model.postcodes.find(
id);
1649 return it == _model.postcodes.end() ? nullptr : &it->second;
1652 static std::string region_text_(
const format_data& f,
const std::string& code,
const std::string& name)
1654 const bool alpha = !code.empty() && code.size() <= 3 &&
1655 std::ranges::all_of(code, [](
char c) {
return c >=
'A' && c <=
'Z'; });
1656 return f.region_as_code && alpha ? code : name;
1659 [[nodiscard]] std::string composed_street_(
const resolved& place, core::rng& r)
const
1661 const country_data* c = place.data;
1662 if (c ==
nullptr || c->types.empty() || c->names.empty()) c = &_model.countries.at(
"*");
1663 if (c->types.empty() || c->names.empty())
1665 throw std::runtime_error(
"No street data and no composed-street tables loaded for country " + place.country);
1667 const auto& t = c->types[c->type_weights.pick_index(r)];
1668 const auto& n = c->names[c->name_weights.pick_index(r)];
1671 case position::before:
return t.type +
" " + n;
1672 case position::after:
return n +
" " + t.type;
1673 case position::suffix:
return n + t.type;
1679 static std::uint32_t house_number_(core::rng& r, std::uint32_t lo, std::uint32_t hi)
1681 const double u = core::uniform01(r);
1684 const double x = u * u;
1685 return 1U +
static_cast<std::uint32_t
>(120.0 * x);
1687 const double x = u * std::sqrt(u);
1688 const auto span =
static_cast<double>(hi - lo + 1U);
1689 return std::min(hi, lo +
static_cast<std::uint32_t
>(span * x));
1692 static double unit_chance_(
const address_request& request,
const format_data& f, std::uint64_t population)
1694 if (f.unit_style.empty() || f.unit_style ==
"none")
return 0.0;
1695 if (request.unit_chance)
return std::clamp(*request.unit_chance, 0.0, 1.0);
1696 return population >= 250'000 ? 0.30 : 0.12;
1699 static std::string unit_text_(
const std::string& style, core::rng& r)
1701 const auto n = core::uniform_int<int>(r, 1, 40);
1702 if (style ==
"flat")
return "Flat " + std::to_string(n);
1703 if (style ==
"app_fr")
return "Appt " + std::to_string(n);
1704 if (style ==
"whg")
return "Whg. " + std::to_string(n);
1705 if (style ==
"ap_br")
return "apto " + std::to_string(n);
1706 if (style ==
"piso")
1708 const int floor = core::uniform_int<int>(r, 1, 9);
1709 const char letter =
static_cast<char>(
'A' + core::uniform_int<int>(r, 0, 3));
1710 return std::to_string(floor) +
"º " + std::string(1, letter);
1712 return "Apt " + std::to_string(n);
1718 static std::string expand_(std::string_view pattern, core::rng& r)
1720 static constexpr std::string_view safe =
"ABCEGHJKLMNPRTVXY";
1721 static constexpr std::string_view uk =
"ABDEFGHJLNPQRSTUWXYZ";
1723 for (
const char c : pattern)
1727 case '#': out +=
static_cast<char>(
'0' + core::uniform_int<int>(r, 0, 9));
break;
1728 case 'N': out +=
static_cast<char>(
'1' + core::uniform_int<int>(r, 0, 8));
break;
1729 case 'A': out +=
static_cast<char>(
'A' + core::uniform_int<int>(r, 0, 25));
break;
1730 case 'L': out += safe[core::uniform_int<std::size_t>(r, 0, safe.size() - 1)];
break;
1731 case 'U': out += uk[core::uniform_int<std::size_t>(r, 0, uk.size() - 1)];
break;
1732 case '_': out +=
' ';
break;
1733 default: out += c;
break;
1739 static std::string postcode_(
const format_data& f,
const place_postcodes* pc, core::rng& r)
1741 if (pc !=
nullptr && !pc->codes.empty())
1743 const auto& c = pc->codes[pc->weights.pick_index(r)];
1744 return c.partial ? c.code + expand_(f.zip_completion, r) : c.code;
1746 if (!f.zip_pattern.empty())
return expand_(f.zip_pattern, r);
1747 if (!f.zip_examples.empty())
1749 return f.zip_examples[core::uniform_int<std::size_t>(r, 0, f.zip_examples.size() - 1)];
1754 static void replace_all_(std::string& s, std::string_view from, std::string_view to)
1756 for (std::size_t pos = s.find(from); pos != std::string::npos; pos = s.find(from, pos + to.size()))
1758 s.replace(pos, from.size(), to);
1762 static std::string tidy_(std::string s)
1765 using fix = std::pair<std::string_view, std::string_view>;
1766 for (
const auto& [bad, good] : {fix{
" ",
" "}, fix{
" ,",
","}, fix{
", ,",
","}, fix{
",,",
","}})
1768 while (s.contains(bad)) replace_all_(s, bad, good);
1770 const auto is_edge = [](
char c) {
return c ==
' ' || c ==
',' || c ==
'-'; };
1772 std::size_t e = s.size();
1773 while (b < e && is_edge(s[b])) ++b;
1774 while (e > b && is_edge(s[e - 1])) --e;
1775 return s.substr(b, e - b);
1778 static std::vector<std::string> layout_(
const format_data& f,
const address& v, address_script script)
1780 std::string street = v.street;
1783 const auto n = v._number_text.empty() ? std::to_string(v.number) : v._number_text;
1784 street = f.number_after ? street + (f.number_comma ?
", " :
" ") + n : n +
" " + street;
1786 if (!v.unit.empty()) street +=
", " + v.unit;
1788 std::vector<std::string> out;
1789 const auto& templates = script == address_script::latin && !f.lines_latin.empty() ? f.lines_latin : f.lines;
1790 const std::string_view none;
1791 for (
const auto& tmpl : templates)
1794 const bool has_city = tmpl.contains(
"{C}");
1795 const std::string_view region = v.region == v.city && has_city ? none : std::string_view{v.region};
1797 line.reserve(tmpl.size() + street.size() + v.city.size() + v.postcode.size());
1799 for (std::size_t i = 0; i < tmpl.size(); ++i)
1801 std::string_view value;
1802 if (tmpl[i] ==
'{' && i + 2 < tmpl.size() && tmpl[i + 2] ==
'}')
1804 switch (tmpl[i + 1])
1806 case 'A': value = street;
break;
1807 case 'C': value = v.city;
break;
1808 case 'S': value = region;
break;
1809 case 'Z': value = v.postcode;
break;
1812 default: line += tmpl[i];
continue;
1814 any = any || !value.empty();
1822 line = tidy_(std::move(line));
1823 if (!line.empty()) out.push_back(std::move(line));