s-m-r-t

@happyvertical/smrt-places

Hierarchical place management with geo-coordinates, address validation, and territory mapping.

v0.19.0GeographyHierarchy

Overview

smrt-places is a comprehensive location management system for the SMRT framework. It provides hierarchical organization of geographic locations with automatic geocoding integration, proximity-based searches, and support for both real-world places (with coordinates) and abstract places (virtual worlds, game zones, organizational units).

The module enables organic database growth through lookupOrCreate(), which automatically queries geocoding services (OpenStreetMap or Google Maps) for missing locations. Places can be organized in unlimited nesting depth (Country → Region → City → Building → Room), and each place can store custom metadata for domain-specific attributes.

Installation

bash
npm install @happyvertical/smrt-places
# or
pnpm add @happyvertical/smrt-places

The module depends on @happyvertical/smrt-core for base classes and @happyvertical/geo for geocoding integration. It works seamlessly with smrt-tenancy for multi-tenant isolation.

Quick Start (5 Minutes)

Here's a minimal example showing how to create a place hierarchy, use geocoding, and search by proximity:

1. Initialize Collections

typescript
import { PlaceCollection, PlaceTypeCollection } from '@happyvertical/smrt-places';

const places = new PlaceCollection({ db: {...} });
const placeTypes = new PlaceTypeCollection({ db: {...} });

// Seed default place types (country, region, city, etc.)
await placeTypes.initializeDefaults();

2. Create Place Hierarchy Manually

typescript
// Create country
const usa = await places.create({
  name: 'United States',
  typeId: (await placeTypes.getBySlug('country'))?.id,
  countryCode: 'US'
});

// Create state/region
const california = await places.create({
  name: 'California',
  typeId: (await placeTypes.getBySlug('region'))?.id,
  parentId: usa.id,
  region: 'CA'
});

// Create city
const sanFrancisco = await places.create({
  name: 'San Francisco',
  typeId: (await placeTypes.getBySlug('city'))?.id,
  parentId: california.id,
  city: 'San Francisco',
  region: 'CA',
  country: 'United States'
});

3. Use Organic Database Growth (lookupOrCreate)

typescript
// Forward geocoding: address → place with coordinates
const office = await places.lookupOrCreate(
  '1234 Market Street, San Francisco, CA',
  {
    geoProvider: 'openstreetmap', // Free, no API key needed
    typeSlug: 'address',
    createIfNotFound: true
  }
);

console.log('Address found:', office.name);
console.log('Coordinates:', office.latitude, office.longitude);
console.log('Full address:', office.streetNumber, office.streetName, office.city);

// Reverse geocoding: coordinates → place with address
const placeFromCoords = await places.lookupOrCreate(
  { lat: 37.7749, lng: -122.4194 },
  {
    geoProvider: 'openstreetmap',
    typeSlug: 'address',
    createIfNotFound: true
  }
);
console.log('Place from coords:', placeFromCoords.name);

4. Search by Proximity

typescript
// Find all places within 5km of coordinates
const nearbyPlaces = await places.searchByProximity(
  37.7749,
  -122.4194,
  5 // radius in km
);

console.log('Found', nearbyPlaces.length, 'places nearby');
nearbyPlaces.forEach(place => {
  console.log('-', place.name, '| Distance:', place.distance, 'km');
});

Core Concepts

Place Model Structure

The Place model supports both physical locations (with coordinates) and abstract places (virtual worlds, game zones). All geographic fields are optional:

typescript
class Place extends SmrtObject {
  // Core fields
  id: string
  typeId: string                // Reference to PlaceType
  parentId: string | null       // Self-referencing hierarchy
  name: string
  description: string

  // Geographic fields (all optional)
  latitude: number | null
  longitude: number | null
  streetNumber: string
  streetName: string
  city: string
  region: string
  country: string
  postalCode: string
  countryCode: string
  timezone: string

