Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 0.5.0 - 2026-11-03

Adds `maps(query, ...)` on `Search1API` and `AsyncSearch1API` for `POST /maps`: one Google Maps page of up to 20 places with name, place ID and Maps link, plus address, phone, website, rating, review count, coordinates and opening hours when available. Keyword arguments: `country`, `language`, `location` (`latitude`, `longitude`, optional `zoom`), `page` (1-6, pages after the first need `location`) and `max_results`. 3 credits per completed page; API key or OAuth only. The bundled OpenAPI snapshot also records the 50-item batch limit for search, news and crawl.

## 0.4.0 - 2026-10-12

Breaking: search/news engine-specific fields move from each result's top level to optional `metadata` (`kind`, `stars`, `language`, `num_comments`, `points`, `story_url`). Read e.g. `result.metadata.stars` instead of `result.stars`. `published_date` stays at the top level. No legacy field aliases.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,28 @@ A completed request costs 5 credits. Ask is not available with pay-per-request
payments, and its default timeout is 45 seconds. Use `search` when you already
know which engine and keywords you want.

## Maps

`maps` searches Google Maps and returns one page of up to 20 places. Each place
has a name, place ID and Maps link, plus address, phone, website, rating,
review count, coordinates and opening hours when available:

```python
places = client.maps(
"coffee shops",
location={"latitude": 40.7455, "longitude": -74.0083, "zoom": 15},
max_results=10,
)

for place in places["results"]:
print(place["name"], place.get("rating"), place.get("address"))
```

A completed page costs 3 credits, including a confirmed empty page.
`max_results` only trims the page and does not lower the price. Request `page`
2-6 with the same query and `location` for more places; each page is billed
separately. Maps is not available with pay-per-request payments.

## Deepcrawl

`deepcrawl` starts a task and waits for it to finish:
Expand Down
404 changes: 398 additions & 6 deletions openapi/search1api.openapi.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "search1api"
version = "0.4.0"
version = "0.5.0"
description = "Official Python client for Search1API"
readme = "README.md"
license = { file = "LICENSE" }
Expand Down
2 changes: 1 addition & 1 deletion src/search1api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@
"UnprocessableEntityError",
]

__version__ = "0.4.0"
__version__ = "0.5.0"
77 changes: 77 additions & 0 deletions src/search1api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
FeedbackCategory,
FeedbackResponse,
HealthResponse,
MapsLocation,
MapsResponse,
NewsEngine,
NewsRequest,
NewsResponse,
Expand Down Expand Up @@ -64,6 +66,27 @@ def _compact(payload: Mapping[str, Any]) -> Dict[str, Any]:
return {key: value for key, value in payload.items() if value is not None}


def _maps_payload(
query: str,
*,
country: Optional[str],
language: Optional[str],
location: Optional[MapsLocation],
page: Optional[int],
max_results: Optional[int],
) -> Dict[str, Any]:
return _compact(
{
"query": query,
"country": country,
"language": language,
"location": dict(location) if location is not None else None,
"page": page,
"max_results": max_results,
}
)


def _search_payload(
query: str,
*,
Expand Down Expand Up @@ -321,6 +344,30 @@ def ask(self, query: str, *, timeout: float = DEFAULT_ASK_TIMEOUT) -> AskRespons
self._request_json("POST", "/ask", json={"query": query}, timeout=timeout),
)

def maps(
self,
query: str,
*,
country: Optional[str] = None,
language: Optional[str] = None,
location: Optional[MapsLocation] = None,
page: Optional[int] = None,
max_results: Optional[int] = None,
) -> MapsResponse:
"""Google Maps place search: one page of up to 20 places.

A completed page costs 3 credits. Pages after the first need ``location``.
"""
payload = _maps_payload(
query,
country=country,
language=language,
location=location,
page=page,
max_results=max_results,
)
return cast(MapsResponse, self._request_json("POST", "/maps", json=payload))

def crawl(
self, url: str, *, enable_fallback: Optional[bool] = None
) -> CrawlResponse:
Expand Down Expand Up @@ -683,6 +730,36 @@ async def ask(
),
)

