Skip to content

feat: Introduce Location Search + Lookup capabilities #545

Closed
jingyli wants to merge 11 commits into
Universal-Commerce-Protocol:mainfrom
jingyli:location-feat
Closed

feat: Introduce Location Search + Lookup capabilities #545
jingyli wants to merge 11 commits into
Universal-Commerce-Protocol:mainfrom
jingyli:location-feat

Conversation

@jingyli

@jingyli jingyli commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Description

Defines standard interfaces for discovering, searching, and retrieving physical locations (e.g., retail stores, restaurants, warehouses, lodging properties).

It introduces two new capabilities under the dev.ucp.common namespace (consistent with Shopping's Catalog capability design):

  1. Location Search (dev.ucp.common.location.search): Discovery-focused endpoint for natural language query, geographic, and offerings-based filters.
  2. Location Lookup (dev.ucp.common.location.lookup): Resolution-focused endpoints supporting single & batch lookups.

Some key commerce flows it will be able to unlock:

  • Local Pickup Discovery: Finding locations like retail stores or restaurant branches
    nearby that support customer pickup and checking their operating hours & inventory availability
    before selection.
  • Fulfillment Area Verification: Checking if a specific location (e.g., utility depot, restaurant,
    or local service provider) has delivery coverage for a buyer's address.

Category (Required)

  • Core Protocol: Changes to the base communication layer, global context, or breaking refactors. (Requires Technical Council approval)
  • Governance/Contributing: Updates to GOVERNANCE.md, CONTRIBUTING.md, or CODEOWNERS. (Requires Governance Council approval)
  • Capability: New schemas (Discovery, Cart, etc.) or extensions. (Requires Maintainer approval)
  • Documentation: Updates to README, or documentations regarding schema or capabilities. (Requires Maintainer approval)
  • Infrastructure: CI/CD, Linters, or build scripts. (Requires DevOps Maintainer approval)
  • Maintenance: Version bumps, lockfile updates, or minor bug fixes. (Requires DevOps Maintainer approval)
  • SDK: Language-specific SDK updates and releases. (Requires DevOps Maintainer approval)
  • Samples / Conformance: Maintaining samples and the conformance suite. (Requires Maintainer approval)
  • UCP Schema: Changes to the ucp-schema tool (resolver, linter, validator). (Requires Maintainer approval)
  • Community Health (.github): Updates to templates, workflows, or org-level configs. (Requires DevOps Maintainer approval)

Related Issues

This is related to RFC #375's section 10.

Checklist

  • I have followed the Contributing Guide (including Conventional Commits title requirements and ! for breaking changes).
  • I have updated the documentation (if applicable).
  • My changes pass all local linting and formatting checks.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • (For Core/Capability) I have included/updated the relevant JSON schemas.
  • I have regenerated Python Pydantic models by running generate_models.sh under python_sdk.

Screenshots / Logs (if applicable)

location_ucp_doc

@jingyli jingyli added this to the Working Draft milestone Jun 24, 2026
@jingyli jingyli added TC review Ready for TC review WIP labels Jun 24, 2026
@jingyli
jingyli marked this pull request as ready for review June 29, 2026 17:33
@jingyli
jingyli requested review from a team as code owners June 29, 2026 17:33
@jingyli jingyli removed the WIP label Jun 29, 2026
Comment thread source/schemas/common/types/location.json
"description": "Geographic coordinates and geofence for the location.",
"ucp_request": "omit"
},
"hours": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two related flags where the same input can be read two ways by different implementations. First, daily_hour/exception_hour say "when is_closed/is_24_hours is true, intervals MUST be omitted," but nothing enforces it, so a contradictory record (is_closed: true + is_24_hours: true + intervals: [...]) validates and two businesses can disagree on whether the store is open. Second, the "overnight = two entries across two days" guidance is prose-only: time_interval leaves close < open undefined (22:00-06:00 validates with no defined meaning), and 24:00 is rejected by the HH:MM pattern, so a shift ending at midnight has to be written 00:00, which is itself ambiguous.

Rather than reach for if/then schema enforcement, should we make every record deterministic with one small schema change plus two normative rules?

  1. Allow 24:00 in time_interval so an end-of-day/midnight close is expressible:
    - "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$"
    + "pattern": "^(([01][0-9]|2[0-3]):[0-5][0-9]|24:00)$"
    
  2. Add a precedence rule (in the Location hours section): for a given day or date, if is_closed is true the location is closed and is_24_hours/intervals MUST be ignored; otherwise if is_24_hours is true it is open all day and intervals MUST be ignored; otherwise it is open during intervals (absent or empty means closed).
  3. Add a cross-midnight rule: within one interval close MUST be later than open; a span that crosses midnight MUST be split into two daily_hour entries (the first ending 24:00, the next day starting 00:00).

With 24:00 allowed, the midnight close becomes unambiguous, close > open always holds, and 22:00-06:00 is invalid (must be split) rather than undefined. Would that work?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proposed rules makes sense and do help to disambiguate & enforce stricter semantics on the schema based on the prose. The only nit is for rule 1, I only adjusted the pattern to time_interval.close to allow for the value 24:00 - given close > open, I don't see any reasons to allow a time interval to start with 24:00.

