A TypeScript library for finding businesses without proper websites using the Google Maps API. Perfect for identifying potential clients for web development services or market research.
- π Search for businesses by type within a specified radius
- π Identify businesses with no website or only social media presence
- π Get detailed place information including ratings, reviews, and contact details
- π Multi-location concurrent search - Search multiple cities simultaneously
- β‘ Configurable search parameters and batch processing
- π§ TypeScript support with full type definitions
- π¦ Clean API suitable for React/Vue/Angular applications
npm install @business-finder/core
# or
yarn add @business-finder/core
# or
pnpm add @business-finder/coreimport { BusinessFinder } from '@business-finder/core';
const finder = new BusinessFinder('YOUR_GOOGLE_MAPS_API_KEY', {
location: { lat: 49.8220544, lng: 19.0319995 }, // Bielsko-BiaΕa, Poland
radius: 20000, // 20km radius
businessTypes: ['restaurant', 'cafe', 'bar']
});
// Find businesses without websites
const businessesWithoutWebsites = await finder.findBusinessesWithoutWebsites();
// Find all businesses (regardless of website status)
const allBusinesses = await finder.findAllBusinesses();
console.log('Found', businessesWithoutWebsites.length, 'businesses without proper websites');import { findAllBusinessesMultiLocation } from '@business-finder/core';
// Search multiple cities concurrently with different radius for each
const results = await findAllBusinessesMultiLocation('YOUR_GOOGLE_MAPS_API_KEY', {
locations: [
{
location: { lat: 50.0647, lng: 19.9450 }, // Krakow
radius: 15000, // 15km
name: 'Krakow'
},
{
location: { lat: 52.2297, lng: 21.0122 }, // Warsaw
radius: 25000, // 25km
name: 'Warsaw'
},
{
location: { lat: 54.3520, lng: 18.6466 }, // Gdansk
radius: 10000, // 10km
name: 'Gdansk'
}
],
businessTypes: ['restaurant', 'cafe', 'bar'],
batchSize: 10
});
// Results include which city each business was found in
results.forEach(business => {
console.log(`${business.name} in ${business.searchLocation}`);
});Perfect for searching multiple cities at once:
import {
findAllBusinessesMultiLocation,
findBusinessesWithoutWebsitesMultiLocation
} from '@business-finder/core';
// Find all businesses across multiple locations
const allBusinesses = await findAllBusinessesMultiLocation('YOUR_API_KEY', {
locations: [
{ location: { lat: 40.7128, lng: -74.0060 }, radius: 20000, name: 'NYC' },
{ location: { lat: 34.0522, lng: -118.2437 }, radius: 15000, name: 'LA' }
],
businessTypes: ['restaurant', 'gym']
});
// Find businesses without websites across multiple locations
const businessesWithoutWebsites = await findBusinessesWithoutWebsitesMultiLocation('YOUR_API_KEY', {
locations: [
{ location: { lat: 40.7128, lng: -74.0060 }, radius: 20000, name: 'NYC' },
{ location: { lat: 34.0522, lng: -118.2437 }, radius: 15000, name: 'LA' }
],
businessTypes: ['restaurant', 'cafe']
});const finder = new BusinessFinder(apiKey: string, options?: Partial<SearchOptions>);findBusinessesWithoutWebsites(overrides?: Partial<SearchOptions>): Promise<BusinessResult[]>findAllBusinesses(overrides?: Partial<SearchOptions>): Promise<BusinessResult[]>searchMultipleLocations(options: MultiLocationSearchOptions, mode: 'no-website' | 'all'): Promise<BusinessResult[]>updateConfig(updates: Partial<SearchOptions>): voidgetConfig(): Required<SearchOptions>
interface SearchOptions {
location?: { lat: number; lng: number };
radius?: number; // in meters
apiKey?: string;
businessTypes?: BusinessType[];
socialMediaDomains?: string[];
batchSize?: number;
batchDelay?: number; // in milliseconds
}interface MultiLocationSearchOptions {
locations: LocationWithRadius[];
apiKey?: string;
businessTypes?: BusinessType[];
socialMediaDomains?: string[];
batchSize?: number;
batchDelay?: number;
}
interface LocationWithRadius {
location: { lat: number; lng: number };
radius: number;
name?: string; // Optional name for the location
}interface BusinessResult {
name: string;
type: string;
hasNoWebsite: boolean;
hasSocialOnly: boolean;
website?: string;
address?: string;
phone?: string;
rating?: number;
totalRatings?: number;
latLng?: { lat: number; lng: number };
place_id?: string;
searchLocation?: string; // Which location this result came from
}import React, { useState } from 'react';
import { findAllBusinessesMultiLocation, BusinessResult } from '@business-finder/core';
function MultiCityBusinessSearch() {
const [businesses, setBusinesses] = useState<BusinessResult[]>([]);
const [loading, setLoading] = useState(false);
const searchBusinesses = async () => {
setLoading(true);
try {
const results = await findAllBusinessesMultiLocation(
process.env.REACT_APP_GOOGLE_MAPS_API_KEY!,
{
locations: [
{ location: { lat: 40.7128, lng: -74.0060 }, radius: 15000, name: 'New York' },
{ location: { lat: 34.0522, lng: -118.2437 }, radius: 20000, name: 'Los Angeles' },
{ location: { lat: 41.8781, lng: -87.6298 }, radius: 18000, name: 'Chicago' }
],
businessTypes: ['restaurant', 'cafe', 'bar'],
batchSize: 8
}
);
setBusinesses(results);
} catch (error) {
console.error('Search failed:', error);
} finally {
setLoading(false);
}
};
// Group businesses by city
const businessesByCity = businesses.reduce((acc, business) => {
const city = business.searchLocation || 'Unknown';
if (!acc[city]) acc[city] = [];
acc[city].push(business);
return acc;
}, {} as Record<string, BusinessResult[]>);
return (
<div>
<button onClick={searchBusinesses} disabled={loading}>
{loading ? 'Searching...' : 'Search Multiple Cities'}
</button>
{Object.entries(businessesByCity).map(([city, cityBusinesses]) => (
<div key={city}>
<h2>{city} ({cityBusinesses.length} businesses)</h2>
{cityBusinesses.map(business => (
<div key={business.place_id}>
<h3>{business.name}</h3>
<p>{business.address}</p>
<p>Rating: {business.rating}/5 ({business.totalRatings} reviews)</p>
</div>
))}
</div>
))}
</div>
);
}Perfect for your use case with interactive maps:
import { findBusinessesWithoutWebsitesMultiLocation } from '@business-finder/core';
// User selects cities on map with custom radius for each
const selectedLocations = [
{ location: { lat: 50.0647, lng: 19.9450 }, radius: 12000, name: 'Krakow' },
{ location: { lat: 51.1079, lng: 17.0385 }, radius: 8000, name: 'Wroclaw' },
{ location: { lat: 52.4064, lng: 16.9252 }, radius: 6000, name: 'Poznan' }
];
// Fetch businesses for all selected locations
const businesses = await findBusinessesWithoutWebsitesMultiLocation(apiKey, {
locations: selectedLocations,
businessTypes: ['restaurant', 'cafe', 'gym', 'beauty_salon']
});
// Save to IndexedDB
await saveToIndexedDB(businesses);The package works perfectly on the backend too. Here's a Hono server example:
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import {
findAllBusinessesMultiLocation,
findBusinessesWithoutWebsitesMultiLocation,
MultiLocationSearchOptions
} from '@business-finder/core';
const app = new Hono()
.use('*', cors())
// Search businesses across multiple locations
.post('/api/businesses/search',
zValidator('json', z.object({
locations: z.array(z.object({
location: z.object({ lat: z.number(), lng: z.number() }),
radius: z.number().min(1000).max(50000),
name: z.string().optional()
})),
businessTypes: z.array(z.string()).optional(),
mode: z.enum(['all', 'no-website']).default('no-website')
})),
async (c) => {
try {
const { locations, businessTypes, mode } = c.req.valid('json');
const searchOptions: MultiLocationSearchOptions = {
locations,
businessTypes: businessTypes || ['restaurant', 'cafe', 'gym'],
batchSize: 8
};
const results = mode === 'all'
? await findAllBusinessesMultiLocation(process.env.GOOGLE_MAPS_API_KEY!, searchOptions)
: await findBusinessesWithoutWebsitesMultiLocation(process.env.GOOGLE_MAPS_API_KEY!, searchOptions);
// Group results by location for easier frontend consumption
const resultsByLocation = results.reduce((acc, business) => {
const location = business.searchLocation || 'unknown';
if (!acc[location]) acc[location] = [];
acc[location].push(business);
return acc;
}, {} as Record<string, typeof results>);
return c.json({
success: true,
totalResults: results.length,
resultsByLocation,
allResults: results
});
} catch (error) {
console.error('Search error:', error);
return c.json({
error: 'Search failed',
message: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
})
// Get businesses for a single location (quick search)
.post('/api/businesses/search-single',
zValidator('json', z.object({
lat: z.number(),
lng: z.number(),
radius: z.number().min(1000).max(50000).default(15000),
businessTypes: z.array(z.string()).default(['restaurant'])
})),
async (c) => {
try {
const { lat, lng, radius, businessTypes } = c.req.valid('json');
const results = await findBusinessesWithoutWebsitesMultiLocation(
process.env.GOOGLE_MAPS_API_KEY!,
{
locations: [{ location: { lat, lng }, radius, name: 'Search Location' }],
businessTypes
}
);
return c.json({
success: true,
count: results.length,
businesses: results
});
} catch (error) {
return c.json({
error: 'Search failed',
message: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
})
// Health check endpoint
.get('/api/health', (c) => {
return c.json({ status: 'ok', timestamp: new Date().toISOString() });
});
export default app;
export type AppType = typeof app;Frontend Integration with Hono RPC:
import { hc } from 'hono/client';
import type { AppType } from './server'; // Your server types
// Create type-safe client
const client = hc<AppType>('/');
// Frontend code with full type safety
async function searchBusinesses(selectedLocations: LocationWithRadius[]) {
const response = await client.api.businesses.search.$post({
json: {
locations: selectedLocations,
businessTypes: ['restaurant', 'cafe', 'gym'],
mode: 'no-website'
}
});
if (response.ok) {
const data = await response.json();
// Save to IndexedDB
await saveToIndexedDB(data.allResults);
// Update UI with results grouped by location
updateMapMarkers(data.resultsByLocation);
}
}
// Quick single location search
async function quickSearch(lat: number, lng: number, radius: number) {
const response = await client.api.businesses['search-single'].$post({
json: { lat, lng, radius, businessTypes: ['cafe', 'restaurant'] }
});
if (response.ok) {
const data = await response.json();
return data.businesses; // Fully typed!
}
}Environment Setup:
# .env
GOOGLE_MAPS_API_KEY=your_api_key_hereSupported business types include:
restaurant,cafe,barcar_repair,car_dealer,car_washgym,beauty_salon,hair_careclothing_store,shoe_store,jewelry_storedentist,doctor,veterinary_care- And many more...
See the full list in the PlaceType2 type definition.
- Node.js 18 or higher
- Google Maps API key with Places API enabled
- TypeScript 5.2+ (for development)
- Go to the Google Cloud Console
- Create a new project or select an existing one
- Enable the "Places API"
- Create credentials (API key)
- (Optional) Restrict the API key to your domains/IP addresses
MIT
Contributions are welcome! Please feel free to submit a Pull Request.