Index: /trunk/BNC/src/PPP/pppEphPool.cpp
===================================================================
--- /trunk/BNC/src/PPP/pppEphPool.cpp	(revision 11046)
+++ /trunk/BNC/src/PPP/pppEphPool.cpp	(revision 11047)
@@ -20,4 +20,5 @@
 #include "pppInclude.h"
 #include "pppClient.h"
+#include "bncephuser.h"
 
 using namespace BNC_PPP;
@@ -26,9 +27,28 @@
 //
 /////////////////////////////////////////////////////////////////////////////
+// Bad, outdated and unhealthy ephemerides are never used. Ephemerides from
+// RINEX navigation files (post-processing) never pass bncEphUser's checks and
+// stay "unchecked", so the health flag and the plausibility of the satellite
+// distance are checked here directly as well.
+/////////////////////////////////////////////////////////////////////////////
 void t_pppEphPool::putEphemeris(t_eph* eph) {
-  if (eph && (eph->checkState() != t_eph::bad ||
-              eph->checkState() != t_eph::unhealthy ||
-              eph->checkState() != t_eph::outdated))  {
-    _satEphPool[eph->prn().toInt()].putEphemeris(_maxQueueSize, eph);
+  if (!eph) {
+    return;
+  }
+  int iPrn = eph->prn().toInt();
+  if (iPrn == 0) { // satellite number out of range
+    delete eph;
+  }
+  else if (eph->checkState() == t_eph::unhealthy || eph->isUnhealthy()) {
+    _satEphPool[iPrn].setUnhealthy(eph);
+  }
+  else if (eph->checkState() == t_eph::unchecked && !bncEphUser::radialDistanceOk(eph)) {
+    LOG << "bad ephemeris " << eph->prn().toString() << ' ' << string(eph->TOC())
+        << " - implausible satellite distance, ignored" << endl;
+    delete eph;
+  }
+  else if (eph->checkState() != t_eph::bad &&
+           eph->checkState() != t_eph::outdated) {
+    _satEphPool[iPrn].putEphemeris(_maxQueueSize, eph);
   }
   else {
@@ -87,4 +107,23 @@
     delete eph;
   }
+}
+
+// A newer ephemeris declaring the satellite unhealthy: rejecting it alone
+// would leave the older, healthy ephemerides in the pool, and the satellite
+// would still be used with them - so they are all discarded, and the
+// satellite stays unused until a newer healthy ephemeris arrives.
+/////////////////////////////////////////////////////////////////////////////
+void t_pppEphPool::t_satEphPool::setUnhealthy(t_eph* eph) {
+  if (_ephs.empty() || eph->isNewerThan(_ephs.front())) {
+    if (!_ephs.empty()) {
+      LOG << "unhealthy ephemeris " << eph->prn().toString() << ' ' << string(eph->TOC())
+          << " - older ephemerides of this satellite discarded" << endl;
+    }
+    for (unsigned ii = 0; ii < _ephs.size(); ii++) {
+      delete _ephs[ii];
+    }
+    _ephs.clear();
+  }
+  delete eph;
 }
 
Index: /trunk/BNC/src/PPP/pppEphPool.h
===================================================================
--- /trunk/BNC/src/PPP/pppEphPool.h	(revision 11046)
+++ /trunk/BNC/src/PPP/pppEphPool.h	(revision 11047)
@@ -41,4 +41,5 @@
     }
     void putEphemeris(unsigned maxQueueSize, t_eph* eph);
+    void setUnhealthy(t_eph* eph);
     void putOrbCorrection(t_orbCorr* corr);
     void putClkCorrection(t_clkCorr* corr);
