H1Places Research AppScript Reference
Status: reference manual for future post-launch Next.js re-versioning.
This preserves the current Google Places AppScript pattern for targeted provider discovery, operational lists, market research, and campaign planning. It is not wired into the Next.js app yet. Use it later as a model for a guarded in-app Places research workflow.
H2Why Keep This
- It uses Google Places Text Search to find businesses/locations from a query.
- It fetches Place Details for website, rating, phone, and address.
- It builds a direct Google Maps URL from
place_id. - It attempts website scraping for public emails and usernames.
- It appends results into a
Placessheet for operational review. - It includes
LAST_QUERYprotection to avoid rerunning the same sheet query.
H2Intended Future Use
This is useful for:
- Per-city trip planning options.
- Partner/provider discovery.
- Hotel, restaurant, venue, guide, and local business research.
- Marketing campaign list building.
- Operations contact cleanup.
- Trip modal options such as "more providers in this city" or "nearby partner candidates".
H2Future In-App Remodel Target
Potential files:
lib/places/places-client.ts
app/api/places/search/route.ts
app/api/places/details/route.ts
app/api/places/research/route.ts
Potential Prisma/cache direction:
PlaceResearchResult (future optional)
or existing data Json? on City, Experience, Project, TripCity, Space, or ProjectContact
Recommended constraints:
- Search must be granular: one city + one category/query at a time.
- Default result cap should be small, for example 5-10 results.
- Fetch Place Details only for selected/expanded results, not every result automatically.
- Website/email scraping should be explicit and separate from the initial search.
- Cache by normalized query + city + region + category.
- Show quota/cost warning before calling Places API or details endpoints.
- Avoid
next_page_tokencrawling unless the user explicitly asks for more.
H2Reference AppScript
function fetchPlaces_() {
var places = Places_;
var eventConfig = EVENT_HANDLERS.fetchPlaces;
try {
eventConfig[0](); // Pre-processing function
initialseEventHandler(eventConfig[1]); // Setup handler
places.fetchPlaces(); // Main action
Bblog.log("Places fetched successfully!"); // Success log
} catch (error) {
handleError(error, eventConfig[2], Log_);
}
}
/**
* Handle errors using Assert and Log_
* @param {Error} error
* @param {string} errorMessage
* @param {Logger} log
*/
function handleError(error, errorMessage, log) {
Assert.handleError(error, errorMessage, log);
}
var Places_ = (function (ns) {
ns.createQueryString = function (params) {
return Object.keys(params).map(function (key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
}).join('&');
};
ns.fetchPlaces = function () {
var sheetCheck = "Places";
var activeSheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var apiKey = Config_.GOOGLE_MAPS_API_KEY_;
var baseUrl = "https://maps.googleapis.com/maps/api/place/textsearch/json";
var querySheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetCheck);
var query = querySheet.getRange("A1").getValue();
Logger.log(query);
var lastQuery = PROPERTIES.getProperty('LAST_QUERY');
if (lastQuery == query) {
Logger.log("Query hasn't changed. Exiting function.");
return;
}
var params = {
key: apiKey,
query: query
};
var hasNextPage = true;
while (hasNextPage) {
var response = UrlFetchApp.fetch(baseUrl + "?" + ns.createQueryString(params));
var json = JSON.parse(response.getContentText());
for (var i = 0; i < json.results.length; i++) {
var result = json.results[i];
var name = result.name;
var location = result.formatted_address || "Not Available";
var placeId = result.place_id;
var url = `https://www.google.com/maps/place/?q=place_id:${placeId}`;
Logger.log(name);
Logger.log(location);
Logger.log(placeId);
var detailedData = ns.getPlaceDetails(placeId);
if (detailedData) {
var [website, emails, rating, phone, usernames] = detailedData;
querySheet.appendRow([url, name, location, website, emails, rating, phone, usernames]);
} else {
querySheet.appendRow([name, url, location]);
}
}
if (json.next_page_token) {
params.pagetoken = json.next_page_token;
} else {
hasNextPage = false;
}
}
PROPERTIES.setProperty('LAST_QUERY', query);
};
ns.getPlaceDetails = function (placeId) {
var baseUrl = "https://maps.googleapis.com/maps/api/place/details/json";
var setup = {
place_id: placeId,
key: Config_.GOOGLE_MAPS_API_KEY,
fields: "name,rating,formatted_phone_number,website,formatted_address"
};
var response = UrlFetchApp.fetch(baseUrl + "?" + ns.createQueryString(setup));
var json = JSON.parse(response.getContentText());
if (json.status === "OK") {
var result = json.result;
var rating = result.rating;
var phone = result.formatted_phone_number;
var website = result.website;
var extractedEmails = ns.extractEmailsFromWebsite(website);
var usernames = ns.extractUsernamesFromWebsite(website);
var emails = (typeof extractedEmails === 'object') ? extractedEmails.join(', ') : extractedEmails;
if (phone && phone.startsWith('6')) {
phone = phone.replace(/\D/g, '');
phone = "https://wa.me/0034" + phone;
}
return [website, emails, rating, phone, usernames];
} else {
Logger.log("Failed to fetch details for place_id: " + placeId);
return null;
}
};
ns.extractEmailsFromWebsite = function (url) {
try {
var response = UrlFetchApp.fetch(url);
var content = response.getContentText();
var emailPattern = /[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}/g;
var foundEmails = content.match(emailPattern);
return foundEmails || "No emails found!, please add manually";
} catch (e) {
return "Failed to fetch site content for email extraction.";
}
};
ns.fetchHotelRatings = function () {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
var hotelName = data[i][0];
var rating = ns.getHotelRating(hotelName);
if (rating) {
sheet.getRange(i + 1, 3).setValue(rating);
}
}
};
ns.getHotelRating = function (hotelName) {
var apiKey = Config_.GOOGLE_MAPS_API_KEY_;
var baseUrl = 'https://maps.googleapis.com/maps/api/place/findplacefromtext/json?';
var queryParams = {
key: apiKey,
input: hotelName,
inputtype: 'textquery',
fields: 'rating'
};
var response = UrlFetchApp.fetch(baseUrl + ns.toQueryString(queryParams));
var jsonResponse = JSON.parse(response.getContentText());
if (jsonResponse.candidates && jsonResponse.candidates.length > 0) {
return jsonResponse.candidates[0].rating;
}
return null;
};
ns.getEmail = function (url) {
try {
var response = UrlFetchApp.fetch(url);
var content = response.getContentText();
var emailPattern = /[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}/g;
var foundEmails = content.match(emailPattern);
return foundEmails || "No emails found!, please add manually";
} catch (e) {
return "Failed to fetch site content.";
}
};
ns.getSocial = function (url) {
if (url) {
try {
var response = UrlFetchApp.fetch(url);
var content = response.getContentText();
var regex = /@\w+/g;
var matches = content.match(regex);
return matches && matches.length > 0 ? matches[0] : null;
} catch (e) {
Logger.log("Error fetching URL: " + url + ". Error: " + e.toString());
return null;
}
}
return null;
};
ns.toQueryString = function (params) {
return Object.keys(params)
.map(function (k) {
return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]);
})
.join('&');
};
return ns;
})(Places_ || {});
H2Next.js Guardrails
- Do not copy the current full-pagination behavior directly into the app.
- Do not fetch details for every result by default.
- Do not scrape websites automatically as part of search.
- Split search, details, and enrichment into separate explicit actions.
- Use small limits, cache keys, and city/category context.
- Treat emails and usernames as unverified operational leads, not customer data.