Address Generator 2.1.2
Postal addresses with real streets by place, laid out in each country's format for C++23
Loading...
Searching...
No Matches
addressgen.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Diego Dasso Migotto
3
4#ifndef DASMIG_ADDRESSGEN_HPP
5#define DASMIG_ADDRESSGEN_HPP
6
7#include "core.hpp"
8#include <algorithm>
9#include <array>
10#include <atomic>
11#include <cmath>
12#include <cstddef>
13#include <cstdint>
14#include <filesystem>
15#include <format>
16#include <limits>
17#include <map>
18#include <memory>
19#include <mutex>
20#include <optional>
21#include <ostream>
22#include <span>
23#include <stdexcept>
24#include <string>
25#include <string_view>
26#include <system_error>
27#include <unordered_map>
28#include <utility>
29#include <vector>
30
31#ifdef DASMIG_ADDRESS_EMBED
32#include "addressgen_data.hpp"
33#endif
34
35/// @file addressgen.hpp
36/// @brief Address Generator library — postal addresses with real streets by place, laid out in each country's format for C++23.
37/// @author Diego Dasso Migotto (diegomigotto at hotmail dot com)
38/// @see See doc/usage.md for the narrative tutorial.
39
40DASMIG_CORE_REQUIRE(1, 2);
41
42namespace dasmig
43{
44
45/// @brief A place a request can anchor an address to, by plain values.
46///
47/// `persona-generator` and `user-generator` pass the city they drew.
49{
50 std::string city; ///< The place's name (any case); matched against the loaded places.
51 std::string region; ///< Optional region name or code, to tell places of one name apart.
52};
53
54/// @brief The script an address is written in.
55enum class address_script : std::uint8_t
56{
57 local, ///< The country's own layout (the default).
58 latin ///< The Latin-script layout, for the countries that have one (`lines_latin`); others keep their own.
59};
60
61/// @brief What an address call asks for.
62///
63/// Every generation call takes one; `address_request{}` asks for the
64/// defaults. Choices are never stored on the generator, so one call cannot
65/// change the next.
67{
68 /// @brief ISO-3166 two-letter country code, or a country drawn when absent.
69 std::optional<std::string> country{std::nullopt};
70
71 /// @brief Anchor the address to this place instead of drawing one.
72 ///
73 /// A place the data does not know is used as given (its street comes from
74 /// the composed fallback and its postcode from the country's pattern),
75 /// which needs `country`.
76 std::optional<place_ref> place{std::nullopt};
77
78 /// @brief Weighted selection (default): countries and places by
79 /// population, streets by address count. False selects uniformly.
80 bool weighted{true};
81
82 /// @brief Chance of a unit (apartment, flat) in the address; the
83 /// country's default when absent. Countries without a unit style
84 /// never have one.
85 std::optional<double> unit_chance{std::nullopt};
86
87 /// @brief The script of the layout and of the street name. With `latin`, a
88 /// street named in another script is given by its Latin name when
89 /// the data has one (OpenStreetMap's `name:en` and similar tags);
90 /// otherwise it keeps the local name. Place names are Latin either way.
91 address_script script{address_script::local};
92};
93
94/// @brief What a street call asks for: the street an address in that place
95/// would have. Same fields as address_request, without the unit and script.
97{
98 std::optional<std::string> country{std::nullopt}; ///< ISO 3166-1 alpha-2, or drawn when absent.
99 std::optional<place_ref> place{std::nullopt}; ///< Anchor to this place instead of drawing one.
100 bool weighted{true}; ///< Streets by address count; false is uniform.
101 address_script script{address_script::local}; ///< `latin` gives the Latin name where the data has one.
102};
103
104/// @brief What a postcode call asks for.
106{
107 std::optional<std::string> country{std::nullopt}; ///< ISO 3166-1 alpha-2, or drawn when absent.
108 std::optional<place_ref> place{std::nullopt}; ///< Anchor to this place instead of drawing one.
109 bool weighted{true}; ///< Postcodes by their weight; false is uniform.
110};
111
112/// @brief Return type for address generation.
113///
114/// Supports implicit conversion to std::string (the address on one line),
115/// streaming via operator<< and JSON output via to_json().
117{
118 public:
119 std::string country_code; ///< ISO 3166-1 alpha-2.
120 std::string city; ///< The place's name.
121 std::string region; ///< The region as the country writes it (an abbreviation in the US).
122 std::string street; ///< The street's name, with its type.
123 std::uint32_t number{0}; ///< House number; 0 when the street has none.
124 std::string unit; ///< Apartment or flat, empty when there is none.
125 std::string postcode; ///< In the country's format; empty where there are no postcodes.
126 bool real_street{false}; ///< True when the street comes from the place's own data, false when composed.
127 std::uint32_t geonameid{0}; ///< The place's GeoNames id (city-generator's); 0 for a place given by name only.
128
129 /// @brief The address lines in the country's order.
130 /// @return One string per line, without the recipient lines.
131 [[nodiscard]] const std::vector<std::string>& lines() const { return _lines; }
132
133 /// @brief The address on one line.
134 /// @param separator Placed between the lines.
135 /// @return The lines joined.
136 [[nodiscard]] std::string formatted(std::string_view separator = ", ") const
137 {
138 std::string out;
139 for (const auto& l : _lines)
140 {
141 if (!out.empty()) out += separator;
142 out += l;
143 }
144 return out;
145 }
146
147 /// @brief Retrieve the random seed used to generate this address.
148 /// @return The call seed; pass it to get_address(request, seed) with
149 /// the same request and data to replay this address.
150 [[nodiscard]] std::uint64_t seed() const { return _signature.seed; }
151
152 /// @brief The seed and the data this address was generated from.
153 /// @return The replay signature; `data` is adg::data_hash() at the
154 /// time of generation.
155 [[nodiscard]] const core::seed_signature& signature() const
156 {
157 return _signature;
158 }
159
160 /// @brief Human-readable summary string.
161 /// @return The address on one line.
162 [[nodiscard]] std::string to_string() const { return formatted(", "); }
163
164 /// @brief Write this address as a JSON object.
165 /// @param w Writer positioned where a value is allowed.
166 void to_json(core::json_writer& w) const
167 {
168 w.begin_object()
169 .field("country_code", country_code)
170 .field("city", city)
171 .field("region", region)
172 .field("street", street)
173 .field("number", number)
174 .field("unit", unit)
175 .field("postcode", postcode)
176 .field("real_street", real_street)
177 .field("geonameid", geonameid)
178 .key("lines")
179 .begin_array();
180 for (const auto& l : _lines) w.value(l);
181 w.end_array().field("formatted", formatted()).key("signature");
182 _signature.to_json(w);
183 w.end_object();
184 }
185
186 /// @brief This address as a JSON document.
187 /// @return Compact JSON.
188 [[nodiscard]] std::string to_json() const
189 {
190 core::json_writer w;
191 to_json(w);
192 return w.str();
193 }
194
195 /// @brief Implicit conversion to std::string.
196 /// @return Summary string.
197 operator std::string() const // NOLINT(hicpp-explicit-conversions)
198 {
199 return to_string();
200 }
201
202 /// @brief Stream the summary to an output stream.
203 /// @param os Output stream.
204 /// @param v The address to stream.
205 /// @return The stream.
206 friend std::ostream& operator<<(std::ostream& os, const address& v)
207 {
208 os << v.to_string();
209 return os;
210 }
211
212 private:
213 std::vector<std::string> _lines;
214 std::string _number_text; // when the number is not a plain integer (Japan's blocks)
215 core::seed_signature _signature;
216 friend class adg;
217};
218
219/// @brief Return type for street generation: a street of a place.
220///
221/// Converts to the street's name, streams as it and writes JSON.
223{
224 public:
225 std::string name; ///< The street's name, with its type.
226 std::string country_code; ///< ISO 3166-1 alpha-2.
227 std::string city; ///< The place the street is in.
228 bool real_street{false}; ///< True when it comes from the place's own data, false when composed.
229 std::uint32_t geonameid{0}; ///< The place's GeoNames id; 0 for a place given by name only.
230
231 /// @brief Retrieve the random seed used to generate this street.
232 /// @return The call seed.
233 [[nodiscard]] std::uint64_t seed() const { return _signature.seed; }
234
235 /// @brief The seed and the data this street was generated from.
236 /// @return The replay signature.
237 [[nodiscard]] const core::seed_signature& signature() const { return _signature; }
238
239 /// @brief Write this street as a JSON object.
240 /// @param w Writer positioned where a value is allowed.
241 void to_json(core::json_writer& w) const
242 {
243 w.begin_object()
244 .field("name", name)
245 .field("country_code", country_code)
246 .field("city", city)
247 .field("real_street", real_street)
248 .field("geonameid", geonameid)
249 .key("signature");
250 _signature.to_json(w);
251 w.end_object();
252 }
253
254 /// @brief This street as a JSON document.
255 /// @return Compact JSON.
256 [[nodiscard]] std::string to_json() const
257 {
258 core::json_writer w;
259 to_json(w);
260 return w.str();
261 }
262
263 /// @brief Implicit conversion to std::string.
264 /// @return The street's name.
265 operator std::string() const { return name; } // NOLINT(hicpp-explicit-conversions)
266
267 /// @brief Stream the name to an output stream.
268 /// @param os Output stream.
269 /// @param v The street to stream.
270 /// @return The stream.
271 friend std::ostream& operator<<(std::ostream& os, const street& v)
272 {
273 os << v.name;
274 return os;
275 }
276
277 private:
278 core::seed_signature _signature;
279 friend class adg;
280};
281
282/// @brief The tables of the address data, for extending it.
283enum class address_table : std::uint8_t
284{
285 places, ///< `places.tsv`: the places addresses are drawn in.
286 formats, ///< `formats.tsv`: each country's layout and postcode format.
287 streets, ///< `streets.tsv`: real streets by place, weighted by address count.
288 postcodes, ///< `postcodes.tsv`: postcodes by place.
289 street_types, ///< `street_types.tsv`: types for composed streets.
290 street_names ///< `street_names.tsv`: names for composed streets.
291};
292
293/// @brief Address Generator: postal addresses with real streets by place.
294///
295/// A call draws a country (by population), a place in it (by population),
296/// a street of that place (by address count), a house number, a unit and a
297/// postcode, each from a stream split from the call seed (`"country"`,
298/// `"place"`, `"street"`, `"number"`, `"unit"`, `"postcode"`), and lays the
299/// address out in the country's order. Places without street data get a
300/// composed street. Output is identical on every compiler and platform
301/// (dasmig-core).
302///
303/// Can be used as a singleton via instance() or constructed
304/// independently.
305///
306/// @par Thread safety
307/// Each instance is independent. Concurrent calls to get_address(request, seed) on the
308/// **same** instance are safe, including the first read of a `full` country's
309/// street file; calls that use the instance engine, loading and extending
310/// require external synchronisation.
311class adg
312{
313 public:
314 /// @brief Default constructor — creates an empty generator with no
315 /// data.
316 adg() : _tables{make_tables_()} {}
317
318 adg(const adg&) = delete; ///< Not copyable.
319 adg& operator=(const adg&) = delete; ///< Not copyable.
320 adg(adg&&) noexcept = default; ///< Movable.
321 adg& operator=(adg&&) noexcept = default; ///< Movable.
322 ~adg() = default; ///< Destructor.
323
324 /// @brief Access the global singleton instance.
325 ///
326 /// Loads the `lite` tier (or `full` if `lite` is missing) on first
327 /// access, from compiled-in data or an auto-probed `resources/`.
328 /// @return The singleton.
329 static adg& instance()
330 {
331 static adg inst{auto_probe_tag{}};
332 return inst;
333 }
334
335 // -- Generation -------------------------------------------------------
336
337 /// @brief Generate a random address.
338 /// Use address_request{} for the defaults.
339 /// @param request What to generate.
340 /// @return The address.
341 /// @throws std::runtime_error If no data has been loaded.
342 /// @throws std::invalid_argument If the request names an unknown country
343 /// or place.
344 [[nodiscard]] address get_address(const address_request& request)
345 {
346 return get_address(request, draw_seed_());
347 }
348
349 /// @brief Generate a deterministic address.
350 /// @param request What to generate.
351 /// @param call_seed Seed; the same request, seed and data give the same
352 /// result. Does not touch the instance engine.
353 /// @return The address.
354 /// @throws std::runtime_error If no data has been loaded.
355 /// @throws std::invalid_argument If the request names an unknown country
356 /// or place.
357 [[nodiscard]] address get_address(const address_request& request, std::uint64_t call_seed) const
358 {
359 require_data_();
360 const core::rng call{call_seed};
361 address v;
362 v._signature = {.seed = call_seed, .data = data_hash()};
363
364 // Country and place.
365 const resolved r = resolve_place_(request.country, request.place, request.weighted, call);
366 const std::string& country = r.country;
367 v.geonameid = r.id;
368 const format_data& fmt = format_of_(r);
369
370 v.country_code = country;
371 v.city = r.city;
372 v.region = region_text_(fmt, r.region_code, r.region);
373
374 // Street and number.
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;
378 v.real_street = pick.real;
379 auto nr = call.split("number");
380 if (fmt.number_blocks)
381 {
382 // Japan: chome-ban-go.
383 v.number = core::uniform_int<std::uint32_t>(nr, 1, 9);
384 // Drawn in statements: the order of a call's arguments is unspecified.
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);
388 }
389 else
390 {
391 v.number = house_number_(nr, pick.lo, pick.hi);
392 }
393
394 // Unit.
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);
398
399 // Postcode.
400 auto zr = call.split("postcode");
401 v.postcode = pick_postcode_(r, fmt, zr);
402
403 v._lines = layout_(fmt, v, request.script);
404 return v;
405 }
406
407 // -- Streets and postcodes -----------------------------------------------
408
409 /// @brief Generate a random street. Use street_request{} for the defaults.
410 /// @param request What to generate.
411 /// @return The street.
412 /// @throws std::runtime_error If no data has been loaded.
413 /// @throws std::invalid_argument If the request names an unknown country or place.
414 [[nodiscard]] street get_street(const street_request& request)
415 {
416 return get_street(request, draw_seed_());
417 }
418
419 /// @brief Generate a deterministic street.
420 ///
421 /// Draws from the same streams as get_address(), so for the same request
422 /// fields and seed it is the street of that address.
423 ///
424 /// @param request What to generate.
425 /// @param call_seed Seed; does not touch the instance engine.
426 /// @return The street.
427 /// @throws std::runtime_error If no data has been loaded.
428 /// @throws std::invalid_argument If the request names an unknown country or place.
429 [[nodiscard]] street get_street(const street_request& request, std::uint64_t call_seed) const
430 {
431 require_data_();
432 const core::rng call{call_seed};
433 street v;
434 v._signature = {.seed = call_seed, .data = data_hash()};
435 const resolved r = resolve_place_(request.country, request.place, request.weighted, call);
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;
439 v.real_street = pick.real;
440 v.country_code = r.country;
441 v.city = r.city;
442 v.geonameid = r.id;
443 return v;
444 }
445
446 /// @brief Generate a batch of streets from a seed drawn like get_street() draws its own.
447 /// @param request What every item is generated from.
448 /// @param options Count, first index, threads and uniqueness.
449 /// @return The results, in index order.
450 [[nodiscard]] std::vector<street> get_street_batch(const street_request& request,
451 const core::batch_options& options)
452 {
453 return get_street_batch(request, options, draw_seed_());
454 }
455
456 /// @brief Generate a batch of streets, deterministically.
457 ///
458 /// Item `i` is `get_street(request, core::item_seed(batch_seed, options.first + i))`.
459 ///
460 /// @param request What every item is generated from.
461 /// @param options Count, first index, threads and uniqueness (a rule made for `street`).
462 /// @param batch_seed Seed of the whole batch.
463 /// @return The results, in index order.
464 [[nodiscard]] std::vector<street> get_street_batch(const street_request& request,
465 const core::batch_options& options,
466 std::uint64_t batch_seed) const
467 {
468 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
469 { return get_street(request, item_seed); });
470 }
471
472 /// @brief Stream a batch of streets to a sink without holding the whole batch.
473 /// @tparam Sink Callable taking `(std::uint64_t, street&)`.
474 /// @param request What every item is generated from.
475 /// @param options Count, first index, threads and uniqueness.
476 /// @param batch_seed Seed of the whole batch.
477 /// @param sink Receives the items, in index order, on the calling thread.
478 template <typename Sink>
479 void for_each_street(const street_request& request, const core::batch_options& options,
480 std::uint64_t batch_seed, Sink&& sink) const
481 {
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));
485 }
486
487 /// @brief Stream a batch of streets with a batch seed drawn like get_street() draws its own.
488 /// @tparam Sink Callable taking `(std::uint64_t, street&)`.
489 /// @param request What every item is generated from.
490 /// @param options Count, first index, threads and uniqueness.
491 /// @param sink Receives the items.
492 template <typename Sink>
493 void for_each_street(const street_request& request, const core::batch_options& options, Sink&& sink)
494 {
495 for_each_street(request, options, draw_seed_(), std::forward<Sink>(sink));
496 }
497
498 /// @brief Generate a random postcode in the country's format. Use
499 /// postcode_request{} for the defaults.
500 /// @param request What to generate.
501 /// @return The postcode; empty for a country without postcodes.
502 /// @throws std::runtime_error If no data has been loaded.
503 /// @throws std::invalid_argument If the request names an unknown country or place.
504 [[nodiscard]] std::string get_postcode(const postcode_request& request)
505 {
506 return get_postcode(request, draw_seed_());
507 }
508
509 /// @brief Generate a deterministic postcode.
510 ///
511 /// Draws from the same streams as get_address(), so for the same request
512 /// fields and seed it is the postcode of that address.
513 ///
514 /// @param request What to generate.
515 /// @param call_seed Seed; does not touch the instance engine.
516 /// @return The postcode; empty for a country without postcodes.
517 /// @throws std::runtime_error If no data has been loaded.
518 /// @throws std::invalid_argument If the request names an unknown country or place.
519 [[nodiscard]] std::string get_postcode(const postcode_request& request, std::uint64_t call_seed) const
520 {
521 require_data_();
522 const core::rng call{call_seed};
523 const resolved r = resolve_place_(request.country, request.place, request.weighted, call);
524 auto zr = call.split("postcode");
525 return pick_postcode_(r, format_of_(r), zr);
526 }
527
528 /// @brief Generate a batch of postcodes from a seed drawn like get_postcode() draws its own.
529 /// @param request What every item is generated from.
530 /// @param options Count, first index, threads and uniqueness.
531 /// @return The results, in index order.
532 [[nodiscard]] std::vector<std::string> get_postcode_batch(const postcode_request& request,
533 const core::batch_options& options)
534 {
535 return get_postcode_batch(request, options, draw_seed_());
536 }
537
538 /// @brief Generate a batch of postcodes, deterministically.
539 ///
540 /// Item `i` is `get_postcode(request, core::item_seed(batch_seed, options.first + i))`.
541 ///
542 /// @param request What every item is generated from.
543 /// @param options Count, first index, threads and uniqueness (a rule made for `std::string`).
544 /// @param batch_seed Seed of the whole batch.
545 /// @return The results, in index order.
546 [[nodiscard]] std::vector<std::string> get_postcode_batch(const postcode_request& request,
547 const core::batch_options& options,
548 std::uint64_t batch_seed) const
549 {
550 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
551 { return get_postcode(request, item_seed); });
552 }
553
554 /// @brief Stream a batch of postcodes to a sink without holding the whole batch.
555 /// @tparam Sink Callable taking `(std::uint64_t, std::string&)`.
556 /// @param request What every item is generated from.
557 /// @param options Count, first index, threads and uniqueness.
558 /// @param batch_seed Seed of the whole batch.
559 /// @param sink Receives the items, in index order, on the calling thread.
560 template <typename Sink>
561 void for_each_postcode(const postcode_request& request, const core::batch_options& options,
562 std::uint64_t batch_seed, Sink&& sink) const
563 {
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));
567 }
568
569 /// @brief Stream a batch of postcodes with a batch seed drawn like get_postcode() draws its own.
570 /// @tparam Sink Callable taking `(std::uint64_t, std::string&)`.
571 /// @param request What every item is generated from.
572 /// @param options Count, first index, threads and uniqueness.
573 /// @param sink Receives the items.
574 template <typename Sink>
575 void for_each_postcode(const postcode_request& request, const core::batch_options& options, Sink&& sink)
576 {
577 for_each_postcode(request, options, draw_seed_(), std::forward<Sink>(sink));
578 }
579
580 // -- Batch generation -------------------------------------------------
581
582 /// @brief Generate a batch of address results from a seed drawn like
583 /// get_address() draws its own.
584 ///
585 /// Item `i` is `get_address(request, core::item_seed(batch_seed, options.first + i))`
586 /// for the drawn batch seed; see the seeded overload.
587 ///
588 /// @param request What every item is generated from.
589 /// @param options Count, first index, threads and uniqueness.
590 /// @return The results, in index order.
591 [[nodiscard]] std::vector<address> get_address_batch(const address_request& request,
592 const core::batch_options& options)
593 {
594 return get_address_batch(request, options, draw_seed_());
595 }
596
597 /// @brief Generate a batch of address results, deterministically.
598 ///
599 /// Item `i` is exactly `get_address(request, core::item_seed(batch_seed,
600 /// options.first + i))`, so an item replays on its own, a range can be
601 /// generated directly, two batches that follow each other equal one, and
602 /// the output does not depend on `options.threads`.
603 ///
604 /// @param request What every item is generated from.
605 /// @param options Count, first index, threads and uniqueness (see
606 /// core::unique_rule; the rule must be made for `address`).
607 /// @param batch_seed Seed of the whole batch.
608 /// @return The results, in index order.
609 /// @throws std::invalid_argument If the request or options are invalid.
610 /// @throws std::runtime_error If no data has been loaded, or a unique
611 /// batch runs out of attempts.
612 [[nodiscard]] std::vector<address> get_address_batch(const address_request& request,
613 const core::batch_options& options,
614 std::uint64_t batch_seed) const
615 {
616 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
617 { return get_address(request, item_seed); });
618 }
619
620 /// @brief Stream a batch of address results to a sink without holding the
621 /// whole batch.
622 ///
623 /// Generates the same items as get_address_batch() and calls
624 /// `sink(index, item)` in index order on the calling thread; `index`
625 /// includes `options.first`.
626 ///
627 /// @tparam Sink Callable taking `(std::uint64_t, address&)`; for example
628 /// core::jsonl_writer or core::csv_writer.
629 /// @param request What every item is generated from.
630 /// @param options Count, first index, threads and uniqueness.
631 /// @param batch_seed Seed of the whole batch.
632 /// @param sink Receives the items.
633 template <typename Sink>
634 void for_each_address(const address_request& request, const core::batch_options& options,
635 std::uint64_t batch_seed, Sink&& sink) const
636 {
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));
640 }
641
642 /// @brief Stream a batch of address results with a batch seed drawn like
643 /// get_address() draws its own.
644 /// @tparam Sink Callable taking `(std::uint64_t, address&)`.
645 /// @param request What every item is generated from.
646 /// @param options Count, first index, threads and uniqueness.
647 /// @param sink Receives the items.
648 template <typename Sink>
649 void for_each_address(const address_request& request, const core::batch_options& options,
650 Sink&& sink)
651 {
652 for_each_address(request, options, draw_seed_(), std::forward<Sink>(sink));
653 }
654
655 // -- Data management --------------------------------------------------
656
657 /// @brief Check whether any data has been loaded.
658 /// @return True when places are loaded.
659 [[nodiscard]] bool has_data() const { return !_model.places.empty(); }
660
661 /// @brief Return the number of loaded places.
662 /// @return The count.
663 [[nodiscard]] std::size_t place_count() const { return _model.places.size(); }
664
665 /// @brief Return the number of countries that have places.
666 /// @return The count.
667 [[nodiscard]] std::size_t country_count() const { return _model.codes.size(); }
668
669 /// @brief The countries that have places, sorted.
670 /// @return ISO codes.
671 [[nodiscard]] const std::vector<std::string>& countries() const { return _model.codes; }
672
673 /// @brief Whether a country has real streets for at least one place.
674 /// @param code ISO 3166-1 alpha-2.
675 /// @return True when street data is loaded for the country.
676 [[nodiscard]] bool has_streets(std::string_view code) const
677 {
678 const auto it = _model.countries.find(code);
679 return it != _model.countries.end() && it->second.has_streets;
680 }
681
682 /// @brief Load the street data of one country now, instead of on the first
683 /// call that needs it.
684 ///
685 /// Only the `full` tier keeps its streets in one file per country; for the
686 /// other data this does nothing. Safe to call from several threads.
687 ///
688 /// @param code ISO 3166-1 alpha-2.
689 /// @return True when the country has real streets.
690 /// @throws std::runtime_error If the country's file is missing or does not
691 /// match `streets.manifest.tsv`.
692 /// @throws core::tsv_error If the file is malformed.
693 bool load_streets(std::string_view code) const // NOLINT(modernize-use-nodiscard)
694 {
695 if (!_shard_cfg.active) return has_streets(code);
696 const shard* s = shard_of_(code);
697 return s != nullptr && !s->streets.empty();
698 }
699
700 /// @brief Whether a country's street data is in memory.
701 /// @param code ISO 3166-1 alpha-2.
702 /// @return True for the `lite` data (always loaded) and for a `full`
703 /// country once a call or load_streets() has read its file.
704 [[nodiscard]] bool streets_loaded(std::string_view code) const
705 {
706 if (!_shard_cfg.active) return has_streets(code);
707 const auto it = _shards.find(code);
708 return it != _shards.end() && it->second->loaded.load();
709 }
710
711 /// @brief Load data from a resource directory, replacing all data and
712 /// layers loaded before.
713 ///
714 /// The directory must contain `places.tsv`; the other tables are read
715 /// when present. A path without `places.tsv` is ignored.
716 ///
717 /// @param dir Directory such as `resources/lite`.
718 /// @throws core::tsv_error If a file is malformed; the data is then
719 /// unchanged.
720 void load(const std::filesystem::path& dir)
721 {
722 std::error_code ec;
723 if (!std::filesystem::is_directory(dir, ec) ||
724 !std::filesystem::is_regular_file(dir / file_name_(address_table::places), ec))
725 {
726 return;
727 }
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))
732 {
733 shards = make_shard_config_(core::tsv_document::load(manifest), dir, "");
734 }
735 apply_(std::move(docs), core::layer_mode::replace, true, std::move(shards));
736 }
737
738 /// @brief Load a dataset tier, replacing all data and layers loaded
739 /// before.
740 ///
741 /// Uses the compiled-in copy when the library is built with
742 /// `DASMIG_ADDRESS_EMBED`, otherwise searches `resources/`, `../resources`,
743 /// `address-generator/resources` and `../address-generator/resources`.
744 ///
745 /// @param tier The tier.
746 /// @return False when the tier was not found.
747 /// @throws core::tsv_error If a file is malformed.
748 [[nodiscard]] bool load(dataset tier)
749 {
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);
753 documents docs;
754 for (const auto t : all_tables_)
755 {
756 docs.at(index_(t)) = core::load_resource(tier_dir + "/" + std::string{file_name_(t)},
757 embedded_files_(), root);
758 }
759 if (!docs.at(index_(address_table::places))) return false;
760 std::optional<shard_config> shards;
761 if (!docs.at(index_(address_table::streets)))
762 {
763 if (auto manifest = core::load_resource(tier_dir + "/" + std::string{manifest_name_},
764 embedded_files_(), root))
765 {
766 shards = make_shard_config_(*manifest, root, tier_dir + "/");
767 }
768 }
769 apply_(std::move(docs), core::layer_mode::replace, true, std::move(shards));
770 return true;
771 }
772
773 /// @brief Add data layers from a directory or a file.
774 ///
775 /// A directory may hold any of the table files (see address_table); all
776 /// are applied together or, if any fails, none. A file must have one of
777 /// those names.
778 ///
779 /// @param path Directory or file.
780 /// @param mode How the layers combine with the data loaded before.
781 /// @throws core::tsv_error If a file cannot be read or a row is
782 /// invalid; the data is then unchanged.
783 /// @throws std::invalid_argument If @p path is a file with another name.
784 void extend(const std::filesystem::path& path, core::layer_mode mode = core::layer_mode::override)
785 {
786 std::error_code ec;
787 if (std::filesystem::is_directory(path, ec))
788 {
789 apply_(read_directory_(path), mode, false);
790 return;
791 }
792 for (const auto t : all_tables_)
793 {
794 if (path.filename() == file_name_(t))
795 {
796 (void)extend(t, core::tsv_document::load(path), mode);
797 return;
798 }
799 }
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());
802 }
803
804 /// @brief Add a data layer to one table.
805 ///
806 /// Rows are keyed by the table's key columns (see doc/data.md). In the
807 /// default override mode a row replaces the loaded row with the same key
808 /// and new keys are added; a row with `disabled` set to 1 removes its key.
809 ///
810 /// @param which The table.
811 /// @param doc TSV with that table's columns.
812 /// @param mode How the layer combines with the data loaded before.
813 /// @return What the layer did (rows added, replaced, removed, warnings).
814 /// @throws core::tsv_error If a row is invalid; the data is unchanged.
815 const core::layer_info& extend(address_table which, core::tsv_document doc,
816 core::layer_mode mode = core::layer_mode::override)
817 {
818 documents docs;
819 docs.at(index_(which)).emplace(std::move(doc));
820 apply_(std::move(docs), mode, false);
821 return _tables.at(index_(which)).layers().back();
822 }
823
824 /// @brief Add a data layer to one table from TSV text (for example
825 /// from a WebAssembly host).
826 /// @param which The table.
827 /// @param text TSV with that table's columns.
828 /// @param source Name used in errors.
829 /// @param mode How the layer combines with the data loaded before.
830 /// @return What the layer did.
831 /// @throws core::tsv_error If a row is invalid; the data is unchanged.
832 const core::layer_info& extend_from_string(address_table which, std::string_view text, std::string source,
833 core::layer_mode mode = core::layer_mode::override)
834 {
835 return extend(which, core::tsv_document::parse(text, std::move(source)), mode);
836 }
837
838 /// @brief The layers in effect for one table, in order.
839 /// @param which The table.
840 /// @return The layer records.
841 [[nodiscard]] const std::vector<core::layer_info>& layers(address_table which) const
842 {
843 return _tables.at(index_(which)).layers();
844 }
845
846 /// @brief Hash of the loaded data (all tables), recorded in each
847 /// result's signature().
848 /// @return The hash; 0 when nothing is loaded.
849 [[nodiscard]] std::uint64_t data_hash() const
850 {
851 std::uint64_t h = 0;
852 bool any = false;
853 for (const auto& t : _tables)
854 {
855 any = any || !t.layers().empty();
856 h = core::hash_combine(h, t.data_hash());
857 }
858 // The per-country street files are read on demand, so the manifest
859 // stands for them: a seed replays whether or not a country is loaded.
860 if (_shard_cfg.active) h = core::hash_combine(h, _shard_cfg.hash);
861 return any ? h : 0;
862 }
863
864 private:
865 using table = address_table;
866
867 // -- Tables -------------------------------------------------------------
868
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};
875
876 using documents = std::array<std::optional<core::tsv_document>, table_count_>;
877 using tables = std::array<core::layered_table, table_count_>;
878
879 static constexpr std::size_t index_(table t) { return static_cast<std::size_t>(t); }
880
881 static constexpr std::string_view file_name_(table t)
882 {
883 switch (t)
884 {
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";
891 }
892 return "";
893 }
894
895 static core::table_schema schema_(table t)
896 {
897 switch (t)
898 {
899 case table::places:
900 return {.key = {"geonameid"},
901 .required = {"country_code", "name", "population"},
902 .optional = {"region_code", "region"}};
903 case table::formats:
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"}};
908 case table::streets:
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 = {}};
918 }
919 return {};
920 }
921
922 static tables make_tables_()
923 {
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)}};
927 }
928
929 // -- Parsed rows --------------------------------------------------------
930
931 static constexpr std::size_t none_ = std::numeric_limits<std::size_t>::max();
932
933 struct place_data
934 {
935 std::uint32_t id{0};
936 std::string country;
937 std::string name;
938 std::string region_code;
939 std::string region;
940 std::uint64_t population{0};
941 };
942
943 enum class position : std::uint8_t { before, after, suffix };
944
945 struct format_data
946 {
947 std::vector<std::string> lines; // templates with {A} {C} {S} {Z} {D} {X}
948 std::vector<std::string> lines_latin; // the same in Latin script, empty when there is none
949 bool number_blocks{false}; // Japan: 3-12-5
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;
957 };
958
959 struct street_data
960 {
961 std::string name;
962 std::string latin; // the Latin name of a street named in another script, else empty
963 std::uint32_t min_number{0};
964 std::uint32_t max_number{0};
965 };
966
967 struct place_streets
968 {
969 std::vector<street_data> streets;
970 core::weighted_table<std::size_t> weights; // street index, by address count
971 };
972
973 // `full` keeps streets in one file per country, `streets/<CC>.tsv`, read on
974 // the first call that needs the country. Layers added with extend() are
975 // replayed over a country's file when it is read.
976 struct shard_entry
977 {
978 std::size_t rows{0};
979 std::uint64_t hash{0}; // tsv_document::content_hash() of the country's file
980 };
981
982 struct layer_doc
983 {
984 core::tsv_document doc;
985 core::layer_mode mode;
986 };
987
988 struct shard_config
989 {
990 bool active{false};
991 std::optional<std::filesystem::path> root; // resources directory; empty when compiled in
992 std::string prefix; // path below root, e.g. "full/"
993 std::map<std::string, shard_entry, std::less<>> entries;
994 std::uint64_t hash{0}; // of streets.manifest.tsv
995 std::vector<layer_doc> layers;
996 };
997
998 struct shard
999 {
1000 std::once_flag once;
1001 std::atomic<bool> loaded{false};
1002 std::unordered_map<std::uint32_t, place_streets> streets;
1003 };
1004
1005 using shard_map = std::map<std::string, std::unique_ptr<shard>, std::less<>>;
1006
1007 struct postcode_data
1008 {
1009 std::string code;
1010 bool partial{false};
1011 };
1012
1013 struct place_postcodes
1014 {
1015 std::vector<postcode_data> codes;
1016 core::weighted_table<std::size_t> weights;
1017 };
1018
1019 struct type_data
1020 {
1021 std::string type;
1022 position pos{position::before};
1023 };
1024
1025 struct country_data
1026 {
1027 std::vector<std::size_t> places; // indices into model.places
1028 core::weighted_table<std::size_t> place_weights; // place index, by population
1029 double population{0.0};
1030 format_data format;
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; // index into types
1035 std::vector<std::string> names;
1036 core::weighted_table<std::size_t> name_weights; // index into names
1037 };
1038
1039 struct model
1040 {
1041 std::vector<place_data> places; // in table row order
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; // lower-cased ASCII
1044 std::map<std::string, country_data, std::less<>> countries;
1045 std::vector<std::string> codes;
1046 core::weighted_table<std::string> country_weights; // by population
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;
1050 };
1051
1052 static void check_code_(const core::tsv_row& row)
1053 {
1054 if (!core::is_alpha2(row["country_code"]))
1055 {
1056 throw row.error("country_code", "expected two capital letters (ISO 3166-1 alpha-2)");
1057 }
1058 }
1059
1060 static std::uint32_t parse_id_(const core::tsv_row& row)
1061 {
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()))
1064 {
1065 throw row.error("geonameid", "must be a positive 32-bit id");
1066 }
1067 return static_cast<std::uint32_t>(id);
1068 }
1069
1070 static double parse_weight_(const core::tsv_row& row)
1071 {
1072 const auto w = row.get<double>("weight");
1073 if (!(w >= 0.0)) throw row.error("weight", "must not be negative");
1074 return w;
1075 }
1076
1077 static place_data parse_place_(const core::tsv_row& row)
1078 {
1079 check_code_(row);
1080 place_data p;
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);
1090 return p;
1091 }
1092
1093 static std::vector<std::string> split_(std::string_view text, char sep)
1094 {
1095 std::vector<std::string> out;
1096 std::size_t start = 0;
1097 while (start <= text.size())
1098 {
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;
1103 start = end + 1;
1104 }
1105 return out;
1106 }
1107
1108 static std::pair<std::string, format_data> parse_format_(const core::tsv_row& row)
1109 {
1110 check_code_(row);
1111 format_data f;
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")
1117 {
1118 throw row.error("number_style", "expected plain or blocks");
1119 }
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)};
1133 }
1134
1135 struct street_row
1136 {
1137 std::uint32_t id{0};
1138 std::string country;
1139 street_data street;
1140 double weight{0.0};
1141 };
1142
1143 static shard_config make_shard_config_(const core::tsv_document& manifest,
1144 std::optional<std::filesystem::path> root, std::string prefix)
1145 {
1146 shard_config c;
1147 c.active = true;
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())
1152 {
1153 check_code_(row);
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)
1160 {
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;
1164 }
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};
1167 }
1168 return c;
1169 }
1170
1171 static street_row parse_street_(const core::tsv_row& row)
1172 {
1173 check_code_(row);
1174 street_row s;
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);
1186 return s;
1187 }
1188
1189 struct postcode_row
1190 {
1191 std::uint32_t id{0};
1192 postcode_data code;
1193 double weight{0.0};
1194 };
1195
1196 static postcode_row parse_postcode_(const core::tsv_row& row)
1197 {
1198 check_code_(row);
1199 postcode_row p;
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);
1205 return p;
1206 }
1207
1208 static std::pair<type_data, double> parse_type_(const core::tsv_row& row, std::string& country)
1209 {
1210 if (row["country_code"] != "*") check_code_(row);
1211 country = row.get<std::string>("country_code");
1212 type_data t;
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)};
1221 }
1222
1223 static void validate_(table t, const core::tsv_row& row)
1224 {
1225 std::string scratch;
1226 switch (t)
1227 {
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);
1237 break;
1238 }
1239 }
1240
1241 // -- State ----------------------------------------------------------------
1242
1243 tables _tables;
1244 model _model;
1245 shard_config _shard_cfg;
1246 shard_map _shards;
1247 core::rng _engine{core::rng::from_entropy()};
1248
1249 struct auto_probe_tag {};
1250
1251 explicit adg(auto_probe_tag /*tag*/) : _tables{make_tables_()}
1252 {
1253 if (!load(dataset::lite)) (void)load(dataset::full);
1254 }
1255
1256 // -- Helpers ----------------------------------------------------------
1257
1258 static std::span<const core::embedded_file> embedded_files_()
1259 {
1260#ifdef DASMIG_ADDRESS_EMBED
1261 return embedded::addressgen::files;
1262#else
1263 return {};
1264#endif
1265 }
1266
1267 std::uint64_t draw_seed_() { return _engine(); }
1268
1269 static documents read_directory_(const std::filesystem::path& dir)
1270 {
1271 documents docs;
1272 for (const auto t : all_tables_)
1273 {
1274 const auto file = dir / file_name_(t);
1275 std::error_code ec;
1276 if (std::filesystem::is_regular_file(file, ec))
1277 {
1278 docs.at(index_(t)) = core::tsv_document::load(file);
1279 }
1280 }
1281 return docs;
1282 }
1283
1284 // Apply layers to copies of the tables, then commit: all or nothing.
1285 // With @p reset (load), tables without a document are emptied.
1286 void apply_(documents docs, core::layer_mode mode, bool reset, std::optional<shard_config> shards = {})
1287 {
1288 // A load discards the old rows, so there is nothing to copy.
1289 tables next = reset ? make_tables_() : _tables;
1290 shard_config cfg = reset ? shard_config{} : _shard_cfg;
1291 if (reset && shards) cfg = std::move(*shards);
1292 // Street layers added while the streets are per-country files are kept
1293 // as documents, to be replayed over each file when it is read.
1294 if (!reset && cfg.active)
1295 {
1296 if (const auto& d = docs.at(index_(table::streets))) cfg.layers.push_back({.doc = *d, .mode = mode});
1297 }
1298 for (const auto t : all_tables_)
1299 {
1300 auto& doc = docs.at(index_(t));
1301 auto& tbl = next.at(index_(t));
1302 if (!doc)
1303 {
1304 if (reset) tbl.clear();
1305 continue;
1306 }
1307 // The rows build_model_() parses are checked there, before anything commits.
1308 if (t == table::street_types || t == table::street_names)
1309 {
1310 (void)tbl.extend(std::move(*doc), mode, [t](const core::tsv_row& row) { validate_(t, row); });
1311 }
1312 else
1313 {
1314 (void)tbl.extend(std::move(*doc), mode);
1315 }
1316 }
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);
1323 }
1324
1325 static std::string lower_(std::string_view s)
1326 {
1327 std::string out{s};
1328 for (auto& c : out)
1329 {
1330 if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a');
1331 }
1332 return out;
1333 }
1334
1335 static shard_map make_shards_(const tables& tbls, const shard_config& cfg)
1336 {
1337 shard_map out;
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())
1341 {
1342 out.emplace(std::string{row["country_code"]}, std::make_unique<shard>());
1343 }
1344 return out;
1345 }
1346
1347 static model build_model_(const tables& tbls, const shard_config& cfg)
1348 {
1349 model m;
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())
1357 {
1358 m.places.push_back(parse_place_(row));
1359 }
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)
1362 {
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);
1367 }
1368 for (auto& [code, idx] : by_country)
1369 {
1370 auto& c = m.countries[code];
1371 c.places = std::move(idx);
1372 for (const auto i : c.places)
1373 {
1374 const double w = static_cast<double>(std::max<std::uint64_t>(m.places[i].population, 1));
1375 c.place_weights.add(i, w);
1376 c.population += w;
1377 }
1378 m.codes.push_back(code);
1379 m.country_weights.add(code, c.population);
1380 }
1381 std::ranges::sort(m.codes);
1382
1383 for (const auto& row : tbls.at(index_(table::formats)).rows())
1384 {
1385 auto [code, f] = parse_format_(row);
1386 auto& c = m.countries[code];
1387 c.format = std::move(f);
1388 c.has_format = true;
1389 }
1390 m.generic_format.lines = {"{A}", "{Z} {C}"};
1391
1392 for (const auto& row : tbls.at(index_(table::streets)).rows())
1393 {
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));
1397 }
1398 if (cfg.active && !std::ranges::any_of(cfg.layers, [](const layer_doc& l)
1399 { return l.mode == core::layer_mode::replace; }))
1400 {
1401 for (const auto& [code, entry] : cfg.entries)
1402 {
1403 if (entry.rows > 0) m.countries[code].has_streets = true;
1404 }
1405 }
1406 for (const auto& row : tbls.at(index_(table::postcodes)).rows())
1407 {
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));
1412 }
1413
1414 // Composed streets: a country's own types and names, else the generic ones.
1415 m.countries.try_emplace("*");
1416 fill_composed_(tbls, m.countries);
1417 return m;
1418 }
1419
1420 static void add_street_(std::unordered_map<std::uint32_t, place_streets>& into, street_row s)
1421 {
1422 auto& ps = into[s.id];
1423 ps.weights.add(ps.streets.size(), s.weight);
1424 ps.streets.push_back(std::move(s.street));
1425 }
1426
1427 // Read one country's street file (once, under the shard's flag).
1428 void fill_shard_(const std::string& country, shard& s) const
1429 {
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)
1432 {
1433 const auto path = std::format("{}streets/{}.tsv", _shard_cfg.prefix, country);
1434 base = core::load_resource(path, embedded_files_(), _shard_cfg.root);
1435 if (!base)
1436 {
1437 throw std::runtime_error("Street data for " + country + " is listed in " +
1438 std::string{manifest_name_} + " but " + path + " was not found");
1439 }
1440 if (base->content_hash() != e->second.hash || base->size() != e->second.rows)
1441 {
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));
1445 }
1446 }
1447 if (_shard_cfg.layers.empty())
1448 {
1449 if (base)
1450 {
1451 for (const auto& row : base->rows()) add_street_(s.streets, parse_street_(row));
1452 }
1453 }
1454 else
1455 {
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)
1459 {
1460 (void)t.extend(l.doc, l.mode, [](const core::tsv_row& row) { validate_(table::streets, row); });
1461 }
1462 for (const auto& row : t.rows())
1463 {
1464 if (row["country_code"] == country) add_street_(s.streets, parse_street_(row));
1465 }
1466 }
1467 s.loaded.store(true);
1468 }
1469
1470 // The country's street shard, read if it was not yet; null when the
1471 // country has no street data.
1472 [[nodiscard]] const shard* shard_of_(std::string_view country) const
1473 {
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); });
1478 return &s;
1479 }
1480
1481 // One pass over each table, handing every row to its country; rows of a
1482 // country no place or format mentions are not kept.
1483 static void fill_composed_(const tables& tbls, std::map<std::string, country_data, std::less<>>& countries)
1484 {
1485 for (const auto& row : tbls.at(index_(table::street_types)).rows())
1486 {
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));
1494 }
1495 for (const auto& row : tbls.at(index_(table::street_names)).rows())
1496 {
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"));
1502 }
1503 }
1504
1505 void require_data_() const
1506 {
1507 if (_model.places.empty())
1508 {
1509 throw std::runtime_error("No address data loaded. Call load() first.");
1510 }
1511 }
1512
1513 struct resolved
1514 {
1515 std::size_t index{none_}; // into model.places, none_ for a place given by name only
1516 std::uint32_t id{0};
1517 std::string country;
1518 std::string city;
1519 std::string region_code;
1520 std::string region;
1521 std::uint64_t population{0};
1522 const country_data* data{nullptr}; // the country's record, null when the model has none
1523 };
1524
1525 [[nodiscard]] const country_data* country_of_(const std::string& code) const
1526 {
1527 const auto it = _model.countries.find(code);
1528 return it != _model.countries.end() ? &it->second : nullptr;
1529 }
1530
1531 [[nodiscard]] resolved from_place_(std::size_t index) const
1532 {
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)};
1537 }
1538
1539 // The place an address is in: the request's, else one drawn from its
1540 // country (itself drawn unless the request gives it).
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
1544 {
1545 if (want_place)
1546 {
1547 if (const auto index = find_place_(want_country, *want_place); index != none_) return from_place_(index);
1548 if (!want_country)
1549 {
1550 throw std::invalid_argument("place '" + want_place->city +
1551 "' is not in the data; give a country to use it as given");
1552 }
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)};
1556 }
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())
1561 {
1562 throw std::invalid_argument("no places for country '" + country + "'");
1563 }
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)]);
1568 }
1569
1570 [[nodiscard]] const format_data& format_of_(const resolved& r) const
1571 {
1572 return r.data != nullptr && r.data->has_format ? r.data->format : _model.generic_format;
1573 }
1574
1575 struct street_pick
1576 {
1577 std::string name;
1578 std::string latin;
1579 std::uint32_t lo{0};
1580 std::uint32_t hi{0};
1581 bool real{false};
1582 };
1583
1584 // A street of the place, from its own data, else composed.
1585 [[nodiscard]] street_pick pick_street_(const resolved& r, bool weighted, core::rng& sr) const
1586 {
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; // all switched off
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};
1593 }
1594
1595 [[nodiscard]] std::string pick_postcode_(const resolved& r, const format_data& fmt, core::rng& zr) const
1596 {
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);
1600 }
1601
1602 [[nodiscard]] std::string pick_country_(core::rng& r, bool weighted) const
1603 {
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)];
1606 }
1607
1608 [[nodiscard]] std::size_t find_place_(const std::optional<std::string>& want_country, const place_ref& ref) const
1609 {
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)
1614 {
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);
1624 if (better)
1625 {
1626 best = it->second;
1627 best_region = region_ok;
1628 }
1629 }
1630 return best;
1631 }
1632
1633 [[nodiscard]] const place_streets* streets_of_(std::string_view country, std::uint32_t id) const
1634 {
1635 if (_shard_cfg.active)
1636 {
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;
1641 }
1642 const auto it = _model.streets.find(id);
1643 return it == _model.streets.end() ? nullptr : &it->second;
1644 }
1645
1646 [[nodiscard]] const place_postcodes* postcodes_of_(std::uint32_t id) const
1647 {
1648 const auto it = _model.postcodes.find(id);
1649 return it == _model.postcodes.end() ? nullptr : &it->second;
1650 }
1651
1652 static std::string region_text_(const format_data& f, const std::string& code, const std::string& name)
1653 {
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;
1657 }
1658
1659 [[nodiscard]] std::string composed_street_(const resolved& place, core::rng& r) const
1660 {
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())
1664 {
1665 throw std::runtime_error("No street data and no composed-street tables loaded for country " + place.country);
1666 }
1667 const auto& t = c->types[c->type_weights.pick_index(r)];
1668 const auto& n = c->names[c->name_weights.pick_index(r)];
1669 switch (t.pos)
1670 {
1671 case position::before: return t.type + " " + n;
1672 case position::after: return n + " " + t.type;
1673 case position::suffix: return n + t.type;
1674 }
1675 return n;
1676 }
1677
1678 // Skewed towards low numbers, like real streets; products feed no addition.
1679 static std::uint32_t house_number_(core::rng& r, std::uint32_t lo, std::uint32_t hi)
1680 {
1681 const double u = core::uniform01(r);
1682 if (hi == 0)
1683 {
1684 const double x = u * u;
1685 return 1U + static_cast<std::uint32_t>(120.0 * x);
1686 }
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));
1690 }
1691
1692 static double unit_chance_(const address_request& request, const format_data& f, std::uint64_t population)
1693 {
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;
1697 }
1698
1699 static std::string unit_text_(const std::string& style, core::rng& r)
1700 {
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")
1707 {
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);
1711 }
1712 return "Apt " + std::to_string(n);
1713 }
1714
1715 // '#' a digit, 'N' a digit 1-9, 'A' a capital letter, 'L' a letter that is
1716 // accepted wherever a letter is restricted, 'U' a UK second-half letter,
1717 // '_' a space (a table cell does not keep a leading space).
1718 static std::string expand_(std::string_view pattern, core::rng& r)
1719 {
1720 static constexpr std::string_view safe = "ABCEGHJKLMNPRTVXY";
1721 static constexpr std::string_view uk = "ABDEFGHJLNPQRSTUWXYZ";
1722 std::string out;
1723 for (const char c : pattern)
1724 {
1725 switch (c)
1726 {
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;
1734 }
1735 }
1736 return out;
1737 }
1738
1739 static std::string postcode_(const format_data& f, const place_postcodes* pc, core::rng& r)
1740 {
1741 if (pc != nullptr && !pc->codes.empty())
1742 {
1743 const auto& c = pc->codes[pc->weights.pick_index(r)];
1744 return c.partial ? c.code + expand_(f.zip_completion, r) : c.code;
1745 }
1746 if (!f.zip_pattern.empty()) return expand_(f.zip_pattern, r);
1747 if (!f.zip_examples.empty())
1748 {
1749 return f.zip_examples[core::uniform_int<std::size_t>(r, 0, f.zip_examples.size() - 1)];
1750 }
1751 return "";
1752 }
1753
1754 static void replace_all_(std::string& s, std::string_view from, std::string_view to)
1755 {
1756 for (std::size_t pos = s.find(from); pos != std::string::npos; pos = s.find(from, pos + to.size()))
1757 {
1758 s.replace(pos, from.size(), to);
1759 }
1760 }
1761
1762 static std::string tidy_(std::string s)
1763 {
1764 // Collapse the gaps left by empty parts.
1765 using fix = std::pair<std::string_view, std::string_view>;
1766 for (const auto& [bad, good] : {fix{" ", " "}, fix{" ,", ","}, fix{", ,", ","}, fix{",,", ","}})
1767 {
1768 while (s.contains(bad)) replace_all_(s, bad, good);
1769 }
1770 const auto is_edge = [](char c) { return c == ' ' || c == ',' || c == '-'; };
1771 std::size_t b = 0;
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);
1776 }
1777
1778 static std::vector<std::string> layout_(const format_data& f, const address& v, address_script script)
1779 {
1780 std::string street = v.street;
1781 if (v.number != 0)
1782 {
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;
1785 }
1786 if (!v.unit.empty()) street += ", " + v.unit;
1787
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)
1792 {
1793 // A region with the city's own name (Tokyo, Singapore) is not repeated on its line.
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};
1796 std::string line;
1797 line.reserve(tmpl.size() + street.size() + v.city.size() + v.postcode.size());
1798 bool any = false;
1799 for (std::size_t i = 0; i < tmpl.size(); ++i)
1800 {
1801 std::string_view value;
1802 if (tmpl[i] == '{' && i + 2 < tmpl.size() && tmpl[i + 2] == '}')
1803 {
1804 switch (tmpl[i + 1])
1805 {
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;
1810 case 'D':
1811 case 'X': break;
1812 default: line += tmpl[i]; continue;
1813 }
1814 any = any || !value.empty();
1815 line += value;
1816 i += 2;
1817 continue;
1818 }
1819 line += tmpl[i];
1820 }
1821 if (!any) continue;
1822 line = tidy_(std::move(line));
1823 if (!line.empty()) out.push_back(std::move(line));
1824 }
1825 return out;
1826 }
1827};
1828
1829} // namespace dasmig
1830
1831#endif // DASMIG_ADDRESSGEN_HPP
address_script
The script an address is written in.
@ latin
The Latin-script layout, for the countries that have one (lines_latin); others keep their own.
@ local
The country's own layout (the default).
address_table
The tables of the address data, for extending it.
@ street_types
street_types.tsv: types for composed streets.
@ formats
formats.tsv: each country's layout and postcode format.
@ places
places.tsv: the places addresses are drawn in.
@ streets
streets.tsv: real streets by place, weighted by address count.
@ street_names
street_names.tsv: names for composed streets.
@ postcodes
postcodes.tsv: postcodes by place.
Return type for address generation.
std::string country_code
ISO 3166-1 alpha-2.
bool real_street
True when the street comes from the place's own data, false when composed.
friend std::ostream & operator<<(std::ostream &os, const address &v)
Stream the summary to an output stream.
std::uint64_t seed() const
Retrieve the random seed used to generate this address.
std::string to_json() const
This address as a JSON document.
std::string street
The street's name, with its type.
std::string postcode
In the country's format; empty where there are no postcodes.
std::string formatted(std::string_view separator=", ") const
The address on one line.
std::uint32_t number
House number; 0 when the street has none.
void to_json(core::json_writer &w) const
Write this address as a JSON object.
std::string city
The place's name.
std::string region
The region as the country writes it (an abbreviation in the US).
const std::vector< std::string > & lines() const
The address lines in the country's order.
std::string unit
Apartment or flat, empty when there is none.
const core::seed_signature & signature() const
The seed and the data this address was generated from.
std::uint32_t geonameid
The place's GeoNames id (city-generator's); 0 for a place given by name only.
std::string to_string() const
Human-readable summary string.
Address Generator: postal addresses with real streets by place.
std::size_t place_count() const
Return the number of loaded places.
std::uint64_t data_hash() const
Hash of the loaded data (all tables), recorded in each result's signature().
std::size_t country_count() const
Return the number of countries that have places.
std::string get_postcode(const postcode_request &request, std::uint64_t call_seed) const
Generate a deterministic postcode.
address get_address(const address_request &request)
Generate a random address. Use address_request{} for the defaults.
const std::vector< std::string > & countries() const
The countries that have places, sorted.
std::vector< street > get_street_batch(const street_request &request, const core::batch_options &options, std::uint64_t batch_seed) const
Generate a batch of streets, deterministically.
std::vector< address > get_address_batch(const address_request &request, const core::batch_options &options, std::uint64_t batch_seed) const
Generate a batch of address results, deterministically.
void for_each_postcode(const postcode_request &request, const core::batch_options &options, std::uint64_t batch_seed, Sink &&sink) const
Stream a batch of postcodes to a sink without holding the whole batch.
void for_each_street(const street_request &request, const core::batch_options &options, Sink &&sink)
Stream a batch of streets with a batch seed drawn like get_street() draws its own.
street get_street(const street_request &request)
Generate a random street. Use street_request{} for the defaults.
static adg & instance()
Access the global singleton instance.
std::vector< std::string > get_postcode_batch(const postcode_request &request, const core::batch_options &options, std::uint64_t batch_seed) const
Generate a batch of postcodes, deterministically.
adg & operator=(const adg &)=delete
Not copyable.
bool has_streets(std::string_view code) const
Whether a country has real streets for at least one place.
std::vector< std::string > get_postcode_batch(const postcode_request &request, const core::batch_options &options)
Generate a batch of postcodes from a seed drawn like get_postcode() draws its own.
std::vector< address > get_address_batch(const address_request &request, const core::batch_options &options)
Generate a batch of address results from a seed drawn like get_address() draws its own.
adg(adg &&) noexcept=default
Movable.
const core::layer_info & extend_from_string(address_table which, std::string_view text, std::string source, core::layer_mode mode=core::layer_mode::override)
Add a data layer to one table from TSV text (for example from a WebAssembly host).
const std::vector< core::layer_info > & layers(address_table which) const
The layers in effect for one table, in order.
bool load_streets(std::string_view code) const
Load the street data of one country now, instead of on the first call that needs it.
bool streets_loaded(std::string_view code) const
Whether a country's street data is in memory.
address get_address(const address_request &request, std::uint64_t call_seed) const
Generate a deterministic address.
void extend(const std::filesystem::path &path, core::layer_mode mode=core::layer_mode::override)
Add data layers from a directory or a file.
bool load(dataset tier)
Load a dataset tier, replacing all data and layers loaded before.
street get_street(const street_request &request, std::uint64_t call_seed) const
Generate a deterministic street.
void for_each_street(const street_request &request, const core::batch_options &options, std::uint64_t batch_seed, Sink &&sink) const
Stream a batch of streets to a sink without holding the whole batch.
std::string get_postcode(const postcode_request &request)
Generate a random postcode in the country's format. Use postcode_request{} for the defaults.
void load(const std::filesystem::path &dir)
Load data from a resource directory, replacing all data and layers loaded before.
bool has_data() const
Check whether any data has been loaded.
void for_each_postcode(const postcode_request &request, const core::batch_options &options, Sink &&sink)
Stream a batch of postcodes with a batch seed drawn like get_postcode() draws its own.
const core::layer_info & extend(address_table which, core::tsv_document doc, core::layer_mode mode=core::layer_mode::override)
Add a data layer to one table.
std::vector< street > get_street_batch(const street_request &request, const core::batch_options &options)
Generate a batch of streets from a seed drawn like get_street() draws its own.
void for_each_address(const address_request &request, const core::batch_options &options, Sink &&sink)
Stream a batch of address results with a batch seed drawn like get_address() draws its own.
adg()
Default constructor — creates an empty generator with no data.
adg(const adg &)=delete
Not copyable.
void for_each_address(const address_request &request, const core::batch_options &options, std::uint64_t batch_seed, Sink &&sink) const
Stream a batch of address results to a sink without holding the whole batch.
Return type for street generation: a street of a place.
std::string to_json() const
This street as a JSON document.
const core::seed_signature & signature() const
The seed and the data this street was generated from.
friend std::ostream & operator<<(std::ostream &os, const street &v)
Stream the name to an output stream.
std::string name
The street's name, with its type.
void to_json(core::json_writer &w) const
Write this street as a JSON object.
std::string country_code
ISO 3166-1 alpha-2.
std::string city
The place the street is in.
std::uint64_t seed() const
Retrieve the random seed used to generate this street.
bool real_street
True when it comes from the place's own data, false when composed.
std::uint32_t geonameid
The place's GeoNames id; 0 for a place given by name only.
What an address call asks for.
std::optional< double > unit_chance
Chance of a unit (apartment, flat) in the address; the country's default when absent....
bool weighted
Weighted selection (default): countries and places by population, streets by address count....
std::optional< std::string > country
ISO-3166 two-letter country code, or a country drawn when absent.
std::optional< place_ref > place
Anchor the address to this place instead of drawing one.
address_script script
The script of the layout and of the street name. With latin, a street named in another script is give...
A place a request can anchor an address to, by plain values.
std::string region
Optional region name or code, to tell places of one name apart.
std::string city
The place's name (any case); matched against the loaded places.
What a postcode call asks for.
std::optional< place_ref > place
Anchor to this place instead of drawing one.
std::optional< std::string > country
ISO 3166-1 alpha-2, or drawn when absent.
bool weighted
Postcodes by their weight; false is uniform.
What a street call asks for: the street an address in that place would have. Same fields as address_r...
std::optional< place_ref > place
Anchor to this place instead of drawing one.
address_script script
latin gives the Latin name where the data has one.
std::optional< std::string > country
ISO 3166-1 alpha-2, or drawn when absent.
bool weighted
Streets by address count; false is uniform.