Add address GID to public report

This is _way_ better than trying to re-transmit structured address data
to the backend via strings
This commit is contained in:
Eli Ribble 2026-04-08 14:40:27 +00:00
parent 765b8fbef7
commit 6c79b8a85e
No known key found for this signature in database
8 changed files with 127 additions and 42 deletions

View file

@ -132,6 +132,15 @@ var Addresses = Table[
Generated: true,
AutoIncr: false,
},
Gid: column{
Name: "gid",
DBType: "text",
Default: "",
Comment: "",
Nullable: false,
Generated: false,
AutoIncr: false,
},
},
Indexes: addressIndexes{
AddressPkey: index{
@ -192,11 +201,12 @@ type addressColumns struct {
Number column
LocationX column
LocationY column
Gid column
}
func (c addressColumns) AsSlice() []column {
return []column{
c.Country, c.Created, c.Location, c.H3cell, c.ID, c.Locality, c.PostalCode, c.Street, c.Unit, c.Region, c.Number, c.LocationX, c.LocationY,
c.Country, c.Created, c.Location, c.H3cell, c.ID, c.Locality, c.PostalCode, c.Street, c.Unit, c.Region, c.Number, c.LocationX, c.LocationY, c.Gid,
}
}

View file

@ -258,6 +258,15 @@ var PublicreportReports = Table[
Generated: true,
AutoIncr: false,
},
AddressGid: column{
Name: "address_gid",
DBType: "text",
Default: "",
Comment: "",
Nullable: false,
Generated: false,
AutoIncr: false,
},
},
Indexes: publicreportReportIndexes{
ReportPkey: index{
@ -368,11 +377,12 @@ type publicreportReportColumns struct {
Status column
LocationLatitude column
LocationLongitude column
AddressGid column
}
func (c publicreportReportColumns) AsSlice() []column {
return []column{
c.AddressRaw, c.AddressNumber, c.AddressStreet, c.AddressLocality, c.AddressRegion, c.AddressPostalCode, c.AddressCountry, c.AddressID, c.Created, c.Location, c.H3cell, c.ID, c.LatlngAccuracyType, c.LatlngAccuracyValue, c.MapZoom, c.OrganizationID, c.PublicID, c.ReporterName, c.ReporterEmail, c.ReporterPhone, c.ReporterContactConsent, c.ReportType, c.Reviewed, c.ReviewerID, c.Status, c.LocationLatitude, c.LocationLongitude,
c.AddressRaw, c.AddressNumber, c.AddressStreet, c.AddressLocality, c.AddressRegion, c.AddressPostalCode, c.AddressCountry, c.AddressID, c.Created, c.Location, c.H3cell, c.ID, c.LatlngAccuracyType, c.LatlngAccuracyValue, c.MapZoom, c.OrganizationID, c.PublicID, c.ReporterName, c.ReporterEmail, c.ReporterPhone, c.ReporterContactConsent, c.ReportType, c.Reviewed, c.ReviewerID, c.Status, c.LocationLatitude, c.LocationLongitude, c.AddressGid,
}
}

View file

@ -0,0 +1,10 @@
-- +goose Up
ALTER TABLE address ADD COLUMN gid TEXT;
UPDATE address SET gid = '';
ALTER TABLE address ALTER COLUMN gid SET NOT NULL;
ALTER TABLE publicreport.report ADD COLUMN address_gid TEXT;
UPDATE publicreport.report SET address_gid = '';
ALTER TABLE publicreport.report ALTER COLUMN address_gid SET NOT NULL;
-- +goose Down
ALTER TABLE publicreport.report DROP COLUMN address_gid;
ALTER TABLE address DROP COLUMN gid;

View file

@ -39,6 +39,7 @@ type Address struct {
Number string `db:"number_" `
LocationX null.Val[float64] `db:"location_x,generated" `
LocationY null.Val[float64] `db:"location_y,generated" `
Gid string `db:"gid" `
R addressR `db:"-" `
}
@ -66,7 +67,7 @@ type addressR struct {
func buildAddressColumns(alias string) addressColumns {
return addressColumns{
ColumnsExpr: expr.NewColumnsExpr(
"country", "created", "location", "h3cell", "id", "locality", "postal_code", "street", "unit", "region", "number_", "location_x", "location_y",
"country", "created", "location", "h3cell", "id", "locality", "postal_code", "street", "unit", "region", "number_", "location_x", "location_y", "gid",
).WithParent("address"),
tableAlias: alias,
Country: psql.Quote(alias, "country"),
@ -82,6 +83,7 @@ func buildAddressColumns(alias string) addressColumns {
Number: psql.Quote(alias, "number_"),
LocationX: psql.Quote(alias, "location_x"),
LocationY: psql.Quote(alias, "location_y"),
Gid: psql.Quote(alias, "gid"),
}
}
@ -101,6 +103,7 @@ type addressColumns struct {
Number psql.Expression
LocationX psql.Expression
LocationY psql.Expression
Gid psql.Expression
}
func (c addressColumns) Alias() string {
@ -126,10 +129,11 @@ type AddressSetter struct {
Unit omit.Val[string] `db:"unit" `
Region omit.Val[string] `db:"region" `
Number omit.Val[string] `db:"number_" `
Gid omit.Val[string] `db:"gid" `
}
func (s AddressSetter) SetColumns() []string {
vals := make([]string, 0, 11)
vals := make([]string, 0, 12)
if s.Country.IsValue() {
vals = append(vals, "country")
}
@ -163,6 +167,9 @@ func (s AddressSetter) SetColumns() []string {
if s.Number.IsValue() {
vals = append(vals, "number_")
}
if s.Gid.IsValue() {
vals = append(vals, "gid")
}
return vals
}
@ -200,6 +207,9 @@ func (s AddressSetter) Overwrite(t *Address) {
if s.Number.IsValue() {
t.Number = s.Number.MustGet()
}
if s.Gid.IsValue() {
t.Gid = s.Gid.MustGet()
}
}
func (s *AddressSetter) Apply(q *dialect.InsertQuery) {
@ -208,7 +218,7 @@ func (s *AddressSetter) Apply(q *dialect.InsertQuery) {
})
q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) {
vals := make([]bob.Expression, 11)
vals := make([]bob.Expression, 12)
if s.Country.IsValue() {
vals[0] = psql.Arg(s.Country.MustGet())
} else {
@ -275,6 +285,12 @@ func (s *AddressSetter) Apply(q *dialect.InsertQuery) {
vals[10] = psql.Raw("DEFAULT")
}
if s.Gid.IsValue() {
vals[11] = psql.Arg(s.Gid.MustGet())
} else {
vals[11] = psql.Raw("DEFAULT")
}
return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "")
}))
}
@ -284,7 +300,7 @@ func (s AddressSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] {
}
func (s AddressSetter) Expressions(prefix ...string) []bob.Expression {
exprs := make([]bob.Expression, 0, 11)
exprs := make([]bob.Expression, 0, 12)
if s.Country.IsValue() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
@ -363,6 +379,13 @@ func (s AddressSetter) Expressions(prefix ...string) []bob.Expression {
}})
}
if s.Gid.IsValue() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
psql.Quote(append(prefix, "gid")...),
psql.Arg(s.Gid),
}})
}
return exprs
}
@ -1141,6 +1164,7 @@ type addressWhere[Q psql.Filterable] struct {
Number psql.WhereMod[Q, string]
LocationX psql.WhereNullMod[Q, float64]
LocationY psql.WhereNullMod[Q, float64]
Gid psql.WhereMod[Q, string]
}
func (addressWhere[Q]) AliasedAs(alias string) addressWhere[Q] {
@ -1162,6 +1186,7 @@ func buildAddressWhere[Q psql.Filterable](cols addressColumns) addressWhere[Q] {
Number: psql.Where[Q, string](cols.Number),
LocationX: psql.WhereNull[Q, float64](cols.LocationX),
LocationY: psql.WhereNull[Q, float64](cols.LocationY),
Gid: psql.Where[Q, string](cols.Gid),
}
}