  // Metadata and tracking
  externalId: string            // For syncing with external systems
  source: string                // 'openstreetmap', 'google', 'manual'
  metadata: Record<string, any> // JSON storage for custom attributes

  // Methods
  getGeoData(): GeoData
  hasCoordinates(): boolean
  getMetadata(): Record<string, any>
  setMetadata(data: Record): void
  updateMetadata(updates: Record): void
}

Hierarchical Organization

Places form a self-referencing hierarchy through the parentId field. This enables unlimited nesting depth for organizing locations:

Example Hierarchies:
  • 🌍 United States → California → San Francisco → Market Street → Building 1234 → Floor 5 → Room 501
  • 🎮 Game World → Eastern Kingdom → Forest of Doom → Ancient Temple → Treasure Chamber
  • 🏢 Acme Corp → West Coast Office → Floor 3 → Engineering → Desk 42
typescript
// Get immediate parent
const parent = await place.getParent();

// Get all children
const children = await place.getChildren();

// Get all ancestors (root to parent)
const ancestors = await place.getAncestors();

// Get all descendants (children, grandchildren, etc.)
const descendants = await place.getDescendants();

// Get complete hierarchy
const hierarchy = await place.getHierarchy();
// Returns: { ancestors: Place[], current: Place, descendants: Place[] }

PlaceType System

Places are categorized using the PlaceType model with slug-based identification. Default types include:

  • country
  • region
  • city
  • address
  • building
  • room
  • zone
  • point_of_interest
typescript
// Get or create type (idempotent)
const buildingType = await placeTypes.getOrCreate('building', 'Building');

// Look up by slug
const cityType = await placeTypes.getBySlug('city');

// Create custom type
const restaurantType = await placeTypes.getOrCreate('restaurant', 'Restaurant');

Geocoding Integration

The module integrates with @happyvertical/geo for automatic geocoding using OpenStreetMap (free, no API key) or Google Maps (requires API key):

typescript
// Forward geocoding: address → coordinates + place data
const place = await places.lookupOrCreate(
  '1600 Amphitheatre Parkway, Mountain View, CA',
  {
    geoProvider: 'google',  // or 'openstreetmap'
    typeSlug: 'address',
    createIfNotFound: true
  }
);
// Automatically populated: lat, lng, streetNumber, streetName, city, region, etc.

// Reverse geocoding: coordinates → address + place data
const place2 = await places.lookupOrCreate(
  { lat: 37.4220, lng: -122.0841 },
  {
    geoProvider: 'openstreetmap',
    typeSlug: 'address',
    createIfNotFound: true
  }
);
// Automatically populated from geocoding service

Abstract Places (No Coordinates)

Places don't require coordinates - perfect for virtual worlds, game zones, or organizational structures:

typescript
// Create game world hierarchy
const world = await places.create({
  name: 'Azeroth',
  typeId: (await placeTypes.getOrCreate('world', 'World')).id,
  description: 'Fantasy game world'
});

const zone = await places.create({
  name: 'Stormwind City',
  typeId: (await placeTypes.getOrCreate('zone', 'Zone')).id,
  parentId: world.id,
  metadata: {
    levelRange: '1-60',
    faction: 'Alliance',
    hasFlyingMount: false
  }
});

const poi = await places.create({
  name: 'Stormwind Keep',
  typeId: (await placeTypes.getOrCreate('point_of_interest', 'POI')).id,
  parentId: zone.id,
  metadata: {
    questGiver: 'King Varian Wrynn',
    services: ['Bank', 'Auction House', 'Vendors']
  }
});

API Reference

Place Model Methods

typescript
// Geographic data
place.getGeoData(): GeoData              // Get all geographic fields
place.hasCoordinates(): boolean          // Check if lat/lng are set

// Metadata management
place.getMetadata(): Record<string, any> // Parse and return JSON metadata
place.setMetadata(data): void            // Replace entire metadata
place.updateMetadata(updates): void      // Merge updates into metadata

