Add reverse geocoding for brewery street addresses (#245)

* Add geocoding to seed application, remove postal codes from generator

Update comments
This commit is contained in:
2026-08-23 23:53:56 -04:00
committed by Aaron Po
parent e738fe484a
commit cd5fd2d998
41 changed files with 2966 additions and 2297 deletions

View File

@@ -38,9 +38,8 @@ std::optional<ApplicationOptions> ParseArguments(
"Number of layers to offload to GPU");
};
// --mocked, --model, and --openai are mutually exclusive; validation is
// enforced below rather than at registration to produce a clear
// diagnostic message.
// --mocked, --model, and --openai are mutually exclusive, validated below
// to produce a clear diagnostic message.
auto add_generator_options = [&]() -> void {
opt("mocked", prog_opts::bool_switch(),
"Use mocked generator for brewery/user data");
@@ -154,9 +153,7 @@ std::optional<ApplicationOptions> ParseArguments(
return std::nullopt;
}
// Prompt directory is only meaningful for live inference (Llama or
// OpenAI) — the mock generator has no use for it and should not
// require it to be present.
// Prompt directory is required only for live inference (Llama or OpenAI).
if (!use_mocked && options.pipeline.prompt_dir.empty()) {
const std::string msg =
"Invalid arguments: --prompt-dir is required when not using "

View File

@@ -13,12 +13,10 @@ BiergartenPipelineOrchestrator::BiergartenPipelineOrchestrator(
std::unique_ptr<DataGenerator> generator,
std::unique_ptr<IExportService> exporter,
std::unique_ptr<ICuratedDataService> curated_data_service,
std::unique_ptr<IPostalCodeService> postal_code_service,
const ApplicationOptions& application_options)
: logger_(std::move(logger)),
context_service_(std::move(context_service)),
generator_(std::move(generator)),
exporter_(std::move(exporter)),
curated_data_service_(std::move(curated_data_service)),
postal_code_service_(std::move(postal_code_service)),
application_options_(application_options) {}

View File

@@ -4,54 +4,16 @@
*/
#include <chrono>
#include <cmath>
#include <format>
#include <numbers>
#include <optional>
#include <random>
#include "biergarten_pipeline_orchestrator.h"
#include "biergarten_pipeline_orchestrator/coords.h"
#include "services/logging/logger.h"
struct Coords {
double longitude;
double latitude;
};
// @todo find a better home for this
namespace {
constexpr double kEarthRadiusKm = 6371.0;
double DegreesToRadians(double degrees) {
return degrees * std::numbers::pi / 180.0;
}
double RadiansToDegrees(double radians) {
return radians * 180.0 / std::numbers::pi;
}
} // namespace
static Coords GetRandomCoordsWithinRange(const Coords& centre,
const int distance_km,
std::mt19937& rng) {
std::uniform_real_distribution<double> angle_dist(0.0,
2.0 * std::numbers::pi);
std::uniform_real_distribution<double> unit_dist(0.0, 1.0);
const double angle_radians = angle_dist(rng);
// sqrt() keeps points uniformly distributed over the disc's area rather
// than clustering near the centre.
const double radius_km = distance_km * std::sqrt(unit_dist(rng));
const double delta_latitude_degrees =
RadiansToDegrees(radius_km * std::cos(angle_radians) / kEarthRadiusKm);
const double delta_longitude_degrees = RadiansToDegrees(
radius_km * std::sin(angle_radians) /
(kEarthRadiusKm * std::cos(DegreesToRadians(centre.latitude))));
return Coords{.longitude = centre.longitude + delta_longitude_degrees,
.latitude = centre.latitude + delta_latitude_degrees};
}
using biergarten_pipeline_orchestrator_internal::Coords;
using biergarten_pipeline_orchestrator_internal::GetRandomCoordsWithinRange;
void BiergartenPipelineOrchestrator::GenerateBreweries(
std::span<const EnrichedCity> cities) {
@@ -70,8 +32,6 @@ void BiergartenPipelineOrchestrator::GenerateBreweries(
try {
const BreweryResult brewery =
generator_->GenerateBrewery(enriched_city);
const std::string postal_code =
postal_code_service_->GeneratePostalCode(enriched_city.location);
constexpr int kMaxDistanceFromCentreKm = 5;
const Coords city_centre{.longitude = enriched_city.location.longitude,
@@ -81,7 +41,6 @@ void BiergartenPipelineOrchestrator::GenerateBreweries(
return BreweryRecord{
.address = BreweryAddress{.city = enriched_city.location,
.postal_code = postal_code,
.longitude = brewery_coords.longitude,
.latitude = brewery_coords.latitude},
.brewery = brewery};

View File

@@ -15,9 +15,13 @@
#include <unordered_set>
#include "biergarten_pipeline_orchestrator.h"
#include "biergarten_pipeline_orchestrator/coords.h"
#include "services/curated_data/curated_json_data_service.h"
#include "services/logging/logger.h"
using biergarten_pipeline_orchestrator_internal::Coords;
using biergarten_pipeline_orchestrator_internal::GetRandomCoordsWithinRange;
namespace {
std::string Sanitize(std::string_view value) {
std::string out;
@@ -135,12 +139,17 @@ void BiergartenPipelineOrchestrator::GenerateUsers(
try {
const UserResult user =
generator_->GenerateUser(city, persona, sampled_name);
const std::string postal_code =
postal_code_service_->GeneratePostalCode(city.location);
constexpr int kMaxDistanceFromCentreKm = 5;
const Coords city_centre{.longitude = city.location.longitude,
.latitude = city.location.latitude};
const Coords user_coords = GetRandomCoordsWithinRange(
city_centre, kMaxDistanceFromCentreKm, rng);
return UserRecord{
.address =
UserAddress{.city = city.location, .postal_code = postal_code},
.address = UserAddress{.city = city.location,
.longitude = user_coords.longitude,
.latitude = user_coords.latitude},
.user = user,
.email = BuildEmail(sampled_name, used_email_local_parts),
.date_of_birth = GenerateDateOfBirth(rng),

View File

@@ -26,7 +26,6 @@ void BiergartenPipelineOrchestrator::LogResults() const {
{"country", city.country},
{"state_province", city.state_province},
{"iso3166_2", city.iso3166_2},
{"postal_code", address.postal_code},
{"longitude", address.longitude},
{"latitude", address.latitude},
}}});

View File

@@ -58,9 +58,7 @@ std::string CondenseWhitespace(std::string_view text) {
return out;
}
// Guard against truncating in the first half of the string.
// This preserves the critical opening content and avoids cutting critical
// context words early in the region description.
// Keeps truncation past the string's midpoint, preserving the critical opening content.
constexpr size_t kTruncationGuardDivisor = 2;
bool ReadRequiredTrimmedStringField(const boost::json::object& obj,

View File

@@ -36,10 +36,7 @@ std::string FormatLocalLanguageCodes(
constexpr int kBreweryMaxTokens = 2800;
// Structured outputs already guarantee schema-valid JSON, so retries here
// are only for transient failures or the model returning placeholder text
// -- not for malformed JSON shape, unlike LlamaGenerator's grammar-assisted
// retry loop.
// Structured outputs already guarantee schema-valid JSON, so retries here only cover transient failures or the model returning placeholder text.
constexpr int kMaxAttempts = 2;
} // namespace
@@ -54,9 +51,7 @@ BreweryResult OpenAIGenerator::GenerateBrewery(
FormatLocalLanguageCodes(location.local_languages);
/**
* Load brewery system prompt via the injected prompt directory. The
* same file LlamaGenerator uses -- the prompt content is backend
* agnostic.
* Loads the backend-agnostic brewery system prompt via the injected prompt directory.
*/
const std::string system_prompt =
prompt_directory_->Load("BREWERY_GENERATION");

View File

@@ -16,8 +16,7 @@
namespace {
constexpr int kUserMaxTokens = 1200;
// Structured outputs already guarantee schema-valid JSON, so retries here
// are only for transient failures or placeholder text -- not JSON shape.
// Structured outputs already guarantee schema-valid JSON, so retries here only cover transient failures or placeholder text.
constexpr int kMaxAttempts = 2;
} // namespace

View File

@@ -87,36 +87,16 @@ int main(const int argc, char** argv) {
di::bind<ApplicationOptions>().to(options),
di::bind<std::string>().to(model_path),
di::bind<IExportService>().to<SqliteExportService>(),
di::bind<IPostalCodeService>().to(
[options,
&log_producer]() -> std::unique_ptr<IPostalCodeService> {
if (options.generator.mode == GeneratorMode::kMock) {
log_producer->Log(
{.level = LogLevel::Info,
.phase = PipelinePhase::Startup,
.message = "Postal code: mock (curated examples)"});
return std::make_unique<MockPostalCodeService>();
}
log_producer->Log(
{.level = LogLevel::Info,
.phase = PipelinePhase::Startup,
.message =
"Postal code: Xeger (regex-based generation)"});
return std::make_unique<XegerPostalCodeService>();
}),
di::bind<ICuratedDataService>().to(
[options,
&log_producer]() -> std::unique_ptr<ICuratedDataService> {
// if (options.generator.mode == GeneratorMode::kMock) {
// log_producer->Log({.level = LogLevel::Info,
// .phase = PipelinePhase::Startup,
// .message = "Curated data: mock"});
//
// return std::make_unique<MockCuratedDataService>();
// }
if (options.generator.mode == GeneratorMode::kMock) {
log_producer->Log({.level = LogLevel::Info,
.phase = PipelinePhase::Startup,
.message = "Curated data: mock"});
return std::make_unique<MockCuratedDataService>();
}
log_producer->Log({.level = LogLevel::Info,
.phase = PipelinePhase::Startup,
@@ -214,9 +194,7 @@ int main(const int argc, char** argv) {
}
#ifdef BIERGARTEN_MOCK_ONLY
// Only reachable when mode == Llama: Mock and OpenAI are
// handled above and don't need llama.cpp, but LlamaGenerator
// itself is not linked into a BIERGARTEN_MOCK_ONLY build.
// Only reachable when mode == Llama, which requires llama.cpp and is excluded from a BIERGARTEN_MOCK_ONLY build.
throw std::runtime_error(
"LlamaGenerator is unavailable in a BIERGARTEN_MOCK_ONLY "
"build");

View File

@@ -77,12 +77,11 @@ static PostalCodeSpec ReadPostalCodeSpec(const boost::json::object& object) {
}
namespace {
boost::json::value ParseJsonFile(const std::filesystem::path& filepath,
const char* what) {
boost::json::value ParseJsonFile(const std::filesystem::path& filepath) {
std::ifstream input(filepath);
if (!input.is_open()) {
throw std::runtime_error(
std::format("Failed to open {} file: {}", what, filepath.string()));
std::format("Failed to open file: {}", filepath.string()));
}
std::stringstream buffer;
@@ -92,7 +91,7 @@ boost::json::value ParseJsonFile(const std::filesystem::path& filepath,
boost::json::value root = boost::json::parse(buffer.str(), error);
if (error) {
throw std::runtime_error(
std::format("Failed to parse {} JSON: {}", what, error.message()));
std::format("Failed to parse JSON: {}", error.message()));
}
return root;
}
@@ -126,34 +125,47 @@ const LocationsList& CuratedJsonDataService::LoadLocations() {
if (!cache_.locations.empty()) {
return cache_.locations;
}
boost::json::value root_val =
ParseJsonFile(filepaths_.locations_path);
const boost::json::value root =
ParseJsonFile(filepaths_.locations_path, "locations");
if (!root.is_array()) {
if (!root_val.is_object()) {
throw std::runtime_error(
"Invalid locations JSON: root element must be an array");
"Invalid locations JSON: root element must be an object");
}
LocationsList locations;
const auto& items = root.as_array();
locations.reserve(items.size());
boost::json::object root_obj = root_val.as_object();
for (const auto& item : items) {
if (!root_obj.contains("cities"))
{
throw std::runtime_error(
"Invalid locations JSON: root object must contain a 'cities' key");
}
if (!root_obj["cities"].is_array())
{
throw std::runtime_error(
"Invalid locations JSON: 'cities' key must be an array");
}
LocationsList locations;
const boost::json::array& cities = root_obj["cities"].as_array();
locations.reserve(cities.size());
for (const boost::json::value& item : cities) {
if (!item.is_object()) {
throw std::runtime_error(
"Invalid locations JSON: each entry must be an object");
}
const auto& object = item.as_object();
const boost::json::object& object = item.as_object();
locations.push_back(City{
.city = ReadRequiredString(object, "city"),
.state_province = ReadRequiredString(object, "state_province"),
.iso3166_2 = ReadRequiredString(object, "iso3166_2"),
.country = ReadRequiredString(object, "country"),
.iso3166_1 = ReadRequiredString(object, "iso3166_1"),
.latitude = ReadRequiredDouble(object, "latitude"),
.longitude = ReadRequiredDouble(object, "longitude"),
.latitude = ReadRequiredDouble(object, "latitude"),
.local_languages = ReadRequiredStringArray(object, "local_languages"),
.postal_code = ReadPostalCodeSpec(object),
});
@@ -168,7 +180,7 @@ const PersonasList& CuratedJsonDataService::LoadPersonas() {
}
const boost::json::value root =
ParseJsonFile(filepaths_.personas_path, "personas");
ParseJsonFile(filepaths_.personas_path);
if (!root.is_array()) {
throw std::runtime_error(
@@ -176,10 +188,10 @@ const PersonasList& CuratedJsonDataService::LoadPersonas() {
}
PersonasList personas;
const auto& items = root.as_array();
const boost::json::array& items = root.as_array();
personas.reserve(items.size());
for (const auto& item : items) {
for (const boost::json::value& item : items) {
if (!item.is_object()) {
throw std::runtime_error(
"Invalid personas JSON: each entry must be an object");
@@ -204,7 +216,7 @@ const ForenamesByCountryMap& CuratedJsonDataService::LoadForenamesByCountry() {
}
const boost::json::value root =
ParseJsonFile(filepaths_.forenames_path, "forenames-by-country");
ParseJsonFile(filepaths_.forenames_path);
if (!root.is_object()) {
throw std::runtime_error(
@@ -252,7 +264,7 @@ const SurnamesByCountryMap& CuratedJsonDataService::LoadSurnamesByCountry() {
}
const boost::json::value root =
ParseJsonFile(filepaths_.surnames_path, "surnames-by-country");
ParseJsonFile(filepaths_.surnames_path);
if (!root.is_object()) {
throw std::runtime_error(

View File

@@ -1,44 +0,0 @@
/**
* @file services/postal_code/xeger_postal_code_service.cc
* @brief XegerPostalCodeService::GeneratePostalCode() implementation.
*/
#include "services/postal_code/xeger_postal_code_service.h"
#include <format>
#include <random>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
#include "services/xeger/xeger.h"
XegerPostalCodeService::XegerPostalCodeService()
: rng_(std::random_device{}()) {}
std::string XegerPostalCodeService::GeneratePostalCode(const City& city) {
const PostalCodeSpec& spec = city.postal_code;
std::vector<std::string_view> city_patterns;
for (const std::string& pattern : spec.city_regexes) {
if (!pattern.empty()) {
city_patterns.push_back(pattern);
}
}
std::string_view pattern;
if (!city_patterns.empty()) {
std::uniform_int_distribution<size_t> dist(0, city_patterns.size() - 1);
pattern = city_patterns[dist(rng_)];
} else if (!spec.country_format_regex.empty()) {
pattern = spec.country_format_regex;
} else {
throw std::runtime_error(std::format(
"XegerPostalCodeService: no postal-code pattern available for "
"city '{}'",
city.city));
}
return GenerateStringFromRegex(pattern, rng_);
}

View File

@@ -170,13 +170,6 @@ uint64_t SqliteExportService::ProcessRecord(const BreweryRecord& brewery) {
sqlite_export_service_internal::kBreweryAddressCityIdBindIndex,
.value = city_id,
.action = "Failed to bind SQLite brewery address city id"});
sqlite_export_service_internal::Bind(
insert_brewery_address_stmt_,
sqlite_export_service_internal::BoundParam<std::string_view>{
.index = sqlite_export_service_internal::
kBreweryAddressPostalCodeBindIndex,
.value = brewery.address.postal_code,
.action = "Failed to bind SQLite brewery address postal code"});
sqlite_export_service_internal::Bind(
insert_brewery_address_stmt_,
sqlite_export_service_internal::BoundParam<double>{

View File

@@ -87,11 +87,18 @@ uint64_t SqliteExportService::ProcessRecord(const UserRecord& user) {
.action = "Failed to bind SQLite user address city id"});
sqlite_export_service_internal::Bind(
insert_user_address_stmt_,
sqlite_export_service_internal::BoundParam<std::string_view>{
sqlite_export_service_internal::BoundParam<double>{
.index =
sqlite_export_service_internal::kUserAddressPostalCodeBindIndex,
.value = user.address.postal_code,
.action = "Failed to bind SQLite user address postal code"});
sqlite_export_service_internal::kUserAddressLongitudeBindIndex,
.value = user.address.longitude,
.action = "Failed to bind SQLite user address longitude"});
sqlite_export_service_internal::Bind(
insert_user_address_stmt_,
sqlite_export_service_internal::BoundParam<double>{
.index =
sqlite_export_service_internal::kUserAddressLatitudeBindIndex,
.value = user.address.latitude,
.action = "Failed to bind SQLite user address latitude"});
sqlite_export_service_internal::StepStatement(
db_handle_, insert_user_address_stmt_,