View file

@ -54,6 +54,7 @@ type PublicreportReport struct {
Status enums.PublicreportReportstatustype `db:"status" `
LocationLatitude null.Val[float64] `db:"location_latitude,generated" `
LocationLongitude null.Val[float64] `db:"location_longitude,generated" `
AddressGid string `db:"address_gid" `
R publicreportReportR `db:"-" `
}
@ -87,7 +88,7 @@ type publicreportReportR struct {
func buildPublicreportReportColumns(alias string) publicreportReportColumns {
return publicreportReportColumns{
ColumnsExpr: expr.NewColumnsExpr(
"address_raw", "address_number", "address_street", "address_locality", "address_region", "address_postal_code", "address_country", "address_id", "created", "location", "h3cell", "id", "latlng_accuracy_type", "latlng_accuracy_value", "map_zoom", "organization_id", "public_id", "reporter_name", "reporter_email", "reporter_phone", "reporter_contact_consent", "report_type", "reviewed", "reviewer_id", "status", "location_latitude", "location_longitude",
"address_raw", "address_number", "address_street", "address_locality", "address_region", "address_postal_code", "address_country", "address_id", "created", "location", "h3cell", "id", "latlng_accuracy_type", "latlng_accuracy_value", "map_zoom", "organization_id", "public_id", "reporter_name", "reporter_email", "reporter_phone", "reporter_contact_consent", "report_type", "reviewed", "reviewer_id", "status", "location_latitude", "location_longitude", "address_gid",
).WithParent("publicreport.report"),
tableAlias: alias,
AddressRaw: psql.Quote(alias, "address_raw"),
@ -117,6 +118,7 @@ func buildPublicreportReportColumns(alias string) publicreportReportColumns {
Status: psql.Quote(alias, "status"),
LocationLatitude: psql.Quote(alias, "location_latitude"),
LocationLongitude: psql.Quote(alias, "location_longitude"),
AddressGid: psql.Quote(alias, "address_gid"),
}
}
@ -150,6 +152,7 @@ type publicreportReportColumns struct {
Status psql.Expression
LocationLatitude psql.Expression
LocationLongitude psql.Expression
AddressGid psql.Expression
}
func (c publicreportReportColumns) Alias() string {
@ -189,10 +192,11 @@ type PublicreportReportSetter struct {
Reviewed omitnull.Val[time.Time] `db:"reviewed" `
ReviewerID omitnull.Val[int32] `db:"reviewer_id" `
Status omit.Val[enums.PublicreportReportstatustype] `db:"status" `
AddressGid omit.Val[string] `db:"address_gid" `
}
func (s PublicreportReportSetter) SetColumns() []string {
vals := make([]string, 0, 25)
vals := make([]string, 0, 26)
if s.AddressRaw.IsValue() {
vals = append(vals, "address_raw")
}
@ -268,6 +272,9 @@ func (s PublicreportReportSetter) SetColumns() []string {
if s.Status.IsValue() {
vals = append(vals, "status")
}
if s.AddressGid.IsValue() {
vals = append(vals, "address_gid")
}
return vals
}
@ -347,6 +354,9 @@ func (s PublicreportReportSetter) Overwrite(t *PublicreportReport) {
if s.Status.IsValue() {
t.Status = s.Status.MustGet()
}
if s.AddressGid.IsValue() {
t.AddressGid = s.AddressGid.MustGet()
}
}
func (s *PublicreportReportSetter) Apply(q *dialect.InsertQuery) {
@ -355,7 +365,7 @@ func (s *PublicreportReportSetter) Apply(q *dialect.InsertQuery) {
})
q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) {
vals := make([]bob.Expression, 25)
vals := make([]bob.Expression, 26)
if s.AddressRaw.IsValue() {
vals[0] = psql.Arg(s.AddressRaw.MustGet())
} else {
@ -506,6 +516,12 @@ func (s *PublicreportReportSetter) Apply(q *dialect.InsertQuery) {
vals[24] = psql.Raw("DEFAULT")
}
if s.AddressGid.IsValue() {
vals[25] = psql.Arg(s.AddressGid.MustGet())
} else {
vals[25] = psql.Raw("DEFAULT")
}
return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "")
}))
}
@ -515,7 +531,7 @@ func (s PublicreportReportSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] {
}
func (s PublicreportReportSetter) Expressions(prefix ...string) []bob.Expression {
exprs := make([]bob.Expression, 0, 25)
exprs := make([]bob.Expression, 0, 26)
if s.AddressRaw.IsValue() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
@ -692,6 +708,13 @@ func (s PublicreportReportSetter) Expressions(prefix ...string) []bob.Expression
}})
}
if s.AddressGid.IsValue() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
psql.Quote(append(prefix, "address_gid")...),
psql.Arg(s.AddressGid),
}})
}
return exprs
}
@ -1964,6 +1987,7 @@ type publicreportReportWhere[Q psql.Filterable] struct {
Status psql.WhereMod[Q, enums.PublicreportReportstatustype]
LocationLatitude psql.WhereNullMod[Q, float64]
LocationLongitude psql.WhereNullMod[Q, float64]
AddressGid psql.WhereMod[Q, string]
}
func (publicreportReportWhere[Q]) AliasedAs(alias string) publicreportReportWhere[Q] {
@ -1999,6 +2023,7 @@ func buildPublicreportReportWhere[Q psql.Filterable](cols publicreportReportColu
Status: psql.Where[Q, enums.PublicreportReportstatustype](cols.Status),
LocationLatitude: psql.WhereNull[Q, float64](cols.LocationLatitude),
LocationLongitude: psql.WhereNull[Q, float64](cols.LocationLongitude),
AddressGid: psql.Where[Q, string](cols.AddressGid),
}
}

View file

@ -75,10 +75,11 @@ func EnsureAddress(ctx context.Context, txn bob.Executor, a types.Address, l typ
}
created := time.Now()
row, err := bob.One(ctx, txn, psql.Insert(
im.Into("address", "country", "created", "h3cell", "id", "locality", "location", "number_", "postal_code", "region", "street", "unit"),
im.Into("address", "country", "created", "gid", "h3cell", "id", "locality", "location", "number_", "postal_code", "region", "street", "unit"),
im.Values(
psql.Arg(a.CountryEnum()),
psql.Arg(created),
psql.Arg(a.GID),
psql.Arg(cell),
psql.Raw("DEFAULT"),
psql.Arg(a.Locality),
@ -97,6 +98,7 @@ func EnsureAddress(ctx context.Context, txn bob.Executor, a types.Address, l typ
return &models.Address{
Country: a.CountryEnum(),
Created: created,
Gid: a.GID,
H3cell: "",
ID: row.ID,
Locality: a.Locality,
@ -135,10 +137,11 @@ func EnsureAddressWithGeocode(ctx context.Context, txn bob.Executor, org *models
}
created := time.Now()
row, err := bob.One(ctx, txn, psql.Insert(
im.Into("address", "country", "created", "h3cell", "id", "locality", "location", "number_", "postal_code", "region", "street", "unit"),
im.Into("address", "country", "created", "gid", "h3cell", "id", "locality", "location", "number_", "postal_code", "region", "street", "unit"),
im.Values(
psql.Arg(geo.Address.Country),
psql.Arg(created),
psql.Arg(geo.Address.GID),
psql.Arg(geo.Cell),
psql.Raw("DEFAULT"),
psql.Arg(geo.Address.Locality),
@ -158,6 +161,7 @@ func EnsureAddressWithGeocode(ctx context.Context, txn bob.Executor, org *models
return &models.Address{
Country: geo.Address.CountryEnum(),
Created: created,
Gid: geo.Address.GID,
H3cell: "",
ID: row.ID,
Locality: geo.Address.Locality,
@ -232,6 +236,7 @@ func toGeocodeResult(resp stadia.GeocodeResponse, address_msg string) (*GeocodeR
// This first structure generally works for forword geocoding
address := types.Address{
Country: country_s,
GID: feature.Properties.GID,
Locality: feature.Properties.Locality,
Number: feature.Properties.HouseNumber,
PostalCode: feature.Properties.PostalCode,

View file

@ -32,13 +32,8 @@ type nuisance struct {
}
type nuisanceForm struct {
AdditionalInfo string `schema:"additional-info"`
AddressRaw string `schema:"address"`
AddressCountry string `schema:"address-country"`
AddressLocality string `schema:"address-locality"`
AddressNumber string `schema:"address-number"`
AddressPostalCode string `schema:"address-postalcode"`
AddressRegion string `schema:"address-region"`
AddressStreet string `schema:"address-street"`
AddressGID string `schema:"address-gid"`
Address string `schema:"address"`
Duration string `schema:"duration"`
Latitude string `schema:"latitude"`
Longitude string `schema:"longitude"`
@ -136,24 +131,19 @@ func (res *nuisanceR) Create(ctx context.Context, r *http.Request, n nuisanceFor
return nil, nhttp.NewError("Failed to extract image uploads: %w", err)
}
address := platform.Address{
Country: n.AddressCountry,
Locality: n.AddressLocality,
Number: n.AddressNumber,
PostalCode: n.AddressPostalCode,
Raw: n.AddressRaw,
Region: n.AddressRegion,
Street: n.AddressStreet,
Unit: "",
GID: n.AddressGID,
Raw: n.Address,
}
setter_report := models.PublicreportReportSetter{
//AddressID: omitnull.From(latlng.Cell.String()),
AddressCountry: omit.From(""),
AddressGid: omit.From(address.GID),
AddressNumber: omit.From(""),
AddressLocality: omit.From(""),
AddressPostalCode: omit.From(""),
AddressRaw: omit.From(address.Raw),
AddressCountry: omit.From(address.Country),
AddressNumber: omit.From(address.Number),
AddressLocality: omit.From(address.Locality),
AddressPostalCode: omit.From(address.PostalCode),
AddressRegion: omit.From(address.Region),
AddressStreet: omit.From(address.Street),
AddressRegion: omit.From(""),
AddressStreet: omit.From(""),
Created: omit.From(time.Now()),
//H3cell: omitnull.From(latlng.Cell.String()),
LatlngAccuracyType: omit.From(latlng.AccuracyType),
@ -188,6 +178,9 @@ func (res *nuisanceR) Create(ctx context.Context, r *http.Request, n nuisanceFor
TodNight: omit.From(n.TODNight),
}
report, err := platform.ReportNuisanceCreate(ctx, setter_report, setter_nuisance, latlng, address, uploads)
if err != nil {
return nil, nhttp.NewError("create nuisance report: %w", err)
}
return &nuisance{
ID: report.PublicID,
}, nil

View file

@ -566,8 +566,13 @@ function doAddressSuggestionSelected(suggestion: GeocodeSuggestion) {
async function doAddressSuggestionDetails(suggestion: GeocodeSuggestion) {
// Fetch full details for the selected suggestion
//const url = `https://api.stadiamaps.com/geocoding/v2/place_details?ids=${suggestion.properties.gid}`;
selectedSuggestion.value = suggestion;
const url = `/api/geocode/by-gid/${suggestion.gid}`;
const response = await fetch(url);
if (!response.ok) {
console.error("Failed to get suggestion detail", response.statusText);
return;
}
const data = (await response.json()) as Geocode;
if (currentCamera.value) {
@ -590,7 +595,14 @@ function doMapClick(location: Location) {
geocode
.reverse(location)
.then((code: Geocode) => {
console.log("geocoded", code);
address.value = code.address.raw;
selectedSuggestion.value = {
detail: code.address.number + " " + code.address.street,
gid: code.address.gid,
locality: code.address.locality,
type: "address",
};
console.log("reverse geocoded", code);
})
.catch((e) => {
console.error("failed to reverse geocode after map click", e);
@ -615,18 +627,13 @@ async function doSubmit() {
formData.append("address-gid", selectedSuggestion.value.gid);
}
if (currentLocation.value) {
formData.append(
"location_latitude",
currentLocation.value.latitude.toString(),
);
formData.append(
"location_longitude",
currentLocation.value.longitude.toString(),
);
formData.append("latitude", currentLocation.value.latitude.toString());
formData.append("longitude", currentLocation.value.longitude.toString());
}
images.value.forEach((image, index) => {
formData.append(`image[${index}]`, image.file, image.name);
});
formData.append("address", address.value);
await fetch("/api/rmo/nuisance", {
method: "POST",
body: formData,