// Relationships
await place.getType(): PlaceType | null
await place.getParent(): Place | null
await place.getChildren(): Place[]
await place.getAncestors(): Place[]      // Root-to-parent chain
await place.getDescendants(): Place[]    // All children recursively
await place.getHierarchy(): PlaceHierarchy

PlaceCollection Methods

typescript
// Organic database growth
await places.lookupOrCreate(query, options?): Promise<Place | null>
// query: address string OR { lat, lng }
// options: { geoProvider?, typeSlug?, parentId?, createIfNotFound? }

// Hierarchy traversal
await places.getChildren(parentId): Promise<Place[]>
await places.getRootPlaces(): Promise<Place[]>
await places.getHierarchy(placeId): Promise<PlaceHierarchy>

// Type-based queries
await places.getByType(typeSlug): Promise<Place[]>

// Proximity search
await places.searchByProximity(lat, lng, radiusKm): Promise<Place[]>
// Returns places sorted by distance with distance property added

PlaceTypeCollection Methods

typescript
// Idempotent type access
await placeTypes.getOrCreate(slug, name?): Promise<PlaceType>

// Lookup
await placeTypes.getBySlug(slug): Promise<PlaceType | null>

// Initialize defaults
await placeTypes.initializeDefaults(): Promise<PlaceType[]>
// Seeds: country, region, city, address, building, room, zone, point_of_interest

Utility Functions

typescript
import {
  validateCoordinates,
  calculateDistance,
  formatCoordinates,
  parseCoordinates,
  areCoordinatesNear,
  generateDisplayName,
  normalizeAddressComponents
} from '@happyvertical/smrt-places';

// Validation
const result = validateCoordinates(37.7749, -122.4194);
// Returns: { valid: true } or { valid: false, error: string }

// Distance calculation (Haversine formula)
const distanceKm = calculateDistance(
  37.7749, -122.4194,  // San Francisco
  34.0522, -118.2437   // Los Angeles
);
console.log(distanceKm); // ~559 km

// Formatting
const formatted = formatCoordinates(37.7749, -122.4194, 4);
// "37.7749, -122.4194"

// Parsing
const coords = parseCoordinates("37.7749, -122.4194");
// { lat: 37.7749, lng: -122.4194 }

// Proximity check
const isNear = areCoordinatesNear(
  37.7749, -122.4194,
  37.7750, -122.4195,
  0.1 // threshold in km
);
// true

// Display name generation
const display = generateDisplayName({
  streetNumber: '1234',
  streetName: 'Market Street',
  city: 'San Francisco',
  region: 'CA'
});
// "1234 Market Street, San Francisco, CA"

Tutorials

Tutorial 1: Basic Place Creation and Hierarchy (10-15 min)

Build a location hierarchy from scratch:

  • Initialize PlaceTypeCollection and seed default types
  • Create country, region, city hierarchy manually
  • Retrieve complete hierarchy using getHierarchy()
  • List all places at each level
  • Navigate parent-child relationships

Tutorial 2: Organic Database Growth with lookupOrCreate (15-20 min)

Automatically populate places using geocoding:

  • Forward geocoding (address string → place with coordinates)
  • Reverse geocoding (coordinates → place with address)
  • Compare OpenStreetMap vs Google Maps providers
  • Control creation with createIfNotFound flag
  • Store and retrieve metadata from geocoding results

Tutorial 3: Proximity Search and Location Queries (15-20 min)

Find nearby places and calculate distances:

  • Populate database with places at real coordinates
  • Search places within 10km radius
  • Calculate distances between specific places
  • Filter results by place type
  • Sort by relevance/distance

Tutorial 4: Abstract Places and Virtual Worlds (15-20 min)

Build location systems without real-world coordinates:

  • Create custom place types for game zones (zone, area, sector)
  • Build hierarchy without geocoding
  • Store game-specific metadata
  • Query abstract place hierarchies
  • Mix physical and abstract places in same system