Index: /trunk/BNC/src/PPP/pppObsPool.cpp
===================================================================
--- /trunk/BNC/src/PPP/pppObsPool.cpp	(revision 11046)
+++ /trunk/BNC/src/PPP/pppObsPool.cpp	(revision 11047)
@@ -82,4 +82,8 @@
 void t_pppObsPool::putCodeBias(t_satCodeBias* satCodeBias) {
   int iPrn = satCodeBias->_prn.toInt();
+  if (iPrn == 0) { // satellite number out of range
+    delete satCodeBias;
+    return;
+  }
   delete _satCodeBiases[iPrn];
   _satCodeBiases[iPrn] = satCodeBias;
@@ -97,4 +101,8 @@
 void t_pppObsPool::putPhaseBias(t_satPhaseBias* satPhaseBias) {
   int iPrn = satPhaseBias->_prn.toInt();
+  if (iPrn == 0) { // satellite number out of range
+    delete satPhaseBias;
+    return;
+  }
   delete _satPhaseBiases[iPrn];
   _satPhaseBiases[iPrn] = satPhaseBias;
@@ -122,4 +130,8 @@
 void t_pppObsPool::putSatAntenna(t_satAntenna* satAntenna) {
   int iPrn = satAntenna->_prn.toInt();
+  if (iPrn == 0) { // satellite number out of range
+    delete satAntenna;
+    return;
+  }
   delete _satAntennas[iPrn];
   _satAntennas[iPrn] = satAntenna;
Index: /trunk/BNC/src/PPP/pppSatObs.cpp
===================================================================
--- /trunk/BNC/src/PPP/pppSatObs.cpp	(revision 11046)
+++ /trunk/BNC/src/PPP/pppSatObs.cpp	(revision 11047)
@@ -400,13 +400,44 @@
 
 
-// A system's Satellite Antenna message is only trustworthy once the
-// provider has confirmed it via Metadata: SatelliteAntennaIOD (DF+010) must
-// be non-zero, and must match the Data IOD (DF+069) of a Metadata entry for
-// model-correction type 1 (satellite antenna PCV) or type 2 (GDV) - either
-// entry matching is sufficient. Metadata carries this IOD once globally, not
-// per system (DF+010 is GNSS-specific, DF+069 is not), so the same Metadata
-// entries are reused as the trust anchor for every system's check - the
-// provider is relied on to keep DF+010 synchronized with DF+069 across all
-// systems for this to work.
+// Satellite Antenna IOD referenced by the SSR Metadata: the Data IOD (DF+069)
+// of the model-correction entry of type 1 (satellite antenna PCV) or type 2
+// (GDV). refIOD = 0 if Metadata references no antenna information (no
+// Metadata, no such entry, no Data IOD, or a zero Data IOD). A provider's
+// solution is relative to one Satellite Antenna information only, so two
+// different non-zero IODs are inconsistent: returns false in that case.
+//
+// A non-zero reference means the provider's biases, and orbits/clocks, are
+// relative to exactly that Satellite Antenna information, so the client
+// must apply it and nothing else (see t_pppSatObs::cmpModel()).
+////////////////////////////////////////////////////////////////////////////
+static bool ssrSatAntennaRefIOD(const t_metaData* metaData, unsigned int& refIOD) {
+  refIOD = 0;
+  if (!metaData) {
+    return true;
+  }
+  for (unsigned ii = 0; ii < metaData->_entries.size(); ii++) {
+    const t_metaDataEntry& entry = metaData->_entries[ii];
+    if ((entry._typeIndicator == 1 || entry._typeIndicator == 2) && // PCV or GDV
+        entry._dataIODIndicator && entry._dataIOD != 0) {
+      if (refIOD != 0 && refIOD != entry._dataIOD) {
+        return false;
+      }
+      refIOD = entry._dataIOD;
+    }
+  }
+  return true;
+}
+
+// Whether cached Satellite Antenna data is the information referenced by
+// Metadata (refIOD, from ssrSatAntennaRefIOD()). SatelliteAntennaIOD (DF+010)
+// must be non-zero and equal refIOD. Metadata carries this IOD once globally,
+// not per system (DF+010 is GNSS-specific, DF+069 is not), so the same
+// Metadata entries are reused for every system's check - the provider is
+// relied on to keep DF+010 synchronized with DF+069 across all systems.
+//
+// The IOD is also unique only per SSR Provider ID (DF414), so the Metadata
+// must come from the same provider as the Antenna message - antenna data may
+// be taken from the bias mountpoint while Metadata comes from the
+// corrections mountpoint (see pppRun.cpp).
 //
 // Separately, providers only guarantee SatelliteAntennaIOD uniqueness within
@@ -414,12 +445,16 @@
 // has been without a fresh Antenna message for that system for 64 days or
 // more must treat any cached data as unverifiable and stop using it, even
-// if the IOD/Metadata check above would otherwise pass - the same IOD value
-// could by then legitimately mean something else. epoTime is the current
-// processing epoch, compared against t_satAntenna::_time (when this data
-// was actually decoded, not a wire epoch - the message carries none).
-////////////////////////////////////////////////////////////////////////////
-static bool ssrSatAntennaTrusted(const t_satAntenna* satAntenna, const bncTime& epoTime) {
+// if the IOD check above would otherwise pass - the same IOD value could by
+// then legitimately mean something else. epoTime is the current processing
+// epoch, compared against t_satAntenna::_time (when this data was last
+// received, to within the decoder's re-emit interval - not a wire epoch,
+// the message carries none; see RTCM3coDecoder::sendResults()).
+////////////////////////////////////////////////////////////////////////////
+static bool ssrSatAntennaTrusted(const t_satAntenna* satAntenna, const t_metaData* metaData,
+                                 unsigned int refIOD, const bncTime& epoTime) {
   const double MAX_AGE_SEC = 64.0 * 86400.0;
-  if (!satAntenna || satAntenna->_satelliteAntennaIOD == 0) {
+  if (!satAntenna || !metaData || refIOD == 0 ||
+      satAntenna->_satelliteAntennaIOD != refIOD ||
+      satAntenna->_ssrProviderID != metaData->_providerID) {
     return false;
   }
@@ -428,24 +463,11 @@
     return false;
   }
-  const t_metaData* metaData = PPP_CLIENT->obsPool()->metaData();
-  if (!metaData) {
-    return false;
-  }
-  for (unsigned ii = 0; ii < metaData->_entries.size(); ii++) {
-    const t_metaDataEntry& entry = metaData->_entries[ii];
-    if ((entry._typeIndicator == 1 || entry._typeIndicator == 2) && // PCV or GDV
-        entry._dataIODIndicator &&
-        entry._dataIOD == satAntenna->_satelliteAntennaIOD) {
-      return true;
-    }
-  }
-  return false;
+  return true;
 }
 
 // Satellite antenna correction (offset + nadir-angle-dependent PCV) from a
 // live SSR Satellite Antenna message, in the same additive sense as
-// bncAntex::satCorr(). Returns false (corr untouched) if no trusted SSR data
-// is available for this PRN/frequency (see ssrSatAntennaTrusted), so the
-// caller falls back to the static ANTEX file.
+// bncAntex::satCorr(). Returns false (corr untouched) if the data contains
+// no entry for this frequency.
 ////////////////////////////////////////////////////////////////////////////
 static bool ssrSatAntennaCorr(const t_satAntenna* satAntenna, const string& frqStr,
@@ -472,4 +494,7 @@
       else if (idx >= (int)frq._nadirAngleCorrection.size()) {
         idx = (int)frq._nadirAngleCorrection.size() - 1;
+      }
+      if (std::isnan(frq._nadirAngleCorrection[idx])) {
+        return false; // invalid value (DF+021 = -2^(n-1)) at this nadir angle
       }
       corr += frq._nadirAngleCorrection[idx];
@@ -555,9 +580,35 @@
   // Antenna Phase Center Offsets and Variations
   // -------------------------------------------
+  // If SSR Metadata references Satellite Antenna information (non-zero IOD),
+  // the provider's biases are relative to exactly that information, so it
+  // is the only satellite antenna model applied - never mixed with ANTEX:
+  // phaseranges get the SSR correction if the Phase Center indicator
+  // (DF+011) is set, otherwise zero; pseudoranges likewise with the Group
+  // Delay indicator (DF+012). Zero values are part of that information
+  // (explicitly encoded, or DF+019/DF+013 not set); omitting a satellite or
+  // frequency within a constellation is not a valid way to signal zero. So
+  // if the referenced information is not (yet) available for this satellite,
+  // or lacks a frequency the PPP linear combinations need, the satellite is
+  // excluded rather than corrected with ANTEX. Without such a reference,
+  // the static ANTEX file is used for both observables.
   if (PPP_CLIENT->antex()) {
+    const t_metaData*   metaData   = PPP_CLIENT->obsPool()->metaData();
     const t_satAntenna* satAntenna = PPP_CLIENT->obsPool()->satAntenna(_prn);
-    if (!ssrSatAntennaTrusted(satAntenna, _time)) {
-      satAntenna = 0; // untrusted (IOD zero, or not confirmed via Metadata) - fall back to ANTEX
-    }
+    unsigned int refIOD;
+    if (!ssrSatAntennaRefIOD(metaData, refIOD)) {
+      LOG << "SSR Metadata references different Satellite Antenna IODs (PCV/GDV) - "
+          << _prn.toString() << " excluded" << endl;
+      return failure;
+    }
+    bool useSsr = (refIOD != 0);
+    if (useSsr && !ssrSatAntennaTrusted(satAntenna, metaData, refIOD, _time)) {
+      LOG << "SSR Satellite Antenna IOD " << refIOD << " referenced by Metadata not available for "
+          << _prn.toString() << " - satellite excluded" << endl;
+      return failure;
+    }
+
+    bool phaseMissing[t_frequency::max] = {};
+    bool codeMissing[t_frequency::max]  = {};
+
     for (unsigned ii = 0; ii < t_frequency::max; ii++) {
       t_frequency::type frqType = static_cast<t_frequency::type>(ii);
@@ -568,21 +619,54 @@
       _model._antPCO[ii] = PPP_CLIENT->antex()->rcvCorr(station->antName(), frqType, _model._eleSat, _model._azSat, found);
 
-      // Prefer a live, trusted SSR Satellite Antenna correction over the
-      // static ANTEX file when available for this satellite/frequency,
-      // mirroring how SSR orbit/clock corrections already take precedence
-      // over broadcast ephemerides elsewhere in this codebase. Unlike
-      // ANTEX's satCorr() (a single geometric PCO/PCV value shared by code
-      // and phase alike), the SSR message's DF+011/DF+012 indicators say
-      // explicitly which observable(s) the correction applies to, so it is
-      // routed into the dedicated phase-only/code-only fields instead of
-      // the shared _antPCO - both may fire for the same ssrCorr value if
-      // both indicators are set.
-      double ssrCorr;
-      if (ssrSatAntennaCorr(satAntenna, frqStr, _model._elTx, ssrCorr)) {
-        if (satAntenna->_phaseCenterInfoInd) {
-          _model._antPCV[ii] += ssrCorr;
-        }
-        if (satAntenna->_groupDelayInfoInd) {
-          _model._antGDV[ii] += ssrCorr;
+      if (useSsr) {
+        // Unlike ANTEX's satCorr() (a single geometric PCO/PCV value shared
+        // by code and phase alike), DF+011/DF+012 say explicitly which
+        // observable(s) the correction applies to, so it is routed into the
+        // dedicated phase-only/code-only fields instead of the shared _antPCO.
+        // BDS 1207.14 MHz: B2 (BDS-2, "C7") or B2b (BDS-3, "C7b") - a
+        // satellite transmits only one of them. The entry of the satellite's
+        // own generation is preferred, taken from the ANTEX block type valid
+        // at this epoch (PRNs have been reassigned from BDS-2 to BDS-3).
+        // Since antenna corrections depend on the carrier, not the signal,
+        // the other entry is used as fallback if the own one is missing; if
+        // the generation is unknown, an entry is used only if unambiguous
+        // (just one present, or both equal).
+        // PRELIMINARY - pending clarification by RTCM SC-104 whether B2 and
+        // B2b entries of one satellite are meant to be identical; to revert
+        // to the strict selection, drop the fallback branches below.
+        double ssrCorr = 0.0;
+        bool   ssrOK   = false;
+        if (frqStr == "C7") {
+          double corrB2 = 0.0, corrB2b = 0.0;
+          bool   hasB2  = ssrSatAntennaCorr(satAntenna, "C7",  _model._elTx, corrB2);
+          bool   hasB2b = ssrSatAntennaCorr(satAntenna, "C7b", _model._elTx, corrB2b);
+          QString blockType = PPP_CLIENT->antex()->satBlockType(prn, _time);
+          if      (blockType.startsWith("3")) {       // BDS-3: B2b, fallback B2
+            ssrOK = hasB2b || hasB2;
+            ssrCorr = hasB2b ? corrB2b : corrB2;
+          }
+          else if (blockType.startsWith("2")) {       // BDS-2: B2, fallback B2b
+            ssrOK = hasB2 || hasB2b;
+            ssrCorr = hasB2 ? corrB2 : corrB2b;
+          }
+          else if (hasB2 != hasB2b || (hasB2 && corrB2 == corrB2b)) { // generation unknown
+            ssrOK = true;
+            ssrCorr = hasB2 ? corrB2 : corrB2b;
+          }
+        }
+        else {
+          ssrOK = ssrSatAntennaCorr(satAntenna, frqStr, _model._elTx, ssrCorr);
+        }
+        if (ssrOK) {
+          if (satAntenna->_phaseCenterInfoInd) {
+            _model._antPCV[ii] += ssrCorr;
+          }
+          if (satAntenna->_groupDelayInfoInd) {
+            _model._antGDV[ii] += ssrCorr;
+          }
+        }
+        else {
+          phaseMissing[ii] = satAntenna->_phaseCenterInfoInd;
+          codeMissing[ii]  = satAntenna->_groupDelayInfoInd;
         }
       }
@@ -604,4 +688,50 @@
             _model._antPCO[ii] -= PPP_CLIENT->antex()->satCorr(prn, t_frequency::C2, _model._elTx, _model._azTx, found);
           }
+        }
+      }
+    }
+
+    // If the SSR antenna information applies to both observables (both
+    // DF+011 and DF+012 set), the code and phase biases must come from the
+    // same SSR solution (same SSR IOD and Provider ID), otherwise they are
+    // not consistent with each other with respect to it. Only checked if
+    // phase biases are used and both IODs are known.
+    if (useSsr && satAntenna->_phaseCenterInfoInd && satAntenna->_groupDelayInfoInd &&
+        OPT->arSystem(_prn.system())) {
+      const t_satCodeBias*  satCodeBias  = PPP_CLIENT->obsPool()->satCodeBias(_prn);
+      const t_satPhaseBias* satPhaseBias = PPP_CLIENT->obsPool()->satPhaseBias(_prn);
+      if (satCodeBias && satPhaseBias &&
+          satCodeBias->_ssrIOD >= 0 && satPhaseBias->_ssrIOD >= 0 &&
+          (satCodeBias->_ssrIOD        != satPhaseBias->_ssrIOD ||
+           satCodeBias->_ssrProviderID != satPhaseBias->_ssrProviderID)) {
+        LOG << "SSR code bias (IOD " << satCodeBias->_ssrIOD << ", provider " << satCodeBias->_ssrProviderID
+            << ") and phase bias (IOD " << satPhaseBias->_ssrIOD << ", provider " << satPhaseBias->_ssrProviderID
+            << ") inconsistent for " << _prn.toString() << " with shared SSR Satellite Antenna information"
+            << " - satellite excluded" << endl;
+        return failure;
+      }
+    }
+
+    // Exclude the satellite if the referenced antenna information omits a
+    // frequency actually used by the linear combinations of this system
+    const vector<t_lc>& LCs = OPT->LCs(_prn.system());
+    for (unsigned iLC = 0; iLC < LCs.size(); iLC++) {
+      map<t_frequency::type, double> codeCoeff;
+      map<t_frequency::type, double> phaseCoeff;
+      map<t_frequency::type, double> ionoCoeff;
+      lcCoeff(LCs[iLC], codeCoeff, phaseCoeff, ionoCoeff);
+      map<t_frequency::type, double>::const_iterator it;
+      for (it = phaseCoeff.begin(); it != phaseCoeff.end(); it++) {
+        if (phaseMissing[it->first]) {
+          LOG << "SSR Satellite Antenna IOD " << refIOD << " has no valid entry for "
+              << _prn.toString() << ' ' << t_frequency::toString(it->first) << " (phase) - satellite excluded" << endl;
+          return failure;
+        }
+      }
+      for (it = codeCoeff.begin(); it != codeCoeff.end(); it++) {
+        if (codeMissing[it->first]) {
+          LOG << "SSR Satellite Antenna IOD " << refIOD << " has no valid entry for "
+              << _prn.toString() << ' ' << t_frequency::toString(it->first) << " (code) - satellite excluded" << endl;
+          return failure;
         }
       }
Index: /trunk/BNC/src/RTCM3/RTCM3Decoder.cpp
===================================================================
--- /trunk/BNC/src/RTCM3/RTCM3Decoder.cpp	(revision 11046)
+++ /trunk/BNC/src/RTCM3/RTCM3Decoder.cpp	(revision 11047)
@@ -141,4 +141,5 @@
     flag = t_corrSSR::getSsrNavTypeFlag(sys, num);
     CurrentObs._prn.set(sys, num, flag);
+    checkPrnRange(CurrentObs._prn);
 
     t_frqObs *frqObs = new t_frqObs;
@@ -723,4 +724,5 @@
           else
             CurrentObs._prn.set(sys, RTCM3_MSM_NUMSAT - i);
+          checkPrnRange(CurrentObs._prn);
           j = RTCM3_MSM_NUMSIG;
           while (!(sigmask & (1 << --j)))
@@ -954,7 +956,10 @@
     int flag = t_corrSSR::getSsrNavTypeFlag(sys, sv);
     CurrentObs._prn.set(sys, sv, flag);
+    checkPrnRange(CurrentObs._prn);
     GETBITS(code, 1)
     GETBITS(freq, 5)
-    GLOFreq[sv - 1] = 100 + freq - 7; /* store frequency for other users (MSM) */
+    if (sv >= 1) { /* 6-bit slot 1..63 fits GLOFreq[64]; 0 is invalid */
+      GLOFreq[sv - 1] = 100 + freq - 7; /* store frequency for other users (MSM) */
+    }
 
     t_frqObs *frqObs = new t_frqObs;
@@ -1066,4 +1071,5 @@
     }
     eph._prn.set('G', i);
+    checkPrnRange(eph._prn);
     GETBITS(week, 10)
     if (week < 0 || week > 1023) {
@@ -1185,4 +1191,5 @@
     }
     eph._prn.set('R', sv);
+    checkPrnRange(eph._prn);
 
     GETBITS(i, 5)
@@ -1373,4 +1380,5 @@
     }
     eph._prn.set('J', i);
+    checkPrnRange(eph._prn);
 
     GETBITS(i, 16)
@@ -1493,4 +1501,5 @@
     }
     eph._prn.set('I', i);
+    checkPrnRange(eph._prn);
     GETBITS(week, 10)
     if (week < 0 || week > 1023) {
@@ -1627,4 +1636,5 @@
     }
     eph._prn.set('S', 20 + i);
+    checkPrnRange(eph._prn);
     GETBITS(eph._IODN, 8)
     GETBITS(i, 13)
@@ -1716,4 +1726,5 @@
     }
     eph._prn.set('E', i, eph._inav ? t_eph::INAV : t_eph::FNAV);
+    checkPrnRange(eph._prn);
 
     GETBITS(week, 12) //FIXME: roll-over after week 4095!!
@@ -1863,4 +1874,5 @@
     }
     eph._prn.set('C', i);
+    checkPrnRange(eph._prn);
 
     GETBITS(week, 13)
@@ -2439,5 +2451,18 @@
 }
 
