pulse-ai-utils
Version:
Utility functions and helpers for AI-powered applications
224 lines • 7.64 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.calculateDistance = calculateDistance;
exports.expandRadius = expandRadius;
exports.calculateRelevanceScore = calculateRelevanceScore;
exports.findContentWithinRadius = findContentWithinRadius;
exports.extractLocationsFromText = extractLocationsFromText;
exports.generateGeohash = generateGeohash;
exports.progressiveGeoSearch = progressiveGeoSearch;
exports.getGeohashBounds = getGeohashBounds;
const area_coordinates_1 = require("../constants/area-coordinates");
/**
* Calculate distance between two points using Haversine formula
* @returns Distance in miles
*/
function calculateDistance(lat1, lng1, lat2, lng2) {
const R = 3959; // Earth's radius in miles
const dLat = toRad(lat2 - lat1);
const dLng = toRad(lng2 - lng1);
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
Math.sin(dLng / 2) * Math.sin(dLng / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
function toRad(degrees) {
return degrees * (Math.PI / 180);
}
/**
* Determine if radius should be expanded based on results
*/
function expandRadius(params) {
const { currentRadius, resultsCount, minThreshold, maxRadius } = params;
if (resultsCount >= minThreshold) {
return { shouldExpand: false, newRadius: currentRadius };
}
if (currentRadius >= maxRadius) {
return { shouldExpand: false, newRadius: maxRadius };
}
// Double the radius, but don't exceed max
const newRadius = Math.min(currentRadius * 2, maxRadius);
return { shouldExpand: true, newRadius };
}
/**
* Calculate relevance score based on distance
* Uses exponential decay: score = 1 / (1 + distance * decayFactor)
*/
function calculateRelevanceScore(params) {
const { distanceMiles, decayFactor = 0.1 } = params;
return 1 / (1 + distanceMiles * decayFactor);
}
/**
* Find content within a radius from user location
*/
function findContentWithinRadius(params) {
const { userLat, userLng, content, radiusMiles } = params;
const results = [];
for (const item of content) {
// Find nearest location from this content's locations
let minDistance = Infinity;
let nearestLocation = item.primary_location;
let nearestAreaId = item.primary_area;
for (const location of item.locations) {
const distance = calculateDistance(userLat, userLng, location.lat, location.lng);
if (distance < minDistance) {
minDistance = distance;
nearestLocation = { lat: location.lat, lng: location.lng };
nearestAreaId = location.area_id;
}
}
// Check if within radius
if (minDistance <= radiusMiles) {
results.push({
content: item,
distance: minDistance,
nearestLocation: {
area_id: nearestAreaId,
lat: nearestLocation.lat,
lng: nearestLocation.lng
},
relevanceScore: calculateRelevanceScore({ distanceMiles: minDistance })
});
}
}
// Sort by distance
return results.sort((a, b) => a.distance - b.distance);
}
/**
* Extract locations from text content
*/
function extractLocationsFromText(text) {
const locations = [];
const lowerText = text.toLowerCase();
const foundAreas = [];
// Look for area names in text
for (const [areaId, coords] of Object.entries(area_coordinates_1.AREA_COORDINATES)) {
// Convert area ID to readable name (e.g., "st-petersburg" -> "st. petersburg")
const areaName = areaId.replace(/-/g, ' ');
const variations = [
areaName,
areaName.replace('st ', 'st. '),
areaName.replace('saint ', 'st. ')
];
for (const variation of variations) {
// Use word boundaries to avoid partial matches
const regex = new RegExp(`\\b${variation}\\b`, 'i');
if (regex.test(lowerText)) {
foundAreas.push(areaId);
break;
}
}
}
// Assign relevance based on order
foundAreas.forEach((areaId, index) => {
const coords = area_coordinates_1.AREA_COORDINATES[areaId];
locations.push({
area_id: areaId,
lat: coords.lat,
lng: coords.lng,
relevance: index === 0 ? 1.0 : 0.8 - (index * 0.1)
});
});
return locations;
}
/**
* Generate geohash for efficient geo queries
* Implementation of simple geohash algorithm
*/
function generateGeohash(lat, lng, precision = 6) {
const BASE32 = '0123456789bcdefghjkmnpqrstuvwxyz';
let latMin = -90, latMax = 90;
let lngMin = -180, lngMax = 180;
let geohash = '';
let bits = 0;
let bit = 0;
let even = true;
while (geohash.length < precision) {
if (even) {
const mid = (lngMin + lngMax) / 2;
if (lng > mid) {
bit |= (1 << (4 - bits));
lngMin = mid;
}
else {
lngMax = mid;
}
}
else {
const mid = (latMin + latMax) / 2;
if (lat > mid) {
bit |= (1 << (4 - bits));
latMin = mid;
}
else {
latMax = mid;
}
}
even = !even;
bits++;
if (bits === 5) {
geohash += BASE32[bit];
bits = 0;
bit = 0;
}
}
return geohash;
}
/**
* Progressive search with radius expansion
*/
async function progressiveGeoSearch(params) {
const { userLat, userLng, content, initialRadius = 5, maxRadius = 50, minResults = 20, radiusSteps = [5, 10, 20, 30, 50] } = params;
let results = [];
let currentRadius = initialRadius;
let expansions = 0;
// Try each radius step
for (const radius of radiusSteps) {
if (radius > maxRadius)
break;
currentRadius = radius;
results = findContentWithinRadius({
userLat,
userLng,
content,
radiusMiles: radius
});
if (results.length >= minResults) {
break;
}
expansions++;
}
return {
results,
finalRadius: currentRadius,
expansions
};
}
/**
* Get geohash bounds for a circular area
* This creates a bounding box that contains the circle
*/
function getGeohashBounds(lat, lng, radiusMiles) {
// Convert radius to degrees (approximate)
const latDelta = radiusMiles / 69; // 1 degree latitude ≈ 69 miles
const lngDelta = radiusMiles / (69 * Math.cos(lat * Math.PI / 180));
// Calculate bounding box
const minLat = lat - latDelta;
const maxLat = lat + latDelta;
const minLng = lng - lngDelta;
const maxLng = lng + lngDelta;
// Generate geohashes for corners
const precision = radiusMiles < 5 ? 6 : radiusMiles < 20 ? 5 : 4;
// For simple implementation, return a single bound
// In production, this would be more sophisticated
const lowerHash = generateGeohash(minLat, minLng, precision);
const upperHash = generateGeohash(maxLat, maxLng, precision);
// Ensure proper ordering
const bounds = [{
lower: lowerHash < upperHash ? lowerHash : upperHash,
upper: lowerHash < upperHash ? upperHash : lowerHash
}];
return bounds;
}
//# sourceMappingURL=geo-search.js.map