Real-World Examples

Example 1: Restaurant Locator Application

Build a restaurant finder with automatic geocoding:

typescript
// Create restaurant type
const restaurantType = await placeTypes.getOrCreate('restaurant', 'Restaurant');

// Add restaurants using addresses (organic growth)
const restaurant1 = await places.lookupOrCreate(
  '123 Main Street, San Francisco, CA',
  {
    geoProvider: 'openstreetmap',
    typeSlug: 'restaurant',
    createIfNotFound: true
  }
);

// Store restaurant-specific metadata
restaurant1.updateMetadata({
  cuisineType: 'Italian',
  priceRange: '$$',
  rating: 4.5,
  hours: {
    monday: '11:00-22:00',
    tuesday: '11:00-22:00',
    // ...
  },
  reservations: true,
  delivery: false
});
await restaurant1.save();

// Find restaurants near user
const userLat = 37.7749;
const userLng = -122.4194;
const nearbyRestaurants = await places.searchByProximity(userLat, userLng, 5);

// Filter by type and metadata
const italianRestaurants = nearbyRestaurants.filter(place => {
  const metadata = place.getMetadata();
  return metadata.cuisineType === 'Italian';
});

console.log('Found', italianRestaurants.length, 'Italian restaurants within 5km');

Example 2: Event Management System

Manage venues with hierarchical room organization:

typescript
// Create venue hierarchy
const city = await places.create({
  name: 'Chicago',
  typeId: (await placeTypes.getBySlug('city'))?.id,
  latitude: 41.8781,
  longitude: -87.6298
});

const building = await places.create({
  name: 'Convention Center',
  typeId: (await placeTypes.getBySlug('building'))?.id,
  parentId: city.id,
  streetName: 'Michigan Avenue',
  streetNumber: '2301',
  latitude: 41.8781,
  longitude: -87.6298
});

// Create floors
const floor1 = await places.create({
  name: 'Floor 1',
  typeId: (await placeTypes.getOrCreate('floor', 'Floor')).id,
  parentId: building.id
});

const floor2 = await places.create({
  name: 'Floor 2',
  typeId: (await placeTypes.getOrCreate('floor', 'Floor')).id,
  parentId: building.id
});

// Create rooms with capacity metadata
const rooms = [
  { name: 'Grand Ballroom', floor: floor1, capacity: 500 },
  { name: 'Conference Room A', floor: floor1, capacity: 50 },
  { name: 'Conference Room B', floor: floor2, capacity: 30 }
];

for (const roomData of rooms) {
  const room = await places.create({
    name: roomData.name,
    typeId: (await placeTypes.getBySlug('room'))?.id,
    parentId: roomData.floor.id,
    metadata: {
      capacity: roomData.capacity,
      equipment: ['Projector', 'Sound system', 'WiFi'],
      accessible: true
    }
  });
}

// Query all rooms in building
const allRooms = await building.getDescendants();
const roomsOnly = allRooms.filter(async place => {
  const type = await place.getType();
  return type?.slug === 'room';
});

// Find available spaces near user location
const nearbyVenues = await places.searchByProximity(41.8781, -87.6298, 10);
console.log('Found', nearbyVenues.length, 'venues within 10km');

Example 3: Game World Builder

Create abstract place hierarchies for virtual worlds:

typescript
// Create game world (no coordinates needed)
const world = await places.create({
  name: 'Eldoria',
  typeId: (await placeTypes.getOrCreate('world', 'World')).id,
  description: 'Fantasy MMORPG world',
  metadata: {
    maxPlayers: 10000,
    serverRegion: 'US-West',
    version: '2.1.0'
  }
});

// Create regions
const easternKingdom = await places.create({
  name: 'Eastern Kingdom',
  typeId: (await placeTypes.getOrCreate('region', 'Region')).id,
  parentId: world.id,
  metadata: {
    faction: 'Alliance',
    recommendedLevel: '1-40',
    climate: 'Temperate'
  }
});

