Skip to content

Commit 094a644

Browse files
Implement V3 Coordinate-to-DCID Resolution using Spanner and S2Cells level 10 (#1828)
### Summary This PR implements the `V3Resolve` path for converting latitude/longitude coordinates to Place DCIDs (via S2 level-10 cells) using the Spanner graph backend. It also introduces a centralized "dominant place type" priority scheme to break ties when multiple places are returned. ### Key Logic & Changes 1. **S2 Cell Mapping (Spanner)**: * Converts `"lat#lng"` nodes into level-10 S2 cell IDs. * Queries `containedInPlace` edges for those cells from the Spanner `Edge` table. 2. **Dominant Type Filtering**: * Loads priority-based rules (e.g., Country > State > County > City) from a new `place_types.yaml` configuration. * Filters and sorts candidate places by their dominant type when returned from Spanner. * `place_types.yaml` - yaml format to allow comments 3. **Regression Testing**: * Expanded `V3Resolve` tests with 4 target coordinates and type filter regressions. ### Verification Results All tests pass locally for V3 Spanner: ```bash ENABLE_SPANNER_GRAPH=true go test ./internal/server/v3/resolve/golden ```
1 parent f8a47a4 commit 094a644

14 files changed

Lines changed: 884 additions & 14 deletions
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package spanner
16+
17+
import (
18+
"context"
19+
"testing"
20+
21+
pb "github.com/datacommonsorg/mixer/internal/proto"
22+
pbv1 "github.com/datacommonsorg/mixer/internal/proto/v1"
23+
pbv2 "github.com/datacommonsorg/mixer/internal/proto/v2"
24+
v2 "github.com/datacommonsorg/mixer/internal/server/v2"
25+
"github.com/datacommonsorg/mixer/internal/translator/types"
26+
"github.com/google/go-cmp/cmp"
27+
"google.golang.org/protobuf/testing/protocmp"
28+
)
29+
30+
type coordinateMockSpannerClient struct {
31+
getNodeEdgesByProp map[string]map[string][]*Edge
32+
assertGetNodeEdges func(ids []string, arc *v2.Arc, pageSize, offset int)
33+
}
34+
35+
func (m *coordinateMockSpannerClient) GetNodeProps(ctx context.Context, ids []string, out bool) (map[string][]*Property, error) {
36+
return nil, nil
37+
}
38+
39+
func (m *coordinateMockSpannerClient) GetNodeEdgesByID(ctx context.Context, ids []string, arc *v2.Arc, pageSize, offset int) (map[string][]*Edge, error) {
40+
if m.assertGetNodeEdges != nil {
41+
m.assertGetNodeEdges(ids, arc, pageSize, offset)
42+
}
43+
if arc != nil {
44+
if result, ok := m.getNodeEdgesByProp[arc.SingleProp]; ok {
45+
return result, nil
46+
}
47+
}
48+
return map[string][]*Edge{}, nil
49+
}
50+
51+
func (m *coordinateMockSpannerClient) GetObservations(ctx context.Context, variables []string, entities []string) ([]*Observation, error) {
52+
return nil, nil
53+
}
54+
55+
func (m *coordinateMockSpannerClient) CheckVariableExistence(ctx context.Context, variables []string, entities []string) ([][]string, error) {
56+
return nil, nil
57+
}
58+
59+
func (m *coordinateMockSpannerClient) GetObservationsContainedInPlace(ctx context.Context, variables []string, containedInPlace *v2.ContainedInPlace) ([]*Observation, error) {
60+
return nil, nil
61+
}
62+
63+
func (m *coordinateMockSpannerClient) SearchNodes(ctx context.Context, query string, types []string) ([]*SearchNode, error) {
64+
return nil, nil
65+
}
66+
67+
func (m *coordinateMockSpannerClient) ResolveByID(ctx context.Context, nodes []string, in, out string) (map[string][]string, error) {
68+
return nil, nil
69+
}
70+
71+
func (m *coordinateMockSpannerClient) GetEventCollectionDate(ctx context.Context, placeID, eventType string) ([]string, error) {
72+
return nil, nil
73+
}
74+
75+
func (m *coordinateMockSpannerClient) GetEventCollectionDcids(ctx context.Context, placeID, eventType, date string) ([]EventIdWithMagnitudeDcid, error) {
76+
return nil, nil
77+
}
78+
79+
func (m *coordinateMockSpannerClient) GetEventCollection(ctx context.Context, req *pbv1.EventCollectionRequest) (*pbv1.EventCollection, error) {
80+
return nil, nil
81+
}
82+
83+
func (m *coordinateMockSpannerClient) Sparql(ctx context.Context, nodes []types.Node, queries []*types.Query, opts *types.QueryOptions) ([][]string, error) {
84+
return nil, nil
85+
}
86+
87+
func (m *coordinateMockSpannerClient) GetProvenanceSummary(ctx context.Context, ids []string) (map[string]map[string]*pb.StatVarSummary_ProvenanceSummary, error) {
88+
return nil, nil
89+
}
90+
91+
func (m *coordinateMockSpannerClient) Id() string { return "mock" }
92+
func (m *coordinateMockSpannerClient) Start() {}
93+
func (m *coordinateMockSpannerClient) Close() {}
94+
95+
func TestResolveCoordinate(t *testing.T) {
96+
t.Parallel()
97+
98+
cellID := level10S2CellID(37.42, -122.08)
99+
ds := NewSpannerDataSource(&coordinateMockSpannerClient{
100+
assertGetNodeEdges: func(ids []string, arc *v2.Arc, pageSize, offset int) {
101+
if arc == nil || arc.SingleProp != "containedInPlace" {
102+
t.Fatalf("unexpected arc: %+v", arc)
103+
}
104+
if pageSize != 50 {
105+
t.Fatalf("GetNodeEdgesByID() pageSize = %d, want 50", pageSize)
106+
}
107+
if offset != 0 {
108+
t.Fatalf("GetNodeEdgesByID() offset = %d, want 0", offset)
109+
}
110+
if len(ids) != 1 || ids[0] != cellID {
111+
t.Fatalf("GetNodeEdgesByID() ids = %v, want [%s]", ids, cellID)
112+
}
113+
},
114+
getNodeEdgesByProp: map[string]map[string][]*Edge{
115+
"containedInPlace": {
116+
cellID: {
117+
{
118+
SubjectID: cellID,
119+
Predicate: "containedInPlace",
120+
Value: "geoId/06085",
121+
Types: []string{"County", "AdministrativeArea2"},
122+
},
123+
{
124+
SubjectID: cellID,
125+
Predicate: "containedInPlace",
126+
Value: "geoId/06",
127+
Types: []string{"State", "AdministrativeArea1"},
128+
},
129+
},
130+
},
131+
},
132+
}, nil, nil)
133+
134+
got, err := ds.Resolve(context.Background(), &pbv2.ResolveRequest{
135+
Nodes: []string{"37.42#-122.08"},
136+
Property: "<-geoCoordinate->dcid",
137+
})
138+
if err != nil {
139+
t.Fatalf("Resolve() error: %v", err)
140+
}
141+
142+
want := &pbv2.ResolveResponse{
143+
Entities: []*pbv2.ResolveResponse_Entity{
144+
{
145+
Node: "37.42#-122.08",
146+
Candidates: []*pbv2.ResolveResponse_Entity_Candidate{
147+
{Dcid: "geoId/06085", DominantType: "County"},
148+
{Dcid: "geoId/06", DominantType: "State"},
149+
},
150+
},
151+
},
152+
}
153+
154+
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
155+
t.Fatalf("Resolve() diff (-want +got):\n%s", diff)
156+
}
157+
}
158+
159+
func TestResolveCoordinateFetchesAllCellsInBatch(t *testing.T) {
160+
t.Parallel()
161+
162+
cellID1 := level10S2CellID(37.42, -122.08)
163+
cellID2 := level10S2CellID(36.77, -119.41)
164+
ds := NewSpannerDataSource(&coordinateMockSpannerClient{
165+
assertGetNodeEdges: func(ids []string, arc *v2.Arc, pageSize, offset int) {
166+
if arc == nil || arc.SingleProp != "containedInPlace" {
167+
t.Fatalf("unexpected arc: %+v", arc)
168+
}
169+
if pageSize != 100 {
170+
t.Fatalf("GetNodeEdgesByID() pageSize = %d, want 100", pageSize)
171+
}
172+
if offset != 0 {
173+
t.Fatalf("GetNodeEdgesByID() offset = %d, want 0", offset)
174+
}
175+
if len(ids) != 2 {
176+
t.Fatalf("GetNodeEdgesByID() ids len = %d, want 2", len(ids))
177+
}
178+
},
179+
getNodeEdgesByProp: map[string]map[string][]*Edge{
180+
"containedInPlace": {
181+
cellID1: {
182+
{
183+
SubjectID: cellID1,
184+
Predicate: "containedInPlace",
185+
Value: "geoId/06085",
186+
Types: []string{"County", "AdministrativeArea2"},
187+
},
188+
},
189+
cellID2: {
190+
{
191+
SubjectID: cellID2,
192+
Predicate: "containedInPlace",
193+
Value: "geoId/06019",
194+
Types: []string{"County", "AdministrativeArea2"},
195+
},
196+
},
197+
},
198+
},
199+
}, nil, nil)
200+
201+
got, err := ds.Resolve(context.Background(), &pbv2.ResolveRequest{
202+
Nodes: []string{"37.42#-122.08", "36.77#-119.41"},
203+
Property: "<-geoCoordinate->dcid",
204+
})
205+
if err != nil {
206+
t.Fatalf("Resolve() error: %v", err)
207+
}
208+
209+
want := &pbv2.ResolveResponse{
210+
Entities: []*pbv2.ResolveResponse_Entity{
211+
{
212+
Node: "37.42#-122.08",
213+
Candidates: []*pbv2.ResolveResponse_Entity_Candidate{
214+
{Dcid: "geoId/06085", DominantType: "County"},
215+
},
216+
},
217+
{
218+
Node: "36.77#-119.41",
219+
Candidates: []*pbv2.ResolveResponse_Entity_Candidate{
220+
{Dcid: "geoId/06019", DominantType: "County"},
221+
},
222+
},
223+
},
224+
}
225+
226+
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
227+
t.Fatalf("Resolve() diff (-want +got):\n%s", diff)
228+
}
229+
}
230+
231+
func TestResolveCoordinateTypeFilterUsesDominantType(t *testing.T) {
232+
t.Parallel()
233+
234+
cellID := level10S2CellID(37.42, -122.08)
235+
ds := NewSpannerDataSource(&coordinateMockSpannerClient{
236+
getNodeEdgesByProp: map[string]map[string][]*Edge{
237+
"containedInPlace": {
238+
cellID: {
239+
{
240+
SubjectID: cellID,
241+
Predicate: "containedInPlace",
242+
Value: "geoId/06085",
243+
Types: []string{"County", "AdministrativeArea2"},
244+
},
245+
{
246+
SubjectID: cellID,
247+
Predicate: "containedInPlace",
248+
Value: "geoId/06",
249+
Types: []string{"State", "AdministrativeArea1"},
250+
},
251+
},
252+
},
253+
},
254+
}, nil, nil)
255+
256+
got, err := ds.Resolve(context.Background(), &pbv2.ResolveRequest{
257+
Nodes: []string{"37.42#-122.08"},
258+
Property: "<-geoCoordinate{typeOf:AdministrativeArea2}->dcid",
259+
})
260+
if err != nil {
261+
t.Fatalf("Resolve() error: %v", err)
262+
}
263+
264+
want := &pbv2.ResolveResponse{
265+
Entities: []*pbv2.ResolveResponse_Entity{
266+
{
267+
Node: "37.42#-122.08",
268+
Candidates: []*pbv2.ResolveResponse_Entity_Candidate{},
269+
},
270+
},
271+
}
272+
273+
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
274+
t.Fatalf("Resolve() diff (-want +got):\n%s", diff)
275+
}
276+
}
277+
278+
func TestResolveCoordinateSkipsS2CellCandidatesByType(t *testing.T) {
279+
t.Parallel()
280+
281+
cellID := level10S2CellID(37.42, -122.08)
282+
ds := NewSpannerDataSource(&coordinateMockSpannerClient{
283+
getNodeEdgesByProp: map[string]map[string][]*Edge{
284+
"containedInPlace": {
285+
cellID: {
286+
{
287+
SubjectID: cellID,
288+
Predicate: "containedInPlace",
289+
Value: level10S2CellID(37.43, -122.09),
290+
Types: []string{"S2CellLevel10"},
291+
},
292+
{
293+
SubjectID: cellID,
294+
Predicate: "containedInPlace",
295+
Value: "geoId/06085",
296+
Types: []string{"County", "AdministrativeArea2"},
297+
},
298+
},
299+
},
300+
},
301+
}, nil, nil)
302+
303+
got, err := ds.Resolve(context.Background(), &pbv2.ResolveRequest{
304+
Nodes: []string{"37.42#-122.08"},
305+
Property: "<-geoCoordinate->dcid",
306+
})
307+
if err != nil {
308+
t.Fatalf("Resolve() error: %v", err)
309+
}
310+
311+
want := &pbv2.ResolveResponse{
312+
Entities: []*pbv2.ResolveResponse_Entity{
313+
{
314+
Node: "37.42#-122.08",
315+
Candidates: []*pbv2.ResolveResponse_Entity_Candidate{
316+
{Dcid: "geoId/06085", DominantType: "County"},
317+
},
318+
},
319+
},
320+
}
321+
322+
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
323+
t.Fatalf("Resolve() diff (-want +got):\n%s", diff)
324+
}
325+
}
326+

0 commit comments

Comments
 (0)