async def maps(
self,
query: str,
*,
country: Optional[str] = None,
language: Optional[str] = None,
location: Optional[MapsLocation] = None,
page: Optional[int] = None,
max_results: Optional[int] = None,
) -> MapsResponse:
"""Google Maps place search: one page of up to 20 places.

A completed page costs 3 credits. Pages after the first need ``location``.
"""
return cast(
MapsResponse,
await self._request_json(
"POST",
"/maps",
json=_maps_payload(
query,
country=country,
language=language,
location=location,
page=page,
max_results=max_results,
),
),
)

async def crawl(
self, url: str, *, enable_fallback: Optional[bool] = None
) -> CrawlResponse:
Expand Down
64 changes: 64 additions & 0 deletions src/search1api/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,70 @@ class AskResponse(TypedDict):
errors: List[AskError]


class MapsLocationRequired(TypedDict):
latitude: float
longitude: float


class MapsLocation(MapsLocationRequired, total=False):
zoom: float


class MapsCoordinates(TypedDict):
latitude: float
longitude: float


class MapsOpeningHours(TypedDict, total=False):
monday: str
tuesday: str
wednesday: str
thursday: str
friday: str
saturday: str
sunday: str


class MapsPlaceRequired(TypedDict):
position: int
name: str
place_id: str
maps_url: str


class MapsPlace(MapsPlaceRequired, total=False):
address: str
phone: str
website: str
thumbnail: str
category: str
categories: List[str]
rating: float
review_count: int
coordinates: MapsCoordinates
opening_hours: MapsOpeningHours
open_state: str
price: str
description: str


class MapsParametersRequired(TypedDict):
query: str
country: str
language: str
page: int
max_results: int


class MapsParameters(MapsParametersRequired, total=False):
location: MapsLocation


class MapsResponse(TypedDict):
searchParameters: MapsParameters
results: List[MapsPlace]


FeedbackCategory = Literal["bug", "feature_request", "docs", "other"]


Expand Down
77 changes: 77 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,82 @@ def handler(request):
http_client.close()


def test_maps_sends_location_and_paging():
seen = []
body = {
"searchParameters": {
"query": "coffee shops",
"country": "us",
"language": "en",
"location": {"latitude": 40.7455, "longitude": -74.0083, "zoom": 15},
"page": 2,
"max_results": 5,
},
"results": [
{
"position": 21,
"name": "Example Coffee",
"place_id": "ChIJ123",
"maps_url": "https://www.google.com/maps/search/?api=1&query=Example",
"rating": 4.6,
}
],
}

def handler(request):
seen.append(request)
return json_response(200, body, request)

http_client = httpx.Client(transport=httpx.MockTransport(handler))
client = Search1API("test-key", client=http_client)

response = client.maps(
"coffee shops",
location={"latitude": 40.7455, "longitude": -74.0083, "zoom": 15},
page=2,
max_results=5,
)

assert response == body
assert str(seen[0].url) == "https://api.search1api.com/maps"
assert json.loads(seen[0].content) == {
"query": "coffee shops",
"location": {"latitude": 40.7455, "longitude": -74.0083, "zoom": 15},
"page": 2,
"max_results": 5,
}
http_client.close()


def test_async_maps():
async def run():
async def handler(request):
assert json.loads(request.content) == {"query": "ramen", "country": "jp"}
return json_response(
200,
{
"searchParameters": {
"query": "ramen",
"country": "jp",
"language": "en",
"page": 1,
"max_results": 20,
},
"results": [],
},
request,
)

http_client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
client = AsyncSearch1API("test-key", client=http_client)
try:
assert (await client.maps("ramen", country="jp"))["results"] == []
finally:
await http_client.aclose()

asyncio.run(run())


def test_feedback_is_sent_once_without_retries():
calls = []

Expand Down Expand Up @@ -409,6 +485,7 @@ def test_sdk_covers_every_public_openapi_operation():
"extract": "extract",
"feedback": "feedback",
"health": "health",
"maps": "maps",
"news": "news",
"search": "search",
"screenshot": "screenshot",
Expand Down
Loading