-
-
-
+// Satellite numbers on the wire can exceed BNC's supported range (e.g. 6-bit
+// IDs up to 63, while t_prn supports G01-G32). They are passed on unchanged,
+// but t_prn::toInt() maps them to the unused index 0, so they are not usable
+// for per-satellite processing - report that once per satellite.
+////////////////////////////////////////////////////////////////////////////
+void RTCM3Decoder::checkPrnRange(const t_prn& prn) {
+  if (prn.toInt() != 0) {
+    return;
+  }
+  QString prnStr = QString::fromStdString(prn.toString());
+  if (!_prnOutOfRangeLogged.contains(prnStr)) {
+    _prnOutOfRangeLogged.insert(prnStr);
+    emit newMessage(QString("%1: satellite %2 outside the range supported by BNC - not usable for processing")
+                    .arg(_staID).arg(prnStr).toLatin1(), true);
+  }
+}
Index: /trunk/BNC/src/RTCM3/RTCM3Decoder.h
===================================================================
--- /trunk/BNC/src/RTCM3/RTCM3Decoder.h	(revision 11046)
+++ /trunk/BNC/src/RTCM3/RTCM3Decoder.h	(revision 11047)
@@ -60,4 +60,8 @@
 
  private:
+  // Log (once per satellite) a satellite number outside BNC's supported
+  // range (t_prn::MAXPRN_*); such satellites map to the unused index 0.
+  void checkPrnRange(const t_prn& prn);
+  QSet<QString>                         _prnOutOfRangeLogged;
   /**
    * Extract a RTCM3 message. Data is passed in the follow fields:<br>
Index: /trunk/BNC/src/RTCM3/RTCM3coDecoder.cpp
===================================================================
--- /trunk/BNC/src/RTCM3/RTCM3coDecoder.cpp	(revision 11046)
+++ /trunk/BNC/src/RTCM3/RTCM3coDecoder.cpp	(revision 11047)
@@ -105,5 +105,13 @@
 
   for (unsigned s = 0; s < CLOCKORBIT_SATNUM; s++) {
-    _antennaSentIOD[s] = UINT_MAX; // "never sent"
+    _antennaReceived[s]    = false;
+    _antennaIODKnown[s]    = false;
+    _antennaIODProvider[s] = 0;
+    _antennaIODCur[s]      = 0;
+    _antennaIODPrev[s]     = 0;
+  }
+  for (unsigned i = 0; i < CLOCKORBIT_COUNTSAT; i++) {
+    _antennaSentProviderID[i] = 0;
+    _antennaSentIOD[i]        = 0; // meaningless while _antennaSentTime[i] is invalid ("never sent")
   }
 
@@ -203,4 +211,13 @@
     memcpy(&_stateSnapshot->antenna,   &_antenna,   sizeof(_antenna));
 
+    // _antenna persists across messages, so its content alone can't tell
+    // whether a Satellite Antenna message arrived in this frame (an unchanged
+    // retransmission looks identical). Clear messageType[] as a marker: the
+    // COBOFS_SATANT decoder sets it (always non-zero) for each system it
+    // decodes. Restored with the snapshot if the frame turns out incomplete.
+    for (unsigned s = 0; s < CLOCKORBIT_SATNUM; s++) {
+      _antenna.messageType[s] = 0;
+    }
+
     int bytesused = 0;
 
@@ -240,4 +257,12 @@
 
       if (irc == GCOBR_OK || irc == GCOBR_MESSAGEFOLLOWS ) {
+        // Latch the reception until sendResults() consumes it - it may not
+        // run for this frame if _lastTime is still invalid.
+        for (unsigned s = 0; s < CLOCKORBIT_SATNUM; s++) {
+          if (_antenna.messageType[s] != 0) {
+            _antennaReceived[s] = true;
+            checkAntennaIOD(s);
+          }
+        }
         setEpochTime(); // sets _lastTime
 
@@ -342,4 +367,5 @@
       t_orbCorr orbCorr;
       orbCorr._prn.set(sys, num, flag);
+      checkPrnRange(orbCorr._prn);
       orbCorr._staID     = _staID.toStdString();
       orbCorr._iod       = _clkOrb.Sat[ii].IOD;
@@ -378,4 +404,5 @@
       t_clkCorr clkCorr;
       clkCorr._prn.set(sys, _clkOrb.Sat[ii].ID, flag);
+      checkPrnRange(clkCorr._prn);
       clkCorr._staID      = _staID.toStdString();
       clkCorr._time       = _lastTime;
@@ -470,7 +497,10 @@
     t_satCodeBias satCodeBias;
     satCodeBias._prn.set(sys, num, flag);
+    checkPrnRange(satCodeBias._prn);
     satCodeBias._staID     = _staID.toStdString();
     satCodeBias._time      = _lastTime;
     satCodeBias._updateInt = _codeBias.UpdateInterval;
+    satCodeBias._ssrIOD        = _codeBias.SSRIOD;
+    satCodeBias._ssrProviderID = _codeBias.SSRProviderID;
     for (unsigned jj = 0; jj < _codeBias.Sat[ii].NumberOfCodeBiases; jj++) {
       const SsrCorr::CodeBias::BiasSat::CodeBiasEntry& biasEntry = _codeBias.Sat[ii].Biases[jj];
@@ -560,7 +590,10 @@
     t_satPhaseBias satPhaseBias;
     satPhaseBias._prn.set(sys, num, flag);
+    checkPrnRange(satPhaseBias._prn);
     satPhaseBias._staID      = _staID.toStdString();
     satPhaseBias._time       = _lastTime;
     satPhaseBias._updateInt  = _phaseBias.UpdateInterval;
+    satPhaseBias._ssrIOD        = _phaseBias.SSRIOD;
+    satPhaseBias._ssrProviderID = _phaseBias.SSRProviderID;
     satPhaseBias._ssrFormat  = (_type == RTCMssr) ? ssrRtcmOld :
                                (_type == RTCMnewssr) ? ssrRtcmNew : ssrUnknown;
@@ -649,9 +682,26 @@
   // _antenna, like _metaData, carries no epoch field of its own and is never
   // wiped by reset(), so each system's data persists across unrelated
-  // messages. _antennaSentIOD tracks the last SatelliteAntennaIOD actually
-  // emitted per system, so unrelated messages (and unchanged retransmissions
-  // of the same antenna data) don't cause duplicate output - the same
-  // freshness problem _phaseBiasSentEpoch solves for phase biases.
+  // messages. Only a system for which an Antenna message was actually
+  // received (_antennaReceived, latched in Decode()) is considered, so
+  // persisted data is never re-stamped with a newer time.
+  //
+  // The SatelliteAntennaIOD is unique only per SSR Provider ID (DF414) and
+  // only within 64 days, and a client that has not received the stream for
+  // 64 days must discard cached antenna data (enforced downstream against
+  // t_satAntenna::_time, see ssrSatAntennaTrusted() in pppSatObs.cpp). So an
+  // unchanged (provider ID, IOD) retransmission is suppressed only if it was
+  // last emitted less than ANT_REFRESH_SEC ago. Re-emitting it after that
+  // keeps _time close to the last reception - not the first - so continuously
+  // received data never ages out. It also means a reception after a long
+  // gap is always emitted, even when the provider has legitimately reused
+  // the IOD for different content, and each .ssr file contains the antenna
+  // data at least once per refresh interval.
+  //
+  // This is tracked per satellite, not per system: providers may split a
+  // system's satellites across several messages with rotating satellite
+  // masks (DF394), all carrying the same IOD - a per-system check would
+  // emit only whichever subset happened to arrive first.
   {
+    const double ANT_REFRESH_SEC = 3600.0;
     struct SysInfo { unsigned sysIdx; char sysChar; unsigned numSat; unsigned offset; };
     const SysInfo sysInfos[] = {
@@ -664,9 +714,10 @@
     for (const SysInfo& si : sysInfos) {
       unsigned s = si.sysIdx;
-      if (_antenna.SatelliteAntennaIOD[s] == _antennaSentIOD[s]) {
-        continue; // nothing new for this system since the last emit
-      }
+      if (!_antennaReceived[s]) {
+        continue; // no Antenna message received for this system since the last check
+      }
+      _antennaReceived[s] = false;
       if (_antenna.SatelliteMask[s] == 0) {
-        continue; // this system's antenna data was never decoded
+        continue; // no satellites in this system's antenna data
       }
       for (unsigned k = 0; k < si.numSat; k++) {
@@ -674,4 +725,14 @@
           continue;
         }
+        unsigned i = si.offset + k;
+        if (_antennaSentTime[i].valid() &&
+            _antenna.SSRProviderID[s]       == _antennaSentProviderID[i] &&
+            _antenna.SatelliteAntennaIOD[s] == _antennaSentIOD[i] &&
+            _lastTime - _antennaSentTime[i] <  ANT_REFRESH_SEC) {
+          continue; // unchanged retransmission, emitted recently
+        }
+        _antennaSentProviderID[i] = _antenna.SSRProviderID[s];
+        _antennaSentIOD[i]        = _antenna.SatelliteAntennaIOD[s];
+        _antennaSentTime[i]       = _lastTime;
         int num  = k + 1; // PRN within this system, DF394 MSB-first convention
         int flag = 0;
@@ -696,5 +757,5 @@
         satAntenna._nadirAngleRangeExtension  = _antenna.NadirAngleDependentCorrectionRangeExtension[s];
 
-        const SsrCorr::Antenna::SatellitePart& satPart = _antenna.Sat[si.offset + k];
+        const SsrCorr::Antenna::SatellitePart& satPart = _antenna.Sat[i];
         for (unsigned f = 0; f < ANT_MAXFREQUENCIES; f++) {
           if (!((satPart.GnssFrequencyMask >> (ANT_MAXFREQUENCIES - 1 - f)) & 1U)) {
@@ -718,5 +779,4 @@
         _satAntennas[_lastTime].append(satAntenna);
       }
-      _antennaSentIOD[s] = _antenna.SatelliteAntennaIOD[s];
     }
   }
@@ -1109,2 +1169,65 @@
   }
 }
+
+// Satellite numbers on the wire can exceed BNC's supported range (e.g. 6-bit
+// IDs up to 63, while t_prn supports G01-G32). They are passed on unchanged,
+// but t_prn::toInt() maps them to the unused index 0, so they are not usable
+// for per-satellite processing - report that once per satellite.
+////////////////////////////////////////////////////////////////////////////
+void RTCM3coDecoder::checkPrnRange(const t_prn& prn) {
+  if (prn.toInt() != 0) {
+    return;
+  }
+  QString prnStr = QString::fromStdString(prn.toString());
+  if (!_prnOutOfRangeLogged.contains(prnStr)) {
+    _prnOutOfRangeLogged.insert(prnStr);
+    emit newMessage(QString("%1: satellite %2 outside the range supported by BNC - not usable for processing")
+                    .arg(_staID).arg(prnStr).toLatin1(), true);
+  }
+}
+
+// All Satellite Antenna messages of one GNSS must use the same Satellite
+// Antenna IOD (a satellite set may be split over several messages). During
+// a legitimate IOD change old and new IOD are both seen for a while, until
+// every message has been sent with the new one - but the IOD never returns
+// to a replaced value within 64 days (uniqueness per SSR Provider ID), so a
+// switch back to the replaced IOD within that period reveals messages of the
+// same GNSS using different IODs. Reported once per GNSS and IOD pair; the
+// data itself is not changed (PPP checks each satellite's IOD individually).
+////////////////////////////////////////////////////////////////////////////
+void RTCM3coDecoder::checkAntennaIOD(unsigned s) {
+  const double MAX_AGE_SEC = 64.0 * 86400.0;
+  unsigned int iod      = _antenna.SatelliteAntennaIOD[s];
+  unsigned int provider = _antenna.SSRProviderID[s];
+
+  int    currentWeek = 0;
+  double currentSec  = 0.0;
+  currentGPSWeeks(currentWeek, currentSec);
+  bncTime currentTime(currentWeek, currentSec);
+
+  if (!_antennaIODKnown[s] || provider != _antennaIODProvider[s]) {
+    _antennaIODKnown[s]    = true;
+    _antennaIODProvider[s] = provider;
+    _antennaIODCur[s]      = iod;
+    _antennaIODPrevTime[s].reset(); // no replaced IOD yet
+    return;
+  }
+  if (iod == _antennaIODCur[s]) {
+    return;
+  }
+  if (_antennaIODPrevTime[s].valid() && iod == _antennaIODPrev[s] &&
+      currentTime - _antennaIODPrevTime[s] < MAX_AGE_SEC) {
+    const char sysChars[CLOCKORBIT_SATNUM] = {'G', 'R', 'E', 'J', 'S', 'C'};
+    char sys = (s < CLOCKORBIT_SATNUM) ? sysChars[s] : '?';
+    QString key = QString("%1_%2_%3").arg(sys).arg(_antennaIODCur[s]).arg(iod);
+    if (!_antennaIODLogged.contains(key)) {
+      _antennaIODLogged.insert(key);
+      emit newMessage(QString("%1: GNSS %2 Satellite Antenna IOD switches back %3 -> %4"
+                              " - messages of one GNSS must use the same IOD")
+                      .arg(_staID).arg(sys).arg(_antennaIODCur[s]).arg(iod).toLatin1(), true);
+    }
+  }
+  _antennaIODPrev[s]     = _antennaIODCur[s];
+  _antennaIODPrevTime[s] = currentTime;
+  _antennaIODCur[s]      = iod;
+}
Index: /trunk/BNC/src/RTCM3/RTCM3coDecoder.h
===================================================================
--- /trunk/BNC/src/RTCM3/RTCM3coDecoder.h	(revision 11046)
+++ /trunk/BNC/src/RTCM3/RTCM3coDecoder.h	(revision 11047)
@@ -30,4 +30,5 @@
 #include <QList>
 #include <QMap>
+#include <QSet>
 #include "GPSDecoder.h"
 #include "../RTCM3/clock_and_orbit/clock_orbit_igs.h"
@@ -75,4 +76,9 @@
   void checkProviderID();
   bool corrIsOutOfRange(const SsrCorr::ClockOrbit::SatData& coSat);
+  // Log (once per satellite) a satellite number outside BNC's supported
+  // range (t_prn::MAXPRN_*); such satellites map to the unused index 0.
+  void checkPrnRange(const t_prn& prn);
+  QSet<QString>                         _prnOutOfRangeLogged;
+
 
   struct t_stateSnapshot {
@@ -111,5 +117,17 @@
   QMap<bncTime, QList<t_satAntenna> >   _satAntennas;
   unsigned int                          _phaseBiasSentEpoch[CLOCKORBIT_SATNUM];
-  unsigned int                          _antennaSentIOD[CLOCKORBIT_SATNUM];
+  bool                                  _antennaReceived[CLOCKORBIT_SATNUM];
+  unsigned int                          _antennaSentProviderID[CLOCKORBIT_COUNTSAT];
+  unsigned int                          _antennaSentIOD[CLOCKORBIT_COUNTSAT];
+  bncTime                               _antennaSentTime[CLOCKORBIT_COUNTSAT];
+  // per GNSS: Satellite Antenna IOD currently in use and the one it replaced,
+  // to detect messages of one GNSS using different IODs (see checkAntennaIOD())
+  void checkAntennaIOD(unsigned s);
+  bool                                  _antennaIODKnown[CLOCKORBIT_SATNUM];
+  unsigned int                          _antennaIODProvider[CLOCKORBIT_SATNUM];
+  unsigned int                          _antennaIODCur[CLOCKORBIT_SATNUM];
+  unsigned int                          _antennaIODPrev[CLOCKORBIT_SATNUM];
+  bncTime                               _antennaIODPrevTime[CLOCKORBIT_SATNUM]; // when _antennaIODPrev was last seen
+  QSet<QString>                         _antennaIODLogged;
 };
 
Index: /trunk/BNC/src/bncantex.cpp
===================================================================
--- /trunk/BNC/src/bncantex.cpp	(revision 11046)
+++ /trunk/BNC/src/bncantex.cpp	(revision 11047)
@@ -169,4 +169,11 @@
     else if (line.indexOf("END OF ANTENNA") == 60) {
       if (newAntMap) {
+        if (!newAntMap->blockType.isEmpty()) { // satellite antenna
+          t_satBlock satBlock;
+          satBlock.validFrom = newAntMap->validFrom;
+          satBlock.validTo   = newAntMap->validTo;
+          satBlock.blockType = newAntMap->blockType;
+          _satBlocks[newAntMap->antName].append(satBlock);
+        }
         if (_maps.contains(newAntMap->antName)) {
           delete _maps[newAntMap->antName];
@@ -213,4 +220,18 @@
         else {
           newAntMap->antName = line.mid(0,20);
+        }
+      }
+      else if (line.indexOf("VALID FROM") == 60 || line.indexOf("VALID UNTIL") == 60) {
+        QTextStream inLine(&line, QIODevice::ReadOnly);
+        int year, month, day, hour, min;
+        double sec;
+        inLine >> year >> month >> day >> hour >> min >> sec;
+        bncTime tt;
+        tt.set(year, month, day, hour, min, sec);
+        if (line.indexOf("VALID FROM") == 60) {
+          newAntMap->validFrom = tt;
+        }
+        else {
+          newAntMap->validTo = tt;
         }
       }
@@ -942,2 +963,23 @@
 
 }
+
+// Satellite block type valid at the given epoch
+////////////////////////////////////////////////////////////////////////////
+QString bncAntex::satBlockType(const QString& prn, const bncTime& epoch) const {
+  QMap<QString, QVector<t_satBlock> >::const_iterator it = _satBlocks.find(prn.mid(0,3));
+  if (it == _satBlocks.end() || it.value().isEmpty()) {
+    return QString();
+  }
+  const QVector<t_satBlock>& blocks = it.value();
+  if (!epoch.valid()) {
+    return blocks.last().blockType;
+  }
+  for (int ii = 0; ii < blocks.size(); ii++) {
+    const t_satBlock& bb = blocks[ii];
+    if ((!bb.validFrom.valid() || !(epoch < bb.validFrom)) &&
+        (!bb.validTo.valid()   || epoch < bb.validTo)) {
+      return bb.blockType;
+    }
+  }
+  return QString();
+}
Index: /trunk/BNC/src/bncantex.h
===================================================================
--- /trunk/BNC/src/bncantex.h	(revision 11046)
+++ /trunk/BNC/src/bncantex.h	(revision 11047)
@@ -46,4 +46,8 @@
   double  rcvCorr(const std::string& antName, t_frequency::type frqType,
                   double eleSat, double azSat, bool& found) const;
+  // Satellite block type (e.g. "IIF", "2I", "3M-CAST") valid for the PRN at
+  // the given epoch, taking PRN reassignments into account (VALID FROM/UNTIL);
+  // empty if unknown
+  QString satBlockType(const QString& prn, const bncTime& epoch) const;
 
   // Attitude model selection for satCoMcorrection().
@@ -194,4 +198,13 @@
 
   QMap<QString, t_antMap*> _maps;
+
+  // Block type history per satellite PRN. _maps keeps only the last ANTEX
+  // entry per PRN, which is wrong for epochs before a PRN was reassigned.
+  struct t_satBlock {
+    bncTime validFrom;
+    bncTime validTo;   // invalid: still valid
+    QString blockType;
+  };
+  QMap<QString, QVector<t_satBlock> > _satBlocks;
 };
 
Index: /trunk/BNC/src/bncephuser.cpp
===================================================================
--- /trunk/BNC/src/bncephuser.cpp	(revision 11046)
+++ /trunk/BNC/src/bncephuser.cpp	(revision 11047)
@@ -185,4 +185,22 @@
 //
 ////////////////////////////////////////////////////////////////////////////
+bool bncEphUser::radialDistanceOk(const t_eph* eph) {
+  ColumnVector xc(6);
+  ColumnVector vv(3);
+  if (eph->getCrd(eph->TOC(), xc, vv, false) != success) {
+    return false;
+  }
+  double rr = xc.Rows(1,3).NormFrobenius();
+
+  const double MINDIST = 2.e7;
+  const double MAXDIST = 6.e7;
+  if (rr < MINDIST || rr > MAXDIST || std::isnan(rr)) {
+    return false;
+  }
+  return true;
+}
+
+//
+////////////////////////////////////////////////////////////////////////////
 void bncEphUser::checkEphemeris(t_eph* eph, bool realTime) {
 
@@ -211,18 +229,11 @@
   // Simple Check - check satellite radial distance
   // ----------------------------------------------
+  if (!radialDistanceOk(eph)) {
+    eph->setCheckState(t_eph::bad);
+    return;
+  }
   ColumnVector xc(6);
   ColumnVector vv(3);
-  if (eph->getCrd(eph->TOC(), xc, vv, false) != success) {
-    eph->setCheckState(t_eph::bad);
-    return;
-  }
-  double rr = xc.Rows(1,3).NormFrobenius();
-
-  const double MINDIST = 2.e7;
-  const double MAXDIST = 6.e7;
-  if (rr < MINDIST || rr > MAXDIST || std::isnan(rr)) {
-    eph->setCheckState(t_eph::bad);
-    return;
-  }
+  eph->getCrd(eph->TOC(), xc, vv, false);
 
   // Check consistency with older ephemeris
Index: /trunk/BNC/src/bncephuser.h
===================================================================
--- /trunk/BNC/src/bncephuser.h	(revision 11046)
+++ /trunk/BNC/src/bncephuser.h	(revision 11047)
@@ -51,4 +51,8 @@
   t_irc putNewEph(t_eph* newEph, bool check);
 
+  // Plausibility check: satellite position computable at TOC and its
+  // distance from the geocenter within 20000-60000 km
+  static bool radialDistanceOk(const t_eph* eph);
+
   t_eph* ephLast(const QString& prn) {
     if (_eph.contains(prn)) {
Index: /trunk/BNC/src/bncrinex.cpp
===================================================================
--- /trunk/BNC/src/bncrinex.cpp	(revision 11046)
+++ /trunk/BNC/src/bncrinex.cpp	(revision 11047)
@@ -390,5 +390,7 @@
     QString monNum = "0";
     QString recNum = "0";
-    if (statIDlength == 9) {
+    if (statIDlength == 9 && 
+        isdigit((unsigned char) _statID[4]) &&
+        isdigit((unsigned char) _statID[5])) {
       monNum = QChar(_statID[4]);
       recNum = QChar(_statID[5]);
@@ -410,5 +412,5 @@
             "_S_" + // stream
             QString("%1").arg(datTim.date().year()) +
-            QString("%1").arg(datTim.date().dayOfYear(), 3, 10, QChar('0')) +
+            QString("%1").arg(datTim.date().dayOfYear(), 3, 10, QChar('0')) + 
             hlpStr + // HMS_period
             QString("%1").arg(_samplingRateStr) + // sampling rate
Index: /trunk/BNC/src/combination/bnccomb.cpp
===================================================================
--- /trunk/BNC/src/combination/bnccomb.cpp	(revision 11046)
+++ /trunk/BNC/src/combination/bnccomb.cpp	(revision 11047)
@@ -1296,4 +1296,6 @@
       t_satCodeBias satCodeBias(corr->_satCodeBias);
       satCodeBias._staID = "INTERNAL";
+      satCodeBias._ssrIOD        = -1; // combined, not any single provider's solution
+      satCodeBias._ssrProviderID = -1;
       satCodeBiasList.push_back(satCodeBias);
       hlp = QString().asprintf(" CodeBias %2lu", satCodeBias._bias.size());
@@ -1592,4 +1594,13 @@
       im.remove();
     }
+    // a newer ephemeris may have arrived (or an older one been re-classified)
+    // since the correction was accepted - never switch to an unusable one
+    else if (ephLast->checkState() == t_eph::bad      ||
+             ephLast->checkState() == t_eph::outdated ||
+             ephLast->checkState() == t_eph::unhealthy) {
+      out << "checkOrbit: last eph not ok (" << ephLast->checkStateToString() << ") " << corr->_prn.mid(0,3) << "\n";
+      delete corr;
+      im.remove();
+    }
     else {
       if ( corr->_eph == ephLast ||
Index: /trunk/BNC/src/pppRun.cpp
===================================================================
--- /trunk/BNC/src/pppRun.cpp	(revision 11046)
+++ /trunk/BNC/src/pppRun.cpp	(revision 11047)
@@ -575,17 +575,10 @@
   }
 
+  // Taken from the same mountpoint as the Metadata (see slotNewMetaData()),
+  // since only Metadata of the same provider can reference this antenna
+  // information (IOD unique per SSR Provider ID only).
   if (_opt->_realTime) {
-    if (_opt->_corrMount.empty() && _opt->_biasMount.empty()) {
+    if (_opt->_corrMount.empty() || _opt->_corrMount != satAntennas[0]._staID) {
       return;
-    }
-    if      (!_opt->_biasMount.empty()) {
-      if (_opt->_biasMount != satAntennas[0]._staID) {
-        return;
-      }
-    }
-    else if (!_opt->_corrMount.empty()) {
-      if (_opt->_corrMount != satAntennas[0]._staID) {
-        return;
-      }
     }
   }
Index: /trunk/BNC/src/satObs.cpp
===================================================================
--- /trunk/BNC/src/satObs.cpp	(revision 11046)
+++ /trunk/BNC/src/satObs.cpp	(revision 11047)
@@ -2,4 +2,5 @@
 #include <iomanip>
 #include <sstream>
+#include <cstdlib>
 #include <newmatio.h>
 
@@ -132,11 +133,22 @@
 
   e_ssrFormat ssrFormat = ssrUnknown;
+
+  // TEMPORARY BACKWARD COMPATIBILITY - remove later on:
+  // .ssr files written before the SSR format line (a single integer) was
+  // added after the "> ORBIT" epoch line don't have it - their first line
+  // is already the first satellite. Such a line is kept (firstLine) and
+  // processed as the first correction below, with ssrFormat unknown.
+  // To revert: read the format line unconditionally again, i.e. drop the
+  // "only one integer on the line" test and the firstLine handling in the
+  // loop below.
+  string firstLine;
+  getline(inStream, firstLine);
   {
-    string line;
-    getline(inStream, line);
-    istringstream in(line.c_str());
-    int ssrFormatInt = 0;
-    if (in >> ssrFormatInt) {
+    istringstream in(firstLine.c_str());
+    int    ssrFormatInt = 0;
+    string rest;
+    if ((in >> ssrFormatInt) && !(in >> rest)) { // format line
       ssrFormat = static_cast<e_ssrFormat>(ssrFormatInt);
+      firstLine.clear();
     }
   }
@@ -150,5 +162,10 @@
 
     string line;
-    getline(inStream, line);
+    if (ii == 0 && !firstLine.empty()) { // TEMPORARY BACKWARD COMPATIBILITY, see above
+      line = firstLine;
+    }
+    else {
+      getline(inStream, line);
+    }
     istringstream in(line.c_str());
 
@@ -245,5 +262,6 @@
       *out << "> CODE_BIAS " << epoTime.datestr(' ') << ' ' << epoTime.timestr(1, ' ') << " "
         << satCodeBias._updateInt << " "
-        << biasList.size() << ' ' << satCodeBias._staID << endl;
+        << biasList.size() << ' ' << satCodeBias._staID << ' '
+        << satCodeBias._ssrIOD << ' ' << satCodeBias._ssrProviderID << endl;
     }
     *out << satCodeBias._prn.toString() << "   " << setw(2) << satCodeBias._bias.size();
@@ -268,4 +286,6 @@
     return;
   }
+  int ssrIOD, ssrProviderID;
+  t_corrSSR::readSsrIOD(epoLine, ssrIOD, ssrProviderID);
   for (int ii = 0; ii < numSat; ii++) {
     t_satCodeBias satCodeBias;
@@ -273,4 +293,6 @@
     satCodeBias._updateInt = updateInt;
     satCodeBias._staID = staID;
+    satCodeBias._ssrIOD = ssrIOD;
+    satCodeBias._ssrProviderID = ssrProviderID;
 
     string line;
@@ -313,5 +335,6 @@
       *out << "> PHASE_BIAS " << epoTime.datestr(' ') << ' ' << epoTime.timestr(1, ' ') << " "
         << satPhaseBias._updateInt << " "
-        << biasList.size() << ' ' << satPhaseBias._staID << endl;
+        << biasList.size() << ' ' << satPhaseBias._staID << ' '
+        << satPhaseBias._ssrIOD << ' ' << satPhaseBias._ssrProviderID << endl;
 
       if (satPhaseBias._ssrFormat == e_ssrFormat::ssrRtcmNew) {
@@ -373,4 +396,6 @@
     return;
   }
+  int ssrIOD, ssrProviderID;
+  t_corrSSR::readSsrIOD(epoLine, ssrIOD, ssrProviderID);
   for (int ii = 0; ii <= numSat; ii++) {
     t_satPhaseBias satPhaseBias;
@@ -378,4 +403,6 @@
     satPhaseBias._updateInt = updateInt;
     satPhaseBias._staID = staID;
+    satPhaseBias._ssrIOD = ssrIOD;
+    satPhaseBias._ssrProviderID = ssrProviderID;
 
     string line;
@@ -392,11 +419,11 @@
     switch (ssrFormat) {
     case e_ssrFormat::ssrRtcmNew:
-      satPhaseBias._dispBiasConsistInd = metadata1;
-      satPhaseBias._MelbWuebConsistInd = metadata2;
+      satPhaseBias._satYawInfoInd = metadata1;
+      satPhaseBias._extPBPhaseInd = metadata2;
       break;
     case e_ssrFormat::ssrRtcmOld:
     case e_ssrFormat::ssrUnknown:
-      satPhaseBias._satYawInfoInd = metadata1;
-      satPhaseBias._extPBPhaseInd = metadata2;
+      satPhaseBias._dispBiasConsistInd = metadata1;
+      satPhaseBias._MelbWuebConsistInd = metadata2;
       break;
     }
@@ -575,7 +602,7 @@
       in >> frqAntenna._frqType >> frqAntenna._nadirCorrectionIndicator >> frqAntenna._nadirCorrection;
       for (unsigned kk = 0; kk < numAngles; kk++) {
-        double val;
-        in >> val;
-        frqAntenna._nadirAngleCorrection.push_back(val);
+        string valStr; // may be "nan" (invalid value), which operator>> can't parse
+        in >> valStr;
+        frqAntenna._nadirAngleCorrection.push_back(strtod(valStr.c_str(), 0));
       }
       satAntenna._freq.push_back(frqAntenna);
@@ -684,4 +711,19 @@
 
   return unknown;
+}
+
+// Optional SSR IOD and Provider ID trailing the staID in CODE_BIAS and
+// PHASE_BIAS epoch lines (older files don't have them: -1 = unknown)
+////////////////////////////////////////////////////////////////////////////
+void t_corrSSR::readSsrIOD(const string& line, int& ssrIOD, int& ssrProviderID) {
+  istringstream inLine(line.c_str());
+  string hlp;
+  for (int ii = 0; ii < 11; ii++) { // '>', type, 6 x date/time, updateInt, numEntries, staID
+    inLine >> hlp;
+  }
+  if (!(inLine >> ssrIOD >> ssrProviderID)) {
+    ssrIOD        = -1;
+    ssrProviderID = -1;
+  }
 }
 
Index: /trunk/BNC/src/satObs.h
===================================================================
--- /trunk/BNC/src/satObs.h	(revision 11046)
+++ /trunk/BNC/src/satObs.h	(revision 11047)
@@ -166,5 +166,7 @@
  public:
   t_satCodeBias() {
-    _updateInt = 0;
+    _updateInt     = 0;
+    _ssrIOD        = -1;
+    _ssrProviderID = -1;
   }
   static void writeEpoch(std::ostream* out, const QList<t_satCodeBias>& biasList);
@@ -174,4 +176,6 @@
   bncTime                    _time;
   unsigned int               _updateInt;
+  int                        _ssrIOD;        // SSR IOD (DF413), -1 if unknown; not satellite specific
+  int                        _ssrProviderID; // SSR Provider ID (DF414), -1 if unknown; not satellite specific
   std::vector<t_frqCodeBias> _bias;
 };
@@ -196,4 +200,6 @@
   t_satPhaseBias() {
     _updateInt  = 0;
+    _ssrIOD        = -1;
+    _ssrProviderID = -1;
     _ssrFormat  = ssrUnknown;
     _dispBiasConsistInd = 0;
@@ -210,4 +216,6 @@
   bncTime                     _time;
   unsigned int                _updateInt;           // not satellite specific
+  int                         _ssrIOD;              // SSR IOD (DF413), -1 if unknown; not satellite specific
+  int                         _ssrProviderID;       // SSR Provider ID (DF414), -1 if unknown; not satellite specific
   e_ssrFormat                 _ssrFormat;           // not satellite specific
   // IGS-SSR
@@ -307,5 +315,5 @@
   std::string               _staID;
   t_prn                     _prn;
-  bncTime                   _time;                       // time this message was decoded (no epoch field on the wire)
+  bncTime                   _time;                       // time this message was last received (no epoch field on the wire)
   unsigned int               _ssrProviderID;              // not satellite specific
   unsigned int               _satelliteAntennaIOD;        // not satellite specific; unique per provider within a 64-day window
@@ -323,4 +331,5 @@
   static e_type readEpoLine(const std::string& line, bncTime& epoTime,
                             unsigned int& updateInt, int& numEntries, std::string& staID);
+  static void   readSsrIOD(const std::string& line, int& ssrIOD, int& ssrProviderID);
   static t_eph::e_type getSsrNavTypeFlag(char sys, int num);
 };
Index: /trunk/BNC/src/t_prn.cpp
===================================================================
--- /trunk/BNC/src/t_prn.cpp	(revision 11046)
+++ /trunk/BNC/src/t_prn.cpp	(revision 11047)
@@ -53,4 +53,22 @@
 //////////////////////////////////////////////////////////////////////////////
 int t_prn::toInt() const {
+  // Every per-satellite array (size MAXPRN+1) and every map keyed by t_prn
+  // (compared via operator unsigned) relies on this index. A number beyond
+  // its system's range - e.g. a 6-bit SSR satellite ID up to 63 - would
+  // otherwise silently map onto a satellite of the next system (G33 = R01)
+  // or beyond the arrays, so it is mapped to the unused index 0 instead.
+  unsigned maxNum = 0;
+  switch (_system) {
+    case 'G': maxNum = MAXPRN_GPS;     break;
+    case 'R': maxNum = MAXPRN_GLONASS; break;
+    case 'E': maxNum = MAXPRN_GALILEO; break;
+    case 'J': maxNum = MAXPRN_QZSS;    break;
+    case 'S': maxNum = MAXPRN_SBAS;    break;
+    case 'C': maxNum = MAXPRN_BDS;     break;
+    case 'I': maxNum = MAXPRN_NavIC;   break;
+  }
+  if (_number < 1 || unsigned(_number) > maxNum) {
+    return 0;
+  }
   if      (_system == 'G') {
     return _number;
Index: /trunk/BNC/src/t_prn.h
===================================================================
--- /trunk/BNC/src/t_prn.h	(revision 11046)
+++ /trunk/BNC/src/t_prn.h	(revision 11047)
@@ -10,5 +10,5 @@
   static const unsigned MAXPRN_GALILEO = 36;
   static const unsigned MAXPRN_QZSS    = 10;
-  static const unsigned MAXPRN_SBAS    = 38;
+  static const unsigned MAXPRN_SBAS    = 58; // numbered S20-S58 (PRN 120-158), S01-S19 unused
   static const unsigned MAXPRN_BDS     = 65;
   static const unsigned MAXPRN_NavIC   = 20;
@@ -54,5 +54,5 @@
     return _flag;
   }
-  int toInt() const;
+  int toInt() const; // 0 (unused index) if the number is out of range for its system
   std::string toString() const;
   std::string toInternalString() const;