// Create zones
const elwynnForest = await places.create({
  name: 'Elwynn Forest',
  typeId: (await placeTypes.getOrCreate('zone', 'Zone')).id,
  parentId: easternKingdom.id,
  metadata: {
    levelRange: '1-10',
    enemyTypes: ['Wolf', 'Bear', 'Defias Bandit'],
    resources: ['Wood', 'Copper Ore'],
    questHubs: ['Goldshire', 'Northshire Abbey']
  }
});

// Create points of interest
const goldshire = await places.create({
  name: 'Goldshire',
  typeId: (await placeTypes.getOrCreate('point_of_interest', 'POI')).id,
  parentId: elwynnForest.id,
  metadata: {
    type: 'town',
    npcs: ['Innkeeper', 'Blacksmith', 'Quest Giver'],
    services: ['Rest area', 'Vendors', 'Flight path'],
    safeZone: true
  }
});

// Query game hierarchy
const worldHierarchy = await world.getHierarchy();
console.log('World structure:');
console.log('- World:', worldHierarchy.current.name);
console.log('- Regions:', worldHierarchy.descendants.filter(p => p.typeId === easternKingdom.typeId).length);
console.log('- Total locations:', worldHierarchy.descendants.length);

Example 4: Real Estate Platform

Organize properties with geocoding and search:

typescript
// Property hierarchy: Country → City → Building → Unit
const propertyType = await placeTypes.getOrCreate('property', 'Property');

// Create building with geocoding
const building = await places.lookupOrCreate(
  '555 California Street, San Francisco, CA',
  {
    geoProvider: 'google',
    typeSlug: 'building',
    createIfNotFound: true
  }
);

building.updateMetadata({
  buildingName: 'Sunset Tower',
  yearBuilt: 2010,
  floors: 20,
  parking: true,
  amenities: ['Pool', 'Gym', 'Concierge']
});
await building.save();

// Create units within building
for (let floor = 1; floor <= 20; floor++) {
  for (const unit of ['A', 'B', 'C', 'D']) {
    const unitNumber = floor * 100 + unit.charCodeAt(0) - 64;

    const propertyUnit = await places.create({
      name: 'Unit ' + unitNumber,
      typeId: propertyType.id,
      parentId: building.id,
      latitude: building.latitude,
      longitude: building.longitude,
      metadata: {
        floor: floor,
        bedrooms: floor < 10 ? 1 : 2,
        bathrooms: floor < 10 ? 1 : 2,
        sqft: floor < 10 ? 800 : 1200,
        price: (floor < 10 ? 2500 : 3500) + (floor * 50),
        available: Math.random() > 0.7,
        viewType: floor > 15 ? 'City view' : 'Street view'
      }
    });
  }
}

// Search for properties near location
const searchLat = 37.7749;
const searchLng = -122.4194;
const nearbyProperties = await places.searchByProximity(searchLat, searchLng, 2);

// Filter by availability and price
const availableUnits = nearbyProperties.filter(place => {
  const metadata = place.getMetadata();
  return metadata.available && metadata.price <= 3000;
});

console.log('Found', availableUnits.length, 'available units under $3000 within 2km');

Example 5: Supply Chain Management

Track warehouse locations and zones:

typescript
// Warehouse hierarchy: Country → Distribution Center → Warehouse → Zone → Shelf
const dcType = await placeTypes.getOrCreate('distribution_center', 'Distribution Center');
const warehouseType = await placeTypes.getOrCreate('warehouse', 'Warehouse');
const zoneType = await placeTypes.getOrCreate('zone', 'Zone');
const shelfType = await placeTypes.getOrCreate('shelf', 'Shelf');

