OpenPlaces

Filtering results with SQL

Filter search results with a raw SQL WHERE clause evaluated against each place.

Both places.search and places.stream take a where option, a SQL WHERE clause checked against each candidate place as it is discovered — anything that does not match is dropped before it is yielded.

import { OpenPlaces } from "openplaces";

const client = new OpenPlaces();

const places = await client.places.search("restaurants in tokyo", {
  where: "rating >= 4.5 AND reviews > 100",
});

Filtering happens before the limit is counted, so { where, limit: 10 } yields up to ten places that match the clause, not ten places of which some are then discarded.

How it works

Each candidate place is loaded into a one-row SQLite table with one column per Place field, and kept if the clause matches. The clause is spliced in verbatim — anything SQLite accepts as a WHERE expression works, and nothing is escaped, so treat it as code you author. id, name, latitude, and longitude are NOT NULL; the rest may be NULL.

The filter needs a SQLite driver. OpenPlaces tries bun:sqlite, then better-sqlite3, then node:sqlite (built into Node.js 22.13+ and Deno 2.2+). If none of these load, it prints an error and exits the process.

Text columns

String fields are text columns and compare directly:

await client.places.search("coffee in paris", {
  where: "category LIKE '%coffee%' AND website IS NOT NULL",
});

Numeric columns

Numeric fields are real or integer columns and compare directly:

await client.places.search("hotels in lisbon", {
  where: "rating >= 4.5 AND reviews > 100",
});

Boolean columns

Boolean fields are stored as 1 or 0 — compare directly, or use them as truth values:

await client.places.search("museums in berlin", {
  where: "accessible = 1 AND NOT sponsored",
});

Object columns

Object fields are stored as JSON-serialized text, so plain equality would compare the whole serialized string rather than anything inside it — extract a single field with json_extract and compare that:

await client.places.search("bars in madrid", {
  where: "json_extract(hours, '$.status') = 'Open'",
});

Array columns

Array fields are JSON-serialized text as well, so reaching inside means iterating their elements with json_each inside an EXISTS subquery, combined with json_extract when the elements are objects:

await client.places.search("bakeries in vienna", {
  where: "EXISTS (SELECT 1 FROM json_each(categories) WHERE value = 'Cafe')",
});

await client.places.search("restaurants in oslo", {
  where: `EXISTS (
    SELECT 1 FROM json_each(services)
    WHERE json_extract(value, '$.label') = 'Delivery'
      AND json_extract(value, '$.available')
  )`,
});

JSON booleans such as available surface as 1 and 0 when read through json_extract, exactly like the boolean columns above — so the last condition matches only places where delivery is offered.

For the shape of each field as it appears on the yielded objects, see the Place reference.

On this page