Skip to content

Commit 88e406a

Browse files
cablateclaude
andauthored
feat: dual-sort reviews — merge relevant + newest for ~10 reviews per place (#64)
Google Places API (New) returns max 5 reviews sorted by relevance. This change fetches an additional 5 newest reviews via Legacy Place Details API (which supports reviews_sort=newest), then merges and deduplicates by author+timestamp. Result: place_details now returns ~10 reviews instead of 5, with both high-quality relevant reviews and the most recent ones — enabling better AI context classification and trend detection. Also fixes Prettier formatting inconsistencies from PR #63 squash merge. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9a68e50 commit 88e406a

File tree

1 file changed

+53
-5
lines changed

1 file changed

+53
-5
lines changed

src/services/NewPlacesService.ts

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Logger } from "../index.js";
33

44
export class NewPlacesService {
55
private client: PlacesClient;
6+
private readonly apiKey: string;
67
private readonly defaultLanguage: string = "en";
78
private readonly placeFieldMask: string = [
89
"displayName",
@@ -78,11 +79,10 @@ export class NewPlacesService {
7879
"places.priceLevel",
7980
].join(",");
8081
constructor(apiKey?: string) {
81-
this.client = new PlacesClient({
82-
apiKey: apiKey || process.env.GOOGLE_MAPS_API_KEY || "",
83-
});
82+
this.apiKey = apiKey || process.env.GOOGLE_MAPS_API_KEY || "";
83+
this.client = new PlacesClient({ apiKey: this.apiKey });
8484

85-
if (!apiKey && !process.env.GOOGLE_MAPS_API_KEY) {
85+
if (!this.apiKey) {
8686
throw new Error("Google Maps API Key is required");
8787
}
8888
}
@@ -213,13 +213,61 @@ export class NewPlacesService {
213213
}
214214
);
215215

216-
return this.transformPlaceResponse(place);
216+
// Fetch newest reviews via REST (gRPC SDK doesn't support reviews_sort)
217+
const newestReviews = await this.fetchNewestReviews(placeId);
218+
219+
// Merge: relevant (default) + newest, deduplicate by author+time
220+
const allReviews = this.mergeReviews(place?.reviews || [], newestReviews);
221+
const merged = { ...place, reviews: allReviews };
222+
223+
return this.transformPlaceResponse(merged);
217224
} catch (error: any) {
218225
Logger.error("Error in getPlaceDetails (New API):", error);
219226
throw new Error(`Failed to get place details for ${placeId}: ${this.extractErrorMessage(error)}`);
220227
}
221228
}
222229

230+
private async fetchNewestReviews(placeId: string): Promise<any[]> {
231+
try {
232+
// Use Legacy Place Details API which supports reviews_sort=newest
233+
// (Places API New does not support this parameter)
234+
const url = `https://maps.googleapis.com/maps/api/place/details/json?place_id=${placeId}&fields=reviews&reviews_sort=newest&language=${this.defaultLanguage}&key=${this.apiKey}`;
235+
const response = await fetch(url);
236+
if (!response.ok) return [];
237+
const data = await response.json();
238+
if (data.status !== "OK") return [];
239+
// Transform Legacy format to match New API format for mergeReviews
240+
return (data.result?.reviews || []).map((r: any) => ({
241+
rating: r.rating,
242+
text: { text: r.text || "", languageCode: r.language || null },
243+
publishTime: { seconds: r.time },
244+
authorAttribution: { displayName: r.author_name || "" },
245+
}));
246+
} catch {
247+
return [];
248+
}
249+
}
250+
251+
private mergeReviews(relevant: any[], newest: any[]): any[] {
252+
const seen = new Set<string>();
253+
const merged: any[] = [];
254+
255+
for (const review of [...relevant, ...newest]) {
256+
const author = review?.authorAttribution?.displayName || "";
257+
const time = String(review?.publishTime?.seconds || "");
258+
const key = `${author}|${time}`;
259+
if (!key || key === "|") {
260+
merged.push(review);
261+
continue;
262+
}
263+
if (seen.has(key)) continue;
264+
seen.add(key);
265+
merged.push(review);
266+
}
267+
268+
return merged;
269+
}
270+
223271
private transformSearchResult(place: any) {
224272
return {
225273
name: place.displayName?.text || "",

0 commit comments

Comments
 (0)