// Create distribution center
const dc = await places.create({
  name: 'West Coast DC',
  typeId: dcType.id,
  latitude: 37.5483,
  longitude: -121.9886,
  city: 'Tracy',
  region: 'CA',
  metadata: {
    operatingHours: '24/7',
    maxCapacity: 1000000, // sqft
    manager: 'John Doe'
  }
});

// Create warehouse within DC
const warehouse = await places.create({
  name: 'Warehouse A',
  typeId: warehouseType.id,
  parentId: dc.id,
  latitude: dc.latitude,
  longitude: dc.longitude,
  metadata: {
    temperature: 'ambient',
    securityLevel: 'high'
  }
});

// Create zones within warehouse
for (const zoneName of ['Receiving', 'Picking', 'Packing', 'Shipping']) {
  const zone = await places.create({
    name: zoneName + ' Zone',
    typeId: zoneType.id,
    parentId: warehouse.id,
    metadata: {
      capacity: 10000,
      currentOccupancy: Math.floor(Math.random() * 10000)
    }
  });

  // Create shelves within zone
  for (let aisle = 1; aisle <= 10; aisle++) {
    for (let position = 1; position <= 20; position++) {
      const shelf = await places.create({
        name: 'Shelf ' + zoneName[0] + '-' + aisle + '-' + position,
        typeId: shelfType.id,
        parentId: zone.id,
        metadata: {
          aisle: aisle,
          position: position,
          height: Math.floor(Math.random() * 3) + 1, // 1-3 levels
          occupied: Math.random() > 0.3
        }
      });
    }
  }
}

// Query all shelves in warehouse
const allDescendants = await warehouse.getDescendants();
const shelves = allDescendants.filter(async place => {
  const type = await place.getType();
  return type?.slug === 'shelf';
});

console.log('Total shelves in warehouse:', shelves.length);

// Calculate distances for route optimization
const warehouseA = await places.findOne({ name: 'Warehouse A' });
const warehouseB = await places.findOne({ name: 'Warehouse B' });
const distance = calculateDistance(
  warehouseA.latitude, warehouseA.longitude,
  warehouseB.latitude, warehouseB.longitude
);
console.log('Distance between warehouses:', distance, 'km');

Integration Patterns

With smrt-core

Place and PlaceType extend SmrtObject with full SMRT framework integration:

  • @smrt decorator auto-generates REST APIs, CLI commands, and MCP tools
  • Automatic database schema generation from TypeScript types
  • Built-in validation, lifecycle hooks (beforeSave, afterSave)
  • AI-powered methods: do() and is()

With @happyvertical/geo

Seamless geocoding integration for organic database growth:

  • PlaceCollection internally uses geo adapter for lookupOrCreate()
  • OpenStreetMap provider (free, no API key)
  • Google Maps provider (requires GOOGLE_MAPS_API_KEY env var)
  • Automatic address component extraction and population

With smrt-tenancy

Multi-tenant place isolation:

  • Each tenant has separate place hierarchies
  • Query scoping via tenant context in collection options
  • Shared schema, separate data per tenant
  • Permission checks control which users access which places

With smrt-users

User location tracking and proximity features:

  • User profiles store placeId foreign key
  • Track user location history (visits to places)
  • Proximity-based user discovery (find users nearby)
  • Location-based permissions/roles

With smrt-content

Location-specific content:

  • Content references places via placeId foreign key
  • Blog posts/articles about specific locations
  • Location guides and documentation
  • Metadata synchronization between content and places

Best Practices

Creating Hierarchies

  • Start with root places (countries) and build downward
  • Use consistent naming conventions across hierarchy levels
  • Assign type slugs early for better querying and filtering
  • Store external IDs for syncing with other systems

Geocoding

  • Use OpenStreetMap by default (free, no API key needed)
  • Switch to Google Maps for better accuracy on edge cases
  • Set createIfNotFound: false for lookup-only queries
  • Validate coordinates with validateCoordinates() before storing
  • Check source field to track which provider created the place

