Skip to content
Recipe API

Nearby Price Search Can Still Produce Faraway Decisions

A fresh Open Prices geofilter proposal and filter bug fix show how grocery-aware recipe APIs should validate query scope, compute ranking and statistics over the same dataset, and distinguish local price evidence from actual availability.

grocerypricingapi-designsearchmeal-planning

One plausible response can hide a wrong query

A grocery-aware recipe product may ask a seemingly simple question: what would this recipe cost near the user? The implementation often starts by finding nearby stores, collecting their IDs, and requesting prices for those locations. If the endpoint returns 200, ordered items, and a total, the result looks trustworthy.

Fresh Open Prices work shows why that confidence can be misplaced. An open pull request created August 15 proposes adding lat, lon, and radius_km directly to GET /prices. Its production-data investigation found that composing /locations/nearby with a second price request creates oversized URLs, wasted downloads, broken global ordering, and misleading totals. It also found that unknown or incomplete filter parameters can be ignored, turning an intended local query into an unfiltered one.

A separate Open Prices fix merged August 16 corrected a prediction_count__lte filter that pointed at price_count. One copied field name caused the endpoint to raise an internal error instead of filtering price-tag predictions.

The thesis: a grocery-aware recipe API should treat geographic scope, evidence filters, ordering, and aggregates as one validated server-side query plan, because client-side joins and silently ignored filters can return internally valid but decision-wrong meal costs.

Source map and the repeated angle to avoid

Fresh primary evidence from the last seven days:

Source Date Contribution
Open Prices geographic price-filter proposal Created August 15; updated August 16, 2026 Documents the failure of a client-side location-to-price join, proposes grouped parameter validation, applies one predicate to price lists and statistics, and records geography edge cases. The change remains open, so it is evidence and a design proposal—not a shipped capability.
Open Prices prediction_count__lte fix Merged August 16, 2026 Shows how a filter can exist in the public surface but target the wrong model field; adds exact, lower-bound, and upper-bound regression tests.

Older background is limited to the Open Prices project documentation and earlier Recipe API articles on price evidence, outliers, and pagination.

This blog has already argued that nearby endpoints need pagination and stable ordering. Repeating that would add little. The new angle is predicate integrity: the list, total, statistics, ranking, and error behavior must all describe the same scope, and the response must make that scope observable.

Keep candidate scope and price evidence in one plan

The tempting client workflow is:

coordinates + radius
  -> GET nearby locations
  -> collect location IDs
  -> GET prices?location_id__in=...
  -> sort, paginate, and estimate recipe cost

That workflow distributes one logical query across two independently paginated collections. The fresh Open Prices proposal measured the consequences. For Paris, a 20 km lookup produced 671 location IDs and a request line over the server's 4,094-byte limit. At 10 km, the location response was 459 KB while the client retained only 2,491 bytes of IDs—about 0.5 percent of the download.

Batching the IDs does not preserve semantics. If each batch is ordered separately, the client cannot recover the first page of the globally newest or cheapest prices without fetching every batch. The proposal reports that, in one Paris test, the ten newest prices came from locations outside the 20 nearest locations. A nearest-location-first client would begin a month behind the actual newest data. Its displayed total would describe fetched batches, not the full radius.

For a recipe product, this is not merely inefficient. It can change which recipe appears cheapest, which substitute is recommended, and whether the system claims enough local observations to calculate a weekly meal plan. Geographic filtering belongs in the price query before ordering, pagination, and aggregation—not in a client-side post-processing step.

Price scope is not availability scope

Even a correct radius filter answers a narrower question than many product interfaces imply. It says that a price observation is attached to a location within the radius. It does not establish that the product is currently in stock, that the observed offer remains valid, or that enough quantity exists for the recipe.

A durable model separates three layers:

Layer Question Example fields
Candidate scope Which observations are eligible for this query? center, radius, location type, market, currency
Price evidence What was observed, where, and when? product, amount, observed time, proof, quality state
Product decision May this evidence drive a cost, substitution, or cart claim? freshness policy, match confidence, coverage, warnings

A response can expose that distinction without forcing every client to reconstruct the query:

{
  "query": {
    "requested": {"lat": 48.8566, "lon": 2.3522, "radiusKm": 5},
    "appliedFilters": ["geographic_radius", "currency", "observed_since"],
    "scopeStatus": "fully_applied"
  },
  "coverage": {
    "pricedIngredients": 8,
    "unpricedIngredients": 2,
    "observationsUsed": 41
  },
  "estimate": {
    "amount": 18.40,
    "currency": "EUR",
    "status": "local_price_estimate",
    "availabilityClaim": "not_evaluated"
  }
}

scopeStatus and availabilityClaim prevent two different overclaims. The first confirms that the server understood the filter contract. The second stops an observed local price from becoming an unsupported stock promise.

