@ciphercross/nestjs-location
v1.0.1
Published
NestJS module for location-based functionality including distance calculation, filtering, nearest search, clustering, and reverse geocoding
Readme
@ciphercross/nestjs-location
NestJS module for location-based functionality including distance calculation, filtering, nearest search, clustering, and reverse geocoding.
Provides geospatial utilities, distance calculations, location clustering, and reverse geocoding out of the box.
✨ Features
🗺️ Distance calculation using Haversine formula
🔍 Nearest services/partners search
📍 Location filtering by radius
⚡ Optimized algorithms - Efficient in-memory filtering and sorting
🎯 Framework-agnostic utilities
🔌 Simple setup - no adapters or database configuration needed
👥 User clustering based on geographic proximity
🌍 Reverse geocoding with OpenStreetMap Nominatim API
📦 Installation
npm install @ciphercross/nestjs-locationHow It Works
This package provides in-memory location utilities - it works with arrays of data you provide:
Your Project Package
┌─────────────┐ ┌─────────────┐
│ Your Data │───passes──────▶│ Location │
│ (arrays) │ │ Service │
│ │ │ (logic) │
└──────┬──────┘ └──────┬──────┘
│ │
│ Gets data from DB │ Calculates distances
│ │ Filters & sorts
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Your DB │ │ Results │
│ (Prisma/ │ │ (filtered) │
│ TypeORM) │ └─────────────┘
└─────────────┘Key point: You fetch data from your database, then pass it to the service for location calculations.
🚀 Quick Start
1. Import the Module
import { Module } from '@nestjs/common';
import { LocationModule } from '@ciphercross/nestjs-location';
@Module({
imports: [
LocationModule.forRoot(), // Simple setup - no adapters needed
],
})
export class AppModule {}2. Use the Service
import { Injectable } from '@nestjs/common';
import { LocationService, SortBy } from '@ciphercross/nestjs-location';
@Injectable()
export class MyService {
constructor(private readonly locationService: LocationService) {}
async findNearbyServices(items: any[], lat: number, lng: number, radius: number) {
// Find nearest items within radius
return this.locationService.findNearest(items, lat, lng, radius, {
page: 1,
limit: 20,
sortBy: SortBy.DISTANCE, // or SortBy.NAME
});
}
}Performance Tips
For large datasets (10,000+ records), consider pre-filtering data at the database level before passing to the service:
// ✅ Good: Pre-filter by city/region before location calculations
const cityServices = await prisma.service.findMany({
where: {
location: {
city: 'Kyiv', // Filter at DB level first
},
},
include: { location: true },
});
// Then use LocationService for precise radius filtering
const result = locationService.findNearest(
cityServices, // Pre-filtered data
userLat,
userLng,
10 // km radius
);Performance:
- Small datasets (< 1,000 records): Fast, no optimization needed
- Large datasets (10,000+ records): Pre-filter at database level, then use LocationService for precise filtering
Using Utilities Directly
You can also use the utilities directly without the full module:
import { calculateDistance, DistanceFilterService } from '@ciphercross/nestjs-location';
// Calculate distance between two points
const distance = calculateDistance(40.7128, -74.006, 34.0522, -118.2437);
// Returns distance in kilometers
// Filter items by distance
const filterService = new DistanceFilterService();
const filteredItems = filterService.applyDistanceFilter(
items,
40.7128, // user lat
-74.006, // user lng
10, // max distance in km
);Reverse Geocoding (City Lookup)
Get city information by coordinates using OpenStreetMap Nominatim API:
import { getCityByCoordinates, GeocodingResultFormat } from '@ciphercross/nestjs-location';
// Get city info (default format)
const cityInfo = await getCityByCoordinates(50.4501, 30.5234);
// Returns: { city: 'Kyiv', country: 'Ukraine', countryCode: 'UA', ... }
// Get full Nominatim response
const fullResponse = await getCityByCoordinates(50.4501, 30.5234, {
format: GeocodingResultFormat.FULL,
});
// Get formatted address string
const address = await getCityByCoordinates(50.4501, 30.5234, {
format: GeocodingResultFormat.ADDRESS,
});
// Returns: 'Kyiv, Ukraine'
// With custom options
const result = await getCityByCoordinates(50.4501, 30.5234, {
format: GeocodingResultFormat.CITY,
language: 'uk', // Ukrainian language
cacheTtl: 3600000, // Cache for 1 hour
});Features:
- Automatic caching (24 hours by default)
- Rate limiting (respects Nominatim API limits)
- Multiple result formats (city info, full response, address string)
- Language support
- Custom API endpoint support
Note: The public Nominatim API has a rate limit of 1 request per second. For production use with high volume, consider setting up your own Nominatim server.
Clustering
Group users into clusters based on their geographic locations:
import { ClusteringService } from '@ciphercross/nestjs-location';
const clusteringService = new ClusteringService();
// With array of users
const users = [
{ id: '1', lat: 50.4501, lng: 30.5234 },
{ id: '2', lat: 50.4547, lng: 30.5238 },
{ id: '3', lat: 40.7128, lng: -74.006 },
];
// Create clusters with default radius (50km)
const clusters = clusteringService.createClusters(users);
// With custom radius
const clustersCustom = clusteringService.createClusters(users, { radiusKm: 25 });
// With single user
const singleUser = { id: '1', lat: 50.4501, lng: 30.5234 };
const singleCluster = clusteringService.createClusters(singleUser);Features:
- Groups users into clusters based on proximity
- Configurable cluster radius (default: 50km)
- Accepts single user or array of users
- Cluster center is fixed after creation (first user's coordinates)
API Reference
LocationService
findNearest<T extends LocationItem>(items: T[], userLat: number, userLng: number, radiusKm: number, options?: FindNearestOptions): FindNearestResult<T>
Note: LocationService extends LocationCoreService and provides location-based utilities. All calculations are performed in-memory on the data you provide.
LocationCoreService
findNearest<T extends LocationItem>(items: T[], userLat: number, userLng: number, radiusKm: number, options?: FindNearestOptions): FindNearestResult<T>filterByRadius<T extends LocationItem>(items: T[], userLat: number, userLng: number, radiusKm: number): Array<T & { distance: number }>sortByDistance<T extends LocationItem>(items: T[], userLat: number, userLng: number): Array<T & { distance: number }>calculateDistanceForItem<T extends LocationItem>(item: T, userLat: number, userLng: number): number | null
DistanceFilterService
applyDistanceFilter<T>(items: T[], lat?: number, lng?: number, maxDistance?: number): T[]calculateDistancesForItems<T>(items: T[], userLat: number, userLng: number): T[]
ClusteringService
createClusters(users: UserWithLocation | UserWithLocation[], options?: ClusteringOptions): Cluster[]addUserToCluster(user: UserWithLocation, clusters: Cluster[], radiusKm?: number): Cluster[]findNearestCluster(user: UserWithLocation, clusters: Cluster[], radiusKm: number): Cluster | null
Utilities
calculateDistance(lat1: number, lng1: number, lat2: number, lng2: number): number- Calculate distance between two coordinates using Haversine formulatoRadians(degrees: number): number- Convert degrees to radiansisValidCoordinate(lat: number, lng: number): boolean- Validate if coordinates are within valid rangesextractCoordinates(item: any): Coordinates | null- Extract coordinates from an object (supports multiple formats)getCityByCoordinates(lat: number, lng: number, options?: GeocodingOptions): Promise<ReverseGeocodingResult>- Get city information by coordinatesclearGeocodingCache(): void- Clear the geocoding cachegetCacheStats(): { size: number; maxSize: number }- Get geocoding cache statistics
Constants and Enums
Enums
SortBy- Sort options enumSortBy.DISTANCE- Sort by distanceSortBy.NAME- Sort by name
GeocodingResultFormat- Geocoding result format enumGeocodingResultFormat.FULL- Full Nominatim responseGeocodingResultFormat.CITY- City info objectGeocodingResultFormat.ADDRESS- Formatted address string
Constants
Geographic Constants:
EARTH_RADIUS_KM- Earth's radius in kilometers (6371)DEFAULT_RADIUS_KM- Default search radius (10 km)MIN_LATITUDE,MAX_LATITUDE- Valid latitude range (-90 to 90)MIN_LONGITUDE,MAX_LONGITUDE- Valid longitude range (-180 to 180)
Pagination Constants:
DEFAULT_PAGE- Default page number (1)DEFAULT_LIMIT- Default items per page (20)MAX_LIMIT- Maximum items per page (100)
Clustering Constants:
DEFAULT_CLUSTER_RADIUS_KM- Default cluster radius (50 km)
Sort Constants:
DEFAULT_SORT_BY- Default sort option (SortBy.DISTANCE)
Geocoding Constants:
DEFAULT_LANGUAGE- Default language for geocoding ('en')DEFAULT_GEOCODING_FORMAT- Default geocoding format (GeocodingResultFormat.CITY)DEFAULT_NOMINATIM_ENDPOINT- Default Nominatim API endpointDEFAULT_CACHE_TTL_MS- Default cache TTL (24 hours)RATE_LIMIT_DELAY_MS- Rate limit delay (1000ms)MAX_CACHE_SIZE- Maximum cache size (10000 entries)
Interfaces
FindNearestOptions
Options for finding nearest items:
interface FindNearestOptions {
page?: number;
limit?: number;
sortBy?: SortBy; // SortBy.DISTANCE or SortBy.NAME
}LocationItem
Base interface for items with location data. The service automatically detects coordinates in various formats:
interface LocationItem {
// Supports multiple formats:
lat?: number;
lng?: number;
location?: { lat: number; lng: number };
coordinates?: { lat: number; lng: number } | [number, number];
// ... and more
}UserWithLocation
Interface for user clustering:
interface UserWithLocation {
id: string;
lat: number;
lng: number;
}Cluster
Result of clustering operation:
interface Cluster {
center: { lat: number; lng: number };
radius: number;
users: UserWithLocation[];
}GeocodingOptions
Options for reverse geocoding:
interface GeocodingOptions {
format?: GeocodingResultFormat; // GeocodingResultFormat.FULL, CITY, or ADDRESS
language?: string; // Language code (e.g., 'en', 'uk')
cacheTtl?: number; // Cache TTL in milliseconds
apiEndpoint?: string; // Custom Nominatim API endpoint
}License
MIT