Metadata Usage

  • Store domain-specific attributes in JSON metadata field
  • Use consistent schema across place types for easier querying
  • Don't store sensitive data in metadata (no encryption)
  • Validate metadata structure before updateMetadata()
  • Consider extracting frequently-queried metadata to dedicated columns

Performance

  • Index frequently-queried fields (parentId, typeId, city, region)
  • Use proximity search carefully on large datasets (pre-filter by type)
  • Cache hierarchy results when possible (e.g., getHierarchy())
  • Batch load relationships to avoid N+1 queries
  • Consider pagination for large result sets

Abstract Places

  • Skip all geographic fields for virtual/abstract places
  • Use descriptive type slugs ('zone', 'area', 'sector')
  • Store domain-specific data in metadata field
  • Hierarchies can be arbitrarily deep (no practical limit)
  • Mix physical and abstract places in same system

Common Mistakes to Avoid

  • Don't create circular references (Child → Parent → Grandparent → Child)
  • Don't store massive objects in metadata (keep under 10KB)
  • Don't use geocoding for every query (cache lookups)
  • Don't forget to set typeId (required field)
  • Don't mix coordinate systems (always use decimal degrees)

Common Issues and Troubleshooting

Error: "Place not found" on lookupOrCreate()

Cause: Geocoding service can't find the address

Solution: Check internet access, verify address format, try different provider, or use reverse geocoding with coordinates instead.

typescript
// Try different format
const place1 = await places.lookupOrCreate('San Francisco, CA, USA', {...});

// Or use coordinates
const place2 = await places.lookupOrCreate(
  { lat: 37.7749, lng: -122.4194 },
  {...}
);

Error: "Invalid coordinates"

Cause: Latitude or longitude out of valid range

Solution: Validate with validateCoordinates(). Latitude: -90 to 90, Longitude: -180 to 180. Watch for swapped values.

typescript
const result = validateCoordinates(lat, lng);
if (!result.valid) {
  console.error('Invalid coordinates:', result.error);
  return;
}

Issue: Slow hierarchy traversal

Cause: Multiple database queries for ancestors/descendants

Solution: Use getHierarchy() once instead of separate calls. Create database indexes on parentId. Cache results for frequently-accessed hierarchies.

typescript
// Efficient: single call
const hierarchy = await place.getHierarchy();
const ancestors = hierarchy.ancestors;
const descendants = hierarchy.descendants;

// Less efficient: multiple calls
const ancestors = await place.getAncestors();
const descendants = await place.getDescendants();

Issue: Geocoding provider inconsistencies

Cause: Different providers return different address formats

Solution: Store source field to track provider. Use normalizeAddressComponents() to standardize formats. Consider fallback provider.

typescript
const place = await places.lookupOrCreate(address, {
  geoProvider: 'google',
  createIfNotFound: true
});

console.log('Created by:', place.source); // 'google'

// Try fallback if needed
if (!place) {
  place = await places.lookupOrCreate(address, {
    geoProvider: 'openstreetmap',
    createIfNotFound: true
  });
}

Issue: Abstract place queries returning empty

Cause: Incorrect parentId or typeId

Solution: Verify parentId is set correctly (null or empty = root level). Check typeId matches expected type. Use getHierarchy() to inspect structure.

typescript
// Check place structure
const hierarchy = await place.getHierarchy();
console.log('Parent:', hierarchy.current.parentId);
console.log('Type:', await hierarchy.current.getType());
console.log('Children:', hierarchy.descendants.length);

Issue: Circular reference errors

Cause: Attempting to create Parent → Child → Parent loop

Solution: Database foreign key constraints prevent this. Always verify parent exists before setting parentId. Use transactions when restructuring hierarchies.

typescript
// Safe: check parent exists
const parent = await places.findById(parentId);
if (!parent) {
  throw new Error('Parent not found');
}

const child = await places.create({
  name: 'Child',
  parentId: parent.id,
  typeId: typeId
});

Related Modules