+ {/* Top accent bar */}
+
+
+ {/* Decorative orbital rings (top-right) */}
+
+
+ {/* Content area */}
+
+ {/* Title + description */}
+
+
+ {title}
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+ {/* Footer */}
+
+
+ {/* Amber dot */}
+
+
+ pg_orrery
+
+
+ Celestial mechanics for PostgreSQL
+
+
+
+ pg-orrery.warehack.ing
+
+
+
+
+ {/* Bottom accent bar */}
+
+
,
+ );
+}
diff --git a/docs/src/styles/custom.css b/docs/src/styles/custom.css
new file mode 100644
index 0000000..24f7d39
--- /dev/null
+++ b/docs/src/styles/custom.css
@@ -0,0 +1,129 @@
+@import "@fontsource/inter/400.css";
+@import "@fontsource/inter/500.css";
+@import "@fontsource/inter/600.css";
+@import "@fontsource/inter/700.css";
+@import "@fontsource/jetbrains-mono/400.css";
+@import "@fontsource/jetbrains-mono/500.css";
+
+/* pg_orrery palette — deep space observation theme */
+:root {
+ --sl-font: "Inter", system-ui, -apple-system, sans-serif;
+ --sl-font-mono: "JetBrains Mono", "Fira Code", ui-monospace, monospace;
+
+ --sl-color-accent-low: #451a03;
+ --sl-color-accent: #f59e0b;
+ --sl-color-accent-high: #fef3c7;
+
+ --sl-color-white: #e2e8f0;
+ --sl-color-gray-1: #cbd5e1;
+ --sl-color-gray-2: #8896a8;
+ --sl-color-gray-3: #556677;
+ --sl-color-gray-4: #2a3f54;
+ --sl-color-gray-5: #1e2d3d;
+ --sl-color-gray-6: #111827;
+ --sl-color-gray-7: #0a0e17;
+
+ --sl-color-bg-nav: var(--sl-color-gray-6);
+ --sl-color-bg-sidebar: var(--sl-color-gray-7);
+ --sl-color-hairline-light: var(--sl-color-gray-5);
+ --sl-color-hairline-shade: var(--sl-color-gray-4);
+}
+
+/* Scrollbar */
+::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--sl-color-gray-7);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--sl-color-gray-5);
+ border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--sl-color-gray-4);
+}
+
+/* Selection color with amber accent */
+::selection {
+ background-color: #92400e;
+ color: #e2e8f0;
+}
+
+/* SQL code blocks get amber left-border accent */
+pre:has(> code.language-sql) {
+ border-left: 3px solid var(--sl-color-accent);
+}
+
+/* Code blocks — raised surface */
+pre {
+ background-color: #111827 !important;
+ border: 1px solid #1e2d3d;
+}
+
+/* Sidebar section labels */
+.sl-sidebar-group summary {
+ font-weight: 600;
+ letter-spacing: 0.02em;
+}
+
+/* Hero title gradient */
+.hero .hero-html h1 {
+ background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 50%, #d97706 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+/* Tables */
+table {
+ border-collapse: collapse;
+ width: 100%;
+}
+
+th {
+ text-align: left;
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--sl-color-gray-2);
+ border-bottom: 1px solid var(--sl-color-gray-5);
+}
+
+td {
+ border-bottom: 1px solid var(--sl-color-gray-5);
+ font-size: 0.875rem;
+}
+
+tr:hover td {
+ background-color: #1a2332;
+}
+
+/* Aside tweaks */
+.starlight-aside--note {
+ border-color: var(--sl-color-accent);
+}
+
+/* Focus visible */
+:focus-visible {
+ outline: 2px solid var(--sl-color-accent);
+ outline-offset: 2px;
+}
+
+/* Workflow comparison blocks */
+.workflow-compare {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1rem;
+}
+
+@media (max-width: 768px) {
+ .workflow-compare {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/docs/src/styles/katex-fixes.css b/docs/src/styles/katex-fixes.css
new file mode 100644
index 0000000..92039e5
--- /dev/null
+++ b/docs/src/styles/katex-fixes.css
@@ -0,0 +1,48 @@
+/*
+ * KaTeX fixes for Starlight
+ *
+ * Starlight sets `svg { height: auto }` globally which breaks KaTeX's
+ * internal SVG elements (fraction bars, radicals, delimiters). These
+ * elements rely on explicit height values from KaTeX's layout engine.
+ */
+
+.katex-html svg {
+ height: inherit;
+}
+
+.katex-html .vlist svg {
+ height: inherit;
+}
+
+/* Wide equations need horizontal scroll */
+.katex-display {
+ overflow-x: auto;
+ overflow-y: hidden;
+ padding: 0.5rem 0;
+}
+
+/* Ensure KaTeX text is visible against dark background */
+.katex {
+ color: var(--sl-color-white, #e2e8f0);
+}
+
+/* Display-mode equations get breathing room */
+.katex-display > .katex {
+ max-width: 100%;
+}
+
+/* Fix KaTeX newline spacing in aligned environments */
+.katex .base {
+ margin-top: 2px;
+ margin-bottom: 2px;
+}
+
+/* Ensure fraction lines are visible */
+.katex .frac-line {
+ border-color: currentColor;
+}
+
+/* Inline math shouldn't break across lines */
+.katex-inline {
+ white-space: nowrap;
+}
diff --git a/docs/tsconfig.json b/docs/tsconfig.json
new file mode 100644
index 0000000..bcbf8b5
--- /dev/null
+++ b/docs/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "astro/tsconfigs/strict"
+}
diff --git a/lib/sat_code b/lib/sat_code
deleted file mode 160000
index ff7b989..0000000
--- a/lib/sat_code
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit ff7b98957dfa2979700a482bde9de9542807293e
diff --git a/pg_orbit.control b/pg_orbit.control
deleted file mode 100644
index b291ade..0000000
--- a/pg_orbit.control
+++ /dev/null
@@ -1,4 +0,0 @@
-comment = 'Orbital mechanics types and functions for PostgreSQL'
-default_version = '0.1.0'
-module_pathname = '$libdir/pg_orbit'
-relocatable = true
diff --git a/pg_orrery.control b/pg_orrery.control
new file mode 100644
index 0000000..677a08c
--- /dev/null
+++ b/pg_orrery.control
@@ -0,0 +1,4 @@
+comment = 'A database orrery — celestial mechanics types and functions for PostgreSQL'
+default_version = '0.5.0'
+module_pathname = '$libdir/pg_orrery'
+relocatable = true
diff --git a/sql/pg_orrery--0.1.0--0.2.0.sql b/sql/pg_orrery--0.1.0--0.2.0.sql
new file mode 100644
index 0000000..43af33f
--- /dev/null
+++ b/sql/pg_orrery--0.1.0--0.2.0.sql
@@ -0,0 +1,188 @@
+-- pg_orrery 0.1.0 -> 0.2.0 migration
+--
+-- Phase 1: Stars, comets, and Keplerian propagation.
+-- Adds heliocentric type, star observation, and two-body propagation.
+
+
+-- ============================================================
+-- Heliocentric type: ecliptic J2000 position in AU
+-- ============================================================
+
+CREATE TYPE heliocentric;
+
+CREATE FUNCTION heliocentric_in(cstring) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION heliocentric_out(heliocentric) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION heliocentric_recv(internal) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION heliocentric_send(heliocentric) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE heliocentric (
+ INPUT = heliocentric_in,
+ OUTPUT = heliocentric_out,
+ RECEIVE = heliocentric_recv,
+ SEND = heliocentric_send,
+ INTERNALLENGTH = 24,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE heliocentric IS 'Heliocentric position in ecliptic J2000 frame (x, y, z in AU)';
+
+CREATE FUNCTION helio_x(heliocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION helio_x(heliocentric) IS 'X component in AU (ecliptic J2000, vernal equinox direction)';
+
+CREATE FUNCTION helio_y(heliocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION helio_y(heliocentric) IS 'Y component in AU (ecliptic J2000)';
+
+CREATE FUNCTION helio_z(heliocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION helio_z(heliocentric) IS 'Z component in AU (ecliptic J2000, north ecliptic pole)';
+
+CREATE FUNCTION helio_distance(heliocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION helio_distance(heliocentric) IS 'Heliocentric distance in AU';
+
+
+-- ============================================================
+-- Star observation functions
+-- ============================================================
+
+CREATE FUNCTION star_observe(float8, float8, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION star_observe(float8, float8, observer, timestamptz) IS
+ 'Observe a star from (ra_hours J2000, dec_degrees J2000, observer, time). Returns topocentric az/el. Range is 0 (infinite distance).';
+
+CREATE FUNCTION star_observe_safe(float8, float8, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
+COMMENT ON FUNCTION star_observe_safe(float8, float8, observer, timestamptz) IS
+ 'Like star_observe but returns NULL on invalid inputs. For batch queries over star catalogs.';
+
+
+-- ============================================================
+-- Keplerian propagation functions
+-- ============================================================
+
+CREATE FUNCTION kepler_propagate(
+ q_au float8, eccentricity float8,
+ inclination_deg float8, arg_perihelion_deg float8,
+ long_asc_node_deg float8, perihelion_jd float8,
+ t timestamptz
+) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION kepler_propagate(float8, float8, float8, float8, float8, float8, timestamptz) IS
+ 'Two-body Keplerian propagation from classical orbital elements. Returns heliocentric ecliptic J2000 position in AU. Handles elliptic, parabolic, and hyperbolic orbits.';
+
+
+-- ============================================================
+-- Comet observation
+-- ============================================================
+
+CREATE FUNCTION comet_observe(
+ q_au float8, eccentricity float8,
+ inclination_deg float8, arg_perihelion_deg float8,
+ long_asc_node_deg float8, perihelion_jd float8,
+ earth_x_au float8, earth_y_au float8, earth_z_au float8,
+ obs observer, t timestamptz
+) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION comet_observe(float8, float8, float8, float8, float8, float8, float8, float8, float8, observer, timestamptz) IS
+ 'Observe a comet/asteroid from orbital elements. Requires Earth heliocentric ecliptic J2000 position (AU). Returns topocentric az/el with geocentric range in km.';
+
+
+-- ============================================================
+-- Phase 2: VSOP87 planets, ELP82B Moon, Sun observation
+-- ============================================================
+
+CREATE FUNCTION planet_heliocentric(int4, timestamptz) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION planet_heliocentric(int4, timestamptz) IS
+ 'VSOP87 heliocentric ecliptic J2000 position (AU). Body IDs: 0=Sun, 1=Mercury, ..., 8=Neptune.';
+
+CREATE FUNCTION planet_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION planet_observe(int4, observer, timestamptz) IS
+ 'Observe a planet from (body_id 1-8, observer, time). Returns topocentric az/el with geocentric range in km.';
+
+CREATE FUNCTION sun_observe(observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION sun_observe(observer, timestamptz) IS
+ 'Observe the Sun from (observer, time). Returns topocentric az/el with geocentric range in km.';
+
+CREATE FUNCTION moon_observe(observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION moon_observe(observer, timestamptz) IS
+ 'Observe the Moon via ELP2000-82B from (observer, time). Returns topocentric az/el with geocentric range in km.';
+
+
+-- ============================================================
+-- Phase 3: Planetary moon observation
+-- ============================================================
+
+CREATE FUNCTION galilean_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION galilean_observe(int4, observer, timestamptz) IS
+ 'Observe a Galilean moon of Jupiter via L1.2 theory. Body IDs: 0=Io, 1=Europa, 2=Ganymede, 3=Callisto.';
+
+CREATE FUNCTION saturn_moon_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION saturn_moon_observe(int4, observer, timestamptz) IS
+ 'Observe a Saturn moon via TASS 1.7. Body IDs: 0=Mimas, 1=Enceladus, 2=Tethys, 3=Dione, 4=Rhea, 5=Titan, 6=Iapetus, 7=Hyperion.';
+
+CREATE FUNCTION uranus_moon_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION uranus_moon_observe(int4, observer, timestamptz) IS
+ 'Observe a Uranus moon via GUST86. Body IDs: 0=Miranda, 1=Ariel, 2=Umbriel, 3=Titania, 4=Oberon.';
+
+CREATE FUNCTION mars_moon_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION mars_moon_observe(int4, observer, timestamptz) IS
+ 'Observe a Mars moon via MarsSat. Body IDs: 0=Phobos, 1=Deimos.';
+
+
+-- ============================================================
+-- Phase 3: Jupiter decametric radio burst prediction
+-- ============================================================
+
+CREATE FUNCTION io_phase_angle(timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION io_phase_angle(timestamptz) IS
+ 'Io orbital phase angle in degrees [0,360). 0=superior conjunction (behind Jupiter). Standard Radio JOVE convention.';
+
+CREATE FUNCTION jupiter_cml(observer, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION jupiter_cml(observer, timestamptz) IS
+ 'Jupiter Central Meridian Longitude, System III (1965.0), in degrees [0,360). Light-time corrected.';
+
+CREATE FUNCTION jupiter_burst_probability(float8, float8) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION jupiter_burst_probability(float8, float8) IS
+ 'Estimated Jupiter decametric burst probability (0-1) from (io_phase_deg, cml_deg). Based on Carr et al. (1983) source regions A, B, C, D.';
+
+
+-- ============================================================
+-- Phase 4: Interplanetary transfer orbits (Lambert solver)
+-- ============================================================
+
+CREATE FUNCTION lambert_transfer(
+ dep_body_id int4, arr_body_id int4,
+ dep_time timestamptz, arr_time timestamptz,
+ OUT c3_departure float8, OUT c3_arrival float8,
+ OUT v_inf_departure float8, OUT v_inf_arrival float8,
+ OUT tof_days float8, OUT transfer_sma float8
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION lambert_transfer(int4, int4, timestamptz, timestamptz) IS
+ 'Solve Lambert transfer between two planets. Returns C3 (km^2/s^2), v_infinity (km/s), TOF (days), SMA (AU). Body IDs 1-8.';
+
+CREATE FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) IS
+ 'Departure C3 (km^2/s^2) for a Lambert transfer. Returns NULL if solver fails. For pork chop plots.';
diff --git a/sql/pg_orbit--0.1.0.sql b/sql/pg_orrery--0.1.0.sql
similarity index 99%
rename from sql/pg_orbit--0.1.0.sql
rename to sql/pg_orrery--0.1.0.sql
index 917e663..cfb3c74 100644
--- a/sql/pg_orbit--0.1.0.sql
+++ b/sql/pg_orrery--0.1.0.sql
@@ -1,4 +1,4 @@
--- pg_orbit -- Orbital mechanics types and functions for PostgreSQL
+-- pg_orrery -- Orbital mechanics types and functions for PostgreSQL
--
-- Types: tle, eci_position, geodetic, topocentric, observer, pass_event
-- Provides SGP4/SDP4 propagation, coordinate transforms, pass prediction,
diff --git a/sql/pg_orrery--0.2.0--0.3.0.sql b/sql/pg_orrery--0.2.0--0.3.0.sql
new file mode 100644
index 0000000..03aef04
--- /dev/null
+++ b/sql/pg_orrery--0.2.0--0.3.0.sql
@@ -0,0 +1,86 @@
+-- pg_orrery 0.2.0 -> 0.3.0 migration
+--
+-- Adds optional JPL DE440/441 ephemeris functions.
+-- Existing VSOP87/ELP2000-82B functions are unchanged (still IMMUTABLE).
+-- New _de() functions are STABLE (depend on external DE binary file).
+-- When DE is unavailable, _de() functions fall back to VSOP87 silently.
+
+-- ============================================================
+-- Phase 5: DE ephemeris functions (optional high-precision)
+-- ============================================================
+
+-- Planet observation with DE ephemeris
+
+CREATE FUNCTION planet_heliocentric_de(int4, timestamptz) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION planet_heliocentric_de(int4, timestamptz) IS
+ 'Heliocentric ecliptic J2000 position via JPL DE (sub-arcsecond). Falls back to VSOP87 if DE unavailable. Body IDs: 0=Sun, 1-8=Mercury-Neptune.';
+
+CREATE FUNCTION planet_observe_de(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION planet_observe_de(int4, observer, timestamptz) IS
+ 'Observe planet via JPL DE. Falls back to VSOP87. Body IDs: 1-8 (Mercury-Neptune).';
+
+CREATE FUNCTION sun_observe_de(observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION sun_observe_de(observer, timestamptz) IS
+ 'Observe Sun via JPL DE. Falls back to VSOP87.';
+
+CREATE FUNCTION moon_observe_de(observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION moon_observe_de(observer, timestamptz) IS
+ 'Observe Moon via JPL DE. Falls back to ELP2000-82B.';
+
+
+-- Lambert transfer with DE positions
+
+CREATE FUNCTION lambert_transfer_de(
+ dep_body_id int4, arr_body_id int4,
+ dep_time timestamptz, arr_time timestamptz,
+ OUT c3_departure float8, OUT c3_arrival float8,
+ OUT v_inf_departure float8, OUT v_inf_arrival float8,
+ OUT tof_days float8, OUT transfer_sma float8
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION lambert_transfer_de(int4, int4, timestamptz, timestamptz) IS
+ 'Lambert transfer via JPL DE positions. Falls back to VSOP87. Body IDs 1-8.';
+
+CREATE FUNCTION lambert_c3_de(int4, int4, timestamptz, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION lambert_c3_de(int4, int4, timestamptz, timestamptz) IS
+ 'Departure C3 via JPL DE. Falls back to VSOP87. For pork chop plots.';
+
+
+-- Planetary moon observation with DE parent positions
+
+CREATE FUNCTION galilean_observe_de(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION galilean_observe_de(int4, observer, timestamptz) IS
+ 'Observe Galilean moon with JPL DE parent position. L1.2 moon offsets. Body IDs: 0-3 (Io-Callisto).';
+
+CREATE FUNCTION saturn_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION saturn_moon_observe_de(int4, observer, timestamptz) IS
+ 'Observe Saturn moon with JPL DE parent position. TASS 1.7 moon offsets. Body IDs: 0-7 (Mimas-Hyperion).';
+
+CREATE FUNCTION uranus_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION uranus_moon_observe_de(int4, observer, timestamptz) IS
+ 'Observe Uranus moon with JPL DE parent position. GUST86 moon offsets. Body IDs: 0-4 (Miranda-Oberon).';
+
+CREATE FUNCTION mars_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION mars_moon_observe_de(int4, observer, timestamptz) IS
+ 'Observe Mars moon with JPL DE parent position. MarsSat moon offsets. Body IDs: 0-1 (Phobos-Deimos).';
+
+
+-- Diagnostic function
+
+CREATE FUNCTION pg_orrery_ephemeris_info(
+ OUT provider text, OUT file_path text,
+ OUT start_jd float8, OUT end_jd float8,
+ OUT version int4, OUT au_km float8
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
+COMMENT ON FUNCTION pg_orrery_ephemeris_info() IS
+ 'Returns current ephemeris provider status: VSOP87 or JPL_DE with file path, JD range, version, and AU value.';
diff --git a/sql/pg_orrery--0.2.0.sql b/sql/pg_orrery--0.2.0.sql
new file mode 100644
index 0000000..35ca364
--- /dev/null
+++ b/sql/pg_orrery--0.2.0.sql
@@ -0,0 +1,704 @@
+-- pg_orrery -- Orbital mechanics types and functions for PostgreSQL
+--
+-- Types: tle, eci_position, geodetic, topocentric, observer, pass_event
+-- Provides SGP4/SDP4 propagation, coordinate transforms, pass prediction,
+-- and GiST indexing on altitude bands for conjunction screening.
+--
+-- All propagation uses WGS-72 constants (matching TLE mean element fitting).
+-- Coordinate output uses WGS-84 (matching modern geodetic standards).
+
+-- ============================================================
+-- Shell types (forward declarations)
+-- ============================================================
+
+CREATE TYPE tle;
+CREATE TYPE eci_position;
+CREATE TYPE geodetic;
+CREATE TYPE topocentric;
+CREATE TYPE observer;
+CREATE TYPE pass_event;
+
+
+-- ============================================================
+-- TLE type: Two-Line Element set
+-- ============================================================
+
+CREATE FUNCTION tle_in(cstring) RETURNS tle
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION tle_out(tle) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION tle_recv(internal) RETURNS tle
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION tle_send(tle) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE tle (
+ INPUT = tle_in,
+ OUTPUT = tle_out,
+ RECEIVE = tle_recv,
+ SEND = tle_send,
+ INTERNALLENGTH = 112,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE tle IS 'Two-Line Element set — parsed mean orbital elements for SGP4/SDP4 propagation';
+
+-- TLE accessor functions
+
+CREATE FUNCTION tle_epoch(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_epoch(tle) IS 'TLE epoch as Julian date (UTC)';
+
+CREATE FUNCTION tle_norad_id(tle) RETURNS int4
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_norad_id(tle) IS 'NORAD catalog number';
+
+CREATE FUNCTION tle_inclination(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_inclination(tle) IS 'Orbital inclination in degrees';
+
+CREATE FUNCTION tle_eccentricity(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_eccentricity(tle) IS 'Orbital eccentricity (dimensionless)';
+
+CREATE FUNCTION tle_raan(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_raan(tle) IS 'Right ascension of ascending node in degrees';
+
+CREATE FUNCTION tle_arg_perigee(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_arg_perigee(tle) IS 'Argument of perigee in degrees';
+
+CREATE FUNCTION tle_mean_anomaly(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_mean_anomaly(tle) IS 'Mean anomaly in degrees';
+
+CREATE FUNCTION tle_mean_motion(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_mean_motion(tle) IS 'Mean motion in revolutions per day';
+
+CREATE FUNCTION tle_bstar(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_bstar(tle) IS 'B* drag coefficient (1/earth-radii)';
+
+CREATE FUNCTION tle_period(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_period(tle) IS 'Orbital period in minutes';
+
+CREATE FUNCTION tle_age(tle, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_age(tle, timestamptz) IS 'TLE age in days (positive = past epoch, negative = before epoch)';
+
+CREATE FUNCTION tle_perigee(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_perigee(tle) IS 'Perigee altitude in km above WGS-72 ellipsoid';
+
+CREATE FUNCTION tle_apogee(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_apogee(tle) IS 'Apogee altitude in km above WGS-72 ellipsoid';
+
+CREATE FUNCTION tle_intl_desig(tle) RETURNS text
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_intl_desig(tle) IS 'International designator (COSPAR ID)';
+
+CREATE FUNCTION tle_from_lines(text, text) RETURNS tle
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_from_lines(text, text) IS
+ 'Construct TLE from separate line1/line2 text columns';
+
+
+-- ============================================================
+-- ECI position type: True Equator Mean Equinox (TEME) frame
+-- ============================================================
+
+CREATE FUNCTION eci_in(cstring) RETURNS eci_position
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_out(eci_position) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_recv(internal) RETURNS eci_position
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_send(eci_position) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE eci_position (
+ INPUT = eci_in,
+ OUTPUT = eci_out,
+ RECEIVE = eci_recv,
+ SEND = eci_send,
+ INTERNALLENGTH = 48,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE eci_position IS 'Earth-Centered Inertial position and velocity in TEME frame (km, km/s)';
+
+-- ECI accessor functions
+
+CREATE FUNCTION eci_x(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_y(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_z(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_vx(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_vy(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_vz(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_speed(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION eci_speed(eci_position) IS 'Velocity magnitude in km/s';
+
+CREATE FUNCTION eci_altitude(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION eci_altitude(eci_position) IS 'Approximate geocentric altitude in km (radius - WGS72_AE)';
+
+
+-- ============================================================
+-- Geodetic type: WGS-84 latitude/longitude/altitude
+-- ============================================================
+
+CREATE FUNCTION geodetic_in(cstring) RETURNS geodetic
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION geodetic_out(geodetic) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION geodetic_recv(internal) RETURNS geodetic
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION geodetic_send(geodetic) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE geodetic (
+ INPUT = geodetic_in,
+ OUTPUT = geodetic_out,
+ RECEIVE = geodetic_recv,
+ SEND = geodetic_send,
+ INTERNALLENGTH = 24,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE geodetic IS 'Geodetic coordinates on WGS-84 ellipsoid (lat/lon in degrees, altitude in km)';
+
+CREATE FUNCTION geodetic_lat(geodetic) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION geodetic_lon(geodetic) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION geodetic_alt(geodetic) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+
+-- ============================================================
+-- Topocentric type: observer-relative az/el/range
+-- ============================================================
+
+CREATE FUNCTION topocentric_in(cstring) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION topocentric_out(topocentric) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION topocentric_recv(internal) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION topocentric_send(topocentric) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE topocentric (
+ INPUT = topocentric_in,
+ OUTPUT = topocentric_out,
+ RECEIVE = topocentric_recv,
+ SEND = topocentric_send,
+ INTERNALLENGTH = 32,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE topocentric IS 'Topocentric coordinates relative to observer (azimuth, elevation, range, range rate)';
+
+CREATE FUNCTION topo_azimuth(topocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION topo_azimuth(topocentric) IS 'Azimuth in degrees (0=N, 90=E, 180=S, 270=W)';
+
+CREATE FUNCTION topo_elevation(topocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION topo_elevation(topocentric) IS 'Elevation in degrees (0=horizon, 90=zenith)';
+
+CREATE FUNCTION topo_range(topocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION topo_range(topocentric) IS 'Slant range in km';
+
+CREATE FUNCTION topo_range_rate(topocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION topo_range_rate(topocentric) IS 'Range rate in km/s (positive = receding)';
+
+
+-- ============================================================
+-- Observer type: ground station location
+-- ============================================================
+
+CREATE FUNCTION observer_in(cstring) RETURNS observer
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION observer_out(observer) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION observer_recv(internal) RETURNS observer
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION observer_send(observer) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE observer (
+ INPUT = observer_in,
+ OUTPUT = observer_out,
+ RECEIVE = observer_recv,
+ SEND = observer_send,
+ INTERNALLENGTH = 24,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE observer IS 'Ground observer location (accepts: 40.0N 105.3W 1655m or decimal degrees)';
+
+CREATE FUNCTION observer_lat(observer) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION observer_lat(observer) IS 'Latitude in degrees (positive = North)';
+
+CREATE FUNCTION observer_lon(observer) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION observer_lon(observer) IS 'Longitude in degrees (positive = East)';
+
+CREATE FUNCTION observer_alt(observer) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION observer_alt(observer) IS 'Altitude in meters above WGS-84 ellipsoid';
+
+CREATE FUNCTION observer_from_geodetic(float8, float8, float8 DEFAULT 0.0) RETURNS observer
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION observer_from_geodetic(float8, float8, float8) IS
+ 'Construct observer from lat (deg), lon (deg), altitude (meters). Avoids text formatting round-trips.';
+
+
+-- ============================================================
+-- Pass event type: satellite visibility window
+-- ============================================================
+
+CREATE FUNCTION pass_event_in(cstring) RETURNS pass_event
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION pass_event_out(pass_event) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION pass_event_recv(internal) RETURNS pass_event
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION pass_event_send(pass_event) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE pass_event (
+ INPUT = pass_event_in,
+ OUTPUT = pass_event_out,
+ RECEIVE = pass_event_recv,
+ SEND = pass_event_send,
+ INTERNALLENGTH = 48,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE pass_event IS 'Satellite pass event (AOS/MAX/LOS times, max elevation, AOS/LOS azimuths)';
+
+CREATE FUNCTION pass_aos_time(pass_event) RETURNS timestamptz
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_aos_time(pass_event) IS 'Acquisition of signal time';
+
+CREATE FUNCTION pass_max_el_time(pass_event) RETURNS timestamptz
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_max_el_time(pass_event) IS 'Maximum elevation time';
+
+CREATE FUNCTION pass_los_time(pass_event) RETURNS timestamptz
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_los_time(pass_event) IS 'Loss of signal time';
+
+CREATE FUNCTION pass_max_elevation(pass_event) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_max_elevation(pass_event) IS 'Maximum elevation in degrees';
+
+CREATE FUNCTION pass_aos_azimuth(pass_event) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_aos_azimuth(pass_event) IS 'AOS azimuth in degrees (0=N)';
+
+CREATE FUNCTION pass_los_azimuth(pass_event) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_los_azimuth(pass_event) IS 'LOS azimuth in degrees (0=N)';
+
+CREATE FUNCTION pass_duration(pass_event) RETURNS interval
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_duration(pass_event) IS 'Pass duration (LOS - AOS)';
+
+
+-- ============================================================
+-- SGP4/SDP4 propagation functions
+-- ============================================================
+
+CREATE FUNCTION sgp4_propagate(tle, timestamptz) RETURNS eci_position
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION sgp4_propagate(tle, timestamptz) IS
+ 'Propagate TLE to a point in time using SGP4 (near-earth) or SDP4 (deep-space). Returns TEME ECI position/velocity.';
+
+CREATE FUNCTION sgp4_propagate_safe(tle, timestamptz) RETURNS eci_position
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
+COMMENT ON FUNCTION sgp4_propagate_safe(tle, timestamptz) IS
+ 'Like sgp4_propagate but returns NULL on error instead of raising an exception. For batch queries with potentially invalid TLEs.';
+
+CREATE FUNCTION sgp4_propagate_series(tle, timestamptz, timestamptz, interval)
+ RETURNS TABLE(t timestamptz, x float8, y float8, z float8, vx float8, vy float8, vz float8)
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE
+ ROWS 100;
+COMMENT ON FUNCTION sgp4_propagate_series(tle, timestamptz, timestamptz, interval) IS
+ 'Propagate TLE over a time range at regular intervals. Returns time series of TEME positions.';
+
+CREATE FUNCTION tle_distance(tle, tle, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_distance(tle, tle, timestamptz) IS
+ 'Euclidean distance in km between two TLEs at a reference time';
+
+
+-- ============================================================
+-- Coordinate transform functions
+-- ============================================================
+
+CREATE FUNCTION eci_to_geodetic(eci_position, timestamptz) RETURNS geodetic
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION eci_to_geodetic(eci_position, timestamptz) IS
+ 'Convert TEME ECI position to WGS-84 geodetic coordinates at given time';
+
+CREATE FUNCTION eci_to_topocentric(eci_position, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION eci_to_topocentric(eci_position, observer, timestamptz) IS
+ 'Convert TEME ECI position to topocentric (az/el/range) relative to observer';
+
+CREATE FUNCTION subsatellite_point(tle, timestamptz) RETURNS geodetic
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION subsatellite_point(tle, timestamptz) IS
+ 'Subsatellite (nadir) point on WGS-84 ellipsoid at given time';
+
+CREATE FUNCTION ground_track(tle, timestamptz, timestamptz, interval)
+ RETURNS TABLE(t timestamptz, lat float8, lon float8, alt float8)
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE
+ ROWS 100;
+COMMENT ON FUNCTION ground_track(tle, timestamptz, timestamptz, interval) IS
+ 'Ground track as time series of subsatellite points (lat/lon in degrees, alt in km)';
+
+CREATE FUNCTION observe(tle, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION observe(tle, observer, timestamptz) IS
+ 'Propagate TLE and compute observer-relative look angles in one call. Returns topocentric (az/el/range/range_rate).';
+
+CREATE FUNCTION observe_safe(tle, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
+COMMENT ON FUNCTION observe_safe(tle, observer, timestamptz) IS
+ 'Like observe() but returns NULL on propagation error. For batch queries with potentially invalid/decayed TLEs.';
+
+
+-- ============================================================
+-- Pass prediction functions
+-- ============================================================
+
+CREATE FUNCTION next_pass(tle, observer, timestamptz) RETURNS pass_event
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION next_pass(tle, observer, timestamptz) IS
+ 'Find the next satellite pass over observer (searches up to 7 days ahead)';
+
+CREATE FUNCTION predict_passes(tle, observer, timestamptz, timestamptz, float8 DEFAULT 0.0)
+ RETURNS SETOF pass_event
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE
+ ROWS 10;
+COMMENT ON FUNCTION predict_passes(tle, observer, timestamptz, timestamptz, float8) IS
+ 'Predict all satellite passes over observer in time window. Optional min_elevation in degrees.';
+
+CREATE FUNCTION pass_visible(tle, observer, timestamptz, timestamptz) RETURNS boolean
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_visible(tle, observer, timestamptz, timestamptz) IS
+ 'True if any pass occurs over observer in the time window';
+
+
+-- ============================================================
+-- GiST operator support functions
+-- ============================================================
+
+-- Overlap operator: do orbital keys overlap in altitude AND inclination?
+CREATE FUNCTION tle_overlap(tle, tle) RETURNS boolean
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+-- Altitude distance operator (altitude-only, for KNN ordering)
+CREATE FUNCTION tle_alt_distance(tle, tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE OPERATOR && (
+ LEFTARG = tle,
+ RIGHTARG = tle,
+ FUNCTION = tle_overlap,
+ COMMUTATOR = &&,
+ RESTRICT = areasel,
+ JOIN = areajoinsel
+);
+
+COMMENT ON OPERATOR && (tle, tle) IS 'Orbital key overlap (altitude band AND inclination range) — necessary condition for conjunction';
+
+CREATE OPERATOR <-> (
+ LEFTARG = tle,
+ RIGHTARG = tle,
+ FUNCTION = tle_alt_distance,
+ COMMUTATOR = <->
+);
+
+COMMENT ON OPERATOR <-> (tle, tle) IS 'Minimum altitude-band separation in km (0 if overlapping). Altitude-only — does not account for inclination. Use && for 2-D filtering.';
+
+
+-- ============================================================
+-- GiST operator class for 2-D orbital indexing (altitude + inclination)
+-- ============================================================
+
+-- GiST internal support functions
+CREATE FUNCTION gist_tle_compress(internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_decompress(internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_consistent(internal, tle, smallint, oid, internal) RETURNS boolean
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_union(internal, internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_penalty(internal, internal, internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_picksplit(internal, internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_same(internal, internal, internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_distance(internal, tle, smallint, oid, internal) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE OPERATOR CLASS tle_ops
+ DEFAULT FOR TYPE tle USING gist AS
+ OPERATOR 3 && ,
+ OPERATOR 15 <-> (tle, tle) FOR ORDER BY float_ops,
+ FUNCTION 1 gist_tle_consistent(internal, tle, smallint, oid, internal),
+ FUNCTION 2 gist_tle_union(internal, internal),
+ FUNCTION 3 gist_tle_compress(internal),
+ FUNCTION 4 gist_tle_decompress(internal),
+ FUNCTION 5 gist_tle_penalty(internal, internal, internal),
+ FUNCTION 6 gist_tle_picksplit(internal, internal),
+ FUNCTION 7 gist_tle_same(internal, internal, internal),
+ FUNCTION 8 gist_tle_distance(internal, tle, smallint, oid, internal);
+-- pg_orrery 0.1.0 -> 0.2.0 migration
+--
+-- Phase 1: Stars, comets, and Keplerian propagation.
+-- Adds heliocentric type, star observation, and two-body propagation.
+
+
+-- ============================================================
+-- Heliocentric type: ecliptic J2000 position in AU
+-- ============================================================
+
+CREATE TYPE heliocentric;
+
+CREATE FUNCTION heliocentric_in(cstring) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION heliocentric_out(heliocentric) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION heliocentric_recv(internal) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION heliocentric_send(heliocentric) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE heliocentric (
+ INPUT = heliocentric_in,
+ OUTPUT = heliocentric_out,
+ RECEIVE = heliocentric_recv,
+ SEND = heliocentric_send,
+ INTERNALLENGTH = 24,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE heliocentric IS 'Heliocentric position in ecliptic J2000 frame (x, y, z in AU)';
+
+CREATE FUNCTION helio_x(heliocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION helio_x(heliocentric) IS 'X component in AU (ecliptic J2000, vernal equinox direction)';
+
+CREATE FUNCTION helio_y(heliocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION helio_y(heliocentric) IS 'Y component in AU (ecliptic J2000)';
+
+CREATE FUNCTION helio_z(heliocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION helio_z(heliocentric) IS 'Z component in AU (ecliptic J2000, north ecliptic pole)';
+
+CREATE FUNCTION helio_distance(heliocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION helio_distance(heliocentric) IS 'Heliocentric distance in AU';
+
+
+-- ============================================================
+-- Star observation functions
+-- ============================================================
+
+CREATE FUNCTION star_observe(float8, float8, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION star_observe(float8, float8, observer, timestamptz) IS
+ 'Observe a star from (ra_hours J2000, dec_degrees J2000, observer, time). Returns topocentric az/el. Range is 0 (infinite distance).';
+
+CREATE FUNCTION star_observe_safe(float8, float8, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
+COMMENT ON FUNCTION star_observe_safe(float8, float8, observer, timestamptz) IS
+ 'Like star_observe but returns NULL on invalid inputs. For batch queries over star catalogs.';
+
+
+-- ============================================================
+-- Keplerian propagation functions
+-- ============================================================
+
+CREATE FUNCTION kepler_propagate(
+ q_au float8, eccentricity float8,
+ inclination_deg float8, arg_perihelion_deg float8,
+ long_asc_node_deg float8, perihelion_jd float8,
+ t timestamptz
+) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION kepler_propagate(float8, float8, float8, float8, float8, float8, timestamptz) IS
+ 'Two-body Keplerian propagation from classical orbital elements. Returns heliocentric ecliptic J2000 position in AU. Handles elliptic, parabolic, and hyperbolic orbits.';
+
+
+-- ============================================================
+-- Comet observation
+-- ============================================================
+
+CREATE FUNCTION comet_observe(
+ q_au float8, eccentricity float8,
+ inclination_deg float8, arg_perihelion_deg float8,
+ long_asc_node_deg float8, perihelion_jd float8,
+ earth_x_au float8, earth_y_au float8, earth_z_au float8,
+ obs observer, t timestamptz
+) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION comet_observe(float8, float8, float8, float8, float8, float8, float8, float8, float8, observer, timestamptz) IS
+ 'Observe a comet/asteroid from orbital elements. Requires Earth heliocentric ecliptic J2000 position (AU). Returns topocentric az/el with geocentric range in km.';
+
+
+-- ============================================================
+-- Phase 2: VSOP87 planets, ELP82B Moon, Sun observation
+-- ============================================================
+
+CREATE FUNCTION planet_heliocentric(int4, timestamptz) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION planet_heliocentric(int4, timestamptz) IS
+ 'VSOP87 heliocentric ecliptic J2000 position (AU). Body IDs: 0=Sun, 1=Mercury, ..., 8=Neptune.';
+
+CREATE FUNCTION planet_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION planet_observe(int4, observer, timestamptz) IS
+ 'Observe a planet from (body_id 1-8, observer, time). Returns topocentric az/el with geocentric range in km.';
+
+CREATE FUNCTION sun_observe(observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION sun_observe(observer, timestamptz) IS
+ 'Observe the Sun from (observer, time). Returns topocentric az/el with geocentric range in km.';
+
+CREATE FUNCTION moon_observe(observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION moon_observe(observer, timestamptz) IS
+ 'Observe the Moon via ELP2000-82B from (observer, time). Returns topocentric az/el with geocentric range in km.';
+
+
+-- ============================================================
+-- Phase 3: Planetary moon observation
+-- ============================================================
+
+CREATE FUNCTION galilean_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION galilean_observe(int4, observer, timestamptz) IS
+ 'Observe a Galilean moon of Jupiter via L1.2 theory. Body IDs: 0=Io, 1=Europa, 2=Ganymede, 3=Callisto.';
+
+CREATE FUNCTION saturn_moon_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION saturn_moon_observe(int4, observer, timestamptz) IS
+ 'Observe a Saturn moon via TASS 1.7. Body IDs: 0=Mimas, 1=Enceladus, 2=Tethys, 3=Dione, 4=Rhea, 5=Titan, 6=Iapetus, 7=Hyperion.';
+
+CREATE FUNCTION uranus_moon_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION uranus_moon_observe(int4, observer, timestamptz) IS
+ 'Observe a Uranus moon via GUST86. Body IDs: 0=Miranda, 1=Ariel, 2=Umbriel, 3=Titania, 4=Oberon.';
+
+CREATE FUNCTION mars_moon_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION mars_moon_observe(int4, observer, timestamptz) IS
+ 'Observe a Mars moon via MarsSat. Body IDs: 0=Phobos, 1=Deimos.';
+
+
+-- ============================================================
+-- Phase 3: Jupiter decametric radio burst prediction
+-- ============================================================
+
+CREATE FUNCTION io_phase_angle(timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION io_phase_angle(timestamptz) IS
+ 'Io orbital phase angle in degrees [0,360). 0=superior conjunction (behind Jupiter). Standard Radio JOVE convention.';
+
+CREATE FUNCTION jupiter_cml(observer, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION jupiter_cml(observer, timestamptz) IS
+ 'Jupiter Central Meridian Longitude, System III (1965.0), in degrees [0,360). Light-time corrected.';
+
+CREATE FUNCTION jupiter_burst_probability(float8, float8) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION jupiter_burst_probability(float8, float8) IS
+ 'Estimated Jupiter decametric burst probability (0-1) from (io_phase_deg, cml_deg). Based on Carr et al. (1983) source regions A, B, C, D.';
+
+
+-- ============================================================
+-- Phase 4: Interplanetary transfer orbits (Lambert solver)
+-- ============================================================
+
+CREATE FUNCTION lambert_transfer(
+ dep_body_id int4, arr_body_id int4,
+ dep_time timestamptz, arr_time timestamptz,
+ OUT c3_departure float8, OUT c3_arrival float8,
+ OUT v_inf_departure float8, OUT v_inf_arrival float8,
+ OUT tof_days float8, OUT transfer_sma float8
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION lambert_transfer(int4, int4, timestamptz, timestamptz) IS
+ 'Solve Lambert transfer between two planets. Returns C3 (km^2/s^2), v_infinity (km/s), TOF (days), SMA (AU). Body IDs 1-8.';
+
+CREATE FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) IS
+ 'Departure C3 (km^2/s^2) for a Lambert transfer. Returns NULL if solver fails. For pork chop plots.';
diff --git a/sql/pg_orrery--0.3.0--0.4.0.sql b/sql/pg_orrery--0.3.0--0.4.0.sql
new file mode 100644
index 0000000..201452f
--- /dev/null
+++ b/sql/pg_orrery--0.3.0--0.4.0.sql
@@ -0,0 +1,64 @@
+-- pg_orrery 0.3.0 -> 0.4.0 migration
+--
+-- Adds observation-to-TLE fitting via batch weighted least-squares
+-- differential correction (Vallado & Crawford 2008, AIAA 2008-6770).
+-- Uses equinoctial elements internally for singularity-free optimization.
+-- LAPACK dgelss_() for SVD solve.
+
+-- ============================================================
+-- Phase 6: Orbit determination (TLE fitting from observations)
+-- ============================================================
+
+-- Fit TLE from ECI position/velocity ephemeris
+
+CREATE FUNCTION tle_from_eci(
+ positions eci_position[],
+ times timestamptz[],
+ seed tle DEFAULT NULL,
+ fit_bstar boolean DEFAULT false,
+ max_iter int4 DEFAULT 15,
+ OUT fitted_tle tle,
+ OUT iterations int4,
+ OUT rms_final float8,
+ OUT rms_initial float8,
+ OUT status text
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
+COMMENT ON FUNCTION tle_from_eci(eci_position[], timestamptz[], tle, boolean, int4) IS
+ 'Fit a TLE from ECI position/velocity observations via differential correction. Returns fitted TLE, iteration count, RMS residuals, and convergence status. Requires >= 6 observations.';
+
+-- Fit TLE from topocentric observations (az/el/range)
+
+CREATE FUNCTION tle_from_topocentric(
+ observations topocentric[],
+ times timestamptz[],
+ obs observer,
+ seed tle DEFAULT NULL,
+ fit_bstar boolean DEFAULT false,
+ max_iter int4 DEFAULT 15,
+ OUT fitted_tle tle,
+ OUT iterations int4,
+ OUT rms_final float8,
+ OUT rms_initial float8,
+ OUT status text
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
+COMMENT ON FUNCTION tle_from_topocentric(topocentric[], timestamptz[], observer, tle, boolean, int4) IS
+ 'Fit a TLE from topocentric (az/el/range) observations via differential correction. Requires seed TLE and >= 6 observations.';
+
+-- Per-observation residuals diagnostic
+
+CREATE FUNCTION tle_fit_residuals(
+ fitted tle,
+ positions eci_position[],
+ times timestamptz[]
+) RETURNS TABLE (
+ t timestamptz,
+ dx_km float8,
+ dy_km float8,
+ dz_km float8,
+ pos_err_km float8
+)
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_fit_residuals(tle, eci_position[], timestamptz[]) IS
+ 'Compute per-observation position residuals (km) between a TLE and ECI observations. Useful for fit quality assessment.';
diff --git a/sql/pg_orrery--0.3.0.sql b/sql/pg_orrery--0.3.0.sql
new file mode 100644
index 0000000..57e31f9
--- /dev/null
+++ b/sql/pg_orrery--0.3.0.sql
@@ -0,0 +1,790 @@
+-- pg_orrery -- Orbital mechanics types and functions for PostgreSQL
+--
+-- Types: tle, eci_position, geodetic, topocentric, observer, pass_event
+-- Provides SGP4/SDP4 propagation, coordinate transforms, pass prediction,
+-- and GiST indexing on altitude bands for conjunction screening.
+--
+-- All propagation uses WGS-72 constants (matching TLE mean element fitting).
+-- Coordinate output uses WGS-84 (matching modern geodetic standards).
+
+-- ============================================================
+-- Shell types (forward declarations)
+-- ============================================================
+
+CREATE TYPE tle;
+CREATE TYPE eci_position;
+CREATE TYPE geodetic;
+CREATE TYPE topocentric;
+CREATE TYPE observer;
+CREATE TYPE pass_event;
+
+
+-- ============================================================
+-- TLE type: Two-Line Element set
+-- ============================================================
+
+CREATE FUNCTION tle_in(cstring) RETURNS tle
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION tle_out(tle) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION tle_recv(internal) RETURNS tle
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION tle_send(tle) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE tle (
+ INPUT = tle_in,
+ OUTPUT = tle_out,
+ RECEIVE = tle_recv,
+ SEND = tle_send,
+ INTERNALLENGTH = 112,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE tle IS 'Two-Line Element set — parsed mean orbital elements for SGP4/SDP4 propagation';
+
+-- TLE accessor functions
+
+CREATE FUNCTION tle_epoch(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_epoch(tle) IS 'TLE epoch as Julian date (UTC)';
+
+CREATE FUNCTION tle_norad_id(tle) RETURNS int4
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_norad_id(tle) IS 'NORAD catalog number';
+
+CREATE FUNCTION tle_inclination(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_inclination(tle) IS 'Orbital inclination in degrees';
+
+CREATE FUNCTION tle_eccentricity(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_eccentricity(tle) IS 'Orbital eccentricity (dimensionless)';
+
+CREATE FUNCTION tle_raan(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_raan(tle) IS 'Right ascension of ascending node in degrees';
+
+CREATE FUNCTION tle_arg_perigee(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_arg_perigee(tle) IS 'Argument of perigee in degrees';
+
+CREATE FUNCTION tle_mean_anomaly(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_mean_anomaly(tle) IS 'Mean anomaly in degrees';
+
+CREATE FUNCTION tle_mean_motion(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_mean_motion(tle) IS 'Mean motion in revolutions per day';
+
+CREATE FUNCTION tle_bstar(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_bstar(tle) IS 'B* drag coefficient (1/earth-radii)';
+
+CREATE FUNCTION tle_period(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_period(tle) IS 'Orbital period in minutes';
+
+CREATE FUNCTION tle_age(tle, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_age(tle, timestamptz) IS 'TLE age in days (positive = past epoch, negative = before epoch)';
+
+CREATE FUNCTION tle_perigee(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_perigee(tle) IS 'Perigee altitude in km above WGS-72 ellipsoid';
+
+CREATE FUNCTION tle_apogee(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_apogee(tle) IS 'Apogee altitude in km above WGS-72 ellipsoid';
+
+CREATE FUNCTION tle_intl_desig(tle) RETURNS text
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_intl_desig(tle) IS 'International designator (COSPAR ID)';
+
+CREATE FUNCTION tle_from_lines(text, text) RETURNS tle
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_from_lines(text, text) IS
+ 'Construct TLE from separate line1/line2 text columns';
+
+
+-- ============================================================
+-- ECI position type: True Equator Mean Equinox (TEME) frame
+-- ============================================================
+
+CREATE FUNCTION eci_in(cstring) RETURNS eci_position
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_out(eci_position) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_recv(internal) RETURNS eci_position
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_send(eci_position) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE eci_position (
+ INPUT = eci_in,
+ OUTPUT = eci_out,
+ RECEIVE = eci_recv,
+ SEND = eci_send,
+ INTERNALLENGTH = 48,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE eci_position IS 'Earth-Centered Inertial position and velocity in TEME frame (km, km/s)';
+
+-- ECI accessor functions
+
+CREATE FUNCTION eci_x(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_y(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_z(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_vx(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_vy(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_vz(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_speed(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION eci_speed(eci_position) IS 'Velocity magnitude in km/s';
+
+CREATE FUNCTION eci_altitude(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION eci_altitude(eci_position) IS 'Approximate geocentric altitude in km (radius - WGS72_AE)';
+
+
+-- ============================================================
+-- Geodetic type: WGS-84 latitude/longitude/altitude
+-- ============================================================
+
+CREATE FUNCTION geodetic_in(cstring) RETURNS geodetic
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION geodetic_out(geodetic) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION geodetic_recv(internal) RETURNS geodetic
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION geodetic_send(geodetic) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE geodetic (
+ INPUT = geodetic_in,
+ OUTPUT = geodetic_out,
+ RECEIVE = geodetic_recv,
+ SEND = geodetic_send,
+ INTERNALLENGTH = 24,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE geodetic IS 'Geodetic coordinates on WGS-84 ellipsoid (lat/lon in degrees, altitude in km)';
+
+CREATE FUNCTION geodetic_lat(geodetic) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION geodetic_lon(geodetic) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION geodetic_alt(geodetic) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+
+-- ============================================================
+-- Topocentric type: observer-relative az/el/range
+-- ============================================================
+
+CREATE FUNCTION topocentric_in(cstring) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION topocentric_out(topocentric) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION topocentric_recv(internal) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION topocentric_send(topocentric) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE topocentric (
+ INPUT = topocentric_in,
+ OUTPUT = topocentric_out,
+ RECEIVE = topocentric_recv,
+ SEND = topocentric_send,
+ INTERNALLENGTH = 32,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE topocentric IS 'Topocentric coordinates relative to observer (azimuth, elevation, range, range rate)';
+
+CREATE FUNCTION topo_azimuth(topocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION topo_azimuth(topocentric) IS 'Azimuth in degrees (0=N, 90=E, 180=S, 270=W)';
+
+CREATE FUNCTION topo_elevation(topocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION topo_elevation(topocentric) IS 'Elevation in degrees (0=horizon, 90=zenith)';
+
+CREATE FUNCTION topo_range(topocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION topo_range(topocentric) IS 'Slant range in km';
+
+CREATE FUNCTION topo_range_rate(topocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION topo_range_rate(topocentric) IS 'Range rate in km/s (positive = receding)';
+
+
+-- ============================================================
+-- Observer type: ground station location
+-- ============================================================
+
+CREATE FUNCTION observer_in(cstring) RETURNS observer
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION observer_out(observer) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION observer_recv(internal) RETURNS observer
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION observer_send(observer) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE observer (
+ INPUT = observer_in,
+ OUTPUT = observer_out,
+ RECEIVE = observer_recv,
+ SEND = observer_send,
+ INTERNALLENGTH = 24,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE observer IS 'Ground observer location (accepts: 40.0N 105.3W 1655m or decimal degrees)';
+
+CREATE FUNCTION observer_lat(observer) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION observer_lat(observer) IS 'Latitude in degrees (positive = North)';
+
+CREATE FUNCTION observer_lon(observer) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION observer_lon(observer) IS 'Longitude in degrees (positive = East)';
+
+CREATE FUNCTION observer_alt(observer) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION observer_alt(observer) IS 'Altitude in meters above WGS-84 ellipsoid';
+
+CREATE FUNCTION observer_from_geodetic(float8, float8, float8 DEFAULT 0.0) RETURNS observer
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION observer_from_geodetic(float8, float8, float8) IS
+ 'Construct observer from lat (deg), lon (deg), altitude (meters). Avoids text formatting round-trips.';
+
+
+-- ============================================================
+-- Pass event type: satellite visibility window
+-- ============================================================
+
+CREATE FUNCTION pass_event_in(cstring) RETURNS pass_event
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION pass_event_out(pass_event) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION pass_event_recv(internal) RETURNS pass_event
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION pass_event_send(pass_event) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE pass_event (
+ INPUT = pass_event_in,
+ OUTPUT = pass_event_out,
+ RECEIVE = pass_event_recv,
+ SEND = pass_event_send,
+ INTERNALLENGTH = 48,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE pass_event IS 'Satellite pass event (AOS/MAX/LOS times, max elevation, AOS/LOS azimuths)';
+
+CREATE FUNCTION pass_aos_time(pass_event) RETURNS timestamptz
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_aos_time(pass_event) IS 'Acquisition of signal time';
+
+CREATE FUNCTION pass_max_el_time(pass_event) RETURNS timestamptz
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_max_el_time(pass_event) IS 'Maximum elevation time';
+
+CREATE FUNCTION pass_los_time(pass_event) RETURNS timestamptz
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_los_time(pass_event) IS 'Loss of signal time';
+
+CREATE FUNCTION pass_max_elevation(pass_event) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_max_elevation(pass_event) IS 'Maximum elevation in degrees';
+
+CREATE FUNCTION pass_aos_azimuth(pass_event) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_aos_azimuth(pass_event) IS 'AOS azimuth in degrees (0=N)';
+
+CREATE FUNCTION pass_los_azimuth(pass_event) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_los_azimuth(pass_event) IS 'LOS azimuth in degrees (0=N)';
+
+CREATE FUNCTION pass_duration(pass_event) RETURNS interval
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_duration(pass_event) IS 'Pass duration (LOS - AOS)';
+
+
+-- ============================================================
+-- SGP4/SDP4 propagation functions
+-- ============================================================
+
+CREATE FUNCTION sgp4_propagate(tle, timestamptz) RETURNS eci_position
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION sgp4_propagate(tle, timestamptz) IS
+ 'Propagate TLE to a point in time using SGP4 (near-earth) or SDP4 (deep-space). Returns TEME ECI position/velocity.';
+
+CREATE FUNCTION sgp4_propagate_safe(tle, timestamptz) RETURNS eci_position
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
+COMMENT ON FUNCTION sgp4_propagate_safe(tle, timestamptz) IS
+ 'Like sgp4_propagate but returns NULL on error instead of raising an exception. For batch queries with potentially invalid TLEs.';
+
+CREATE FUNCTION sgp4_propagate_series(tle, timestamptz, timestamptz, interval)
+ RETURNS TABLE(t timestamptz, x float8, y float8, z float8, vx float8, vy float8, vz float8)
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE
+ ROWS 100;
+COMMENT ON FUNCTION sgp4_propagate_series(tle, timestamptz, timestamptz, interval) IS
+ 'Propagate TLE over a time range at regular intervals. Returns time series of TEME positions.';
+
+CREATE FUNCTION tle_distance(tle, tle, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_distance(tle, tle, timestamptz) IS
+ 'Euclidean distance in km between two TLEs at a reference time';
+
+
+-- ============================================================
+-- Coordinate transform functions
+-- ============================================================
+
+CREATE FUNCTION eci_to_geodetic(eci_position, timestamptz) RETURNS geodetic
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION eci_to_geodetic(eci_position, timestamptz) IS
+ 'Convert TEME ECI position to WGS-84 geodetic coordinates at given time';
+
+CREATE FUNCTION eci_to_topocentric(eci_position, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION eci_to_topocentric(eci_position, observer, timestamptz) IS
+ 'Convert TEME ECI position to topocentric (az/el/range) relative to observer';
+
+CREATE FUNCTION subsatellite_point(tle, timestamptz) RETURNS geodetic
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION subsatellite_point(tle, timestamptz) IS
+ 'Subsatellite (nadir) point on WGS-84 ellipsoid at given time';
+
+CREATE FUNCTION ground_track(tle, timestamptz, timestamptz, interval)
+ RETURNS TABLE(t timestamptz, lat float8, lon float8, alt float8)
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE
+ ROWS 100;
+COMMENT ON FUNCTION ground_track(tle, timestamptz, timestamptz, interval) IS
+ 'Ground track as time series of subsatellite points (lat/lon in degrees, alt in km)';
+
+CREATE FUNCTION observe(tle, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION observe(tle, observer, timestamptz) IS
+ 'Propagate TLE and compute observer-relative look angles in one call. Returns topocentric (az/el/range/range_rate).';
+
+CREATE FUNCTION observe_safe(tle, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
+COMMENT ON FUNCTION observe_safe(tle, observer, timestamptz) IS
+ 'Like observe() but returns NULL on propagation error. For batch queries with potentially invalid/decayed TLEs.';
+
+
+-- ============================================================
+-- Pass prediction functions
+-- ============================================================
+
+CREATE FUNCTION next_pass(tle, observer, timestamptz) RETURNS pass_event
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION next_pass(tle, observer, timestamptz) IS
+ 'Find the next satellite pass over observer (searches up to 7 days ahead)';
+
+CREATE FUNCTION predict_passes(tle, observer, timestamptz, timestamptz, float8 DEFAULT 0.0)
+ RETURNS SETOF pass_event
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE
+ ROWS 10;
+COMMENT ON FUNCTION predict_passes(tle, observer, timestamptz, timestamptz, float8) IS
+ 'Predict all satellite passes over observer in time window. Optional min_elevation in degrees.';
+
+CREATE FUNCTION pass_visible(tle, observer, timestamptz, timestamptz) RETURNS boolean
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_visible(tle, observer, timestamptz, timestamptz) IS
+ 'True if any pass occurs over observer in the time window';
+
+
+-- ============================================================
+-- GiST operator support functions
+-- ============================================================
+
+-- Overlap operator: do orbital keys overlap in altitude AND inclination?
+CREATE FUNCTION tle_overlap(tle, tle) RETURNS boolean
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+-- Altitude distance operator (altitude-only, for KNN ordering)
+CREATE FUNCTION tle_alt_distance(tle, tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE OPERATOR && (
+ LEFTARG = tle,
+ RIGHTARG = tle,
+ FUNCTION = tle_overlap,
+ COMMUTATOR = &&,
+ RESTRICT = areasel,
+ JOIN = areajoinsel
+);
+
+COMMENT ON OPERATOR && (tle, tle) IS 'Orbital key overlap (altitude band AND inclination range) — necessary condition for conjunction';
+
+CREATE OPERATOR <-> (
+ LEFTARG = tle,
+ RIGHTARG = tle,
+ FUNCTION = tle_alt_distance,
+ COMMUTATOR = <->
+);
+
+COMMENT ON OPERATOR <-> (tle, tle) IS 'Minimum altitude-band separation in km (0 if overlapping). Altitude-only — does not account for inclination. Use && for 2-D filtering.';
+
+
+-- ============================================================
+-- GiST operator class for 2-D orbital indexing (altitude + inclination)
+-- ============================================================
+
+-- GiST internal support functions
+CREATE FUNCTION gist_tle_compress(internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_decompress(internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_consistent(internal, tle, smallint, oid, internal) RETURNS boolean
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_union(internal, internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_penalty(internal, internal, internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_picksplit(internal, internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_same(internal, internal, internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_distance(internal, tle, smallint, oid, internal) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE OPERATOR CLASS tle_ops
+ DEFAULT FOR TYPE tle USING gist AS
+ OPERATOR 3 && ,
+ OPERATOR 15 <-> (tle, tle) FOR ORDER BY float_ops,
+ FUNCTION 1 gist_tle_consistent(internal, tle, smallint, oid, internal),
+ FUNCTION 2 gist_tle_union(internal, internal),
+ FUNCTION 3 gist_tle_compress(internal),
+ FUNCTION 4 gist_tle_decompress(internal),
+ FUNCTION 5 gist_tle_penalty(internal, internal, internal),
+ FUNCTION 6 gist_tle_picksplit(internal, internal),
+ FUNCTION 7 gist_tle_same(internal, internal, internal),
+ FUNCTION 8 gist_tle_distance(internal, tle, smallint, oid, internal);
+-- pg_orrery 0.1.0 -> 0.2.0 migration
+--
+-- Phase 1: Stars, comets, and Keplerian propagation.
+-- Adds heliocentric type, star observation, and two-body propagation.
+
+
+-- ============================================================
+-- Heliocentric type: ecliptic J2000 position in AU
+-- ============================================================
+
+CREATE TYPE heliocentric;
+
+CREATE FUNCTION heliocentric_in(cstring) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION heliocentric_out(heliocentric) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION heliocentric_recv(internal) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION heliocentric_send(heliocentric) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE heliocentric (
+ INPUT = heliocentric_in,
+ OUTPUT = heliocentric_out,
+ RECEIVE = heliocentric_recv,
+ SEND = heliocentric_send,
+ INTERNALLENGTH = 24,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE heliocentric IS 'Heliocentric position in ecliptic J2000 frame (x, y, z in AU)';
+
+CREATE FUNCTION helio_x(heliocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION helio_x(heliocentric) IS 'X component in AU (ecliptic J2000, vernal equinox direction)';
+
+CREATE FUNCTION helio_y(heliocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION helio_y(heliocentric) IS 'Y component in AU (ecliptic J2000)';
+
+CREATE FUNCTION helio_z(heliocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION helio_z(heliocentric) IS 'Z component in AU (ecliptic J2000, north ecliptic pole)';
+
+CREATE FUNCTION helio_distance(heliocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION helio_distance(heliocentric) IS 'Heliocentric distance in AU';
+
+
+-- ============================================================
+-- Star observation functions
+-- ============================================================
+
+CREATE FUNCTION star_observe(float8, float8, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION star_observe(float8, float8, observer, timestamptz) IS
+ 'Observe a star from (ra_hours J2000, dec_degrees J2000, observer, time). Returns topocentric az/el. Range is 0 (infinite distance).';
+
+CREATE FUNCTION star_observe_safe(float8, float8, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
+COMMENT ON FUNCTION star_observe_safe(float8, float8, observer, timestamptz) IS
+ 'Like star_observe but returns NULL on invalid inputs. For batch queries over star catalogs.';
+
+
+-- ============================================================
+-- Keplerian propagation functions
+-- ============================================================
+
+CREATE FUNCTION kepler_propagate(
+ q_au float8, eccentricity float8,
+ inclination_deg float8, arg_perihelion_deg float8,
+ long_asc_node_deg float8, perihelion_jd float8,
+ t timestamptz
+) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION kepler_propagate(float8, float8, float8, float8, float8, float8, timestamptz) IS
+ 'Two-body Keplerian propagation from classical orbital elements. Returns heliocentric ecliptic J2000 position in AU. Handles elliptic, parabolic, and hyperbolic orbits.';
+
+
+-- ============================================================
+-- Comet observation
+-- ============================================================
+
+CREATE FUNCTION comet_observe(
+ q_au float8, eccentricity float8,
+ inclination_deg float8, arg_perihelion_deg float8,
+ long_asc_node_deg float8, perihelion_jd float8,
+ earth_x_au float8, earth_y_au float8, earth_z_au float8,
+ obs observer, t timestamptz
+) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION comet_observe(float8, float8, float8, float8, float8, float8, float8, float8, float8, observer, timestamptz) IS
+ 'Observe a comet/asteroid from orbital elements. Requires Earth heliocentric ecliptic J2000 position (AU). Returns topocentric az/el with geocentric range in km.';
+
+
+-- ============================================================
+-- Phase 2: VSOP87 planets, ELP82B Moon, Sun observation
+-- ============================================================
+
+CREATE FUNCTION planet_heliocentric(int4, timestamptz) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION planet_heliocentric(int4, timestamptz) IS
+ 'VSOP87 heliocentric ecliptic J2000 position (AU). Body IDs: 0=Sun, 1=Mercury, ..., 8=Neptune.';
+
+CREATE FUNCTION planet_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION planet_observe(int4, observer, timestamptz) IS
+ 'Observe a planet from (body_id 1-8, observer, time). Returns topocentric az/el with geocentric range in km.';
+
+CREATE FUNCTION sun_observe(observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION sun_observe(observer, timestamptz) IS
+ 'Observe the Sun from (observer, time). Returns topocentric az/el with geocentric range in km.';
+
+CREATE FUNCTION moon_observe(observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION moon_observe(observer, timestamptz) IS
+ 'Observe the Moon via ELP2000-82B from (observer, time). Returns topocentric az/el with geocentric range in km.';
+
+
+-- ============================================================
+-- Phase 3: Planetary moon observation
+-- ============================================================
+
+CREATE FUNCTION galilean_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION galilean_observe(int4, observer, timestamptz) IS
+ 'Observe a Galilean moon of Jupiter via L1.2 theory. Body IDs: 0=Io, 1=Europa, 2=Ganymede, 3=Callisto.';
+
+CREATE FUNCTION saturn_moon_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION saturn_moon_observe(int4, observer, timestamptz) IS
+ 'Observe a Saturn moon via TASS 1.7. Body IDs: 0=Mimas, 1=Enceladus, 2=Tethys, 3=Dione, 4=Rhea, 5=Titan, 6=Iapetus, 7=Hyperion.';
+
+CREATE FUNCTION uranus_moon_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION uranus_moon_observe(int4, observer, timestamptz) IS
+ 'Observe a Uranus moon via GUST86. Body IDs: 0=Miranda, 1=Ariel, 2=Umbriel, 3=Titania, 4=Oberon.';
+
+CREATE FUNCTION mars_moon_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION mars_moon_observe(int4, observer, timestamptz) IS
+ 'Observe a Mars moon via MarsSat. Body IDs: 0=Phobos, 1=Deimos.';
+
+
+-- ============================================================
+-- Phase 3: Jupiter decametric radio burst prediction
+-- ============================================================
+
+CREATE FUNCTION io_phase_angle(timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION io_phase_angle(timestamptz) IS
+ 'Io orbital phase angle in degrees [0,360). 0=superior conjunction (behind Jupiter). Standard Radio JOVE convention.';
+
+CREATE FUNCTION jupiter_cml(observer, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION jupiter_cml(observer, timestamptz) IS
+ 'Jupiter Central Meridian Longitude, System III (1965.0), in degrees [0,360). Light-time corrected.';
+
+CREATE FUNCTION jupiter_burst_probability(float8, float8) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION jupiter_burst_probability(float8, float8) IS
+ 'Estimated Jupiter decametric burst probability (0-1) from (io_phase_deg, cml_deg). Based on Carr et al. (1983) source regions A, B, C, D.';
+
+
+-- ============================================================
+-- Phase 4: Interplanetary transfer orbits (Lambert solver)
+-- ============================================================
+
+CREATE FUNCTION lambert_transfer(
+ dep_body_id int4, arr_body_id int4,
+ dep_time timestamptz, arr_time timestamptz,
+ OUT c3_departure float8, OUT c3_arrival float8,
+ OUT v_inf_departure float8, OUT v_inf_arrival float8,
+ OUT tof_days float8, OUT transfer_sma float8
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION lambert_transfer(int4, int4, timestamptz, timestamptz) IS
+ 'Solve Lambert transfer between two planets. Returns C3 (km^2/s^2), v_infinity (km/s), TOF (days), SMA (AU). Body IDs 1-8.';
+
+CREATE FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) IS
+ 'Departure C3 (km^2/s^2) for a Lambert transfer. Returns NULL if solver fails. For pork chop plots.';
+-- pg_orrery 0.2.0 -> 0.3.0 migration
+--
+-- Adds optional JPL DE440/441 ephemeris functions.
+-- Existing VSOP87/ELP2000-82B functions are unchanged (still IMMUTABLE).
+-- New _de() functions are STABLE (depend on external DE binary file).
+-- When DE is unavailable, _de() functions fall back to VSOP87 silently.
+
+-- ============================================================
+-- Phase 5: DE ephemeris functions (optional high-precision)
+-- ============================================================
+
+-- Planet observation with DE ephemeris
+
+CREATE FUNCTION planet_heliocentric_de(int4, timestamptz) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION planet_heliocentric_de(int4, timestamptz) IS
+ 'Heliocentric ecliptic J2000 position via JPL DE (sub-arcsecond). Falls back to VSOP87 if DE unavailable. Body IDs: 0=Sun, 1-8=Mercury-Neptune.';
+
+CREATE FUNCTION planet_observe_de(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION planet_observe_de(int4, observer, timestamptz) IS
+ 'Observe planet via JPL DE. Falls back to VSOP87. Body IDs: 1-8 (Mercury-Neptune).';
+
+CREATE FUNCTION sun_observe_de(observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION sun_observe_de(observer, timestamptz) IS
+ 'Observe Sun via JPL DE. Falls back to VSOP87.';
+
+CREATE FUNCTION moon_observe_de(observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION moon_observe_de(observer, timestamptz) IS
+ 'Observe Moon via JPL DE. Falls back to ELP2000-82B.';
+
+
+-- Lambert transfer with DE positions
+
+CREATE FUNCTION lambert_transfer_de(
+ dep_body_id int4, arr_body_id int4,
+ dep_time timestamptz, arr_time timestamptz,
+ OUT c3_departure float8, OUT c3_arrival float8,
+ OUT v_inf_departure float8, OUT v_inf_arrival float8,
+ OUT tof_days float8, OUT transfer_sma float8
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION lambert_transfer_de(int4, int4, timestamptz, timestamptz) IS
+ 'Lambert transfer via JPL DE positions. Falls back to VSOP87. Body IDs 1-8.';
+
+CREATE FUNCTION lambert_c3_de(int4, int4, timestamptz, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION lambert_c3_de(int4, int4, timestamptz, timestamptz) IS
+ 'Departure C3 via JPL DE. Falls back to VSOP87. For pork chop plots.';
+
+
+-- Planetary moon observation with DE parent positions
+
+CREATE FUNCTION galilean_observe_de(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION galilean_observe_de(int4, observer, timestamptz) IS
+ 'Observe Galilean moon with JPL DE parent position. L1.2 moon offsets. Body IDs: 0-3 (Io-Callisto).';
+
+CREATE FUNCTION saturn_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION saturn_moon_observe_de(int4, observer, timestamptz) IS
+ 'Observe Saturn moon with JPL DE parent position. TASS 1.7 moon offsets. Body IDs: 0-7 (Mimas-Hyperion).';
+
+CREATE FUNCTION uranus_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION uranus_moon_observe_de(int4, observer, timestamptz) IS
+ 'Observe Uranus moon with JPL DE parent position. GUST86 moon offsets. Body IDs: 0-4 (Miranda-Oberon).';
+
+CREATE FUNCTION mars_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION mars_moon_observe_de(int4, observer, timestamptz) IS
+ 'Observe Mars moon with JPL DE parent position. MarsSat moon offsets. Body IDs: 0-1 (Phobos-Deimos).';
+
+
+-- Diagnostic function
+
+CREATE FUNCTION pg_orrery_ephemeris_info(
+ OUT provider text, OUT file_path text,
+ OUT start_jd float8, OUT end_jd float8,
+ OUT version int4, OUT au_km float8
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
+COMMENT ON FUNCTION pg_orrery_ephemeris_info() IS
+ 'Returns current ephemeris provider status: VSOP87 or JPL_DE with file path, JD range, version, and AU value.';
diff --git a/sql/pg_orrery--0.4.0--0.5.0.sql b/sql/pg_orrery--0.4.0--0.5.0.sql
new file mode 100644
index 0000000..bc5764a
--- /dev/null
+++ b/sql/pg_orrery--0.4.0--0.5.0.sql
@@ -0,0 +1,62 @@
+-- pg_orrery 0.4.0 -> 0.5.0 migration
+--
+-- Adds multi-observer support, IOD bootstrap (seed-free fitting),
+-- and covariance output for uncertainty estimation.
+--
+-- Covariance changes the return type of tle_from_eci and
+-- tle_from_topocentric (5 → 8 OUT params), which requires
+-- DROP + re-CREATE.
+
+-- ============================================================
+-- Drop old 5-column OD functions
+-- ============================================================
+
+DROP FUNCTION IF EXISTS tle_from_eci(eci_position[], timestamptz[], tle, boolean, int4);
+DROP FUNCTION IF EXISTS tle_from_topocentric(topocentric[], timestamptz[], observer, tle, boolean, int4);
+
+-- ============================================================
+-- Re-create with 8-column output (adds covariance)
+-- ============================================================
+
+CREATE FUNCTION tle_from_eci(
+ positions eci_position[], times timestamptz[],
+ seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false,
+ max_iter int4 DEFAULT 15,
+ OUT fitted_tle tle, OUT iterations int4,
+ OUT rms_final float8, OUT rms_initial float8, OUT status text,
+ OUT condition_number float8, OUT covariance float8[], OUT nstate int4
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
+COMMENT ON FUNCTION tle_from_eci(eci_position[], timestamptz[], tle, boolean, int4) IS
+ 'Fit a TLE from ECI position/velocity observations via differential correction. Returns fitted TLE, iteration count, RMS residuals, convergence status, condition number, and formal covariance matrix.';
+
+CREATE FUNCTION tle_from_topocentric(
+ observations topocentric[], times timestamptz[],
+ obs observer,
+ seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false,
+ max_iter int4 DEFAULT 15,
+ OUT fitted_tle tle, OUT iterations int4,
+ OUT rms_final float8, OUT rms_initial float8, OUT status text,
+ OUT condition_number float8, OUT covariance float8[], OUT nstate int4
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
+COMMENT ON FUNCTION tle_from_topocentric(topocentric[], timestamptz[], observer, tle, boolean, int4) IS
+ 'Fit a TLE from topocentric (az/el/range) observations via differential correction. Returns fitted TLE, RMS residuals, convergence status, condition number, and formal covariance matrix.';
+
+-- ============================================================
+-- Multi-observer topocentric fitting (new overload)
+-- ============================================================
+
+CREATE FUNCTION tle_from_topocentric(
+ observations topocentric[], times timestamptz[],
+ observers observer[], observer_ids int4[],
+ seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false,
+ max_iter int4 DEFAULT 15,
+ OUT fitted_tle tle, OUT iterations int4,
+ OUT rms_final float8, OUT rms_initial float8, OUT status text,
+ OUT condition_number float8, OUT covariance float8[], OUT nstate int4
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME', 'tle_from_topocentric_multi'
+ LANGUAGE C STABLE PARALLEL SAFE;
+COMMENT ON FUNCTION tle_from_topocentric(topocentric[], timestamptz[], observer[], int4[], tle, boolean, int4) IS
+ 'Fit a TLE from topocentric observations collected by multiple ground stations. observer_ids[i] indexes into observers[]. Returns convergence status, condition number, and formal covariance matrix.';
diff --git a/sql/pg_orrery--0.4.0.sql b/sql/pg_orrery--0.4.0.sql
new file mode 100644
index 0000000..ae1c7d4
--- /dev/null
+++ b/sql/pg_orrery--0.4.0.sql
@@ -0,0 +1,854 @@
+-- pg_orrery -- Orbital mechanics types and functions for PostgreSQL
+--
+-- Types: tle, eci_position, geodetic, topocentric, observer, pass_event
+-- Provides SGP4/SDP4 propagation, coordinate transforms, pass prediction,
+-- and GiST indexing on altitude bands for conjunction screening.
+--
+-- All propagation uses WGS-72 constants (matching TLE mean element fitting).
+-- Coordinate output uses WGS-84 (matching modern geodetic standards).
+
+-- ============================================================
+-- Shell types (forward declarations)
+-- ============================================================
+
+CREATE TYPE tle;
+CREATE TYPE eci_position;
+CREATE TYPE geodetic;
+CREATE TYPE topocentric;
+CREATE TYPE observer;
+CREATE TYPE pass_event;
+
+
+-- ============================================================
+-- TLE type: Two-Line Element set
+-- ============================================================
+
+CREATE FUNCTION tle_in(cstring) RETURNS tle
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION tle_out(tle) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION tle_recv(internal) RETURNS tle
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION tle_send(tle) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE tle (
+ INPUT = tle_in,
+ OUTPUT = tle_out,
+ RECEIVE = tle_recv,
+ SEND = tle_send,
+ INTERNALLENGTH = 112,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE tle IS 'Two-Line Element set — parsed mean orbital elements for SGP4/SDP4 propagation';
+
+-- TLE accessor functions
+
+CREATE FUNCTION tle_epoch(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_epoch(tle) IS 'TLE epoch as Julian date (UTC)';
+
+CREATE FUNCTION tle_norad_id(tle) RETURNS int4
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_norad_id(tle) IS 'NORAD catalog number';
+
+CREATE FUNCTION tle_inclination(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_inclination(tle) IS 'Orbital inclination in degrees';
+
+CREATE FUNCTION tle_eccentricity(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_eccentricity(tle) IS 'Orbital eccentricity (dimensionless)';
+
+CREATE FUNCTION tle_raan(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_raan(tle) IS 'Right ascension of ascending node in degrees';
+
+CREATE FUNCTION tle_arg_perigee(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_arg_perigee(tle) IS 'Argument of perigee in degrees';
+
+CREATE FUNCTION tle_mean_anomaly(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_mean_anomaly(tle) IS 'Mean anomaly in degrees';
+
+CREATE FUNCTION tle_mean_motion(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_mean_motion(tle) IS 'Mean motion in revolutions per day';
+
+CREATE FUNCTION tle_bstar(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_bstar(tle) IS 'B* drag coefficient (1/earth-radii)';
+
+CREATE FUNCTION tle_period(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_period(tle) IS 'Orbital period in minutes';
+
+CREATE FUNCTION tle_age(tle, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_age(tle, timestamptz) IS 'TLE age in days (positive = past epoch, negative = before epoch)';
+
+CREATE FUNCTION tle_perigee(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_perigee(tle) IS 'Perigee altitude in km above WGS-72 ellipsoid';
+
+CREATE FUNCTION tle_apogee(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_apogee(tle) IS 'Apogee altitude in km above WGS-72 ellipsoid';
+
+CREATE FUNCTION tle_intl_desig(tle) RETURNS text
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_intl_desig(tle) IS 'International designator (COSPAR ID)';
+
+CREATE FUNCTION tle_from_lines(text, text) RETURNS tle
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_from_lines(text, text) IS
+ 'Construct TLE from separate line1/line2 text columns';
+
+
+-- ============================================================
+-- ECI position type: True Equator Mean Equinox (TEME) frame
+-- ============================================================
+
+CREATE FUNCTION eci_in(cstring) RETURNS eci_position
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_out(eci_position) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_recv(internal) RETURNS eci_position
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_send(eci_position) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE eci_position (
+ INPUT = eci_in,
+ OUTPUT = eci_out,
+ RECEIVE = eci_recv,
+ SEND = eci_send,
+ INTERNALLENGTH = 48,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE eci_position IS 'Earth-Centered Inertial position and velocity in TEME frame (km, km/s)';
+
+-- ECI accessor functions
+
+CREATE FUNCTION eci_x(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_y(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_z(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_vx(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_vy(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_vz(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_speed(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION eci_speed(eci_position) IS 'Velocity magnitude in km/s';
+
+CREATE FUNCTION eci_altitude(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION eci_altitude(eci_position) IS 'Approximate geocentric altitude in km (radius - WGS72_AE)';
+
+
+-- ============================================================
+-- Geodetic type: WGS-84 latitude/longitude/altitude
+-- ============================================================
+
+CREATE FUNCTION geodetic_in(cstring) RETURNS geodetic
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION geodetic_out(geodetic) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION geodetic_recv(internal) RETURNS geodetic
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION geodetic_send(geodetic) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE geodetic (
+ INPUT = geodetic_in,
+ OUTPUT = geodetic_out,
+ RECEIVE = geodetic_recv,
+ SEND = geodetic_send,
+ INTERNALLENGTH = 24,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE geodetic IS 'Geodetic coordinates on WGS-84 ellipsoid (lat/lon in degrees, altitude in km)';
+
+CREATE FUNCTION geodetic_lat(geodetic) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION geodetic_lon(geodetic) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION geodetic_alt(geodetic) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+
+-- ============================================================
+-- Topocentric type: observer-relative az/el/range
+-- ============================================================
+
+CREATE FUNCTION topocentric_in(cstring) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION topocentric_out(topocentric) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION topocentric_recv(internal) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION topocentric_send(topocentric) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE topocentric (
+ INPUT = topocentric_in,
+ OUTPUT = topocentric_out,
+ RECEIVE = topocentric_recv,
+ SEND = topocentric_send,
+ INTERNALLENGTH = 32,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE topocentric IS 'Topocentric coordinates relative to observer (azimuth, elevation, range, range rate)';
+
+CREATE FUNCTION topo_azimuth(topocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION topo_azimuth(topocentric) IS 'Azimuth in degrees (0=N, 90=E, 180=S, 270=W)';
+
+CREATE FUNCTION topo_elevation(topocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION topo_elevation(topocentric) IS 'Elevation in degrees (0=horizon, 90=zenith)';
+
+CREATE FUNCTION topo_range(topocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION topo_range(topocentric) IS 'Slant range in km';
+
+CREATE FUNCTION topo_range_rate(topocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION topo_range_rate(topocentric) IS 'Range rate in km/s (positive = receding)';
+
+
+-- ============================================================
+-- Observer type: ground station location
+-- ============================================================
+
+CREATE FUNCTION observer_in(cstring) RETURNS observer
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION observer_out(observer) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION observer_recv(internal) RETURNS observer
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION observer_send(observer) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE observer (
+ INPUT = observer_in,
+ OUTPUT = observer_out,
+ RECEIVE = observer_recv,
+ SEND = observer_send,
+ INTERNALLENGTH = 24,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE observer IS 'Ground observer location (accepts: 40.0N 105.3W 1655m or decimal degrees)';
+
+CREATE FUNCTION observer_lat(observer) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION observer_lat(observer) IS 'Latitude in degrees (positive = North)';
+
+CREATE FUNCTION observer_lon(observer) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION observer_lon(observer) IS 'Longitude in degrees (positive = East)';
+
+CREATE FUNCTION observer_alt(observer) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION observer_alt(observer) IS 'Altitude in meters above WGS-84 ellipsoid';
+
+CREATE FUNCTION observer_from_geodetic(float8, float8, float8 DEFAULT 0.0) RETURNS observer
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION observer_from_geodetic(float8, float8, float8) IS
+ 'Construct observer from lat (deg), lon (deg), altitude (meters). Avoids text formatting round-trips.';
+
+
+-- ============================================================
+-- Pass event type: satellite visibility window
+-- ============================================================
+
+CREATE FUNCTION pass_event_in(cstring) RETURNS pass_event
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION pass_event_out(pass_event) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION pass_event_recv(internal) RETURNS pass_event
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION pass_event_send(pass_event) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE pass_event (
+ INPUT = pass_event_in,
+ OUTPUT = pass_event_out,
+ RECEIVE = pass_event_recv,
+ SEND = pass_event_send,
+ INTERNALLENGTH = 48,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE pass_event IS 'Satellite pass event (AOS/MAX/LOS times, max elevation, AOS/LOS azimuths)';
+
+CREATE FUNCTION pass_aos_time(pass_event) RETURNS timestamptz
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_aos_time(pass_event) IS 'Acquisition of signal time';
+
+CREATE FUNCTION pass_max_el_time(pass_event) RETURNS timestamptz
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_max_el_time(pass_event) IS 'Maximum elevation time';
+
+CREATE FUNCTION pass_los_time(pass_event) RETURNS timestamptz
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_los_time(pass_event) IS 'Loss of signal time';
+
+CREATE FUNCTION pass_max_elevation(pass_event) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_max_elevation(pass_event) IS 'Maximum elevation in degrees';
+
+CREATE FUNCTION pass_aos_azimuth(pass_event) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_aos_azimuth(pass_event) IS 'AOS azimuth in degrees (0=N)';
+
+CREATE FUNCTION pass_los_azimuth(pass_event) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_los_azimuth(pass_event) IS 'LOS azimuth in degrees (0=N)';
+
+CREATE FUNCTION pass_duration(pass_event) RETURNS interval
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_duration(pass_event) IS 'Pass duration (LOS - AOS)';
+
+
+-- ============================================================
+-- SGP4/SDP4 propagation functions
+-- ============================================================
+
+CREATE FUNCTION sgp4_propagate(tle, timestamptz) RETURNS eci_position
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION sgp4_propagate(tle, timestamptz) IS
+ 'Propagate TLE to a point in time using SGP4 (near-earth) or SDP4 (deep-space). Returns TEME ECI position/velocity.';
+
+CREATE FUNCTION sgp4_propagate_safe(tle, timestamptz) RETURNS eci_position
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
+COMMENT ON FUNCTION sgp4_propagate_safe(tle, timestamptz) IS
+ 'Like sgp4_propagate but returns NULL on error instead of raising an exception. For batch queries with potentially invalid TLEs.';
+
+CREATE FUNCTION sgp4_propagate_series(tle, timestamptz, timestamptz, interval)
+ RETURNS TABLE(t timestamptz, x float8, y float8, z float8, vx float8, vy float8, vz float8)
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE
+ ROWS 100;
+COMMENT ON FUNCTION sgp4_propagate_series(tle, timestamptz, timestamptz, interval) IS
+ 'Propagate TLE over a time range at regular intervals. Returns time series of TEME positions.';
+
+CREATE FUNCTION tle_distance(tle, tle, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_distance(tle, tle, timestamptz) IS
+ 'Euclidean distance in km between two TLEs at a reference time';
+
+
+-- ============================================================
+-- Coordinate transform functions
+-- ============================================================
+
+CREATE FUNCTION eci_to_geodetic(eci_position, timestamptz) RETURNS geodetic
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION eci_to_geodetic(eci_position, timestamptz) IS
+ 'Convert TEME ECI position to WGS-84 geodetic coordinates at given time';
+
+CREATE FUNCTION eci_to_topocentric(eci_position, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION eci_to_topocentric(eci_position, observer, timestamptz) IS
+ 'Convert TEME ECI position to topocentric (az/el/range) relative to observer';
+
+CREATE FUNCTION subsatellite_point(tle, timestamptz) RETURNS geodetic
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION subsatellite_point(tle, timestamptz) IS
+ 'Subsatellite (nadir) point on WGS-84 ellipsoid at given time';
+
+CREATE FUNCTION ground_track(tle, timestamptz, timestamptz, interval)
+ RETURNS TABLE(t timestamptz, lat float8, lon float8, alt float8)
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE
+ ROWS 100;
+COMMENT ON FUNCTION ground_track(tle, timestamptz, timestamptz, interval) IS
+ 'Ground track as time series of subsatellite points (lat/lon in degrees, alt in km)';
+
+CREATE FUNCTION observe(tle, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION observe(tle, observer, timestamptz) IS
+ 'Propagate TLE and compute observer-relative look angles in one call. Returns topocentric (az/el/range/range_rate).';
+
+CREATE FUNCTION observe_safe(tle, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
+COMMENT ON FUNCTION observe_safe(tle, observer, timestamptz) IS
+ 'Like observe() but returns NULL on propagation error. For batch queries with potentially invalid/decayed TLEs.';
+
+
+-- ============================================================
+-- Pass prediction functions
+-- ============================================================
+
+CREATE FUNCTION next_pass(tle, observer, timestamptz) RETURNS pass_event
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION next_pass(tle, observer, timestamptz) IS
+ 'Find the next satellite pass over observer (searches up to 7 days ahead)';
+
+CREATE FUNCTION predict_passes(tle, observer, timestamptz, timestamptz, float8 DEFAULT 0.0)
+ RETURNS SETOF pass_event
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE
+ ROWS 10;
+COMMENT ON FUNCTION predict_passes(tle, observer, timestamptz, timestamptz, float8) IS
+ 'Predict all satellite passes over observer in time window. Optional min_elevation in degrees.';
+
+CREATE FUNCTION pass_visible(tle, observer, timestamptz, timestamptz) RETURNS boolean
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_visible(tle, observer, timestamptz, timestamptz) IS
+ 'True if any pass occurs over observer in the time window';
+
+
+-- ============================================================
+-- GiST operator support functions
+-- ============================================================
+
+-- Overlap operator: do orbital keys overlap in altitude AND inclination?
+CREATE FUNCTION tle_overlap(tle, tle) RETURNS boolean
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+-- Altitude distance operator (altitude-only, for KNN ordering)
+CREATE FUNCTION tle_alt_distance(tle, tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE OPERATOR && (
+ LEFTARG = tle,
+ RIGHTARG = tle,
+ FUNCTION = tle_overlap,
+ COMMUTATOR = &&,
+ RESTRICT = areasel,
+ JOIN = areajoinsel
+);
+
+COMMENT ON OPERATOR && (tle, tle) IS 'Orbital key overlap (altitude band AND inclination range) — necessary condition for conjunction';
+
+CREATE OPERATOR <-> (
+ LEFTARG = tle,
+ RIGHTARG = tle,
+ FUNCTION = tle_alt_distance,
+ COMMUTATOR = <->
+);
+
+COMMENT ON OPERATOR <-> (tle, tle) IS 'Minimum altitude-band separation in km (0 if overlapping). Altitude-only — does not account for inclination. Use && for 2-D filtering.';
+
+
+-- ============================================================
+-- GiST operator class for 2-D orbital indexing (altitude + inclination)
+-- ============================================================
+
+-- GiST internal support functions
+CREATE FUNCTION gist_tle_compress(internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_decompress(internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_consistent(internal, tle, smallint, oid, internal) RETURNS boolean
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_union(internal, internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_penalty(internal, internal, internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_picksplit(internal, internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_same(internal, internal, internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_distance(internal, tle, smallint, oid, internal) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE OPERATOR CLASS tle_ops
+ DEFAULT FOR TYPE tle USING gist AS
+ OPERATOR 3 && ,
+ OPERATOR 15 <-> (tle, tle) FOR ORDER BY float_ops,
+ FUNCTION 1 gist_tle_consistent(internal, tle, smallint, oid, internal),
+ FUNCTION 2 gist_tle_union(internal, internal),
+ FUNCTION 3 gist_tle_compress(internal),
+ FUNCTION 4 gist_tle_decompress(internal),
+ FUNCTION 5 gist_tle_penalty(internal, internal, internal),
+ FUNCTION 6 gist_tle_picksplit(internal, internal),
+ FUNCTION 7 gist_tle_same(internal, internal, internal),
+ FUNCTION 8 gist_tle_distance(internal, tle, smallint, oid, internal);
+-- pg_orrery 0.1.0 -> 0.2.0 migration
+--
+-- Phase 1: Stars, comets, and Keplerian propagation.
+-- Adds heliocentric type, star observation, and two-body propagation.
+
+
+-- ============================================================
+-- Heliocentric type: ecliptic J2000 position in AU
+-- ============================================================
+
+CREATE TYPE heliocentric;
+
+CREATE FUNCTION heliocentric_in(cstring) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION heliocentric_out(heliocentric) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION heliocentric_recv(internal) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION heliocentric_send(heliocentric) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE heliocentric (
+ INPUT = heliocentric_in,
+ OUTPUT = heliocentric_out,
+ RECEIVE = heliocentric_recv,
+ SEND = heliocentric_send,
+ INTERNALLENGTH = 24,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE heliocentric IS 'Heliocentric position in ecliptic J2000 frame (x, y, z in AU)';
+
+CREATE FUNCTION helio_x(heliocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION helio_x(heliocentric) IS 'X component in AU (ecliptic J2000, vernal equinox direction)';
+
+CREATE FUNCTION helio_y(heliocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION helio_y(heliocentric) IS 'Y component in AU (ecliptic J2000)';
+
+CREATE FUNCTION helio_z(heliocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION helio_z(heliocentric) IS 'Z component in AU (ecliptic J2000, north ecliptic pole)';
+
+CREATE FUNCTION helio_distance(heliocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION helio_distance(heliocentric) IS 'Heliocentric distance in AU';
+
+
+-- ============================================================
+-- Star observation functions
+-- ============================================================
+
+CREATE FUNCTION star_observe(float8, float8, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION star_observe(float8, float8, observer, timestamptz) IS
+ 'Observe a star from (ra_hours J2000, dec_degrees J2000, observer, time). Returns topocentric az/el. Range is 0 (infinite distance).';
+
+CREATE FUNCTION star_observe_safe(float8, float8, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
+COMMENT ON FUNCTION star_observe_safe(float8, float8, observer, timestamptz) IS
+ 'Like star_observe but returns NULL on invalid inputs. For batch queries over star catalogs.';
+
+
+-- ============================================================
+-- Keplerian propagation functions
+-- ============================================================
+
+CREATE FUNCTION kepler_propagate(
+ q_au float8, eccentricity float8,
+ inclination_deg float8, arg_perihelion_deg float8,
+ long_asc_node_deg float8, perihelion_jd float8,
+ t timestamptz
+) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION kepler_propagate(float8, float8, float8, float8, float8, float8, timestamptz) IS
+ 'Two-body Keplerian propagation from classical orbital elements. Returns heliocentric ecliptic J2000 position in AU. Handles elliptic, parabolic, and hyperbolic orbits.';
+
+
+-- ============================================================
+-- Comet observation
+-- ============================================================
+
+CREATE FUNCTION comet_observe(
+ q_au float8, eccentricity float8,
+ inclination_deg float8, arg_perihelion_deg float8,
+ long_asc_node_deg float8, perihelion_jd float8,
+ earth_x_au float8, earth_y_au float8, earth_z_au float8,
+ obs observer, t timestamptz
+) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION comet_observe(float8, float8, float8, float8, float8, float8, float8, float8, float8, observer, timestamptz) IS
+ 'Observe a comet/asteroid from orbital elements. Requires Earth heliocentric ecliptic J2000 position (AU). Returns topocentric az/el with geocentric range in km.';
+
+
+-- ============================================================
+-- Phase 2: VSOP87 planets, ELP82B Moon, Sun observation
+-- ============================================================
+
+CREATE FUNCTION planet_heliocentric(int4, timestamptz) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION planet_heliocentric(int4, timestamptz) IS
+ 'VSOP87 heliocentric ecliptic J2000 position (AU). Body IDs: 0=Sun, 1=Mercury, ..., 8=Neptune.';
+
+CREATE FUNCTION planet_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION planet_observe(int4, observer, timestamptz) IS
+ 'Observe a planet from (body_id 1-8, observer, time). Returns topocentric az/el with geocentric range in km.';
+
+CREATE FUNCTION sun_observe(observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION sun_observe(observer, timestamptz) IS
+ 'Observe the Sun from (observer, time). Returns topocentric az/el with geocentric range in km.';
+
+CREATE FUNCTION moon_observe(observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION moon_observe(observer, timestamptz) IS
+ 'Observe the Moon via ELP2000-82B from (observer, time). Returns topocentric az/el with geocentric range in km.';
+
+
+-- ============================================================
+-- Phase 3: Planetary moon observation
+-- ============================================================
+
+CREATE FUNCTION galilean_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION galilean_observe(int4, observer, timestamptz) IS
+ 'Observe a Galilean moon of Jupiter via L1.2 theory. Body IDs: 0=Io, 1=Europa, 2=Ganymede, 3=Callisto.';
+
+CREATE FUNCTION saturn_moon_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION saturn_moon_observe(int4, observer, timestamptz) IS
+ 'Observe a Saturn moon via TASS 1.7. Body IDs: 0=Mimas, 1=Enceladus, 2=Tethys, 3=Dione, 4=Rhea, 5=Titan, 6=Iapetus, 7=Hyperion.';
+
+CREATE FUNCTION uranus_moon_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION uranus_moon_observe(int4, observer, timestamptz) IS
+ 'Observe a Uranus moon via GUST86. Body IDs: 0=Miranda, 1=Ariel, 2=Umbriel, 3=Titania, 4=Oberon.';
+
+CREATE FUNCTION mars_moon_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION mars_moon_observe(int4, observer, timestamptz) IS
+ 'Observe a Mars moon via MarsSat. Body IDs: 0=Phobos, 1=Deimos.';
+
+
+-- ============================================================
+-- Phase 3: Jupiter decametric radio burst prediction
+-- ============================================================
+
+CREATE FUNCTION io_phase_angle(timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION io_phase_angle(timestamptz) IS
+ 'Io orbital phase angle in degrees [0,360). 0=superior conjunction (behind Jupiter). Standard Radio JOVE convention.';
+
+CREATE FUNCTION jupiter_cml(observer, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION jupiter_cml(observer, timestamptz) IS
+ 'Jupiter Central Meridian Longitude, System III (1965.0), in degrees [0,360). Light-time corrected.';
+
+CREATE FUNCTION jupiter_burst_probability(float8, float8) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION jupiter_burst_probability(float8, float8) IS
+ 'Estimated Jupiter decametric burst probability (0-1) from (io_phase_deg, cml_deg). Based on Carr et al. (1983) source regions A, B, C, D.';
+
+
+-- ============================================================
+-- Phase 4: Interplanetary transfer orbits (Lambert solver)
+-- ============================================================
+
+CREATE FUNCTION lambert_transfer(
+ dep_body_id int4, arr_body_id int4,
+ dep_time timestamptz, arr_time timestamptz,
+ OUT c3_departure float8, OUT c3_arrival float8,
+ OUT v_inf_departure float8, OUT v_inf_arrival float8,
+ OUT tof_days float8, OUT transfer_sma float8
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION lambert_transfer(int4, int4, timestamptz, timestamptz) IS
+ 'Solve Lambert transfer between two planets. Returns C3 (km^2/s^2), v_infinity (km/s), TOF (days), SMA (AU). Body IDs 1-8.';
+
+CREATE FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) IS
+ 'Departure C3 (km^2/s^2) for a Lambert transfer. Returns NULL if solver fails. For pork chop plots.';
+-- pg_orrery 0.2.0 -> 0.3.0 migration
+--
+-- Adds optional JPL DE440/441 ephemeris functions.
+-- Existing VSOP87/ELP2000-82B functions are unchanged (still IMMUTABLE).
+-- New _de() functions are STABLE (depend on external DE binary file).
+-- When DE is unavailable, _de() functions fall back to VSOP87 silently.
+
+-- ============================================================
+-- Phase 5: DE ephemeris functions (optional high-precision)
+-- ============================================================
+
+-- Planet observation with DE ephemeris
+
+CREATE FUNCTION planet_heliocentric_de(int4, timestamptz) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION planet_heliocentric_de(int4, timestamptz) IS
+ 'Heliocentric ecliptic J2000 position via JPL DE (sub-arcsecond). Falls back to VSOP87 if DE unavailable. Body IDs: 0=Sun, 1-8=Mercury-Neptune.';
+
+CREATE FUNCTION planet_observe_de(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION planet_observe_de(int4, observer, timestamptz) IS
+ 'Observe planet via JPL DE. Falls back to VSOP87. Body IDs: 1-8 (Mercury-Neptune).';
+
+CREATE FUNCTION sun_observe_de(observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION sun_observe_de(observer, timestamptz) IS
+ 'Observe Sun via JPL DE. Falls back to VSOP87.';
+
+CREATE FUNCTION moon_observe_de(observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION moon_observe_de(observer, timestamptz) IS
+ 'Observe Moon via JPL DE. Falls back to ELP2000-82B.';
+
+
+-- Lambert transfer with DE positions
+
+CREATE FUNCTION lambert_transfer_de(
+ dep_body_id int4, arr_body_id int4,
+ dep_time timestamptz, arr_time timestamptz,
+ OUT c3_departure float8, OUT c3_arrival float8,
+ OUT v_inf_departure float8, OUT v_inf_arrival float8,
+ OUT tof_days float8, OUT transfer_sma float8
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION lambert_transfer_de(int4, int4, timestamptz, timestamptz) IS
+ 'Lambert transfer via JPL DE positions. Falls back to VSOP87. Body IDs 1-8.';
+
+CREATE FUNCTION lambert_c3_de(int4, int4, timestamptz, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION lambert_c3_de(int4, int4, timestamptz, timestamptz) IS
+ 'Departure C3 via JPL DE. Falls back to VSOP87. For pork chop plots.';
+
+
+-- Planetary moon observation with DE parent positions
+
+CREATE FUNCTION galilean_observe_de(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION galilean_observe_de(int4, observer, timestamptz) IS
+ 'Observe Galilean moon with JPL DE parent position. L1.2 moon offsets. Body IDs: 0-3 (Io-Callisto).';
+
+CREATE FUNCTION saturn_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION saturn_moon_observe_de(int4, observer, timestamptz) IS
+ 'Observe Saturn moon with JPL DE parent position. TASS 1.7 moon offsets. Body IDs: 0-7 (Mimas-Hyperion).';
+
+CREATE FUNCTION uranus_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION uranus_moon_observe_de(int4, observer, timestamptz) IS
+ 'Observe Uranus moon with JPL DE parent position. GUST86 moon offsets. Body IDs: 0-4 (Miranda-Oberon).';
+
+CREATE FUNCTION mars_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION mars_moon_observe_de(int4, observer, timestamptz) IS
+ 'Observe Mars moon with JPL DE parent position. MarsSat moon offsets. Body IDs: 0-1 (Phobos-Deimos).';
+
+
+-- Diagnostic function
+
+CREATE FUNCTION pg_orrery_ephemeris_info(
+ OUT provider text, OUT file_path text,
+ OUT start_jd float8, OUT end_jd float8,
+ OUT version int4, OUT au_km float8
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
+COMMENT ON FUNCTION pg_orrery_ephemeris_info() IS
+ 'Returns current ephemeris provider status: VSOP87 or JPL_DE with file path, JD range, version, and AU value.';
+-- pg_orrery 0.3.0 -> 0.4.0 migration
+--
+-- Adds observation-to-TLE fitting via batch weighted least-squares
+-- differential correction (Vallado & Crawford 2008, AIAA 2008-6770).
+-- Uses equinoctial elements internally for singularity-free optimization.
+-- LAPACK dgelss_() for SVD solve.
+
+-- ============================================================
+-- Phase 6: Orbit determination (TLE fitting from observations)
+-- ============================================================
+
+-- Fit TLE from ECI position/velocity ephemeris
+
+CREATE FUNCTION tle_from_eci(
+ positions eci_position[],
+ times timestamptz[],
+ seed tle DEFAULT NULL,
+ fit_bstar boolean DEFAULT false,
+ max_iter int4 DEFAULT 15,
+ OUT fitted_tle tle,
+ OUT iterations int4,
+ OUT rms_final float8,
+ OUT rms_initial float8,
+ OUT status text
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
+COMMENT ON FUNCTION tle_from_eci(eci_position[], timestamptz[], tle, boolean, int4) IS
+ 'Fit a TLE from ECI position/velocity observations via differential correction. Returns fitted TLE, iteration count, RMS residuals, and convergence status. Requires >= 6 observations.';
+
+-- Fit TLE from topocentric observations (az/el/range)
+
+CREATE FUNCTION tle_from_topocentric(
+ observations topocentric[],
+ times timestamptz[],
+ obs observer,
+ seed tle DEFAULT NULL,
+ fit_bstar boolean DEFAULT false,
+ max_iter int4 DEFAULT 15,
+ OUT fitted_tle tle,
+ OUT iterations int4,
+ OUT rms_final float8,
+ OUT rms_initial float8,
+ OUT status text
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
+COMMENT ON FUNCTION tle_from_topocentric(topocentric[], timestamptz[], observer, tle, boolean, int4) IS
+ 'Fit a TLE from topocentric (az/el/range) observations via differential correction. Requires seed TLE and >= 6 observations.';
+
+-- Per-observation residuals diagnostic
+
+CREATE FUNCTION tle_fit_residuals(
+ fitted tle,
+ positions eci_position[],
+ times timestamptz[]
+) RETURNS TABLE (
+ t timestamptz,
+ dx_km float8,
+ dy_km float8,
+ dz_km float8,
+ pos_err_km float8
+)
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_fit_residuals(tle, eci_position[], timestamptz[]) IS
+ 'Compute per-observation position residuals (km) between a TLE and ECI observations. Useful for fit quality assessment.';
diff --git a/sql/pg_orrery--0.5.0.sql b/sql/pg_orrery--0.5.0.sql
new file mode 100644
index 0000000..9d56206
--- /dev/null
+++ b/sql/pg_orrery--0.5.0.sql
@@ -0,0 +1,862 @@
+-- pg_orrery -- Orbital mechanics types and functions for PostgreSQL
+--
+-- Types: tle, eci_position, geodetic, topocentric, observer, pass_event
+-- Provides SGP4/SDP4 propagation, coordinate transforms, pass prediction,
+-- and GiST indexing on altitude bands for conjunction screening.
+--
+-- All propagation uses WGS-72 constants (matching TLE mean element fitting).
+-- Coordinate output uses WGS-84 (matching modern geodetic standards).
+
+-- ============================================================
+-- Shell types (forward declarations)
+-- ============================================================
+
+CREATE TYPE tle;
+CREATE TYPE eci_position;
+CREATE TYPE geodetic;
+CREATE TYPE topocentric;
+CREATE TYPE observer;
+CREATE TYPE pass_event;
+
+
+-- ============================================================
+-- TLE type: Two-Line Element set
+-- ============================================================
+
+CREATE FUNCTION tle_in(cstring) RETURNS tle
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION tle_out(tle) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION tle_recv(internal) RETURNS tle
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION tle_send(tle) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE tle (
+ INPUT = tle_in,
+ OUTPUT = tle_out,
+ RECEIVE = tle_recv,
+ SEND = tle_send,
+ INTERNALLENGTH = 112,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE tle IS 'Two-Line Element set — parsed mean orbital elements for SGP4/SDP4 propagation';
+
+-- TLE accessor functions
+
+CREATE FUNCTION tle_epoch(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_epoch(tle) IS 'TLE epoch as Julian date (UTC)';
+
+CREATE FUNCTION tle_norad_id(tle) RETURNS int4
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_norad_id(tle) IS 'NORAD catalog number';
+
+CREATE FUNCTION tle_inclination(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_inclination(tle) IS 'Orbital inclination in degrees';
+
+CREATE FUNCTION tle_eccentricity(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_eccentricity(tle) IS 'Orbital eccentricity (dimensionless)';
+
+CREATE FUNCTION tle_raan(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_raan(tle) IS 'Right ascension of ascending node in degrees';
+
+CREATE FUNCTION tle_arg_perigee(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_arg_perigee(tle) IS 'Argument of perigee in degrees';
+
+CREATE FUNCTION tle_mean_anomaly(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_mean_anomaly(tle) IS 'Mean anomaly in degrees';
+
+CREATE FUNCTION tle_mean_motion(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_mean_motion(tle) IS 'Mean motion in revolutions per day';
+
+CREATE FUNCTION tle_bstar(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_bstar(tle) IS 'B* drag coefficient (1/earth-radii)';
+
+CREATE FUNCTION tle_period(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_period(tle) IS 'Orbital period in minutes';
+
+CREATE FUNCTION tle_age(tle, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_age(tle, timestamptz) IS 'TLE age in days (positive = past epoch, negative = before epoch)';
+
+CREATE FUNCTION tle_perigee(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_perigee(tle) IS 'Perigee altitude in km above WGS-72 ellipsoid';
+
+CREATE FUNCTION tle_apogee(tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_apogee(tle) IS 'Apogee altitude in km above WGS-72 ellipsoid';
+
+CREATE FUNCTION tle_intl_desig(tle) RETURNS text
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_intl_desig(tle) IS 'International designator (COSPAR ID)';
+
+CREATE FUNCTION tle_from_lines(text, text) RETURNS tle
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_from_lines(text, text) IS
+ 'Construct TLE from separate line1/line2 text columns';
+
+
+-- ============================================================
+-- ECI position type: True Equator Mean Equinox (TEME) frame
+-- ============================================================
+
+CREATE FUNCTION eci_in(cstring) RETURNS eci_position
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_out(eci_position) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_recv(internal) RETURNS eci_position
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_send(eci_position) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE eci_position (
+ INPUT = eci_in,
+ OUTPUT = eci_out,
+ RECEIVE = eci_recv,
+ SEND = eci_send,
+ INTERNALLENGTH = 48,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE eci_position IS 'Earth-Centered Inertial position and velocity in TEME frame (km, km/s)';
+
+-- ECI accessor functions
+
+CREATE FUNCTION eci_x(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_y(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_z(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_vx(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_vy(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_vz(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION eci_speed(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION eci_speed(eci_position) IS 'Velocity magnitude in km/s';
+
+CREATE FUNCTION eci_altitude(eci_position) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION eci_altitude(eci_position) IS 'Approximate geocentric altitude in km (radius - WGS72_AE)';
+
+
+-- ============================================================
+-- Geodetic type: WGS-84 latitude/longitude/altitude
+-- ============================================================
+
+CREATE FUNCTION geodetic_in(cstring) RETURNS geodetic
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION geodetic_out(geodetic) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION geodetic_recv(internal) RETURNS geodetic
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION geodetic_send(geodetic) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE geodetic (
+ INPUT = geodetic_in,
+ OUTPUT = geodetic_out,
+ RECEIVE = geodetic_recv,
+ SEND = geodetic_send,
+ INTERNALLENGTH = 24,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE geodetic IS 'Geodetic coordinates on WGS-84 ellipsoid (lat/lon in degrees, altitude in km)';
+
+CREATE FUNCTION geodetic_lat(geodetic) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION geodetic_lon(geodetic) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION geodetic_alt(geodetic) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+
+-- ============================================================
+-- Topocentric type: observer-relative az/el/range
+-- ============================================================
+
+CREATE FUNCTION topocentric_in(cstring) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION topocentric_out(topocentric) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION topocentric_recv(internal) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION topocentric_send(topocentric) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE topocentric (
+ INPUT = topocentric_in,
+ OUTPUT = topocentric_out,
+ RECEIVE = topocentric_recv,
+ SEND = topocentric_send,
+ INTERNALLENGTH = 32,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE topocentric IS 'Topocentric coordinates relative to observer (azimuth, elevation, range, range rate)';
+
+CREATE FUNCTION topo_azimuth(topocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION topo_azimuth(topocentric) IS 'Azimuth in degrees (0=N, 90=E, 180=S, 270=W)';
+
+CREATE FUNCTION topo_elevation(topocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION topo_elevation(topocentric) IS 'Elevation in degrees (0=horizon, 90=zenith)';
+
+CREATE FUNCTION topo_range(topocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION topo_range(topocentric) IS 'Slant range in km';
+
+CREATE FUNCTION topo_range_rate(topocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION topo_range_rate(topocentric) IS 'Range rate in km/s (positive = receding)';
+
+
+-- ============================================================
+-- Observer type: ground station location
+-- ============================================================
+
+CREATE FUNCTION observer_in(cstring) RETURNS observer
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION observer_out(observer) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION observer_recv(internal) RETURNS observer
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION observer_send(observer) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE observer (
+ INPUT = observer_in,
+ OUTPUT = observer_out,
+ RECEIVE = observer_recv,
+ SEND = observer_send,
+ INTERNALLENGTH = 24,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE observer IS 'Ground observer location (accepts: 40.0N 105.3W 1655m or decimal degrees)';
+
+CREATE FUNCTION observer_lat(observer) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION observer_lat(observer) IS 'Latitude in degrees (positive = North)';
+
+CREATE FUNCTION observer_lon(observer) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION observer_lon(observer) IS 'Longitude in degrees (positive = East)';
+
+CREATE FUNCTION observer_alt(observer) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION observer_alt(observer) IS 'Altitude in meters above WGS-84 ellipsoid';
+
+CREATE FUNCTION observer_from_geodetic(float8, float8, float8 DEFAULT 0.0) RETURNS observer
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION observer_from_geodetic(float8, float8, float8) IS
+ 'Construct observer from lat (deg), lon (deg), altitude (meters). Avoids text formatting round-trips.';
+
+
+-- ============================================================
+-- Pass event type: satellite visibility window
+-- ============================================================
+
+CREATE FUNCTION pass_event_in(cstring) RETURNS pass_event
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION pass_event_out(pass_event) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION pass_event_recv(internal) RETURNS pass_event
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION pass_event_send(pass_event) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE pass_event (
+ INPUT = pass_event_in,
+ OUTPUT = pass_event_out,
+ RECEIVE = pass_event_recv,
+ SEND = pass_event_send,
+ INTERNALLENGTH = 48,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE pass_event IS 'Satellite pass event (AOS/MAX/LOS times, max elevation, AOS/LOS azimuths)';
+
+CREATE FUNCTION pass_aos_time(pass_event) RETURNS timestamptz
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_aos_time(pass_event) IS 'Acquisition of signal time';
+
+CREATE FUNCTION pass_max_el_time(pass_event) RETURNS timestamptz
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_max_el_time(pass_event) IS 'Maximum elevation time';
+
+CREATE FUNCTION pass_los_time(pass_event) RETURNS timestamptz
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_los_time(pass_event) IS 'Loss of signal time';
+
+CREATE FUNCTION pass_max_elevation(pass_event) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_max_elevation(pass_event) IS 'Maximum elevation in degrees';
+
+CREATE FUNCTION pass_aos_azimuth(pass_event) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_aos_azimuth(pass_event) IS 'AOS azimuth in degrees (0=N)';
+
+CREATE FUNCTION pass_los_azimuth(pass_event) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_los_azimuth(pass_event) IS 'LOS azimuth in degrees (0=N)';
+
+CREATE FUNCTION pass_duration(pass_event) RETURNS interval
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_duration(pass_event) IS 'Pass duration (LOS - AOS)';
+
+
+-- ============================================================
+-- SGP4/SDP4 propagation functions
+-- ============================================================
+
+CREATE FUNCTION sgp4_propagate(tle, timestamptz) RETURNS eci_position
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION sgp4_propagate(tle, timestamptz) IS
+ 'Propagate TLE to a point in time using SGP4 (near-earth) or SDP4 (deep-space). Returns TEME ECI position/velocity.';
+
+CREATE FUNCTION sgp4_propagate_safe(tle, timestamptz) RETURNS eci_position
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
+COMMENT ON FUNCTION sgp4_propagate_safe(tle, timestamptz) IS
+ 'Like sgp4_propagate but returns NULL on error instead of raising an exception. For batch queries with potentially invalid TLEs.';
+
+CREATE FUNCTION sgp4_propagate_series(tle, timestamptz, timestamptz, interval)
+ RETURNS TABLE(t timestamptz, x float8, y float8, z float8, vx float8, vy float8, vz float8)
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE
+ ROWS 100;
+COMMENT ON FUNCTION sgp4_propagate_series(tle, timestamptz, timestamptz, interval) IS
+ 'Propagate TLE over a time range at regular intervals. Returns time series of TEME positions.';
+
+CREATE FUNCTION tle_distance(tle, tle, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_distance(tle, tle, timestamptz) IS
+ 'Euclidean distance in km between two TLEs at a reference time';
+
+
+-- ============================================================
+-- Coordinate transform functions
+-- ============================================================
+
+CREATE FUNCTION eci_to_geodetic(eci_position, timestamptz) RETURNS geodetic
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION eci_to_geodetic(eci_position, timestamptz) IS
+ 'Convert TEME ECI position to WGS-84 geodetic coordinates at given time';
+
+CREATE FUNCTION eci_to_topocentric(eci_position, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION eci_to_topocentric(eci_position, observer, timestamptz) IS
+ 'Convert TEME ECI position to topocentric (az/el/range) relative to observer';
+
+CREATE FUNCTION subsatellite_point(tle, timestamptz) RETURNS geodetic
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION subsatellite_point(tle, timestamptz) IS
+ 'Subsatellite (nadir) point on WGS-84 ellipsoid at given time';
+
+CREATE FUNCTION ground_track(tle, timestamptz, timestamptz, interval)
+ RETURNS TABLE(t timestamptz, lat float8, lon float8, alt float8)
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE
+ ROWS 100;
+COMMENT ON FUNCTION ground_track(tle, timestamptz, timestamptz, interval) IS
+ 'Ground track as time series of subsatellite points (lat/lon in degrees, alt in km)';
+
+CREATE FUNCTION observe(tle, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION observe(tle, observer, timestamptz) IS
+ 'Propagate TLE and compute observer-relative look angles in one call. Returns topocentric (az/el/range/range_rate).';
+
+CREATE FUNCTION observe_safe(tle, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
+COMMENT ON FUNCTION observe_safe(tle, observer, timestamptz) IS
+ 'Like observe() but returns NULL on propagation error. For batch queries with potentially invalid/decayed TLEs.';
+
+
+-- ============================================================
+-- Pass prediction functions
+-- ============================================================
+
+CREATE FUNCTION next_pass(tle, observer, timestamptz) RETURNS pass_event
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION next_pass(tle, observer, timestamptz) IS
+ 'Find the next satellite pass over observer (searches up to 7 days ahead)';
+
+CREATE FUNCTION predict_passes(tle, observer, timestamptz, timestamptz, float8 DEFAULT 0.0)
+ RETURNS SETOF pass_event
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE
+ ROWS 10;
+COMMENT ON FUNCTION predict_passes(tle, observer, timestamptz, timestamptz, float8) IS
+ 'Predict all satellite passes over observer in time window. Optional min_elevation in degrees.';
+
+CREATE FUNCTION pass_visible(tle, observer, timestamptz, timestamptz) RETURNS boolean
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION pass_visible(tle, observer, timestamptz, timestamptz) IS
+ 'True if any pass occurs over observer in the time window';
+
+
+-- ============================================================
+-- GiST operator support functions
+-- ============================================================
+
+-- Overlap operator: do orbital keys overlap in altitude AND inclination?
+CREATE FUNCTION tle_overlap(tle, tle) RETURNS boolean
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+-- Altitude distance operator (altitude-only, for KNN ordering)
+CREATE FUNCTION tle_alt_distance(tle, tle) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE OPERATOR && (
+ LEFTARG = tle,
+ RIGHTARG = tle,
+ FUNCTION = tle_overlap,
+ COMMUTATOR = &&,
+ RESTRICT = areasel,
+ JOIN = areajoinsel
+);
+
+COMMENT ON OPERATOR && (tle, tle) IS 'Orbital key overlap (altitude band AND inclination range) — necessary condition for conjunction';
+
+CREATE OPERATOR <-> (
+ LEFTARG = tle,
+ RIGHTARG = tle,
+ FUNCTION = tle_alt_distance,
+ COMMUTATOR = <->
+);
+
+COMMENT ON OPERATOR <-> (tle, tle) IS 'Minimum altitude-band separation in km (0 if overlapping). Altitude-only — does not account for inclination. Use && for 2-D filtering.';
+
+
+-- ============================================================
+-- GiST operator class for 2-D orbital indexing (altitude + inclination)
+-- ============================================================
+
+-- GiST internal support functions
+CREATE FUNCTION gist_tle_compress(internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_decompress(internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_consistent(internal, tle, smallint, oid, internal) RETURNS boolean
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_union(internal, internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_penalty(internal, internal, internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_picksplit(internal, internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_same(internal, internal, internal) RETURNS internal
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION gist_tle_distance(internal, tle, smallint, oid, internal) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE OPERATOR CLASS tle_ops
+ DEFAULT FOR TYPE tle USING gist AS
+ OPERATOR 3 && ,
+ OPERATOR 15 <-> (tle, tle) FOR ORDER BY float_ops,
+ FUNCTION 1 gist_tle_consistent(internal, tle, smallint, oid, internal),
+ FUNCTION 2 gist_tle_union(internal, internal),
+ FUNCTION 3 gist_tle_compress(internal),
+ FUNCTION 4 gist_tle_decompress(internal),
+ FUNCTION 5 gist_tle_penalty(internal, internal, internal),
+ FUNCTION 6 gist_tle_picksplit(internal, internal),
+ FUNCTION 7 gist_tle_same(internal, internal, internal),
+ FUNCTION 8 gist_tle_distance(internal, tle, smallint, oid, internal);
+-- pg_orrery 0.1.0 -> 0.2.0 migration
+--
+-- Phase 1: Stars, comets, and Keplerian propagation.
+-- Adds heliocentric type, star observation, and two-body propagation.
+
+
+-- ============================================================
+-- Heliocentric type: ecliptic J2000 position in AU
+-- ============================================================
+
+CREATE TYPE heliocentric;
+
+CREATE FUNCTION heliocentric_in(cstring) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION heliocentric_out(heliocentric) RETURNS cstring
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION heliocentric_recv(internal) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION heliocentric_send(heliocentric) RETURNS bytea
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE TYPE heliocentric (
+ INPUT = heliocentric_in,
+ OUTPUT = heliocentric_out,
+ RECEIVE = heliocentric_recv,
+ SEND = heliocentric_send,
+ INTERNALLENGTH = 24,
+ ALIGNMENT = double,
+ STORAGE = plain
+);
+
+COMMENT ON TYPE heliocentric IS 'Heliocentric position in ecliptic J2000 frame (x, y, z in AU)';
+
+CREATE FUNCTION helio_x(heliocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION helio_x(heliocentric) IS 'X component in AU (ecliptic J2000, vernal equinox direction)';
+
+CREATE FUNCTION helio_y(heliocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION helio_y(heliocentric) IS 'Y component in AU (ecliptic J2000)';
+
+CREATE FUNCTION helio_z(heliocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION helio_z(heliocentric) IS 'Z component in AU (ecliptic J2000, north ecliptic pole)';
+
+CREATE FUNCTION helio_distance(heliocentric) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION helio_distance(heliocentric) IS 'Heliocentric distance in AU';
+
+
+-- ============================================================
+-- Star observation functions
+-- ============================================================
+
+CREATE FUNCTION star_observe(float8, float8, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION star_observe(float8, float8, observer, timestamptz) IS
+ 'Observe a star from (ra_hours J2000, dec_degrees J2000, observer, time). Returns topocentric az/el. Range is 0 (infinite distance).';
+
+CREATE FUNCTION star_observe_safe(float8, float8, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
+COMMENT ON FUNCTION star_observe_safe(float8, float8, observer, timestamptz) IS
+ 'Like star_observe but returns NULL on invalid inputs. For batch queries over star catalogs.';
+
+
+-- ============================================================
+-- Keplerian propagation functions
+-- ============================================================
+
+CREATE FUNCTION kepler_propagate(
+ q_au float8, eccentricity float8,
+ inclination_deg float8, arg_perihelion_deg float8,
+ long_asc_node_deg float8, perihelion_jd float8,
+ t timestamptz
+) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION kepler_propagate(float8, float8, float8, float8, float8, float8, timestamptz) IS
+ 'Two-body Keplerian propagation from classical orbital elements. Returns heliocentric ecliptic J2000 position in AU. Handles elliptic, parabolic, and hyperbolic orbits.';
+
+
+-- ============================================================
+-- Comet observation
+-- ============================================================
+
+CREATE FUNCTION comet_observe(
+ q_au float8, eccentricity float8,
+ inclination_deg float8, arg_perihelion_deg float8,
+ long_asc_node_deg float8, perihelion_jd float8,
+ earth_x_au float8, earth_y_au float8, earth_z_au float8,
+ obs observer, t timestamptz
+) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION comet_observe(float8, float8, float8, float8, float8, float8, float8, float8, float8, observer, timestamptz) IS
+ 'Observe a comet/asteroid from orbital elements. Requires Earth heliocentric ecliptic J2000 position (AU). Returns topocentric az/el with geocentric range in km.';
+
+
+-- ============================================================
+-- Phase 2: VSOP87 planets, ELP82B Moon, Sun observation
+-- ============================================================
+
+CREATE FUNCTION planet_heliocentric(int4, timestamptz) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION planet_heliocentric(int4, timestamptz) IS
+ 'VSOP87 heliocentric ecliptic J2000 position (AU). Body IDs: 0=Sun, 1=Mercury, ..., 8=Neptune.';
+
+CREATE FUNCTION planet_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION planet_observe(int4, observer, timestamptz) IS
+ 'Observe a planet from (body_id 1-8, observer, time). Returns topocentric az/el with geocentric range in km.';
+
+CREATE FUNCTION sun_observe(observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION sun_observe(observer, timestamptz) IS
+ 'Observe the Sun from (observer, time). Returns topocentric az/el with geocentric range in km.';
+
+CREATE FUNCTION moon_observe(observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION moon_observe(observer, timestamptz) IS
+ 'Observe the Moon via ELP2000-82B from (observer, time). Returns topocentric az/el with geocentric range in km.';
+
+
+-- ============================================================
+-- Phase 3: Planetary moon observation
+-- ============================================================
+
+CREATE FUNCTION galilean_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION galilean_observe(int4, observer, timestamptz) IS
+ 'Observe a Galilean moon of Jupiter via L1.2 theory. Body IDs: 0=Io, 1=Europa, 2=Ganymede, 3=Callisto.';
+
+CREATE FUNCTION saturn_moon_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION saturn_moon_observe(int4, observer, timestamptz) IS
+ 'Observe a Saturn moon via TASS 1.7. Body IDs: 0=Mimas, 1=Enceladus, 2=Tethys, 3=Dione, 4=Rhea, 5=Titan, 6=Iapetus, 7=Hyperion.';
+
+CREATE FUNCTION uranus_moon_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION uranus_moon_observe(int4, observer, timestamptz) IS
+ 'Observe a Uranus moon via GUST86. Body IDs: 0=Miranda, 1=Ariel, 2=Umbriel, 3=Titania, 4=Oberon.';
+
+CREATE FUNCTION mars_moon_observe(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION mars_moon_observe(int4, observer, timestamptz) IS
+ 'Observe a Mars moon via MarsSat. Body IDs: 0=Phobos, 1=Deimos.';
+
+
+-- ============================================================
+-- Phase 3: Jupiter decametric radio burst prediction
+-- ============================================================
+
+CREATE FUNCTION io_phase_angle(timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION io_phase_angle(timestamptz) IS
+ 'Io orbital phase angle in degrees [0,360). 0=superior conjunction (behind Jupiter). Standard Radio JOVE convention.';
+
+CREATE FUNCTION jupiter_cml(observer, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION jupiter_cml(observer, timestamptz) IS
+ 'Jupiter Central Meridian Longitude, System III (1965.0), in degrees [0,360). Light-time corrected.';
+
+CREATE FUNCTION jupiter_burst_probability(float8, float8) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION jupiter_burst_probability(float8, float8) IS
+ 'Estimated Jupiter decametric burst probability (0-1) from (io_phase_deg, cml_deg). Based on Carr et al. (1983) source regions A, B, C, D.';
+
+
+-- ============================================================
+-- Phase 4: Interplanetary transfer orbits (Lambert solver)
+-- ============================================================
+
+CREATE FUNCTION lambert_transfer(
+ dep_body_id int4, arr_body_id int4,
+ dep_time timestamptz, arr_time timestamptz,
+ OUT c3_departure float8, OUT c3_arrival float8,
+ OUT v_inf_departure float8, OUT v_inf_arrival float8,
+ OUT tof_days float8, OUT transfer_sma float8
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION lambert_transfer(int4, int4, timestamptz, timestamptz) IS
+ 'Solve Lambert transfer between two planets. Returns C3 (km^2/s^2), v_infinity (km/s), TOF (days), SMA (AU). Body IDs 1-8.';
+
+CREATE FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) IS
+ 'Departure C3 (km^2/s^2) for a Lambert transfer. Returns NULL if solver fails. For pork chop plots.';
+-- pg_orrery 0.2.0 -> 0.3.0 migration
+--
+-- Adds optional JPL DE440/441 ephemeris functions.
+-- Existing VSOP87/ELP2000-82B functions are unchanged (still IMMUTABLE).
+-- New _de() functions are STABLE (depend on external DE binary file).
+-- When DE is unavailable, _de() functions fall back to VSOP87 silently.
+
+-- ============================================================
+-- Phase 5: DE ephemeris functions (optional high-precision)
+-- ============================================================
+
+-- Planet observation with DE ephemeris
+
+CREATE FUNCTION planet_heliocentric_de(int4, timestamptz) RETURNS heliocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION planet_heliocentric_de(int4, timestamptz) IS
+ 'Heliocentric ecliptic J2000 position via JPL DE (sub-arcsecond). Falls back to VSOP87 if DE unavailable. Body IDs: 0=Sun, 1-8=Mercury-Neptune.';
+
+CREATE FUNCTION planet_observe_de(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION planet_observe_de(int4, observer, timestamptz) IS
+ 'Observe planet via JPL DE. Falls back to VSOP87. Body IDs: 1-8 (Mercury-Neptune).';
+
+CREATE FUNCTION sun_observe_de(observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION sun_observe_de(observer, timestamptz) IS
+ 'Observe Sun via JPL DE. Falls back to VSOP87.';
+
+CREATE FUNCTION moon_observe_de(observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION moon_observe_de(observer, timestamptz) IS
+ 'Observe Moon via JPL DE. Falls back to ELP2000-82B.';
+
+
+-- Lambert transfer with DE positions
+
+CREATE FUNCTION lambert_transfer_de(
+ dep_body_id int4, arr_body_id int4,
+ dep_time timestamptz, arr_time timestamptz,
+ OUT c3_departure float8, OUT c3_arrival float8,
+ OUT v_inf_departure float8, OUT v_inf_arrival float8,
+ OUT tof_days float8, OUT transfer_sma float8
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION lambert_transfer_de(int4, int4, timestamptz, timestamptz) IS
+ 'Lambert transfer via JPL DE positions. Falls back to VSOP87. Body IDs 1-8.';
+
+CREATE FUNCTION lambert_c3_de(int4, int4, timestamptz, timestamptz) RETURNS float8
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION lambert_c3_de(int4, int4, timestamptz, timestamptz) IS
+ 'Departure C3 via JPL DE. Falls back to VSOP87. For pork chop plots.';
+
+
+-- Planetary moon observation with DE parent positions
+
+CREATE FUNCTION galilean_observe_de(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION galilean_observe_de(int4, observer, timestamptz) IS
+ 'Observe Galilean moon with JPL DE parent position. L1.2 moon offsets. Body IDs: 0-3 (Io-Callisto).';
+
+CREATE FUNCTION saturn_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION saturn_moon_observe_de(int4, observer, timestamptz) IS
+ 'Observe Saturn moon with JPL DE parent position. TASS 1.7 moon offsets. Body IDs: 0-7 (Mimas-Hyperion).';
+
+CREATE FUNCTION uranus_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION uranus_moon_observe_de(int4, observer, timestamptz) IS
+ 'Observe Uranus moon with JPL DE parent position. GUST86 moon offsets. Body IDs: 0-4 (Miranda-Oberon).';
+
+CREATE FUNCTION mars_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION mars_moon_observe_de(int4, observer, timestamptz) IS
+ 'Observe Mars moon with JPL DE parent position. MarsSat moon offsets. Body IDs: 0-1 (Phobos-Deimos).';
+
+
+-- Diagnostic function
+
+CREATE FUNCTION pg_orrery_ephemeris_info(
+ OUT provider text, OUT file_path text,
+ OUT start_jd float8, OUT end_jd float8,
+ OUT version int4, OUT au_km float8
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
+COMMENT ON FUNCTION pg_orrery_ephemeris_info() IS
+ 'Returns current ephemeris provider status: VSOP87 or JPL_DE with file path, JD range, version, and AU value.';
+-- pg_orrery 0.3.0 -> 0.4.0 migration
+--
+-- Adds observation-to-TLE fitting via batch weighted least-squares
+-- differential correction (Vallado & Crawford 2008, AIAA 2008-6770).
+-- Uses equinoctial elements internally for singularity-free optimization.
+-- LAPACK dgelss_() for SVD solve.
+
+-- ============================================================
+-- Phase 6: Orbit determination (TLE fitting from observations)
+-- ============================================================
+
+-- Fit TLE from ECI position/velocity ephemeris
+
+CREATE FUNCTION tle_from_eci(
+ positions eci_position[], times timestamptz[],
+ seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false,
+ max_iter int4 DEFAULT 15,
+ OUT fitted_tle tle, OUT iterations int4,
+ OUT rms_final float8, OUT rms_initial float8, OUT status text,
+ OUT condition_number float8, OUT covariance float8[], OUT nstate int4
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
+COMMENT ON FUNCTION tle_from_eci(eci_position[], timestamptz[], tle, boolean, int4) IS
+ 'Fit a TLE from ECI position/velocity observations via differential correction. Returns fitted TLE, iteration count, RMS residuals, convergence status, condition number, and formal covariance matrix.';
+
+-- Fit TLE from topocentric observations (az/el/range) — single observer
+
+CREATE FUNCTION tle_from_topocentric(
+ observations topocentric[], times timestamptz[],
+ obs observer,
+ seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false,
+ max_iter int4 DEFAULT 15,
+ OUT fitted_tle tle, OUT iterations int4,
+ OUT rms_final float8, OUT rms_initial float8, OUT status text,
+ OUT condition_number float8, OUT covariance float8[], OUT nstate int4
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
+COMMENT ON FUNCTION tle_from_topocentric(topocentric[], timestamptz[], observer, tle, boolean, int4) IS
+ 'Fit a TLE from topocentric (az/el/range) observations via differential correction. Returns fitted TLE, RMS residuals, convergence status, condition number, and formal covariance matrix.';
+
+-- Fit TLE from topocentric observations — multiple observers
+
+CREATE FUNCTION tle_from_topocentric(
+ observations topocentric[], times timestamptz[],
+ observers observer[], observer_ids int4[],
+ seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false,
+ max_iter int4 DEFAULT 15,
+ OUT fitted_tle tle, OUT iterations int4,
+ OUT rms_final float8, OUT rms_initial float8, OUT status text,
+ OUT condition_number float8, OUT covariance float8[], OUT nstate int4
+) RETURNS RECORD
+ AS 'MODULE_PATHNAME', 'tle_from_topocentric_multi'
+ LANGUAGE C STABLE PARALLEL SAFE;
+COMMENT ON FUNCTION tle_from_topocentric(topocentric[], timestamptz[], observer[], int4[], tle, boolean, int4) IS
+ 'Fit a TLE from topocentric observations collected by multiple ground stations. observer_ids[i] indexes into observers[]. Returns convergence status, condition number, and formal covariance matrix.';
+
+-- Per-observation residuals diagnostic
+
+CREATE FUNCTION tle_fit_residuals(
+ fitted tle,
+ positions eci_position[],
+ times timestamptz[]
+) RETURNS TABLE (
+ t timestamptz,
+ dx_km float8,
+ dy_km float8,
+ dz_km float8,
+ pos_err_km float8
+)
+ AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+COMMENT ON FUNCTION tle_fit_residuals(tle, eci_position[], timestamptz[]) IS
+ 'Compute per-observation position residuals (km) between a TLE and ECI observations. Useful for fit quality assessment.';
diff --git a/src/astro_math.h b/src/astro_math.h
new file mode 100644
index 0000000..251210c
--- /dev/null
+++ b/src/astro_math.h
@@ -0,0 +1,220 @@
+/*
+ * astro_math.h -- Shared astronomical math for pg_orrery
+ *
+ * Static inline functions used by star_funcs.c, kepler_funcs.c,
+ * and future planet/moon observation code.
+ *
+ * Using static inline preserves the project convention of no
+ * cross-translation-unit symbol coupling.
+ */
+
+#ifndef PG_ORRERY_ASTRO_MATH_H
+#define PG_ORRERY_ASTRO_MATH_H
+
+#include