Added the recommendations accordingly.

Comment thread source/schemas/common/types/location_filter.json
"properties": {
"center": {
"$ref": "geo.json",
"description": "The center coordinates. If omitted, the user's course location hints in the request context MUST be used to derive the center."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description says that when center is omitted, "the user's coarse location hints in the request context MUST be used to derive the center." But center is a geo (required latitude/longitude), while context composes locality.json, which only carries address_country, address_region, and postal_code, with no coordinates anywhere. As written, the one normative geo input a search can have is not expressible from the request alone, so the MUST cannot be met without an unstated geocoding step. The first REST example sidesteps this by sending explicit center coordinates, so the fallback path is never exercised.
Could we either (a) relax this to SHOULD and state that the business geocodes the coarse locality hint into a centroid, or (b) let center also accept a locality so the postal/region centroid is a first-class, schema-expressible input? Whichever we choose, could the spec name the exact field the center is derived from, so two implementations geocode the same thing?

@jingyli jingyli Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to pressure test this comment a bit more - I think there is value to enforce a MUST semantics here, because when the values is omitted, then there needs to be another data source to which the distance can be measured against - so loosening it up to a SHOULD semantics will introduce an undefined behaviour for implementors.

I also feel it's not ideal to introduce locality into center since for distance measurement, in my mind, it's always going to be based off a set point. So curious if there is an option c) we can explore here: we mention specifically that the expectation, when using the user address hints in context as fallback, is that business must convert it using forward geocoding strategies?

"type": "boolean",
"description": "Only return locations that are currently open."
},
"open_at": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

open_at describes two incompatible time meanings; could we pick one?
open_at is format: date-time with the description "the specified date and time (RFC 3339) in the location's local time." Those two halves disagree. An RFC 3339 date-time carries an offset and denotes one absolute instant on the global timeline; "in the location's local time" describes a wall-clock value (2 PM at the store), which is a different quantity and is not even a valid RFC 3339 date-time (no offset). So an implementer cannot tell which is meant.

It changes results for any multi-timezone search. If a platform sends 2026-06-29T14:00:00-04:00, read as an absolute instant the business converts it to each location's local time and tests New York at 14:00 but Los Angeles at 11:00; read as "2 PM local everywhere," a single instant cannot express that at all (each timezone is a different instant). Same request, different locations returned.

Could we commit to one reading in the description? The cleaner option, matching format: date-time, is absolute-instant: "An RFC 3339 instant. The business converts it to the location's local time using location.timezone and evaluates it against hours/exception_hours (the HH:MM intervals)." If the intent is actually a local wall-clock time, then could we drop format: date-time and model it as a date plus HH:MM evaluated in each location's own timezone? Either works; the ask is just to state which, since open_now (a plain boolean evaluated "now" per location) is unambiguous but open_at currently is not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on this disagreement, I favour the option where platform still pass in a RFC 3339 instant and let the business converts it into the local timezone of the location and do the comparison as it will also support the use case where platform may not know about the location's timezone ahead of the request.

Will update this description here accordingly.

Comment thread source/schemas/common/types/location_filter.json Outdated
"max_distance": {
"type": "number",
"minimum": 0,
"description": "Maximum distance in RFC 7035 distance unit (meters)."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the right RFC to link?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to propose yes, https://datatracker.ietf.org/doc/html/rfc7035 is the RFC titled Relative Location Representation and this field is semantically equivalent to the concept of relative offset.

Comment thread source/schemas/common/types/signals.json
"type": "object",
"required": ["id"],
"properties": {
"id": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Undeclared cross-capability coupling.
inventory_filter.id is "product or variant ID in shopping, or dish ID in food ordering," and type well-known values are product/dish. That ties this common capability to Catalog/menu ID namespaces. There is no $ref from common/ into shopping/, so this creates a documentation gap. Could we add a short Dependencies note (IDs originate from Catalog/menu; type routes backend resolution) so integrators know how to obtain valid IDs?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good call out. Related to this comment, I'm almost leaning towards removing type as a field in this filter here as a start due to:

  • Like what's being called out in the comment, as soon as we start introducing well-known values into type, it introduces a new connection between common capability and vertical service names. This might be a decision made too pre-maturely since it opens up the precedence for the dependency to go beyond schema constructs.
  • This field originally was introduced with the intention to support businesses that operate in multiple verticals. However, I now feel like even in those cases, the products/items/offerings they have should be unique enough for them to easily understand the lookup via id (the additional backend explicit routing mechanism may not be needed, and even if we do end up introducing it later, it would be a straightforward non-breaking change).

I'm open to hearing more thoughts and opinions from folks, but as a start I will be removing type from inventory_filter and only leverage the unique id while also adding a dependency note in the schema description.

@jingyli jingyli self-assigned this Jun 30, 2026
@jingyli

jingyli commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Closing this PR as #589 will supersede it moving forward.

@jingyli jingyli closed this Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants