From 6c79b8a85ef1e74f90a055f3fcc4cbe29755e4b0 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 8 Apr 2026 14:40:27 +0000 Subject: [PATCH] Add address GID to public report This is _way_ better than trying to re-transmit structured address data to the backend via strings --- db/dbinfo/address.bob.go | 12 ++++++- db/dbinfo/publicreport.report.bob.go | 12 ++++++- .../00127_publicreport_address_gid.sql | 10 ++++++ db/models/address.bob.go | 33 ++++++++++++++--- db/models/publicreport.report.bob.go | 33 ++++++++++++++--- platform/geocode/geocode.go | 9 +++-- resource/nuisance.go | 35 ++++++++----------- ts/rmo/content/Nuisance.vue | 25 ++++++++----- 8 files changed, 127 insertions(+), 42 deletions(-) create mode 100644 db/migrations/00127_publicreport_address_gid.sql diff --git a/db/dbinfo/address.bob.go b/db/dbinfo/address.bob.go index 76e30696..7f3ceadb 100644 --- a/db/dbinfo/address.bob.go +++ b/db/dbinfo/address.bob.go @@ -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, } } diff --git a/db/dbinfo/publicreport.report.bob.go b/db/dbinfo/publicreport.report.bob.go index 5118a777..b73e46eb 100644 --- a/db/dbinfo/publicreport.report.bob.go +++ b/db/dbinfo/publicreport.report.bob.go @@ -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, } } diff --git a/db/migrations/00127_publicreport_address_gid.sql b/db/migrations/00127_publicreport_address_gid.sql new file mode 100644 index 00000000..a4925a9b --- /dev/null +++ b/db/migrations/00127_publicreport_address_gid.sql @@ -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; diff --git a/db/models/address.bob.go b/db/models/address.bob.go index fd94c7ba..82745e1f 100644 --- a/db/models/address.bob.go +++ b/db/models/address.bob.go @@ -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), } } diff --git a/db/models/publicreport.report.bob.go b/db/models/publicreport.report.bob.go index 7737fcfd..1bbbfad5 100644 --- a/db/models/publicreport.report.bob.go +++ b/db/models/publicreport.report.bob.go @@ -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), } } diff --git a/platform/geocode/geocode.go b/platform/geocode/geocode.go index 772d1392..928778cd 100644 --- a/platform/geocode/geocode.go +++ b/platform/geocode/geocode.go @@ -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, diff --git a/resource/nuisance.go b/resource/nuisance.go index f46fe4a0..412b5cba 100644 --- a/resource/nuisance.go +++ b/resource/nuisance.go @@ -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 diff --git a/ts/rmo/content/Nuisance.vue b/ts/rmo/content/Nuisance.vue index 1d0267f0..a4f344bc 100644 --- a/ts/rmo/content/Nuisance.vue +++ b/ts/rmo/content/Nuisance.vue @@ -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,