Invalid filters should fail closed

The geofilter proposal describes an especially dangerous default: the filtering library ignores unknown parameters. Before the proposed fields exist, a request such as ?lat=48.85&lon=2.35&radius_km=5 can return every price with 200, just like a request containing an arbitrary parameter. A client that guessed the API shape receives more data than intended, not an error.

The proposal addresses the grouped nature of the query. Latitude, longitude, and radius must be supplied together and be non-empty; partial groups return 400. It also declares latitude, longitude, and radius bounds in the filter schema. That is better than validating each scalar independently, because lat alone is numerically valid but operationally meaningless for this query.

The prediction-count fix exposes another failure class. prediction_count__lte was wired to a field from a different model, so the upper-bound path returned an internal error. The repair was one line, but the added test covers exact, gte, and lte forms. API teams should adopt that pattern for every documented operator rather than assuming similar-looking filters share correct wiring.

A strong filter contract should therefore:

  • reject unknown parameters, or explicitly return them in ignoredParameters with a non-success status;
  • validate parameter groups, not only individual values;
  • echo canonicalized filters and units in the response;
  • return stable error codes such as INCOMPLETE_GEO_SCOPE or UNKNOWN_FILTER;
  • test exact, inclusion, lower-bound, upper-bound, empty, and malformed variants;
  • prevent a failed narrowing filter from broadening the result set.

Lists and aggregates must share the predicate

A recipe-cost UI often fetches both observations and summary statistics. If the list is local but the count, minimum, maximum, or median remains global, the interface can present a coherent-looking contradiction: ten nearby rows backed by a total or range calculated from another population.

The Open Prices proposal routes /prices and /prices/stats through the same filtering function and tests that the count, minimum, and maximum all narrow to the nearby subset. That is the right invariant. The same predicate fingerprint should accompany list responses, statistics, exports, and asynchronous calculations.

{
  "predicateId": "sha256:illustrative-query-fingerprint",
  "asOf": "2026-08-16T12:00:00Z",
  "total": 21540,
  "statistics": {
    "population": "same_as_list_predicate",
    "currency": "EUR"
  }
}

The fingerprint is not a substitute for readable filters. It is a way to detect accidental divergence across services and caches. If a meal-plan estimate is computed asynchronously, store the predicate, data snapshot, and observation IDs or aggregate version that produced it.

Geography has edge cases beyond radius math

The proposed tests explicitly exclude online locations and physical locations without coordinates. They cover zero radius, large radius, invalid coordinate bounds, combined filters, and pagination. The pull request also records an existing antimeridian limitation: a bounding calculation that does not wrap longitude at ±180 degrees can miss two locations that are geographically close but numerically on opposite sides of the boundary.

Recipe and grocery APIs should decide, document, and test:

  • whether online offers participate in local queries;
  • whether missing coordinates are excluded or returned as unknown;
  • whether zero radius means an exact coordinate match;
  • which earth-distance method and coordinate precision are used;
  • how the antimeridian and poles behave;
  • whether distance sorts by store, observation, fulfillment point, or delivery area;
  • how location privacy affects stored and echoed user coordinates;
  • whether a broad radius is capped, rate-limited, or allowed with a cost budget.

The proposal's local benchmark found the server-side subquery faster than the unfiltered first page at its tested data size, with 1 km, 5 km, and 20 km queries measured at 23 ms, 81 ms, and 218 ms over 290,792 prices. Those are useful implementation measurements, not universal capacity promises. API operators still need production cardinality, query-plan, cache, and abuse tests.

Validation matrix for grocery query contracts

Before using geographic prices in recipe ranking or meal planning, run at least this matrix:

Case Expected behavior
Complete valid geo group Filter before ordering, pagination, and statistics
One missing geo parameter 400 with a stable incomplete-scope error
Unknown filter name Reject; never return a silently broader dataset
No coordinates on a store Exclude with documented coverage metadata
Online offer Apply an explicit channel policy, not radius math
Radius crosses ±180° Return geodesically correct matches or a documented unsupported error
Geo plus freshness or quality filter Apply intersection semantics and test the total
Empty local result Return zero local evidence, not a global fallback
List and statistics request Share one predicate and snapshot policy
Cost estimate Report ingredient coverage and avoid an inventory claim

Structured recipe data is what makes these controls actionable. Canonical ingredients, quantities, package mappings, and serving yields let a product connect recipes to price observations without pretending that one barcode, one store, or one price represents the ingredient everywhere.

The practical lesson from this week's Open Prices work is not simply “add latitude and longitude.” It is that local grocery intelligence depends on query truthfulness. A nearby estimate is dependable only when the server can prove which scope it applied, compute every derived value over that scope, and preserve the boundary between observed price and current availability.

Sources

Start Building

One consistent schema on every response. Get a free key and ship in minutes.